GeoJSONcn
GeoJSONcn / 技术专栏 / 全国省市区县geojson数据下…

全国省市区县geojson数据下载后按要素体积分配渲染预算:一个乡镇 48.6 万顶点怎么垫高

发布于 2026-09-22 · GeoJSONcn 技术团队 · 阅读约 14 分钟

做全国乡镇街道边界数据下载的项目,常会遇到一个很难复现的卡顿。整份文件在桌面浏览器里转得挺顺,交给客户之后,他在评审会上放大到某一块,页面就掉到个位数帧率,鼠标拖不动。远程连过去才发现,出问题的永远不是整个图层,而是其中一两个要素。

把全国 32 个省级聚合文件全量扫了一遍,乡镇级要素合计 41526 个、顶点总量 1.68 亿。平均每个要素四千出头个顶点,看着很温和。但均值在大几何数据上是最没用的一个数——浙江淳安县安阳乡一个乡占了 48.65 万个顶点,占浙江全省 807 万个顶点的 6%,而它的县级邻居千岛湖镇只有 4.3 万个。同一个县,两个乡镇差 11 倍。卡顿来自分布的尾部,不是来自平均值。

顶点密度:比顶点总数更有用的一个指标

按要素绝对顶点数排序,头部全是沿海滩涂、库区、海岛这类"轮廓天然碎"的地方:

要素所属区县顶点数面积 km²顶点密度(顶点/km²)
安阳乡浙江·淳安县486503700.39694.6
文昌镇浙江·淳安县361079512.66704.3
上盘镇浙江·临海市63713145.19438.8
衢山镇浙江·岱山县57064151.22377.4
里商乡浙江·淳安县56765213.40266.0
菜园镇浙江·嵊泗县53231128.55414.1

六条里有五条落在浙江,但真正值得看的是最后一列。淳安县安阳乡 694.6 顶点/km²,浙江全省中位数 69.9,差了近十倍;湖南、甘肃、新疆不少乡镇的密度在 20 以下。同一个省内部差十倍,说明问题不在"哪个省的数据细",而在同一批数据里精度基准不统一。

按密度排序比按总数排序更能预测渲染开销。同样一千个顶点,摊在 50 平方千米上的图形在屏幕上只有几个像素宽,顶点全部落在同一像素列里,光栅化阶段全在空转;摊在 5 平方千米上的才真正画出线来。评估时把这个比值算出来,一次就能筛出真正需要单独处理的要素。

密度还牵出第二个用途:合省时的精度对齐。按面积加权算省级平均密度,再用"目标密度"反推各省应该抽到多少顶点,得到的容差表比套一个全局值合理得多。前面提过的四川抽稀留存率异常偏高,用这个方法能直接解释——它的原始密度本来就比邻省高一档,不在同一水平线上比较抽稀率没有意义。

顶点的分布形态:一个乡 18 个部件、一个镇 871 个部件

头部的顶点总量并不能说明顶点花在什么地方。拆开看几何结构,两个极端案例的成因完全不同。

安阳乡是 MultiPolygon,18 个部件,其中最大那个环独占 26.9 万个顶点。这不是"轮廓复杂",是一条超长外环——它沿着水库岸线绕了一整圈。这种结构对渲染不友好:即使视口里只看得到该乡的一小块,浏览器仍需处理整条外环。

文昌镇是另一个方向。它有 871 个部件,部件数量是浙江省内最多的,总顶点 35.6 万个,最大环仅 2.6 万个。这个镇本身不是海岛型,871 个部件来自合并前的村面碎片——库区周边地形把村界切得很碎,聚合时把碎块全部保留了。

两种形态的优化手段不通用。长环要靠抽稀压顶点,碎块要靠部件级过滤(面积或顶点数低于阈值的部件直接合并到主部件,或在低缩放级别下不渲染)。用同一套参数处理这两类要素,必然是前者压不够、后者被压坏。

全国要素里 MultiPolygon 类型只有 4086 个,占 9.8%,其余 90.2% 是单一多边形。另外有 62 个要素的 geometry 字段是 null——都出现在各省的 _无码要素 附属文件里,江苏 51 个、新疆 8 个、黑龙江 3 个。这类要素在大多数渲染库里会让整个图层静默中断,入图前必须显式剔除。

用分级预算替代全局抽稀

既然开销集中在少数要素,就没有必要为了它们牺牲绝大部分数据的精度。可行的做法是先把要素按"顶点数 × 是否进视口"分档,再给每档分配不同的处理策略:

import json, math

# 分档:面积加权密度 > 400 且顶点 > 5 万,视为重要素,需要单独标定容差
HEAVY_DENSITY, HEAVY_VERTEX = 400.0, 50000
MULTI_PART_LIMIT = 200          # MultiPolygon 部件数上限
MIN_PART_AREA = 0.01            # 单位 km²,低于此值的部件并入主部件

def profile(feature):
    gm = feature["geometry"]
    if not gm:
        return None                                   # 空几何直接剔除
    verts, parts = 0, 0
    polys = gm["coordinates"] if gm["type"] == "MultiPolygon" else [gm["coordinates"]]
    for poly in polys:
        parts += 1
        verts += sum(len(ring) for ring in poly)
    return {"verts": verts, "parts": parts,
            "heavy": verts > HEAVY_VERTEX,
            "fragment": parts > MULTI_PART_LIMIT}

假设阈值定在顶点五万、部件两百:重要素走部件级瘦身加更强抽稀,普通要素走常规容差;部件数超限的按面积阈值裁掉零碎块——这一步对视觉轮廓几乎无影响,却能同时砍掉顶点数和部件数。

对小面积要素要留一条反向规则。全国有 651 个乡镇级要素面积不足 1 平方千米,最极端的几个还挺费顶点:上海老西门街道 0.86 平方千米、2834 个顶点;豫园街道 0.92 平方千米、2435 个顶点;广东一个未命名单元 0.28 平方千米、1811 个顶点。这类要素本身没多少数据量,但它们的相对误差极敏感。上海豫园街道按 1e-3 度做抽稀,相当于按约 111 米取点,而街道尺度不到 1 千米,轮廓会被拉成三角形。所以抽稀容差不能按绝对米数拍,得按"要素自身尺度"的百分比来定,小要素的容差要跟着尺寸缩。

底图轮廓和顶点预算这件事,落到对外交付时还有一层。把某一级单独抠出来做汇报页的,模板选购页 里按省市逐级列了可编辑底图,单份 ¥4.99,预览免费,可以先看轮廓密不密再定要不要自己渲染。省市区县geojson数据下载后的数据适合做分析和二次加工,直接拿去排版是另一条路,那套模板里已经帮你把部件数和顶点数控制在能编辑的量级上了。

收尾判断

这套预算的验收标准只落三条,其余细节交给常规流程:

1. 先算密度再看总数。要素级顶点密度超过全省中位数十倍以上的,单独标定容差,不进统一批处理。 2. 长环与碎块分开治。单环顶点占比超过八成的按抽稀压;部件数超过两百的按面积阈值裁。两者用同一组参数一定出问题。 3. 空几何与极小面积要素入图前拦两道。geometry 为 null 的直接剔,面积不足 1 平方千米的走尺度相关容差。

全国村级geojson数据下载回来做聚合的路径,顶点是从村面一路继承上来的,乡镇级头部要素之所以夸张,往往只是因为某个村面的原始精度特别高,聚合时全量保留了。真正需要处理的要素不到 1%,把它挑出来单独对付,比整层降精度稳得多。

本文 41526、1.68 亿、486503 这组数字取自特定一轮聚合版本,换一版源会浮动,方法比数字更值得带走。省市区县区划边界下载的读者若要在自己的数据集上复用上面这套分档逻辑,只需把阈值按本地中位数重新标一次——GeoJSON下载 回来的文件精度基准混杂是常态,一次性把全局容差调到最低只会两败俱伤。全国省市区县区划边界数据下载后的文件如果来自多个来源,更要先做一次密度普查再定参数。区划口径请以国家有关部门发布的为准。