按功能拆分CesiumUtils类
按功能拆分CesiumUtils类,更易于管理
This commit is contained in:
@@ -8,6 +8,9 @@
|
|||||||
"2e8111f9bc84149cbf24f562ed4e9229",
|
"2e8111f9bc84149cbf24f562ed4e9229",
|
||||||
"88055d3d7f13f8f7e6e8eeb67cf6d78a"
|
"88055d3d7f13f8f7e6e8eeb67cf6d78a"
|
||||||
],
|
],
|
||||||
"cesiumIonDefaultAccessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI1ZDBjZjAxOS0wMDhhLTRmZjEtYjNmOC1iNmM2ZmY2ZmQ1N2IiLCJpZCI6MjAxMDI1LCJpYXQiOjE3MTAxNTgxNjJ9.mdbJYEzXQkBnHNqpozz7MvZjJ_X9a3JZRGPA-ytGhLI",
|
"cesiumIonDefaultAccessToken": [
|
||||||
|
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI1ZDBjZjAxOS0wMDhhLTRmZjEtYjNmOC1iNmM2ZmY2ZmQ1N2IiLCJpZCI6MjAxMDI1LCJpYXQiOjE3MTAxNTgxNjJ9.mdbJYEzXQkBnHNqpozz7MvZjJ_X9a3JZRGPA-ytGhLI",
|
||||||
|
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJiNjczZTVlMy1kNDEwLTRhZWItYWM0NS1mNjYxMzJjODMwYTQiLCJpZCI6MzIxMzI2LCJpYXQiOjE3NzU2NDU1OTd9._MPcZQsxK1dGPl8IMVhKHV3PIPu4-TaOUgzsUUOP6WE"
|
||||||
|
],
|
||||||
"defaultPosition": [108.948024, 34.263161, 200000]
|
"defaultPosition": [108.948024, 34.263161, 200000]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
+218
-1501
File diff suppressed because it is too large
Load Diff
@@ -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]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user