GeoJSON 规范里四个隐蔽的坑:坐标顺序、crs 字段、环方向与 bbox
--- title: GeoJSON 规范里四个隐蔽的坑:坐标顺序、crs 字段、环方向与 bbox ---
GeoJSON 看着简单,就是个 JSON 文件。但现行规范 RFC 7946 里有几处规定特别容易被忽视,出了问题还很难排查:点明明存在却渲染到了海里、多边形把整个地球填成一种颜色、老数据挂着一个解析器根本不认的坐标系声明。这篇把最常见的四个坑讲清楚,每个都附上能直接跑的检查代码。
坑一:坐标顺序是先经度、后纬度
RFC 7946 规定,position 的顺序是 [经度, 纬度, 高程(可选)]。天安门写作 [116.397428, 39.90923],经度在前。
写反的后果分两种。北京这类高纬度、高经度的点写反后变成纬度 116,超过 ±90 的合法区间,校验工具能直接报错,还算好排查。麻烦的是广州 [113.264385, 23.129112] 这种:写反后的 [23.129112, 113.264385] 是印度洋上一个完全合法的坐标,数据不报错,点却默默漂到了海上。
更常见的翻车场景是和 Leaflet 混用。Leaflet 自己的 API 用的是纬度在前:L.marker([39.90923, 116.397428])。用 L.geoJSON() 加载数据时 Leaflet 内部会自动交换顺序,不用管;但如果手动从 feature.geometry.coordinates 里取值再传给 L.marker,就必须自己交换。漏掉这一步,是我见过的最高频错误来源。两套顺序约定并存的历史原因,在坐标系那篇里有更完整的说明。
坑二:crs 字段在 2016 年就被删掉了
GeoJSON 有两版规范。2008 年的社区版规范允许用 crs 成员声明坐标参考系;2016 年发布的 RFC 7946 把 crs 整个删除,规定 GeoJSON 坐标一律是 WGS84 经纬度(等同 EPSG:4326 的经纬度表达)。所以老数据里这样的声明:
"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } }
现代解析库大多直接忽略。忽略 CRS84 没事,它本来就是经纬度。真正的风险在另一种老数据上:有些数据生产方把投影坐标系写进了 crs,坐标值是几十万、几百万量级的米制数值。解析器忽略 crs 声明后,会把这些米制数值直接当成经纬度处理,几何图形瞬间飞出地球范围。判断方法不复杂:打开文件看第一个坐标,数值超出 [-180, 180] 就一定是投影坐标,需要先重投影成经纬度,具体流程可以照搬 Shapefile 转 GeoJSON 那篇里的 pyproj 步骤。
顺带说一句:国内常见的 CGCS2000(EPSG:4490)经纬度与 WGS84 经纬度数值差异在厘米级,Web 制图场景直接按 4326 使用即可,不需要专门转换。
坑三:多边形环方向,右手定则
RFC 7946 第 3.1.6 节规定:Polygon 的外环必须逆时针,内环(洞)必须顺时针。判断一个环的方向,用鞋带公式算有向面积即可:
// 鞋带公式求有向面积:> 0 逆时针,< 0 顺时针(平面近似,判方向足够)
function ringArea(ring) {
let sum = 0;
for (let i = 0; i < ring.length - 1; i++) {
const [x1, y1] = ring[i];
const [x2, y2] = ring[i + 1];
sum += (x2 - x1) * (y2 + y1);
}
return -sum / 2;
}
const isCounterClockwise = ring => ringArea(ring) > 0;
Leaflet 和 OpenLayers 在平面上渲染,不校验环方向,方向错了图也能画对。所以很多人从来没意识到这条规定的存在。d3-geo 是例外:它按球面几何解释多边形,环方向决定"哪一侧算内部"。如果外环方向不符合它的预期,d3 会认为你要画的是"整个地球减去这块区域",渲染结果就是全球被填色、目标区域反而被抠掉。这是 d3 画地图最经典的错误现象。
修复用 @turf/rewind 一行搞定。要注意 d3-geo 采用的环方向约定与 RFC 7946 相反(d3 要求外环顺时针),所以喂给 d3 的数据要用 reverse: true:
import rewind from '@turf/rewind';
const spec = rewind(geojson, { reverse: false }); // RFC 7946:外环逆时针
const forD3 = rewind(geojson, { reverse: true }); // d3-geo:外环顺时针
坑四:bbox 的顺序是西、南、东、北
bbox 是可选成员,四个数的顺序是 [最西经度, 最南纬度, 最东经度, 最北纬度]。以北京市为例,bbox 大致是 [115.4, 39.4, 117.5, 41.1]。
它的实用价值在加载优化:前端拿到 bbox 就能在解析整个 FeatureCollection 之前判断数据是否落在当前视野内,不相关的数据直接跳过。但注意又是坐标顺序坑:bbox 里是经度在前,而 Leaflet 的 LatLngBounds 是纬度在前,把 bbox 转成 fitBounds 参数时要交换:
const [w, s, e, n] = geojson.bbox;
map.fitBounds([[s, w], [n, e]]); // Leaflet 用 [纬度, 经度]
一张自查表
拿到一份来路不明的 GeoJSON,按这张表过一遍:
| 检查项 | 规范要求 | 快速检查方法 |
|---|---|---|
| 坐标顺序 | [经度, 纬度] | 中国境内数据第一个数应落在 73~135 区间 |
| crs 成员 | RFC 7946 已删除,默认 WGS84 | 坐标值超出 [-180, 180] 说明是投影坐标,先重投影 |
| 外环方向 | 逆时针 | 鞋带公式有向面积 > 0 |
| 内环方向 | 顺时针 | 鞋带公式有向面积 < 0 |
| bbox | [西, 南, 东, 北] | 第 1 项 < 第 3 项,第 2 项 < 第 4 项 |
如果对 GeoJSON 的整体结构还不熟,建议先读GeoJSON 到底是什么打底;需要和空间数据库交换数据的场景,环方向问题在 WKT 互转那篇提到的 PostGIS 里同样存在,ST_ForcePolygonCCW 可以做同样的修正。
全国省、市、区县三级的行政区划边界 GeoJSON(坐标顺序、环方向均按 RFC 7946 整理),可以在 GeoJSONcn 主站按行政区划检索预览。