高德纠偏 · Gaode Transform · ▶ 在线运行案例

高德纠偏

你将学到什么

  • Cesium Entity 高层实体 API

效果说明

本案例演示 高德纠偏 效果:BD-09(百度坐标系) To GCJ-02(火星坐标系);核心用到 Cesium。建议先打开文首在线案例查看动态画面,再对照下方源码逐步理解。

核心概念

  • Viewer 聚合 Scene、Camera、Clock 与渲染循环,是 Cesium 应用入口。
  • Entity 面向点线面/模型/标签的高层 API;与 Primitive 相比更适合交互与属性驱动。
  • 阅读下方完整源码时,建议从 init / load / animate 三条主线入手,再深入 shader 与工具函数。

实现步骤

  1. 创建 Viewer,配置地形/影像(若案例需要)并设置初始相机
  2. requestAnimationFrame 循环中更新状态并 render(Cesium 为 viewer.render 或自动渲染)

代码要点

import * as Cesium from 'cesium';
import * as dat from 'dat.gui';

// 定义一些常量
const BD_FACTOR = (3.14159265358979324 * 3000.0) / 180.0;
const PI = 3.1415926535897932384626;
const RADIUS = 6378245.0;
const EE = 0.00669342162296594323;

class CoordTransform {
    /**
     * BD-09(百度坐标系) To GCJ-02(火星坐标系)
     * @param lng
     * @param lat
     * @returns {number[]}
     */
    static BD09ToGCJ02(lng, lat) {
        let x = +lng - 0.0065;
        let y = +lat - 0.006;
        let z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * BD_FACTOR);
        let theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * BD_FACTOR);
        let gg_lng = z * Math.cos(theta);
        let gg_lat = z * Math.sin(theta);
        return [gg_lng, gg_lat];
    }

    /**
     * GCJ-02(火星坐标系) To BD-09(百度坐标系)
     * @param lng
     * @param lat
     * @returns {number[]}
     * @constructor
     */
    static GCJ02ToBD09(lng, lat) {
        lat = +lat;
        lng = +lng;
        let z = Math.sqrt(lng * lng + lat * lat) + 0.00002 * Math.sin(lat * BD_FACTOR);
        let theta = Math.atan2(lat, lng) + 0.000003 * Math.cos(lng * BD_FACTOR);
        let bd_lng = z * Math.cos(theta) + 0.0065;
        let bd_lat = z * Math.sin(theta) + 0.006;
        return [bd_lng, bd_lat];
    }

    /**
     * WGS-84(世界大地坐标系) To GCJ-02(火星坐标系)
     * @param lng
     * @param lat
     * @returns {number[]}
     */
    static WGS84ToGCJ02(lng, lat) {
        lat = +lat;
        lng = +lng;
        if (this.out_of_china(lng, lat)) {
            return [lng, lat];
        } else {
            let d = this.delta(lng, lat);
            return [lng + d[0], lat + d[1]];
        }
    }

    /**
     * GCJ-02(火星坐标系) To WGS-84(世界大地坐标系)
     * @param lng
     * @param lat
     * @returns {number[]}
     * @constructor
     */
    static GCJ02ToWGS84(lng, lat) {
        lat = +lat;
        lng = +lng;
        if (this.out_of_china(lng, lat)) {
            return [lng, lat];
        } else {
            let d = this.delta(lng, lat);
            let mgLng = lng + d[0];
            let mgLat = lat + d[1];
            return [lng * 2 - mgLng, lat * 2 - mgLat];
        }
    }

    /**
     *
     * @param lng
     * @param lat
     * @returns {number[]}
     */
    static delta(lng, lat) {
        let dLng = this.transformLng(lng - 105, lat - 35);
        let dLat = this.transformLat(lng - 105, lat - 35);
        const radLat = (lat / 180) * PI;
        let magic = Math.sin(radLat);
        magic = 1 - EE * magic * magic;
        const sqrtMagic = Math.sqrt(magic);
        dLng = (dLng * 180) / ((RADIUS / sqrtMagic) * Math.cos(radLat) * PI);
        dLat = (dLat * 180) / (((RADIUS * (1 - EE)) / (magic * sqrtMagic)) * PI);
        return [dLng, dLat];
    }

    /**
     *
     * @param lng
     * @param lat
     * @returns {number}
     */
    static transformLng(lng, lat) {
        lat = +lat;
        lng = +lng;
        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 * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0) / 3.0;
        ret += ((20.0 * Math.sin(lng * PI) + 40.0 * Math.sin((lng / 3.0) * PI)) * 2.0) / 3.0;
        ret += ((150.0 * Math.sin((lng / 12.0) * PI) + 300.0 * Math.sin((lng / 30.0) * PI)) * 2.0) / 3.0;
        return ret;
    }

    /**
     *
     * @param lng
     * @param lat
     * @returns {number}
     */
    static transformLat(lng, lat) {
        lat = +lat;
        lng = +lng;
        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 * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0) / 3.0;
        ret += ((20.0 * Math.sin(lat * PI) + 40.0 * Math.sin((lat / 3.0) * PI)) * 2.0) / 3.0;
        ret += ((160.0 * Math.sin((lat / 12.0) * PI) + 320 * Math.sin((lat * PI) / 30.0)) * 2.0) / 3.0;
        return ret;
    }

    /**
     * 判断是否在国内。不在国内不做偏移
     * @param lng
     * @param lat
     * @returns {boolean}
     */
    static out_of_china(lng, lat) {
        lat = +lat;
        lng = +lng;
        return !(lng > 73.66 && lng < 135.05 && lat > 3.86 && lat < 53.55);
    }
}

