本学习系列将以Cesium + Vue3 + Vite +Typescript+elementplus作为主要技术栈展开,后续会循序渐进,持续探索Cesium的高级功能,相关源码全部免费公开。 请关注文中图片右下角gzh,关注后私信“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>
效果如下:
