GeoJSONcn
GeoJSONcn / 技术专栏 / 给 GeoJSON 边界“抽顶点…

给 GeoJSON 边界“抽顶点”:Douglas-Peucker 抽稀怎么用才不丢精度

发布于 2026-07-20 · GeoJSONcn 技术团队 · 阅读约 8 分钟

处理全国省、市、区县四级行政区划边界时,我常被同一个问题绊住:一份区县的 GeoJSON 丢进浏览器,光一个沿海县的海岸线就几万个顶点,整张地图拖得鼠标都卡。顶点不是越多越准,大部分拐点肉眼根本看不出来,却实打实吃着体积和渲染时间。

这篇只聊一件事:用 Douglas-Peucker 算法,在你能接受的容差内删掉冗余顶点,让边界瘦下来、形状还不变。

抽稀在删什么

一条边界折线,本质是点的序列。相邻两点之间如果几乎在一条直线上,中间那些点就是冗余的。Douglas-Peucker(简称 DP)的做法很直观:

1. 连首尾两点,得到一条基准线; 2. 找出离这条线最远的点,算它到线的垂距; 3. 如果垂距大于容差 tolerance,这个点保留,并以它为界把折线切成两段,各自递归; 4. 如果所有点的垂距都不超过容差,整段用首尾直线替代,中间点全部删除。

容差就是你给形状设的误差上限:给得越大,删得越狠,文件越小,边界也越方。

一段能跑的代码

下面这段是 DP 的最小可用实现,输入坐标数组、容差,返回抽稀后的坐标:

// 点到线段的垂距(平面坐标,单位需与坐标一致)
function perpDistance(p, a, b) {
  const dx = b[0] - a[0], dy = b[1] - a[1];
  const len = Math.hypot(dx, dy) || 1e-9;
  return Math.abs((p[0] - a[0]) * dy - (p[1] - a[1]) * dx) / len;
}

// Douglas-Peucker 抽稀:coords 为 [x, y] 数组,tolerance 为容差
function simplifyRing(coords, tolerance) {
  if (coords.length < 3) return coords.slice();
  let maxD = 0, idx = 0;
  const a = coords[0], b = coords[coords.length - 1];
  for (let i = 1; i < coords.length - 1; i++) {
    const d = perpDistance(coords[i], a, b);
    if (d > maxD) { maxD = d; idx = i; }
  }
  if (maxD > tolerance) {
    const left = simplifyRing(coords.slice(0, idx + 1), tolerance);
    const right = simplifyRing(coords.slice(idx), tolerance);
    return left.slice(0, -1).concat(right);
  }
  return [a, b];
}

实际项目里一般不用手写,@turf/simplify 就是这套逻辑,还能直接吃 GeoJSON 的 FeatureCollection。

最容易踩的坑:容差用"米"还是"度"

很多人直接把经纬度 [lng, lat] 喂给上面的函数,容差写个 0.001 就想当然当成"约 100 米"。这是错的,而且错得很隐蔽。

经纬度是角度,1 经度对应的实地距离随纬度变化:赤道上约 111 km,到北纬 40° 就缩到约 85 km。你用同一个数值容差去删东西,高纬度和低纬度的"误差"根本不是一回事,边界会被删得东畸西扭。

正确做法是先把经纬度投影成平面米坐标(Web 墨卡托 EPSG:3857 或等距圆柱都行),在米的坐标系下用真实米容差抽稀,再把结果反投影回经纬度。10 米、50 米、100 米的容差,这时候才有统一的意义。关于坐标系本身的坑,WGS84 与 GCJ-02 那篇讲得更透。

抽稀前后的量级

以某沿海区县的一段边界环为例(原始约 12,000 个顶点),在 Web 墨卡托投影下、用不同米容差抽稀的结果:

容差顶点数相对原始GeoJSON 体积
原始12,000100%520 KB
10 m1,86015.5%82 KB
50 m7206.0%33 KB
100 m4103.4%19 KB

可以看到,容差 10 米就能砍掉八成以上的顶点,体积从 520 KB 降到 82 KB,肉眼几乎看不出形状变化。GeoJSONcn 的省、市、区县全图都走这一步处理,前端渲染 时拖拽才顺。

抽稀不是压缩,别混为一谈

DP 抽稀和 TopoJSON 压缩 是两回事,但能叠加用:抽稀先减点,TopoJSON 再把相邻行政区共享的那条边只存一次,最后再 gzip,三管齐下体积能压到原来的几十分之一。需要把处理好的边界导成 KML、SVG 或 CSV 的,看 格式转换。

想直观对比抽稀前后的边界贴合度,把它叠到 天地图或 OSM 底图 上一眼就明白。