diff --git a/env.d.ts b/env.d.ts
index 9f6f7e4..efb8d29 100644
--- a/env.d.ts
+++ b/env.d.ts
@@ -18,7 +18,8 @@ interface ImportMetaEnv {
}
interface ImportMeta {
- readonly env: ImportMetaEnv
+ readonly env: ImportMetaEnv,
+ readonly glob
}
// 声明 CSS 模块类型
diff --git a/src/component/map/Map.vue b/src/component/map/Map.vue
index 9883d03..7e66da9 100644
--- a/src/component/map/Map.vue
+++ b/src/component/map/Map.vue
@@ -3,20 +3,60 @@
diff --git a/src/types/cesium/EntityOptions.ts b/src/types/cesium/EntityOptions.ts
index 71da5ae..0c1dc43 100644
--- a/src/types/cesium/EntityOptions.ts
+++ b/src/types/cesium/EntityOptions.ts
@@ -1,4 +1,5 @@
import type { Cartesian3, Color } from "cesium"
+import { HeightReference, MaterialProperty } from 'cesium'
/**
* 实体配置通用类型
@@ -15,6 +16,7 @@ export interface EntityOptions {
pixelSize?: number // 像素大小(默认:8)
outlineColor?: Color // 轮廓颜色(默认:白色)
outlineWidth?: number // 轮廓宽度(默认:1)
+ heightReference?: HeightReference // 高度参考(默认:CLAMP_TO_GROUND)
}
// 线配置(type='polyline' 时必填)
polylineOptions?: {
@@ -30,17 +32,19 @@ export interface EntityOptions {
color?: Color // 颜色(默认:白色)
verticalOrigin?: number // 垂直对齐方式(默认:CENTER)
horizontalOrigin?: number // 水平对齐方式(默认:CENTER)
+ heightReference?: HeightReference // 高度参考(默认:CLAMP_TO_GROUND)
}
// 面配置(type='polygon' 时必填)
polygonOptions?: {
hierarchy: Cartesian3[] | [number, number, number][] // 面顶点数组
- color?: Color // 颜色(默认:绿色)
+ // color?: Color // 颜色(默认:绿色)
outline?: boolean // 是否显示轮廓(默认:true)
outlineColor?: Color // 轮廓颜色(默认:黑色)
outlineWidth?: number // 轮廓宽度(默认:1)
height?: number // 高度(默认:0)
extrudedHeight?: number // extrudedHeight 高度(默认:0)
- perPositionHeight?: boolean // 是否每个顶点高度不同(默认:true)
+ heightReference?: HeightReference // 高度参考(默认:CLAMP_TO_GROUND)
+ material?: MaterialProperty // 材质(默认:Color.WHITE)
}
attributes?: Record // 自定义属性(用于存储额外信息)
-}
+}
\ No newline at end of file
diff --git a/src/types/cesium/GeoJsonOptions.ts b/src/types/cesium/GeoJsonOptions.ts
index e69de29..5dd7eb1 100644
--- a/src/types/cesium/GeoJsonOptions.ts
+++ b/src/types/cesium/GeoJsonOptions.ts
@@ -0,0 +1,35 @@
+import type {
+ Cartesian3,
+ Color,
+ DataSource,
+} from "cesium";
+import type { LabelConfig } from "./LabelConfig";
+
+// 数据源:字符串路径/URL | GeoJSON对象
+export type CustomizeGeoJsonDataSource = string | object;
+
+// 唯一配置项接口
+export interface GeoJsonOptions {
+ showName?: boolean;
+ labelStyle?: LabelConfig;
+ polygonStyle?: {
+ fill?: boolean;
+ fillColor?: Color;
+ outline?: boolean;
+ outlineColor?: Color;
+ outlineWidth?: number;
+ center?: Cartesian3 | [number, number, number];
+ };
+ polylineStyle?: {
+ width?: number;
+ material?: Color;
+ clampToGround?: boolean;
+ };
+ pointStyle?: {
+ pixelSize?: number;
+ color?: Color;
+ outlineColor?: Color;
+ outlineWidth?: number;
+ };
+ onComplete?: (dataSource: DataSource) => void;
+}
\ No newline at end of file
diff --git a/src/types/cesium/LabelConfig.ts b/src/types/cesium/LabelConfig.ts
new file mode 100644
index 0000000..be47860
--- /dev/null
+++ b/src/types/cesium/LabelConfig.ts
@@ -0,0 +1,13 @@
+import type { Cartesian3, Color, HorizontalOrigin, VerticalOrigin } from 'cesium'
+
+export interface LabelConfig {
+ labelText?: string // 文本,默认空白
+ labelFont?: string // 字体样式,默认16px "微软雅黑"
+ labelColor?: Color // 标签颜色, 默认白色
+ labelSize?: number // 字体大小,默认16
+ labelOffset?: { x: number; y: number } // 标签偏移,默认0,0
+ horizontalOrigin?: HorizontalOrigin // 水平位置,默认居中
+ verticalOrigin?: VerticalOrigin // 垂直位置,默认居中
+ backgroundColor?: Color // 背景颜色,默认透明
+ center?: Cartesian3 | [number, number, number]
+}
\ No newline at end of file
diff --git a/src/utils/cesium/CesiumUtils.ts b/src/utils/cesium/CesiumUtils.ts
index 45b2a14..792cd76 100644
--- a/src/utils/cesium/CesiumUtils.ts
+++ b/src/utils/cesium/CesiumUtils.ts
@@ -11,8 +11,7 @@ import {
PolylineGraphics,
BillboardGraphics,
SceneMode,
- CesiumTerrainProvider,
- EllipsoidTerrainProvider,
+ HeightReference,
VerticalOrigin,
HorizontalOrigin,
Cartographic,
@@ -36,59 +35,83 @@ import {
PolygonGeometry,
ArcGisMapServerImageryProvider,
WebMapServiceImageryProvider,
+ DataSource,
+ LabelGraphics,
+ LabelStyle,
+ Cartesian2,
+ JulianDate,
+ ConstantPositionProperty,
+ createWorldTerrain, GridMaterialProperty,
+ GeoJsonDataSource
} from 'cesium'
import config from '@/config/config.json'
+import type { LabelConfig } from '@/types/cesium/LabelConfig'
+import type { CustomizeGeoJsonDataSource, GeoJsonOptions } from '@/types/cesium/GeoJsonOptions'
-// 定义清除类型枚举(增强类型安全)
+// 定义清除类型枚举
export type ClearType = 'default' | 'custom' | 'all'
/**
* Cesium 工具类
- * 封装 Cesium 核心操作,区分默认/自定义资源管理,支持精准增删改查
+ * 封装 Cesium 核心操作,区分默认/自定义资源管理
*/
export class CesiumUtils {
- // ===================== 实体管理(区分默认/自定义) =====================
- // 私有属性:默认实体ID集合(不希望被轻易清空的)
- #defaultEntityIds: Set = new Set()
- // 私有属性:自定义实体ID集合(业务添加的,可自由清空)
- #customEntityIds: Set = new Set()
+ // ===================== 定义viewer =====================
+ #viewer: Viewer | null = null
- // ===================== Primitive管理(区分默认/自定义) =====================
- // 私有属性:默认Primitive映射(key: ID,value: 实例)
- #defaultPrimitiveMap: Map = new Map<
- string,
- Primitive | BillboardCollection
- >()
- // 私有属性:自定义Primitive映射
- #customPrimitiveMap: Map = new Map<
- string,
- Primitive | BillboardCollection
- >()
-
- // ===================== 图层管理(区分默认/自定义) =====================
- // 私有属性:默认图层映射(key: layerConfig.layers,value: 实例)
- #defaultLayerMap: Map = new Map()
- // 私有属性:自定义图层映射
- #customLayerMap: Map = new Map()
+ // ===================== 私有属性定义 =====================
+ #defaultEntityIds = new Set()
+ #customEntityIds = new Set()
+ #defaultPrimitiveMap = new Map()
+ #customPrimitiveMap = new Map()
+ #defaultLayerMap = new Map()
+ #customLayerMap = new Map()
+ #defaultGeoJsonMap = new Map()
+ #customGeoJsonMap = new Map()
constructor() {
- // 初始化Cesium Ion Token
Ion.defaultAccessToken = config.cesiumIonDefaultAccessToken
}
- /**
- * 初始化 Cesium Viewer 实例
- * @description 封装默认配置,支持自定义扩展,返回 Viewer 实例供全局使用
- * @param options - 初始化配置项
- * @param tdMapToken - 天地图服务 token 数组
- * @param type - 底图类型:0 - 天地图「影像底图 + 影像注记」其他 - 天地图「纯矢量底图」(无注记)
- * @returns Viewer 实例
- */
- initCesiumViewer(options: CesiumInitOptions, tdMapToken?: string[], type: number = 0): Viewer {
- // 默认天地图token
- tdMapToken = tdMapToken || config.tdMapToken
+ // ===================== 静态配置 ========================
+ static readonly DEFAULT_OPTIONS: Required = {
+ 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: () => { },
+ };
- // 默认配置(优先级:用户传入 > 默认)
+ // ===================== 初始化与销毁 =====================
+
+ /**
+ * 初始化 Cesium Viewer
+ */
+ initCesiumViewer(options: CesiumInitOptions, tdMapToken?: string[], type: number = 0): void {
const defaultOptions: CesiumInitOptions = {
containerId: options.containerId,
shouldAnimate: true,
@@ -107,19 +130,16 @@ export class CesiumUtils {
const finalOptions = { ...defaultOptions, ...options }
const container = document.getElementById(finalOptions.containerId)
-
+
if (!container) {
throw new Error(`Cesium 容器 #${finalOptions.containerId} 不存在`)
}
- // 初始化 Viewer
const viewer = new Viewer(container, {
...finalOptions,
- terrainProvider: finalOptions.terrain
- ? new CesiumTerrainProvider({ url: finalOptions.terrain })
- : new EllipsoidTerrainProvider(),
-
- //截图和渲染相关的一些配置
+ terrainProvider: createWorldTerrain(),
+ selectionIndicator: false, // 禁用选择指示器
+ baseLayerPicker: false, // 禁用默认底图
contextOptions: {
webgl: {
alpha: true,
@@ -127,7 +147,6 @@ export class CesiumUtils {
stencil: true,
antialias: true,
premultipliedAlpha: true,
- //cesium状态下允许canvas转图片convertToImage
preserveDrawingBuffer: true,
failIfMajorPerformanceCaveat: true,
},
@@ -135,34 +154,38 @@ export class CesiumUtils {
},
})
- // 优化性能:关闭不必要的渲染
+ // 性能优化配置
viewer.scene.globe.depthTestAgainstTerrain = false
viewer.scene.fog.enabled = false
- viewer.scene.globe.enableLighting = false //全局光照
+ viewer.scene.globe.enableLighting = false
viewer.shadows = false
- // 禁用天空盒和天空大气
- viewer.scene.skyBox.show = false
- viewer.scene.skyAtmosphere.show = false
- // 禁用月球
- viewer.scene.moon.show = false
const creditContainer = viewer.cesiumWidget.creditContainer as HTMLElement
creditContainer.style.display = 'none'
// 添加底图
- this.imageryProvider(type, tdMapToken).forEach((imageryProvider) => {
- viewer.imageryLayers.addImageryProvider(imageryProvider)
+ this.imageryProvider(type, tdMapToken || config.tdMapToken).forEach((provider) => {
+ viewer.imageryLayers.addImageryProvider(provider)
})
- return viewer
+ this.#viewer = viewer
}
/**
- * 添加底图
- * @param type - 底图类型:0 - 天地图影像;1 - 内网自定义瓦片底图;其他 - 天地图「纯矢量底图」(无注记)
- * @param tdMapToken - 天地图token数组
- * @returns ImageryProvider 实例数组
+ * 销毁 Cesium Viewer
*/
- imageryProvider(type: number, tdMapToken: string[]): ImageryProvider[] {
+ destroyCesiumViewer(): void {
+ if (this.#viewer) {
+ this.clearAllResources('all')
+ this.#viewer.destroy()
+ }
+ }
+
+ // ===================== 底图配置 =====================
+
+ /**
+ * 创建底图 ImageryProvider
+ */
+ private imageryProvider(type: number, tdMapToken: string[]): ImageryProvider[] {
const option = {
tileMatrixSetID: 'w',
format: 'tiles',
@@ -174,21 +197,17 @@ export class CesiumUtils {
}
if (type === 0) {
- // 随机选择token避免单token超限
- const currentTokenIndex = Math.floor(Math.random() * tdMapToken.length)
-
+ const token = tdMapToken[Math.floor(Math.random() * tdMapToken.length)]
const imageryProvider = new WebMapTileServiceImageryProvider({
- url: `https://{s}.tianditu.gov.cn/img_w/wmts?tk=${tdMapToken[currentTokenIndex]}`,
+ 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=${tdMapToken[currentTokenIndex]}`,
+ url: `https://{s}.tianditu.gov.cn/cia_w/wmts?tk=${token}`,
layer: 'cia',
...option,
})
-
return [imageryProvider, annotationProvider]
} else {
const vectorProvider = new WebMapTileServiceImageryProvider({
@@ -200,45 +219,724 @@ export class CesiumUtils {
}
}
+ // ===================== 实体管理 =====================
+
/**
- * 添加实体到场景
- * @description 统一处理点、线、Billboard 实体的创建,支持区分默认/自定义实体
- * @param viewer - Cesium Viewer 实例
- * @param entityOptions - 实体配置项(必传 id、position、type,default标识是否为默认实体)
- * @returns 创建的 Entity 实例
+ * 添加实体
*/
- addCesiumEntity(viewer: Viewer, entityOptions: EntityOptions): Entity {
- const { id, position, type, attributes = {}, default: isDefault = false } = entityOptions
+ 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)
- // 校验ID唯一性(跨默认/自定义集合)
- if (this.#defaultEntityIds.has(id) || this.#customEntityIds.has(id)) {
- throw new Error(`实体 ID ${id} 已存在,请勿重复添加`)
- }
-
- // 实体基础配置
const entity = new Entity({
id,
position: this.convertPosition(position),
- ...attributes, // 挂载自定义属性
+ ...attributes,
})
- // 根据类型配置实体图形
+ this.#configureEntityGraphics(entity, entityOptions)
+
+ this.#viewer?.entities.add(entity)
+ this.#storeEntityId(id, isDefault)
+ return entity
+ }
+
+ /**
+ * 查询实体
+ */
+ getCesiumEntityById(entityId: string): Entity | null {
+ if (!this.#entityExists(entityId)) return null
+ return this.#viewer?.entities.getById(entityId) || null
+ }
+
+ /**
+ * 删除实体
+ */
+ 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
+ }
+
+ /**
+ * 批量删除实体
+ */
+ batchRemoveCesiumEntities(entityIds: string[]): void {
+ entityIds.forEach((id) => this.removeCesiumEntity(id))
+ }
+
+ // ===================== Primitive 管理 =====================
+
+ /**
+ * 批量添加 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
+ */
+ getPrimitiveById(id: string): Primitive | BillboardCollection | undefined {
+ return this.#defaultPrimitiveMap.get(id) || this.#customPrimitiveMap.get(id)
+ }
+
+ /**
+ * 删除 Primitive
+ */
+ 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
+ }
+
+ // ===================== 图层管理 =====================
+
+ /**
+ * 创建图层
+ */
+ 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!
+ }
+
+ /**
+ * 查询图层
+ */
+ getLayerByKey(key: string): ImageryLayer | undefined {
+ return this.#defaultLayerMap.get(key) || this.#customLayerMap.get(key)
+ }
+
+ /**
+ * 删除图层
+ */
+ 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!
+ }
+
+ /**
+ * 批量删除图层
+ */
+ batchRemoveLayers(layerIds: string[]): void {
+ layerIds.forEach((id) => this.removeLayerByKey(id))
+ }
+
+ // ===================== GeoJSON 图层管理 =====================
+
+ /**
+ * 添加 GeoJSON 图层
+ * @param layerId 图层唯一ID
+ * @param geojsonData 数据源(路径/URL/对象)
+ * @param isDefault 是否为默认图层
+ * @param options 配置项(GeoJsonOptions)
+ */
+ async addGeoJsonLayer(
+ layerId: string,
+ geojsonData: CustomizeGeoJsonDataSource,
+ isDefault: boolean = false,
+ options?: GeoJsonOptions
+ ): Promise {
+ 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查询图层 */
+ getGeoJsonLayerById(layerId: string): DataSource | undefined {
+ return this.#getGeoJsonLayer(layerId).ds;
+ }
+
+ /** 删除图层 */
+ 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
+ * @param geojsonDatas
+ * @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]));
+ }
+
+ /** 批量删除 */
+ batchRemoveGeoJsonLayers(layerIds: string[]): void {
+ layerIds.forEach(id => this.removeGeoJsonLayer(id));
+ }
+
+ /** 清空图层 */
+ 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();
+ }
+
+ /** 显示图层 */
+ 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;
+ }
+
+ /** 隐藏图层 */
+ 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;
+ }
+
+ /** 切换显隐 */
+ 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;
+ }
+
+ /** 批量显示 */
+ batchShowGeoJsonLayers(layerIds: string[]): number {
+ return layerIds.reduce((n, id) => n + (this.showGeoJsonLayer(id) ? 1 : 0), 0);
+ }
+
+ /** 批量隐藏 */
+ batchHideGeoJsonLayers(layerIds: string[]): number {
+ return layerIds.reduce((n, id) => n + (this.hideGeoJsonLayer(id) ? 1 : 0), 0);
+ }
+
+ /** 获取显示状态 */
+ getGeoJsonLayerVisibility(layerId: string): boolean | null {
+ const ds = this.getGeoJsonLayerById(layerId);
+ return ds ? ds.show : null;
+ }
+
+ // ===================== EWKB操作 =====================
+
+ /**
+ * 添加 EWKB 解析后的 Cesium 实体
+ * @param ewkb EWKB 十六进制字符串
+ * @param entityOptions 实体配置项
+ * @returns 添加后的 Cesium 实体
+ */
+ addEWkbLayer(ewkb: string, entityOptions: EntityOptions) {
+ let height = 0
+ if (Array.isArray(entityOptions.position)) {
+ height = entityOptions.position[2] ?? 0
+ } else if (entityOptions.position instanceof Cartesian3) {
+ height = entityOptions.position.z ?? 0
+ }
+
+ // 解析 EWKB 获取几何类型和坐标
+ const { type: geomType, coordinates } = this.parseEWKB(ewkb)
+
+ // 根据几何类型配置实体
+ switch (geomType) {
+ case 'point': {
+ // 断言坐标类型为点坐标
+ const pointCoords = coordinates as [number, number]
+ // 统一设置为数组类型(兼容接口)
+ entityOptions.position = [pointCoords[0], pointCoords[1], height]
+ break
+ }
+
+ case 'polyline': {
+ // 断言坐标类型为线坐标
+ const lineCoords = coordinates as [number, number][]
+
+ // 初始化 polylineOptions(避免 undefined 报错)
+ if (!entityOptions.polylineOptions) {
+ entityOptions.polylineOptions = {
+ positions: [],
+ color: Color.BLUE,
+ width: 3,
+ clampToGround: false,
+ }
+ }
+
+ // 转换坐标:补充高程,严格匹配 [number, number, number][] 类型
+ entityOptions.polylineOptions.positions = lineCoords.map((coord) => [
+ coord[0],
+ coord[1],
+ height,
+ ]) as [number, number, number][]
+ break
+ }
+
+ case 'polygon': {
+ // 断言坐标类型为面坐标(兼容带洞多边形的嵌套结构)
+ const polygonCoords = coordinates as [number, number][][] | [number, number][]
+
+ // 初始化 polygonOptions(避免 undefined 报错)
+ if (!entityOptions.polygonOptions) {
+ entityOptions.polygonOptions = {
+ hierarchy: [],
+ outline: true,
+ outlineColor: Color.BLACK,
+ outlineWidth: 1,
+ height: 0,
+ extrudedHeight: 0,
+ }
+ }
+
+ // 处理多边形坐标(兼容单层/双层数组)
+ let finalPolygonCoords: [number, number, number][]
+ if (Array.isArray(polygonCoords[0]) && typeof polygonCoords[0][0] === 'number') {
+ // 单层数组(普通多边形)
+ finalPolygonCoords = (polygonCoords as [number, number][]).map((coord) => [
+ coord[0],
+ coord[1],
+ height,
+ ]) as [number, number, number][]
+ } else {
+ // 双层数组(带洞多边形,取外环)
+ const outerRing = (polygonCoords as [number, number][][])[0]
+ finalPolygonCoords = outerRing?.map((coord) => [coord[0], coord[1], height]) as [
+ number,
+ number,
+ number,
+ ][]
+ }
+
+ entityOptions.polygonOptions.hierarchy = finalPolygonCoords
+ break
+ }
+
+ default:
+ throw new Error(`不支持的 EWKB 几何类型: ${geomType}`)
+ }
+
+ // 添加实体并返回
+ return this.addCesiumEntity(entityOptions)
+ }
+
+ /**
+ * 解析EWKB
+ * @param ewkbHex
+ * @returns
+ */
+ parseEWKB(ewkbHex: string): {
+ type: 'point' | 'polyline' | 'polygon'
+ coordinates: [number, number] | [number, number][] | [number, number][][]
+ srid: number
+ bbox?: [number, number, number, number]
+ } {
+ // 去掉可能的空格
+ const hexString = ewkbHex.trim()
+
+ if (!hexString || hexString.length < 2) {
+ throw new Error('Invalid EWKB hex string')
+ }
+
+ // 将十六进制字符串转换为字节数组
+ const bytes = []
+ for (let i = 0; i < hexString.length; i += 2) {
+ bytes.push(parseInt(hexString.substr(i, 2), 16))
+ }
+
+ const dataView = new DataView(new Uint8Array(bytes).buffer)
+ let offset = 0
+
+ // 第一个字节:字节顺序 (0 = big-endian, 1 = little-endian)
+ const byteOrder = dataView.getUint8(offset)
+ const isLittleEndian = byteOrder === 1
+ offset += 1
+
+ // 读取类型码(4字节)
+ let typeCode = dataView.getUint32(offset, isLittleEndian)
+ offset += 4
+
+ // 检查是否有SRID(如果类型码的第30位为1)
+ let srid = 4326 // 默认WGS84
+ const hasSRID = (typeCode & 0x20000000) !== 0
+
+ if (hasSRID) {
+ // 清除SRID标志位
+ typeCode = typeCode & ~0x20000000
+ // 读取SRID(4字节)
+ srid = dataView.getUint32(offset, isLittleEndian)
+ offset += 4
+ }
+
+ // 解析几何类型(取低16位)
+ const geometryType = typeCode & 0xffff
+
+ // 转换为您的类型系统
+ let type: 'point' | 'polyline' | 'polygon'
+ let coordinates: [number, number] | [number, number][] | [number, number][][]
+
+ switch (geometryType) {
+ case 1: // WKB_POINT
+ type = 'point'
+ coordinates = this.#parsePointCoords(dataView, offset, isLittleEndian)
+ break
+
+ case 2: // WKB_LINESTRING
+ type = 'polyline'
+ coordinates = this.#parseLineStringCoords(dataView, offset, isLittleEndian)
+ break
+
+ case 3: // WKB_POLYGON
+ type = 'polygon'
+ coordinates = this.#parsePolygonCoords(dataView, offset, isLittleEndian)
+ break
+ default:
+ throw new Error(`Unsupported geometry type: ${geometryType}`)
+ }
+
+ // 计算边界框
+ const bbox = this.#calculateBbox(type, coordinates)
+
+ return {
+ type: type,
+ coordinates: coordinates,
+ srid: srid,
+ bbox: bbox,
+ }
+ }
+
+ /**
+ * 将地理数据转换为 WKB 十六进制字符串
+ * @param type 几何类型 (point/polyline/polygon)
+ * @param coordinates 坐标数据
+ * @param srid 空间参考系ID (默认4326)
+ * @param includeSRID 是否包含SRID信息(包含则为EWKB)
+ * @param littleEndian 是否使用小端字节序 (默认true)
+ * @returns WKB十六进制字符串
+ */
+ convertToWkb(
+ type: 'point' | 'polyline' | 'polygon',
+ coordinates: [number, number] | [number, number][] | [number, number][][],
+ srid: number = 4326,
+ includeSRID: boolean = true,
+ littleEndian: boolean = true,
+ ): string {
+ // 验证坐标格式
+ this.#validateCoordinates(type, coordinates)
+
+ // 创建字节缓冲区(初始大小预估,后续会动态调整)
+ const buffer = new ArrayBuffer(1024) // 初始分配1KB,足够大多数情况
+ const dataView = new DataView(buffer)
+ let offset = 0
+
+ // 1. 写入字节序标识 (1字节)
+ dataView.setUint8(offset, littleEndian ? 1 : 0)
+ offset += 1
+
+ // 2. 写入几何类型 (4字节)
+ let typeCode: number
switch (type) {
+ case 'point':
+ typeCode = 1
+ break
+ case 'polyline':
+ typeCode = 2
+ break
+ case 'polygon':
+ typeCode = 3
+ break
+ default:
+ throw new Error(`不支持的几何类型: ${type}`)
+ }
+
+ // 如果包含SRID,设置EWKB标志位 (0x20000000)
+ if (includeSRID) {
+ typeCode |= 0x20000000
+ }
+ dataView.setUint32(offset, typeCode, littleEndian)
+ offset += 4
+
+ // 3. 写入SRID (如果需要)
+ if (includeSRID) {
+ dataView.setUint32(offset, srid, littleEndian)
+ offset += 4
+ }
+
+ // 4. 写入坐标数据
+ switch (type) {
+ case 'point':
+ this.#writePointCoordinates(dataView, offset, coordinates as [number, number], littleEndian)
+ offset += 16 // 2个double(x,y)共16字节
+ break
+ case 'polyline':
+ offset = this.#writeLineStringCoordinates(
+ dataView,
+ offset,
+ coordinates as [number, number][],
+ littleEndian,
+ )
+ break
+ case 'polygon':
+ offset = this.#writePolygonCoordinates(
+ dataView,
+ offset,
+ coordinates as [number, number][][],
+ littleEndian,
+ )
+ break
+ }
+
+ // 将缓冲区转换为十六进制字符串
+ return this.#bufferToHexString(buffer, offset)
+ }
+
+ // ===================== 标签 =====================
+
+ /**
+ * 添加标签
+ * @param dataSource
+ * @param label
+ */
+ 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),
+ })
+ }
+ })
+ }
+
+ // ===================== 视角控制 =====================
+
+ /**
+ * 飞行到目标位置
+ */
+ 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,
+ })
+ }
+
+ /**
+ * 调整视角到目标位置
+ */
+ 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,
+ },
+ })
+ }
+
+ // ===================== 清除与资源管理 =====================
+
+ /**
+ * 清除实体
+ */
+ clearAllEntities(clearType: ClearType = 'custom'): void {
+ const targetIds = this.#getTargetIdsByType(
+ clearType,
+ this.#defaultEntityIds,
+ this.#customEntityIds,
+ )
+
+ targetIds.forEach((id) => {
+ const entity = this.#viewer?.entities.getById(id)
+ if (entity) this.#viewer?.entities.remove(entity)
+ })
+
+ this.#clearCollectionsByType(clearType, this.#defaultEntityIds, this.#customEntityIds)
+ }
+
+ /**
+ * 清除 Primitive
+ */
+ clearAllPrimitives(clearType: ClearType = 'custom'): void {
+ const targetMap = this.#getTargetMapByType(
+ clearType,
+ this.#defaultPrimitiveMap,
+ this.#customPrimitiveMap,
+ )
+
+ targetMap.forEach((primitive) => {
+ this.#viewer?.scene.primitives.remove(primitive)
+ })
+
+ this.#clearMapsByType(clearType, this.#defaultPrimitiveMap, this.#customPrimitiveMap)
+ }
+
+ /**
+ * 清除图层
+ */
+ clearAllLayers(clearType: ClearType = 'custom'): void {
+ const targetMap = this.#getTargetMapByType(
+ clearType,
+ this.#defaultLayerMap,
+ this.#customLayerMap,
+ )
+
+ targetMap.forEach((layer) => {
+ this.#viewer?.imageryLayers.remove(layer, true)
+ })
+
+ this.#clearMapsByType(clearType, this.#defaultLayerMap, this.#customLayerMap)
+ }
+
+ /**
+ * 清除所有资源
+ */
+ clearAllResources(clearType: ClearType = 'custom'): void {
+ this.clearAllEntities(clearType)
+ this.clearAllPrimitives(clearType)
+ this.clearAllLayers(clearType)
+ this.clearAllGeoJsonLayers(clearType)
+ }
+
+ // ===================== getter 和 setter函数 =====================
+ getViewer(): Viewer | null {
+ return this.#viewer
+ }
+
+ // ===================== 辅助函数 =====================
+
+ 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))
+ }
+
+ // ===================== 私有方法 =====================
+
+ #configureEntityGraphics(entity: Entity, options: EntityOptions): void {
+ switch (options.type) {
case 'point': {
const {
color = Color.RED,
pixelSize = 8,
outlineColor = Color.WHITE,
outlineWidth = 1,
- } = entityOptions.pointOptions || {}
+ heightReference = HeightReference.CLAMP_TO_GROUND,
+ } = options.pointOptions || {}
entity.point = new PointGraphics({
color,
pixelSize,
outlineColor,
outlineWidth,
+ heightReference,
})
break
}
@@ -248,7 +946,7 @@ export class CesiumUtils {
color = Color.BLUE,
width = 3,
clampToGround = false,
- } = entityOptions.polylineOptions || {}
+ } = options.polylineOptions || {}
if (!positions) throw new Error('线实体必须传入 polylineOptions.positions')
entity.polyline = new PolylineGraphics({
@@ -266,7 +964,8 @@ export class CesiumUtils {
color = Color.WHITE,
verticalOrigin = VerticalOrigin.CENTER,
horizontalOrigin = HorizontalOrigin.CENTER,
- } = entityOptions.billboardOptions || {}
+ heightReference = HeightReference.CLAMP_TO_GROUND,
+ } = options.billboardOptions || {}
if (!image) throw new Error('Billboard 实体必须传入 billboardOptions.image')
entity.billboard = new BillboardGraphics({
@@ -275,651 +974,58 @@ export class CesiumUtils {
color,
verticalOrigin,
horizontalOrigin,
+ heightReference,
})
break
}
case 'polygon': {
const {
hierarchy,
- color = Color.GREEN.withAlpha(0.7),
+ // color = Color.GREEN.withAlpha(0.7),
outline = true,
outlineColor = Color.BLACK,
outlineWidth = 1,
height = 0,
extrudedHeight,
- perPositionHeight = true,
- } = entityOptions.polygonOptions || {}
+ heightReference = HeightReference.CLAMP_TO_GROUND,
+ material = new GridMaterialProperty({
+ color: Color.GREEN.withAlpha(0.3), // 栅格线颜色
+ cellAlpha: 0.2, // 栅格背景透明度
+ lineCount: new Cartesian2(8, 8), // 栅格线条数
+ lineThickness: new Cartesian2(2.0, 2.0) // 线条粗细
+ }),
+ } = options.polygonOptions || {}
if (!hierarchy) throw new Error('多边形实体必须传入 polygonOptions.hierarchy')
entity.polygon = new PolygonGraphics({
hierarchy: this.#createConstantProperty(this.#processHierarchy(hierarchy)),
- material: new ColorMaterialProperty(color),
+ 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,
- perPositionHeight: this.#createConstantProperty(perPositionHeight),
+ heightReference,
})
break
}
default:
- throw new Error(`不支持的实体类型:${type}`)
- }
-
- // 添加到场景并根据default标识存入对应集合
- viewer.entities.add(entity)
- if (isDefault) {
- this.#defaultEntityIds.add(id)
- } else {
- this.#customEntityIds.add(id)
- }
-
- return entity
- }
-
- /**
- * 根据 ID 删除实体
- * @description 安全删除实体,自动识别默认/自定义并清理对应存储
- * @param viewer - Cesium Viewer 实例
- * @param entityId - 实体 ID
- * @returns 是否删除成功
- */
- removeCesiumEntity(viewer: Viewer, entityId: string): boolean {
- // 先判断实体类型(默认/自定义)
- const isDefault = this.#defaultEntityIds.has(entityId)
- const isCustom = this.#customEntityIds.has(entityId)
-
- if (!isDefault && !isCustom) {
- console.warn(`实体 ID ${entityId} 不存在`)
- return false
- }
-
- const entity = viewer.entities.getById(entityId)
- if (entity) {
- viewer.entities.remove(entity)
- // 清理对应集合
- if (isDefault) {
- this.#defaultEntityIds.delete(entityId)
- } else {
- this.#customEntityIds.delete(entityId)
- }
- return true
- }
- return false
- }
-
- /**
- * 批量删除实体
- * @param viewer - Cesium Viewer 实例
- * @param entityIds - 要删除的实体ID数组
- */
- batchRemoveCesiumEntities(viewer: Viewer, entityIds: string[]): void {
- entityIds.forEach((id) => this.removeCesiumEntity(viewer, id))
- }
-
- /**
- * 清除实体(支持按类型筛选)
- * @description 精准清除默认/自定义/全部实体
- * @param viewer - Cesium Viewer 实例
- * @param clearType - 清除类型:default(默认)/custom(自定义)/all(全部)
- */
- clearAllEntities(viewer: Viewer, clearType: ClearType = 'custom'): void {
- const targetIds = new Set()
-
- // 确定要清除的实体ID集合
- if (clearType === 'default') {
- targetIds.forEach((id) => this.#defaultEntityIds.add(id))
- } else if (clearType === 'custom') {
- this.#customEntityIds.forEach((id) => targetIds.add(id))
- } else {
- this.#defaultEntityIds.forEach((id) => targetIds.add(id))
- this.#customEntityIds.forEach((id) => targetIds.add(id))
- }
-
- // 执行删除并清理对应集合
- targetIds.forEach((id) => {
- const entity = viewer.entities.getById(id)
- if (entity) viewer.entities.remove(entity)
- })
-
- if (clearType === 'default') {
- this.#defaultEntityIds.clear()
- } else if (clearType === 'custom') {
- this.#customEntityIds.clear()
- } else {
- this.#defaultEntityIds.clear()
- this.#customEntityIds.clear()
+ throw new Error(`不支持的实体类型:${options.type}`)
}
}
- /**
- * 根据 ID 查询实体
- * @description 安全查询实体,自动识别默认/自定义
- * @param viewer - Cesium Viewer 实例
- * @param entityId - 实体 ID
- * @returns Entity 实例或 null
- */
- getCesiumEntityById(viewer: Viewer, entityId: string): Entity | null {
- // 先校验是否在管理集合中
- if (!this.#defaultEntityIds.has(entityId) && !this.#customEntityIds.has(entityId)) {
- return null
- }
- return viewer.entities.getById(entityId) || null
- }
-
- /**
- * 定位视角到目标位置
- * @description 支持经纬度数组或 Cartesian3,可配置飞行时间
- * @param viewer - Cesium Viewer 实例
- * @param target - 目标位置(经纬度高程数组 或 Cartesian3)
- * @param duration - 飞行时间(秒,默认:2)
- */
- flyToTarget(viewer: Viewer, target: [number, number, number] | Cartesian3, duration = 2): void {
- const position = this.convertPosition(target)
- const cartographic = Cartographic.fromCartesian(position)
- viewer.camera.flyTo({
- destination: Cartesian3.fromDegrees(
- CesiumMath.toDegrees(cartographic.longitude),
- CesiumMath.toDegrees(cartographic.latitude),
- cartographic.height,
- ),
- duration,
- })
- }
-
- /**
- * 调整视角到目标位置
- * @param viewer - Cesium Viewer 实例
- * @param target - 目标位置(经纬度高程数组 或 Cartesian3)
- */
- viewToTarget(viewer: Viewer, target: [number, number, number] | Cartesian3): void {
- const position = this.convertPosition(target)
- viewer.camera.setView({
- destination: position,
- orientation: {
- heading: CesiumMath.toRadians(0),
- pitch: CesiumMath.toRadians(-90),
- roll: 0.0,
- },
- })
- }
-
- /**
- * 批量添加Primitive类型的点线面和广告牌
- * @description 区分默认/自定义Primitive,精准管理
- * @param viewer - Cesium Viewer实例
- * @param primitives - 要添加的primitive数组(含default标识)
- */
- addPrimitivesBatch(viewer: Viewer, primitives: PrimitiveOptions[]): void {
- // 按类型分组处理,提高渲染性能
- const pointOptions: PrimitiveOptions[] = []
- const polylineOptions: PrimitiveOptions[] = []
- const polygonOptions: PrimitiveOptions[] = []
- const billboardOptions: PrimitiveOptions[] = []
-
- // 分组并校验ID唯一性(跨默认/自定义)
- primitives.forEach((option) => {
- const { id } = option
- if (this.#defaultPrimitiveMap.has(id) || this.#customPrimitiveMap.has(id)) {
- throw new Error(`Primitive ID ${id} 已存在,请勿重复添加`)
- }
-
- switch (option.type) {
- case 'point':
- pointOptions.push(option)
- break
- case 'polyline':
- polylineOptions.push(option)
- break
- case 'polygon':
- polygonOptions.push(option)
- break
- case 'billboard':
- billboardOptions.push(option)
- break
- }
- })
-
- // 处理点
- if (pointOptions.length > 0) {
- const appearance = new PerInstanceColorAppearance({
- translucent: false,
- closed: true,
- })
-
- const instances = pointOptions.map((option) => {
- const firstPosition = option.positions?.[0]
- if (!firstPosition) {
- throw new Error('positions 数组为空或首项缺失')
- }
- const position = this.convertPosition(firstPosition)
- return new GeometryInstance({
- id: option.id,
- geometry: new CircleGeometry({
- center: position,
- radius: option.pixelSize || 8,
- vertexFormat: appearance.vertexFormat,
- }),
- attributes: {
- color: ColorGeometryInstanceAttribute.fromColor(option.color || Color.RED),
- },
- })
- })
-
- const pointPrimitive = new Primitive({
- geometryInstances: instances,
- appearance: appearance,
- asynchronous: false,
- })
-
- viewer.scene.primitives.add(pointPrimitive)
-
- // 按default标识存入对应映射
- pointOptions.forEach((option) => {
- const { id, default: isDefault = false } = option
- if (isDefault) {
- this.#defaultPrimitiveMap.set(id, pointPrimitive)
- } else {
- this.#customPrimitiveMap.set(id, pointPrimitive)
- }
- })
- }
-
- // 处理线
- if (polylineOptions.length > 0) {
- const appearance = new PolylineColorAppearance({
- translucent: true,
- })
-
- const instances = polylineOptions.map((option) => {
- const positions = this.convertPositionArray(option.positions)
- return new GeometryInstance({
- id: option.id,
- geometry: new PolylineGeometry({
- positions,
- width: option.width || 3,
- vertexFormat: appearance.vertexFormat,
- }),
- attributes: {
- color: ColorGeometryInstanceAttribute.fromColor(option.color || Color.BLUE),
- },
- })
- })
-
- const polylinePrimitive = new Primitive({
- geometryInstances: instances,
- appearance: new PolylineColorAppearance({
- translucent: true,
- }),
- asynchronous: false,
- })
-
- viewer.scene.primitives.add(polylinePrimitive)
-
- // 按default标识存入对应映射
- polylineOptions.forEach((option) => {
- const { id, default: isDefault = false } = option
- if (isDefault) {
- this.#defaultPrimitiveMap.set(id, polylinePrimitive)
- } else {
- this.#customPrimitiveMap.set(id, polylinePrimitive)
- }
- })
- }
-
- // 处理面
- if (polygonOptions.length > 0) {
- const appearance = new PerInstanceColorAppearance({
- translucent: true,
- closed: true,
- })
-
- const instances = polygonOptions.map((option) => {
- const positions = this.convertPositionArray(option.positions)
- return new GeometryInstance({
- id: option.id,
- geometry: new PolygonGeometry({
- polygonHierarchy: new PolygonHierarchy(positions),
- vertexFormat: appearance.vertexFormat,
- }),
- attributes: {
- color: ColorGeometryInstanceAttribute.fromColor(
- option.color || Color.GREEN.withAlpha(0.5),
- ),
- },
- })
- })
-
- const polygonPrimitive = new Primitive({
- geometryInstances: instances,
- appearance: new PerInstanceColorAppearance({
- translucent: true,
- closed: true,
- }),
- asynchronous: false,
- })
-
- viewer.scene.primitives.add(polygonPrimitive)
-
- // 按default标识存入对应映射
- polygonOptions.forEach((option) => {
- const { id, default: isDefault = false } = option
- if (isDefault) {
- this.#defaultPrimitiveMap.set(id, polygonPrimitive)
- } else {
- this.#customPrimitiveMap.set(id, polygonPrimitive)
- }
- })
- }
-
- // 处理广告牌
- if (billboardOptions.length > 0) {
- const billboardCollection = new BillboardCollection()
-
- billboardOptions.forEach((option) => {
- const firstPosition = option.positions?.[0]
- if (!firstPosition) {
- throw new Error('positions 数组为空或第一个元素未定义')
- }
- const position = this.convertPosition(firstPosition)
- billboardCollection.add({
- id: option.id,
- position,
- image: option.image,
- scale: option.scale || 1,
- color: option.color || Color.WHITE,
- })
- })
-
- viewer.scene.primitives.add(billboardCollection)
-
- // 按default标识存入对应映射
- billboardOptions.forEach((option) => {
- const { id, default: isDefault = false } = option
- if (isDefault) {
- this.#defaultPrimitiveMap.set(id, billboardCollection)
- } else {
- this.#customPrimitiveMap.set(id, billboardCollection)
- }
- })
- }
- }
-
- /**
- * 根据ID获取Primitive实例
- * @description 自动识别默认/自定义Primitive
- * @param id - Primitive的ID
- * @returns 对应的Primitive或BillboardCollection实例(不存在返回undefined)
- */
- getPrimitiveById(id: string): Primitive | BillboardCollection | undefined {
- return this.#defaultPrimitiveMap.get(id) || this.#customPrimitiveMap.get(id)
- }
-
- /**
- * 根据ID删除Primitive
- * @description 自动识别默认/自定义并清理对应存储
- * @param viewer - Cesium Viewer实例
- * @param id - Primitive的ID
- * @returns 是否删除成功
- */
- removePrimitiveById(viewer: Viewer, id: string): boolean {
- // 先判断Primitive类型
- const isDefault = this.#defaultPrimitiveMap.has(id)
- const isCustom = this.#customPrimitiveMap.has(id)
-
- if (!isDefault && !isCustom) {
- console.warn(`Primitive ID ${id} 不存在`)
- return false
- }
-
- const primitive = isDefault ? this.#defaultPrimitiveMap.get(id) : this.#customPrimitiveMap.get(id)
- if (primitive) {
- // 从场景中移除
- viewer.scene.primitives.remove(primitive)
- // 清理对应映射
- if (isDefault) {
- this.#defaultPrimitiveMap.delete(id)
- } else {
- this.#customPrimitiveMap.delete(id)
- }
- return true
- }
- return false
- }
-
- /**
- * 清除Primitive(支持按类型筛选)
- * @description 精准清除默认/自定义/全部Primitive
- * @param viewer - Cesium Viewer实例
- * @param clearType - 清除类型:default(默认)/custom(自定义)/all(全部)
- */
- clearAllPrimitives(viewer: Viewer, clearType: ClearType = 'custom'): void {
- const targetMap = new Map()
-
- // 确定要清除的Primitive映射
- if (clearType === 'default') {
- this.#defaultPrimitiveMap.forEach((value, key) => targetMap.set(key, value))
- } else if (clearType === 'custom') {
- this.#customPrimitiveMap.forEach((value, key) => targetMap.set(key, value))
- } else {
- this.#defaultPrimitiveMap.forEach((value, key) => targetMap.set(key, value))
- this.#customPrimitiveMap.forEach((value, key) => targetMap.set(key, value))
- }
-
- // 执行删除
- targetMap.forEach((primitive) => {
- viewer.scene.primitives.remove(primitive)
- })
-
- // 清理对应映射
- if (clearType === 'default') {
- this.#defaultPrimitiveMap.clear()
- } else if (clearType === 'custom') {
- this.#customPrimitiveMap.clear()
- } else {
- this.#defaultPrimitiveMap.clear()
- this.#customPrimitiveMap.clear()
- }
- }
-
- /**
- * 创建通用图层(支持imagery/wms/wmts)
- * @description 区分默认/自定义图层,精准管理
- * @param viewer - Cesium Viewer实例
- * @param layerConfig - 图层配置(含default标识)
- * @returns 创建的ImageryLayer实例(失败返回null)
- */
- createLayer(viewer: Viewer, layerConfig: LayerConfig): ImageryLayer | null {
- if (!layerConfig.layers) {
- throw new Error('layers 参数未定义')
- }
-
- const { layers: layerKey, default: isDefault = false } = layerConfig
-
- // 校验图层唯一性(跨默认/自定义)
- if (this.#defaultLayerMap.has(layerKey) || this.#customLayerMap.has(layerKey)) {
- console.warn(`图层 ${layerKey} 已存在,将覆盖原有图层`)
- this.removeLayerByKey(layerKey, viewer)
- }
-
- let provider: ImageryProvider | null = null
- switch (layerConfig.type) {
- case 'imagery':
- provider = new ArcGisMapServerImageryProvider({ url: layerConfig.url })
- break
- case 'wms': // Geoserver WMS
- provider = new WebMapServiceImageryProvider({
- url: layerConfig.url,
- layers: layerConfig.layers,
- parameters: layerConfig.parameters || { format: 'image/png' },
- })
- break
- case 'wmts': // Geoserver WMTS
- provider = new WebMapTileServiceImageryProvider({
- url: layerConfig.url,
- layer: layerConfig.layers,
- style: layerConfig.style || 'default',
- format: layerConfig.format || 'image/png',
- tileMatrixSetID: layerConfig.tileMatrixSetID || 'EPSG:4326',
- credit: '',
- })
- break
- default:
- console.error(`不支持的图层类型:${layerConfig.type}`)
- return null
- }
-
- if (provider) {
- const layer = viewer.imageryLayers.addImageryProvider(provider)
- // 按default标识存入对应映射
- if (isDefault) {
- this.#defaultLayerMap.set(layerKey, layer)
- } else {
- this.#customLayerMap.set(layerKey, layer)
- }
- return layer
- }
- return null
- }
-
- /**
- * 根据图层key(layerConfig.layers)获取图层实例
- * @description 自动识别默认/自定义图层
- * @param key - 图层key(layerConfig.layers)
- * @returns ImageryLayer实例(不存在返回undefined)
- */
- getLayerByKey(key: string): ImageryLayer | undefined {
- return this.#defaultLayerMap.get(key) || this.#customLayerMap.get(key)
- }
-
- /**
- * 根据图层key删除图层
- * @description 自动识别默认/自定义并清理对应存储
- * @param key - 图层key(layerConfig.layers)
- * @param viewer - Cesium Viewer实例
- * @returns 是否删除成功
- */
- removeLayerByKey(key: string, viewer: Viewer): boolean {
- // 先判断图层类型
- const isDefault = this.#defaultLayerMap.has(key)
- const isCustom = this.#customLayerMap.has(key)
-
- if (!isDefault && !isCustom) {
- console.warn(`图层 key ${key} 不存在`)
- return false
- }
-
- const layer = isDefault ? this.#defaultLayerMap.get(key) : this.#customLayerMap.get(key)
- if (layer) {
- // 从场景中移除图层
- viewer.imageryLayers.remove(layer)
- // 清理对应映射
- if (isDefault) {
- this.#defaultLayerMap.delete(key)
- } else {
- this.#customLayerMap.delete(key)
- }
- return true
- }
- return false
- }
-
- /**
- * 清除图层(支持按类型筛选)
- * @description 精准清除默认/自定义/全部自定义图层
- * @param viewer - Cesium Viewer实例
- * @param clearType - 清除类型:default(默认)/custom(自定义)/all(全部)
- */
- clearAllCustomLayers(viewer: Viewer, clearType: ClearType = 'custom'): void {
- const targetMap = new Map()
-
- // 确定要清除的图层映射
- if (clearType === 'default') {
- this.#defaultLayerMap.forEach((value, key) => targetMap.set(key, value))
- } else if (clearType === 'custom') {
- this.#customLayerMap.forEach((value, key) => targetMap.set(key, value))
- } else {
- this.#defaultLayerMap.forEach((value, key) => targetMap.set(key, value))
- this.#customLayerMap.forEach((value, key) => targetMap.set(key, value))
- }
-
- // 执行删除
- targetMap.forEach((layer) => {
- viewer.imageryLayers.remove(layer)
- })
-
- // 清理对应映射
- if (clearType === 'default') {
- this.#defaultLayerMap.clear()
- } else if (clearType === 'custom') {
- this.#customLayerMap.clear()
- } else {
- this.#defaultLayerMap.clear()
- this.#customLayerMap.clear()
- }
- }
-
- /**
- * 批量清除所有私有资源(实体/Primitive/图层)
- * @description 支持按类型筛选,精准控制清除范围
- * @param viewer - Cesium Viewer实例
- * @param clearType - 清除类型:default(默认)/custom(自定义)/all(全部)
- */
- clearAllPrivateObject(viewer: Viewer, clearType: ClearType = 'custom'): void {
- this.clearAllEntities(viewer, clearType)
- this.clearAllPrimitives(viewer, clearType)
- this.clearAllCustomLayers(viewer, clearType)
- }
-
- /**
- * 销毁 Cesium 实例
- * @description 释放所有内存,避免泄漏(页面卸载时调用)
- * @param viewer - Cesium Viewer 实例
- */
- destroyCesiumViewer(viewer: Viewer): void {
- if (viewer) {
- // 清除所有资源(默认+自定义)
- this.clearAllPrivateObject(viewer, 'all')
- // 销毁viewer
- viewer.destroy()
- }
- }
-
- /**
- * 坐标转换辅助函数
- */
- 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))
- }
-
- /**
- * 多边形层级处理辅助函数
- */
#processHierarchy(
hier: PolygonHierarchy | Cartesian3[] | [number, number][] | [number, number, number][],
): PolygonHierarchy {
- if (hier instanceof PolygonHierarchy) {
- return hier
- }
-
+ 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 (pos instanceof Cartesian3) return pos
if (Array.isArray(pos) && pos.length >= 2) {
return Cartesian3.fromDegrees(pos[0], pos[1], pos[2] || 0)
}
@@ -931,10 +1037,662 @@ export class CesiumUtils {
return new PolygonHierarchy(positions)
}
- /**
- * 创建ConstantProperty包装器
- */
#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)
+ }
+
+ #groupPrimitivesByType(primitives: PrimitiveOptions[]) {
+ // 替换原第640行附近的代码段落为以下内容:
+ 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)
+ }
+ })
+ }
+
+ #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)
+ }
+
+ /** 图层是否存在 */
+ #exists(layerId: string): boolean {
+ return this.#defaultGeoJsonMap.has(layerId) || this.#customGeoJsonMap.has(layerId);
+ }
+
+ /** 合并用户配置 + 默认配置 */
+ #mergeOptions(options?: GeoJsonOptions): Required {
+ return {
+ ...CesiumUtils.DEFAULT_OPTIONS,
+ ...options,
+ labelStyle: { ...CesiumUtils.DEFAULT_OPTIONS.labelStyle, ...options?.labelStyle },
+ polygonStyle: { ...CesiumUtils.DEFAULT_OPTIONS.polygonStyle, ...options?.polygonStyle },
+ polylineStyle: { ...CesiumUtils.DEFAULT_OPTIONS.polylineStyle, ...options?.polylineStyle },
+ pointStyle: { ...CesiumUtils.DEFAULT_OPTIONS.pointStyle, ...options?.pointStyle },
+ };
+ }
+
+ /** 统一应用样式到实体 */
+ #applyStyle(entity: Entity, options: Required): 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),
+ });
+ }
+ }
+
+ /** 获取图层信息 */
+ #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 };
+ }
+
+ #getTargetIdsByType(
+ clearType: ClearType,
+ defaultSet: Set,
+ customSet: Set,
+ ): Set {
+ const targetIds = new Set()
+ if (clearType === 'default' || clearType === 'all')
+ defaultSet.forEach((id) => targetIds.add(id))
+ if (clearType === 'custom' || clearType === 'all') customSet.forEach((id) => targetIds.add(id))
+ return targetIds
+ }
+
+ #getTargetMapByType(
+ clearType: ClearType,
+ defaultMap: Map,
+ customMap: Map,
+ ): Map {
+ const targetMap = new Map()
+ if (clearType === 'default' || clearType === 'all')
+ defaultMap.forEach((value, key) => targetMap.set(key, value))
+ if (clearType === 'custom' || clearType === 'all')
+ customMap.forEach((value, key) => targetMap.set(key, value))
+ return targetMap
+ }
+
+ #clearCollectionsByType(
+ clearType: ClearType,
+ defaultSet: Set,
+ customSet: Set,
+ ): void {
+ if (clearType === 'default' || clearType === 'all') defaultSet.clear()
+ if (clearType === 'custom' || clearType === 'all') customSet.clear()
+ }
+
+ #clearMapsByType(
+ clearType: ClearType,
+ defaultMap: Map,
+ customMap: Map,
+ ): void {
+ if (clearType === 'default' || clearType === 'all') defaultMap.clear()
+ if (clearType === 'custom' || clearType === 'all') customMap.clear()
+ }
+
+ /**
+ * 计算面要素的中心点作为标签位置
+ * @param entity
+ * @returns
+ */
+ #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
+ }
+
+ // 解析点坐标
+ #parsePointCoords(dataView: DataView, offset: number, isLittleEndian: boolean): [number, number] {
+ let currentOffset = offset
+
+ // 读取X坐标(8字节,双精度浮点数)
+ const x = dataView.getFloat64(currentOffset, isLittleEndian)
+ currentOffset += 8
+
+ // 读取Y坐标(8字节,双精度浮点数)
+ const y = dataView.getFloat64(currentOffset, isLittleEndian)
+
+ return [x, y]
+ }
+
+ // 解析3D点坐标(用于billboard)
+ #parsePoint3DCoords(
+ dataView: DataView,
+ offset: number,
+ isLittleEndian: boolean,
+ ): [number, number] {
+ let currentOffset = offset
+
+ // 读取X坐标
+ const x = dataView.getFloat64(currentOffset, isLittleEndian)
+ currentOffset += 8
+
+ // 读取Y坐标
+ const y = dataView.getFloat64(currentOffset, isLittleEndian)
+ currentOffset += 8
+
+ return [x, y]
+ }
+
+ // 解析线坐标
+ #parseLineStringCoords(
+ dataView: DataView,
+ offset: number,
+ isLittleEndian: boolean,
+ ): [number, number][] {
+ let currentOffset = offset
+
+ // 读取点的数量(4字节)
+ const numPoints = dataView.getUint32(currentOffset, isLittleEndian)
+ currentOffset += 4
+
+ const coordinates: [number, number][] = []
+
+ // 读取所有点
+ for (let i = 0; i < numPoints; i++) {
+ const x = dataView.getFloat64(currentOffset, isLittleEndian)
+ currentOffset += 8
+
+ const y = dataView.getFloat64(currentOffset, isLittleEndian)
+ currentOffset += 8
+
+ coordinates.push([x, y])
+ }
+
+ return coordinates
+ }
+
+ // 解析面坐标
+ #parsePolygonCoords(
+ dataView: DataView,
+ offset: number,
+ isLittleEndian: boolean,
+ ): [number, number][][] {
+ let currentOffset = offset
+
+ // 读取环的数量(4字节)
+ const numRings = dataView.getUint32(currentOffset, isLittleEndian)
+ currentOffset += 4
+
+ const coordinates: [number, number][][] = []
+
+ // 读取所有环
+ for (let ringIndex = 0; ringIndex < numRings; ringIndex++) {
+ // 读取环中点的数量(4字节)
+ const numPoints = dataView.getUint32(currentOffset, isLittleEndian)
+ currentOffset += 4
+
+ const ring: [number, number][] = []
+
+ // 读取环中的所有点
+ for (let i = 0; i < numPoints; i++) {
+ const x = dataView.getFloat64(currentOffset, isLittleEndian)
+ currentOffset += 8
+
+ const y = dataView.getFloat64(currentOffset, isLittleEndian)
+ currentOffset += 8
+
+ ring.push([x, y])
+ }
+
+ coordinates.push(ring)
+ }
+
+ return coordinates
+ }
+
+ // 计算边界框
+ #calculateBbox(
+ type: string,
+ coordinates: [number, number] | [number, number][] | [number, number][][],
+ ): [number, number, number, number] | undefined {
+ if (!coordinates) return undefined
+
+ let minX = Infinity,
+ minY = Infinity,
+ maxX = -Infinity,
+ maxY = -Infinity
+
+ const updateBbox = (x: number, y: number) => {
+ minX = Math.min(minX, x)
+ minY = Math.min(minY, y)
+ maxX = Math.max(maxX, x)
+ maxY = Math.max(maxY, y)
+ }
+
+ switch (type) {
+ case 'point':
+ case 'billboard':
+ // coordinates 是 [number, number]
+ const coordsPoint = coordinates as [number, number]
+ updateBbox(coordsPoint[0], coordsPoint[1])
+ break
+
+ case 'polyline':
+ // coordinates 是 [number, number][]
+ const coordsLine = coordinates as [number, number][]
+ coordsLine.forEach((coord: [number, number]) => {
+ updateBbox(coord[0], coord[1])
+ })
+ break
+
+ case 'polygon':
+ // coordinates 是 [number, number][][]
+ const coordsPolygon = coordinates as [number, number][][]
+ coordsPolygon.forEach((ring: [number, number][]) => {
+ ring.forEach((coord: [number, number]) => {
+ updateBbox(coord[0], coord[1])
+ })
+ })
+ break
+ }
+
+ if (minX === Infinity) return undefined
+
+ return [minX, minY, maxX, maxY]
+ }
+
+ /**
+ * 验证坐标格式是否符合几何类型要求
+ */
+ #validateCoordinates(
+ type: 'point' | 'polyline' | 'polygon',
+ coordinates: [number, number] | [number, number][] | [number, number][][],
+ ): void {
+ switch (type) {
+ case 'point':
+ if (!Array.isArray(coordinates) || coordinates.length < 2) {
+ throw new Error('点坐标必须是 [x, y] 格式的数组')
+ }
+ break
+ case 'polyline':
+ if (!Array.isArray(coordinates) || coordinates.length < 2) {
+ throw new Error('线坐标必须包含至少2个点的数组')
+ }
+ const coordsLine = coordinates as [number, number][]
+ coordsLine.forEach((coord: [number, number], idx: number) => {
+ if (!Array.isArray(coord) || coord.length < 2) {
+ throw new Error(`线坐标第${idx}个点格式错误,应为 [x, y]`)
+ }
+ })
+ break
+ case 'polygon':
+ if (!Array.isArray(coordinates) || coordinates.length === 0) {
+ throw new Error('面坐标必须是包含至少一个环的数组')
+ }
+ const coordsPolygon = coordinates as [number, number][][]
+ coordsPolygon.forEach((ring: [number, number][], ringIdx: number) => {
+ if (!Array.isArray(ring) || ring.length < 4 || !this.#isRingClosed(ring)) {
+ throw new Error(`面第${ringIdx}个环必须是至少4个点的闭合环`)
+ }
+ ring.forEach((coord: [number, number]) => {
+ if (!Array.isArray(coord) || coord.length < 2) {
+ throw new Error(`面环第${ringIdx}个点格式错误,应为 [x, y]`)
+ }
+ })
+ })
+ break
+ }
+ }
+
+ /**
+ * 检查面环是否闭合(首尾点是否相同)
+ */
+ #isRingClosed(ring: [number, number][]): boolean {
+ const first = ring[0]
+ const last = ring[ring.length - 1]
+ return first![0] === last![0] && first![1] === last![1]
+ }
+
+ /**
+ * 写入点坐标 (x, y 双精度浮点数)
+ */
+ #writePointCoordinates(
+ dataView: DataView,
+ offset: number,
+ coord: [number, number],
+ littleEndian: boolean,
+ ): void {
+ dataView.setFloat64(offset, coord[0], littleEndian) // x
+ dataView.setFloat64(offset + 8, coord[1], littleEndian) // y
+ }
+
+ /**
+ * 写入线坐标 (点数量 + 每个点坐标)
+ */
+ #writeLineStringCoordinates(
+ dataView: DataView,
+ offset: number,
+ coords: [number, number][],
+ littleEndian: boolean,
+ ): number {
+ // 写入点数量 (4字节)
+ dataView.setUint32(offset, coords.length, littleEndian)
+ offset += 4
+
+ // 写入每个点坐标
+ coords.forEach((coord) => {
+ this.#writePointCoordinates(dataView, offset, coord, littleEndian)
+ offset += 16 // 每个点16字节
+ })
+ return offset
+ }
+
+ /**
+ * 写入面坐标 (环数量 + 每个环的点数量 + 每个点坐标)
+ */
+ #writePolygonCoordinates(
+ dataView: DataView,
+ offset: number,
+ rings: [number, number][][],
+ littleEndian: boolean,
+ ): number {
+ // 写入环数量 (4字节)
+ dataView.setUint32(offset, rings.length, littleEndian)
+ offset += 4
+
+ // 写入每个环
+ rings.forEach((ring) => {
+ // 写入环的点数量 (4字节)
+ dataView.setUint32(offset, ring.length, littleEndian)
+ offset += 4
+
+ // 写入环的每个点
+ ring.forEach((coord) => {
+ this.#writePointCoordinates(dataView, offset, coord, littleEndian)
+ offset += 16
+ })
+ })
+ return offset
+ }
+
+ /**
+ * 将ArrayBuffer转换为十六进制字符串
+ */
+ #bufferToHexString(buffer: ArrayBuffer, byteLength: number): string {
+ const uint8Array = new Uint8Array(buffer, 0, byteLength)
+ return Array.from(uint8Array)
+ .map((byte) => byte.toString(16).padStart(2, '0'))
+ .join('')
+ .toUpperCase()
+ }
}
+
+// 导出单例模式
+export const CesiumUtilsSingleton = new CesiumUtils()
\ No newline at end of file