Cesium学习教程系列(4)---图层管理功能封装

96 阅读3分钟

本学习系列将以Cesium + Vue3 + Vite +Typescript+elementplus作为主要技术栈展开,后续会循序渐进,持续探索Cesium的高级功能。 更多详情请关注原文点击本链接查看原文

Cesium能够加载各种GIS数据,如wms、wmts、xyz切片、kml、czml、gltf、glb、3dtiles等。本篇介绍如何将这些加载的数据图层进行封装管理,并以管理底图数据和注记为例进行介绍。

1、新建CesiumViewer类,很多功能都需要用到Cesium里的viewer,这里先简单将其封装,后续待功能增加后再对其进行持续优化。其他功能如果用到viewer只需将CesiumViewer.viewer注入进去即可。

    import * as Cesium from "cesium";
    import MouseStatusInViewer from "./MouseStatusInViewer";
    import { CESIUM_TOKEN, TERRAIN_URL } from "@/system/Config/SystemConfig";
    export default class CesiumViewer {
        public static viewer: Cesium.Viewer | undefined
        public static handler: Cesium.ScreenSpaceEventHandler
        public static ellipsoid:Cesium.Ellipsoid
        constructor() { }
        static async CreateViewer(containerId: string) {
            Cesium.Ion.defaultAccessToken = CESIUM_TOKEN
            CesiumViewer.viewer = await CesiumViewer.InitViewer(containerId)
            CesiumViewer.handler = new Cesium.ScreenSpaceEventHandler(CesiumViewer.viewer.canvas)
            CesiumViewer.ellipsoid = CesiumViewer.viewer.scene.globe.ellipsoid
        }
        static async InitViewer(containerId: string) {
            const viewer = new Cesium.Viewer(containerId, {
                baseLayerPicker: false, // 基础影响图层选择器
                navigationHelpButton: false, // 导航帮助按钮
                animation: false, // 动画控件
                timeline: false, // 时间控件
                shadows: true, // 显示阴影
                shouldAnimate: true, // 模型动画效果 大气
                skyBox: false,
                infoBox: false, // 显示 信息框
                fullscreenButton: true, // 是否显示全屏按钮
                homeButton: false, // 是否显示首页按钮
                geocoder: false, // 默认不显示搜索栏地址
                sceneModePicker: false, // 是否显示视角切换按钮
                selectionIndicator: false, // 是否显示选择框
            });
            viewer.terrainProvider = await Cesium.CesiumTerrainProvider.fromUrl(TERRAIN_URL, {
                requestWaterMask: true,
                requestVertexNormals: true,
            })
            viewer.scene.globe.depthTestAgainstTerrain = true
            // 暂时隐藏 fps
            viewer.scene.debugShowFramesPerSecond = false
            const creditContainer = viewer.cesiumWidget.creditContainer as HTMLElement
            creditContainer.style.display = 'none' // 隐藏logo
            //抗锯齿  
            viewer.scene.postProcessStages.fxaa.enabled = true;
            viewer.camera.flyTo({
                destination: Cesium.Cartesian3.fromDegrees(110.535314, 40.960521, 20000000.0),
                duration: 2
            })
            viewer.screenSpaceEventHandler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK)
          
            return viewer
        }
     
    }

