HarmonyOS 鸿蒙Next中坐标系转换:WGS84、GCJ-02、BD-09 相互转换

HarmonyOS 鸿蒙Next中坐标系转换:WGS84、GCJ-02、BD-09 相互转换 在开发地图应用时,经常遇到坐标偏移问题:

  1. GPS 定位返回的坐标(WGS84)在高德/腾讯地图上显示偏移 100-700 米
  2. 从百度地图获取的坐标在高德地图上显示位置不对
  3. 不同坐标系的数据如何统一处理?
3 回复

原因分析

中国使用三种主要坐标系:

坐标系 别名 使用场景 特点
WGS84 GPS坐标 GPS设备、国际地图 国际标准,无偏移
GCJ-02 火星坐标系 高德、腾讯、谷歌中国 国测局加密,法定坐标系
BD-09 百度坐标系 百度地图 在 GCJ-02 基础上二次加密

为什么会偏移?

出于国家安全考虑,中国要求所有地图服务必须使用 GCJ-02 坐标系。GPS 原始坐标(WGS84)经过非线性加密算法转换后才能在国内地图上正确显示。

GPS设备 → WGS84 → 加密 → GCJ-02 → 高德/腾讯地图
                              ↓
                           二次加密
                              ↓
                           BD-09 → 百度地图

解决方案

1. 创建坐标转换工具类

// common/utils/CoordinateConverter.ets

/**
 * 坐标点接口
 */
export interface Coordinate {
  latitude: number
  longitude: number
}

/**
 * 坐标转换工具类
 * 支持 WGS84、GCJ-02、BD-09 三种坐标系相互转换
 */
export class CoordinateConverter {
  // 椭球参数(克拉索夫斯基椭球)
  private static readonly PI: number = 3.1415926535897932384626
  private static readonly A: number = 6378245.0        // 长半轴
  private static readonly EE: number = 0.00669342162296594323  // 偏心率平方
}

2. 判断是否在中国境内

只有中国境内的坐标才需要转换,境外坐标直接返回原值:

/**
 * 判断坐标是否在中国境外
 * 中国经纬度范围:经度 72°~137°,纬度 0°~55°
 */
private static outOfChina(lat: number, lng: number): boolean {
  return lng < 72.004 || lng > 137.8347 || lat < 0.8293 || lat > 55.8271
}

3. 核心转换算法

/**
 * 转换纬度(加密算法)
 */
private static transformLat(lng: number, lat: number): number {
  let ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat +
    0.1 * lng * lat + 0.2 * Math.sqrt(Math.abs(lng))
  ret += (20.0 * Math.sin(6.0 * lng * this.PI) +
    20.0 * Math.sin(2.0 * lng * this.PI)) * 2.0 / 3.0
  ret += (20.0 * Math.sin(lat * this.PI) +
    40.0 * Math.sin(lat / 3.0 * this.PI)) * 2.0 / 3.0
  ret += (160.0 * Math.sin(lat / 12.0 * this.PI) +
    320 * Math.sin(lat * this.PI / 30.0)) * 2.0 / 3.0
  return ret
}

/**
 * 转换经度(加密算法)
 */
private static transformLng(lng: number, lat: number): number {
  let ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng +
    0.1 * lng * lat + 0.1 * Math.sqrt(Math.abs(lng))
  ret += (20.0 * Math.sin(6.0 * lng * this.PI) +
    20.0 * Math.sin(2.0 * lng * this.PI)) * 2.0 / 3.0
  ret += (20.0 * Math.sin(lng * this.PI) +
    40.0 * Math.sin(lng / 3.0 * this.PI)) * 2.0 / 3.0
  ret += (150.0 * Math.sin(lng / 12.0 * this.PI) +
    300.0 * Math.sin(lng / 30.0 * this.PI)) * 2.0 / 3.0
  return ret
}

4. WGS84 ↔ GCJ-02 转换

/**
 * WGS84 转 GCJ-02(GPS坐标 → 火星坐标)
 * 场景:GPS定位结果用于高德/腾讯地图显示
 * 
 * @param wgsLat WGS84 纬度
 * @param wgsLng WGS84 经度
 * @returns GCJ-02 坐标
 */
