4. 坐标系统与投影:地理坐标系(WGS84、GCJ02)、投影坐标系、坐标转换实战

坐标系统,说白了就是给地球上的每个点发一个「身份证号」。但地球是个不规则的椭球体,怎么发这个号,不同国家、不同场景有各自的玩法。

我刚开始做地图开发时,觉得坐标就是个经纬度嘛,有啥好学的?直到有一次,我用 WGS84 的数据直接叠到高德地图上,结果点位偏了五六百米。嗯,从那以后我再也不敢小看坐标系了。

核心认知:坐标系统不是数学问题,是政治问题 + 历史问题 + 工程问题的混合体。你不需要背公式,但必须知道「谁在用哪个坐标系」以及「怎么转」。

坐标系统与投影知识体系 地理坐标系 投影坐标系 坐标转换实战 WGS84 GCJ02 高斯-克吕格 Web 墨卡托 WGS84 ↔ GCJ02 GPS 原生 国内互联网地图 分带投影 平面显示 算法 + 库 避坑要点 • 国内互联网地图(高德、百度、腾讯)全部使用 GCJ02 • GPS 设备、Google Earth、OSM 使用 WGS84 • 投影坐标系用于面积/距离计算,地理坐标系用于定位

4.1 地理坐标系:地球的「经纬度身份证」

地理坐标系用经纬度描述位置。经度范围 -180° 到 180°,纬度范围 -90° 到 90°。听起来简单,但不同坐标系用的「地球模型」不一样。

4.1.1 WGS84 —— 全球通用标准

WGS84 全称 World Geodetic System 1984。GPS 卫星用的就是它。Google Earth、OpenStreetMap、大部分国际 GIS 数据都基于 WGS84。

我个人习惯把 WGS84 当作「原始数据格式」。所有从 GPS 设备、手机定位 API 拿到的坐标,默认都是 WGS84。

小技巧:WGS84 的经纬度通常用十进制度数表示,比如 39.9042°N, 116.4074°E。不要用度分秒格式做计算,容易出精度问题。

4.1.2 GCJ02 —— 中国特有的「加密坐标系」

GCJ02 全称是「国测局 2002 坐标系」。说白了,就是在 WGS84 基础上加了一个非线性偏移。偏移量大约在 100-700 米之间,随机且不可逆。

为什么会有这个?嗯,这是国家测绘局的要求。所有在中国境内发行的互联网地图,必须使用 GCJ02。高德、腾讯、百度(百度有自己的二次偏移,叫 BD09)都遵循这个规则。

我在项目中遇到过最坑的事:客户拿了一台进口 GPS 设备,直接往高德地图上打点。结果所有点位都偏到隔壁楼去了。后来加了坐标转换才解决。

坐标系 全称 使用场景 偏移量
WGS84 World Geodetic System 1984 GPS、Google Earth、OSM、国际数据 无
GCJ02 国测局 2002 高德、腾讯、国内互联网地图 100-700m 随机偏移
BD09 百度坐标系 2009 百度地图 在 GCJ02 基础上再偏移

警告:千万不要把 WGS84 坐标直接扔到高德或百度地图上显示。偏移量足够让你的 POI 跑到隔壁街区。反过来,从高德拿到的坐标也不能直接用于 GPS 导航。

4.2 投影坐标系:把球面压成平面

地球是球体,但屏幕和纸张是平面。投影坐标系就是把球面上的经纬度「压」到平面上。压的过程必然有变形——面积、距离、角度、形状,至少牺牲一个。

4.2.1 Web 墨卡托投影(EPSG:3857)

Web 墨卡托是目前互联网地图的主流。Google Maps、高德、百度都用它。它的特点是:

  • 角度不变(等角投影),适合导航
  • 面积变形严重,越靠近两极越大
  • 把地球近似为球体,计算简单

你想想看,为什么 Greenland 在地图上看起来比非洲还大?就是墨卡托投影的锅。实际面积非洲是 Greenland 的 14 倍。

4.2.2 高斯-克吕格投影(Gauss-Krüger)

高斯-克吕格是分带投影。把地球切成 60 个 6° 宽的带,每个带单独投影。中国主要用 3° 带和 6° 带。

这个投影在国内测绘、国土、规划领域用得非常多。如果你做的是政府项目,大概率会遇到 CGCS2000 坐标系 + 高斯-克吕格投影的组合。

经验之谈:做 Web 前端地图用 Web 墨卡托(EPSG:3857),做专业测绘分析用高斯-克吕格(EPSG:4490 系列)。选错了投影,面积计算能差出 30%。

4.3 坐标转换实战:代码说话

理论说完了,来点实际的。坐标转换最常用的场景就是 WGS84 ↔ GCJ02。我直接给你一个可用的 Python 实现。

4.3.1 WGS84 转 GCJ02

