From 1448aed7f0be55046a1045000b2fdd414658bcfe Mon Sep 17 00:00:00 2001 From: wzy-warehouse <18135009705@163.com> Date: Wed, 8 Apr 2026 18:17:54 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E5=88=9D=E5=A7=8B=E5=8C=96?= =?UTF-8?q?=E5=9F=BA=E6=9C=AC=E5=8A=A0=E8=BD=BD=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- env.d.ts | 3 +- src/component/map/Map.vue | 56 +- src/types/cesium/EntityOptions.ts | 10 +- src/types/cesium/GeoJsonOptions.ts | 35 + src/types/cesium/LabelConfig.ts | 13 + src/utils/cesium/CesiumUtils.ts | 2154 +++++++++++++++++++--------- 6 files changed, 1561 insertions(+), 710 deletions(-) create mode 100644 src/types/cesium/LabelConfig.ts 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