static wgs84ToGcj02(wgsLat: number, wgsLng: number): Coordinate {
  // 境外坐标不转换
  if (this.outOfChina(wgsLat, wgsLng)) {
    return { latitude: wgsLat, longitude: wgsLng }
  }

  let dLat = this.transformLat(wgsLng - 105.0, wgsLat - 35.0)
  let dLng = this.transformLng(wgsLng - 105.0, wgsLat - 35.0)
  
  const radLat = wgsLat / 180.0 * this.PI
  let magic = Math.sin(radLat)
  magic = 1 - this.EE * magic * magic
  const sqrtMagic = Math.sqrt(magic)
  
  dLat = (dLat * 180.0) / ((this.A * (1 - this.EE)) / (magic * sqrtMagic) * this.PI)
  dLng = (dLng * 180.0) / (this.A / sqrtMagic * Math.cos(radLat) * this.PI)
  
  return {
    latitude: wgsLat + dLat,
    longitude: wgsLng + dLng
  }
}

/**
 * GCJ-02 转 WGS84(火星坐标 → GPS坐标)
 * 场景:从高德地图获取的坐标转换为标准GPS坐标
 * 
 * @param gcjLat GCJ-02 纬度
 * @param gcjLng GCJ-02 经度
 * @returns WGS84 坐标
 */
static gcj02ToWgs84(gcjLat: number, gcjLng: number): Coordinate {
  if (this.outOfChina(gcjLat, gcjLng)) {
    return { latitude: gcjLat, longitude: gcjLng }
  }

  let dLat = this.transformLat(gcjLng - 105.0, gcjLat - 35.0)
  let dLng = this.transformLng(gcjLng - 105.0, gcjLat - 35.0)
  
  const radLat = gcjLat / 180.0 * this.PI
  let magic = Math.sin(radLat)
  magic = 1 - this.EE * magic * magic
  const sqrtMagic = Math.sqrt(magic)
  
  dLat = (dLat * 180.0) / ((this.A * (1 - this.EE)) / (magic * sqrtMagic) * this.PI)
  dLng = (dLng * 180.0) / (this.A / sqrtMagic * Math.cos(radLat) * this.PI)
  
  return {
    latitude: gcjLat - dLat,
    longitude: gcjLng - dLng
  }
}

5. GCJ-02 ↔ BD-09 转换

/**
 * GCJ-02 转 BD-09(火星坐标 → 百度坐标)
 * 场景:高德地图坐标用于百度地图显示
 */
static gcj02ToBd09(gcjLat: number, gcjLng: number): Coordinate {
  const z = Math.sqrt(gcjLng * gcjLng + gcjLat * gcjLat) +
    0.00002 * Math.sin(gcjLat * this.PI * 3000.0 / 180.0)
  const theta = Math.atan2(gcjLat, gcjLng) +
    0.000003 * Math.cos(gcjLng * this.PI * 3000.0 / 180.0)
  
  return {
    latitude: z * Math.sin(theta) + 0.006,
    longitude: z * Math.cos(theta) + 0.0065
  }
}

/**
 * BD-09 转 GCJ-02(百度坐标 → 火星坐标)
 * 场景:百度地图坐标用于高德地图显示
 */
static bd09ToGcj02(bdLat: number, bdLng: number): Coordinate {
  const x = bdLng - 0.0065
  const y = bdLat - 0.006
  const z = Math.sqrt(x * x + y * y) -
    0.00002 * Math.sin(y * this.PI * 3000.0 / 180.0)
  const theta = Math.atan2(y, x) -
    0.000003 * Math.cos(x * this.PI * 3000.0 / 180.0)
  
  return {
    latitude: z * Math.sin(theta),
    longitude: z * Math.cos(theta)
  }
}

6. WGS84 ↔ BD-09 转换(组合转换)

/**
 * WGS84 转 BD-09(GPS坐标 → 百度坐标)
 * 场景:GPS定位结果用于百度地图显示
 */
static wgs84ToBd09(wgsLat: number, wgsLng: number): Coordinate {
  // 先转 GCJ-02,再转 BD-09
  const gcj02 = this.wgs84ToGcj02(wgsLat, wgsLng)
  return this.gcj02ToBd09(gcj02.latitude, gcj02.longitude)
}

/**
 * BD-09 转 WGS84(百度坐标 → GPS坐标)
 * 场景:百度地图坐标转换为标准GPS坐标
 */
static bd09ToWgs84(bdLat: number, bdLng: number): Coordinate {
  // 先转 GCJ-02,再转 WGS84
  const gcj02 = this.bd09ToGcj02(bdLat, bdLng)
  return this.gcj02ToWgs84(gcj02.latitude, gcj02.longitude)
}

7. 完整工具类代码

// common/utils/CoordinateConverter.ets

export interface Coordinate {
  latitude: number
  longitude: number
}

export class CoordinateConverter {
  private static readonly PI: number = 3.1415926535897932384626
  private static readonly A: number = 6378245.0
  private static readonly EE: number = 0.00669342162296594323

  private static outOfChina(lat: number, lng: number): boolean {
    return lng < 72.004 || lng > 137.8347 || lat < 0.8293 || lat > 55.8271
  }