import math

def wgs84_to_gcj02(lng, lat):
    """
    WGS84 转 GCJ02(高德/腾讯坐标系)
    :param lng: 经度
    :param lat: 纬度
    :return: (gcj_lng, gcj_lat)
    """
    a = 6378245.0  # 长半轴
    ee = 0.00669342162296594323  # 偏心率平方

    def _transform_lat(x, y):
        ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * math.sqrt(abs(x))
        ret += (20.0 * math.sin(6.0 * x * math.pi) + 20.0 * math.sin(2.0 * x * math.pi)) * 2.0 / 3.0
        ret += (20.0 * math.sin(y * math.pi) + 40.0 * math.sin(y / 3.0 * math.pi)) * 2.0 / 3.0
        ret += (160.0 * math.sin(y / 12.0 * math.pi) + 320.0 * math.sin(y * math.pi / 30.0)) * 2.0 / 3.0
        return ret

    def _transform_lng(x, y):
        ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * math.sqrt(abs(x))
        ret += (20.0 * math.sin(6.0 * x * math.pi) + 20.0 * math.sin(2.0 * x * math.pi)) * 2.0 / 3.0
        ret += (20.0 * math.sin(x * math.pi) + 40.0 * math.sin(x / 3.0 * math.pi)) * 2.0 / 3.0
        ret += (150.0 * math.sin(x / 12.0 * math.pi) + 300.0 * math.sin(x / 30.0 * math.pi)) * 2.0 / 3.0
        return ret

    # 判断是否在中国境内
    if lng < 72.004 or lng > 137.8347 or lat < 0.8293 or lat > 55.8271:
        return lng, lat  # 境外不偏移

    dlat = _transform_lat(lng - 105.0, lat - 35.0)
    dlng = _transform_lng(lng - 105.0, lat - 35.0)
    radlat = lat / 180.0 * math.pi
    magic = math.sin(radlat)
    magic = 1 - ee * magic * magic
    sqrtmagic = math.sqrt(magic)
    dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * math.pi)
    dlng = (dlng * 180.0) / (a / sqrtmagic * math.cos(radlat) * math.pi)

    gcj_lat = lat + dlat
    gcj_lng = lng + dlng
    return gcj_lng, gcj_lat

4.3.2 GCJ02 转 WGS84

def gcj02_to_wgs84(lng, lat):
    """
    GCJ02 转 WGS84(迭代法,精度约 0.5 米)
    """
    wgs_lng, wgs_lat = lng, lat
    for _ in range(5):  # 迭代 5 次
        gcj_lng, gcj_lat = wgs84_to_gcj02(wgs_lng, wgs_lat)
        wgs_lng += lng - gcj_lng
        wgs_lat += lat - gcj_lat
    return wgs_lng, wgs_lat

注意:GCJ02 转 WGS84 没有解析解,只能用迭代逼近。上面的代码迭代 5 次,精度已经足够日常使用。如果你需要更高精度,可以迭代 10 次。

4.3.3 使用第三方库(推荐)

自己手写转换代码适合理解原理。实际项目中,我建议直接用现成的库:

  • Python:coordTransform 或 geopy
  • JavaScript:coordtransform(npm 包)
  • Java:CoordinateTransform(GitHub 开源)

我曾经在一个数据管道项目里,每天要转换 500 万条 GPS 轨迹。手写代码跑了 3 天发现精度不够,换成 coordTransform 库后,精度从 5 米提升到 0.5 米,速度还快了 30%。

4.4 常见踩坑与避坑指南

做坐标转换这么多年,我总结了几条血泪教训:

  • 不要相信「一次转换永久使用」——不同数据源的坐标系标注可能不准,拿到数据先抽几个点验证
  • 投影坐标系一定要带 EPSG 编号——光说「高斯-克吕格」不够,要说明是 3° 带还是 6° 带,中央经线是多少
  • 百度坐标是 GCJ02 的二次加密——从百度地图拿到的坐标,要先转 GCJ02,再转 WGS84,不能直接转
  • 境外坐标不需要偏移——GCJ02 只在中国境内生效,境外坐标直接透传

我曾经踩过的坑:有一次做全国 POI 数据融合,把高德数据和 GPS 数据直接合并分析。结果北京地区的 POI 密度异常高,查了两天才发现是坐标偏移导致同一个 POI 在两种坐标系下被识别成两个点。后来加了坐标统一转换,数据量直接砍掉 40%。

4.5 本章小结

坐标系统是 GIS 的基石。你不需要记住所有公式,但必须记住三件事:

  1. WGS84 是原始数据,GCJ02 是国内互联网地图的标准
  2. 投影坐标系把球面压成平面,Web 墨卡托是 Web 地图的默认选择
  3. 坐标转换用现成库,别自己造轮子