1698 lines
51 KiB
TypeScript
1698 lines
51 KiB
TypeScript
import type { CesiumInitOptions } from '@/types/cesium/CesiumInitOptions'
|
||
import type { EntityOptions } from '@/types/cesium/EntityOptions'
|
||
import type { PrimitiveOptions } from '@/types/cesium/PrimitiveOptions'
|
||
import type { LayerConfig } from '@/types/cesium/LayerConfig'
|
||
import {
|
||
Viewer,
|
||
Entity,
|
||
Cartesian3,
|
||
Color,
|
||
PointGraphics,
|
||
PolylineGraphics,
|
||
BillboardGraphics,
|
||
SceneMode,
|
||
HeightReference,
|
||
VerticalOrigin,
|
||
HorizontalOrigin,
|
||
Cartographic,
|
||
ColorMaterialProperty,
|
||
Ion,
|
||
WebMapTileServiceImageryProvider,
|
||
ImageryProvider,
|
||
ImageryLayer,
|
||
Math as CesiumMath,
|
||
PolygonHierarchy,
|
||
PolygonGraphics,
|
||
ConstantProperty,
|
||
Primitive,
|
||
BillboardCollection,
|
||
GeometryInstance,
|
||
CircleGeometry,
|
||
ColorGeometryInstanceAttribute,
|
||
PerInstanceColorAppearance,
|
||
PolylineGeometry,
|
||
PolylineColorAppearance,
|
||
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 核心操作,区分默认/自定义资源管理
|
||
*/
|
||
export class CesiumUtils {
|
||
// ===================== 定义viewer =====================
|
||
#viewer: Viewer | null = null
|
||
|
||
// ===================== 私有属性定义 =====================
|
||
#defaultEntityIds = new Set<string>()
|
||
#customEntityIds = new Set<string>()
|
||
#defaultPrimitiveMap = new Map<string, Primitive | BillboardCollection>()
|
||
#customPrimitiveMap = new Map<string, Primitive | BillboardCollection>()
|
||
#defaultLayerMap = new Map<string, ImageryLayer>()
|
||
#customLayerMap = new Map<string, ImageryLayer>()
|
||
#defaultGeoJsonMap = new Map<string, DataSource>()
|
||
#customGeoJsonMap = new Map<string, DataSource>()
|
||
|
||
constructor() {
|
||
Ion.defaultAccessToken = config.cesiumIonDefaultAccessToken
|
||
}
|
||
|
||
// ===================== 静态配置 ========================
|
||
static readonly DEFAULT_OPTIONS: Required<GeoJsonOptions> = {
|
||
showName: false,
|
||
labelStyle: {
|
||
labelFont: '16px "微软雅黑"',
|
||
labelColor: Color.RED,
|
||
backgroundColor: Color.BLACK,
|
||
labelSize: 16,
|
||
horizontalOrigin: HorizontalOrigin.CENTER,
|
||
verticalOrigin: VerticalOrigin.CENTER,
|
||
labelOffset: new Cartesian2(0, -10)
|
||
},
|
||
polygonStyle: {
|
||
fill: true,
|
||
fillColor: Color.RED.withAlpha(0.3),
|
||
outline: true,
|
||
outlineColor: Color.BLACK,
|
||
outlineWidth: 2,
|
||
},
|
||
polylineStyle: {
|
||
width: 2,
|
||
material: Color.BLUE,
|
||
clampToGround: true,
|
||
},
|
||
pointStyle: {
|
||
pixelSize: 8,
|
||
color: Color.RED,
|
||
outlineColor: Color.WHITE,
|
||
outlineWidth: 2,
|
||
},
|
||
onComplete: () => { },
|
||
};
|
||
|
||
// ===================== 初始化与销毁 =====================
|
||
|
||
/**
|
||
* 初始化 Cesium Viewer
|
||
*/
|
||
initCesiumViewer(options: CesiumInitOptions, tdMapToken?: string[], type: number = 0): void {
|
||
const defaultOptions: CesiumInitOptions = {
|
||
containerId: options.containerId,
|
||
shouldAnimate: true,
|
||
baseLayerPicker: false,
|
||
timeline: false,
|
||
animation: false,
|
||
infoBox: false,
|
||
navigationHelpButton: false,
|
||
fullscreenButton: false,
|
||
homeButton: false,
|
||
scene3DOnly: false,
|
||
sceneModePicker: false,
|
||
geocoder: false,
|
||
sceneMode: SceneMode.SCENE3D,
|
||
}
|
||
|
||
const finalOptions = { ...defaultOptions, ...options }
|
||
const container = document.getElementById(finalOptions.containerId)
|
||
|
||
if (!container) {
|
||
throw new Error(`Cesium 容器 #${finalOptions.containerId} 不存在`)
|
||
}
|
||
|
||
const viewer = new Viewer(container, {
|
||
...finalOptions,
|
||
terrainProvider: createWorldTerrain(),
|
||
selectionIndicator: false, // 禁用选择指示器
|
||
baseLayerPicker: false, // 禁用默认底图
|
||
contextOptions: {
|
||
webgl: {
|
||
alpha: true,
|
||
depth: false,
|
||
stencil: true,
|
||
antialias: true,
|
||
premultipliedAlpha: true,
|
||
preserveDrawingBuffer: true,
|
||
failIfMajorPerformanceCaveat: true,
|
||
},
|
||
allowTextureFilterAnisotropic: true,
|
||
},
|
||
})
|
||
|
||
// 性能优化配置
|
||
viewer.scene.globe.depthTestAgainstTerrain = false
|
||
viewer.scene.fog.enabled = false
|
||
viewer.scene.globe.enableLighting = false
|
||
viewer.shadows = false
|
||
const creditContainer = viewer.cesiumWidget.creditContainer as HTMLElement
|
||
creditContainer.style.display = 'none'
|
||
|
||
// 添加底图
|
||
this.imageryProvider(type, tdMapToken || config.tdMapToken).forEach((provider) => {
|
||
viewer.imageryLayers.addImageryProvider(provider)
|
||
})
|
||
|
||
this.#viewer = viewer
|
||
}
|
||
|
||
/**
|
||
* 销毁 Cesium Viewer
|
||
*/
|
||
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',
|
||
style: 'default',
|
||
minimumLevel: 0,
|
||
maximumLevel: 18,
|
||
credit: 'Tianditu',
|
||
subdomains: ['t0', 't1', 't2', 't3', 't4', 't5', 't6', 't7'],
|
||
}
|
||
|
||
if (type === 0) {
|
||
const token = tdMapToken[Math.floor(Math.random() * tdMapToken.length)]
|
||
const imageryProvider = new WebMapTileServiceImageryProvider({
|
||
url: `https://{s}.tianditu.gov.cn/img_w/wmts?tk=${token}`,
|
||
layer: 'img',
|
||
...option,
|
||
})
|
||
const annotationProvider = new WebMapTileServiceImageryProvider({
|
||
url: `https://{s}.tianditu.gov.cn/cia_w/wmts?tk=${token}`,
|
||
layer: 'cia',
|
||
...option,
|
||
})
|
||
return [imageryProvider, annotationProvider]
|
||
} else {
|
||
const vectorProvider = new WebMapTileServiceImageryProvider({
|
||
url: `https://{s}.tianditu.gov.cn/vec_w/wmts?tk=cc`,
|
||
layer: 'vec',
|
||
...option,
|
||
})
|
||
return [vectorProvider]
|
||
}
|
||
}
|
||
|
||
// ===================== 实体管理 =====================
|
||
|
||
/**
|
||
* 添加实体
|
||
*/
|
||
addCesiumEntity(entityOptions: EntityOptions): Entity {
|
||
const { id, position, attributes = {}, default: isDefault = false } = entityOptions
|
||
|
||
if (!id) throw new Error('实体 id 为必填项')
|
||
if (!position) throw new Error('实体 position 为必填项')
|
||
this.#validateUniqueId(id)
|
||
|
||
const entity = new Entity({
|
||
id,
|
||
position: this.convertPosition(position),
|
||
...attributes,
|
||
})
|
||
|
||
this.#configureEntityGraphics(entity, entityOptions)
|
||
|
||
this.#viewer?.entities.add(entity)
|
||
this.#storeEntityId(id, isDefault)
|
||
return entity
|
||
}
|
||
|
||
/**
|
||
* 查询实体
|
||
*/
|
||
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<DataSource> {
|
||
if (this.#exists(layerId)) throw new Error(`图层 ${layerId} 已存在`);
|
||
const opt = this.#mergeOptions(options);
|
||
|
||
// 加载并应用样式
|
||
const dataSource = await GeoJsonDataSource.load(geojsonData);
|
||
dataSource.entities.values.forEach(e => this.#applyStyle(e, opt));
|
||
|
||
// 添加到地图
|
||
await this.#viewer!.dataSources.add(dataSource);
|
||
isDefault ? this.#defaultGeoJsonMap.set(layerId, dataSource) : this.#customGeoJsonMap.set(layerId, dataSource);
|
||
|
||
// 如果需要显示标签,调用 addLabelsToDataSource
|
||
if (opt.showName && opt.labelStyle) {
|
||
this.addLabelsToDataSource(dataSource, {
|
||
labelText: opt.labelStyle.labelText,
|
||
labelFont: opt.labelStyle.labelFont,
|
||
labelColor: opt.labelStyle.labelColor,
|
||
labelSize: opt.labelStyle.labelSize,
|
||
labelOffset: opt.labelStyle.labelOffset,
|
||
horizontalOrigin: opt.labelStyle.horizontalOrigin,
|
||
verticalOrigin: opt.labelStyle.verticalOrigin,
|
||
backgroundColor: opt.labelStyle.backgroundColor,
|
||
center: opt.labelStyle.center,
|
||
});
|
||
}
|
||
|
||
opt.onComplete(dataSource);
|
||
return dataSource;
|
||
}
|
||
|
||
/** 根据ID查询图层 */
|
||
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,
|
||
heightReference = HeightReference.CLAMP_TO_GROUND,
|
||
} = options.pointOptions || {}
|
||
entity.point = new PointGraphics({
|
||
color,
|
||
pixelSize,
|
||
outlineColor,
|
||
outlineWidth,
|
||
heightReference,
|
||
})
|
||
break
|
||
}
|
||
case 'polyline': {
|
||
const {
|
||
positions,
|
||
color = Color.BLUE,
|
||
width = 3,
|
||
clampToGround = false,
|
||
} = options.polylineOptions || {}
|
||
if (!positions) throw new Error('线实体必须传入 polylineOptions.positions')
|
||
|
||
entity.polyline = new PolylineGraphics({
|
||
positions: this.convertPositionArray(positions),
|
||
material: new ColorMaterialProperty(color),
|
||
width,
|
||
clampToGround,
|
||
})
|
||
break
|
||
}
|
||
case 'billboard': {
|
||
const {
|
||
image,
|
||
scale = 1,
|
||
color = Color.WHITE,
|
||
verticalOrigin = VerticalOrigin.CENTER,
|
||
horizontalOrigin = HorizontalOrigin.CENTER,
|
||
heightReference = HeightReference.CLAMP_TO_GROUND,
|
||
} = options.billboardOptions || {}
|
||
if (!image) throw new Error('Billboard 实体必须传入 billboardOptions.image')
|
||
|
||
entity.billboard = new BillboardGraphics({
|
||
image,
|
||
scale,
|
||
color,
|
||
verticalOrigin,
|
||
horizontalOrigin,
|
||
heightReference,
|
||
})
|
||
break
|
||
}
|
||
case 'polygon': {
|
||
const {
|
||
hierarchy,
|
||
// color = Color.GREEN.withAlpha(0.7),
|
||
outline = true,
|
||
outlineColor = Color.BLACK,
|
||
outlineWidth = 1,
|
||
height = 0,
|
||
extrudedHeight,
|
||
heightReference = HeightReference.CLAMP_TO_GROUND,
|
||
material = new GridMaterialProperty({
|
||
color: Color.GREEN.withAlpha(0.3), // 栅格线颜色
|
||
cellAlpha: 0.2, // 栅格背景透明度
|
||
lineCount: new 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: material,
|
||
outline: this.#createConstantProperty(outline),
|
||
outlineColor: this.#createConstantProperty(outlineColor),
|
||
outlineWidth: this.#createConstantProperty(outlineWidth),
|
||
height: this.#createConstantProperty(height),
|
||
extrudedHeight:
|
||
extrudedHeight !== undefined ? this.#createConstantProperty(extrudedHeight) : undefined,
|
||
heightReference,
|
||
})
|
||
break
|
||
}
|
||
default:
|
||
throw new Error(`不支持的实体类型:${options.type}`)
|
||
}
|
||
}
|
||
|
||
#processHierarchy(
|
||
hier: PolygonHierarchy | Cartesian3[] | [number, number][] | [number, number, number][],
|
||
): PolygonHierarchy {
|
||
if (hier instanceof PolygonHierarchy) return hier
|
||
if (!Array.isArray(hier) || hier.length < 3) {
|
||
throw new Error('多边形层级必须是非空数组且至少 3 个顶点')
|
||
}
|
||
|
||
const positions = hier.map((pos) => {
|
||
if (pos instanceof Cartesian3) return pos
|
||
if (Array.isArray(pos) && pos.length >= 2) {
|
||
return Cartesian3.fromDegrees(pos[0], pos[1], pos[2] || 0)
|
||
}
|
||
throw new Error(
|
||
`无效坐标格式:${JSON.stringify(pos)},应为 [经, 纬] 或 [经, 纬, 高] 或 Cartesian3`,
|
||
)
|
||
})
|
||
|
||
return new PolygonHierarchy(positions)
|
||
}
|
||
|
||
#createConstantProperty(value: unknown): ConstantProperty {
|
||
return new ConstantProperty(value)
|
||
}
|
||
|
||
#validateUniqueId(id: string): void {
|
||
if (this.#defaultEntityIds.has(id) || this.#customEntityIds.has(id)) {
|
||
throw new Error(`实体 ID ${id} 已存在`)
|
||
}
|
||
}
|
||
|
||
#entityExists(id: string): boolean {
|
||
return this.#defaultEntityIds.has(id) || this.#customEntityIds.has(id)
|
||
}
|
||
|
||
#storeEntityId(id: string, isDefault: boolean): void {
|
||
if (isDefault) {
|
||
this.#defaultEntityIds.add(id)
|
||
} else {
|
||
this.#customEntityIds.add(id)
|
||
}
|
||
}
|
||
|
||
#removeEntityId(id: string): void {
|
||
this.#defaultEntityIds.delete(id)
|
||
this.#customEntityIds.delete(id)
|
||
}
|
||
|
||
#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<GeoJsonOptions> {
|
||
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<GeoJsonOptions>): void {
|
||
const { polygonStyle, polylineStyle, pointStyle } = options;
|
||
|
||
if (entity.point) {
|
||
Object.assign(entity.point, {
|
||
pixelSize: new ConstantProperty(pointStyle.pixelSize),
|
||
color: new ConstantProperty(pointStyle.color),
|
||
outlineColor: new ConstantProperty(pointStyle.outlineColor),
|
||
outlineWidth: new ConstantProperty(pointStyle.outlineWidth),
|
||
});
|
||
}
|
||
|
||
if (entity.polyline) {
|
||
Object.assign(entity.polyline, {
|
||
width: new ConstantProperty(polylineStyle.width),
|
||
material: new ColorMaterialProperty(polylineStyle.material as Color),
|
||
clampToGround: new ConstantProperty(polylineStyle.clampToGround),
|
||
});
|
||
}
|
||
|
||
if (entity.polygon) {
|
||
Object.assign(entity.polygon, {
|
||
fill: new ConstantProperty(polygonStyle.fill),
|
||
material: new ColorMaterialProperty(polygonStyle.fillColor as Color),
|
||
outline: new ConstantProperty(polygonStyle.outline),
|
||
outlineColor: new ConstantProperty(polygonStyle.outlineColor),
|
||
outlineWidth: new ConstantProperty(polygonStyle.outlineWidth),
|
||
});
|
||
}
|
||
}
|
||
|
||
/** 获取图层信息 */
|
||
#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<string>,
|
||
customSet: Set<string>,
|
||
): Set<string> {
|
||
const targetIds = new Set<string>()
|
||
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<T>(
|
||
clearType: ClearType,
|
||
defaultMap: Map<string, T>,
|
||
customMap: Map<string, T>,
|
||
): Map<string, T> {
|
||
const targetMap = new Map<string, T>()
|
||
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<string>,
|
||
customSet: Set<string>,
|
||
): void {
|
||
if (clearType === 'default' || clearType === 'all') defaultSet.clear()
|
||
if (clearType === 'custom' || clearType === 'all') customSet.clear()
|
||
}
|
||
|
||
#clearMapsByType<T>(
|
||
clearType: ClearType,
|
||
defaultMap: Map<string, T>,
|
||
customMap: Map<string, T>,
|
||
): 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() |