  private static transformLat(lng: number, lat: number): number {
    let ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat +
      0.1 * lng * lat + 0.2 * Math.sqrt(Math.abs(lng))
    ret += (20.0 * Math.sin(6.0 * lng * this.PI) + 20.0 * Math.sin(2.0 * lng * this.PI)) * 2.0 / 3.0
    ret += (20.0 * Math.sin(lat * this.PI) + 40.0 * Math.sin(lat / 3.0 * this.PI)) * 2.0 / 3.0
    ret += (160.0 * Math.sin(lat / 12.0 * this.PI) + 320 * Math.sin(lat * this.PI / 30.0)) * 2.0 / 3.0
    return ret
  }

  private static transformLng(lng: number, lat: number): number {
    let ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng +
      0.1 * lng * lat + 0.1 * Math.sqrt(Math.abs(lng))
    ret += (20.0 * Math.sin(6.0 * lng * this.PI) + 20.0 * Math.sin(2.0 * lng * this.PI)) * 2.0 / 3.0
    ret += (20.0 * Math.sin(lng * this.PI) + 40.0 * Math.sin(lng / 3.0 * this.PI)) * 2.0 / 3.0
    ret += (150.0 * Math.sin(lng / 12.0 * this.PI) + 300.0 * Math.sin(lng / 30.0 * this.PI)) * 2.0 / 3.0
    return ret
  }

  // WGS84 → GCJ-02
  static wgs84ToGcj02(wgsLat: number, wgsLng: number): Coordinate {
    if (this.outOfChina(wgsLat, wgsLng)) {
      return { latitude: wgsLat, longitude: wgsLng }
    }
    let dLat = this.transformLat(wgsLng - 105.0, wgsLat - 35.0)
    let dLng = this.transformLng(wgsLng - 105.0, wgsLat - 35.0)
    const radLat = wgsLat / 180.0 * this.PI
    let magic = Math.sin(radLat)
    magic = 1 - this.EE * magic * magic
    const sqrtMagic = Math.sqrt(magic)
    dLat = (dLat * 180.0) / ((this.A * (1 - this.EE)) / (magic * sqrtMagic) * this.PI)
    dLng = (dLng * 180.0) / (this.A / sqrtMagic * Math.cos(radLat) * this.PI)
    return { latitude: wgsLat + dLat, longitude: wgsLng + dLng }
  }

  // GCJ-02 → WGS84
  static gcj02ToWgs84(gcjLat: number, gcjLng: number): Coordinate {
    if (this.outOfChina(gcjLat, gcjLng)) {
      return { latitude: gcjLat, longitude: gcjLng }
    }
    let dLat = this.transformLat(gcjLng - 105.0, gcjLat - 35.0)
    let dLng = this.transformLng(gcjLng - 105.0, gcjLat - 35.0)
    const radLat = gcjLat / 180.0 * this.PI
    let magic = Math.sin(radLat)
    magic = 1 - this.EE * magic * magic
    const sqrtMagic = Math.sqrt(magic)
    dLat = (dLat * 180.0) / ((this.A * (1 - this.EE)) / (magic * sqrtMagic) * this.PI)
    dLng = (dLng * 180.0) / (this.A / sqrtMagic * Math.cos(radLat) * this.PI)
    return { latitude: gcjLat - dLat, longitude: gcjLng - dLng }
  }

  // GCJ-02 → BD-09
  static gcj02ToBd09(gcjLat: number, gcjLng: number): Coordinate {
    const z = Math.sqrt(gcjLng * gcjLng + gcjLat * gcjLat) +
      0.00002 * Math.sin(gcjLat * this.PI * 3000.0 / 180.0)
    const theta = Math.atan2(gcjLat, gcjLng) +
      0.000003 * Math.cos(gcjLng * this.PI * 3000.0 / 180.0)
    return { latitude: z * Math.sin(theta) + 0.006, longitude: z * Math.cos(theta) + 0.0065 }
  }

  // BD-09 → GCJ-02
  static bd09ToGcj02(bdLat: number, bdLng: number): Coordinate {
    const x = bdLng - 0.0065
    const y = bdLat - 0.006
    const z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * this.PI * 3000.0 / 180.0)
    const theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * this.PI * 3000.0 / 180.0)
    return { latitude: z * Math.sin(theta), longitude: z * Math.cos(theta) }
  }

  // WGS84 → BD-09
  static wgs84ToBd09(wgsLat: number, wgsLng: number): Coordinate {
    const gcj02 = this.wgs84ToGcj02(wgsLat, wgsLng)
    return this.gcj02ToBd09(gcj02.latitude, gcj02.longitude)
  }

  // BD-09 → WGS84
  static bd09ToWgs84(bdLat: number, bdLng: number): Coordinate {
    const gcj02 = this.bd09ToGcj02(bdLat, bdLng)
    return this.gcj02ToWgs84(gcj02.latitude, gcj02.longitude)
  }
}

