前言

刚开始用地图 SDK,很多人会遇到一个奇怪的问题:GPS 获取到的当前位置,标到地图上发现偏了几百米甚至一两公里——明明站在马路上,标记却在马路旁边的小区里。

这不是 bug,这是坐标系的问题。中国有专属的地图坐标系 GCJ02,GPS 用的是 WGS84,两者之间存在系统性偏移。这篇文章把这个问题讲清楚,然后看项目里是怎么处理的。

项目预览

一、三种坐标系

1.1 WGS84(全球通用)

World Geodetic System 1984,国际标准坐标系,GPS 卫星使用的就是这套坐标。geoLocationManager.getCurrentLocation() 返回的位置就是 WGS84 坐标。

1.2 GCJ02(中国国标)

国测局02坐标系,也叫"火星坐标系",是中国政府要求在中国境内地图必须使用的坐标系。高德地图、腾讯地图、华为 MapKit 在中国显示地图时都用 GCJ02。

WGS84 → GCJ02 的转换算法叫"火星偏移",偏移量在 100-300 米之间,随地理位置变化。

1.3 BD09(百度专用)

百度地图在 GCJ02 基础上又加了一层偏移,得到 BD09。只有百度地图使用,其他地图不认。

1.4 三者对比

坐标系 使用者 精确度 转换关系
WGS84 GPS、Google(国际版) 基准 → GCJ02(加偏移)
GCJ02 高德、腾讯、华为MapKit -100~-300m → BD09(再加偏移)
BD09 百度地图 更偏 独立体系

二、为什么偏移这么大?

GCJ02 的偏移是故意的(保密级测绘数据保护),且偏移量不是简单的固定值,而是根据经纬度变化的非线性偏移,无法通过简单加减得到原始坐标(除非你知道转换算法)。

正确做法:

  • geoLocationManager 获取到 WGS84 坐标
  • 显示到地图上之前,转换为 GCJ02
  • 地图上的操作(标记、路线)统一用 GCJ02

三、项目中的坐标转换

// entry/src/main/ets/utils/MapUtil.ets
public async convertToGCJ02(latitude: number, longitude: number): Promise<mapCommon.LatLng> {
  let theWGS84Position: mapCommon.LatLng = {
    latitude: latitude,
    longitude: longitude
  };

  // MapKit 提供的官方转换 API
  let theGCJ02Position: mapCommon.LatLng =
    await map.convertCoordinate(
      mapCommon.CoordinateType.WGS84,   // 源坐标系:WGS84
      mapCommon.CoordinateType.GCJ02,   // 目标坐标系:GCJ02
      theWGS84Position
    );

  return theGCJ02Position;
}

map.convertCoordinate 是 MapKit 提供的官方坐标转换 API,支持:

目标
WGS84 GCJ02
GCJ02 WGS84

四、转换在哪里被调用?

// MapUtil.ets
async moveToMyLocation(mapController: map.MapComponentController): Promise<void> {
  // 1. 获取 GPS 位置(WGS84 坐标)
  let location: geoLocationManager.Location = await this.getMyLocation();

  // 2. 更新地图的"我的位置"
  mapController?.setMyLocation(location);  // 注意:这里传的是 WGS84(地图内部处理)

  // 3. 移动镜头前,先转换为 GCJ02
  let gcj02Position = await this.convertToGCJ02(location.latitude, location.longitude);

  // 4. 用 GCJ02 坐标移动镜头
  this.moveToCurrentPosition(gcj02Position.latitude, gcj02Position.longitude, mapController);
}

流程:

  1. geoLocationManager.getCurrentLocation() 获取 WGS84 位置
  2. setMyLocation(location) 设置地图上显示的用户位置(SDK 内部会处理转换)
  3. convertToGCJ02() 手动转换坐标
  4. moveToCurrentPosition() 用 GCJ02 坐标移动地图镜头

提示:setMyLocation 接受的是 WGS84 位置,SDK 内部自动转换;而 moveToCurrentPositionanimateCamera)期望的是 GCJ02,所以需要手动转换后传入。这是一个容易搞混的地方。

五、加油站标记为什么不用转换?

// 加油站数据(StationData.ets)
'latitude': 31.937176963332842,
'longitude': 118.86018812656404,

加油站的坐标是手动配置的,注释里说 // Please configure it yourself——这些坐标应该直接填入 GCJ02 坐标(从高德、腾讯地图上取点,那些平台给的就是 GCJ02)。

// 添加标记时直接用(假设数据里就是 GCJ02)
async addMapMaker(latitude: number, longitude: number, mapController: ...): Promise<void> {
  let markerOptions: mapCommon.MarkerOptions = {
    position: {
      latitude: latitude,    // 直接用,不转换
      longitude: longitude
    },
    // ...
  };
  await mapController.addMarker(markerOptions);
}

结论:地图 API(animateCameraaddMarker)统一使用 GCJ02,只有 GPS 获取到的位置是 WGS84,需要转换。

六、常见踩坑

踩坑 1:标记位置偏移

症状:在地图上点了一个位置,记录下经纬度,再次标记发现偏了。

原因:混用了坐标系。地图上点击返回的是 GCJ02,但存储时以为是 WGS84,再次标记时又转换了一次,偏了两次。

方法:统一约定"地图相关操作全部用 GCJ02,GPS 获取的才需要转换"。

踩坑 2:convertCoordinate 是异步的

// 错误:同步调用,结果可能不正确
let gcj02 = map.convertCoordinate(WGS84, GCJ02, position); // 返回 Promise,不是结果

// 正确:等待 Promise
let gcj02 = await map.convertCoordinate(WGS84, GCJ02, position);

提示:注释里也提到了 // the synchronous conversion method here may cause errors——不要尝试同步调用,必须 await

踩坑 3:中国境外不需要转换

GCJ02 偏移只在中国境内地图有效(境外地图和 WGS84 一致)。如果你的应用支持全球定位,需要判断用户是否在中国境内再决定是否转换。

七、可视化理解

想象这样的场景:

真实位置(WGS84):公司大楼门口 [31.9370, 118.8600]
                         ↓ 偏移约200米
GCJ02地图上:          小区内部 [31.9372, 118.8618](大约)

所以用 GPS 坐标直接标在 GCJ02 地图上,你的标记会"飞"到几百米外的地方。转换后才能准确落在实际位置。

总结

坐标系问题是所有在中国做地图应用的开发者必过的坎:

  1. GPS / geoLocationManager → WGS84 坐标
  2. 中国地图(高德、腾讯、华为MapKit) → GCJ02 坐标
  3. 两者之间有 100-300 米的系统性偏移,不转换会导致标记偏位
  4. 使用 map.convertCoordinate(WGS84, GCJ02, position) 转换,记得 await
  5. 手动配置的加油站坐标应直接使用 GCJ02,不需要转换

下一篇讲 Marker 地图标记——怎么给地图上每个加油站加一个自定义图标,点击标记还有弹窗和动画。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