经纬志
首页 / 实战手册 / 坐标与投影

GeoJSON 没写坐标系,3857 的数据被当成经纬度

先看结论

文件能正常解析,features.length 也对,就是地图上什么都看不见。

最后验证 2026-11-03 · 多技术栈 · 更新 2026-10-29 · 引用本文

现象

从内部接口导出的 districts.geojson 有 312 个要素, 在 QGIS 里打开提示「图层范围无效」,在网页里用 L.geoJSON(data).addTo(map) 既不报错也不显示,调用 map.fitBounds(layer.getBounds()) 得到 LatLngBounds(20037508, 20037508) 这种越界值。

打开文件看第一个坐标:[13522390.4, 3669871.9],是米,不是度。

原因

GeoJSON 的现行规范 RFC 7946 取消了 crs 成员,规定坐标必须是 WGS84 经纬度, 顺序是 [经度, 纬度]。所有主流解析器因此默认按 4326 处理, 见到 13522390 这个数字就当成 13522390°,直接丢到视野之外。

旧版 GeoJSON 2008 规范允许 "crs": {"type":"name","properties":{"name":"EPSG:3857"}}, 但现在 Leaflet 1.x 和 Mapbox GL 都不读这个字段,写了也没用。 服务端用 ST_AsGeoJSON(geom, 15) 直接从 3857 几何导出时,就会产生这种文件。

能解析不等于坐标系对。GeoJSON 是「无类型」的坐标数组, 解析器没有能力判断单位,只能相信调用方。

排查过程

  1. 读第一个坐标对,判断数值量级:绝对值大于 180 就不是经纬度。
  2. 看文件顶层有没有 crs 成员。有也不代表解析器会用它。
  3. 算一遍要素的 bbox。x 在 7.3e6~1.5e7、y 在 2.4e6~6.1e6 就是中国区域的 3857。
  4. 回到导出环节,确认 SQL 里有没有做 ST_Transform(geom, 4326)。
// 快速判断一份 GeoJSON 的坐标系
const c = fc.features[0].geometry.coordinates;
const is4326 = Math.abs(c[0]) <= 180 && Math.abs(c[1]) <= 90;
console.log(is4326 ? '经纬度' : '投影坐标,量级 ' + c[0].toExponential(2));
// 13522390.4 -> "投影坐标,量级 1.35e+7"

解决方案

根治在导出端:GeoJSON 一律转成 4326 再序列化。

-- PostGIS:导出前必须转换
SELECT jsonb_build_object(
  'type', 'FeatureCollection',
  'features', jsonb_agg(ST_AsGeoJSON(t, 15)::jsonb)
)
FROM (
  SELECT id, name, ST_Transform(geom, 4326) AS geom FROM districts
) AS t;

导出端暂时改不了时,前端可以兜底转换。注意要在 L.geoJSON 之前完成, 不要依赖 coordsToLatLng 去猜。

// 前端兜底:判定为米制后整份转换
const projected = Math.abs(fc.features[0].geometry.coordinates[0]) > 180;
if (projected) fc = reproject(fc, 'EPSG:3857', 'EPSG:4326');

// Leaflet 也支持自定义 coordsToLatLng,但只适合单一坐标系
L.geoJSON(fc, {
  coordsToLatLng: coords => L.CRS.EPSG3857.unproject(L.point(coords[0], coords[1]))
}).addTo(map);

验证

转换后首个坐标应为 [121.4737, 31.2304] 这种量级; layer.getBounds() 返回的经纬度都在合法范围内, 地图缩放到 10 级能看到完整的行政区轮廓。

const b = L.geoJSON(fc).getBounds();
console.assert(b.isValid(), '范围仍无效');
console.assert(b.getNorth() <= 90 && b.getEast() <= 180, '仍在投影坐标');

适用版本

工具版本说明
RFC 79462016 起取消 crs,只允许 WGS84
Leaflet1.0 及以上不读 crs 成员,默认 4326
Mapbox GL JS2.x / 3.x只接受 4326,其它坐标必须预处理
PostGIS3.xST_AsGeoJSON 不改变坐标系
引用本文

经纬志(2026)。《GeoJSON 没写坐标系,3857 的数据被当成经纬度》。https://www.jingweizhi.com/cookbook/geojson-no-crs.html(访问日期 2026-11-03)。