GeoJSONcn
GeoJSONcn / 技术专栏 / GCJ-02 火星坐标:WGS8…

GCJ-02 火星坐标:WGS84 互转代码与位移实测

发布于 2026-08-08 · GeoJSONcn 技术团队 · 阅读约 12 分钟

你从 GPS 设备、高德、OSM 导出的经纬度经常对不上,差几百米是常态。根子在坐标系:我国要求公开地图服务使用 GCJ-02,而 GPS 和绝大多数 GeoJSON 数据是 WGS84。它跟 WGS84 之间没法用旋转平移描述,本质是一次非线性加密偏移。

两套坐标系的关系

WGS84 是全球 GPS 采用的基准,经纬度即地球表面的真实位置。GCJ-02(俗称火星坐标)是在 WGS84 基础上按经纬度做的一次非线性加偏,自 2002 年起作为我国公开地图的强制标准。没有公开的反向参数,所以高德、腾讯的经纬度不能直接喂给 Leaflet 去叠加 WGS84 底图。

RFC 7946 第 4 节明确规定 GeoJSON 坐标必须基于 WGS84,并删除了旧的 crs 成员。规范的 GeoJSON 理论上不该带 GCJ-02 坐标,但现实里混用太多,导入前一定要确认来源。坐标系与规范的交叉点在 RFC 7946 的隐蔽坑 里讲过。

正向转换代码

GCJ-02 的加偏算法是公开的,基于克拉索夫斯基椭球,下面这段直接能用:

const PI = Math.PI;
const a = 6378245.0;                  // 克拉索夫斯基椭球长半轴
const ee = 0.00669342162296594323;    // 第一偏心率平方

function outOfChina(lng, lat) {
  return (lng < 72.004 || lng > 137.8347) ||
         (lat < 0.8293 || lat > 55.8271);
}

function tLat(x, y) {
  let r = -100 + 2*x + 3*y + 0.2*y*y + 0.1*x*y + 0.2*Math.sqrt(Math.abs(x));
  r += (20*Math.sin(6*x*PI) + 20*Math.sin(2*x*PI)) * 2/3;
  r += (20*Math.sin(y*PI) + 40*Math.sin(y/3*PI)) * 2/3;
  r += (160*Math.sin(y/12*PI) + 320*Math.sin(y/30*PI)) * 2/3;
  return r;
}

function tLng(x, y) {
  let r = 300 + x + 2*y + 0.1*x*x + 0.1*x*y + 0.1*Math.sqrt(Math.abs(x));
  r += (20*Math.sin(6*x*PI) + 20*Math.sin(2*x*PI)) * 2/3;
  r += (20*Math.sin(x*PI) + 40*Math.sin(x/3*PI)) * 2/3;
  r += (150*Math.sin(x/12*PI) + 300*Math.sin(x/30*PI)) * 2/3;
  return r;
}

// WGS84 -> GCJ-02
function wgs84ToGcj02(lng, lat) {
  if (outOfChina(lng, lat)) return [lng, lat];
  let dLat = tLat(lng - 105, lat - 35);
  let dLng = tLng(lng - 105, lat - 35);
  const radLat = lat / 180 * PI;
  let magic = Math.sin(radLat);
  magic = 1 - ee * magic * magic;
  const sq = Math.sqrt(magic);
  dLat = (dLat * 180) / ((a * (1 - ee)) / (magic * sq) * PI);
  dLng = (dLng * 180) / (a / sq * Math.cos(radLat) * PI);
  return [lng + dLng, lat + dLat];
}

偏移量跟位置强相关。几个代表点的换算结果:

地点WGS84 经度WGS84 纬度GCJ-02 经度GCJ-02 纬度水平位移
天安门116.39742839.90923116.40367239.910634约 555 米
广州113.26438523.129112113.26971523.126435约 621 米
哈尔滨126.53496745.803775126.54100745.805757约 517 米
喀什75.98926139.46766375.99221439.467888约 255 米

天安门从 WGS84(116.397428, 39.90923)换到 GCJ-02 落到 116.403672, 39.910634,水平位移约 555 米。这就是地图"飘"几百米的来源。

逆向转换:没有闭式解

GCJ-02 转回 WGS84 没有解析公式。一次近似是把它当对称偏移减回去,误差 1 到 2 米:

// GCJ-02 -> WGS84(一次近似)
function gcj02ToWgs84(lng, lat) {
  if (outOfChina(lng, lat)) return [lng, lat];
  const [glng, glat] = wgs84ToGcj02(lng, lat);
  return [lng - (glng - lng), lat - (glat - lat)];
}

想要更高精度就迭代两次,第二次用第一次的残差再减一次,误差能压到亚米级。绝大多数可视化场景一次近似已经够用。

顺手处理 BD-09

百度地图在 GCJ-02 之上又叠了一层 BD-09,所以百度坐标要先转回 GCJ-02 再换 WGS84:

// GCJ-02 -> BD-09
function gcj02ToBd09(lng, lat) {
  const r = Math.sqrt(lng*lng + lat*lat) + 0.00002*Math.sin(lat*PI);
  const t = Math.atan2(lat, lng) + 0.000003*Math.cos(lng*PI);
  return [r*Math.cos(t) + 0.0065, r*Math.sin(t) + 0.006];
}

三个坑

偏移只对国境内生效。outOfChina 把境外直接透传,但 72.004 到 137.8347 这个框是按我国陆地轮廓粗划的,中越、中缅边界附近的零散点会算错,要严就自己加边界判定。坐标系对齐、把 GCJ-02 点正确叠加到 WGS84 面上做缓冲,在 GeoJSON 缓冲区分析 里展开过。

批量转换别在循环里反复 new 对象。几百万个点的轨迹,把 transform 内联、复用数组,能快不少。EPSG:4490 转 4326 这种投影级转换是另一回事,属于重投影而非加偏,做法在 Shapefile 转 GeoJSON 实战 里。

底图坐标系要配对。天地图用的是 CGCS2000,和 WGS84 差到厘米级,通常可忽略;OSM 是纯 WGS84。拿 GCJ-02 的点去叠 OSM 底图却不转换,偏差就是真实的火星偏移,这部分在 天地图与 OSM 底图对比 里对照过。

小结

GCJ-02 本质是一次位置相关的非线性加偏,谈不上投影变换。正向往外偏移有公开公式,逆向只能逼近。处理 GeoJSON 前先确认坐标来源:GPS 和标准 GeoJSON 是 WGS84,高德腾讯是 GCJ-02,百度是 BD-09,按链路逐层还原,叠加到 WGS84 底图才不会飘。全国各级行政区划的 WGS84 GeoJSON 数据可在 geojsoncn.com 获取。