EPSG:4326地理坐标系 和 EPSG:3857Web 墨卡托投影
EPSG:4326
- 定义:EPSG:4326 是基于 WGS84 椭球的地理坐标系,使用经度(Longitude)和纬度(Latitude)表示地球上的位置。
- 特点 :
- 经度范围为 -180° 至 +180°,纬度范围为 -90° 至 +90°。
- 单位为度(°),适合精确的地理定位和科学应用。
- 由于其基于椭球体模型,计算复杂度较高。
- 应用场景 :
- 用于存储地理数据,如 GPS 数据、地理信息系统(GIS)数据等。
- 适用于大范围的地理信息描述。
EPSG:3857
- 定义 :EPSG:3857 是基于 Web 墨卡托投影(伪墨卡托投影)的投影坐标系,将地球视为正球体进行投影。
- 特点 :
- 坐标单位为米,范围为纬度 ±85.051129°。
- 由于投影的等角特性,地图在不同层级上保持形状不变,适合导航和地图显示。
- 高纬度地区存在面积变形,但计算简单,适合 Web 地图应用。
- 应用场景 :
- 广泛用于 Web 地图服务,如 Google Maps、OpenStreetMap、ArcGIS 等。
- 适合地图显示和导航,但 不适合存储地理数据。
两者关系
通常地理数据以 EPSG:4326 存储,因为其精度高且易于理解;而在 Web 地图中显示时,会转换为 EPSG:3857(大部分地图api底层会进行转换,开发调用api只需要写入 EPSG:4326坐标)。
当网页地图中加载数据时,系统会将 EPSG:4326 坐标转换为 EPSG:3857 坐标,以适应地图的投影和显示。这种转换是必要的,因为 EPSG:3857 坐标系使用的是平面坐标,对地图的缩放和平移效果更好。
各大地图服务提供商所使用的坐标系:
地图服务 | 坐标系 |
---|---|
天地图 | CGCS2000(中国2000国家大地坐标系),EPSG代码为4490。 |
高德地图 | GCJ-02(火星坐标系),基于WGS-84加密处理。 |
百度地图 | BD-09,基于GCJ-02二次加密。 |
谷歌地图 | 国外使用WGS-84坐标系;在中国使用GCJ-02。 |
Mapbox | 默认使用WGS-84(EPSG:4326)或Web墨卡托投影(EPSG:3857),但可通过修改支持其他坐标系。 |
ArcGIS | 通常使用WGS-84(EPSG:4326)或Web墨卡托投影(EPSG:3857),具体取决于数据源。 |
说明
- CGCS2000 与 WGS-84 非常接近,差异在厘米级别,通常可忽略。
关于火星坐标系要注意的
火星坐标系用于避免地图数据被直接使用或分析。由于中国的法律对地图数据有严格的管理,因此在中国境内的地图API(如高德和百度地图)都会对GPS坐标进行加密,转换为火星坐标系。
这种转换使得直接从GPS设备获取的WGS 84坐标不再准确,需要通过特定的算法进行转换才能在这些地图服务中正确显示。
例如:leaflet加载高德地图,如果添加点击事件获取地图上的点那么点的坐标是火星坐标系的,如果向地图上添加已知点,为了加到正确的位置上,也要求是火星坐标系。
下面是java的基于geotools api实现高德火星坐标系转WGS-84坐标系(基于wkb字符串)
java
@Slf4j
public class WKBToGeoJSONUtil {
/**
* 将WKB十六进制字符串转换为GeoJSON格式的Map
* @param hexWKB PostGIS返回的WKB十六进制字符串
* @param decimals 保留的小数位数
* @return 对应GeoJSON结构的Map
* @throws Exception 如果解析WKB或生成GeoJSON时发生错误
*/
public static Map<String, Object> convert(String hexWKB, int decimals) throws Exception {
Geometry geometry = parseWKB(hexWKB);
Geometry wgs84Geometry = CoordinateTransform.gcj02ToWgs84(geometry);
return convertToGeoJSONMap(wgs84Geometry, decimals);
}
/**
* 解析WKB字符串为JTS Geometry对象
* @param hexWKB WKB十六进制字符串
* @return 解析后的Geometry对象
* @throws Exception 如果解析WKB时发生错误
*/
private static Geometry parseWKB(String hexWKB) throws Exception {
try {
WKBReader wkbReader = new WKBReader();
byte[] wkbBytes = WKBReader.hexToBytes(hexWKB);
return wkbReader.read(wkbBytes);
} catch (ParseException e) {
log.error("Failed to parse WKB string: {}", hexWKB, e);
throw new Exception("Failed to parse WKB string", e);
}
}
/**
* 将Geometry对象转换为GeoJSON Map
* @param geometry JTS Geometry对象
* @param decimals 保留的小数位数
* @return GeoJSON格式的Map
* @throws IOException 如果生成GeoJSON时发生错误
*/
private static Map<String, Object> convertToGeoJSONMap(Geometry geometry, int decimals) throws IOException {
try {
GeometryJSON geometryJSON = new GeometryJSON(decimals);
StringWriter writer = new StringWriter();
geometryJSON.write(geometry, writer);
String geoJsonStr = writer.toString();
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readValue(geoJsonStr, new TypeReference<Map<String, Object>>() {});
} catch (IOException e) {
log.error("Failed to convert Geometry to GeoJSON Map", e);
throw new IOException("Failed to convert Geometry to GeoJSON Map", e);
}
}
/**
* 坐标转换工具类
*/
private static class CoordinateTransform {
private static final double PI = Math.PI;
private static final double AXIS = 6378245.0;
private static final double OFFSET = 0.00669342162296594323;
/**
* 将GCJ-02坐标系的Geometry转换为WGS84坐标系
* @param geometry GCJ-02坐标系的Geometry
* @return WGS84坐标系的Geometry
*/
public static Geometry gcj02ToWgs84(Geometry geometry) {
GeometryEditor editor = new GeometryEditor();
return editor.edit(geometry, new GeometryEditor.CoordinateSequenceOperation() {
@Override
public CoordinateSequence edit(CoordinateSequence coordSeq, Geometry geom) {
Coordinate[] coords = coordSeq.toCoordinateArray();
for (Coordinate coord : coords) {
double[] wgs84 = gcj02ToWgs84(coord.x, coord.y);
coord.x = wgs84[0];
coord.y = wgs84[1];
}
return new CoordinateArraySequence(coords);
}
});
}
/**
* 将GCJ-02坐标转换为WGS84坐标
* @param lng 经度
* @param lat 纬度
* @return WGS84坐标
*/
private static double[] gcj02ToWgs84(double lng, double lat) {
if (outOfChina(lng, lat)) {
return new double[]{lng, lat};
}
double[] delta = delta(lng, lat);
return new double[]{lng - delta[0], lat - delta[1]};
}
/**
* 判断坐标是否在中国范围内
* @param lng 经度
* @param lat 纬度
* @return 是否在中国范围内
*/
private static boolean outOfChina(double lng, double lat) {
return lng < 72.004 || lng > 137.8347 || lat < 0.8293 || lat > 55.8271;
}
/**
* 计算GCJ-02与WGS84之间的偏移量
* @param lng 经度
* @param lat 纬度
* @return 偏移量
*/
private static double[] delta(double lng, double 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 - OFFSET * magic * magic;
double sqrtMagic = Math.sqrt(magic);
dLat = (dLat * 180.0) / ((AXIS * (1 - OFFSET)) / (magic * sqrtMagic) * PI);
dLng = (dLng * 180.0) / (AXIS / sqrtMagic * Math.cos(radLat) * PI);
return new double[]{dLng, dLat};
}
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;
ret += 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 * 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;
ret += 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;
}
}
/**
* 测试方法
*/
public static void main(String[] args) {
// 示例 WKB 字符串
String wkb = "0104000020E6100000......";// wkb字符串
try {
Map<String, Object> geoJsonMap = convert(wkb, 6); // 转换为GeoJSON,保留6位小数
log.info("GeoJSON Map: {}", geoJsonMap);
} catch (Exception e) {
log.error("Error converting WKB to GeoJSON", e);
}
}
}
效果展示(上图为mapbox4326坐标系、下面是高德地图火星坐标系)
可见偏差比较小。