适用版本:ElaneMap H5 API 3.5
适用读者:接入船讯网海图 GIS 平台的前端 / 全栈开发人员
官方示例:https://api.shipxy.com/h5s/api/3.5/demo/
在接入 ShipXY 海图 GIS 平台时,很多开发者会遇到一个令人困惑的问题:同一个经纬度坐标,在地图模式和海图模式下显示的位置不一致,存在几十到几百米的偏移。
这不是 Bug,而是中国地理坐标系的特殊性导致的。国内法律要求所有在中国境内发布的地理信息必须经过加密偏移处理(即 GCJ-02 火星坐标系),而国际通用的 GPS 坐标(WGS84)和电子海图坐标均为未加密的标准坐标。ShipXY 平台同时使用多种瓦片图源,不同图源采用的坐标系不同,因此在开发时需要根据当前地图模式进行坐标转换。
一句话总结:你的船舶 AIS 数据是 WGS84 坐标,在地图/卫星图模式下需要转换为 GCJ-02 才能正确显示在中国地区的底图上;在海图模式下则直接使用 WGS84 即可。
| 属性 | 说明 |
|---|---|
| 全称 | World Geodetic System 1984 |
| 定义者 | 美国国防部 / NGA |
| 用途 | GPS 全球定位、国际海事电子海图(ENC)、国际航空 |
| 精度 | 厘米级(精确定位)/ 米级(民用 GPS) |
| 加密 | 无加密,原始坐标 |
ShipXY 场景:船舶 AIS 上报的位置数据、历史轨迹数据、港口坐标等业务数据均使用 WGS84 坐标系。
| 属性 | 说明 |
|---|---|
| 全称 | GCJ-02(俗称"火星坐标系") |
| 定义者 | 中国国家测绘地理信息局 |
| 用途 | 中国境内所有公开发布的电子地图 |
| 加密 | 基于 WGS84 的非线性加密偏移,偏移量约 50~500 米 |
| 特点 | 偏移量随地理位置非线性变化,无法通过简单加减常数还原 |
ShipXY 场景:地图模式(谷歌地图瓦片)和卫星图模式(卫星影像瓦片)在中国地区使用的底图瓦片为 GCJ-02 坐标系。
在 ShipXY 平台中,我们仅关注 WGS84 → GCJ-02 的单向转换(用于将业务数据适配到地图/卫星图底图)。
ShipXY ElaneMap H5 API 3.5 提供三种地图底图模式,不同模式下坐标系要求不同:
| 地图模式 | 底图图源 | 适用区域坐标系 | 是否需要转换 | 转换方向 |
|---|---|---|---|---|
| 地图模式 | 谷歌/类似地图瓦片 | GCJ-02(中国陆地及沿海) | ✅ 需要 | WGS84 → GCJ-02 |
| 卫星图模式 | 卫星影像瓦片 | GCJ-02(中国陆地及沿海) | ✅ 需要 | WGS84 → GCJ-02 |
| 海图模式 | 电子海图瓦片 | WGS84 | ❌ 不需要 | — |
关键结论:
- 地图模式 + 卫星图模式:中国陆地及沿海地区的底图瓦片使用 GCJ-02 坐标系,如果你的业务数据(如船位)是 WGS84,必须转换为 GCJ-02 后再标注到地图上,否则点位会偏移 50~500 米。
- 海图模式:电子海图遵循国际海事标准(IHO S-57/S-101),底图瓦片使用 WGS84 坐标系,业务数据直接使用即可,无需转换。
- 适用范围说明:需要转换的区域为中国陆地范围边界及沿海地区,不包含南海远洋区域。远洋及公海区域的坐标在所有模式下均使用 WGS84,无需转换。
法律要求:中国《测绘法》规定,在中国境内公开发布的地图必须使用 GCJ-02 加密坐标系。ShipXY 地图模式和卫星图模式使用的瓦片图源(如谷歌地图瓦片)在中国陆地及沿海区域已按法规要求进行了 GCJ-02 加密偏移处理。
瓦片与数据不匹配:你的船舶 AIS 数据来自卫星定位,是标准 WGS84 坐标。如果直接将 WGS84 坐标标注在 GCJ-02 的底图上,由于底图本身有非线性偏移,标注点会出现肉眼可见的位置偏差(尤其在中国沿海和内陆地区)。
偏移量示例:
| 城市 | WGS84 经纬度 | GCJ-02 经纬度 | 纬度偏移 | 经度偏移 | 实际距离偏移 |
|---|---|---|---|---|---|
| 上海 | 31.2304°N, 121.4737°E | 31.2278°N, 121.4784°E | -0.0026° | +0.0047° | ~490 米 |
| 北京 | 39.9042°N, 116.4074°E | 39.9056°N, 116.4132°E | +0.0014° | +0.0058° | ~490 米 |
| 深圳 | 22.5431°N, 114.0579°E | 22.5405°N, 114.0627°E | -0.0026° | +0.0048° | ~460 米 |
| 大连 | 38.9140°N, 121.6147°E | 38.9117°N, 121.6199°E | -0.0023° | +0.0052° | ~470 米 |
可以看到,在中国主要港口城市,WGS84 与 GCJ-02 的偏移量约为 460~490 米。对于船舶监控这种对位置精度要求较高的场景,这个偏移是不可接受的。
国际海事标准:电子海图(Electronic Navigational Chart, ENC)遵循国际海道测量组织(IHO)的 S-57/S-101 标准,该标准强制要求使用 WGS84 坐标系。
船舶导航需求:电子海图用于船舶实际导航,坐标精度直接关系航行安全,不允许有任何加密偏移。
ShipXY 海图瓦片:ShipXY 的海图底图瓦片源为国际标准电子海图,使用 WGS84 坐标系,因此你的 WGS84 业务数据可以直接标注,无需任何转换。
┌─────────────────────────────┐
│ 开始:获得一个 WGS84 坐标 │
│ (来自 AIS / GPS / 业务数据) │
└──────────────┬──────────────┘
│
▼
┌─────────────────────────────┐
│ 当前地图是什么模式? │
└──────────────┬──────────────┘
│
┌─────────────────┼─────────────────┐
│ │ │
▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌────────────┐
│ 地图模式 │ │ 卫星图模式 │ │ 海图模式 │
└─────┬──────┘ └─────┬──────┘ └─────┬──────┘
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 坐标在中国 │ │ 坐标在中国 │ │ 直接使用 │
│ 陆地及沿海? │ │ 陆地及沿海? │ │ WGS84 坐标 │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
┌──────┴──────┐ ┌──────┴──────┐ │
│ │ │ │ │
▼ ▼ ▼ ▼ │
┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
│ 是 │ │ 否 │ │ 是 │ │ 否 │ │
└──┬───┘ └──┬───┘ └──┬───┘ └──┬───┘ │
│ │ │ │ │
▼ ▼ ▼ ▼ │
┌──────────┐ ┌──────┐ ┌──────────┐ ┌──────┐ │
│WGS84→ │ │直接 │ │WGS84→ │ │直接 │ │
│GCJ-02 │ │使用 │ │GCJ-02 │ │使用 │ │
└────┬─────┘ └──┬───┘ └────┬─────┘ └──┬───┘ │
│ │ │ │ │
└──────────┴──────────┴─────────┴─────────┘
│
▼
┌─────────────────────┐
│ 标注到 ShipXY 地图 │
└─────────────────────┘
并非所有坐标都需要转换——只有落在中国陆地及沿海地区的坐标才需要。南海远洋区域、公海、其他国家港口的坐标在所有模式下均使用 WGS84,无需转换。
判定方法采用经纬度边界框检查(Bounding Box):
/**
* 判断坐标是否在中国陆地及沿海地区(不含南海远洋区域)
* @param {number} lng - 经度
* @param {number} lat - 纬度
* @returns {boolean} - true 表示在转换范围内(需要转换),false 表示在范围外(无需转换)
*/
function isInChina(lng, lat) {
// 中国陆地及沿海地区大致经纬度范围
// 纬度下限 18.0:覆盖海南岛(18.2°N)及北部湾,排除南海深处远洋区域
// 纬度上限 53.55:覆盖中国最北端(漠河约 53.5°N)
// 经度范围 73.66~135.05:覆盖中国全境
return lng > 73.66 && lng < 135.05 && lat > 18.0 && lat < 53.55;
}
范围说明:
- 本判定覆盖中国陆地领土及沿海近岸水域,包括海南岛、北部湾等近海区域。
- 不包含南海远洋区域(纬度低于 18°N 的南海深处)及其他远洋区域。这些区域的中国境外/远洋坐标无需转换。
- 这是一个粗略的矩形边界框判定。对于更精确的陆地边界匹配,可引入 GeoJSON 行政区划数据进行点-多边形检测。
GCJ-02 加密算法基于一组非线性偏移函数。核心思想是对 WGS84 坐标施加一个与位置相关的偏移量,得到 GCJ-02 坐标。
偏移计算公式:
K = 偏移参数常量组
x = 经度, y = 纬度
transformLat(x, y):
ret = -100.0 + 2.0*x + 3.0*y + 0.2*y*y + 0.1*x*y + 0.2*sqrt(|x|)
ret += (20.0*sin(6.0*x*π) + 20.0*sin(2.0*x*π)) * 2.0/3.0
ret += (20.0*sin(y*π) + 40.0*sin(y/3.0*π)) * 2.0/3.0
ret += (160.0*sin(y/12.0*π) + 320.0*sin(y*π/30.0)) * 2.0/3.0
return ret
transformLng(x, y):
ret = 300.0 + x + 2.0*y + 0.1*x*x + 0.1*x*y + 0.1*sqrt(|x|)
ret += (20.0*sin(6.0*x*π) + 20.0*sin(2.0*x*π)) * 2.0/3.0
ret += (20.0*sin(x*π) + 40.0*sin(x/3.0*π)) * 2.0/3.0
ret += (150.0*sin(x/12.0*π) + 300.0*sin(x/30.0*π)) * 2.0/3.0
return ret
dLat = transformLat(x - 105.0, y - 35.0)
dLng = transformLng(x - 105.0, y - 35.0)
radLat = y / 180.0 * π
magic = sin(radLat)
magic = 1 - ee * magic * magic // ee = 0.00669342162296594323
sqrtMagic = sqrt(magic)
dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * π)
dLng = (dLng * 180.0) / (a / sqrtMagic * cos(radLat) * π)
// a = 6378245.0 (克拉索夫斯基椭球体长半轴)
GCJ-02 纬度 = y + dLat
GCJ-02 经度 = x + dLng
这是最常用的版本,直接在前端页面中使用,与 ShipxyAPI 无缝配合。
/**
* ============================================================
* ShipXY 坐标系转换工具 (JavaScript)
* 支持 WGS84 → GCJ-02 单向转换
* ============================================================
*/
var CoordTransform = (function () {
// 克拉索夫斯基椭球体参数
var a = 6378245.0; // 长半轴
var ee = 0.00669342162296594323; // 偏心率平方
/**
* 判断坐标是否在中国陆地及沿海地区(不含南海远洋区域)
*/
function isInChina(lng, lat) {
return lng > 73.66 && lng < 135.05 && lat > 18.0 && lat < 53.55;
}
/**
* 纬度偏移变换
*/
function transformLat(x, y) {
var ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y
+ 0.1 * x * y + 0.2 * Math.sqrt(Math.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;
}
/**
* 经度偏移变换
*/
function transformLng(x, y) {
var ret = 300.0 + x + 2.0 * y + 0.1 * x * x
+ 0.1 * x * y + 0.1 * Math.sqrt(Math.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;
}
/**
* WGS84 → GCJ-02(火星坐标系)
* 仅对中国陆地及沿海地区的坐标进行转换,其他区域原样返回
* @param {number} lng - WGS84 经度
* @param {number} lat - WGS84 纬度
* @returns {{lng: number, lat: number}} GCJ-02 坐标
*/
function wgs84ToGcj02(lng, lat) {
// 中国陆地及沿海地区以外不转换
if (!isInChina(lng, lat)) {
return { lng: lng, lat: lat };
}
var dLat = transformLat(lng - 105.0, lat - 35.0);
var dLng = transformLng(lng - 105.0, lat - 35.0);
var radLat = lat / 180.0 * Math.PI;
var magic = Math.sin(radLat);
magic = 1 - ee * magic * magic;
var 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);
return {
lng: lng + dLng,
lat: lat + dLat
};
}
return {
isInChina: isInChina,
wgs84ToGcj02: wgs84ToGcj02
};
})();
适用于后端对 AIS 数据进行批量坐标转换后再返回前端的场景。
"""
ShipXY 坐标系转换工具 (Python)
支持 WGS84 -> GCJ-02 单向转换
"""
import math
# 克拉索夫斯基椭球体参数
_a = 6378245.0 # 长半轴
_ee = 0.00669342162296594323 # 偏心率平方
def is_in_china(lng: float, lat: float) -> bool:
"""判断坐标是否在中国陆地及沿海地区(不含南海远洋区域)"""
return 73.66 < lng < 135.05 and 18.0 < lat < 53.55
def _transform_lat(x: float, y: float) -> float:
"""纬度偏移变换"""
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: float, y: float) -> float:
"""经度偏移变换"""
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
def wgs84_to_gcj02(lng: float, lat: float) -> tuple:
"""
WGS84 -> GCJ-02 (火星坐标系)
仅对中国陆地及沿海地区的坐标进行转换,其他区域原样返回
返回 (gcj02_lng, gcj02_lat)
"""
if not is_in_china(lng, lat):
return (lng, lat)
d_lat = _transform_lat(lng - 105.0, lat - 35.0)
d_lng = _transform_lng(lng - 105.0, lat - 35.0)
rad_lat = lat / 180.0 * math.pi
magic = math.sin(rad_lat)
magic = 1 - _ee * magic * magic
sqrt_magic = math.sqrt(magic)
d_lat = (d_lat * 180.0) / ((_a * (1 - _ee)) / (magic * sqrt_magic) * math.pi)
d_lng = (d_lng * 180.0) / (_a / sqrt_magic * math.cos(rad_lat) * math.pi)
return (lng + d_lng, lat + d_lat)
# ===================== 使用示例 =====================
if __name__ == "__main__":
# 上海港 WGS84 坐标
wgs_lng, wgs_lat = 121.4737, 31.2304
gcj_lng, gcj_lat = wgs84_to_gcj02(wgs_lng, wgs_lat)
print(f"WGS84: ({wgs_lng}, {wgs_lat})")
print(f"GCJ-02: ({gcj_lng:.6f}, {gcj_lat:.6f})")
print(f"偏移: 经度 {gcj_lng - wgs_lng:.6f}°, 纬度 {gcj_lat - wgs_lat:.6f}°")
适用于 Java Spring Boot 等企业级后端服务。
/**
* ShipXY 坐标系转换工具 (Java)
* 支持 WGS84 -> GCJ-02 单向转换
*/
public class CoordTransform {
private static final double A = 6378245.0;
private static final double EE = 0.00669342162296594323;
private static final double PI = Math.PI;
/**
* 判断坐标是否在中国陆地及沿海地区(不含南海远洋区域)
*/
public static boolean isInChina(double lng, double lat) {
return lng > 73.66 && lng < 135.05 && lat > 18.0 && lat < 53.55;
}
private static double transformLat(double x, double y) {
double ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y
+ 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x));
ret += (20.0 * Math.sin(6.0 * x * PI)
+ 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0;
ret += (20.0 * Math.sin(y * PI)
+ 40.0 * Math.sin(y / 3.0 * PI)) * 2.0 / 3.0;
ret += (160.0 * Math.sin(y / 12.0 * PI)
+ 320.0 * Math.sin(y * PI / 30.0)) * 2.0 / 3.0;
return ret;
}
private static double transformLng(double x, double y) {
double ret = 300.0 + x + 2.0 * y + 0.1 * x * x
+ 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x));
ret += (20.0 * Math.sin(6.0 * x * PI)
+ 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0;
ret += (20.0 * Math.sin(x * PI)
+ 40.0 * Math.sin(x / 3.0 * PI)) * 2.0 / 3.0;
ret += (150.0 * Math.sin(x / 12.0 * PI)
+ 300.0 * Math.sin(x / 30.0 * PI)) * 2.0 / 3.0;
return ret;
}
/**
* WGS84 -> GCJ-02
* 仅对中国陆地及沿海地区的坐标进行转换,其他区域原样返回
* @return double[]{lng, lat}
*/
public static double[] wgs84ToGcj02(double lng, double lat) {
if (!isInChina(lng, lat)) {
return new double[]{lng, lat};
}
double dLat = transformLat(lng - 105.0, lat - 35.0);
double dLng = transformLng(lng - 105.0, lat - 35.0);
double radLat = lat / 180.0 * PI;
double magic = Math.sin(radLat);
magic = 1 - EE * magic * magic;
double sqrtMagic = Math.sqrt(magic);
dLat = (dLat * 180.0) / ((A * (1 - EE)) / (magic * sqrtMagic) * PI);
dLng = (dLng * 180.0) / (A / sqrtMagic * Math.cos(radLat) * PI);
return new double[]{lng + dLng, lat + dLat};
}
}
在实际开发中,建议封装一个坐标适配器,根据当前地图模式自动决定是否转换坐标,避免在业务代码中到处写 if-else。
/**
* ShipXY 坐标适配器
* 根据当前地图模式自动处理坐标转换
*/
var CoordAdapter = (function () {
// 地图模式常量(与 ShipxyAPI 内部枚举对应)
var MAP_MODE_NORMAL = 'normal'; // 地图模式
var MAP_MODE_SATELLITE = 'satellite'; // 卫星图模式
var MAP_MODE_SEA = 'sea'; // 海图模式
var currentMode = MAP_MODE_SEA; // 默认海图模式
/**
* 设置当前地图模式
* @param {string} mode - 'normal' | 'satellite' | 'sea'
*/
function setMode(mode) {
currentMode = mode;
console.log('[CoordAdapter] 地图模式切换为: ' + mode);
}
/**
* 判断当前模式是否需要坐标转换
*/
function needTransform() {
return currentMode === MAP_MODE_NORMAL || currentMode === MAP_MODE_SATELLITE;
}
/**
* 将 WGS84 坐标适配为当前地图模式所需的坐标
* - 地图/卫星图模式:WGS84 → GCJ-02(仅中国陆地及沿海地区)
* - 海图模式:直接返回 WGS84
*
* @param {number} lng - WGS84 经度
* @param {number} lat - WGS84 纬度
* @returns {{lng: number, lat: number}} 适配后的坐标
*/
function adapt(lng, lat) {
if (needTransform()) {
return CoordTransform.wgs84ToGcj02(lng, lat);
}
return { lng: lng, lat: lat };
}
/**
* 批量适配坐标(用于轨迹、船队等多点场景)
* @param {Array<{lng: number, lat: number}>} coords - WGS84 坐标数组
* @returns {Array<{lng: number, lat: number}>} 适配后的坐标数组
*/
function adaptBatch(coords) {
return coords.map(function (c) {
return adapt(c.lng, c.lat);
});
}
return {
setMode: setMode,
needTransform: needTransform,
adapt: adapt,
adaptBatch: adaptBatch,
MODE_NORMAL: MAP_MODE_NORMAL,
MODE_SATELLITE: MAP_MODE_SATELLITE,
MODE_SEA: MAP_MODE_SEA
};
})();
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title>ShipXY 坐标转换示例</title>
<script src="https://api.shipxy.com/h5s/api/3.5/plugins/jquery/jquery.min.js"></script>
<script src="https://api.shipxy.com/h5s/api/3.5/?k=您的密钥"></script>
<style>
.my-map {
position: absolute;
left: 0; top: 0;
width: 100%; height: 100%;
overflow: hidden;
outline: none;
background-color: #A3CCFF;
}
</style>
</head>
<body>
<div id="map" class="my-map"></div>
<script>
// 1. 初始化地图(默认海图模式)
var map = new ShipxyAPI.Map("map", { ak: "您的密钥" });
// 2. 设置初始模式
CoordAdapter.setMode(CoordAdapter.MODE_SEA);
// 3. 添加船舶标注(WGS84 数据来自 AIS)
var shipWgs84 = { lng: 121.4737, lat: 31.2304 }; // 上海港
// 适配坐标
var displayCoord = CoordAdapter.adapt(shipWgs84.lng, shipWgs84.lat);
console.log("原始 WGS84: ", shipWgs84);
console.log("显示坐标: ", displayCoord);
// 添加标注(注意 Leaflet 坐标顺序为 [纬度, 经度])
var marker = L.marker([displayCoord.lat, displayCoord.lng], {
title: "测试船舶"
}).addTo(map);
marker.bindPopup("船舶位置(已适配坐标系)<br>WGS84: " +
shipWgs84.lng.toFixed(6) + ", " + shipWgs84.lat.toFixed(6) + "<br>" +
"显示: " + displayCoord.lng.toFixed(6) + ", " + displayCoord.lat.toFixed(6));
// 定位到该点
map.setView([displayCoord.lat, displayCoord.lng], 10);
</script>
</body>
</html>
/**
* 监听地图模式切换,自动更新坐标适配器
*/
function onMapModeChange(newMode) {
// 更新适配器模式
switch (newMode) {
case 'normal':
CoordAdapter.setMode(CoordAdapter.MODE_NORMAL);
break;
case 'satellite':
CoordAdapter.setMode(CoordAdapter.MODE_SATELLITE);
break;
case 'sea':
default:
CoordAdapter.setMode(CoordAdapter.MODE_SEA);
break;
}
// 刷新所有标注点的坐标
refreshAllMarkers();
}
/**
* 刷新地图上所有标注的坐标
* 适用于模式切换后重新计算坐标位置
*/
function refreshAllMarkers() {
// 假设 shipDataList 是你的船舶数据列表(WGS84 原始数据)
shipDataList.forEach(function (ship) {
if (ship.marker) {
var coord = CoordAdapter.adapt(ship.lng, ship.lat);
ship.marker.setLatLng([coord.lat, coord.lng]);
}
});
// 如果有轨迹线,也需要刷新
if (currentTrackPolyline) {
var adaptedCoords = CoordAdapter.adaptBatch(
rawTrackCoords.map(function (c) {
return { lng: c[0], lat: c[1] };
})
);
currentTrackPolyline.setLatLngs(
adaptedCoords.map(function (c) {
return [c.lat, c.lng];
})
);
}
}
/**
* 渲染船位服务返回的船舶数据
* ShipXY 船位 API 返回的是 WGS84 坐标
*/
function renderShips(shipList) {
shipList.forEach(function (ship) {
// 适配坐标
var coord = CoordAdapter.adapt(ship.lon, ship.lat);
// 创建船舶图标标注
var shipMarker = L.marker([coord.lat, coord.lng], {
icon: createShipIcon(ship),
title: ship.name || ('MMSI:' + ship.mmsi)
}).addTo(map);
// 保存标注引用,用于模式切换时刷新
ship.marker = shipMarker;
// 弹窗信息
shipMarker.bindPopup(
'<b>' + (ship.name || '未知') + '</b><br>' +
'MMSI: ' + ship.mmsi + '<br>' +
'航速: ' + (ship.sog || 0) + ' 节<br>' +
'航向: ' + (ship.cog || 0) + '°<br>' +
'状态: ' + (ship.navStatus || '未知') + '<br>' +
'<hr>' +
'原始 WGS84: ' + ship.lon.toFixed(6) + ', ' + ship.lat.toFixed(6) + '<br>' +
'显示坐标: ' + coord.lng.toFixed(6) + ', ' + coord.lat.toFixed(6)
);
});
}
// 示例:通过 ShipXY 区域船位服务获取数据
// var searchService = ShipxyAPI.SeachShipService(map, {...});
// searchService.getData(function(data) {
// renderShips(data.ships);
// });
/**
* 渲染历史轨迹
* ShipXY 轨迹 API 返回的是 WGS84 坐标数组
*/
function renderTrack(trackPoints) {
// trackPoints: [{lon, lat, time, sog, cog}, ...]
// 适配所有轨迹点坐标
var adaptedPoints = trackPoints.map(function (p) {
var coord = CoordAdapter.adapt(p.lon, p.lat);
return {
lat: coord.lat,
lng: coord.lng,
time: p.time,
sog: p.sog,
cog: p.cog,
// 保留原始 WGS84 坐标用于弹窗展示
originalLon: p.lon,
originalLat: p.lat
};
});
// 绘制轨迹线
var latlngs = adaptedPoints.map(function (p) {
return [p.lat, p.lng];
});
var polyline = L.polyline(latlngs, {
color: '#FF6B35',
weight: 3,
opacity: 0.8
}).addTo(map);
// 在关键点添加标注(起点、终点、锚泊点等)
var startMarker = L.marker(latlngs[0], { title: '起点' })
.addTo(map)
.bindPopup('起点<br>WGS84: ' + adaptedPoints[0].originalLon.toFixed(6) +
', ' + adaptedPoints[0].originalLat.toFixed(6));
var endMarker = L.marker(latlngs[latlngs.length - 1], { title: '终点' })
.addTo(map)
.bindPopup('终点<br>WGS84: ' + adaptedPoints[adaptedPoints.length - 1].originalLon.toFixed(6) +
', ' + adaptedPoints[adaptedPoints.length - 1].originalLat.toFixed(6));
// 适配地图视野
map.fitBounds(polyline.getBounds(), { padding: [50, 50] });
// 保存引用,用于模式切换时刷新
currentTrackPolyline = polyline;
rawTrackCoords = trackPoints.map(function (p) {
return [p.lon, p.lat];
});
}
以下是一个可直接运行的完整 Demo 页面,展示坐标系转换效果。替换 您的密钥 为实际的 ShipXY API Key 后即可使用。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ShipXY 坐标系转换演示</title>
<script src="https://api.shipxy.com/h5s/api/3.5/plugins/jquery/jquery.min.js"></script>
<script src="https://api.shipxy.com/h5s/api/3.5/?k=您的密钥"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: "Microsoft YaHei", sans-serif; }
#map {
position: absolute;
left: 0; top: 0;
width: 100%; height: 100%;
background-color: #A3CCFF;
}
.control-panel {
position: absolute;
top: 10px; right: 10px;
z-index: 1000;
background: rgba(255,255,255,0.95);
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 12px rgba(0,0,0,0.15);
width: 340px;
font-size: 13px;
}
.control-panel h3 {
font-size: 15px;
margin-bottom: 10px;
color: #333;
}
.control-panel .section {
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid #eee;
}
.control-panel .section:last-child {
border-bottom: none;
}
.control-panel label {
display: inline-block;
width: 70px;
color: #666;
}
.control-panel input {
width: 100px;
padding: 3px 6px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 13px;
}
.control-panel button {
padding: 5px 12px;
margin: 2px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
background: #1890ff;
color: #fff;
}
.control-panel button:hover { background: #40a9ff; }
.control-panel button.active { background: #096dd9; }
.control-panel .info {
background: #f6f8fa;
padding: 8px;
border-radius: 4px;
font-family: monospace;
font-size: 12px;
line-height: 1.8;
color: #333;
}
.control-panel .offset { color: #ff4d4f; font-weight: bold; }
.control-panel .note { color: #999; font-size: 11px; margin-top: 4px; }
</style>
</head>
<body>
<div id="map"></div>
<div class="control-panel">
<h3>ShipXY 坐标系转换演示</h3>
<div class="section">
<label>经度 (WGS84):</label>
<input type="text" id="inputLng" value="121.4737">
<br>
<label>纬度 (WGS84):</label>
<input type="text" id="inputLat" value="31.2304">
<br>
<button onclick="updatePoint()">更新点位</button>
</div>
<div class="section">
<label>地图模式:</label>
<button id="btnSea" class="active" onclick="switchMode('sea')">海图模式</button>
<button id="btnNormal" onclick="switchMode('normal')">地图模式</button>
<button id="btnSatellite" onclick="switchMode('satellite')">卫星图</button>
</div>
<div class="section">
<div class="info" id="coordInfo">
WGS84: 121.473700, 31.230400<br>
GCJ-02: --<br>
偏移: --<br>
是否在中国: 是<br>
当前模式: 海图模式 (无需转换)
</div>
<div class="note">蓝点 = WGS84 原始坐标 | 红点 = GCJ-02 转换后坐标</div>
</div>
</div>
<script>
// ============ 坐标转换工具 ============
var CoordTransform = (function () {
var a = 6378245.0, ee = 0.00669342162296594323;
function isInChina(lng, lat) {
// 中国陆地及沿海地区(不含南海远洋区域)
return lng > 73.66 && lng < 135.05 && lat > 18.0 && lat < 53.55;
}
function transformLat(x, y) {
var ret = -100.0 + 2.0*x + 3.0*y + 0.2*y*y + 0.1*x*y + 0.2*Math.sqrt(Math.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;
}
function transformLng(x, y) {
var ret = 300.0 + x + 2.0*y + 0.1*x*x + 0.1*x*y + 0.1*Math.sqrt(Math.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;
}
function wgs84ToGcj02(lng, lat) {
if (!isInChina(lng, lat)) return { lng: lng, lat: lat };
var dLat = transformLat(lng - 105.0, lat - 35.0);
var dLng = transformLng(lng - 105.0, lat - 35.0);
var radLat = lat / 180.0 * Math.PI;
var magic = Math.sin(radLat);
magic = 1 - ee * magic * magic;
var 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);
return { lng: lng + dLng, lat: lat + dLat };
}
return { isInChina: isInChina, wgs84ToGcj02: wgs84ToGcj02 };
})();
// ============ 地图初始化 ============
var map = new ShipxyAPI.Map("map", { ak: "您的密钥" });
var currentMode = 'sea';
var wgs84Marker = null; // 蓝点(WGS84 原始)
var gcj02Marker = null; // 红点(GCJ-02 转换后)
var offsetLine = null; // 偏移连线
function updatePoint() {
var lng = parseFloat(document.getElementById('inputLng').value);
var lat = parseFloat(document.getElementById('inputLat').value);
var inChina = CoordTransform.isInChina(lng, lat);
var gcj = CoordTransform.wgs84ToGcj02(lng, lat);
var needTransform = (currentMode === 'normal' || currentMode === 'satellite') && inChina;
// 清除旧标注
if (wgs84Marker) { map.removeLayer(wgs84Marker); wgs84Marker = null; }
if (gcj02Marker) { map.removeLayer(gcj02Marker); gcj02Marker = null; }
if (offsetLine) { map.removeLayer(offsetLine); offsetLine = null; }
// 地图/卫星图模式:显示两个点(原始 vs 转换后)
// 海图模式:只显示原始点
var displayCoord = needTransform ? gcj : { lng: lng, lat: lat };
// 蓝点 = 实际应标注的位置(适配当前模式)
wgs84Marker = L.circleMarker([displayCoord.lat, displayCoord.lng], {
radius: 8, color: '#1890ff', fillColor: '#1890ff',
fillOpacity: 0.8, weight: 2
}).addTo(map).bindPopup(
'<b>显示坐标(已适配模式)</b><br>' +
'当前模式: ' + getModeName(currentMode) + '<br>' +
(needTransform ? '已从 WGS84 转换为 GCJ-02' : '使用原始 WGS84') + '<br>' +
'坐标: ' + displayCoord.lng.toFixed(6) + ', ' + displayCoord.lat.toFixed(6)
);
// 如果需要转换,再显示红色对比点(未转换的 WGS84 直接标注,展示偏移)
if (needTransform) {
gcj02Marker = L.circleMarker([lat, lng], {
radius: 8, color: '#ff4d4f', fillColor: '#ff4d4f',
fillOpacity: 0.8, weight: 2
}).addTo(map).bindPopup(
'<b>未转换的 WGS84 坐标</b><br>' +
'直接标注在 GCJ-02 底图上会产生偏移<br>' +
'坐标: ' + lng.toFixed(6) + ', ' + lat.toFixed(6)
);
// 偏移连线
offsetLine = L.polyline(
[[displayCoord.lat, displayCoord.lng], [lat, lng]],
{ color: '#ff4d4f', weight: 2, dashArray: '5,5' }
).addTo(map);
}
// 更新信息面板
var dLng = gcj.lng - lng, dLat = gcj.lat - lat;
var distance = Math.sqrt(dLng*dLng + dLat*dLat) * 111000; // 粗略估算米数
document.getElementById('coordInfo').innerHTML =
'<b>WGS84:</b> ' + lng.toFixed(6) + ', ' + lat.toFixed(6) + '<br>' +
'<b>GCJ-02:</b> ' + gcj.lng.toFixed(6) + ', ' + gcj.lat.toFixed(6) + '<br>' +
'<span class="offset">偏移: 经度 ' + (dLng >= 0 ? '+' : '') + dLng.toFixed(6) +
'°, 纬度 ' + (dLat >= 0 ? '+' : '') + dLat.toFixed(6) + '° (≈' + Math.round(distance) + '米)</span><br>' +
'是否在中国: ' + (inChina ? '是' : '否') + '<br>' +
'当前模式: ' + getModeName(currentMode) + ' ' +
(needTransform ? '(✅ 需要转换)' : '(❌ 无需转换)');
// 定位地图
map.setView([displayCoord.lat, displayCoord.lng], 13);
}
function switchMode(mode) {
currentMode = mode;
// 更新按钮样式
document.querySelectorAll('.control-panel button').forEach(function(btn) {
btn.classList.remove('active');
});
document.getElementById('btn' + mode.charAt(0).toUpperCase() + mode.slice(1)).classList.add('active');
// TODO: 调用 ShipxyAPI 切换底图
// 刷新点位显示
updatePoint();
}
function getModeName(mode) {
return { 'normal': '地图模式', 'satellite': '卫星图模式', 'sea': '海图模式' }[mode];
}
// 初始加载
updatePoint();
</script>
</body>
</html>
| 场景 | 地图/卫星图模式 | 海图模式 |
|---|---|---|
| 中国陆地及沿海船位标注 | ✅ WGS84 → GCJ-02 | ❌ 直接使用 WGS84 |
| 远洋/公海船位标注 | ❌ 直接使用 WGS84 | ❌ 直接使用 WGS84 |
| 历史轨迹绘制(中国沿海) | ✅ 逐点转换 | ❌ 直接使用 WGS84 |
| 港口/航标标注(中国沿海) | ✅ 转换 | ❌ 直接使用 WGS84 |
| 区域多边形查询 | ✅ 顶点转 GCJ-02 | ❌ 直接使用 WGS84 |
批量转换:对于轨迹等大量坐标点,使用批量转换函数,减少函数调用开销。
缓存转换结果:对于不频繁变动的静态标注(如港口位置),转换后缓存结果,避免重复计算。
区域外跳过:isInChina() 判定开销极小(4 次浮点比较),但能跳过大量远洋/公海坐标的三角函数计算。远洋船舶无需转换。
前端转换优先:坐标转换计算量很小(单次 < 0.01ms),建议在前端完成,避免增加后端接口延迟。批量场景(如万级轨迹点)也可在后端预转换。
模式切换时批量刷新:模式切换时统一刷新所有标注,而不是逐个响应事件。可以使用 requestAnimationFrame 或 debounce 优化。
| 陷阱 | 说明 | 解决方案 |
|---|---|---|
| Leaflet 坐标顺序 | Leaflet 使用 [lat, lng] 顺序,而业务数据通常是 {lng, lat} 或 [lng, lat] | 转换时注意顺序,统一封装适配层 |
| 地图模式未切换就标注 | 默认海图模式不加转换,切到地图模式后点位偏移 | 模式切换后立即刷新所有标注 |
| 轨迹点部分在远洋 | 轨迹跨越大洋,部分点在中国沿海部分在远洋 | wgs84ToGcj02() 内部已自动判断,远洋点原样返回 |
| 海图叠加地图图层 | 在海图上叠加地图瓦片作为混合底图 | 需要根据实际显示的底图决定坐标系,建议避免混用 |
Q1: 偏移量有多大?不转换会怎样?
A: 在中国陆地及沿海地区,WGS84 与 GCJ-02 的偏移量约为 50500 米,具体取决于地理位置。在沿海港口区域通常约 460490 米。对于船舶监控,这个偏移会导致船舶图标显示在错误的航道甚至岸上。
Q2: 转换算法的精度如何?
A: WGS84 → GCJ-02 正向转换精度极高(与官方偏移一致),完全满足船舶监控和可视化展示的需求。
Q3: 坐标转换是否合法?
A: GCJ-02 算法是公开的非保密算法,在开发者社区广泛使用。坐标转换本身是技术对接需要,不涉及违法问题。但在公开发布的地图产品中,仍需确保符合国家测绘法规的相关要求。
Q4: ShipXY 船位 API 返回的是什么坐标系?
A: ShipXY 所有业务数据 API(船位、轨迹、港口等)返回的均为 WGS84 坐标系。坐标转换仅在前端展示环节根据地图模式按需进行。
Q5: 如何判断当前是否在地图/卫星图模式?
A: 可通过监听 ShipxyAPI 的底图切换事件,或检查 map 对象的当前图层类型。建议在应用层维护一个 currentMode 变量,在用户切换底图时同步更新。
Q6: 南海远洋区域的船舶需要转换坐标吗?
A: 不需要。本文档定义的转换范围为中国陆地及沿海地区(纬度 18°N 以北),南海远洋区域及公海区域的坐标在所有模式下均使用 WGS84,无需转换。
┌─────────────────────────────────────────────────────────────────┐
│ ShipXY 坐标转换速查表 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 业务数据始终是 WGS84,展示时按地图模式适配: │
│ │
│ ┌─────────────┐ 中国陆地及沿海 ┌──────────────┐ │
│ │ WGS84 │ ────地图/卫星────▶ │ GCJ-02 │ │
│ │ (AIS/GPS) │ 模式 │ (标注到地图) │ │
│ └─────────────┘ └──────────────┘ │
│ │ │
│ │─────海图模式────────▶ 直接标注(WGS84 = 海图坐标) │
│ │ │
│ │──远洋/南海/公海─────▶ 直接标注(无需任何转换) │
│ │
│ 核心函数: │
│ CoordTransform.wgs84ToGcj02(lng, lat) → {lng, lat} │
│ CoordTransform.isInChina(lng, lat) → boolean │
│ │
│ 适配器(推荐使用): │
│ CoordAdapter.setMode('sea' | 'normal' | 'satellite') │
│ CoordAdapter.adapt(lng, lat) → {lng, lat} │
│ CoordAdapter.adaptBatch(coords) → [{lng, lat}, ...] │
│ │
└─────────────────────────────────────────────────────────────────┘