经纬度在 WGS-84/GCJ-02/BD09 坐标系之间的转换

国内各大地图厂商SDK都会内置了一些转换函数以供使用,但是有时候我们的小项目并不需要引入这些庞大的SDK,也没去申请相关授权key(毕竟现在商用都是收费了,费用还不低呢)。

下面整理了一些经纬度在不同的地理坐标系之间相互转换的方法,提供 Objective-C 和 TypeScript 版本。

Objective-C 版本

苹果自带的地图功能,在中国使用的是由高德地图所提供的数据,在国外则是由 TomTom、OpenStreetMap 等第三方公司提供数据。

使用自带的 CoreLocationMapKit 框架,需要注意返回的经纬度所采用的坐标系,CoreLocation 返回的是 WGS-84 坐标系,而 MapKit 需要的是 GCJ-02 坐标系。

#import <Foundation/Foundation.h>
#import <CoreLocation/CoreLocation.h>

@interface XPCoordinateConverter : NSObject

/// 世界标准地理坐标转成中国国测局地理坐标(WGS-84 -> GCJ-02)
+ (CLLocationCoordinate2D)wgs84ToGcj02:(CLLocationCoordinate2D)coordinate;

/// 中国国测局地理坐标转成世界标准地理坐标(GCJ-02 -> WGS-84)
+ (CLLocationCoordinate2D)gcj02ToWgs84:(CLLocationCoordinate2D)coordinate;

/// 世界标准地理坐标转成百度地理坐标(WGS-84 -> BD09)
+ (CLLocationCoordinate2D)wgs84ToBd09:(CLLocationCoordinate2D)coordinate;

/// 百度地理坐标转成世界标准地理坐标(BD09 -> WGS-84)
+ (CLLocationCoordinate2D)bd09ToWgs84:(CLLocationCoordinate2D)coordinate;

/// 中国国测局地理坐标转成百度地理坐标(GCJ-02 -> BD09)
+ (CLLocationCoordinate2D)gcj02ToBd09:(CLLocationCoordinate2D)coordinate;

/// 百度地理坐标转成中国国测局地理坐标(BD09 -> GCJ-02)
+ (CLLocationCoordinate2D)bd09ToGcj02:(CLLocationCoordinate2D)coordinate;

@end
#import "XPCoordinateConverter.h"

#define CC_X_PI     (3.14159265358979324 * 3000.0 / 180.0)
#define CC_PI       (3.1415926535897932384626)
#define CC_A        (6378245.0)
#define CC_EE       (0.00669342162296594323)

double cc_transformlat(double lng, double lat) {
    double ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + 0.1 * lng * lat + 0.2 * sqrt(fabs(lng));
    ret += (20.0 * sin(6.0 * lng * CC_PI) + 20.0 * sin(2.0 * lng * CC_PI)) * 2.0 / 3.0;
    ret += (20.0 * sin(lat * CC_PI) + 40.0 * sin(lat / 3.0 * CC_PI)) * 2.0 / 3.0;
    ret += (160.0 * sin(lat / 12.0 * CC_PI) + 320 * sin(lat * CC_PI / 30.0)) * 2.0 / 3.0;
    return ret;
}

double cc_transformlng(double lng, double lat) {
    double ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + 0.1 * lng * lat + 0.1 * sqrt(fabs(lng));
    ret += (20.0 * sin(6.0 * lng * CC_PI) + 20.0 * sin(2.0 * lng * CC_PI)) * 2.0 / 3.0;
    ret += (20.0 * sin(lng * CC_PI) + 40.0 * sin(lng / 3.0 * CC_PI)) * 2.0 / 3.0;
    ret += (150.0 * sin(lng / 12.0 * CC_PI) + 300.0 * sin(lng / 30.0 * CC_PI)) * 2.0 / 3.0;
    return ret;
}

BOOL cc_location_in_china(CLLocationCoordinate2D coordinate) {
    if (coordinate.longitude < 72.004 ||
        coordinate.longitude > 137.8347 ||
        coordinate.latitude < 0.8293 ||
        coordinate.latitude > 55.8271) {
        return NO;
    }
    return YES;
}

@implementation XPCoordinateConverter

