1. 介绍
我们做城市治理大屏、园区数字孪生和应急指挥项目时,经常把高德地图 JSAPI 当作默认选择。它在国内的地图数据、中文 POI、地址搜索和导航上积累很深,前端也能很快叠加点位、轨迹和模型。
但某些项目会把条件推得更严格了一些,客户希望地图能运行在内网甚至断网环境,还要有 GL 三维效果。底图上需要继续叠加建筑、动态模型、交通流、边界墙和自定义 POI。地图系统从一个在线 SDK 的调用问题,变成了数据、渲染和服务如何留在自己环境里的问题。
本文以这份方案和石河子工程的落地结果为基础,讲清楚哪些能力可以自己搭建地图服务。
示例项目的三维地图全景完全脱离了高德地图的支持。画面包含建筑、道路、文字标注、边界和业务动态效果。
2. 需求
最先遇到的是离线问题。浏览器里的地图组件、底图瓦片、文字标注、POI 搜索和路径规划,如果每一项都要访问公网,项目就无法在隔离网络中使用。
我们当然可以使用爬取地图瓦片资源和缓存 JS 执行模块的方式,来解决在线地图引擎的离线问题,但这种做法有较大的违规风险,除非跟供应商签署过授权合同,否则在商业中并不可取。
这类方案即使暂时能跑起来,后面也很难解释清楚数据来源、更新方式和交付边界。客户要的是一套可长期维护的内网地图,工程里就应当保存原始数据、构建脚本、样式配置和服务代码。
第二个问题是三维效果。普通图片瓦片能提供一张底图,却无法像矢量地图一样按缩放级别控制道路、用地、文字和建筑,也不方便让业务模型进入同一套 WebGL 场景。项目里的建筑、灯杆、交通流和边界墙都需要继续存在,地图引擎必须能接受自定义图层。
最后是导航和地理编码。地图上能画出路,不代表它就能算路线。用户输入一个地点,也不等于系统已经具备地址搜索和逆地理编码。底图、路径规划和地理编码需要分开建设,不能把它们都当成一张地图的附属功能。
3. 方案
新方案以 OSM 原始数据和 MapLibre GL JS 为基础,后端按需要加入路径规划和地理编码服务。
3.1 数据来源
从 geoFabrik 可以下载到全国各省地图数据数据以OSM PBF 包的形式,包含以下信息
OSM PBF
├── 矢量瓦片与样式 → MapLibre GL JS
├── 路网数据 → OSRM 或 Valhalla
└── 地址与 POI 数据 → Nominatim 或业务搜索索引
3.1.1 矢量瓦片与样式
底图数据从 OSM PBF 生成本地 MBTiles 或 PMTiles,再由本地 HTTP 服务按标准矢量瓦片接口提供给浏览器。道路、建筑、水系、用地、行政边界、POI 和名称都在数据层中分开保存,样式由 MapLibre Style JSON 控制。
这带来一个很直接的变化。底色、道路、绿地、建筑高度和文字层级都在项目代码里,不再受在线主题的限制。每个项目的底图可任意使用或定制各种颜色方案,方案可置于项目工程中与项目深度绑定,不用与其他项目共享,不受地图供应商限制,不需要另外付费。
3.1.2 路网数据
指 PBF 里带通行语义的道路要素。OSM 用 node 记录坐标,用 way 把一串 node 按顺序连成道路线,highway 标签区分高速、主干道、支路和步行道等等级,oneway、maxspeed、access 描述单行向、限速和通行权限,转向限制挂在 relation 上,红绿灯是带 highway=traffic_signals 的 node。
这些标签合起来是一张有方向、有连接、有规则的图,而不只是画在底图上的线。OSRM 和 Valhalla 直接读取 osm.pbf,把道路整理成这样的图结构再做最短路径计算,所以导航依赖的不是瓦片,而是完整的道路拓扑。
3.1.3 地址与 POI 数据
地址与 POI 在 OSM 里都是挂在要素上的标签,没有独立的地址表。门牌、街道、城市写在 addr:housenumber、addr:street 这类地址标签里;餐厅、医院、商店、酒店由 amenity、shop、tourism 等类别标签加 name 构成,一个带名称的 node 或建筑轮廓就是一个 POI。
OSM 在国内的覆盖并不均匀,主干道沿街 POI 相对齐全,小区内部、门牌和长尾店铺缺失较多。
3.2 矢量地图与 GL 三维效果
MapLibre GL JS 是浏览器端的 WebGL 矢量地图引擎。它能加载本地矢量瓦片和 GeoJSON,也支持建筑拉伸、倾斜视角、地形与自定义渲染图层。
项目里的普通建筑由 fill-extrusion 渲染,动态建筑、交通流、水面、灯杆和边界墙继续由 Three.js 图层实现。工程只需要处理 MapLibre 的坐标与生命周期适配,现有的三维业务效果可以继续复用。
map.addSource('business-buildings', {
type: 'geojson',
data: '/buildings_wgs84.geojson'
})
map.addLayer({
id: 'business-buildings-3d',
type: 'fill-extrusion',
source: 'business-buildings',
paint: {
'fill-extrusion-color': '#4a6d88',
'fill-extrusion-height': ['coalesce', ['get', 'height'], 18]
}
})
历史项目里常见 GCJ-02 数据,这次为了让已有建筑和新底图对齐,在导入时转换成 WGS84。新项目建议从数据采集、业务库到地图服务全部使用 WGS84。它和 OSM、GPS、GeoJSON、MapLibre 的兼容性更好,也能避免后续每接一层数据都做坐标转换。
3.3 路径规划
导航路网从原始 osm.pbf 数据构建。它保留道路的 node、way、单行方向和相关标签,能够给路径服务提供正确的道路拓扑。
示例工程目前提供一个本地导航演示,支持驾车、骑行、步行、多条候选路线和可拖动的途径点。这个页面用于验证交互与离线数据流程。生产环境建议使用 OSRM 或 Valhalla 处理原始 PBF,再按车型、道路等级、速度、限行和转向规则配置路由策略。
3.4 地理编码
地址搜索可以从最轻的一档起步。石河子示例工程在底图构建时顺手导出一份名称索引,把具名 POI、地名、建筑/区域名称和去重道路名收进一个 JSON 文件,服务端在内存里完成关键词过滤和最近点计算。几百条名称的规模连 SQLite 都不需要,就能满足固定区域的名称检索和地图定位。
数据量上来后,第二步再对业务 POI、道路名、小区名和园区设施建立 SQLite FTS 索引。第三步在确实需要完整 OSM 地址层级时部署 Nominatim。Nominatim 依赖 PostgreSQL、PostGIS 和 osm2pgsql,资源需求更高。无论使用哪种方案,国内门牌、楼栋、园区入口和长尾 POI 的质量最终仍然取决于项目自己补充的数据。
3.5 新方案还能得到什么
离线只是这套方案的起点。它还有几个实际优势。
- 数据和样式都在项目内,可以在客户内网部署,也可以按项目节奏更新。
- 图层顺序完全可控。道路在地面层,建筑位于其上,文字最后绘制,空间关系更自然。
- 底图、业务建筑、模型和实时数据使用同一个 WebGL 场景,视觉风格可以统一。
- 每个能力都可独立替换。底图服务、搜索服务和导航服务不会绑死在同一个供应商接口上。
- 对gl-layers做了适配调整,可以在这套方案上直接使用,不需要重新造轮子。
3.6 所需资源
| 使用范围 | 建议资源 | 磁盘 | 用途 |
|---|---|---|---|
| 本地开发与单人演示 | 2 核 4GB | 5GB SSD | 瓦片与路网构建、MapLibre、Node 服务,全工程含依赖约 500MB |
| 内网演示环境 | 2 核 4GB | 5GB | 预构建数据静态服务,数十人并发 |
| Nominatim 与 PostGIS | 4 核 8GB | 10GB NVMe | 城市级地址导入、搜索和逆地理编码 |
若后续将 Nominatim 的导入范围扩大到全省或全国,内存和磁盘需求会上升一个数量级,16GB 内存、100GB 磁盘起。
项目还需要准备自有建筑、业务 POI、道路别名、标准地址、园区内部道路、停车场入口、门牌、楼栋和通行规则。地图数据覆盖不到的地方,最终都会变成搜索与导航体验的问题。
4. 具体实施
4.1 案例的技术栈
以城市级别为示例,它以新疆 OSM 原始数据为输入,只截取整个石河子市行政区用于展示和导航。
| 模块 | 当前实现 | 作用 |
|---|---|---|
| 原始数据 | Geofabrik 新疆 osm.pbf | 道路、建筑、水系、用地、POI 和路网来源 |
| 地图引擎 | MapLibre GL JS | 本地矢量底图、样式、建筑拉伸和交互 |
| 三维业务图层 | Three.js 图层库 | 动态建筑、交通流、水面、灯杆和边界墙 |
| 本地服务 | Node.js | 瓦片、样式、前端页面和演示导航接口 |
| 前端页面 | Vue 3 与 Vite | 图层控制、地图交互、路线选择与途径点操作 |
| 生产导航候选 | OSRM 或 Valhalla | 原始 PBF 的路由构建与导航服务 |
| 地理编码 | 本地名称索引与 /geocode 接口 | 名称搜索与点图反查;生产候选 SQLite FTS 或 Nominatim |
新疆原始文件约 46MB。构建后的石河子底图约 13MB,地理编码名称索引包含 451 条名称,当前演示路网包含 16092 个节点、18074 条边和 2651 条道路。这个规模用于城市级演示很轻,真正影响后续效果的是 POI、门牌、内部道路和通行规则是否完整。
4.2 构建过程
先下载或准备区域 OSM PBF,再从 PBF 中解析石河子行政区关系,生成 WGS84 AOI 边界。后面的瓦片构建、POI 提取和路网构建都使用这个范围。
pnpm install
pnpm run prepare:aoi
pnpm run build
pnpm run build:routing-demo
pnpm run build:web
pnpm run serve
pnpm run build 负责生成底图 MBTiles 和地理编码名称索引。pnpm run build:routing-demo 从原始 osm.pbf 生成演示路网。pnpm run serve 启动本地服务,默认端口为 4174。开发时也可以执行 pnpm run dev,由 Vite 提供前端热更新。
4.3 示例页面一 三维底图
首页展示石河子三维底图和业务效果,左侧图层控制面板可以单独显示或隐藏道路、文字标识、POI、建筑、水系、绿地与用地、行政边界,以及交通流、动态建筑、水面、灯杆和边界墙。
这个页面主要验证两件事。
第一,本地矢量底图在没有外部地图服务时能否正常显示。
第二,原有的 GL 三维业务图层能否继续叠加到新的地图引擎上。
三维底图页面截图。左侧保留图层控制面板,地图显示三维建筑、道路、文字标识与动态业务效果。
4.4 示例页面二 路径规划
用户可以在地图上设置起点、终点和最多五个途径点,切换驾车、骑行和步行策略。页面返回多条路线,当前选择的路线高亮,其余路线降低透明度。途径点可以拖动,拖动结束后自动重新规划。
页面还提供路网图层开关,用于查看当前导航数据覆盖的道路。这个功能在演示阶段很实用,路线异常时能先判断是起终点没有吸附到道路,还是原始道路数据本身缺少连接。
路径规划页面截图。画面包含多条候选路线、选中路线、起终点、可拖动途径点、出行方式和路网图层控制。
4.5 示例页面三 地理编码
地理编码页面位于 /geocode-demo,演示的是 3.4 节里最轻的一档方案。底图构建时把具名 POI、地名、建筑/区域名称和去重道路名导出为一份名称索引,服务端在内存中完成检索,不引入任何搜索中间件。
页面提供两种模式。名称搜索按全等、前缀、包含的顺序返回匹配列表,点击结果定位到地图并弹出名称标签;点图反查在地图上点击任意位置,返回半径内最近的名称点、类别和距离。这两步正好对应正向与逆向地理编码。
这个页面用于验证离线地理编码的数据流程。它本质上是一份扁平名称列表,没有分词、拼音和地址层级,需要更强检索能力时按 3.4 节的路径演进到 SQLite FTS 或 Nominatim。
地理编码页面截图。画面包含名称搜索结果列表、选中结果的定位标签,以及点图反查的最近名称点与距离。
5. 总结
这套方案适合范围固定、强调私有化部署、需要三维视觉定制的项目,例如园区、港口、矿区、城市治理大屏、数字孪生、应急指挥和涉密内网系统。项目如果已经掌握建筑、设备、管线和业务 POI 数据,OSM 底图可以提供稳定的空间参照,业务数据则负责把地图变成项目自己的地图。
高德依然有很难直接替代的优势。国内 POI、门牌、道路名称和地址解析覆盖更完整,实时路况、封路、施工、交通事件和预计到达时间也需要持续采集。驾车导航还涉及转向限制、限行、收费、复杂路口和不同出行方式的长期规则维护。高德的搜索、定位、导航和移动端 SDK 已经是成熟产品,自建系统需要逐项投入。
因此,选择方案时先看项目边界。固定区域、内网运行、可接受自主维护数据的项目,OSM 加 MapLibre 是一条可行路线。全国范围公众服务、强实时导航、海量地址搜索和持续更新 POI 的项目,继续使用商业地图服务更现实。也可以让底图和核心业务数据在内网运行,把必须依赖实时数据的功能留给合规的在线服务。
案例工程说明离线地图的门槛并不在于把一张底图放到服务器上。数据源、矢量瓦片、三维图层、路网、搜索和服务地址要各自处理清楚。把这些部分拆开以后,一个城市范围的地图系统可以从容地跑在普通服务器上。