2、新建LayerManager类,用于管理加载的GIS图层。通过layersIdMap 建立ID与数据图层的映射。其他功能模块可能也会用到LayerManager对象,因此这里用了单例模式。通过id对图层进行删除操作。


    import { ILayerItem } from "@/system/Common/interfaces";
    import * as Cesium from "cesium";
    export default class LayerManager {
        private static instance: LayerManager;
        private viewer: Cesium.Viewer
        private layersIdMap = new Map<string, ILayerItem & { handle?: any }>()
        constructor(viewer: Cesium.Viewer) {
            this.viewer = viewer
        }
        // 获取单例实例
        public static getInstance(viewer?: Cesium.Viewer): LayerManager {
            if (!LayerManager.instance) {
                if (!viewer) throw new Error("LayerManager需要传入viewer实例");
                LayerManager.instance = new LayerManager(viewer);
            }
            return LayerManager.instance;
        }
        /* 添加图层 */
        async Add(item: ILayerItem): Promise<void> {
            if (this.layersIdMap.has(item.id)) {
                return
            }
            const { type, show = true, alpha = 1, zIndex = 0 } = item
            let handle: any
            let provider: Cesium.ImageryProvider
            switch (type) {
                /* ---- 影像 ---- */
                case 'imagery_wmts':
                    provider = new Cesium.WebMapTileServiceImageryProvider({
                        url: item.url!,
                        layer: item.layer!,
                        style: item.style || 'default',
                        format: item.format || 'image/png',
                        tileMatrixSetID: item.tileMatrixSetID!,
                    });
                    handle = this.AddImageryLayer(provider, show, alpha, zIndex)
                    break
                case 'imagery_wms':
                    provider = new Cesium.WebMapServiceImageryProvider({
                        url: item.url!,
                        layers: item.layer!,
                    });
                    handle = this.AddImageryLayer(provider, show, alpha, zIndex)
                    break
                case 'imagery_xyz':
                    provider = new Cesium.UrlTemplateImageryProvider({ url: item.url! })
                    handle = this.AddImageryLayer(provider, show, alpha, zIndex)
                    break
                /* ---- 地形 ---- */
                case 'terrain':
                    this.viewer.terrainProvider = await Cesium.CesiumTerrainProvider.fromUrl(
                        item.url!,
                        {
                            requestWaterMask: item.requestWaterMask ?? false,
                            requestVertexNormals: item.requestVertexNormals ?? false,
                        }
                    );
                    handle = { __terrain: true }; // 占位,方便 remove 时切回 ellipsoid
                    break
                /* ---- 矢量 ---- */
                case 'geojson':
                    handle = await Cesium.GeoJsonDataSource.load(item.url!, {
                        clampToGround: true,
                    });
                    (handle as Cesium.GeoJsonDataSource).show = show;
                    this.viewer.dataSources.add(handle)
                    break
                case 'kml':
                    handle = await Cesium.KmlDataSource.load(item.url!, {
                        clampToGround: true,
                    });
                    (handle as Cesium.KmlDataSource).show = show
                    this.viewer.dataSources.add(handle)
                    break
                case 'czml':
                    handle = await Cesium.CzmlDataSource.load(item.url!);
                    (handle as Cesium.CzmlDataSource).show = show
                    this.viewer.dataSources.add(handle)
                    break
                /* ---- 3DTiles ---- */
                case '3dtiles':
                    handle = await Cesium.Cesium3DTileset.fromUrl(item.url!)
                    handle.show = show;
                    this.viewer.scene.primitives.add(handle)
                    break
                default:
                    throw new Error(`[LayerManager] 未知类型 ${type}`)
            }
            this.layersIdMap.set(item.id, { ...item, handle })
        }
        /**增加影像数据 */
        AddImageryLayer(provider: Cesium.ImageryProvider, show = true, alpha = 1, zIndex = 0) {
            let handle = this.viewer.imageryLayers.addImageryProvider(provider)
            handle.alpha = alpha
            handle.show = show
            this.viewer.imageryLayers.raiseToTop(handle) // 先置顶,再按 zIndex 微调
            if (zIndex)
                this.SetImageryLayerIndex(handle, zIndex)
            return handle
        }
        /**设置影像数据的叠加顺序 */
        SetImageryLayerIndex(layer: Cesium.ImageryLayer, targetIndex: number) {
            const imageryLayers = this.viewer.imageryLayers
            const curIndex = imageryLayers.indexOf(layer)
            if (curIndex === targetIndex) return
            if (targetIndex === 0) {
                imageryLayers.lowerToBottom(layer)          // 直接到底
            } else if (targetIndex === imageryLayers.length - 1) {
                imageryLayers.raiseToTop(layer)            // 直接到顶
            } else {
                // 逐层移动,直到索引匹配
                while (imageryLayers.indexOf(layer) > targetIndex) imageryLayers.lower(layer)
                while (imageryLayers.indexOf(layer) < targetIndex) imageryLayers.raise(layer)
            }
        }
        /* 移除图层 */
        Remove(id: string): void {
            const item = this.layersIdMap.get(id)
            console.log(item)
            if (!item) return;
            const { type, handle } = item;
            switch (type) {
                case 'imagery_wms':
                case 'imagery_wmts':
                case 'imagery_xyz':
                    this.viewer.imageryLayers.remove(handle)
                    break
                case 'terrain':
                    // 回到默认椭球
                    this.viewer.terrainProvider = new Cesium.EllipsoidTerrainProvider()
                    break
                case 'geojson':
                case 'kml':
                case 'czml':
                    this.viewer.dataSources.remove(handle)
                    break;
                case '3dtiles':
                    this.viewer.scene.primitives.remove(handle)
                    break;
            }
            this.layersIdMap.delete(id);
        }
        RemoveAllImageLayer() {
            this.viewer.imageryLayers.removeAll()
        }
    }

