同一份 GeoJSON 换个软件就画不出来?跨软件兼容差异排查清单
> 主题:GeoJSON下载 后跨软件兼容 / QGIS / Leaflet / PostGIS 行为差异 / 通用兼容清单
踩坑现场:一份文件,三个软件三种结果
做行政区划数据这行,最磨人的不是找数据,而是同一份 GeoJSON 换个软件就翻车。去年我导出一份省级下辖地市的边界,在 Leaflet 里画得漂漂亮亮,可一丢进 QGIS 坐标全跑到海里;同样一份文件,PostGIS 的 ST_GeomFromGeoJSON 直接 ERROR: geometry contains non-closed rings。最后排查出来,三者看到的是同一串字节,读出的却是三种"真相"。
这事的根子在于:GeoJSON 是"标准",但每个软件对标准的宽容度不一样。能正常解析的,多半是它对不合法的情况做了静默纠正;报错或画歪的,是它选择了严格遵守。今天我不讲规范本身,讲我踩过的差异和对应的兼容对策——都是能让全国省市区县geojson数据下载回来就能直接用的小经验。
差异一:坐标顺序,WGS84 到底该 lng,lat 还是 lat,lng
这是踩雷率最高的一条。GeoJSON 规范(RFC 7946)明确规定坐标是 经度在前、纬度在后([lng, lat])。可现实里大批数据源(尤其 OSM 部分导出、某些爬虫)是反着存的 [lat, lng]。Leaflet 默认按经度-纬度读,所以反序文件在它那儿画出来就是"竖着的一条线";QGIS 有 WKT/GeoJSON 的坐标系猜测,可能恰好读对,也可能给你再叠一层投影误差,肉眼很难判断到底对不对。
我的排查套路是抓一个你知道坐标的县城做锚点。比如成都大概在 [104.07, 30.67],用脚本把第一个 feature 的坐标打出来,看 x 是 104 还是 30:
const f = geojson.features[0];
const [x, y] = f.geometry.coordinates[0][0];
console.log(`${x}, ${y}`); // 期望 x 是经度 ~104, y 是纬度 ~30
如果 x 是 30 出头,说明整份是反序 [lat,lng],批量翻一下就行:遍历所有坐标把它们 swap 回来。千万别手动一个个改,几万要素会把你改疯,写个脚本十秒搞定。
差异二:环方向,外环是顺时针还是逆时针
RFC 7946 又一条要求:外环逆时针、内环(洞)顺时针,即"右手定则"。Leaflet / MapLibre 多数不较真,画出来差不多;可 PostGIS 和不少空间库对环方向是敏感的,方向错的 MultiPolygon 一旦参与 ST_Intersects、ST_Buffer,面积、包含关系全错。典型报错就是上面那个 geometry contains non-closed rings。
不手工改,用 Turf.js 或 QGIS 的 Fix geometries 一键修。QGIS 里这一步其实被很多人忽略——拿到数据直接开画,直到算面积时才发现对面错了。建议入库前统一过一次环方向,一劳永逸。
差异三:要素类型,Polygon 与 MultiPolygon 有没有混存
行政区划边界几乎全是面,但一份要素集里既可能有单个面,也可能有带飞地、岛屿的 MultiPolygon。有的导出工具偷懒,把 MultiPolygon 塞进 type: Polygon 或反之。QGIS 会尽量兼容,PostGIS 同样 ST_GeometryN 能解;可一旦你写脚本 feature.geometry.coordinates[0] 假设它是 Polygon,遇到 MultiPolygon 就少算了一个层级。
更隐蔽的是同一要素集里两种类型混存:前 100 个要素是 Polygon,第 101 个因为带个无人岛变成了 MultiPolygon。我当初就是写 coordinates[0][0] 直接取外环,结果处理到有岛的那个县时整个报错,排查半天才发现是类型跳变。解决办法很简单:入库前统一转成 MultiPolygon,一层 coordinates[0] 走天下。
写校验脚本时统一口径比较省心:
| 软件 | 坐标层级敏感 | 环方向敏感 | 反序敏感 | 静默纠正 |
|---|---|---|---|---|
| Leaflet / MapLibre | 低 | 低 | 高(画歪) | 较多 |
| QGIS | 中 | 中 | 中 | 中 |
| PostGIS / PostGIS ST_* | 高 | 高 | 高(报错) | 极少 |
| Python shapely | 中 | 高 | 高 | 较少 |
这张表不是绝对真理,但能帮你定位:"哪个最'挑剔',就以谁为准写兼容层"。我的做法是:先用 PostGIS 或 shapely 把数据修到能被我接受的程度,再交给 Leaflet 消费,这样前端怎么画都不会炸。
一份通用的"入库前兼容清单"
踩过一轮后,我总结了一份固定流程,每次拿到新数据(省、市、区县、乃至村级边界)都走一遍:
1. 统一坐标序:脚本打锚点验证 lng,lat,反序就批量 swap。 2. 统一要素类型:把散落的 Polygon 归并成 FeatureCollection,Geometry 统一成 MultiPolygon(省得写两层判断)。 3. 修环方向:过一遍 Turf 的 turf.rewind 或 QGIS Fix geometries。 4. 删多余字段:把 crs、非标准属性、空 Geometry 清掉——有些软件看到不认识的 crs 会去猜投影,反而帮倒忙。 5. 三次自检:原文件、写入 PostGIS 后再读回、烘干成小的预览版,三次顶点数应一致;不一致就是中间某步动了几何。
// 最小兼容修复管道:坐标序 + 类型归并 + 环方向(示意)
import { rewind } from '@turf/rewind';
import { featureCollection } from '@turf/helpers';
let fc = featureCollection(features.map(f => ({
type: 'Feature',
properties: f.properties,
geometry: { type: 'MultiPolygon', // 统一成 MultiPolygon
coordinates: normalize(f.geometry) }
})));
fc = rewind(fc, { reverse: false }); // 强制右手定则
最后提醒一句:预览归预览,商用入库前务必过一遍这份清单。像本站的行政区划边界,预览阶段免费给大家看版式,真要导出整份 GeoJSON / PNG / SVG / EMF 再拿去做分析或交付(按次 ¥1.99/次 起),我会建议先跑上面这五步自检——省得辛苦导出的数据在对方的软件里第一步就翻车,那才叫白忙一场。
> 需要全国省市区县区划边界数据下载、全国村级geojson数据下载,或想体验 GeoJSON下载 后的跨软件兼容,欢迎到 geojsoncn.com 预览与导出。