按功能拆分CesiumUtils类

按功能拆分CesiumUtils类,更易于管理
This commit is contained in:
wzy-warehouse
2026-04-08 18:59:00 +08:00
parent 1448aed7f0
commit c285c752fc
8 changed files with 1572 additions and 1506 deletions
+4 -1
View File
@@ -8,6 +8,9 @@
"2e8111f9bc84149cbf24f562ed4e9229",
"88055d3d7f13f8f7e6e8eeb67cf6d78a"
],
"cesiumIonDefaultAccessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI1ZDBjZjAxOS0wMDhhLTRmZjEtYjNmOC1iNmM2ZmY2ZmQ1N2IiLCJpZCI6MjAxMDI1LCJpYXQiOjE3MTAxNTgxNjJ9.mdbJYEzXQkBnHNqpozz7MvZjJ_X9a3JZRGPA-ytGhLI",
"cesiumIonDefaultAccessToken": [
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI1ZDBjZjAxOS0wMDhhLTRmZjEtYjNmOC1iNmM2ZmY2ZmQ1N2IiLCJpZCI6MjAxMDI1LCJpYXQiOjE3MTAxNTgxNjJ9.mdbJYEzXQkBnHNqpozz7MvZjJ_X9a3JZRGPA-ytGhLI",
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJiNjczZTVlMy1kNDEwLTRhZWItYWM0NS1mNjYxMzJjODMwYTQiLCJpZCI6MzIxMzI2LCJpYXQiOjE3NzU2NDU1OTd9._MPcZQsxK1dGPl8IMVhKHV3PIPu4-TaOUgzsUUOP6WE"
],
"defaultPosition": [108.948024, 34.263161, 200000]
}
+57
View File
@@ -0,0 +1,57 @@
import {
Cartesian3,
Cartographic,
Math as CesiumMath,
} from 'cesium'
import type { Viewer } from 'cesium'
/**
* 相机控制器
*/
export class CameraController {
#viewer: Viewer
constructor(viewer: Viewer) {
this.#viewer = viewer
}
/**
* 飞行到目标位置
* @param target - 目标位置 [经度, 纬度, 高度] 或 Cartesian3
* @param duration - 飞行持续时间(秒,默认 2)
*/
flyToTarget(target: [number, number, number] | Cartesian3, duration = 2): void {
const position = this.#convertPosition(target)
const cartographic = Cartographic.fromCartesian(position)
this.#viewer.camera.flyTo({
destination: Cartesian3.fromDegrees(
CesiumMath.toDegrees(cartographic.longitude),
CesiumMath.toDegrees(cartographic.latitude),
cartographic.height,
),
duration,
})
}
/**
* 调整视角到目标位置
* @param target - 目标位置 [经度, 纬度, 高度] 或 Cartesian3
*/
viewToTarget(target: [number, number, number] | Cartesian3): void {
const position = this.#convertPosition(target)
this.#viewer?.camera.setView({
destination: position,
orientation: {
heading: CesiumMath.toRadians(0),
pitch: CesiumMath.toRadians(-90),
roll: 0.0,
},
})
}
// ===================== 私有方法 =====================
#convertPosition(pos: Cartesian3 | [number, number, number]): Cartesian3 {
return Array.isArray(pos) ? Cartesian3.fromDegrees(pos[0], pos[1], pos[2] || 0) : pos
}
}
File diff suppressed because it is too large Load Diff
+184
View File
@@ -0,0 +1,184 @@
import {
Viewer,
SceneMode,
Ion,
WebMapTileServiceImageryProvider,
ImageryProvider,
createWorldTerrain,
} from 'cesium'
import type { CesiumInitOptions } from '@/types/cesium/CesiumInitOptions'
import config from '@/config/config.json'
/**
* Cesium Viewer 管理器
*/
export class CesiumViewerManager {
#viewer: Viewer | null = null
#currentTokenIndex: number = 0
#failedTokens: Set<number> = new Set()
constructor() {
this.#initializeToken()
}
/**
* 初始化并设置有效的 Token
*/
#initializeToken(): void {
const tokens = config.cesiumIonDefaultAccessToken
if (!Array.isArray(tokens) || tokens.length === 0) {
console.warn('Cesium Ion Token 配置为空')
return
}
Ion.defaultAccessToken = tokens[this.#currentTokenIndex]
}
/**
* 切换到下一个可用的 Token
* @returns 是否成功切换
*/
#switchToNextToken(): boolean {
const tokens = config.cesiumIonDefaultAccessToken
if (!Array.isArray(tokens) || tokens.length <= 1) {
return false
}
this.#failedTokens.add(this.#currentTokenIndex)
for (let i = 1; i < tokens.length; i++) {
const nextIndex = (this.#currentTokenIndex + i) % tokens.length
if (!this.#failedTokens.has(nextIndex)) {
this.#currentTokenIndex = nextIndex
Ion.defaultAccessToken = tokens[nextIndex]
console.log(`已切换到 Cesium Ion Token #${nextIndex + 1}`)
return true
}
}
console.warn('所有 Cesium Ion Token 均已失效')
return false
}
/**
* 初始化 Cesium Viewer
* @param options - Viewer 初始化选项
* @param tdMapToken - 天地图 Token 数组(可选)
* @param type - 底图类型:0=影像图,1=矢量图(默认 0)
*/
initCesiumViewer(options: CesiumInitOptions, tdMapToken?: string[], type: number = 0): void {
const defaultOptions: CesiumInitOptions = {
containerId: options.containerId,
shouldAnimate: true,
baseLayerPicker: false,
timeline: false,
animation: false,
infoBox: false,
navigationHelpButton: false,
fullscreenButton: false,
homeButton: false,
scene3DOnly: false,
sceneModePicker: false,
geocoder: false,
sceneMode: SceneMode.SCENE3D,
}
const finalOptions = { ...defaultOptions, ...options }
const container = document.getElementById(finalOptions.containerId)
if (!container) {
throw new Error(`Cesium 容器 #${finalOptions.containerId} 不存在`)
}
const viewer = new Viewer(container, {
...finalOptions,
terrainProvider: createWorldTerrain(),
selectionIndicator: false,
baseLayerPicker: false,
contextOptions: {
webgl: {
alpha: true,
depth: false,
stencil: true,
antialias: true,
premultipliedAlpha: true,
preserveDrawingBuffer: true,
failIfMajorPerformanceCaveat: true,
},
allowTextureFilterAnisotropic: true,
},
})
// 性能优化配置
viewer.scene.globe.depthTestAgainstTerrain = false
viewer.scene.fog.enabled = false
viewer.scene.globe.enableLighting = false
viewer.shadows = false
const creditContainer = viewer.cesiumWidget.creditContainer as HTMLElement
creditContainer.style.display = 'none'
// 添加底图
this.#createImageryProviders(type, tdMapToken || config.tdMapToken).forEach((provider) => {
viewer.imageryLayers.addImageryProvider(provider)
})
this.#viewer = viewer
}
/**
* 销毁 Cesium Viewer
* @param clearAllResources - 清理所有资源的回调函数
*/
destroyCesiumViewer(clearAllResources: () => void): void {
if (this.#viewer) {
clearAllResources()
this.#viewer.destroy()
this.#viewer = null
}
}
/**
* 获取 Viewer 实例
* @returns Viewer 实例,如果未初始化则返回 null
*/
getViewer(): Viewer | null {
return this.#viewer
}
/**
* 创建底图 ImageryProvider
*/
#createImageryProviders(type: number, tdMapToken: string[]): ImageryProvider[] {
const option = {
tileMatrixSetID: 'w',
format: 'tiles',
style: 'default',
minimumLevel: 0,
maximumLevel: 18,
credit: 'Tianditu',
subdomains: ['t0', 't1', 't2', 't3', 't4', 't5', 't6', 't7'],
}
const token = tdMapToken[Math.floor(Math.random() * tdMapToken.length)]
if (type === 0) {
const imageryProvider = new WebMapTileServiceImageryProvider({
url: `https://{s}.tianditu.gov.cn/img_w/wmts?tk=${token}`,
layer: 'img',
...option,
})
const annotationProvider = new WebMapTileServiceImageryProvider({
url: `https://{s}.tianditu.gov.cn/cia_w/wmts?tk=${token}`,
layer: 'cia',
...option,
})
return [imageryProvider, annotationProvider]
} else {
const vectorProvider = new WebMapTileServiceImageryProvider({
url: `https://{s}.tianditu.gov.cn/vec_w/wmts?tk=${token}`,
layer: 'vec',
...option,
})
return [vectorProvider]
}
}
}
+284
View File
@@ -0,0 +1,284 @@
import {
Entity,
Cartesian3,
Color,
PointGraphics,
PolylineGraphics,
BillboardGraphics,
HeightReference,
VerticalOrigin,
HorizontalOrigin,
ColorMaterialProperty,
PolygonHierarchy,
PolygonGraphics,
ConstantProperty,
GridMaterialProperty,
} from 'cesium'
import type { EntityOptions } from '@/types/cesium/EntityOptions'
import type { Viewer } from 'cesium'
/**
* 实体管理器
*/
export class EntityManager {
#viewer: Viewer
#defaultEntityIds = new Set<string>()
#customEntityIds = new Set<string>()
constructor(viewer: Viewer) {
this.#viewer = viewer
}
/**
* 添加实体
* @param entityOptions - 实体配置选项
* @returns 创建的 Entity 实例
*/
addCesiumEntity(entityOptions: EntityOptions): Entity {
const { id, position, attributes = {}, default: isDefault = false } = entityOptions
if (!id) throw new Error('实体 id 为必填项')
if (!position) throw new Error('实体 position 为必填项')
this.#validateUniqueId(id)
const entity = new Entity({
id,
position: this.#convertPosition(position),
...attributes,
})
this.#configureEntityGraphics(entity, entityOptions)
this.#viewer.entities.add(entity)
this.#storeEntityId(id, isDefault)
return entity
}
/**
* 查询实体
* @param entityId - 实体 ID
* @returns Entity 实例,不存在则返回 null
*/
getCesiumEntityById(entityId: string): Entity | null {
if (!this.#entityExists(entityId)) return null
return this.#viewer.entities.getById(entityId) || null
}
/**
* 删除实体
* @param entityId - 实体 ID
* @returns 是否删除成功
*/
removeCesiumEntity(entityId: string): boolean {
if (!this.#entityExists(entityId)) {
console.warn(`实体 ID ${entityId} 不存在`)
return false
}
const entity = this.#viewer.entities.getById(entityId)
if (entity) {
this.#viewer.entities.remove(entity)
this.#removeEntityId(entityId)
return true
}
return false
}
/**
* 批量删除实体
* @param entityIds - 实体 ID 数组
*/
batchRemoveCesiumEntities(entityIds: string[]): void {
entityIds.forEach((id) => this.removeCesiumEntity(id))
}
/**
* 清除实体
* @param clearType - 清除类型:'default'=默认实体,'custom'=自定义实体,'all'=所有实体(默认 'custom'
*/
clearAllEntities(clearType: 'default' | 'custom' | 'all' = 'custom'): void {
const targetIds = this.#getTargetIdsByType(clearType)
targetIds.forEach((id) => {
const entity = this.#viewer.entities.getById(id)
if (entity) this.#viewer.entities.remove(entity)
})
this.#clearCollectionsByType(clearType)
}
/**
* 获取所有实体ID
* @param clearType - 类型:'default'=默认实体,'custom'=自定义实体,'all'=所有实体(默认 'all'
* @returns 实体 ID 集合
*/
getEntityIds(clearType: 'default' | 'custom' | 'all' = 'all'): Set<string> {
return this.#getTargetIdsByType(clearType)
}
// ===================== 私有方法 =====================
#configureEntityGraphics(entity: Entity, options: EntityOptions): void {
switch (options.type) {
case 'point': {
const {
color = Color.RED,
pixelSize = 8,
outlineColor = Color.WHITE,
outlineWidth = 1,
heightReference = HeightReference.CLAMP_TO_GROUND,
} = options.pointOptions || {}
entity.point = new PointGraphics({
color,
pixelSize,
outlineColor,
outlineWidth,
heightReference,
})
break
}
case 'polyline': {
const {
positions,
color = Color.BLUE,
width = 3,
clampToGround = false,
} = options.polylineOptions || {}
if (!positions) throw new Error('线实体必须传入 polylineOptions.positions')
entity.polyline = new PolylineGraphics({
positions: this.#convertPositionArray(positions),
material: new ColorMaterialProperty(color),
width,
clampToGround,
})
break
}
case 'billboard': {
const {
image,
scale = 1,
color = Color.WHITE,
verticalOrigin = VerticalOrigin.CENTER,
horizontalOrigin = HorizontalOrigin.CENTER,
heightReference = HeightReference.CLAMP_TO_GROUND,
} = options.billboardOptions || {}
if (!image) throw new Error('Billboard 实体必须传入 billboardOptions.image')
entity.billboard = new BillboardGraphics({
image,
scale,
color,
verticalOrigin,
horizontalOrigin,
heightReference,
})
break
}
case 'polygon': {
const {
hierarchy,
outline = true,
outlineColor = Color.BLACK,
outlineWidth = 1,
height = 0,
extrudedHeight,
heightReference = HeightReference.CLAMP_TO_GROUND,
material = new GridMaterialProperty({
color: Color.GREEN.withAlpha(0.3),
cellAlpha: 0.2,
lineCount: new Cartesian3(8, 8, 0) as any,
lineThickness: new Cartesian3(2.0, 2.0, 0) as any,
}),
} = options.polygonOptions || {}
if (!hierarchy) throw new Error('多边形实体必须传入 polygonOptions.hierarchy')
entity.polygon = new PolygonGraphics({
hierarchy: this.#createConstantProperty(this.#processHierarchy(hierarchy)),
material: material,
outline: this.#createConstantProperty(outline),
outlineColor: this.#createConstantProperty(outlineColor),
outlineWidth: this.#createConstantProperty(outlineWidth),
height: this.#createConstantProperty(height),
extrudedHeight:
extrudedHeight !== undefined ? this.#createConstantProperty(extrudedHeight) : undefined,
heightReference,
})
break
}
default:
throw new Error(`不支持的实体类型:${options.type}`)
}
}
#processHierarchy(
hier: PolygonHierarchy | Cartesian3[] | [number, number][] | [number, number, number][],
): PolygonHierarchy {
if (hier instanceof PolygonHierarchy) return hier
if (!Array.isArray(hier) || hier.length < 3) {
throw new Error('多边形层级必须是非空数组且至少 3 个顶点')
}
const positions = hier.map((pos) => {
if (pos instanceof Cartesian3) return pos
if (Array.isArray(pos) && pos.length >= 2) {
return Cartesian3.fromDegrees(pos[0], pos[1], pos[2] || 0)
}
throw new Error(
`无效坐标格式:${JSON.stringify(pos)},应为 [经, 纬] 或 [经, 纬, 高] 或 Cartesian3`,
)
})
return new PolygonHierarchy(positions)
}
#createConstantProperty(value: unknown): ConstantProperty {
return new ConstantProperty(value)
}
#validateUniqueId(id: string): void {
if (this.#defaultEntityIds.has(id) || this.#customEntityIds.has(id)) {
throw new Error(`实体 ID ${id} 已存在`)
}
}
#entityExists(id: string): boolean {
return this.#defaultEntityIds.has(id) || this.#customEntityIds.has(id)
}
#storeEntityId(id: string, isDefault: boolean): void {
if (isDefault) {
this.#defaultEntityIds.add(id)
} else {
this.#customEntityIds.add(id)
}
}
#removeEntityId(id: string): void {
this.#defaultEntityIds.delete(id)
this.#customEntityIds.delete(id)
}
#getTargetIdsByType(clearType: 'default' | 'custom' | 'all'): Set<string> {
const targetIds = new Set<string>()
if (clearType === 'default' || clearType === 'all')
this.#defaultEntityIds.forEach((id) => targetIds.add(id))
if (clearType === 'custom' || clearType === 'all')
this.#customEntityIds.forEach((id) => targetIds.add(id))
return targetIds
}
#clearCollectionsByType(clearType: 'default' | 'custom' | 'all'): void {
if (clearType === 'default' || clearType === 'all') this.#defaultEntityIds.clear()
if (clearType === 'custom' || clearType === 'all') this.#customEntityIds.clear()
}
#convertPosition(pos: Cartesian3 | [number, number, number]): Cartesian3 {
return Array.isArray(pos) ? Cartesian3.fromDegrees(pos[0], pos[1], pos[2] || 0) : pos
}
#convertPositionArray(positions: (Cartesian3 | [number, number, number])[]): Cartesian3[] {
return positions.map((pos) => this.#convertPosition(pos))
}
}
+387
View File
@@ -0,0 +1,387 @@
import {
DataSource,
GeoJsonDataSource,
ConstantProperty,
ColorMaterialProperty,
LabelGraphics,
LabelStyle,
Cartesian2,
VerticalOrigin,
HorizontalOrigin,
Color,
ConstantPositionProperty,
Cartesian3,
Cartographic,
JulianDate,
} from 'cesium'
import type { CustomizeGeoJsonDataSource, GeoJsonOptions } from '@/types/cesium/GeoJsonOptions'
import type { LabelConfig } from '@/types/cesium/LabelConfig'
import type { Viewer, Entity } from 'cesium'
// 定义清除类型枚举
export type ClearType = 'default' | 'custom' | 'all'
/**
* GeoJSON 图层管理器
*/
export class GeoJsonManager {
#viewer: Viewer
#defaultGeoJsonMap = new Map<string, DataSource>()
#customGeoJsonMap = new Map<string, DataSource>()
// 默认配置
static readonly DEFAULT_OPTIONS: Required<GeoJsonOptions> = {
showName: false,
labelStyle: {
labelFont: '16px "微软雅黑"',
labelColor: Color.RED,
backgroundColor: Color.BLACK,
labelSize: 16,
horizontalOrigin: HorizontalOrigin.CENTER,
verticalOrigin: VerticalOrigin.CENTER,
labelOffset: new Cartesian2(0, -10),
},
polygonStyle: {
fill: true,
fillColor: Color.RED.withAlpha(0.3),
outline: true,
outlineColor: Color.BLACK,
outlineWidth: 2,
},
polylineStyle: {
width: 2,
material: Color.BLUE,
clampToGround: true,
},
pointStyle: {
pixelSize: 8,
color: Color.RED,
outlineColor: Color.WHITE,
outlineWidth: 2,
},
onComplete: () => {},
}
constructor(viewer: Viewer) {
this.#viewer = viewer
}
/**
* 添加 GeoJSON 图层
* @param layerId - 图层唯一标识
* @param geojsonData - GeoJSON 数据(路径、URL 或对象)
* @param isDefault - 是否为默认图层(默认 false)
* @param options - 配置选项(样式、标签等)
* @returns Promise<DataSource> 数据源实例
*/
async addGeoJsonLayer(
layerId: string,
geojsonData: CustomizeGeoJsonDataSource,
isDefault: boolean = false,
options?: GeoJsonOptions,
): Promise<DataSource> {
if (this.#exists(layerId)) throw new Error(`图层 ${layerId} 已存在`)
const opt = this.#mergeOptions(options)
// 加载并应用样式
const dataSource = await GeoJsonDataSource.load(geojsonData)
dataSource.entities.values.forEach((e) => this.#applyStyle(e, opt))
// 添加到地图
await this.#viewer.dataSources.add(dataSource)
isDefault
? this.#defaultGeoJsonMap.set(layerId, dataSource)
: this.#customGeoJsonMap.set(layerId, dataSource)
// 如果需要显示标签,调用 addLabelsToDataSource
if (opt.showName && opt.labelStyle) {
this.#addLabelsToDataSource(dataSource, {
labelText: opt.labelStyle.labelText,
labelFont: opt.labelStyle.labelFont,
labelColor: opt.labelStyle.labelColor,
labelSize: opt.labelStyle.labelSize,
labelOffset: opt.labelStyle.labelOffset,
horizontalOrigin: opt.labelStyle.horizontalOrigin,
verticalOrigin: opt.labelStyle.verticalOrigin,
backgroundColor: opt.labelStyle.backgroundColor,
center: opt.labelStyle.center,
})
}
opt.onComplete(dataSource)
return dataSource
}
/**
* 根据ID查询图层
* @param layerId - 图层 ID
* @returns DataSource 实例,不存在则返回 undefined
*/
getGeoJsonLayerById(layerId: string): DataSource | undefined {
return this.#getGeoJsonLayer(layerId).ds
}
/**
* 删除图层
* @param layerId - 图层 ID
* @returns 是否删除成功
*/
removeGeoJsonLayer(layerId: string): boolean {
const { isDefault, ds } = this.#getGeoJsonLayer(layerId)
if (!ds) return false
const removed = this.#viewer.dataSources.remove(ds, true)
removed &&
(isDefault
? this.#defaultGeoJsonMap.delete(layerId)
: this.#customGeoJsonMap.delete(layerId))
return removed
}
/**
* 批量添加GeoJSON图层
* @param layerIds - 图层 ID 数组
* @param geojsonDatas - GeoJSON 数据数组
* @param isDefaults - 是否为默认图层数组
* @param options - 配置选项数组
*/
batchAddGeoJsonLayers(
layerIds: string[],
geojsonDatas: CustomizeGeoJsonDataSource[],
isDefaults: boolean[],
options?: GeoJsonOptions[],
): void {
layerIds.forEach((id, index) =>
this.addGeoJsonLayer(id, geojsonDatas?.[index], isDefaults?.[index], options?.[index]),
)
}
/**
* 批量删除
* @param layerIds - 图层 ID 数组
*/
batchRemoveGeoJsonLayers(layerIds: string[]): void {
layerIds.forEach((id) => this.removeGeoJsonLayer(id))
}
/**
* 清空图层
* @param clearType - 清除类型:'default'=默认图层,'custom'=自定义图层,'all'=所有图层(默认 'custom'
*/
clearAllGeoJsonLayers(clearType: ClearType = 'custom'): void {
const maps = {
default: this.#defaultGeoJsonMap,
custom: this.#customGeoJsonMap,
all: new Map([...this.#defaultGeoJsonMap, ...this.#customGeoJsonMap]),
}
maps[clearType].forEach((ds) => this.#viewer.dataSources.remove(ds, true))
clearType === 'all'
? (this.#defaultGeoJsonMap.clear(), this.#customGeoJsonMap.clear())
: maps[clearType].clear()
}
/**
* 显示图层
* @param layerId - 图层 ID
* @returns 是否操作成功
*/
showGeoJsonLayer(layerId: string): boolean {
const ds = this.getGeoJsonLayerById(layerId)
if (!ds) return false
ds.show = true
ds.entities.values.forEach(
(e) => e.label && (e.label.show = new ConstantProperty(true)),
)
return true
}
/**
* 隐藏图层
* @param layerId - 图层 ID
* @returns 是否操作成功
*/
hideGeoJsonLayer(layerId: string): boolean {
const ds = this.getGeoJsonLayerById(layerId)
if (!ds) return false
ds.show = false
ds.entities.values.forEach(
(e) => e.label && (e.label.show = new ConstantProperty(false)),
)
return true
}
/**
* 切换显隐
* @param layerId - 图层 ID
* @returns 切换后的显示状态,图层不存在则返回 null
*/
toggleGeoJsonLayer(layerId: string): boolean | null {
const ds = this.getGeoJsonLayerById(layerId)
if (!ds) return null
const state = !ds.show
ds.show = state
ds.entities.values.forEach(
(e) => e.label && (e.label.show = new ConstantProperty(state)),
)
return state
}
/**
* 批量显示
* @param layerIds - 图层 ID 数组
* @returns 成功显示的图层数量
*/
batchShowGeoJsonLayers(layerIds: string[]): number {
return layerIds.reduce((n, id) => n + (this.showGeoJsonLayer(id) ? 1 : 0), 0)
}
/**
* 批量隐藏
* @param layerIds - 图层 ID 数组
* @returns 成功隐藏的图层数量
*/
batchHideGeoJsonLayers(layerIds: string[]): number {
return layerIds.reduce((n, id) => n + (this.hideGeoJsonLayer(id) ? 1 : 0), 0)
}
/**
* 获取显示状态
* @param layerId - 图层 ID
* @returns 显示状态,图层不存在则返回 null
*/
getGeoJsonLayerVisibility(layerId: string): boolean | null {
const ds = this.getGeoJsonLayerById(layerId)
return ds ? ds.show : null
}
// ===================== 私有方法 =====================
/** 图层是否存在 */
#exists(layerId: string): boolean {
return this.#defaultGeoJsonMap.has(layerId) || this.#customGeoJsonMap.has(layerId)
}
/** 合并用户配置 + 默认配置 */
#mergeOptions(options?: GeoJsonOptions): Required<GeoJsonOptions> {
return {
...GeoJsonManager.DEFAULT_OPTIONS,
...options,
labelStyle: { ...GeoJsonManager.DEFAULT_OPTIONS.labelStyle, ...options?.labelStyle },
polygonStyle: { ...GeoJsonManager.DEFAULT_OPTIONS.polygonStyle, ...options?.polygonStyle },
polylineStyle: { ...GeoJsonManager.DEFAULT_OPTIONS.polylineStyle, ...options?.polylineStyle },
pointStyle: { ...GeoJsonManager.DEFAULT_OPTIONS.pointStyle, ...options?.pointStyle },
}
}
/** 统一应用样式到实体 */
#applyStyle(entity: Entity, options: Required<GeoJsonOptions>): void {
const { polygonStyle, polylineStyle, pointStyle } = options
if (entity.point) {
Object.assign(entity.point, {
pixelSize: new ConstantProperty(pointStyle.pixelSize),
color: new ConstantProperty(pointStyle.color),
outlineColor: new ConstantProperty(pointStyle.outlineColor),
outlineWidth: new ConstantProperty(pointStyle.outlineWidth),
})
}
if (entity.polyline) {
Object.assign(entity.polyline, {
width: new ConstantProperty(polylineStyle.width),
material: new ColorMaterialProperty(polylineStyle.material as Color),
clampToGround: new ConstantProperty(polylineStyle.clampToGround),
})
}
if (entity.polygon) {
Object.assign(entity.polygon, {
fill: new ConstantProperty(polygonStyle.fill),
material: new ColorMaterialProperty(polygonStyle.fillColor as Color),
outline: new ConstantProperty(polygonStyle.outline),
outlineColor: new ConstantProperty(polygonStyle.outlineColor),
outlineWidth: new ConstantProperty(polygonStyle.outlineWidth),
})
}
}
/**
* 添加标签到数据源
*/
#addLabelsToDataSource(dataSource: DataSource, label: LabelConfig): void {
const entities = dataSource.entities.values
entities.forEach((entity) => {
const labelText = label?.labelText || 'name'
const center: Cartesian3 | [number, number, number] =
label.center || this.#calculateTheCenterPositionOfTheSurface(entity)
// 设置中心位置
entity.position = new ConstantPositionProperty(this.#convertPosition(center))
if (labelText && entity.position) {
entity.label = new LabelGraphics({
text: new ConstantProperty(labelText),
font: new ConstantProperty(label?.labelFont || `${label?.labelSize || 16}px "微软雅黑"`),
fillColor: new ConstantProperty(label?.labelColor || Color.WHITE),
outlineColor: new ConstantProperty(Color.BLACK),
outlineWidth: new ConstantProperty(1),
style: new ConstantProperty(LabelStyle.FILL_AND_OUTLINE),
pixelOffset: new ConstantProperty(
new Cartesian2(label?.labelOffset?.x || 0, label?.labelOffset?.y || -20),
),
verticalOrigin: new ConstantProperty(label?.verticalOrigin || VerticalOrigin.CENTER),
horizontalOrigin: new ConstantProperty(
label?.horizontalOrigin || HorizontalOrigin.CENTER,
),
showBackground: new ConstantProperty(true),
backgroundColor: new ConstantProperty(label?.backgroundColor || Color.TRANSPARENT),
backgroundPadding: new ConstantProperty(new Cartesian2(5, 3)),
disableDepthTestDistance: new ConstantProperty(Number.POSITIVE_INFINITY),
})
}
})
}
/** 获取图层信息 */
#getGeoJsonLayer(layerId: string): { isDefault: boolean; ds: DataSource | undefined } {
const def = this.#defaultGeoJsonMap.get(layerId)
if (def) return { isDefault: true, ds: def }
const custom = this.#customGeoJsonMap.get(layerId)
return { isDefault: false, ds: custom }
}
/**
* 计算面要素的中心点作为标签位置
*/
#calculateTheCenterPositionOfTheSurface(entity: Entity): Cartesian3 {
if (entity.polygon) {
const hierarchy = entity.polygon.hierarchy?.getValue(JulianDate.now())
if (hierarchy) {
const positions = hierarchy.positions
if (positions && positions.length > 0) {
let lonSum = 0,
latSum = 0,
heightSum = 0
positions.forEach((pos: Cartesian3) => {
const cartographic = Cartographic.fromCartesian(pos)
lonSum += cartographic.longitude
latSum += cartographic.latitude
heightSum += cartographic.height || 0
})
const centerLon = lonSum / positions.length
const centerLat = latSum / positions.length
const centerHeight = heightSum / positions.length + 100
return Cartesian3.fromRadians(centerLon, centerLat, centerHeight)
}
}
}
return Cartesian3.ZERO
}
#convertPosition(pos: Cartesian3 | [number, number, number]): Cartesian3 {
return Array.isArray(pos) ? Cartesian3.fromDegrees(pos[0], pos[1], pos[2] || 0) : pos
}
}
+158
View File
@@ -0,0 +1,158 @@
import {
ImageryLayer,
ArcGisMapServerImageryProvider,
WebMapServiceImageryProvider,
WebMapTileServiceImageryProvider,
ImageryProvider,
} from 'cesium'
import type { LayerConfig } from '@/types/cesium/LayerConfig'
import type { Viewer } from 'cesium'
/**
* 图层管理器
*/
export class LayerManager {
#viewer: Viewer
#defaultLayerMap = new Map<string, ImageryLayer>()
#customLayerMap = new Map<string, ImageryLayer>()
constructor(viewer: Viewer) {
this.#viewer = viewer
}
/**
* 创建图层
* @param layerConfig - 图层配置
* @returns 创建的 ImageryLayer 实例,失败则返回 null
*/
createLayer(layerConfig: LayerConfig): ImageryLayer | null {
const { layers: layerKey, default: isDefault = false } = layerConfig
if (!layerKey) throw new Error('layers 参数未定义')
this.#validateUniqueLayerKey(layerKey)
const provider = this.#createImageryProvider(layerConfig)
if (!provider) return null
const layer = this.#viewer.imageryLayers.addImageryProvider(provider)
this.#storeLayer(layerKey, layer!, isDefault)
return layer!
}
/**
* 查询图层
* @param key - 图层 key
* @returns ImageryLayer 实例,不存在则返回 undefined
*/
getLayerByKey(key: string): ImageryLayer | undefined {
return this.#defaultLayerMap.get(key) || this.#customLayerMap.get(key)
}
/**
* 删除图层
* @param key - 图层 key
* @returns 是否删除成功
*/
removeLayerByKey(key: string): boolean {
const { isDefault, layer } = this.#getLayerInfo(key)
if (!layer) {
console.warn(`图层 key ${key} 不存在`)
return false
}
const removed = this.#viewer.imageryLayers.remove(layer, true)
if (removed) {
this.#removeLayerKey(key, isDefault)
}
return removed!
}
/**
* 批量删除图层
* @param layerIds - 图层 ID 数组
*/
batchRemoveLayers(layerIds: string[]): void {
layerIds.forEach((id) => this.removeLayerByKey(id))
}
/**
* 清除图层
* @param clearType - 清除类型:'default'=默认图层,'custom'=自定义图层,'all'=所有图层(默认 'custom'
*/
clearAllLayers(clearType: 'default' | 'custom' | 'all' = 'custom'): void {
const targetMap = this.#getTargetMapByType(clearType)
targetMap.forEach((layer) => {
this.#viewer.imageryLayers.remove(layer, true)
})
this.#clearMapsByType(clearType)
}
// ===================== 私有方法 =====================
#validateUniqueLayerKey(key: string): void {
if (this.#defaultLayerMap.has(key) || this.#customLayerMap.has(key)) {
console.warn(`图层 ${key} 已存在,将覆盖原有图层`)
this.removeLayerByKey(key)
}
}
#createImageryProvider(layerConfig: LayerConfig): ImageryProvider | null {
switch (layerConfig.type) {
case 'imagery':
return new ArcGisMapServerImageryProvider({ url: layerConfig.url })
case 'wms':
return new WebMapServiceImageryProvider({
url: layerConfig.url,
layers: layerConfig.layers,
parameters: layerConfig.parameters || { format: 'image/png' },
})
case 'wmts':
return new WebMapTileServiceImageryProvider({
url: layerConfig.url,
layer: layerConfig.layers,
style: layerConfig.style || 'default',
format: layerConfig.format || 'image/png',
tileMatrixSetID: layerConfig.tileMatrixSetID || 'EPSG:4326',
credit: '',
})
default:
console.error(`不支持的图层类型:${layerConfig.type}`)
return null
}
}
#storeLayer(key: string, layer: ImageryLayer, isDefault: boolean): void {
if (isDefault) {
this.#defaultLayerMap.set(key, layer)
} else {
this.#customLayerMap.set(key, layer)
}
}
#getLayerInfo(key: string) {
const isDefault = this.#defaultLayerMap.has(key)
const layer = isDefault ? this.#defaultLayerMap.get(key) : this.#customLayerMap.get(key)
return { isDefault, layer }
}
#removeLayerKey(key: string, isDefault: boolean): void {
if (isDefault) this.#defaultLayerMap.delete(key)
else this.#customLayerMap.delete(key)
}
#getTargetMapByType(clearType: 'default' | 'custom' | 'all'): Map<string, ImageryLayer> {
const targetMap = new Map<string, ImageryLayer>()
if (clearType === 'default' || clearType === 'all')
this.#defaultLayerMap.forEach((value, key) => targetMap.set(key, value))
if (clearType === 'custom' || clearType === 'all')
this.#customLayerMap.forEach((value, key) => targetMap.set(key, value))
return targetMap
}
#clearMapsByType(clearType: 'default' | 'custom' | 'all'): void {
if (clearType === 'default' || clearType === 'all') this.#defaultLayerMap.clear()
if (clearType === 'custom' || clearType === 'all') this.#customLayerMap.clear()
}
}
+276
View File
@@ -0,0 +1,276 @@
import {
Primitive,
BillboardCollection,
GeometryInstance,
CircleGeometry,
ColorGeometryInstanceAttribute,
PerInstanceColorAppearance,
PolylineGeometry,
PolylineColorAppearance,
PolygonGeometry,
PolygonHierarchy,
Cartesian3,
Color,
BillboardGraphics,
VerticalOrigin,
HorizontalOrigin,
} from 'cesium'
import type { PrimitiveOptions } from '@/types/cesium/PrimitiveOptions'
import type { Viewer } from 'cesium'
/**
* Primitive 管理器
*/
export class PrimitiveManager {
#viewer: Viewer
#defaultPrimitiveMap = new Map<string, Primitive | BillboardCollection>()
#customPrimitiveMap = new Map<string, Primitive | BillboardCollection>()
constructor(viewer: Viewer) {
this.#viewer = viewer
}
/**
* 批量添加 Primitive
* @param primitives - Primitive 配置选项数组
*/
addPrimitivesBatch(primitives: PrimitiveOptions[]): void {
const grouped = this.#groupPrimitivesByType(primitives)
if (grouped.points.length > 0) this.#addPointPrimitives(grouped.points)
if (grouped.polylines.length > 0) this.#addPolylinePrimitives(grouped.polylines)
if (grouped.polygons.length > 0) this.#addPolygonPrimitives(grouped.polygons)
if (grouped.billboards.length > 0) this.#addBillboardPrimitives(grouped.billboards)
}
/**
* 查询 Primitive
* @param id - Primitive ID
* @returns Primitive 或 BillboardCollection 实例,不存在则返回 undefined
*/
getPrimitiveById(id: string): Primitive | BillboardCollection | undefined {
return this.#defaultPrimitiveMap.get(id) || this.#customPrimitiveMap.get(id)
}
/**
* 删除 Primitive
* @param id - Primitive ID
* @returns 是否删除成功
*/
removePrimitiveById(id: string): boolean {
const { isDefault, primitive } = this.#getPrimitiveInfo(id)
if (!primitive) {
console.warn(`Primitive ID ${id} 不存在`)
return false
}
this.#viewer.scene.primitives.remove(primitive)
this.#removePrimitiveId(id, isDefault)
return true
}
/**
* 清除 Primitive
* @param clearType - 清除类型:'default'=默认 Primitive'custom'=自定义 Primitive'all'=所有 Primitive(默认 'custom'
*/
clearAllPrimitives(clearType: 'default' | 'custom' | 'all' = 'custom'): void {
const targetMap = this.#getTargetMapByType(clearType)
targetMap.forEach((primitive) => {
this.#viewer.scene.primitives.remove(primitive)
})
this.#clearMapsByType(clearType)
}
// ===================== 私有方法 =====================
#groupPrimitivesByType(primitives: PrimitiveOptions[]) {
const grouped: {
points: PrimitiveOptions[]
polylines: PrimitiveOptions[]
polygons: PrimitiveOptions[]
billboards: PrimitiveOptions[]
} = {
points: [],
polylines: [],
polygons: [],
billboards: [],
}
primitives.forEach((option) => {
const { id } = option
this.#validatePrimitiveUniqueId(id)
switch (option.type) {
case 'point':
grouped.points.push(option)
break
case 'polyline':
grouped.polylines.push(option)
break
case 'polygon':
grouped.polygons.push(option)
break
case 'billboard':
grouped.billboards.push(option)
break
}
})
return grouped
}
#validatePrimitiveUniqueId(id: string): void {
if (this.#defaultPrimitiveMap.has(id) || this.#customPrimitiveMap.has(id)) {
throw new Error(`Primitive ID ${id} 已存在`)
}
}
#getPrimitiveInfo(id: string) {
const isDefault = this.#defaultPrimitiveMap.has(id)
const primitive = isDefault
? this.#defaultPrimitiveMap.get(id)
: this.#customPrimitiveMap.get(id)
return { isDefault, primitive }
}
#removePrimitiveId(id: string, isDefault: boolean): void {
if (isDefault) {
this.#defaultPrimitiveMap.delete(id)
} else {
this.#customPrimitiveMap.delete(id)
}
}
#addPointPrimitives(options: PrimitiveOptions[]): void {
const instances = options.map((option) => {
const position = this.#convertPosition(option.positions[0]!)
return new GeometryInstance({
id: option.id,
geometry: new CircleGeometry({
center: position,
radius: option.pixelSize || 8,
vertexFormat: PerInstanceColorAppearance.VERTEX_FORMAT,
}),
attributes: {
color: ColorGeometryInstanceAttribute.fromColor(option.color || Color.RED),
},
})
})
const primitive = new Primitive({
geometryInstances: instances,
appearance: new PerInstanceColorAppearance({ translucent: false, closed: true }),
asynchronous: false,
})
this.#viewer.scene.primitives.add(primitive)
this.#storePrimitives(options, primitive)
}
#addPolylinePrimitives(options: PrimitiveOptions[]): void {
const instances = options.map((option) => {
const positions = this.#convertPositionArray(option.positions)
return new GeometryInstance({
id: option.id,
geometry: new PolylineGeometry({
positions,
width: option.width || 3,
vertexFormat: PolylineColorAppearance.VERTEX_FORMAT,
}),
attributes: {
color: ColorGeometryInstanceAttribute.fromColor(option.color || Color.BLUE),
},
})
})
const primitive = new Primitive({
geometryInstances: instances,
appearance: new PolylineColorAppearance({ translucent: true }),
asynchronous: false,
})
this.#viewer.scene.primitives.add(primitive)
this.#storePrimitives(options, primitive)
}
#addPolygonPrimitives(options: PrimitiveOptions[]): void {
const instances = options.map((option) => {
const positions = this.#convertPositionArray(option.positions)
return new GeometryInstance({
id: option.id,
geometry: new PolygonGeometry({
polygonHierarchy: new PolygonHierarchy(positions),
vertexFormat: PerInstanceColorAppearance.VERTEX_FORMAT,
}),
attributes: {
color: ColorGeometryInstanceAttribute.fromColor(
option.color || Color.GREEN.withAlpha(0.5),
),
},
})
})
const primitive = new Primitive({
geometryInstances: instances,
appearance: new PerInstanceColorAppearance({ translucent: true, closed: true }),
asynchronous: false,
})
this.#viewer.scene.primitives.add(primitive)
this.#storePrimitives(options, primitive)
}
#addBillboardPrimitives(options: PrimitiveOptions[]): void {
const collection = new BillboardCollection()
options.forEach((option) => {
const position = this.#convertPosition(option.positions[0]!)
collection.add({
id: option.id,
position,
image: option.image,
scale: option.scale || 1,
color: option.color || Color.WHITE,
})
})
this.#viewer.scene.primitives.add(collection)
this.#storePrimitives(options, collection)
}
#storePrimitives(options: PrimitiveOptions[], primitive: Primitive | BillboardCollection): void {
options.forEach((option) => {
if (option.default) {
this.#defaultPrimitiveMap.set(option.id, primitive)
} else {
this.#customPrimitiveMap.set(option.id, primitive)
}
})
}
#getTargetMapByType(
clearType: 'default' | 'custom' | 'all',
): Map<string, Primitive | BillboardCollection> {
const targetMap = new Map<string, Primitive | BillboardCollection>()
if (clearType === 'default' || clearType === 'all')
this.#defaultPrimitiveMap.forEach((value, key) => targetMap.set(key, value))
if (clearType === 'custom' || clearType === 'all')
this.#customPrimitiveMap.forEach((value, key) => targetMap.set(key, value))
return targetMap
}
#clearMapsByType(clearType: 'default' | 'custom' | 'all'): void {
if (clearType === 'default' || clearType === 'all') this.#defaultPrimitiveMap.clear()
if (clearType === 'custom' || clearType === 'all') this.#customPrimitiveMap.clear()
}
#convertPosition(pos: Cartesian3 | [number, number, number]): Cartesian3 {
return Array.isArray(pos) ? Cartesian3.fromDegrees(pos[0], pos[1], pos[2] || 0) : pos
}
#convertPositionArray(positions: (Cartesian3 | [number, number, number])[]): Cartesian3[] {
return positions.map((pos) => this.#convertPosition(pos))
}
}