3、管理底图数据和注记。通过mLayerManager = LayerManager.getInstance(viewer)创建对象,然后在UI中调用。

    <template>
        <div class="layer-manager-container">
            <span class="layer-manager-title"> 全球数据 </span>
            <div style="text-align: left;">
                <div class="layer-items">
                    <span>底图</span>
                    <div style="margin-left:20px;margin-bottom: 10px;margin-top:5px;">
                        <el-radio-group v-model="imageDataRadio" size="small" @change="changeImageData">
                            <el-radio-button value="bingImage">Bing影像</el-radio-button>
                            <el-radio-button value="tdtImage"> 天地图-影像</el-radio-button>
                            <el-radio-button value="tdtVec">天地图-地图</el-radio-button>
                        </el-radio-group>
                    </div>
                </div>
                <div class="layer-items">
                    <span>注记</span>
                    <div>
                        <el-checkbox v-model="tdtAnnotationChecked" label="天地图-注记" size="large" @change="changeTdtAnnotation"/>
                    </div>
                </div>
            </div>
        </div>
    </template>
    <script lang="ts" setup>
    import { LayerIdFlag, tdtAnnotationInfo, tdtImageLayerInfo, tdtVecLayerInfo } from '@/system/LayerManager/LayerConfig'
    import LayerManager from '@/system/LayerManager/LayerManager'
    import CesiumViewer from '@/Viewer/CesiumViewer'
    const imageDataRadio = ref('bingImage')
    const tdtAnnotationChecked = ref(false)
    const viewer = CesiumViewer.viewer
    let mLayerManager: LayerManager | null = null
    onMounted(() => {
        mLayerManager = LayerManager.getInstance(viewer!)
    })
    const changeImageData = (val: string | number | boolean | undefined) => {
        imageDataRadio.value = val as string
        // mLayerManager?.RemoveAllImageLayer()
        switch (val) {
            case 'bingImage':
                mLayerManager?.Remove(LayerIdFlag.TDT_IMAGERY_WMTS)
                mLayerManager?.Remove(LayerIdFlag.TDT_VECTOR_WMTS)
                break
            case 'tdtImage':
                mLayerManager?.Remove(LayerIdFlag.TDT_VECTOR_WMTS)
                mLayerManager?.Add(tdtImageLayerInfo)
                break
            case 'tdtVec':
                mLayerManager?.Remove(LayerIdFlag.TDT_IMAGERY_WMTS)
                mLayerManager?.Add(tdtVecLayerInfo)
                break
            default:
                break
        }
    }
    const changeTdtAnnotation = (val: string | number | boolean) => {
        tdtAnnotationChecked.value = val as boolean
        if (val) {
            mLayerManager?.Add(tdtAnnotationInfo)
        } else {
            mLayerManager?.Remove(LayerIdFlag.TDT_ANNOTATION_WMTS)
        }
    }
    </script>
    <style lang="scss" scoped>
    .layer-manager-container {
        padding: 10px;
        .layer-manager-title {
            font-size: 16px;
            font-weight: bold;
            margin-bottom: 10px;
        }
        .image-radio-container {
            text-align: left;
        }
        .layer-items {
            .el-checkbox {
                margin-left: 20px;
                height: 30px;
            }
            :deep(.el-checkbox__label) {
                color: white;
            }
        }
    }
    </style>

效果如下: image.png