8. 使用示例

// 场景1: GPS定位结果用于高德地图
import geoLocationManager from '[@ohos](/user/ohos).geoLocationManager'
import { CoordinateConverter } from '../common/utils/CoordinateConverter'

// GPS 定位回调
const locationCallback = (location: geoLocationManager.Location) => {
  console.info(`GPS原始坐标(WGS84): ${location.latitude}, ${location.longitude}`)
  
  // 转换为 GCJ

更多关于HarmonyOS 鸿蒙Next中坐标系转换:WGS84、GCJ-02、BD-09 相互转换的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


在HarmonyOS Next中,坐标系转换可通过@ohos.geoLocationManager模块实现。WGS84、GCJ-02、BD-09之间的转换主要依赖geoLocationManager.convert方法,传入源坐标、目标坐标系类型(如geoLocationManager.CoordinateSystemType.GCJ02)即可获取转换结果。系统已内置转换算法,无需引入第三方库。注意坐标参数需符合标准格式,转换精度符合国家测绘要求。

在HarmonyOS Next中处理坐标系转换,核心是理解三种坐标系的差异并实现精确算法。

1. 坐标系解析

  • WGS84:GPS原始坐标系,国际标准
  • GCJ-02:中国国测局加密坐标系(高德、腾讯使用)
  • BD-09:百度在GCJ-02基础上二次加密的坐标系

2. 转换实现方案

方案A:使用系统定位能力

// 获取原始WGS84坐标
import geoLocationManager from '@ohos.geoLocationManager';

let location = await geoLocationManager.getCurrentLocation();
// 系统会自动处理坐标系适配

方案B:纯算法转换(需自行实现)

// WGS84转GCJ-02核心算法示例
class CoordinateConverter {
  static transformWGS84ToGCJ02(wgsLat: number, wgsLon: number): [number, number] {
    const a = 6378245.0;  // 长半轴
    const ee = 0.00669342162296594323;  // 偏心率平方
    
    // 偏移量计算逻辑
    let dLat = this.transformLat(wgsLon - 105.0, wgsLat - 35.0);
    let dLon = this.transformLon(wgsLon - 105.0, wgsLat - 35.0);
    
    const radLat = wgsLat / 180.0 * Math.PI;
    let magic = Math.sin(radLat);
    magic = 1 - ee * magic * magic;
    
    const sqrtMagic = Math.sqrt(magic);
    dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * Math.PI);
    dLon = (dLon * 180.0) / (a / sqrtMagic * Math.cos(radLat) * Math.PI);
    
    return [wgsLat + dLat, wgsLon + dLon];
  }
  
  // BD-09与GCJ-02转换(线性变换)
  static transformGCJ02ToBD09(gcjLat: number, gcjLon: number): [number, number] {
    const z = Math.sqrt(gcjLon * gcjLon + gcjLat * gcjLat) + 0.00002 * Math.sin(gcjLat * Math.PI * 3000.0 / 180.0);
    const theta = Math.atan2(gcjLat, gcjLon) + 0.000003 * Math.cos(gcjLon * Math.PI * 3000.0 / 180.0);
    
    return [z * Math.sin(theta) + 0.006, z * Math.cos(theta) + 0.0065];
  }
}

3. 实际应用建议

地图SDK选择

  • 高德地图:直接使用GCJ-02坐标
  • 百度地图:使用BD-09坐标
  • 系统地图:自动适配

数据处理策略

  1. 存储时统一为WGS84坐标系
  2. 显示时根据使用的地图SDK实时转换
  3. 跨平台数据交换时注明坐标系类型

性能优化

  • 批量坐标转换使用Worker线程
  • 缓存常用区域的转换结果
  • 使用Float32Array处理大量坐标数据

4. 精度控制 转换误差主要来自:

  • 算法实现精度(建议使用double精度计算)
  • 加密参数版本差异
  • 不同地区的偏移参数

建议在实际使用区域进行精度验证,必要时加入区域化校正参数。

注意事项

  1. 转换算法涉及国家安全规定,需确保合规使用
  2. 商业应用建议直接使用各厂商提供的地图SDK
  3. 海外应用通常只需处理WGS84坐标系

通过合理选择转换方案和地图SDK,可以有效解决HarmonyOS Next应用中的坐标系偏移问题。

回到顶部