/// 世界标准地理坐标转成中国国测局地理坐标(WGS-84 -> GCJ-02)
+ (CLLocationCoordinate2D)wgs84ToGcj02:(CLLocationCoordinate2D)coordinate {
    double lng = coordinate.longitude;
    double lat = coordinate.latitude;
    double dlat = cc_transformlat(lng - 105.0, lat - 35.0);
    double dlng = cc_transformlng(lng - 105.0, lat - 35.0);
    double radlat = lat / 180.0 * CC_PI;
    double magic = sin(radlat);
    magic = 1 - CC_EE * magic * magic;
    double sqrtmagic = sqrt(magic);
    dlat = (dlat * 180.0) / ((CC_A * (1 - CC_EE)) / (magic * sqrtmagic) * CC_PI);
    dlng = (dlng * 180.0) / (CC_A / sqrtmagic * cos(radlat) * CC_PI);
    return CLLocationCoordinate2DMake(lat + dlat, lng + dlng);
}

/// 中国国测局地理坐标转成世界标准地理坐标(GCJ-02 -> WGS-84)
+ (CLLocationCoordinate2D)gcj02ToWgs84:(CLLocationCoordinate2D)coordinate {
    double lng = coordinate.longitude;
    double lat = coordinate.latitude;
    double dlat = cc_transformlat(lng - 105.0, lat - 35.0);
    double dlng = cc_transformlng(lng - 105.0, lat - 35.0);
    double radlat = lat / 180.0 * CC_PI;
    double magic = sin(radlat);
    magic = 1 - CC_EE * magic * magic;
    double sqrtmagic = sqrt(magic);
    dlat = (dlat * 180.0) / ((CC_A * (1 - CC_EE)) / (magic * sqrtmagic) * CC_PI);
    dlng = (dlng * 180.0) / (CC_A / sqrtmagic * cos(radlat) * CC_PI);
    return CLLocationCoordinate2DMake(lat * 2 - (lat + dlat), lng * 2 - (lng + dlng));
}

/// 世界标准地理坐标转成百度地理坐标(WGS-84 -> BD09)
+ (CLLocationCoordinate2D)wgs84ToBd09:(CLLocationCoordinate2D)coordinate {
    coordinate = [self wgs84ToGcj02:coordinate];
    return [self gcj02ToBd09:coordinate];
}

/// 百度地理坐标转成世界标准地理坐标(BD09 -> WGS-84)
+ (CLLocationCoordinate2D)bd09ToWgs84:(CLLocationCoordinate2D)coordinate {
    coordinate = [self bd09ToGcj02:coordinate];
    return [self gcj02ToWgs84:coordinate];
}

/// 中国国测局地理坐标转成百度地理坐标(GCJ-02 -> BD09)
+ (CLLocationCoordinate2D)gcj02ToBd09:(CLLocationCoordinate2D)coordinate {
    CLLocationCoordinate2D bd09Coordinate;
    double x = coordinate.longitude;
    double y = coordinate.latitude;
    double z = sqrt(x * x + y * y) + 0.00002 * sin(y * CC_X_PI);
    double theta = atan2(y, x) + 0.000003 * cos(x * CC_X_PI);
    bd09Coordinate.longitude = z * cos(theta) + 0.0065;
    bd09Coordinate.latitude = z * sin(theta) + 0.006;
    return bd09Coordinate;
}

/// 百度地理坐标转成中国国测局地理坐标(BD09 -> GCJ-02)
+ (CLLocationCoordinate2D)bd09ToGcj02:(CLLocationCoordinate2D)coordinate {
    double x = coordinate.longitude - 0.0065;
    double y = coordinate.latitude - 0.006;
    double z = sqrt(x * x + y * y) - 0.00002 * sin(y * CC_X_PI);
    double theta = atan2(y, x) - 0.000003 * cos(x * CC_X_PI);
    return CLLocationCoordinate2DMake(z * sin(theta), z * cos(theta));
}

@end

TypeScript 版本

const CC_X_PI   = 3.14159265358979324 * 3000.0 / 180.0
const CC_PI     = 3.1415926535897932384626
const CC_A      = 6378245.0
const CC_EE     = 0.00669342162296594323

function cc_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 * CC_PI) + 20.0 * Math.sin(2.0 * lng * CC_PI)) * 2.0 / 3.0
    ret += (20.0 * Math.sin(lat * CC_PI) + 40.0 * Math.sin(lat / 3.0 * CC_PI)) * 2.0 / 3.0
    ret += (160.0 * Math.sin(lat / 12.0 * CC_PI) + 320 * Math.sin(lat * CC_PI / 30.0)) * 2.0 / 3.0
    return ret
}

function cc_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 * CC_PI) + 20.0 * Math.sin(2.0 * lng * CC_PI)) * 2.0 / 3.0
    ret += (20.0 * Math.sin(lng * CC_PI) + 40.0 * Math.sin(lng / 3.0 * CC_PI)) * 2.0 / 3.0
    ret += (150.0 * Math.sin(lng / 12.0 * CC_PI) + 300.0 * Math.sin(lng / 30.0 * CC_PI)) * 2.0 / 3.0
    return ret
}

