From 16bd368d473771b08ff056eec35761f81cb48178 Mon Sep 17 00:00:00 2001 From: zhulongchuan <1624729110@qq.com> Date: Sat, 11 Oct 2025 16:10:41 +0800 Subject: [PATCH] =?UTF-8?q?cesium=E5=9C=B0=E5=9B=BE=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E7=94=BB=E7=A9=BA=E5=9F=9F=E7=9A=84=E5=B7=A5=E5=85=B7=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- auto-imports.d.ts | 1 + src/views/cesiums/App.vue | 49 + src/views/cesiums/CesiumToolPanel.vue | 168 ++++ src/views/cesiums/CesiumViewer.vue | 249 +++++ src/views/cesiums/CoordinatePickerPanel.vue | 437 +++++++++ src/views/cesiums/DrawingToolPanel.vue | 407 ++++++++ src/views/cesiums/ModelControlPanel.vue | 386 ++++++++ .../cesiums/components/CoordinatePicker.ts | 377 +++++++ src/views/cesiums/components/DrawingTool.ts | 919 ++++++++++++++++++ src/views/cesiums/components/MarkerTool.ts | 93 ++ src/views/cesiums/components/ModelTool.ts | 415 ++++++++ src/views/cesiums/components/ViewTool.ts | 68 ++ src/views/cesiums/components/useCesium.ts | 90 ++ .../cesiums/components/useCesiumTools.ts | 73 ++ .../cesiums/components/useCoordinatePicker.ts | 107 ++ .../cesiums/components/useDrawingManager.ts | 302 ++++++ .../cesiums/components/useModelManager.ts | 184 ++++ 17 files changed, 4325 insertions(+) create mode 100644 src/views/cesiums/App.vue create mode 100644 src/views/cesiums/CesiumToolPanel.vue create mode 100644 src/views/cesiums/CesiumViewer.vue create mode 100644 src/views/cesiums/CoordinatePickerPanel.vue create mode 100644 src/views/cesiums/DrawingToolPanel.vue create mode 100644 src/views/cesiums/ModelControlPanel.vue create mode 100644 src/views/cesiums/components/CoordinatePicker.ts create mode 100644 src/views/cesiums/components/DrawingTool.ts create mode 100644 src/views/cesiums/components/MarkerTool.ts create mode 100644 src/views/cesiums/components/ModelTool.ts create mode 100644 src/views/cesiums/components/ViewTool.ts create mode 100644 src/views/cesiums/components/useCesium.ts create mode 100644 src/views/cesiums/components/useCesiumTools.ts create mode 100644 src/views/cesiums/components/useCoordinatePicker.ts create mode 100644 src/views/cesiums/components/useDrawingManager.ts create mode 100644 src/views/cesiums/components/useModelManager.ts diff --git a/auto-imports.d.ts b/auto-imports.d.ts index ba90363..f06c23a 100644 --- a/auto-imports.d.ts +++ b/auto-imports.d.ts @@ -2,6 +2,7 @@ export {} declare global { const EffectScope: typeof import('vue')['EffectScope'] + const ElMessage: typeof import('element-plus/es')['ElMessage'] const computed: typeof import('vue')['computed'] const createApp: typeof import('vue')['createApp'] const customRef: typeof import('vue')['customRef'] diff --git a/src/views/cesiums/App.vue b/src/views/cesiums/App.vue new file mode 100644 index 0000000..8323bec --- /dev/null +++ b/src/views/cesiums/App.vue @@ -0,0 +1,49 @@ + + + + + + \ No newline at end of file diff --git a/src/views/cesiums/CesiumToolPanel.vue b/src/views/cesiums/CesiumToolPanel.vue new file mode 100644 index 0000000..74c6223 --- /dev/null +++ b/src/views/cesiums/CesiumToolPanel.vue @@ -0,0 +1,168 @@ + + + + + + \ No newline at end of file diff --git a/src/views/cesiums/CesiumViewer.vue b/src/views/cesiums/CesiumViewer.vue new file mode 100644 index 0000000..2586cd7 --- /dev/null +++ b/src/views/cesiums/CesiumViewer.vue @@ -0,0 +1,249 @@ + + + + + + \ No newline at end of file diff --git a/src/views/cesiums/CoordinatePickerPanel.vue b/src/views/cesiums/CoordinatePickerPanel.vue new file mode 100644 index 0000000..dc2fe1c --- /dev/null +++ b/src/views/cesiums/CoordinatePickerPanel.vue @@ -0,0 +1,437 @@ + + + + + + \ No newline at end of file diff --git a/src/views/cesiums/DrawingToolPanel.vue b/src/views/cesiums/DrawingToolPanel.vue new file mode 100644 index 0000000..7ccbf0e --- /dev/null +++ b/src/views/cesiums/DrawingToolPanel.vue @@ -0,0 +1,407 @@ + + + + + + + \ No newline at end of file diff --git a/src/views/cesiums/ModelControlPanel.vue b/src/views/cesiums/ModelControlPanel.vue new file mode 100644 index 0000000..cb5a21b --- /dev/null +++ b/src/views/cesiums/ModelControlPanel.vue @@ -0,0 +1,386 @@ + + + + + + \ No newline at end of file diff --git a/src/views/cesiums/components/CoordinatePicker.ts b/src/views/cesiums/components/CoordinatePicker.ts new file mode 100644 index 0000000..8592241 --- /dev/null +++ b/src/views/cesiums/components/CoordinatePicker.ts @@ -0,0 +1,377 @@ +// src/utils/cesium/CoordinatePicker.ts +import * as Cesium from 'cesium'; + +export interface PickResult { + longitude: number; + latitude: number; + height: number; + cartesian: Cesium.Cartesian3; + cartographic: Cesium.Cartographic; + terrainHeight: number | undefined; + hasTerrain: boolean; +} + +export class CoordinatePicker { + private viewer: Cesium.Viewer; + private handler: Cesium.ScreenSpaceEventHandler; + private isEnabled: boolean = false; + private pickCallbacks: ((result: PickResult) => void)[] = []; + + constructor(viewer: Cesium.Viewer) { + this.viewer = viewer; + this.handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas); + } + + /** + * 启用坐标拾取 + */ + enable(): void { + if (this.isEnabled) return; + + this.handler.setInputAction((event: any) => { + this.handleRightClick(event); + }, Cesium.ScreenSpaceEventType.RIGHT_CLICK); + + // 添加鼠标移动时的预览效果 + this.handler.setInputAction((event: any) => { + this.handleMouseMove(event); + }, Cesium.ScreenSpaceEventType.MOUSE_MOVE); + + this.isEnabled = true; + this.updateCursorStyle(); + } + + /** + * 禁用坐标拾取 + */ + disable(): void { + if (!this.isEnabled) return; + + this.handler.removeInputAction(Cesium.ScreenSpaceEventType.RIGHT_CLICK); + this.handler.removeInputAction(Cesium.ScreenSpaceEventType.MOUSE_MOVE); + + this.isEnabled = false; + this.updateCursorStyle(); + this.clearPreview(); + } + + /** + * 切换拾取状态 + */ + toggle(): void { + if (this.isEnabled) { + this.disable(); + } else { + this.enable(); + } + } + + /** + * 添加拾取回调 + */ + onPick(callback: (result: PickResult) => void): void { + this.pickCallbacks.push(callback); + } + + /** + * 移除拾取回调 + */ + offPick(callback: (result: PickResult) => void): void { + const index = this.pickCallbacks.indexOf(callback); + if (index > -1) { + this.pickCallbacks.splice(index, 1); + } + } + + /** + * 处理右键点击 + */ + private async handleRightClick(event: any): Promise { + const pickResult = await this.pickCoordinate(event.position); + + if (pickResult) { + // 执行所有回调 + this.pickCallbacks.forEach(callback => { + callback(pickResult); + }); + + // 添加临时标记 + this.addTemporaryMarker(pickResult); + + // 显示坐标信息 + this.showCoordinateInfo(pickResult); + } + } + + /** + * 处理鼠标移动 + */ + private handleMouseMove(event: any): void { + this.updatePreview(event.endPosition); + } + + /** + * 拾取坐标 + */ + private async pickCoordinate(position: Cesium.Cartesian2): Promise { + try { + // 尝试从场景中拾取位置(包括地形) + const pickedObject = this.viewer.scene.pick(position); + + let cartesian: Cesium.Cartesian3; + let terrainHeight: number | undefined; + + if (pickedObject && pickedObject.position) { + // 如果拾取到了实体,使用实体的位置 + cartesian = pickedObject.position; + } else { + // 否则从相机射线与地形的交点获取位置 + const ray = this.viewer.camera.getPickRay(position); + if (!ray) return null; + + cartesian = this.viewer.scene.globe.pick(ray, this.viewer.scene); + if (!cartesian) { + // 如果没有地形,使用椭球体表面 + cartesian = this.viewer.scene.camera.pickEllipsoid(position, this.viewer.scene.globe.ellipsoid); + } + } + + if (!cartesian) return null; + + // 转换为地理坐标 + const cartographic = Cesium.Cartographic.fromCartesian(cartesian); + const longitude = Cesium.Math.toDegrees(cartographic.longitude); + const latitude = Cesium.Math.toDegrees(cartographic.latitude); + let height = cartographic.height; + + // 获取地形高度 + try { + terrainHeight = await this.getTerrainHeight(longitude, latitude); + if (terrainHeight !== undefined) { + // 调整高度为相对地形的高度 + height = height - terrainHeight; + } + } catch (error) { + console.warn('Failed to get terrain height:', error); + } + + return { + longitude, + latitude, + height, + cartesian, + cartographic, + terrainHeight, + hasTerrain: terrainHeight !== undefined + }; + } catch (error) { + console.error('Error picking coordinate:', error); + return null; + } + } + + /** + * 获取地形高度 + */ + private async getTerrainHeight(longitude: number, latitude: number): Promise { + return new Promise((resolve) => { + const cartographic = new Cesium.Cartographic( + Cesium.Math.toRadians(longitude), + Cesium.Math.toRadians(latitude) + ); + + try { + const terrainProvider = this.viewer.terrainProvider; + if (terrainProvider instanceof Cesium.EllipsoidTerrainProvider) { + resolve(undefined); // 没有地形数据 + return; + } + + terrainProvider.getHeight(cartographic).then((height: number) => { + resolve(height); + }).catch(() => { + resolve(undefined); + }); + } catch (error) { + resolve(undefined); + } + }); + } + + /** + * 添加临时标记 + */ + private addTemporaryMarker(result: PickResult): void { + const entity = this.viewer.entities.add({ + position: result.cartesian, + point: { + pixelSize: 10, + color: Cesium.Color.YELLOW, + outlineColor: Cesium.Color.BLACK, + outlineWidth: 2, + heightReference: Cesium.HeightReference.CLAMP_TO_GROUND + }, + label: { + text: `经度: ${result.longitude.toFixed(6)}\n纬度: ${result.latitude.toFixed(6)}\n高程: ${result.height.toFixed(2)}米`, + font: '12pt sans-serif', + pixelOffset: new Cesium.Cartesian2(0, -40), + fillColor: Cesium.Color.WHITE, + backgroundColor: Cesium.Color.BLACK.withAlpha(0.7), + outlineColor: Cesium.Color.BLACK, + outlineWidth: 2, + style: Cesium.LabelStyle.FILL_AND_OUTLINE, + heightReference: Cesium.HeightReference.CLAMP_TO_GROUND + } + }); + + // 5秒后自动移除标记 + setTimeout(() => { + this.viewer.entities.remove(entity); + }, 5000); + } + + /** + * 显示坐标信息 + */ + private showCoordinateInfo(result: PickResult): void { + const terrainInfo = result.hasTerrain ? + `地形高程: ${result.terrainHeight?.toFixed(2)}米` : + '无地形数据'; + + const message = + `📍 坐标拾取成功!\n\n` + + `经度: ${result.longitude.toFixed(6)}\n` + + `纬度: ${result.latitude.toFixed(6)}\n` + + `相对高程: ${result.height.toFixed(2)}米\n` + + `${terrainInfo}`; + + console.log('Coordinate picked:', result); + + // 可以在这里集成消息提示组件 + this.showNotification(message); + } + + /** + * 显示通知 + */ + private showNotification(message: string): void { + // 创建临时通知元素 + const notification = document.createElement('div'); + notification.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + background: rgba(0, 0, 0, 0.8); + color: white; + padding: 15px; + border-radius: 8px; + border-left: 4px solid #52c41a; + max-width: 300px; + z-index: 1000; + font-family: sans-serif; + font-size: 14px; + white-space: pre-line; + `; + notification.textContent = message; + + document.body.appendChild(notification); + + // 3秒后自动移除 + setTimeout(() => { + if (notification.parentNode) { + notification.parentNode.removeChild(notification); + } + }, 30000); + } + + /** + * 更新预览效果 + */ + private updatePreview(position: Cesium.Cartesian2): void { + this.clearPreview(); + + // 添加预览点 + const ray = this.viewer.camera.getPickRay(position); + if (!ray) return; + + const cartesian = this.viewer.scene.globe.pick(ray, this.viewer.scene) || + this.viewer.scene.camera.pickEllipsoid(position, this.viewer.scene.globe.ellipsoid); + + if (cartesian) { + const previewEntity = this.viewer.entities.add({ + position: cartesian, + point: { + pixelSize: 8, + color: Cesium.Color.CYAN.withAlpha(0.7), + outlineColor: Cesium.Color.BLUE, + outlineWidth: 1, + heightReference: Cesium.HeightReference.CLAMP_TO_GROUND + } + }); + + // 存储预览实体引用以便清除 + (this as any).previewEntity = previewEntity; + } + } + + /** + * 清除预览效果 + */ + private clearPreview(): void { + const previewEntity = (this as any).previewEntity; + if (previewEntity) { + this.viewer.entities.remove(previewEntity); + (this as any).previewEntity = null; + } + } + + /** + * 更新光标样式 + */ + private updateCursorStyle(): void { + const canvas = this.viewer.scene.canvas; + if (this.isEnabled) { + canvas.style.cursor = 'crosshair'; + } else { + canvas.style.cursor = ''; + } + } + + /** + * 手动拾取指定屏幕坐标 + */ + async pickAtPosition(x: number, y: number): Promise { + const position = new Cesium.Cartesian2(x, y); + return this.pickCoordinate(position); + } + + /** + * 手动拾取指定地理坐标 + */ + async pickAtCoordinate(longitude: number, latitude: number): Promise { + const cartesian = Cesium.Cartesian3.fromDegrees(longitude, latitude); + const cartographic = Cesium.Cartographic.fromCartesian(cartesian); + + const terrainHeight = await this.getTerrainHeight(longitude, latitude); + const height = cartographic.height; + + return { + longitude, + latitude, + height: terrainHeight !== undefined ? height - terrainHeight : height, + cartesian, + cartographic, + terrainHeight, + hasTerrain: terrainHeight !== undefined + }; + } + + /** + * 销毁 + */ + destroy(): void { + this.disable(); + this.handler.destroy(); + this.pickCallbacks = []; + } +} \ No newline at end of file diff --git a/src/views/cesiums/components/DrawingTool.ts b/src/views/cesiums/components/DrawingTool.ts new file mode 100644 index 0000000..9f79dc8 --- /dev/null +++ b/src/views/cesiums/components/DrawingTool.ts @@ -0,0 +1,919 @@ +// src/utils/cesium/DrawingTool.ts +import * as Cesium from 'cesium'; + +export interface DrawingOptions { + color?: Cesium.Color; + outlineColor?: Cesium.Color; + outlineWidth?: number; + fill?: boolean; + classificationType?: Cesium.ClassificationType; + height?: number; + extrudedHeight?: number; +} + +export interface CircleOptions extends DrawingOptions { + radius?: number; +} + +export interface PolygonOptions extends DrawingOptions { + closePath?: boolean; +} + + + +export interface DrawingInfo { + id: string; + type: 'circle' | 'polygon'; + positions: Cesium.Cartesian3[]; + center?: Cesium.Cartesian3; // 圆心(仅圆形) + radius?: number; // 半径(仅圆形) + bounds?: { // 边界信息(仅多边形) + north: number; + south: number; + east: number; + west: number; + }; + area: number; + properties: any; +} + +export interface DrawingResult { + id: string; + type: 'circle' | 'polygon'; + positions: Cesium.Cartesian3[]; + entity: Cesium.Entity; + properties: any; + info: DrawingInfo; // 新增信息字段 +} + +export class DrawingTool { + private viewer: Cesium.Viewer; + private handler: Cesium.ScreenSpaceEventHandler; + private entities: Cesium.EntityCollection; + + private isDrawing: boolean = false; + private currentType: 'circle' | 'polygon' | null = null; + private currentPositions: Cesium.Cartesian3[] = []; + private tempEntities: Cesium.Entity[] = []; + private drawingEntities: Map = new Map(); + + private defaultOptions: DrawingOptions = { + color: Cesium.Color.YELLOW.withAlpha(0.3), + outlineColor: Cesium.Color.YELLOW, + outlineWidth: 2, + fill: true, + classificationType: Cesium.ClassificationType.BOTH, + height: 0, + extrudedHeight: 1000 + }; + + private drawingCallbacks: { + onStart?: () => void; + onPointAdd?: (position: Cesium.Cartesian3) => void; + onComplete?: (result: DrawingResult) => void; + onCancel?: () => void; + onClick?: (result: DrawingResult, info: DrawingInfo) => void; // 新增点击回调 + } = {}; + + constructor(viewer: Cesium.Viewer) { + this.viewer = viewer; + this.handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas); + this.entities = viewer.entities; + + // 初始化点击事件监听 + this.initClickHandler(); + } + + /** + * 初始化点击事件处理器 + */ + private initClickHandler(): void { + this.handler.setInputAction((event: any) => { + this.handleEntityClick(event.position); + }, Cesium.ScreenSpaceEventType.LEFT_CLICK); + } + + /** + * 处理实体点击事件 + */ + private handleEntityClick(position: Cesium.Cartesian2): void { + const pickedObject = this.viewer.scene.pick(position); + if (!pickedObject || !pickedObject.id) return; + + const clickedEntity = pickedObject.id; + const drawingResult = this.findDrawingByEntity(clickedEntity); + + if (drawingResult) { + const drawingInfo = this.generateDrawingInfo(drawingResult); + this.drawingCallbacks.onClick?.(drawingResult, drawingInfo); + } + } + + /** + * 根据实体查找绘图结果 + */ + private findDrawingByEntity(entity: Cesium.Entity): DrawingResult | undefined { + for (const drawing of this.drawingEntities.values()) { + if (drawing.entity === entity) { + return drawing; + } + } + return undefined; + } + + /** + * 生成绘图信息 + */ + private generateDrawingInfo(drawing: DrawingResult): DrawingInfo { + if (drawing.type === 'circle') { + return this.generateCircleInfo(drawing); + } else { + return this.generatePolygonInfo(drawing); + } + } + + /** + * 生成圆形信息 + */ + private generateCircleInfo(drawing: DrawingResult): DrawingInfo { + const center = drawing.positions[0]; + const radius = drawing.positions.length > 1 ? + Cesium.Cartesian3.distance(center, drawing.positions[1]) : + (drawing.properties.options.radius || 1000); + + const centerCartographic = Cesium.Cartographic.fromCartesian(center); + const centerLongitude = Cesium.Math.toDegrees(centerCartographic.longitude); + const centerLatitude = Cesium.Math.toDegrees(centerCartographic.latitude); + const centerHeight = centerCartographic.height; + + return { + id: drawing.id, + type: 'circle', + positions: drawing.positions, + center: center, + radius: radius, + area: drawing.properties.area, + properties: { + ...drawing.properties, + center: { + longitude: centerLongitude, + latitude: centerLatitude, + height: centerHeight + }, + radius: radius, + circumference: 2 * Math.PI * radius, + diameter: 2 * radius + } + }; + } + + /** + * 生成多边形信息 + */ + private generatePolygonInfo(drawing: DrawingResult): DrawingInfo { + // 计算边界 + let minLon = Infinity, maxLon = -Infinity; + let minLat = Infinity, maxLat = -Infinity; + let minHeight = Infinity, maxHeight = -Infinity; + + const boundaryPoints = drawing.positions.map(position => { + const cartographic = Cesium.Cartographic.fromCartesian(position); + const lon = Cesium.Math.toDegrees(cartographic.longitude); + const lat = Cesium.Math.toDegrees(cartographic.latitude); + const height = cartographic.height; + + // 更新边界 + minLon = Math.min(minLon, lon); + maxLon = Math.max(maxLon, lon); + minLat = Math.min(minLat, lat); + maxLat = Math.max(maxLat, lat); + minHeight = Math.min(minHeight, height); + maxHeight = Math.max(maxHeight, height); + + return { + longitude: lon, + latitude: lat, + height: height + }; + }); + + // 计算中心点 + const centerLon = (minLon + maxLon) / 2; + const centerLat = (minLat + maxLat) / 2; + const centerHeight = (minHeight + maxHeight) / 2; + + return { + id: drawing.id, + type: 'polygon', + positions: drawing.positions, + bounds: { + north: maxLat, + south: minLat, + east: maxLon, + west: minLon + }, + area: drawing.properties.area, + properties: { + ...drawing.properties, + boundaryPoints: boundaryPoints, + bounds: { + north: maxLat, + south: minLat, + east: maxLon, + west: minLon + }, + center: { + longitude: centerLon, + latitude: centerLat, + height: centerHeight + }, + width: this.calculateDistance(minLon, minLat, maxLon, minLat), // 东西宽度 + height: this.calculateDistance(minLon, minLat, minLon, maxLat), // 南北高度 + perimeter: this.calculatePerimeter(drawing.positions) + } + }; + } + + /** + * 计算两点间距离 + */ + private calculateDistance(lon1: number, lat1: number, lon2: number, lat2: number): number { + const cartesian1 = Cesium.Cartesian3.fromDegrees(lon1, lat1); + const cartesian2 = Cesium.Cartesian3.fromDegrees(lon2, lat2); + return Cesium.Cartesian3.distance(cartesian1, cartesian2); + } + + /** + * 计算多边形周长 + */ + private calculatePerimeter(positions: Cesium.Cartesian3[]): number { + let perimeter = 0; + const n = positions.length; + + for (let i = 0; i < n; i++) { + const j = (i + 1) % n; + perimeter += Cesium.Cartesian3.distance(positions[i], positions[j]); + } + + return perimeter; + } + + + + + + /** + * 开始绘制圆形 + */ + startDrawingCircle(options: CircleOptions = {}): void { + this.startDrawing('circle', options); + } + + /** + * 开始绘制多边形 + */ + startDrawingPolygon(options: PolygonOptions = {}): void { + this.startDrawing('polygon', options); + } + + /** + * 开始绘制 + */ + private startDrawing(type: 'circle' | 'polygon', options: DrawingOptions = {}): void { + if (this.isDrawing) { + this.cancelDrawing(); + } + + this.isDrawing = true; + this.currentType = type; + this.currentPositions = []; + this.tempEntities = []; + + const mergedOptions = { ...this.defaultOptions, ...options }; + + // 设置鼠标样式 + this.viewer.scene.canvas.style.cursor = 'crosshair'; + + // 左键点击添加点 + this.handler.setInputAction((event: any) => { + this.handleLeftClick(event.position, mergedOptions); + }, Cesium.ScreenSpaceEventType.LEFT_CLICK); + + // 鼠标移动预览 + this.handler.setInputAction((event: any) => { + this.handleMouseMove(event.endPosition, mergedOptions); + }, Cesium.ScreenSpaceEventType.MOUSE_MOVE); + + // 右键完成绘制 + this.handler.setInputAction(() => { + this.completeDrawing(mergedOptions); + }, Cesium.ScreenSpaceEventType.RIGHT_CLICK); + + // 取消绘制 + const cancelHandler = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + this.cancelDrawing(); + } + }; + document.addEventListener('keydown', cancelHandler); + (this as any).cancelHandler = cancelHandler; + + this.drawingCallbacks.onStart?.(); + } + + /** + * 处理左键点击 + */ + private handleLeftClick(position: Cesium.Cartesian2, options: DrawingOptions): void { + const cartesian = this.pickCoordinate(position); + if (!cartesian) return; + + this.currentPositions.push(cartesian); + + // 添加临时点标记 + const pointEntity = this.entities.add({ + position: cartesian, + point: { + pixelSize: 8, + color: Cesium.Color.RED, + outlineColor: Cesium.Color.WHITE, + outlineWidth: 2, + heightReference: Cesium.HeightReference.CLAMP_TO_GROUND + } + }); + this.tempEntities.push(pointEntity); + + this.drawingCallbacks.onPointAdd?.(cartesian); + + // 如果是圆形,第一个点确定圆心,第二个点确定半径 + if (this.currentType === 'circle' && this.currentPositions.length === 2) { + this.completeDrawing(options); + } + } + + /** + * 处理鼠标移动 + */ + private handleMouseMove(position: Cesium.Cartesian2, options: DrawingOptions): void { + if (this.currentPositions.length === 0) return; + + const cartesian = this.pickCoordinate(position); + if (!cartesian) return; + + // 清除之前的临时图形 + this.clearTempShapes(); + + if (this.currentType === 'circle') { + this.drawTempCircle(cartesian, options); + } else if (this.currentType === 'polygon') { + this.drawTempPolygon(cartesian, options); + } + } + + /** + * 绘制临时圆形 + */ + private drawTempCircle(mousePosition: Cesium.Cartesian3, options: CircleOptions): void { + const center = this.currentPositions[0]; + const radius = Cesium.Cartesian3.distance(center, mousePosition); + + const circleEntity = this.entities.add({ + position: center, + ellipse: { + semiMinorAxis: radius, + semiMajorAxis: radius, + material: options.color || this.defaultOptions.color, + outline: true, + outlineColor: options.outlineColor || this.defaultOptions.outlineColor, + outlineWidth: options.outlineWidth || this.defaultOptions.outlineWidth, + height: options.height || this.defaultOptions.height, + extrudedHeight: options.extrudedHeight || this.defaultOptions.extrudedHeight, + classificationType: options.classificationType || this.defaultOptions.classificationType + } + }); + this.tempEntities.push(circleEntity); + + // 绘制半径线 + if (this.currentPositions.length === 1) { + const radiusLine = this.entities.add({ + polyline: { + positions: [center, mousePosition], + width: 2, + material: Cesium.Color.WHITE.withAlpha(0.7) + } + }); + this.tempEntities.push(radiusLine); + } + } + + /** + * 绘制临时多边形 + */ + private drawTempPolygon(mousePosition: Cesium.Cartesian3, options: PolygonOptions): void { + const positions = [...this.currentPositions, mousePosition]; + + // 绘制临时多边形 + const polygonEntity = this.entities.add({ + polygon: { + hierarchy: new Cesium.PolygonHierarchy(positions), + material: options.color || this.defaultOptions.color, + outline: true, + outlineColor: options.outlineColor || this.defaultOptions.outlineColor, + outlineWidth: options.outlineWidth || this.defaultOptions.outlineWidth, + height: options.height || this.defaultOptions.height, + extrudedHeight: options.extrudedHeight || this.defaultOptions.extrudedHeight, + classificationType: options.classificationType || this.defaultOptions.classificationType + } + }); + this.tempEntities.push(polygonEntity); + + // 绘制临时边线 + if (positions.length > 1) { + const linePositions = [...positions]; + if (positions.length > 2) { + linePositions.push(positions[0]); // 闭合多边形 + } + + const lineEntity = this.entities.add({ + polyline: { + positions: linePositions, + width: 3, + material: options.outlineColor || this.defaultOptions.outlineColor + } + }); + this.tempEntities.push(lineEntity); + } + } + + /** + * 完成绘制 + */ + // private completeDrawing(options: DrawingOptions): void { + // if (this.currentPositions.length < 2) { + // this.cancelDrawing(); + // return; + // } + + // const id = `drawing_${Date.now()}`; + // let entity: Cesium.Entity; + + // if (this.currentType === 'circle') { + // entity = this.createCircleEntity(id, this.currentPositions, options as CircleOptions); + // } else { + // entity = this.createPolygonEntity(id, this.currentPositions, options as PolygonOptions); + // } + + // const result: DrawingResult = { + // id, + // type: this.currentType!, + // positions: this.currentPositions, + // entity, + // properties: { + // area: this.calculateArea(this.currentPositions, this.currentType!), + // options + // } + // }; + + // this.drawingEntities.set(id, result); + // this.cleanupDrawing(); + + // this.drawingCallbacks.onComplete?.(result); + // } + + /** + * 完成绘制 + */ + private completeDrawing(options: DrawingOptions): void { + if (this.currentPositions.length < 2) { + this.cancelDrawing(); + return; + } + + const id = `drawing_${Date.now()}`; + let entity: Cesium.Entity; + + if (this.currentType === 'circle') { + entity = this.createCircleEntity(id, this.currentPositions, options as CircleOptions); + } else { + entity = this.createPolygonEntity(id, this.currentPositions, options as PolygonOptions); + } + + // 生成绘图信息 + const drawingInfo = this.currentType === 'circle' ? + this.generateCircleInfo({ id, type: 'circle', positions: this.currentPositions, entity, properties: { options, area: 0 } } as DrawingResult) : + this.generatePolygonInfo({ id, type: 'polygon', positions: this.currentPositions, entity, properties: { options, area: 0 } } as DrawingResult); + + // 计算面积 + const area = this.calculateArea(this.currentPositions, this.currentType!); + + const result: DrawingResult = { + id, + type: this.currentType!, + positions: this.currentPositions, + entity, + properties: { + area: area, + options + }, + info: { + ...drawingInfo, + area: area + } + }; + + this.drawingEntities.set(id, result); + this.cleanupDrawing(); + + this.drawingCallbacks.onComplete?.(result); + } + + /** + * 创建圆形实体 + */ + // private createCircleEntity(id: string, positions: Cesium.Cartesian3[], options: CircleOptions): Cesium.Entity { + // const center = positions[0]; + // const radius = positions.length > 1 ? Cesium.Cartesian3.distance(center, positions[1]) : (options.radius || 1000); + + // return this.entities.add({ + // id, + // position: center, + // ellipse: { + // semiMinorAxis: radius, + // semiMajorAxis: radius, + // material: options.color || this.defaultOptions.color, + // outline: true, + // outlineColor: options.outlineColor || this.defaultOptions.outlineColor, + // outlineWidth: options.outlineWidth || this.defaultOptions.outlineWidth, + // height: options.height || this.defaultOptions.height, + // extrudedHeight: options.extrudedHeight || this.defaultOptions.extrudedHeight, + // classificationType: options.classificationType || this.defaultOptions.classificationType + // }, + // label: { + // text: `圆形空域\n半径: ${(radius / 1000).toFixed(2)}km`, + // font: '14pt sans-serif', + // pixelOffset: new Cesium.Cartesian2(0, -50), + // fillColor: Cesium.Color.WHITE, + // outlineColor: Cesium.Color.BLACK, + // outlineWidth: 2, + // style: Cesium.LabelStyle.FILL_AND_OUTLINE, + // heightReference: Cesium.HeightReference.CLAMP_TO_GROUND + // } + // }); + // } + + /** + * 创建圆形实体 + */ + private createCircleEntity(id: string, positions: Cesium.Cartesian3[], options: CircleOptions): Cesium.Entity { + const center = positions[0]; + const radius = positions.length > 1 ? Cesium.Cartesian3.distance(center, positions[1]) : (options.radius || 1000); + const centerCartographic = Cesium.Cartographic.fromCartesian(center); + + return this.entities.add({ + id, + position: center, + ellipse: { + semiMinorAxis: radius, + semiMajorAxis: radius, + material: options.color || this.defaultOptions.color, + outline: true, + outlineColor: options.outlineColor || this.defaultOptions.outlineColor, + outlineWidth: options.outlineWidth || this.defaultOptions.outlineWidth, + height: options.height || this.defaultOptions.height, + extrudedHeight: options.extrudedHeight || this.defaultOptions.extrudedHeight, + classificationType: options.classificationType || this.defaultOptions.classificationType + }, + label: { + text: `圆形空域\n半径: ${(radius / 1000).toFixed(2)}km`, + font: '14pt sans-serif', + pixelOffset: new Cesium.Cartesian2(0, -50), + fillColor: Cesium.Color.WHITE, + outlineColor: Cesium.Color.BLACK, + outlineWidth: 2, + style: Cesium.LabelStyle.FILL_AND_OUTLINE, + heightReference: Cesium.HeightReference.CLAMP_TO_GROUND + }, + // 添加自定义属性便于识别 + drawingType: 'circle', + drawingId: id + }); + console.log("圆形空域:",) + } + + /** + * 创建多边形实体 + */ + private createPolygonEntity(id: string, positions: Cesium.Cartesian3[], options: PolygonOptions): Cesium.Entity { + const hierarchy = new Cesium.PolygonHierarchy(positions); + const area = this.calculateArea(positions, 'polygon'); + + return this.entities.add({ + id, + polygon: { + hierarchy, + material: options.color || this.defaultOptions.color, + outline: true, + outlineColor: options.outlineColor || this.defaultOptions.outlineColor, + outlineWidth: options.outlineWidth || this.defaultOptions.outlineWidth, + height: options.height || this.defaultOptions.height, + extrudedHeight: options.extrudedHeight || this.defaultOptions.extrudedHeight, + classificationType: options.classificationType || this.defaultOptions.classificationType + }, + label: { + text: `多边形空域\n面积: ${(area / 1000000).toFixed(2)}km²`, + font: '14pt sans-serif', + pixelOffset: new Cesium.Cartesian2(0, -50), + fillColor: Cesium.Color.WHITE, + outlineColor: Cesium.Color.BLACK, + outlineWidth: 2, + style: Cesium.LabelStyle.FILL_AND_OUTLINE, + heightReference: Cesium.HeightReference.CLAMP_TO_GROUND + }, + // 添加自定义属性便于识别 + drawingType: 'polygon', + drawingId: id + }); + } + + /** + * 创建多边形实体 + */ + // private createPolygonEntity(id: string, positions: Cesium.Cartesian3[], options: PolygonOptions): Cesium.Entity { + // const hierarchy = new Cesium.PolygonHierarchy(positions); + // const area = this.calculateArea(positions, 'polygon'); + + // return this.entities.add({ + // id, + // polygon: { + // hierarchy, + // material: options.color || this.defaultOptions.color, + // outline: true, + // outlineColor: options.outlineColor || this.defaultOptions.outlineColor, + // outlineWidth: options.outlineWidth || this.defaultOptions.outlineWidth, + // height: options.height || this.defaultOptions.height, + // extrudedHeight: options.extrudedHeight || this.defaultOptions.extrudedHeight, + // classificationType: options.classificationType || this.defaultOptions.classificationType + // }, + // label: { + // text: `多边形空域\n面积: ${(area / 1000000).toFixed(2)}km²`, + // font: '14pt sans-serif', + // pixelOffset: new Cesium.Cartesian2(0, -50), + // fillColor: Cesium.Color.WHITE, + // outlineColor: Cesium.Color.BLACK, + // outlineWidth: 2, + // style: Cesium.LabelStyle.FILL_AND_OUTLINE, + // heightReference: Cesium.HeightReference.CLAMP_TO_GROUND + // } + // }); + // } + + /** + * 计算面积 + */ + private calculateArea(positions: Cesium.Cartesian3[], type: 'circle' | 'polygon'): number { + if (type === 'circle') { + const center = positions[0]; + console.log("圆形的圆心:",center) + const radius = positions.length > 1 ? Cesium.Cartesian3.distance(center, positions[1]) : 0; + console.log("圆形的半径:",radius) + return Math.PI * radius * radius; + } else { + // 计算多边形面积 + // const cartographics = positions.map(pos => + // Cesium.Cartographic.fromCartesian(pos) + // ); + // return Cesium.PolygonGeometry.computeArea(cartographics); + + // 修正:使用正确的多边形面积计算方法 + const area = this.computePolygonAreaSimple(positions); + console.log(`Polygon area: ${area} m², vertices: ${positions.length}`); + return area; + } + } + + /** + * 简化但有效的多边形面积计算 + */ + private computePolygonAreaSimple(positions: Cesium.Cartesian3[]): number { + if (positions.length < 3) return 0; + + // 将3D坐标转换为2D平面坐标(使用第一个点作为参考平面) + const normal = new Cesium.Cartesian3(); + Cesium.Ellipsoid.WGS84.geodeticSurfaceNormal(positions[0], normal); + + let area = 0; + const n = positions.length; + + for (let i = 0; i < n; i++) { + const j = (i + 1) % n; + + const p1 = positions[i]; + const p2 = positions[j]; + + // 计算两个向量在切平面上的投影 + const v1 = Cesium.Cartesian3.subtract(p1, positions[0], new Cesium.Cartesian3()); + const v2 = Cesium.Cartesian3.subtract(p2, positions[0], new Cesium.Cartesian3()); + + // 计算叉积的模长(平行四边形面积) + const cross = Cesium.Cartesian3.cross(v1, v2, new Cesium.Cartesian3()); + const parallelogramArea = Cesium.Cartesian3.magnitude(cross); + + area += parallelogramArea; + } + + // 三角形面积是平行四边形面积的一半 + return Math.abs(area) / 2; + } + + + /** + * 取消绘制 + */ + cancelDrawing(): void { + this.cleanupDrawing(); + this.drawingCallbacks.onCancel?.(); + } + + /** + * 清理绘制状态 + */ + private cleanupDrawing(): void { + this.isDrawing = false; + this.currentType = null; + this.currentPositions = []; + + // 清除临时实体 + this.tempEntities.forEach(entity => this.entities.remove(entity)); + this.tempEntities = []; + + // 移除事件监听 + this.handler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_CLICK); + this.handler.removeInputAction(Cesium.ScreenSpaceEventType.MOUSE_MOVE); + this.handler.removeInputAction(Cesium.ScreenSpaceEventType.RIGHT_CLICK); + + // 移除键盘事件 + if ((this as any).cancelHandler) { + document.removeEventListener('keydown', (this as any).cancelHandler); + (this as any).cancelHandler = null; + } + + // 恢复鼠标样式 + this.viewer.scene.canvas.style.cursor = ''; + } + + /** + * 清除临时图形 + */ + private clearTempShapes(): void { + this.tempEntities.forEach(entity => this.entities.remove(entity)); + this.tempEntities = []; + } + + /** + * 拾取坐标 + */ + private pickCoordinate(position: Cesium.Cartesian2): Cesium.Cartesian3 | null { + const ray = this.viewer.camera.getPickRay(position); + if (!ray) return null; + + const cartesian = this.viewer.scene.globe.pick(ray, this.viewer.scene) || + this.viewer.scene.camera.pickEllipsoid(position, this.viewer.scene.globe.ellipsoid); + return cartesian; + } + + /** + * 设置绘图回调 + */ + setCallbacks(callbacks: typeof this.drawingCallbacks): void { + this.drawingCallbacks = { ...this.drawingCallbacks, ...callbacks }; + } + + /** + * 获取所有绘制的图形 + */ + getAllDrawings(): DrawingResult[] { + return Array.from(this.drawingEntities.values()); + } + + /** + * 根据ID获取图形 + */ + getDrawing(id: string): DrawingResult | undefined { + return this.drawingEntities.get(id); + } + + /** + * 移除图形 + */ + removeDrawing(id: string): boolean { + const drawing = this.drawingEntities.get(id); + if (drawing) { + this.entities.remove(drawing.entity); + this.drawingEntities.delete(id); + return true; + } + return false; + } + + /** + * 清除所有图形 + */ + clearAllDrawings(): void { + this.drawingEntities.forEach(drawing => { + this.entities.remove(drawing.entity); + }); + this.drawingEntities.clear(); + } + + /** + * 高亮显示图形 + */ + highlightDrawing(id: string, highlight: boolean = true): void { + const drawing = this.drawingEntities.get(id); + if (!drawing) return; + + if (drawing.type === 'circle' && drawing.entity.ellipse) { + drawing.entity.ellipse.outlineColor = highlight ? + Cesium.Color.RED : + (drawing.properties.options.outlineColor || this.defaultOptions.outlineColor); + drawing.entity.ellipse.outlineWidth = highlight ? 4 : 2; + } else if (drawing.entity.polygon) { + drawing.entity.polygon.outlineColor = highlight ? + Cesium.Color.RED : + (drawing.properties.options.outlineColor || this.defaultOptions.outlineColor); + drawing.entity.polygon.outlineWidth = highlight ? 4 : 2; + } + } + + /** + * 飞向图形 + */ + flyToDrawing(id: string, duration: number = 2): void { + console.log("飞飞飞非法欸") + const drawing = this.drawingEntities.get(id); + console.log("飞飞飞非111") + if (drawing) { + console.log("飞飞飞222") + this.viewer.flyTo(drawing.entity, { + duration: duration, + offset: new Cesium.HeadingPitchRange(0, -0.5, 0) + }); + } + } + + /** + * 导出图形数据 + */ + exportDrawings(): any[] { + return this.getAllDrawings().map(drawing => ({ + id: drawing.id, + type: drawing.type, + positions: drawing.positions.map(pos => { + const cartographic = Cesium.Cartographic.fromCartesian(pos); + return { + longitude: Cesium.Math.toDegrees(cartographic.longitude), + latitude: Cesium.Math.toDegrees(cartographic.latitude), + height: cartographic.height + }; + }), + properties: drawing.properties + })); + } + + /** + * 导入图形数据 + */ + importDrawings(data: any[]): void { + data.forEach(item => { + const positions = item.positions.map((pos: any) => + Cesium.Cartesian3.fromDegrees(pos.longitude, pos.latitude, pos.height) + ); + + let entity: Cesium.Entity; + if (item.type === 'circle') { + entity = this.createCircleEntity(item.id, positions, item.properties.options); + } else { + entity = this.createPolygonEntity(item.id, positions, item.properties.options); + } + + const drawing: DrawingResult = { + id: item.id, + type: item.type, + positions, + entity, + properties: item.properties + }; + + this.drawingEntities.set(item.id, drawing); + }); + } + + /** + * 销毁 + */ + destroy(): void { + this.cancelDrawing(); + this.clearAllDrawings(); + this.handler.destroy(); + } +} \ No newline at end of file diff --git a/src/views/cesiums/components/MarkerTool.ts b/src/views/cesiums/components/MarkerTool.ts new file mode 100644 index 0000000..c246a2c --- /dev/null +++ b/src/views/cesiums/components/MarkerTool.ts @@ -0,0 +1,93 @@ +// src/utils/cesium/MarkerTool.ts +import * as Cesium from 'cesium'; + +export interface Landmark { + name: string; + lon: number; + lat: number; + desc: string; +} + +export class MarkerTool { + private viewer: Cesium.Viewer; + private entities: Cesium.EntityCollection; + private markers: Map; + + constructor(viewer: Cesium.Viewer) { + this.viewer = viewer; + this.entities = viewer.entities; + this.markers = new Map(); + } + + // 添加点标注 + addPoint( + longitude: number, + latitude: number, + name: string, + description: string = '', + options: any = {} + ): Cesium.Entity { + const defaultOptions = { + position: Cesium.Cartesian3.fromDegrees(longitude, latitude), + point: { + pixelSize: 10, + color: Cesium.Color.YELLOW, + outlineColor: Cesium.Color.BLACK, + outlineWidth: 2, + heightReference: Cesium.HeightReference.CLAMP_TO_GROUND + }, + label: { + text: name, + font: '14pt sans-serif', + pixelOffset: new Cesium.Cartesian2(0, -40), + fillColor: Cesium.Color.WHITE, + outlineColor: Cesium.Color.BLACK, + outlineWidth: 2, + style: Cesium.LabelStyle.FILL_AND_OUTLINE + } + }; + + const mergedOptions = { ...defaultOptions, ...options }; + if (description) { + mergedOptions.description = description; + } + + const entity = this.entities.add(mergedOptions); + this.markers.set(name, entity); + + return entity; + } + + // 添加重庆地标 + addChongqingLandmark(): void { + const landmarks: Landmark[] = [ + { name: '解放碑', lon: 106.574, lat: 29.556, desc: '重庆解放碑' }, + { name: '洪崖洞', lon: 106.577, lat: 29.561, desc: '洪崖洞民俗风貌区' }, + { name: '朝天门', lon: 106.583, lat: 29.565, desc: '朝天门广场' }, + { name: '南山', lon: 106.600, lat: 29.553, desc: '南山一棵树观景台' } + ]; + + landmarks.forEach(landmark => { + this.addPoint(landmark.lon, landmark.lat, landmark.name, landmark.desc); + }); + } + + // 移除标注 + removeMarker(name: string): void { + if (this.markers.has(name)) { + const entity = this.markers.get(name); + if (entity) { + this.entities.remove(entity); + } + this.markers.delete(name); + } + } + + // 清除所有标注 + clearAllMarkers(): void { + this.markers.forEach((entity) => { + this.entities.remove(entity); + }); + this.markers.clear(); + } +} \ No newline at end of file diff --git a/src/views/cesiums/components/ModelTool.ts b/src/views/cesiums/components/ModelTool.ts new file mode 100644 index 0000000..9f64abc --- /dev/null +++ b/src/views/cesiums/components/ModelTool.ts @@ -0,0 +1,415 @@ +// src/utils/cesium/ModelTool.ts +import * as Cesium from 'cesium'; + +export interface ModelOptions { + position: Cesium.Cartesian3; + orientation?: Cesium.Quaternion; + scale?: number; + minimumPixelSize?: number; + maximumScale?: number; + heightReference?: Cesium.HeightReference; + color?: Cesium.Color; + colorBlendMode?: Cesium.ColorBlendMode; + colorBlendAmount?: number; + silhouetteColor?: Cesium.Color; + silhouetteSize?: number; + debugShowBoundingVolume?: boolean; + debugWireframe?: boolean; +} + +export interface ModelAnimation { + name: string; + speed?: number; + loop?: boolean; + startTime?: Cesium.JulianDate; + stopTime?: Cesium.JulianDate; +} + +export class ModelTool { + private viewer: Cesium.Viewer; + private entities: Cesium.EntityCollection; + private models: Map; + private modelCache: Map; + + constructor(viewer: Cesium.Viewer) { + this.viewer = viewer; + this.entities = viewer.entities; + this.models = new Map(); + this.modelCache = new Map(); + } + + /** + * 加载 3D 模型 + */ + async loadModel( + id: string, + url: string, + position: { longitude: number; latitude: number; height: number }, + options: Partial = {} + ): Promise { + try { + const defaultOptions: Partial = { + scale: 1.0, + minimumPixelSize: 128, + maximumScale: 1000, + heightReference: Cesium.HeightReference.NONE, + color: Cesium.Color.WHITE, + colorBlendMode: Cesium.ColorBlendMode.HIGHLIGHT, + colorBlendAmount: 0.5, + debugShowBoundingVolume: false, + debugWireframe: false + }; + + const mergedOptions = { ...defaultOptions, ...options }; + + const entity = this.entities.add({ + id: id, + position: Cesium.Cartesian3.fromDegrees( + position.longitude, + position.latitude, + position.height + ), + orientation: mergedOptions.orientation, + model: { + uri: url, + scale: mergedOptions.scale, + minimumPixelSize: mergedOptions.minimumPixelSize, + maximumScale: mergedOptions.maximumScale, + heightReference: mergedOptions.heightReference, + color: mergedOptions.color, + colorBlendMode: mergedOptions.colorBlendMode, + colorBlendAmount: mergedOptions.colorBlendAmount, + silhouetteColor: mergedOptions.silhouetteColor, + silhouetteSize: mergedOptions.silhouetteSize, + debugShowBoundingVolume: mergedOptions.debugShowBoundingVolume, + debugWireframe: mergedOptions.debugWireframe + } + }); + + this.models.set(id, entity); + + // 等待模型加载完成 + await new Promise((resolve, reject) => { + const removeListener = this.viewer.scene.postRender.addEventListener(() => { + const model = entity.model; + if (model && model.ready) { + removeListener(); + resolve(entity); + } + }); + + // 超时处理 + setTimeout(() => { + removeListener(); + reject(new Error(`Model loading timeout: ${id}`)); + }, 30000); + }); + + return entity; + } catch (error) { + console.error(`Failed to load model ${id}:`, error); + throw error; + } + } + + /** + * 加载 GLTF/GLB 模型 + */ + async loadGltfModel( + id: string, + url: string, + position: { longitude: number; latitude: number; height: number }, + options: Partial = {} + ): Promise { + return this.loadModel(id, url, position, options); + } + + /** + * 添加重庆地标建筑模型 + */ + async addChongqingLandmarkModels(): Promise { + const landmarks = [ + { + id: 'jiefangbei', + name: '解放碑', + url: '/models/jiefangbei.glb', // 替换为实际模型路径 + position: { longitude: 106.574, latitude: 29.556, height: 50 }, + scale: 2.0 + }, + { + id: 'hongyadong', + name: '洪崖洞', + url: '/models/hongyadong.glb', + position: { longitude: 106.577, latitude: 29.561, height: 20 }, + scale: 1.5 + }, + { + id: 'chaotianmen', + name: '朝天门', + url: '/models/chaotianmen.glb', + position: { longitude: 106.583, latitude: 29.565, height: 10 }, + scale: 1.8 + } + ]; + + for (const landmark of landmarks) { + try { + await this.loadGltfModel( + landmark.id, + landmark.url, + landmark.position, + { scale: landmark.scale } + ); + console.log(`Loaded landmark model: ${landmark.name}`); + } catch (error) { + console.error(`Failed to load landmark model ${landmark.name}:`, error); + } + } + } + + /** + * 添加动态车辆模型 + */ + async addVehicleModel( + id: string, + path: { longitude: number; latitude: number; height: number }[], + speed: number = 10 + ): Promise { + const modelUrl = '/models/vehicle.glb'; // 车辆模型路径 + + // 创建路径位置 + const positions = path.map(point => + Cesium.Cartesian3.fromDegrees(point.longitude, point.latitude, point.height) + ); + + // 创建样条曲线路径 + const spline = new Cesium.HermiteSpline({ + times: path.map((_, index) => index), + points: positions + }); + + const entity = this.entities.add({ + id: id, + position: new Cesium.CallbackProperty((time, result) => { + const seconds = Cesium.JulianDate.secondsDifference(time, startTime); + const position = spline.evaluate(seconds * speed, result); + return position; + }, false), + orientation: new Cesium.VelocityOrientationProperty( + new Cesium.CallbackProperty((time, result) => { + const seconds = Cesium.JulianDate.secondsDifference(time, startTime); + return spline.evaluate(seconds * speed, result); + }, false) + ), + model: { + uri: modelUrl, + scale: 1.0, + minimumPixelSize: 64 + } + }); + + const startTime = Cesium.JulianDate.now(); + this.models.set(id, entity); + + return entity; + } + + /** + * 控制模型动画 + */ + playModelAnimation( + modelId: string, + animation: ModelAnimation + ): void { + const entity = this.models.get(modelId); + if (!entity || !entity.model) return; + + const model = entity.model; + + // 获取模型动画 + const modelAnimation = model.activeAnimations.add({ + name: animation.name, + speedup: animation.speed || 1.0, + loop: animation.loop || true, + startTime: animation.startTime, + stopTime: animation.stopTime + }); + } + + /** + * 停止模型动画 + */ + stopModelAnimation(modelId: string, animationName?: string): void { + const entity = this.models.get(modelId); + if (!entity || !entity.model) return; + + const model = entity.model; + + if (animationName) { + // 停止特定动画 + const animations = model.activeAnimations; + for (let i = 0; i < animations.length; i++) { + if (animations[i].name === animationName) { + animations.remove(animations[i]); + break; + } + } + } else { + // 停止所有动画 + model.activeAnimations.removeAll(); + } + } + + /** + * 设置模型位置 + */ + setModelPosition( + modelId: string, + position: { longitude: number; latitude: number; height: number } + ): void { + const entity = this.models.get(modelId); + if (entity) { + entity.position = Cesium.Cartesian3.fromDegrees( + position.longitude, + position.latitude, + position.height + ); + } + } + + /** + * 设置模型缩放 + */ + setModelScale(modelId: string, scale: number): void { + const entity = this.models.get(modelId); + if (entity && entity.model) { + entity.model.scale = scale; + } + } + + /** + * 设置模型颜色 + */ + setModelColor(modelId: string, color: Cesium.Color): void { + const entity = this.models.get(modelId); + if (entity && entity.model) { + entity.model.color = color; + } + } + + /** + * 显示/隐藏模型 + */ + setModelVisibility(modelId: string, visible: boolean): void { + const entity = this.models.get(modelId); + if (entity) { + entity.show = visible; + } + } + + /** + * 高亮显示模型 + */ + highlightModel(modelId: string, highlight: boolean = true): void { + const entity = this.models.get(modelId); + if (entity && entity.model) { + if (highlight) { + entity.model.silhouetteColor = Cesium.Color.YELLOW; + entity.model.silhouetteSize = 2; + } else { + entity.model.silhouetteColor = undefined; + entity.model.silhouetteSize = 0; + } + } + } + + /** + * 移除模型 + */ + removeModel(modelId: string): void { + const entity = this.models.get(modelId); + if (entity) { + this.entities.remove(entity); + this.models.delete(modelId); + } + } + + /** + * 清除所有模型 + */ + clearAllModels(): void { + this.models.forEach((entity, modelId) => { + this.entities.remove(entity); + }); + this.models.clear(); + } + + /** + * 获取模型信息 + */ + getModelInfo(modelId: string): any { + const entity = this.models.get(modelId); + if (!entity) return null; + + const position = entity.position?.getValue(Cesium.JulianDate.now()); + const cartographic = position ? + Cesium.Cartographic.fromCartesian(position) : null; + + return { + id: modelId, + position: cartographic ? { + longitude: Cesium.Math.toDegrees(cartographic.longitude), + latitude: Cesium.Math.toDegrees(cartographic.latitude), + height: cartographic.height + } : null, + visible: entity.show, + model: entity.model ? { + scale: entity.model.scale, + color: entity.model.color + } : null + }; + } + + /** + * 飞向模型 + */ + flyToModel(modelId: string, duration: number = 2): void { + const entity = this.models.get(modelId); + if (entity) { + this.viewer.flyTo(entity, { + duration: duration, + offset: new Cesium.HeadingPitchRange(0, -0.5, 100) + }); + } + } + + /** + * 创建模型点击事件 + */ + onModelClick(callback: (modelId: string, entity: Cesium.Entity) => void): void { + this.viewer.entities.collectionChanged.addEventListener(() => { + this.viewer.screenSpaceEventHandler.setInputAction((clickEvent: any) => { + const pickedObject = this.viewer.scene.pick(clickEvent.position); + if (pickedObject && pickedObject.id) { + const entity = pickedObject.id; + const modelId = this.findModelIdByEntity(entity); + if (modelId) { + callback(modelId, entity); + } + } + }, Cesium.ScreenSpaceEventType.LEFT_CLICK); + }); + } + + /** + * 根据实体查找模型ID + */ + private findModelIdByEntity(entity: Cesium.Entity): string | null { + for (const [modelId, modelEntity] of this.models) { + if (modelEntity === entity) { + return modelId; + } + } + return null; + } +} \ No newline at end of file diff --git a/src/views/cesiums/components/ViewTool.ts b/src/views/cesiums/components/ViewTool.ts new file mode 100644 index 0000000..ede848d --- /dev/null +++ b/src/views/cesiums/components/ViewTool.ts @@ -0,0 +1,68 @@ +// src/utils/cesium/ViewTool.ts +import * as Cesium from 'cesium'; + +export class ViewTool { + private viewer: Cesium.Viewer; + + constructor(viewer: Cesium.Viewer) { + this.viewer = viewer; + } + + // 飞往指定坐标 + flyTo(longitude: number, latitude: number, height: number = 10000, duration: number = 2): void { + this.viewer.camera.flyTo({ + destination: Cesium.Cartesian3.fromDegrees(longitude, latitude, height), + duration: duration + }); + } + + // 飞往矩形区域 + flyToRectangle( + west: number, + south: number, + east: number, + north: number, + height: number | null = null, + duration: number = 2 + ): void { + const rectangle = Cesium.Rectangle.fromDegrees(west, south, east, north); + + const options: any = { + destination: rectangle, + duration: duration + }; + + if (height) { + options.offset = new Cesium.HeadingPitchRange(0, -Cesium.Math.PI / 4, height); + } + + this.viewer.camera.flyTo(options); + } + + // 设置重庆视角 + setChongqingView(viewType: string = 'default'): void { + const views: { [key: string]: any } = { + 'default': { west: 106.3, south: 29.3, east: 106.8, north: 29.8, height: 50000 }, + 'yuzhong': { west: 106.52, south: 29.52, east: 106.60, north: 29.58, height: 5000 }, + 'overview': { west: 105.5, south: 28.5, east: 108.5, north: 31.5, height: 100000 } + }; + + const view = views[viewType] || views['default']; + this.flyToRectangle(view.west, view.south, view.east, view.north, view.height); + } + + // 获取当前相机信息 + getCameraInfo() { + const position = this.viewer.camera.position; + const cartographic = Cesium.Cartographic.fromCartesian(position); + const longitude = Cesium.Math.toDegrees(cartographic.longitude); + const latitude = Cesium.Math.toDegrees(cartographic.latitude); + const height = cartographic.height; + + return { + longitude: longitude.toFixed(6), + latitude: latitude.toFixed(6), + height: height.toFixed(2) + }; + } +} \ No newline at end of file diff --git a/src/views/cesiums/components/useCesium.ts b/src/views/cesiums/components/useCesium.ts new file mode 100644 index 0000000..c55c085 --- /dev/null +++ b/src/views/cesiums/components/useCesium.ts @@ -0,0 +1,90 @@ +// src/composables/useCesium.ts +import { ref, onMounted, onUnmounted } from 'vue'; +import * as Cesium from 'cesium'; +import { ViewTool } from './ViewTool'; +import { MarkerTool } from './MarkerTool'; +import { ModelTool } from './ModelTool'; +import { CoordinatePicker } from './CoordinatePicker'; +import { DrawingTool } from './DrawingTool'; + +export function useCesium(containerId: string) { + const viewer = ref(null); + const viewTool = ref(null); + const markerTool = ref(null); + const modelTool = ref(null); + const coordinatePicker = ref(null); + const drawingTool = ref(null); + const isInitialized = ref(false); + + // 初始化 Cesium + const initializeCesium = async (): Promise => { + if (viewer.value) return; + + try { + // Cesium.Ion.defaultAccessToken = 'your-cesium-ion-access-token'; + + viewer.value = new Cesium.Viewer(containerId, { + terrainProvider: await Cesium.createWorldTerrainAsync(), + animation: false, + timeline: false, + baseLayerPicker: false, + geocoder: false, + homeButton: false, + sceneModePicker: false, + navigationHelpButton: false, + fullscreenButton: false, + infoBox: false + }); + + // 初始化工具类 + viewTool.value = new ViewTool(viewer.value); + markerTool.value = new MarkerTool(viewer.value); + modelTool.value = new ModelTool(viewer.value); + coordinatePicker.value = new CoordinatePicker(viewer.value); + drawingTool.value = new DrawingTool(viewer.value); + + isInitialized.value = true; + + // 设置初始视角 + viewTool.value.setChongqingView('overview'); + + } catch (error) { + console.error('Failed to initialize Cesium:', error); + } + }; + + // 销毁 Cesium +const destroyCesium = (): void => { + if (viewer.value) { + coordinatePicker.value?.destroy(); + drawingTool.value?.destroy(); + viewer.value.destroy(); + viewer.value = null; + viewTool.value = null; + markerTool.value = null; + modelTool.value = null; + coordinatePicker.value = null; + drawingTool.value = null; + isInitialized.value = false; + } + }; + onMounted(() => { + initializeCesium(); + }); + + onUnmounted(() => { + destroyCesium(); + }); + + return { + viewer, + viewTool, + markerTool, + modelTool, + coordinatePicker, + drawingTool, + isInitialized, + initializeCesium, + destroyCesium + }; +} \ No newline at end of file diff --git a/src/views/cesiums/components/useCesiumTools.ts b/src/views/cesiums/components/useCesiumTools.ts new file mode 100644 index 0000000..e937d9c --- /dev/null +++ b/src/views/cesiums/components/useCesiumTools.ts @@ -0,0 +1,73 @@ +// src/composables/useCesiumTools.ts +import { ref, computed } from 'vue'; +import type { ViewTool } from './ViewTool'; +import type { MarkerTool } from './MarkerTool'; + +export function useCesiumTools(viewTool: ViewTool | null, markerTool: MarkerTool | null) { + const currentView = ref('overview'); + const cameraInfo = ref({ longitude: '0', latitude: '0', height: '0' }); + + // 视角选项 + const viewOptions = [ + { value: 'overview', label: '重庆全景', icon: '🌆' }, + { value: 'yuzhong', label: '渝中半岛', icon: '🏞️' }, + { value: 'default', label: '默认视角', icon: '📍' } + ]; + + // 地标选项 + const landmarkOptions = [ + { value: 'chongqing', label: '重庆地标', icon: '🏛️' } + ]; + + // 设置视角 + const setView = (viewType: string): void => { + if (viewTool.value) { + viewTool.value.setChongqingView(viewType); + currentView.value = viewType; + } + }; + + // 添加地标 + const addLandmarks = (type: string): void => { + console.log("添加地标") + if (markerTool.value) { + switch (type) { + case 'chongqing': + markerTool.value.addChongqingLandmark(); + break; + } + } + }; + + // 清除地标 + const clearLandmarks = (): void => { + console.log("清除地标") + if (markerTool.value) { + markerTool.value.clearAllMarkers(); + } + }; + + // 更新相机信息 + const updateCameraInfo = (): void => { + if (viewTool.value) { + cameraInfo.value = viewTool.value.getCameraInfo(); + } + }; + + // 自动更新相机信息 + const startCameraTracking = (): void => { + setInterval(updateCameraInfo, 1000); + }; + + return { + currentView, + cameraInfo, + viewOptions, + landmarkOptions, + setView, + addLandmarks, + clearLandmarks, + updateCameraInfo, + startCameraTracking + }; +} \ No newline at end of file diff --git a/src/views/cesiums/components/useCoordinatePicker.ts b/src/views/cesiums/components/useCoordinatePicker.ts new file mode 100644 index 0000000..05b891d --- /dev/null +++ b/src/views/cesiums/components/useCoordinatePicker.ts @@ -0,0 +1,107 @@ +// src/composables/useCoordinatePicker.ts +import { ref, reactive } from 'vue'; +import type { CoordinatePicker, PickResult } from './CoordinatePicker'; + +export function useCoordinatePicker(coordinatePicker: CoordinatePicker | null) { + const isPickerEnabled = ref(false); + const pickHistory = reactive([]); + const lastPickResult = ref(null); + const maxHistoryLength = 10; + + // 启用坐标拾取 + const enablePicker = (): void => { + if (coordinatePicker.value) { + coordinatePicker.value.enable(); + isPickerEnabled.value = true; + + // 添加拾取回调 + coordinatePicker.value.onPick(handlePick); + } + }; + + // 禁用坐标拾取 + const disablePicker = (): void => { + if (coordinatePicker.value) { + coordinatePicker.value.disable(); + isPickerEnabled.value = false; + } + }; + + // 切换拾取状态 + const togglePicker = (): void => { + if (isPickerEnabled.value) { + disablePicker(); + } else { + enablePicker(); + } + }; + + // 处理拾取结果 + const handlePick = (result: PickResult): void => { + lastPickResult.value = result; + + // 添加到历史记录 + pickHistory.unshift(result); + + // 限制历史记录长度 + if (pickHistory.length > maxHistoryLength) { + pickHistory.pop(); + } + }; + + // 清除历史记录 + const clearHistory = (): void => { + pickHistory.splice(0, pickHistory.length); + lastPickResult.value = null; + }; + + // 复制坐标到剪贴板 + const copyToClipboard = (result: PickResult): void => { + const text = `经度: ${result.longitude.toFixed(6)}, 纬度: ${result.latitude.toFixed(6)}, 高程: ${result.height.toFixed(2)}米`; + + navigator.clipboard.writeText(text).then(() => { + console.log('坐标已复制到剪贴板:', text); + }).catch(err => { + console.error('复制失败:', err); + }); + }; + + // 格式化坐标显示 + const formatCoordinate = (result: PickResult): string => { + return `经度: ${result.longitude.toFixed(6)}\n纬度: ${result.latitude.toFixed(6)}\n高程: ${result.height.toFixed(2)}米`; + }; + + // 获取WKT格式坐标 + const getWKT = (result: PickResult): string => { + return `POINT(${result.longitude.toFixed(6)} ${result.latitude.toFixed(6)})`; + }; + + // 获取GeoJSON格式坐标 + const getGeoJSON = (result: PickResult): any => { + return { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [result.longitude, result.latitude, result.height] + }, + properties: { + height: result.height, + terrainHeight: result.terrainHeight + } + }; + }; + + return { + isPickerEnabled, + pickHistory, + lastPickResult, + enablePicker, + disablePicker, + togglePicker, + clearHistory, + copyToClipboard, + formatCoordinate, + getWKT, + getGeoJSON + }; +} \ No newline at end of file diff --git a/src/views/cesiums/components/useDrawingManager.ts b/src/views/cesiums/components/useDrawingManager.ts new file mode 100644 index 0000000..173fdb6 --- /dev/null +++ b/src/views/cesiums/components/useDrawingManager.ts @@ -0,0 +1,302 @@ +// src/composables/useDrawingManager.ts +import { ref, reactive,watch} from 'vue'; +import type { DrawingTool, DrawingResult, DrawingInfo, DrawingOptions, CircleOptions, PolygonOptions } from './DrawingTool'; + +export function useDrawingManager(drawingTool: any) { // 修改参数类型为 any 或 Ref + const isDrawing = ref(false); + const currentDrawingType = ref<'circle' | 'polygon' | null>(null); + const drawings = reactive(new Map()); + const selectedDrawing = ref(null); + const drawingInfo = ref(null); + + // 绘图选项 + const drawingOptions = reactive<{ + circle: CircleOptions; + polygon: PolygonOptions; + }>({ + circle: { + color: Cesium.Color.YELLOW.withAlpha(0.3), + outlineColor: Cesium.Color.YELLOW, + outlineWidth: 2, + fill: true, + height: 0, + extrudedHeight: 1000 + }, + polygon: { + color: Cesium.Color.CYAN.withAlpha(0.3), + outlineColor: Cesium.Color.CYAN, + outlineWidth: 2, + fill: true, + height: 0, + extrudedHeight: 1000, + closePath: true + } + }); + + // // 初始化点击回调 - 使用 watch 来确保 drawingTool 已初始化 + // import { watch } from 'vue'; + + watch(() => drawingTool?.value, (newTool) => { + if (newTool) { + newTool.setCallbacks({ + onClick: (result: DrawingResult, info: DrawingInfo) => { + handleDrawingClick(result, info); + } + }); + } + }, { immediate: true }); + + // 处理绘图点击 + const handleDrawingClick = (result: DrawingResult, info: DrawingInfo): void => { + selectedDrawing.value = result.id; + drawingInfo.value = info; + + // 高亮显示 + if (drawingTool?.value) { + drawingTool.value.highlightDrawing(result.id, true); + } + + console.log('Drawing clicked:', info); + }; + + // 开始绘制圆形 + const startCircleDrawing = (): void => { + const tool = drawingTool?.value; + if (!tool) return; + + tool.setCallbacks({ + onStart: () => { + isDrawing.value = true; + currentDrawingType.value = 'circle'; + drawingInfo.value = null; + }, + onComplete: (result: DrawingResult) => { + isDrawing.value = false; + currentDrawingType.value = null; + drawings.set(result.id, result); + selectedDrawing.value = result.id; + drawingInfo.value = result.info; + }, + onCancel: () => { + isDrawing.value = false; + currentDrawingType.value = null; + }, + onClick: (result: DrawingResult, info: DrawingInfo) => { + handleDrawingClick(result, info); + } + }); + + tool.startDrawingCircle(drawingOptions.circle); + }; + + // 开始绘制多边形 + const startPolygonDrawing = (): void => { + const tool = drawingTool?.value; + if (!tool) return; + + tool.setCallbacks({ + onStart: () => { + isDrawing.value = true; + currentDrawingType.value = 'polygon'; + drawingInfo.value = null; + }, + onComplete: (result: DrawingResult) => { + isDrawing.value = false; + currentDrawingType.value = null; + drawings.set(result.id, result); + selectedDrawing.value = result.id; + drawingInfo.value = result.info; + }, + onCancel: () => { + isDrawing.value = false; + currentDrawingType.value = null; + }, + onClick: (result: DrawingResult, info: DrawingInfo) => { + handleDrawingClick(result, info); + } + }); + + tool.startDrawingPolygon(drawingOptions.polygon); + }; + + // 取消绘制 + const cancelDrawing = (): void => { + const tool = drawingTool?.value; + if (tool) { + tool.cancelDrawing(); + isDrawing.value = false; + currentDrawingType.value = null; + } + }; + + // 选择图形 + const selectDrawing = (id: string): void => { + const tool = drawingTool?.value; + if (selectedDrawing.value === id) { + deselectDrawing(); + return; + } + + if (selectedDrawing.value && tool) { + tool.highlightDrawing(selectedDrawing.value, false); + } + + selectedDrawing.value = id; + const drawing = drawings.get(id); + if (drawing && tool) { + tool.highlightDrawing(id, true); + drawingInfo.value = drawing.info; + } + }; + + // 取消选择 + const deselectDrawing = (): void => { + const tool = drawingTool?.value; + if (selectedDrawing.value && tool) { + tool.highlightDrawing(selectedDrawing.value, false); + } + selectedDrawing.value = null; + drawingInfo.value = null; + }; + + // 移除图形 + const removeDrawing = (id: string): void => { + const tool = drawingTool?.value; + if (tool && tool.removeDrawing(id)) { + drawings.delete(id); + if (selectedDrawing.value === id) { + deselectDrawing(); + } + } + }; + + // 清除所有图形 + const clearAllDrawings = (): void => { + const tool = drawingTool?.value; + if (tool) { + tool.clearAllDrawings(); + drawings.clear(); + deselectDrawing(); + } + }; + + // 飞向图形 + const flyToDrawing = (id: string): void => { + const tool = drawingTool?.value; + if (tool) { + tool.flyToDrawing(id); + } + }; + + // 更新绘图选项 + const updateDrawingOptions = (type: 'circle' | 'polygon', options: Partial): void => { + drawingOptions[type] = { ...drawingOptions[type], ...options }; + }; + + // 导出图形数据 + const exportDrawings = (): any[] => { + const tool = drawingTool?.value; + if (tool) { + return tool.exportDrawings(); + } + return []; + }; + + // 导入图形数据 + const importDrawings = (data: any[]): void => { + const tool = drawingTool?.value; + if (tool) { + tool.importDrawings(data); + data.forEach(item => { + drawings.set(item.id, item as DrawingResult); + }); + } + }; + + // 获取绘图状态文本 + const getDrawingStatus = (): string => { + if (!isDrawing.value) return '准备就绪'; + + if (currentDrawingType.value === 'circle') { + return '绘制圆形: 点击确定圆心,再次点击确定半径,右键完成'; + } else if (currentDrawingType.value === 'polygon') { + return '绘制多边形: 点击添加顶点,右键完成绘制,ESC取消'; + } + + return '绘制中...'; + }; + + // 格式化坐标显示 + const formatCoordinate = (coord: { longitude: number; latitude: number; height: number }): string => { + return `经度: ${coord.longitude.toFixed(6)}\n纬度: ${coord.latitude.toFixed(6)}\n高程: ${coord.height.toFixed(2)}米`; + }; + + // 获取绘图信息显示文本 + const getDrawingInfoText = (): string => { + if (!drawingInfo.value) return ''; + + const info = drawingInfo.value; + + if (info.type === 'circle') { + const center = info.properties.center; + return `⭕ 圆形空域信息\n\n` + + `📍 圆心坐标:\n${formatCoordinate(center)}\n\n` + + `📏 半径: ${(info.radius! / 1000).toFixed(3)} km\n` + + `📐 直径: ${(info.radius! * 2 / 1000).toFixed(3)} km\n` + + `🔄 周长: ${(info.properties.circumference / 1000).toFixed(3)} km\n` + + `📊 面积: ${(info.area / 1000000).toFixed(3)} km²`; + } else { + const bounds = info.properties.bounds; + const center = info.properties.center; + const boundaryPoints = info.properties.boundaryPoints; + + let text = `🔷 多边形空域信息\n\n` + + `📍 中心点:\n${formatCoordinate(center)}\n\n` + + `🗺️ 边界范围:\n` + + `北: ${bounds.north.toFixed(6)}\n` + + `南: ${bounds.south.toFixed(6)}\n` + + `东: ${bounds.east.toFixed(6)}\n` + + `西: ${bounds.west.toFixed(6)}\n\n` + + `📏 尺寸:\n` + + `宽度: ${(info.properties.width / 1000).toFixed(3)} km\n` + + `高度: ${(info.properties.height / 1000).toFixed(3)} km\n` + + `周长: ${(info.properties.perimeter / 1000).toFixed(3)} km\n` + + `面积: ${(info.area / 1000000).toFixed(3)} km²\n\n` + + `📍 边界顶点 (${boundaryPoints.length}个):\n`; + + // 添加前几个顶点信息,避免信息过长 + boundaryPoints.slice(0, 6).forEach((point, index) => { + text += `顶点${index + 1}: ${point.longitude.toFixed(6)}, ${point.latitude.toFixed(6)}\n`; + }); + + if (boundaryPoints.length > 6) { + text += `... 还有 ${boundaryPoints.length - 6} 个顶点`; + } + + return text; + } + }; + + return { + isDrawing, + currentDrawingType, + drawings, + selectedDrawing, + drawingInfo, + drawingOptions, + startCircleDrawing, + startPolygonDrawing, + cancelDrawing, + selectDrawing, + deselectDrawing, + removeDrawing, + clearAllDrawings, + flyToDrawing, + updateDrawingOptions, + exportDrawings, + importDrawings, + getDrawingStatus, + getDrawingInfoText, + formatCoordinate + }; +} \ No newline at end of file diff --git a/src/views/cesiums/components/useModelManager.ts b/src/views/cesiums/components/useModelManager.ts new file mode 100644 index 0000000..342b630 --- /dev/null +++ b/src/views/cesiums/components/useModelManager.ts @@ -0,0 +1,184 @@ +// src/composables/useModelManager.ts +import { ref, reactive } from 'vue'; +// import type { ModelTool } from './ModelTool'; +import type { ModelOptions, ModelAnimation,ModelTool } from './ModelTool'; + +export function useModelManager(modelTool: ModelTool | null) { + const models = reactive(new Map()); + const selectedModel = ref(null); + const isModelLoading = ref(false); + + // 预定义模型配置 + const predefinedModels = { + building: { + name: '建筑模型', + url: '/models/building.glb', + scale: 1.0 + }, + vehicle: { + name: '车辆模型', + url: '/models/vehicle.glb', + scale: 0.5 + }, + tree: { + name: '树木模型', + url: '/models/tree.glb', + scale: 2.0 + } + }; + + // 重庆地标配置 + const chongqingLandmarks = [ + { + id: 'landmark_1', + name: '重庆大剧院', + type: 'building', + position: { longitude: 106.582, latitude: 29.568, height: 10 } + }, + { + id: 'landmark_2', + name: '来福士广场', + type: 'building', + position: { longitude: 106.584, latitude: 29.566, height: 15 } + } + ]; + + // 加载模型 + const loadModel = async ( + id: string, + url: string, + position: { longitude: number; latitude: number; height: number }, + options: Partial = {} + ): Promise => { + if (!modelTool.value) return; + + isModelLoading.value = true; + try { + const entity = await modelTool.value.loadModel(id, url, position, options); + models.set(id, { + id, + entity, + position, + options + }); + } catch (error) { + console.error('Failed to load model:', error); + throw error; + } finally { + isModelLoading.value = false; + } + }; + + // 加载预定义模型 + const loadPredefinedModel = async ( + id: string, + modelType: keyof typeof predefinedModels, + position: { longitude: number; latitude: number; height: number } + ): Promise => { + const modelConfig = predefinedModels[modelType]; + return loadModel(id, modelConfig.url, position, { scale: modelConfig.scale }); + }; + + // 加载重庆地标 + const loadChongqingLandmarks = async (): Promise => { + if (!modelTool.value) return; + + for (const landmark of chongqingLandmarks) { + try { + await loadPredefinedModel( + landmark.id, + landmark.type as keyof typeof predefinedModels, + landmark.position + ); + } catch (error) { + console.error(`Failed to load landmark ${landmark.name}:`, error); + } + } + }; + + // 选择模型 + const selectModel = (modelId: string): void => { + selectedModel.value = modelId; + if (modelTool.value) { + modelTool.value.highlightModel(modelId, true); + } + }; + + // 取消选择模型 + const deselectModel = (): void => { + if (selectedModel.value && modelTool.value) { + modelTool.value.highlightModel(selectedModel.value, false); + } + selectedModel.value = null; + }; + + // 更新模型位置 + const updateModelPosition = ( + modelId: string, + position: { longitude: number; latitude: number; height: number } + ): void => { + if (modelTool.value) { + modelTool.value.setModelPosition(modelId, position); + const model = models.get(modelId); + if (model) { + model.position = position; + } + } + }; + + // 更新模型缩放 + const updateModelScale = (modelId: string, scale: number): void => { + if (modelTool.value) { + modelTool.value.setModelScale(modelId, scale); + const model = models.get(modelId); + if (model) { + model.options.scale = scale; + } + } + }; + + // 移除模型 + const removeModel = (modelId: string): void => { + if (modelTool.value) { + modelTool.value.removeModel(modelId); + models.delete(modelId); + if (selectedModel.value === modelId) { + deselectModel(); + } + } + }; + + // 清除所有模型 + const clearAllModels = (): void => { + if (modelTool.value) { + modelTool.value.clearAllModels(); + models.clear(); + deselectModel(); + } + }; + + // 飞向模型 + const flyToModel = (modelId: string): void => { + if (modelTool.value) { + modelTool.value.flyToModel(modelId); + } + }; + + return { + models, + selectedModel, + isModelLoading, + predefinedModels, + chongqingLandmarks, + loadModel, + loadPredefinedModel, + loadChongqingLandmarks, + selectModel, + deselectModel, + updateModelPosition, + updateModelScale, + removeModel, + clearAllModels, + flyToModel + }; +} \ No newline at end of file