按功能拆分CesiumUtils类
按功能拆分CesiumUtils类,更易于管理
This commit is contained in:
@@ -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]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
+221
-1504
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