export type LocationDegrees = number | string

export interface LocationCoordinate2D {
    longitude: LocationDegrees
    latitude: LocationDegrees
}

export function LocationCoordinate2DMake(longitude: LocationDegrees, latitude: LocationDegrees): LocationCoordinate2D {
    return { longitude, latitude }
}

export function getLocationDegreesNumber(degrees: LocationDegrees): number {
    if (typeof degrees === 'number') return degrees as number
    return parseFloat(degrees)
}

export default class CoordinateConverter {
    
    static wgs84ToGcj02(coordinate: LocationCoordinate2D): LocationCoordinate2D {
        let lng = getLocationDegreesNumber(coordinate.longitude)
        let lat = getLocationDegreesNumber(coordinate.latitude)
        let dlat = cc_transformlat(lng - 105.0, lat - 35.0)
        let dlng = cc_transformlng(lng - 105.0, lat - 35.0)
        let radlat = lat / 180.0 * CC_PI
        let magic = Math.sin(radlat)
        magic = 1 - CC_EE * magic * magic
        let sqrtmagic = Math.sqrt(magic)
        dlat = (dlat * 180.0) / ((CC_A * (1 - CC_EE)) / (magic * sqrtmagic) * CC_PI)
        dlng = (dlng * 180.0) / (CC_A / sqrtmagic * Math.cos(radlat) * CC_PI)
        return LocationCoordinate2DMake(lng + dlng, lat + dlat)
    }

    static gcj02ToWgs84(coordinate: LocationCoordinate2D): LocationCoordinate2D {
        let lng = getLocationDegreesNumber(coordinate.longitude)
        let lat = getLocationDegreesNumber(coordinate.latitude)
        let dlat = cc_transformlat(lng - 105.0, lat - 35.0)
        let dlng = cc_transformlng(lng - 105.0, lat - 35.0)
        let radlat = lat / 180.0 * CC_PI
        let magic = Math.sin(radlat)
        magic = 1 - CC_EE * magic * magic
        let sqrtmagic = Math.sqrt(magic)
        dlat = (dlat * 180.0) / ((CC_A * (1 - CC_EE)) / (magic * sqrtmagic) * CC_PI)
        dlng = (dlng * 180.0) / (CC_A / sqrtmagic * Math.cos(radlat) * CC_PI)
        return LocationCoordinate2DMake(lng * 2 - (lng + dlng), lat * 2 - (lat + dlat))
    }

    static wgs84ToBd09(coordinate: LocationCoordinate2D): LocationCoordinate2D {
        return this.gcj02ToBd09(this.wgs84ToGcj02(coordinate))
    }

    static bd09ToWgs84(coordinate: LocationCoordinate2D): LocationCoordinate2D {
        return this.gcj02ToWgs84(this.bd09ToGcj02(coordinate))
    }

    static gcj02ToBd09(coordinate: LocationCoordinate2D): LocationCoordinate2D {
        const x = getLocationDegreesNumber(coordinate.longitude)
        const y = getLocationDegreesNumber(coordinate.latitude)
        const z = Math.sqrt(x * x + y * y) + 0.00002 * Math.sin(y * CC_X_PI)
        const theta = Math.atan2(y, x) + 0.000003 * Math.cos(x * CC_X_PI)
        const longitude = z * Math.cos(theta) + 0.0065
        const latitude = z * Math.sin(theta) + 0.006
        return LocationCoordinate2DMake(longitude, latitude)
    }

    static bd09ToGcj02(coordinate: LocationCoordinate2D): LocationCoordinate2D {
        const x = getLocationDegreesNumber(coordinate.longitude) - 0.0065
        const y = getLocationDegreesNumber(coordinate.latitude) - 0.006
        const z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * CC_X_PI)
        const theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * CC_X_PI)
        const longitude = z * Math.cos(theta)
        const latitude = z * Math.sin(theta)
        return LocationCoordinate2DMake(longitude, latitude)
    }
}

调用示例:

<script setup lang="ts">
import { onMounted } from 'vue'
import CoordinateConverter, { LocationCoordinate2DMake } from './CoordinateConverter'

onMounted(() => {
  const wgs84Coordinate = LocationCoordinate2DMake(114.060239, 22.530405)
  console.log('==> gcj02:', CoordinateConverter.wgs84ToGcj02(wgs84Coordinate))
  console.log('==> bd09:', CoordinateConverter.wgs84ToBd09(wgs84Coordinate))
})

</script>

参考

感谢 ni1o1/CoordinatesConverter 开源项目,该项目的坐标转换结果相对来说还是比较准确的。