把全国区县边界切成矢量瓦片:tippecanoe 参数与 PMTiles 单文件部署
--- title: 把全国区县边界切成矢量瓦片:tippecanoe 参数与 PMTiles 单文件部署 ---
整包 GeoJSON 丢给浏览器,数据量一大就撑不住。全国 2800 多个区县面,压缩后仍有几十 MB,首屏白屏十几秒。这时候再去调渲染没用,卡住的是传输和解析,不是绘制。得把数据切成矢量瓦片,按视野、按缩放级取。
为什么是切片,不是继续压
抽稀能把顶点砍掉八成,TopoJSON 能让共享边只存一份。可它们改不掉一件事:客户端拿到的还是全国所有要素。只看广州一个市,新疆和黑龙江照样得先下载完。
矢量瓦片是另一套思路。数据在服务端按 z/x/y 网格预先切开,每块单独编码成 Protobuf,也就是 MVT。浏览器只请求当前视野盖到的那几块,一块通常几十 KB。低缩放级的瓦片里,要素已经被简化甚至丢掉了一部分,放大到高级别才给完整精度。体积优化的其他路子,可以看 TopoJSON 压缩 和 Douglas-Peucker 抽稀。
MVT 内部存的是相对坐标,每个瓦片划成 4096×4096 的整数网格,经纬度编码时映射进去。瓦片里没有浮点数,体积自然就下来了。
tippecanoe 的几个关键参数
切片工具首选 tippecanoe。最小可用的命令长这样:
tippecanoe -o china-district.mbtiles \
-l district \
-zg \
--drop-densest-as-needed \
--extend-zooms-if-still-dropping \
china-district.geojson
-l district 定图层名,前端配 source-layer 得用同一个。-zg 让它自己推最大缩放级,依据是要素密集程度。
--drop-densest-as-needed 是保命参数。tippecanoe 单块瓦片默认上限 500KB,超了直接报错退出。加上它之后,密集区域会主动丢掉一部分要素压进限额,而不是整个任务失败。配套的 --extend-zooms-if-still-dropping 管的是另一头:到了最大缩放级还在丢,就自动往上加级数。
属性字段也要瘦身:
tippecanoe -o china-district.mbtiles \
-l district \
-zg \
-y adcode -y name \
-S 4 \
--drop-densest-as-needed \
--read-parallel \
china-district.geojsonl
-y 是属性白名单,只留 adcode 和 name。行政区划数据经常带着面积、周长、拼音、上级代码一长串字段,每个要素重复一份,切完能占掉瓦片体积的一半。-S 4 调简化强度,默认值是 1,调到 4 之后低缩放级的边界明显平滑,体积还能再降。
--read-parallel(简写 -P)走多核并行读入,前提是输入为按行分隔的 GeoJSON 序列,也就是 .geojsonl。普通 GeoJSON 怎么转过去,流式处理超大 GeoJSON 里讲过。
MBTiles 转成 PMTiles
tippecanoe 吐出来的是 MBTiles,说白了是个 SQLite 数据库。想用它就得跑一个瓦片服务器,接 z/x/y 请求、查库、返回 Protobuf。多一个常驻服务,多一份运维。
PMTiles 把这一层省了。它是单个文件,内部自带目录索引,客户端先用 HTTP Range 请求取索引,再按偏移量直接抓某一块瓦片的字节区间。全程只要一个支持 Range 的静态文件服务,对象存储和 CDN 都现成支持。
pmtiles convert china-district.mbtiles china-district.pmtiles
pmtiles show china-district.pmtiles
pmtiles show 会打元数据,缩放范围、图层名、要素数量都在里面。切完先看一眼,确认没切歪。
三种分发方式的差别:
| 方式 | 文件形态 | 服务端要求 | 缓存 | 适合场景 |
|---|---|---|---|---|
| 散装 z/x/y.pbf | 几万到几十万个小文件 | 静态服务即可 | CDN 按瓦片缓存,命中率高 | 瓦片数量可控、追求极致首字节 |
| MBTiles + 服务 | 单个 SQLite | 需常驻瓦片服务 | 要自己加缓存层 | 已有后端、需动态过滤 |
| PMTiles | 单个文件 | 静态服务 + 支持 Range | CDN 按字节区间缓存 | 纯静态托管、想省运维 |
散装瓦片最烦人的是文件数量。全国区县切到 z12,文件数轻松过十万,传对象存储要跑很久,后续删除和更新一样痛苦。PMTiles 只有一个文件,替换就是更新。
前端接上去
MapLibre GL 不认识 pmtiles://,得先注册协议处理器:
import maplibregl from 'maplibre-gl';
import { Protocol } from 'pmtiles';
const protocol = new Protocol();
maplibregl.addProtocol('pmtiles', protocol.tile);
const map = new maplibregl.Map({
container: 'map',
center: [104.1, 35.8],
zoom: 3,
style: {
version: 8,
sources: {
district: {
type: 'vector',
url: 'pmtiles://https://geojsoncn.com/tiles/china-district.pmtiles',
promoteId: 'adcode'
}
},
layers: [
{
id: 'district-fill',
type: 'fill',
source: 'district',
'source-layer': 'district',
paint: {
'fill-color': '#e8f0fb',
'fill-outline-color': '#3b6fb5'
}
}
]
}
});
source-layer 必须等于切片时 -l 给的图层名。这两个对不上是最常见的翻车点,地图正常加载,控制台一声不吭,页面上什么都没有。
promoteId: 'adcode' 把属性里的 adcode 提升成要素 id。矢量瓦片和整包 GeoJSON 的渲染取舍不太一样,可以对照 前端渲染三条路线 看。
三个容易踩的坑
跨瓦片的要素会被切开。一个区县横跨两块瓦片,它在两块里各占一半,是两个独立几何。做 hover 高亮时不设 promoteId,这两半就是两个要素,鼠标移上去只亮一半。设了之后共享同一个 id,setFeatureState 一次生效。
低缩放级的要素可能不全。--drop-densest-as-needed 丢的是整个要素,不是简化它。z3 的全国视野下,密集区域的某些区县压根不在瓦片里,这时候用 queryRenderedFeatures 数数量会偏少。要统计就回原始数据算,别信渲染结果。
坐标系必须是 WGS84。tippecanoe 只吃 EPSG:4326 的经纬度,切片时按 Web 墨卡托规则映射到瓦片网格。源数据要是 GCJ-02 或者 CGCS2000 投影坐标,转好了再切;切完才发现偏移,只能整个重来。行政区划数据可以在 geojsoncn.com 找。
什么时候别切
数据量不大就别折腾。单个省的区县边界压缩后一两 MB,直接加载体验够用,切片只会多出构建环节和更新成本。我自己的分界线是:整包压缩后超过 5MB,或者要素数超过 5000 个,切片才划算。