摘要:野外、井下、涉密内网——没网的时候地图怎么显示?三条可落地路线(矢量现算 / 影像贴图 / 瓦片归档),它们的本质差异只有一句话:"矢量 → 图片"的换算时机不同:现算 / 现贴 / 现取。这篇给出每条路线的 MapLibre 接入原语、三路线对比表、以及一个真实教训——四角顺序错一位,影像就翻过来了。
目录(TOC)
- 一、现场:巷道里一格信号都没有
- 二、路线一:矢量渲染(现算)
- 三、路线二:影像贴图(现贴)
- 四、路线三:瓦片归档(现取)
- 五、三路线对比
- 六、统一切换与清理
- 七、与 osmdroid 的异同
- 八、选型建议
- 九、坑对照表与小结
一、现场:巷道里一格信号都没有
想象这个现场:你在矿区井下做巡检,手机开着地图 App,但巷道里一格信号都没有。业务数据(监测点、边界、设备位置)还是要叠在地图上,还要能点、能查、能缩放。
你总不能跟领导说"没网所以看不了"。
野外、井下、涉密内网、国际漫游等场景,设备往往拿不到在线瓦片服务。但业务仍需在地图上叠加自有数据(监测点、矢量边界、影像底图)。
移动端地图 SDK 的离线能力各有取舍,本文把三种可落地路线归纳成一句话差异:
差异只在"矢量 → 图片"的换算时机:现算 / 现贴 / 现取。
flowchart LR
DATA["离线数据"] --> B{"数据形态"}
B -->|"矢量几何"| C["① 矢量现算<br/>GPU 实时算图元"]
B -->|"单张影像"| D["② 影像贴图<br/>整图仿射铺"]
B -->|"预切瓦片"| E["③ 瓦片归档<br/>按需取现成 PNG"]
style C fill:#e6f7ff,stroke:#1890ff
style D fill:#fff7e6,stroke:#fa8c16
style E fill:#f6ffed,stroke:#52c41a
二、路线一:矢量渲染(现算)
思路:不依赖任何底图图片。直接用程序或离线文件生成矢量几何(点 / 线 / 面),离线时由地图引擎实时"算"成图元渲染出来。
- 数据源:SHP / DXF / 自有点库,或 DEMO 里程序化生成的
surveyFeatures()(一批统一几何对象); - 渲染:解析出统一几何对象后,交给统一的图层构造器,按点 / 线 / 面分发到对应的圆点 / 线 / 填充图层(GPU 实时绘制);
- 优点:体积小、可交互(点击查询)、缩放无损;
- 缺点:没有"影像底图"的直观背景,只有线框 / 色块。
DEMO 按钮"矢量渲染"即:清除在线底图叠加 → 解析统一几何 → 由统一构造器分发渲染,Toast 提示"离线现算"。
这一条路线在测绘场景里被严重低估——很多时候你根本不需要实景底图,业务数据本身就是"图"。它体积最小、交互最强,是"叠加自有数据"场景的默认选择。
三、路线二:影像贴图(现贴)
思路:预先有一张带地理范围的影像位图(卫星图、航拍图、扫描图),把它按四个角点的经纬度"贴"到地图上,充当离线底图。
核心是 ImageSource + 地理四角 LatLngQuad:
val quad = LatLngQuad(
LatLng(topLeftLat, topLeftLon), // 左上
LatLng(topLeftLat, bottomRightLon), // 右上
LatLng(bottomRightLat, bottomRightLon), // 右下
LatLng(bottomRightLat, topLeftLon) // 左下
)
val imageSource = ImageSource("offline-img-src", quad, bitmap)
val rasterLayer = RasterLayer("offline-img-layer", "offline-img-src")
关键点
- 四角顺序必须正确(左上 → 右上 → 右下 → 左下),否则影像会被扭曲 / 翻转。DEMO 用
imageCorners()给出一对对角点并严格按序组装; - 影像是一整张位图,引擎按四角做仿射拉伸铺满——所以它本质是"一张贴图",缩放时会糊(不像瓦片那样分级清晰)。适合小范围、中等缩放;
- DEMO 用 Canvas 程序化画网格 + 色块模拟遥感影像,免去外部素材,验证"读角点 + 整图铺地理范围"的完整链路。
四角顺序为什么这么容易错?
因为它有四种"看起来都对"的排法,而错了不报错,只是图翻转或扭曲:
| 排法 | 结果 |
|---|---|
| 左上 → 右上 → 右下 → 左下 ✅ | 正确 |
| 左上 → 左下 → 右下 → 右上 | 上下翻转 |
| 左上 → 右上 → 左下 → 右下 | 扭曲成蝴蝶结 |
| 顺时针 vs 逆时针搞反 | 镜像翻转 |
防御办法:先用一张"明显不对称"的测试图(比如带文字的图)验证四角顺序,确认方向对了再换真实影像。用一张对称的网格图去测,翻转了也看不出来。
四、路线三:瓦片归档(现取)
思路:把"预切好的 PNG 瓦片"按 z/x/y.png 目录结构打成一个 zip 归档,离线时引擎按需从归档里"取"对应瓦片。
DEMO 程序化生成归档:
fun generateTileArchive(zipFile, levels, tilesPerSide): Boolean {
ZipOutputStream(zipFile.outputStream()).use { zos ->
for (z in 0 until levels)
for (x in 0 until tilesPerSide)
for (y in 0 until tilesPerSide) {
val tile = generateTileBitmap(z, x, y) // 按 z/x/y 给不同颜色
zos.putNextEntry(ZipEntry("$z/$x/$y.png"))
tile.compress(PNG, 100, zos)
zos.closeEntry()
}
}
return true
}
要点:
- 目录结构即瓦片坐标:
3 级 × 4×4 格生成 48 张 PNG 入 zip,模拟"预切好的瓦片包"; - 真工程里这个 zip 由桌面端瓦片下载器预先产出,App 内置或随包下发;引擎支持
MbTiles/GeoPackage/ 自定义归档源时直接指向该文件; - 优点:缩放分级清晰、与在线瓦片体验一致;
- 缺点:归档体积随级别指数膨胀(每升一级瓦片数 ×4)。
体积是这条路线唯一的硬约束
每升一级,瓦片数量翻 4 倍。做个粗略估算:
| 级别范围 | 覆盖一个中等城市所需瓦片数(量级) |
|---|---|
| 10-14 级 | 数千张 |
| 10-16 级 | 数万张 |
| 10-18 级 | 数十万张 |
所以"控制级别数"不是优化建议,是可行性前提。 生产上通常做到 16 级左右就停,再深的细节交给矢量层(业务数据)表达——这正是"矢量 + 瓦片归档"组合的意义。
五、三路线对比
| 维度 | 矢量现算 | 影像贴图 | 瓦片归档 |
|---|---|---|---|
| 换算时机 | 实时算图元 | 整图仿射贴 | 按需取瓦片 |
| 离线底图观感 | 无线框背景 | 一张图,缩放会糊 | 分级清晰 |
| 体积 | 极小 | 中(取决于图大小) | 大(指数增长) |
| 交互 / 查询 | 强 | 弱 | 弱 |
| 缩放无损 | ✅ 是 | ❌ 会糊 | ✅ 是(到归档上限) |
| 适用 | 叠加自有数据 | 小范围实景背景 | 大范围分级浏览 |
六、统一切换与清理
无论哪条路线,叠加层都要能被干净清除,以便"恢复在线":
fun clearData() {
dataLayers.forEach { style.removeLayer(it.id) }
dataSources.forEach { style.removeSource(it.id) }
dataLayers.clear(); dataSources.clear()
}
DEMO 的"恢复在线"按钮即清掉所有离线叠加、把在线底图重新显示。
这里有个容易漏的点:只 removeLayer 不 removeSource,源会残留在 Style 里累积。离线 / 在线来回切几次,Style 里就堆了一堆孤立 Source——内存涨、还可能 ID 冲突。所以两个都要清。
七、与 osmdroid 的异同
通用原理(一致)
三种离线路线(矢量现算 / 影像贴图 / 瓦片归档)是地图离线化的通用范式,与引擎无关——"矢量 → 图片的换算时机不同"这个分类在任一 SDK 都成立。
MapLibre 专属实现差异
| 路线 | osmdroid 篇(03) | 本文(MapLibre) |
|---|---|---|
| 矢量现算 | 通过 FolderOverlay / 自定义 Overlay 画几何 | 统一几何 → GeoJsonSource + Symbol/Line/Fill 声明式图层(GPU 实时算) |
| 影像贴图 | GroundOverlay / GeoPoint 四角定位 | ImageSource + LatLngQuad 四角地理范围,仿射铺图 |
| 瓦片归档 | 内置 MBTileProvider / ZipTileProvider 直接指文件 | 自定义归档源(或 MbTiles / GeoPackage),结构同为 z/x/y |
一句话:
路线选择是通用的,接入原语不同。 MapLibre 把影像贴图收敛为"带地理四角的
ImageSource"、把矢量收敛为"统一几何 → 源 + 图层",比 osmdroid 的 Overlay 体系更声明式——离线与在线走同一条"源 + 图层"管线,切换只需换源。
这个"同一条管线"的收益是实打实的:osmdroid 下离线和在线是两套机制(Overlay vs TileSource),切换要分别处理;MapLibre 下都一样,换 Source 即可。
八、选型建议
- 只是叠加业务数据且不需要实景背景 → 矢量现算(最轻、可交互);
- 需要小范围实景底图、缩放不深 → 影像贴图(实现最简单);
- 需要大面积、可缩放浏览的离线底图 → 瓦片归档(但要控制级别数,避免包体爆炸);
- 生产环境常是"矢量 + 瓦片归档"组合:瓦片当背景,矢量当业务层。
九、坑对照表与小结
| # | 症状 | 根因 | 解法 |
|---|---|---|---|
| 1 | 影像翻转 / 扭曲成蝴蝶结 | LatLngQuad 四角顺序错 | 严格按"左上→右上→右下→左下",用不对称测试图验证 |
| 2 | 放大后影像糊成马赛克 | 贴图是整图仿射拉伸,无分级 | 小范围用;大范围改瓦片归档 |
| 3 | 瓦片包体积爆炸 | 每升一级瓦片数 ×4 | 控制级别上限(通常到 16 级),细节交给矢量层 |
| 4 | 切换离线/在线后内存涨 | 只 removeLayer 未 removeSource | 两者都清,避免孤立 Source 累积 |
| 5 | 矢量层看不见 | 缺描边 / 点图标资源缺失(见第 11 篇) | 面必配描边,点用文本图标 |
| 6 | 误以为离线必须有底图 | 高估了实景背景的必要性 | 业务数据本身就是图,矢量现算是默认选择 |
小结:
- 离线地图三条路线不是互斥的,而是按"数据形态 + 观感需求"组合使用;
- "矢量 → 图片的换算时机"是选型的一把尺子:实时算 / 整图贴 / 按需取;
- MapLibre 下三者统一走"源 + 图层"管线,离线 / 在线切换只需换源,架构上最干净。
完整可运行源码:GitCode 仓库 · android_osmdroid_maplibre
对照阅读:本系列第 03 篇(osmdroid 分层世界:离线方案、图层模型与 Overlay 体系)——同样的三条路线,osmdroid 要分别处理 Overlay 与 TileSource 两套机制,MapLibre 只需换 Source,对照看能理解"统一管线"的价值。
延伸阅读:本系列第 11 篇(MapLibre 统一图层模型:矢量现算的渲染出口)
评论区聊聊:你在没网的环境里用地图踩过什么坑?影像贴图翻转、瓦片包体积爆炸、还是"领导说没网就别看了"?说说你的离线实战。
本系列为 osmdroid / MapLibre 双引擎对照实战,源码开源可运行。如果这篇对你有帮助,点个赞收藏一下,后续会持续更新地图接入、数据绘制、性能优化的完整链路。