关于网页地图的坐标系

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),具体取决于数据源。

说明

  • CGCS2000WGS-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坐标系、下面是高德地图火星坐标系)

可见偏差比较小。

相关推荐
GIS好难学1 天前
考研出分24小时,人类精神状态图鉴
前端·考研·gis·gis开发·webgis·地信
GIS追梦人5 天前
Cesium 入门之 Entity API
gis·cesium
安大桃子7 天前
Cesium实现深色地图效果
前端·gis·cesium
图导物联8 天前
基于WebGIS技术的校园地图导航系统架构与核心功能设计
系统架构·智慧校园·gis·webgl·地图导航·电子地图·校园地图导航
GIS学姐嘉欣9 天前
DeepSeek预测25考研分数线
前端·考研·gis·webgis
vjmap16 天前
用AI绘制CAD气温曲线图
人工智能·ai·gis·cad·dwg
KY_chenzhao17 天前
无人机遥感技术在农业中的具体应用:株数和株高、冠层覆盖度、作物倒伏检测、叶面积指数、病虫害监测、产量估算、空间数据综合制图
gis·无人机·遥感·农林信息
小艳加油17 天前
无人机遥感农林信息提取实现方法;农作物形态信息提取、农作物生理生化信息提取、农作物胁迫信息提取、农作物产量信息提取、遥感提取结果的空间表达——GIS制图流程等
gis·无人机·遥感·农林信息提取
岁月如歌,青春不败17 天前
无人机生态环境监测、图像处理与GIS数据分析
图像处理·数据分析·gis·无人机·生态学·激光雷达·环境科学