
最近在 HN 上看到这样一个展示帖SHOW HN: Substantial update to UK train mapping。如果你也做过地图可视化应该能理解“更新一套国家铁路网地图”和“画一张简单路线图”完全不是一回事。英国铁路由大量干线、支线、地下铁、轻轨和货运线组成在 OpenStreetMap 里这些铁路线被拆成成千上万条短线段数据量大拓扑关系复杂稍微不注意就会出现断线、错位、样式混乱的问题。本文会围绕这类英国火车线路图项目的大版本更新整理一套完整的实现思路从 Overpass API 获取铁路数据到 GeoPandas 做投影和简化再到 Leaflet 前端渲染最后补充更新后的验证方法和性能优化经验。适合对地图可视化感兴趣、想用开源数据做交通项目的开发者也适合已经在做类似数据项目、需要一次系统升级的团队参考。1. 为什么要给英国火车线路图做一次大更新1.1 铁路数据不是静态的很多人以为铁路线路是很稳定的数据实际上铁路网络一直在变化。英国近几年就有多条新线路开通或恢复客运比如伦敦的 Elizabeth line、苏格兰的 Levenmouth Rail Link以及各城市轻轨延伸段。车站也会更名、停用或迁移。如果线路图数据来自几年前的 OSM 快照这些变化都不会反映在图上。更重要的是旧版本地图往往是一座“数据孤岛”数据是一次性手工整理的没有标准化流程更新成本很高。所以“substantial update”并不是简单地把线重新描一遍而是要建立一套能持续获取、清洗、发布铁路数据的管道。1.2 传统静态线路图的局限老式火车线路图大多是静态图片适合打印但放在网页上有很多局限无法缩放查看细节小站名看不清没有图层开关干线、地铁、轻轨混在一起无法与真实地理底图对齐用户很难判断站点之间的实际距离缺少交互比如点击线路查看名称、点击站点查看换乘信息。现代地图可视化已经可以解决这些问题数据用 GeoJSON 或矢量瓦片组织前端用 Leaflet、MapLibre GL JS 渲染用户能自由缩放、切换图层、点击查看属性。1.3 “Substantial update”到底更新了什么从项目角度理解一个“大版本更新”至少应该覆盖以下几个方面更新维度含义数据刷新基于最新 OSM/官方公开数据重新生成线路数据几何修复修复断线、重叠、自相交等拓扑问题分类完善区分干线、城区线、地铁、轻轨、废弃铁路交互升级增加站点标签、换乘信息、图层过滤、缩放响应性能优化控制数据体积提升加载和渲染速度下文就按照这些方向逐一展开实现方案。2. 数据源与准备工作2.1 常见铁路数据来源做英国铁路可视化最常用的开源数据源是 OpenStreetMap特别是在 OSM 基础上专门整理铁路信息的 OpenRailwayMap 项目。OSM 里的铁路要素包含以下关键标签railwayrail标准铁路轨道railwaysubway地下铁/地铁railwaylight_rail轻轨railwaytram有轨电车railwaydisused、railwayabandoned废弃铁路。另外英国官方有一些公共交通开放数据Network Rail 也提供了列车运行相关的数据接口但通常需要注册开发者账号并且接口主要用于实时列车运行数据而不是线路几何数据。因此如果你关注的是地图绘制和线路可视化首选还是 OSM 数据。使用 OSM 数据要注意版权问题。OSM 数据基于 ODbLOpen Database License许可发布成果时需要在页面或文档中标注数据来源这个在最后一部分会再说。2.2 环境准备本文示例以 Python Leaflet 为主环境如下操作系统Windows / macOS / Linux 均可Python 3.10 或更高版本主要依赖requests、geopandas、shapely、pandas前端依赖Leaflet 1.9.4通过 CDN 引入。建议先创建一个虚拟环境mkdir uk-train-map cd uk-train-map python -m venv venv source venv/bin/activate # Windows 使用 venv\Scripts\activate pip install requests geopandas shapely pandas2.3 项目目录结构为了方便后面维护先把项目结构规划好uk-train-map/ ├── data/ │ ├── raw/ # 原始下载数据 │ └── processed/ # 清洗后的中间数据 ├── scripts/ │ ├── download_data.py # 数据下载 │ ├── clean_data.py # 数据清洗 │ └── simplify_geojson.py # 简化与坐标转换 ├── output/ │ └── uk-rail.geojson # 最终输出给前端的数据 ├── web/ │ ├── index.html │ ├── app.js │ └── style.css └── requirements.txt数据、脚本、前端输出分离后面用自动化流程更新数据时会更清晰。3. 铁路数据的获取与预处理3.1 使用 Overpass API 拉取英国铁路数据Overpass API 是 OSM 数据的查询接口。我们可以用它按地理范围拉取铁路线并直接返回节点经纬度。英国范围较大建议通过 ISO 国家代码ISO3166-1GB来限定区域。下面是一段 Overpass QL 查询[out:json][timeout:300]; area[ISO3166-1GB]-.uk; ( way[railwayrail](area.uk); way[railwaysubway](area.uk); way[railwaylight_rail](area.uk); ); out geom;这段查询的含义[out:json]返回 JSON 格式[timeout:300]允许查询最长运行 300 秒防止超时area[ISO3166-1GB]-.uk;选取英国范围作为查询区域( ... );在范围内查询 rail、subway、light_rail 三类铁路 wayout geom;把每个 way 的节点经纬度也输出出来。使用 Python 请求这个查询并把结果保存到本地# 文件路径scripts/download_data.py import requests import json query [out:json][timeout:300]; area[ISO3166-1GB]-.uk; ( way[railwayrail](area.uk); way[railwaysubway](area.uk); way[railwaylight_rail](area.uk); ); out geom; url https://overpass-api.de/api/interpreter resp requests.get(url, params{data: query}, timeout320) resp.raise_for_status() data resp.json() with open(data/raw/uk-rail.osm.json, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse) print(f下载完成共 {len(data.get(elements, []))} 条 element)注意Overpass 公共接口有并发限制如果频繁请求会被封 IP。建议增加请求间隔或者自行部署本地 Overpass 实例。生产场景下更推荐使用 OSM 的按区域历史文件导出然后做本地过滤。3.2 将 Overpass 结果转成 GeoJSONOverpass 返回的elements中每个 way 结构大致如下{ type: way, id: 123456, tags: { railway: rail, name: South West Main Line }, geometry: [ {lat: 51.5, lon: -0.1}, {lat: 51.6, lon: -0.2} ] }我们需要把它转换成 GeoJSON FeatureCollection方便后续用 GeoPandas 读取。# 文件路径scripts/clean_data.py import json with open(data/raw/uk-rail.osm.json, r, encodingutf-8) as f: raw json.load(f) features [] for element in raw.get(elements, []): # 只处理 way 类型 if element.get(type) ! way: continue geometry element.get(geometry, []) if len(geometry) 2: continue tags element.get(tags, {}) coords [(p[lon], p[lat]) for p in geometry] features.append({ type: Feature, properties: { osm_id: element.get(id), railway: tags.get(railway, ), name: tags.get(name, ), electrified: tags.get(electrified, ), tunnel: tags.get(tunnel, ), bridge: tags.get(bridge, ) }, geometry: { type: LineString, coordinates: coords } }) geojson { type: FeatureCollection, features: features } with open(data/processed/uk-rail.geojson, w, encodingutf-8) as f: json.dump(geojson, f, ensure_asciiFalse) print(f共生成 {len(features)} 条 LineString 要素)这里保留了tunnel和bridge字段后面前端绘制时可以根据这些属性做差异化样式比如隧道部分用虚线或者弱化处理。3.3 合并线段与拓扑修复OSM 中一条完整的铁路线会被切分成很多 way原因有很多不同路段属性不同比如电气化状态变化一个 way 过长时OSM 数据模型会要求拆成多个 way编辑者在不同时间分批绘制。如果直接把所有 LineString 放进地图样式上会看到大量“断开”的小线段。这时候需要用shapely.ops.linemerge尽量把端点相连的线合并起来。# 文件路径scripts/clean_data.py 中的合并部分 from shapely.geometry import shape, mapping from shapely.ops import linemerge lines [] original_prop [] # 与 lines 一一对应保留合并前的属性参考 for feature in features: geom shape(feature[geometry]) lines.append(geom) merged_geom linemerge(lines) print(type(merged_geom))linemerge会把能按端点顺序拼接的线合并成更长的 LineString 或 MultiLineString。但有两点需要留意如果两条线虽然空间上相交但交点不是端点linemerge不会把二者合并如果属性不同合并后属性会丢失所以通常只对同一railway类型的数据做合并。另一种更稳妥的方案是用unary_union先把几何全部合并成拓扑干净的集合再提取线段。这类操作计算量较大适合离线脚本执行。3.4 大数据量下的分块处理直接查询整个英国的铁路线返回的数据量可能会很大公共 Overpass API 可能超时。常见做法是按区域拆分英格兰、苏格兰、威尔士分别查询或者按 bounding box 分块查询比如每 2 度经度 × 1 度纬度一块之后在本地把多份 GeoJSON 合并。示例按 bbox 查询的思路# 示意代码按 bbox 分段拉取数据 bboxes [ (50.0, -6.0, 52.0, 0.0), (52.0, -6.0, 54.0, 0.0), (54.0, -6.0, 56.0, 0.0), (56.0, -6.0, 59.0, 0.0), ] for i, (south, west, north, east) in enumerate(bboxes): query f [out:json][timeout:180]; way[railwayrail]({south},{west},{north},{east}); out geom; # 发送请求并保存为 uk-rail-part-{i}.osm.json pass分块处理虽然要多写一些代码但对内存占用更友好也更容易排查某个区域的数据问题。4. 坐标投影与几何简化4.1 三种常用坐标系统铁路数据的原始经纬度基于 WGS84EPSG:4326这是全球通用的地理坐标系统。但在处理几何形状时直接用经纬度计算“长度”和“距离”并不准确因为不同纬度下 1 度经度的实际距离不同。因此在数据后端处理时建议先用英国本地投影 EPSG:27700OSGB36 / British National Grid它的单位是米可以直接用“米”作为简化公差。最后输出给前端时再转回 EPSG:4326或者转成网页地图常用的 EPSG:3857Web Mercator。4.2 用 GeoPandas 完成投影与简化GeoPandas 可以很方便地读取 GeoJSON、转换投影、做几何简化。# 文件路径scripts/simplify_geojson.py import geopandas as gpd gdf gpd.read_file(data/processed/uk-rail.geojson) gdf gdf.set_crs(epsg4326) # 1. 转换到英国本地投影以米为尺度 gdf_bng gdf.to_crs(epsg27700) # 2. 使用 Douglas-Peucker 算法简化 # tolerance50 表示允许最大 50 米的偏移 gdf_simplified gdf_bng.simplify( tolerance50, preserve_topologyTrue ) # 3. 转回 WGS84方便前端直接使用 gdf_out gdf_simplified.to_crs(epsg4326) # 4. 写回 GeoJSON gdf_out.to_file(output/uk-rail-simplified.geojson, driverGeoJSON) print(f简化前要素数: {len(gdf)}) print(f简化后要素数: {len(gdf_out)})这里preserve_topologyTrue很重要它会在简化时尽量保持线段之间的拓扑关系避免两条相邻铁路线彼此穿过。4.3 简化参数的工程取舍tolerance参数不是越大越好。50 米通常在“全国概览 城市缩放”场景下表现不错文件体积明显减小肉眼几乎察觉不到偏差。但如果你需要放大到站台级别50 米带来的偏移就很明显了。一个更精细的做法是把全国铁路按 zoom 级别拆成多套数据低缩放级别使用 100 米简化中缩放级别使用 20 米简化高缩放级别使用原始数据。前端根据当前缩放级别动态加载对应精度的数据。这种方式实现复杂度更高但数据加载体验明显更好。5. 前端渲染英国铁路图层5.1 使用 Leaflet 加载 GeoJSON当数据整理成 GeoJSON 后前端就能直接渲染了。这里推荐 Leaflet因为它轻量、上手快对 GeoJSON 支持很好。web/index.html!DOCTYPE html html langzh-CN head meta charsetUTF-8 / meta nameviewport contentwidthdevice-width, initial-scale1.0 / titleUK Train Map/title link relstylesheet hrefhttps://unpkg.com/leaflet1.9.4/dist/leaflet.css / script srchttps://unpkg.com/leaflet1.9.4/dist/leaflet.js/script style html, body { margin: 0; padding: 0; width: 100%; height: 100%; } #map { width: 100%; height: 100%; background-color: #e8e8e8; } /style /head body div idmap/div script srcapp.js/script /body /htmlweb/app.js// 文件路径web/app.js const map L.map(map, { center: [54.0, -2.0], zoom: 6, preferCanvas: true // 数据量大时优先使用 Canvas 渲染 }); L.tileLayer(https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png, { maxZoom: 18, attribution: © a hrefhttps://www.openstreetmap.org/copyrightOpenStreetMap/a contributors }).addTo(map); fetch(../output/uk-rail-simplified.geojson) .then((res) { return res.json(); }) .then((data) { const railLayer L.geoJSON(data, { style: function (feature) { const type feature.properties.railway; if (type rail) { return { color: #666666, weight: 1.2 }; } else if (type subway) { return { color: #e53935, weight: 1 }; } else if (type light_rail) { return { color: #fb8c00, weight: 1 }; } return { color: #999999, weight: 0.8 }; } }); railLayer.addTo(map); console.log(铁路图层加载完成要素数量, data.features.length); }) .catch((err) { console.error(加载铁路数据失败, err); });这段代码做了几件事初始化地图中心点放在英国中部加载 OpenStreetMap 标准底图从本地 GeoJSON 文件读取铁路数据按railway字段设置不通颜色将图层添加到地图上。打开浏览器后预测能看到全英国范围内的铁路线网络干线为灰色地铁为红色轻轨为橙色。5.2 为什么优先开启 Canvas 渲染Leaflet 默认对矢量图形使用 SVG 渲染。SVG 优点是清晰、支持 CSS 样式但当要素数量超过几万条时DOM 数量过多会导致页面卡顿。此时开启preferCanvas: trueLeaflet 会改用 Canvas 绘制矢量图形显著减少浏览器开销。对于英国全国铁路数据要素数量通常有几万条强烈建议开启 Canvas。5.3 按缩放级别调整样式全国范围看铁路线不需要很粗放大到城市范围时粗糙的 1px 线会显得不够清楚。可以通过监听zoomend事件动态调整样式// 文件路径web/app.js 中的扩展代码 map.on(zoomend, function () { const zoom map.getZoom(); if (railLayer) { if (zoom 12) { railLayer.setStyle({ weight: 2.5 }); } else if (zoom 9) { railLayer.setStyle({ weight: 1.8 }); } else { railLayer.setStyle({ weight: 1 }); } } });注意这里假设railLayer已经通过let或全局变量声明实际项目中需要调整变量作用域。5.4 叠加站点与弹窗信息要画站点需要额外的 Point 数据。可以继续用 Overpass API 拉取railwaystation的节点同样转成 GeoJSON。简化后的查询思路[out:json][timeout:180]; area[ISO3166-1GB]-.uk; ( node[railwaystation](area.uk); node[railwaytram_stop](area.uk); ); out;前端可以把站点图层叠加到铁路图层上并给站点加点击弹窗// 文件路径web/app.js 中的站点图层示例 fetch(../output/uk-stations.geojson) .then((res) res.json()) .then((stationData) { const stationLayer L.geoJSON(stationData, { pointToLayer: function (feature, latlng) { return L.circleMarker(latlng, { radius: 3, color: #222222, fillColor: #ffd54f, fillOpacity: 0.9, weight: 1 }); }, onEachFeature: function (feature, layer) { const name feature.properties.name || 未知站台; layer.bindPopup(strong name /strong); } }); stationLayer.addTo(map); });站点点位很多时建议只在缩放级别大于某个阈值时显示站点避免全国范围瞬间加载几万个圆点。6. 本次更新的核心方向6.1 数据版本刷新大版本更新最重要的就是数据刷新。建议建立固定的数据更新节奏例如每月从 OSM 提取最新铁路线数据经过清洗后发布。这样新开通线路能够及时上线。如果使用 Overpass API建议记录抓取时间import datetime with open(data/processed/uk-rail.geojson, w, encodingutf-8) as f: json.dump(geojson, f, ensure_asciiFalse) print(f数据生成时间: {datetime.datetime.now()})6.2 几何拓扑修正旧版地图最常见的拓扑问题交叉口没有公共节点两条铁路线视觉上相交但实际没有连通线路端点悬空看起来像是断头路线段重叠导致渲染时颜色叠黑。这些问题可以通过检查线端点是否匹配、面线相交关系来发现。在 Shapely 中可以使用intersects、touches、within等方法做批量判断。6.3 隧道、桥梁与电气化分类更新时可以顺便把细节属性加进来。比如 OSM 中隧道用tunnelyes标记桥梁用bridgeyes标记。前端渲染时可以将隧道路段设置为浅灰色虚线桥梁保持深色实线这样地图更易读。style: function (feature) { const props feature.properties; const isTunnel props.tunnel yes; const isBridge props.bridge yes; if (isTunnel) { return { color: #bbbbbb, weight: 1, opacity: 0.6 }; } if (isBridge) { return { color: #222222, weight: 1.4 }; } return { color: #666666, weight: 1.2 }; }6.4 渲染性能优化一次合格的更新必须在性能上有明显提升。常见手段包括简化几何控制文件体积使用preferCanvas按缩放级别加载不同精度的数据站点数据和线路数据拆分避免重复渲染如果数据继续增大考虑转为矢量瓦片MapLibre GL JS PMTiles。如果更新后加载速度还是慢建议先从数据体积排查再检查前端是否单次加载了太多要素。7. 更新效果的验证方法7.1 基础坐标检查在地图上标注几个已知坐标比如伦敦国王十字车站、曼彻斯特皮卡迪利车站查看铁路线是否与站点重合。如果站点显示在离线很远的地方通常是坐标转换出了问题。7.2 与标准底图对照用 OpenStreetMap 标准底图叠加观察检查铁路线是否穿越不应该穿越的区域。如果看到铁路线大面积偏离道路和地形优先检查 CRS。7.3 性能指标观测建议按以下标准做更新前后的对比检查项方法建议标准文件大小查看 GeoJSON 体积简化后尽量小于 8MB首屏加载时间浏览器 Network 面板3 秒内完成加载地图交互流畅度缩放 / 平移体验不掉帧、不卡顿要素数量data.features.length记录以便对比拓扑正确性抽查主要换乘站线路连续无断裂性能优化不是一味追求“变快”而是在可接受的显示精度下减少无效数据。8. 常见问题与排查思路8.1 Overpass API 查询英国全境数据时超时问题现象常见原因解决思路请求超时或返回空数据查询范围过大公共 API 负载高缩小范围分区域请求延长 timeout自建 Overpass 实例英国铁路数据量很大尤其是 rail subway light_rail 全部查询时返回的数据可能几十 MB。建议按英格兰、苏格兰、威尔士分开查询最后再合并。8.2 简化后的线路在车站位置明显偏移问题现象常见原因解决思路线路与站点距离过远tolerance 过大或 CRS 转换错误降低 tolerance检查是否统一到同一坐标系记住一个原则数据处理的每个阶段都要明确当前坐标系。不要把 EPSG:27700 的数据再当作 EPSG:4326 读取。8.3 浏览器加载地图后卡死问题现象常见原因解决思路页面无响应或 GPU 占用过高要素数量过多SVG 渲染压力大开启 preferCanvas简化数据降采样如果数据确实很大可以考虑把 GeoJSON 转成 TopoJSON它能通过拓扑编码节省大量重复坐标体积通常可以再压缩 30% 到 50%。8.4 铁路线看起来断成一截一截问题现象常见原因解决思路同一条线路被拆成多段颜色断断续续OSM 中 way 被人为拆分或属性标签不同用 linemerge 合并或在渲染时统一同一条线路的样式因为 OSM 数据天然是碎片化的所以发布前的拓扑合并步骤不能省略。9. 工程化最佳实践与后续扩展9.1 数据更新流水线化一次“substantial update”不应该只是手工脚本跑一遍最好做成可重复执行的流水线。典型的流程是定时触发下载脚本运行清洗和简化脚本自动生成 GeoJSON/TopoJSON更新前端数据目录构建并部署到静态站点或 CDN。可以用 GitHub Actions 定时触发也可以直接用 Makefile 管理# Makefile download: python scripts/download_data.py clean: python scripts/clean_data.py simplify: python scripts/simplify_geojson.py build: download clean simplify serve: cd web python -m http.server 8000每次更新数据时只执行make build就可以完成全流程。9.2 版权与数据标注OSM 数据使用 ODbL 许可。如果你基于 OSM 数据生成了英国铁路地图页面底部需要标注 OpenStreetMap 贡献者并且说明数据遵循 ODbL。如果只是内部测试可以暂时不发布但正式上线时一定要加上声明。9.3 拓展方向完成了英国铁路线路图的更新之后后续可以继续探索接入实时列车位置数据在地图上显示列车运行状态按线路聚合支持点击线路查看沿途站点迁移到 MapLibre GL JS 矢量瓦片支持更平滑的大数据渲染增加线路规划算法把地图从“展示型”升级为“分析型”工具。最后建议先从小范围开始验证。不要一开始就尝试全英国一次性渲染先选择一个区域比如伦敦周边或曼彻斯特周边把数据清洗、投影简化、前端渲染整条链路跑通再扩大到全国。这样排查问题更高效后续更新也更有把握。如果你也在做类似的地图可视化项目欢迎先动手跑一遍文中的示例流程再结合自己的数据源做调整和优化。