绝了!Cesium 与 Echarts 梦幻联动,打造超酷炫迁徙图

2,118 阅读5分钟

各位掘友,大家好,我是 安大桃子。在当下数字化地图与地理信息蓬勃发展的浪潮里,WebGIS 技术成为关键力量。而 VueCesium 作为 WebGIS 开发中的 “黄金搭档”,一个凭借组件化优势让开发高效有序,一个专注地理空间渲染,呈现逼真地图。接下来,我将结合实际项目案例,为大家深度解析如何巧用 Vue、Cesium 以及相关 WebGIS 技术,攻克复杂地理数据可视化与交互难题,开启 WebGIS 开发 进阶之路

1. 开篇介绍

在当今的 Web 应用开发领域,数据可视化与 3D 地球展示的需求日益增长。Cesium 作为强大的开源 JavaScript 库,能轻松构建 3D 地球和地图场景;ECharts 则是广受欢迎的数据可视化工具,支持多种图表类型。本文将详细介绍如何把 ECharts 图表嵌入 Cesium 中,实现二者的无缝融合,呈现出令人惊艳的数据可视化效果。

image.png

2. 主要功能

2.1 图表嵌入

在 Cesium 构建的 3D 地球场景里,成功嵌入 ECharts 图表,让各类数据图表能直观地展示在地球表面,极大地丰富了数据的呈现形式。

2.2 精准坐标转换

通过特定的坐标系转换机制,把地理坐标精确地转化为屏幕坐标。这一过程确保了 ECharts 图表在 Cesium 场景中的位置精准无误,使图表与 3D 地球场景完美贴合。

2.3 交互协同

针对 Cesium 场景中的平移、缩放等操作,精心设计交互处理逻辑。使得 ECharts 图表能够实时响应这些变化,与 Cesium 场景保持高度同步,为用户带来流畅的交互体验。

2.4 资源管理

提供一套完善的资源管理方案,涵盖初始化、更新和销毁等方法。有效管理和释放资源,保障应用程序在运行过程中的稳定性和性能。

3. 代码实现

3.1 RegisterCoordinateSystem

RegisterCoordinateSystem类承担着坐标系转换的关键任务,它确保 ECharts 图表能在 Cesium 场景中正确显示。

class RegisterCoordinateSystem {
    constructor(glMap) {
        this._GLMap = glMap;
        this._mapOffset = [0, 0];
        this.dimensions = ['lng', 'lat'];
    }

    setMapOffset(mapOffset) {
        this._mapOffset = mapOffset;
    }

    getMap() {
        return this._GLMap;
    }

    fixLat(lat) {
        return lat >= 90 ? 89.99999999999999 : lat <= -90 ? -89.99999999999999 : lat;
    }

    dataToPoint(coords) {
        coords[1] = this.fixLat(coords[1]);
        let position = Cesium.Cartesian3.fromDegrees(coords[0], coords[1]);
        if (!position) return [];
        let coordinates = this._GLMap.cartesianToCanvasCoordinates(position);
        if (!coordinates) return [];
        if (this._GLMap.mode === Cesium.SceneMode.SCENE3D) {
            const pointA = position;
            const pointB = this._GLMap.camera.position;
            const transform = Cesium.Transforms.eastNorthUpToFixedFrame(pointA);
            const positionvector = Cesium.Cartesian3.subtract(pointB, pointA, new Cesium.Cartesian3());
            const vector = Cesium.Matrix4.multiplyByPointAsVector(Cesium.Matrix4.inverse(transform, new Cesium.Matrix4()), positionvector, new Cesium.Cartesian3());
            const direction = Cesium.Cartesian3.normalize(vector, new Cesium.Cartesian3());
            if (direction.z < 0) return [];
        }
        return [coordinates.x - this._mapOffset[0], coordinates.y - this._mapOffset[1]];
    }

    pointToData(pixel) {
        let mapOffset = this._mapOffset;
        // 这里的_bmap.project方法可能未定义,假设在实际代码中有正确定义和实现
        let coords = this._bmap.project([pixel[0] + pixel[0], pixel[1] + pixel[1]]);
        return [coords.lng, coords.lat];
    }

    getViewRect() {
        let api = this._api;
        return new echarts.graphic.BoundingRect(0, 0, api.getWidth(), api.getHeight());
    }

    getRoamTransform() {
        return echarts.matrix.create();
    }

    create(echartModel, api) {
        this._api = api;
        let registerCoordinateSystem;
        echartModel.eachComponent("GLMap", function (seriesModel) {
            let painter = api.getZr().painter;
            if (painter) {
                try {
                    let glMap = echarts.glMap;
                    registerCoordinateSystem = new RegisterCoordinateSystem(glMap, api);
                    registerCoordinateSystem.setMapOffset(seriesModel.__mapOffset || [0, 0]);
                    seriesModel.coordinateSystem = registerCoordinateSystem;
                } catch (error) {
                    console.log(error);
                }
            }
        });
        echartModel.eachSeries(function (series) {
            "GLMap" === series.get("coordinateSystem") && (series.coordinateSystem = registerCoordinateSystem);
        });
    }
}

3.2 EchartsLayer

EchartsLayer类负责在 Cesium 场景中创建和管理 ECharts 图表。

export default class EchartsLayer {
    constructor(viewer, option) {
        this._viewer = viewer;
        this._isRegistered = false;
        this._chartLayer = this._createLayerContainer();
        this.option = option;
        this._chartLayer.setOption(option);
        this.resizeFuc = null;
        this.resize();
    }

