MapLibre 实战 12|矿区井下一格信号都没有:离线地图三路线,差异只在"什么时候算成图"

3 阅读9分钟

摘要:野外、井下、涉密内网——没网的时候地图怎么显示?三条可落地路线(矢量现算 / 影像贴图 / 瓦片归档),它们的本质差异只有一句话:"矢量 → 图片"的换算时机不同:现算 / 现贴 / 现取。这篇给出每条路线的 MapLibre 接入原语、三路线对比表、以及一个真实教训——四角顺序错一位,影像就翻过来了。

目录(TOC)


一、现场:巷道里一格信号都没有

想象这个现场:你在矿区井下做巡检,手机开着地图 App,但巷道里一格信号都没有。业务数据(监测点、边界、设备位置)还是要叠在地图上,还要能点、能查、能缩放。

你总不能跟领导说"没网所以看不了"。

野外、井下、涉密内网、国际漫游等场景,设备往往拿不到在线瓦片服务。但业务仍需在地图上叠加自有数据(监测点、矢量边界、影像底图)。

移动端地图 SDK 的离线能力各有取舍,本文把三种可落地路线归纳成一句话差异:

差异只在"矢量 → 图片"的换算时机:现算 / 现贴 / 现取。

flowchart LR
    DATA["离线数据"] --> B{"数据形态"}
    B -->|&#34;矢量几何&#34;| C[&#34;① 矢量现算<br/>GPU 实时算图元&#34;]
    B -->|&#34;单张影像&#34;| D[&#34;② 影像贴图<br/>整图仿射铺&#34;]
    B -->|&#34;预切瓦片&#34;| E[&#34;③ 瓦片归档<br/>按需取现成 PNG&#34;]

    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 的"恢复在线"按钮即清掉所有离线叠加、把在线底图重新显示。

这里有个容易漏的点:只 removeLayerremoveSource,源会残留在 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 双引擎对照实战,源码开源可运行。如果这篇对你有帮助,点个赞收藏一下,后续会持续更新地图接入、数据绘制、性能优化的完整链路。