class AmapMercatorTilingScheme extends Cesium.WebMercatorTilingScheme {

    constructor() {
        super();

        let projection = new Cesium.WebMercatorProjection();

        this._projection.project = function (cartographic, result) {
            result = CoordTransform.WGS84ToGCJ02(
                Cesium.Math.toDegrees(cartographic.longitude),
                Cesium.Math.toDegrees(cartographic.latitude)
            );
            result = projection.project(new Cesium.Cartographic(Cesium.Math.toRadians(result[0]), Cesium.Math.toRadians(result[1])));
            return new Cesium.Cartesian2(result.x, result.y);
        };

        this._projection.unproject = function (cartesian, result) {
            let cartographic = projection.unproject(cartesian);
            result = CoordTransform.GCJ02ToWGS84(
                Cesium.Math.toDegrees(cartographic.longitude),
                Cesium.Math.toDegrees(cartographic.latitude)
            );
            return new Cesium.Cartographic(Cesium.Math.toRadians(result[0]), Cesium.Math.toRadians(result[1]));
        };
    }
}

const box = document.getElementById('box')

const viewer = new Cesium.Viewer(box, {

    animation: false,//是否创建动画小器件,左下角仪表    

    baseLayerPicker: false,//是否显示图层选择器,右上角图层选择按钮

    baseLayer: false, // 不显示默认图层

    fullscreenButton: false,//是否显示全屏按钮,右下角全屏选择按钮

    timeline: false,//是否显示时间轴    

    infoBox: false,//是否显示信息框   

})

viewer.imageryLayers.addImageryProvider(
    new Cesium.UrlTemplateImageryProvider({
        url: 'https://webrd02.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=2&style=8&x={x}&y={y}&z={z}',
        maximumLevel: 18,
        tilingScheme: new AmapMercatorTilingScheme()
    })
)

viewer.entities.add({

    name: 'gltf',

    position: Cesium.Cartesian3.fromDegrees(116.3975, 39.9085, 0), // 北京的经纬度和高度

    model: {

        uri: FILE_HOST + '/files/model/LittlestTokyo.glb',

        minimumPixelSize: 128,

        maximumScale: 200,

    }

})

const gui = new dat.GUI();

const mapParams = {
    mapType: '纠偏校正', // 默认使用纠偏后的高德地图
    locations: '北京',
};

// 预定义常用位置
const locations = {
    '北京': [116.3975, 39.9085, 2000],
    '上海': [121.4737, 31.2304, 2000],
    '广州': [113.2644, 23.1292, 2000],
    '深圳': [114.0579, 22.5431, 2000]
};

// 飞行到指定位置
function flyToLocation(name) {
    const position = locations[name];
    if (position) {
        viewer.camera.flyTo({
            destination: Cesium.Cartesian3.fromDegrees(position[0], position[1], position[2]),
            duration: 1.5
        });
    }
}

// 添加GUI控制
const mapFolder = gui.addFolder('坐标纠偏对比');

// 添加地图类型选择
mapFolder.add(mapParams, 'mapType', ['纠偏校正', '未纠正']).name('地图类型').onChange(function (value) {
    viewer.imageryLayers.removeAll();
    viewer.imageryLayers.addImageryProvider(
        new Cesium.UrlTemplateImageryProvider({
            url: 'https://webrd02.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=2&style=8&x={x}&y={y}&z={z}',
            maximumLevel: 18,
            tilingScheme: value === '纠偏校正' ? new AmapMercatorTilingScheme() : null
        })
    )
});

// 添加位置选择
mapFolder.add(mapParams, 'locations', Object.keys(locations)).name('常用位置')
    .onChange(function (value) {
        flyToLocation(value);
    });


// 默认展开文件夹
mapFolder.open();

flyToLocation(mapParams.locations);

完整源码:GitHub

小结