    _createLayerContainer() {
        let scene = this._viewer.scene;
        let container = document.createElement('div');
        container.style.position = 'absolute';
        container.style.top = '60px';
        container.style.left = '0px';
        container.style.right = '0px';
        container.style.bottom = '0px';
        container.style.width = "100vw";
        container.style.height = scene.canvas.height + "px";
        container.style.pointerEvents = "none";
        this._viewer.container.appendChild(container);
        this._echartsContainer = container;
        if (!echarts.glMap) {
            echarts.glMap = scene;
            this._register();
        }
        return echarts.init(container);
    }

    _register() {
        if (this._isRegistered) return;
        echarts.registerCoordinateSystem("GLMap", new RegisterCoordinateSystem(echarts.glMap));
        echarts.registerAction({
            type: "GLMapRoam",
            event: "GLMapRoam",
            update: "updateLayout"
            // eslint-disable-next-line @typescript-eslint/no-empty-function
        }, function (e, t) {});
        echarts.extendComponentModel({
            type: "GLMap",
            getBMap: function () {
                return this.__GLMap;
            },
            defaultOption: {
                roam: false
            }
        });
        echarts.extendComponentView({
            type: "GLMap",
            init: function (echartModel, api) {
                this.api = api;
                echarts.glMap.postRender.addEventListener(this.moveHandler, this);
            },
            moveHandler: function (e, t) {
                this.api.dispatchAction({
                    type: "GLMapRoam"
                });
            },
            // eslint-disable-next-line @typescript-eslint/no-empty-function
            render: function (e, t, i) {},
            dispose: function () {
                echarts.glMap.postRender.removeEventListener(this.moveHandler, this);
            }
        });
        this._isRegistered = true;
    }

    dispose() {
        this._echartsContainer && (this._viewer.container.removeChild(this._echartsContainer), this._echartsContainer = null);
        this._chartLayer && (this._chartLayer.dispose(), this._chartLayer = null);
        this._isRegistered = false;
    }

    destroy() {
        window.removeEventListener('resize', this.resizeFuc);
        this.dispose();
    }

    updateEchartsLayer(option) {
        this._chartLayer && this._chartLayer.setOption(option);
    }

    getMap() {
        return this._viewer;
    }

    getEchartsLayer() {
        return this._chartLayer;
    }

    show() {
        this._echartsContainer && (this._echartsContainer.style.visibility = "visible");
    }

    hide() {
        this._echartsContainer && (this._echartsContainer.style.visibility = "hidden");
    }

    resize() {
        const me = this;
        window.addEventListener('resize', this.resizeFuc = () => {
            const scene = me._viewer.scene;
            me._echartsContainer.style.width = scene.canvas.style.width;
            me._echartsContainer.style.height = scene.canvas.style.height;
            me._chartLayer.resize();
        });
    }
}

3.3 addEcharts函数

addEcharts函数用于在 Cesium 场景中添加 ECharts 图表。

const addEcharts = (viewer) => {
    if (!layer) {
        layer = new EchartsLayer(viewer, geoOption);
        console.log("addEcharts", layer);
    }
    viewer.camera.setView({
        destination: Cesium.Cartesian3.fromDegrees(117.16, 32.71, 10000000.0)
    });
}

4. 原理讲解

4.1 坐标系转换

RegisterCoordinateSystem类的dataToPointpointToData方法,实现地理坐标与屏幕坐标的相互转换。其中,fixLat方法用于修正纬度值,防止其超出有效范围,确保图表在 3D 场景中的精准定位。

4.2 交互支持

注册自定义的GLMapRoam动作,专门处理地图的平移和缩放事件。当用户在 Cesium 场景中执行平移或缩放操作时,会触发GLMapRoam事件,进而更新 ECharts 图表的位置和大小,保证图表与 Cesium 场景的同步变化。在GLMap组件视图的init方法中,注册postRender事件的处理函数moveHandler。每次 Cesium 场景渲染完成后,moveHandler会被调用,触发GLMapRoam事件,更新 ECharts 图表的布局。

4.3 动态更新

updateEchartsLayer方法允许动态修改 ECharts 图表的配置选项。通过调用setOption方法,可以实时更新图表的数据和样式,及时反映数据的变化情况。

4.4 资源管理

dispose方法负责清理 ECharts 容器和实例,移除相关事件监听器,有效避免内存泄漏。当不再需要 ECharts 图表时,调用该方法释放资源。destroy方法不仅移除窗口大小调整事件监听器,还会调用dispose方法,确保在组件销毁时,所有相关资源都能被彻底清理。

4.5 窗口大小调整

resize方法监听窗口大小变化事件,动态调整 ECharts 容器的尺寸。当窗口大小改变时,该方法会更新容器的宽度和高度,并调用 ECharts 图表的resize方法,使图表始终与 Cesium 场景保持同步显示。

5. 总结

借助EchartsLayer类,成功将 ECharts 图表嵌入 Cesium 场景,实现数据的动态可视化展示。RegisterCoordinateSystem类负责坐标转换和地图偏移处理,保证图表在 Cesium 场景中的精准定位。通过注册自定义动作和扩展 ECharts 组件,实现了地图的平移、缩放等交互操作,使图表与 Cesium 场景协同工作。此外,完善的资源管理和窗口大小调整功能,进一步提升了应用的稳定性和性能。

图片描述
我是 安大桃子,专注前端代码世界的‘筑梦师’。这一趟前端开发的代码之旅暂时告一段落啦,希望文章中的代码思路和技巧能成为你构建前端项目的得力工具。前端技术日新月异,咱们一起在这充满挑战与机遇的领域持续探索,下次代码冒险,咱们不见不散!

源码仓库:完整源码和使用案例请查看此处,里面还有其他相关案例,如果觉得有用,欢迎给个star