| src/api/airspace/airspace.js | ●●●●● patch | view | raw | blame | history | |
| src/utils/cesium/publicCesium.js | ●●●●● patch | view | raw | blame | history | |
| src/views/airspace/airspaceType.vue | ●●●●● patch | view | raw | blame | history | |
| src/views/gridManagement/GridSettings/OccupancyGrid.js | ●●●●● patch | view | raw | blame | history | |
| src/views/gridManagement/GridSettings/PathPlanning.js | ●●●●● patch | view | raw | blame | history | |
| src/views/gridManagement/gridManagement.vue | ●●●●● patch | view | raw | blame | history | |
| vite.config.mjs | ●●●●● patch | view | raw | blame | history |
src/api/airspace/airspace.js
@@ -62,4 +62,36 @@ params }) } // ----------空域录入相关接口调用---------- // ----------空域录入相关接口调用---------- // ----------网格管理相关接口调用---------- export const airGridPage = params => { return request({ url: '/drone-device-core/airgrid/page', method: 'get', params }) } export const airGridUpdate = data => { return request({ url: '/drone-device-core/airgrid/update', method: 'put', data: data, }) } export const airGridAdd= data => { return request({ url: '/drone-device-core/airgrid/add', method: 'post', data: data, }) } export const airGridDelete= data => { return request({ url: '/drone-device-core/airgrid/delete/' + data, method: 'delete', }) } src/utils/cesium/publicCesium.js
@@ -224,6 +224,7 @@ // 飞行 flyto flyTo (pointOption, time = 4, height = 3000, orientation = {}, complete = () => { }) { console.log(pointOption, ) if (!pointOption.longitude && !pointOption.latitude) return const destination = Cesium.Cartesian3.fromDegrees(pointOption.longitude, pointOption.latitude, height) const duration = time src/views/airspace/airspaceType.vue
@@ -126,10 +126,19 @@ rowView.value = row } function handleDelete (row) { airSpaceTypeDelete(row.id).then(res => { ElMessage.success('删除成功') getList() // 确定要删除么? ElMessage({ message: '确定要删除么?', type: 'warning', showClose: true, onClose: () => { airSpaceTypeDelete(row.id).then(res => { ElMessage.success('删除成功') getList() }) } }) } function handleAdd() { titleTxt.value = '新增' src/views/gridManagement/GridSettings/OccupancyGrid.js
New file @@ -0,0 +1,1293 @@ // ================================ // 3D占用网格系统模块 // ================================ import * as Cesium from 'cesium' class OccupancyGrid { constructor(viewer, config = {}, gridParams) { this.viewer = viewer this.gridEntities = [] this.tilesBoundingBoxes = [] // 存储瓦片边界框 this.gridParams = gridParams // 网格配置参数 - 可自定义 this.config = { // 网格单元尺寸配置 gridSize: config.gridSize || 50, // 网格单元大小(米) gridWidth: config.gridWidth || 50, // 网格宽度(米) gridHeight: config.gridHeight || 50, // 网格高度(米) gridDepth: config.gridDepth || 50, // 网格深度(米) // 区域扩展配置 heightExtension: config.heightExtension || 120, // 向上向下各扩展120米,确保4层网格(总高度240米,4×50=200米,留有余量) widthExtension: config.widthExtension || 100, // 水平方向扩展100米 // 颜色配置 occupiedColor: config.occupiedColor || Cesium.Color.RED.withAlpha(0.7), // 占用网格颜色(红色) freeColor: config.freeColor || Cesium.Color.GREEN.withAlpha(0.3), // 空闲网格颜色(绿色) occupiedOutlineColor: config.occupiedOutlineColor || Cesium.Color.DARKRED, // 占用网格边框颜色 freeOutlineColor: config.freeOutlineColor || Cesium.Color.DARKGREEN, // 空闲网格边框颜色 outlineWidth: config.outlineWidth || 1, // 边框宽度 // 透明度配置 occupiedAlpha: config.occupiedAlpha || 0.7, // 占用网格透明度 freeAlpha: config.freeAlpha || 0.3, // 空闲网格透明度 // 性能优化配置 maxGridCount: config.maxGridCount || 10000, // 最大网格数量限制 enableBatching: config.enableBatching || true, // 是否启用批处理优化 // 调试配置 showOccupiedOnly: config.showOccupiedOnly || false, // 是否只显示占用的网格 showFreeOnly: config.showFreeOnly || false, // 是否只显示空闲的网格 enableLogging: config.enableLogging || true // 是否启用详细日志 } // 向后兼容旧参数 this.gridSize = this.config.gridSize this.heightExtension = this.config.heightExtension } // ================================ // 更新配置 // ================================ updateConfig (newConfig) { this.config = { ...this.config, ...newConfig } // 更新向后兼容参数 this.gridSize = this.config.gridSize this.heightExtension = this.config.heightExtension if (this.config.enableLogging) { console.log('网格配置已更新:', this.config) } } // ================================ // 获取当前配置 // ================================ getConfig () { return { ...this.config } } // ================================ // 生成占用网格 - 使用配置参数 // ================================ async generateOccupancyGrid (waypoints) { if (waypoints.length < 2) { console.warn('需要至少2个航点来生成占用网格') return } // 清除之前的网格 this.clearGrid() // 获取起点和终点 const startPoint = waypoints[0] const endPoint = waypoints[waypoints.length - 1] // 计算边界框 const bounds = this.calculateBounds(startPoint, endPoint) // 收集3D瓦片的边界框信息 await this.collectTilesBoundingBoxes(bounds) // 生成网格 await this.createGrid(bounds) if (this.config.enableLogging) { console.log('3D占用网格生成完成') console.log(`网格尺寸: ${this.config.gridWidth}m x ${this.config.gridHeight}m x ${this.config.gridDepth}m`) console.log(`总网格数量: ${this.gridEntities.length}`) console.log(`瓦片边界框数量: ${this.tilesBoundingBoxes.length}`) } } // ================================ // 使用航线瓦片数据生成占用网格 - 使用配置参数 // ================================ async generateOccupancyGridWithTiles (waypoints, flightPathTiles) { if (waypoints.length < 2) { console.warn('需要至少2个航点来生成占用网格') return } if (this.config.enableLogging) { console.log(`开始生成占用网格,将分析网格范围内的所有叶子瓦片节点`) } // 清除之前的网格 this.clearGrid() // 获取起点和终点 const startPoint = waypoints[0] const endPoint = waypoints[waypoints.length - 1] // 计算边界框 const bounds = this.calculateBounds(startPoint, endPoint) // 获取瓦片集,用于遍历叶子节点 this.tileset = this.getTileset() if (!this.tileset) { console.warn('未找到瓦片集,无法生成占用网格') return } // 生成网格 await this.createGrid(bounds) if (this.config.enableLogging) { console.log('使用叶子瓦片节点的3D占用网格生成完成') console.log(`网格尺寸: ${this.config.gridWidth}m x ${this.config.gridHeight}m x ${this.config.gridDepth}m`) console.log(`总网格数量: ${this.gridEntities.length}`) } } // ================================ // 获取瓦片集 // ================================ getTileset () { const primitives = this.viewer.scene.primitives for (let i = 0; i < primitives.length; i++) { const primitive = primitives.get(i) if (primitive instanceof Cesium.Cesium3DTileset) { return primitive } } return null } // ================================ // 计算边界框 - 使用配置参数 // ================================ calculateBounds (startPoint, endPoint) { // 转换为地理坐标 const startCartographic = Cesium.Cartographic.fromCartesian(startPoint.position) const endCartographic = Cesium.Cartographic.fromCartesian(endPoint.position) // 计算基础边界 let minLon = Math.min(startCartographic.longitude, endCartographic.longitude) let maxLon = Math.max(startCartographic.longitude, endCartographic.longitude) let minLat = Math.min(startCartographic.latitude, endCartographic.latitude) let maxLat = Math.max(startCartographic.latitude, endCartographic.latitude) // 应用水平扩展 const earthRadius = 6371000 const widthExtensionLon = this.config.widthExtension / (earthRadius * Math.cos((minLat + maxLat) / 2)) const widthExtensionLat = this.config.widthExtension / earthRadius minLon -= widthExtensionLon maxLon += widthExtensionLon minLat -= widthExtensionLat maxLat += widthExtensionLat // 计算平均高度和垂直扩展 const avgHeight = (startPoint.height + endPoint.height) / 2 const heightOffset = 20 // 向上平移50米,避免网格被tileset遮盖 const minHeight = avgHeight - this.config.heightExtension + heightOffset const maxHeight = avgHeight + this.config.heightExtension + heightOffset const bounds = { minLon: minLon, maxLon: maxLon, minLat: minLat, maxLat: maxLat, minHeight: minHeight, maxHeight: maxHeight, avgHeight: avgHeight } if (this.config.enableLogging) { console.log('计算网格边界范围:', { 经度范围: `${Cesium.Math.toDegrees(minLon).toFixed(6)} 到 ${Cesium.Math.toDegrees(maxLon).toFixed(6)}`, 纬度范围: `${Cesium.Math.toDegrees(minLat).toFixed(6)} 到 ${Cesium.Math.toDegrees(maxLat).toFixed(6)}`, 高度范围: `${minHeight.toFixed(1)}m 到 ${maxHeight.toFixed(1)}m (向上平移50m)`, 水平扩展: `${this.config.widthExtension}m`, 垂直扩展: `${this.config.heightExtension}m` }) } return bounds } // ================================ // 收集3D瓦片的边界框信息 // ================================ async collectTilesBoundingBoxes (bounds) { this.tilesBoundingBoxes = [] // 获取所有的图元 const primitives = this.viewer.scene.primitives let tileset = null // 查找3D Tiles图层 for (let i = 0; i < primitives.length; i++) { const primitive = primitives.get(i) if (primitive instanceof Cesium.Cesium3DTileset) { tileset = primitive break } } if (!tileset || !tileset.root) { console.warn('未找到3D Tiles图层') return } // 等待瓦片集加载完成 if (!tileset.ready) { await tileset.readyPromise } // 遍历瓦片树并收集边界框 this.traverseTiles(tileset.root, bounds) console.log(`收集到${this.tilesBoundingBoxes.length}个瓦片边界框`) } // ================================ // 收集航线瓦片的边界框信息 // ================================ collectFlightPathTilesBoundingBoxes (flightPathTiles) { console.log('开始收集航线瓦片边界框信息') this.tilesBoundingBoxes = [] flightPathTiles.forEach((tile, index) => { try { // 获取边界体信息 let boundingVolume = null if (tile.boundingVolume) { boundingVolume = tile.boundingVolume } else if (tile._boundingVolume) { boundingVolume = tile._boundingVolume } else if (tile.contentBoundingVolume) { boundingVolume = tile.contentBoundingVolume } if (!boundingVolume) { console.warn(`瓦片 ${index} 没有边界体信息`) return } let boundingBox = null // 处理不同类型的边界体 if (boundingVolume instanceof Cesium.OrientedBoundingBox) { boundingBox = this.orientedBoundingBoxToAxisAligned(boundingVolume) } else if (boundingVolume instanceof Cesium.BoundingSphere) { boundingBox = this.sphereToAxisAligned(boundingVolume) } else if (boundingVolume.orientedBoundingBox) { boundingBox = this.orientedBoundingBoxToAxisAligned(boundingVolume.orientedBoundingBox) } else if (boundingVolume.boundingSphere) { boundingBox = this.sphereToAxisAligned(boundingVolume.boundingSphere) } else if (boundingVolume._orientedBoundingBox) { boundingBox = this.orientedBoundingBoxToAxisAligned(boundingVolume._orientedBoundingBox) } else if (boundingVolume._boundingSphere) { boundingBox = this.sphereToAxisAligned(boundingVolume._boundingSphere) } if (boundingBox) { this.tilesBoundingBoxes.push(boundingBox) console.log(`添加瓦片 ${index} 的边界框`) } else { console.warn(`无法处理瓦片 ${index} 的边界体类型`) } } catch (error) { console.warn(`处理瓦片 ${index} 边界体时出错:`, error) } }) console.log(`成功收集 ${this.tilesBoundingBoxes.length} 个航线瓦片边界框`) } // ================================ // 遍历瓦片树 // ================================ traverseTiles (tile, bounds) { if (!tile) return // 获取瓦片的边界框 const boundingVolume = tile.boundingVolume if (boundingVolume) { let boundingBox = null try { // 检查是否有中心点和半径(球体) if (boundingVolume.center && boundingVolume.radius !== undefined) { boundingBox = this.sphereToBox(boundingVolume) } // 检查是否有中心点和半轴(盒子) else if (boundingVolume.center && boundingVolume.halfAxes) { const halfAxes = boundingVolume.halfAxes const halfExtents = new Cesium.Cartesian3( Math.max(10, Cesium.Cartesian3.magnitude(new Cesium.Cartesian3(halfAxes[0], halfAxes[1], halfAxes[2]))), Math.max(10, Cesium.Cartesian3.magnitude(new Cesium.Cartesian3(halfAxes[3], halfAxes[4], halfAxes[5]))), Math.max(10, Cesium.Cartesian3.magnitude(new Cesium.Cartesian3(halfAxes[6], halfAxes[7], halfAxes[8]))) ) boundingBox = { center: boundingVolume.center, halfExtents: halfExtents } } // 检查是否有区域信息 else if (boundingVolume.west !== undefined && boundingVolume.east !== undefined) { boundingBox = this.regionToBox(boundingVolume) } // 默认情况:创建一个小的边界框 else if (boundingVolume.center) { boundingBox = { center: boundingVolume.center, halfExtents: new Cesium.Cartesian3(50, 50, 50) // 默认50米 } } } catch (error) { console.warn('处理边界体积时出错:', error) return } if (boundingBox && this.isBoundingBoxInRegion(boundingBox, bounds)) { this.tilesBoundingBoxes.push(boundingBox) } } // 递归遍历子瓦片 if (tile.children && tile.children.length > 0) { for (const child of tile.children) { this.traverseTiles(child, bounds) } } } // ================================ // 将球体转换为包围盒 // ================================ sphereToBox (sphere) { const center = sphere.center const radius = sphere.radius // 创建一个立方体包围盒 const halfExtents = new Cesium.Cartesian3(radius, radius, radius) return { center: center, halfExtents: halfExtents } } // ================================ // 将区域转换为包围盒 // ================================ regionToBox (region) { // 计算区域的中心点 const centerLon = (region.west + region.east) / 2 const centerLat = (region.south + region.north) / 2 const centerHeight = (region.minimumHeight + region.maximumHeight) / 2 const center = Cesium.Cartesian3.fromRadians(centerLon, centerLat, centerHeight) // 计算区域的半尺寸 const lonExtent = (region.east - region.west) / 2 const latExtent = (region.north - region.south) / 2 const heightExtent = (region.maximumHeight - region.minimumHeight) / 2 // 转换为笛卡尔坐标的半扩展 const earthRadius = 6371000 const halfExtents = new Cesium.Cartesian3( lonExtent * earthRadius * Math.cos(centerLat), latExtent * earthRadius, heightExtent ) return { center: center, halfExtents: halfExtents } } // ================================ // 检查边界框是否在感兴趣区域内 // ================================ isBoundingBoxInRegion (boundingBox, bounds) { // 将边界框中心转换为地理坐标 const centerCartographic = Cesium.Cartographic.fromCartesian(boundingBox.center) // 检查是否在地理范围内 return (centerCartographic.longitude >= bounds.minLon && centerCartographic.longitude <= bounds.maxLon && centerCartographic.latitude >= bounds.minLat && centerCartographic.latitude <= bounds.maxLat && centerCartographic.height >= bounds.minHeight && centerCartographic.height <= bounds.maxHeight) } // ================================ // 创建网格 - 使用配置参数 // ================================ async createGrid (bounds) { // 将地理坐标转换为距离(米) const earthRadius = 6371000 // 地球半径(米) // 计算经纬度范围对应的实际距离 const latDistance = (bounds.maxLat - bounds.minLat) * earthRadius const lonDistance = (bounds.maxLon - bounds.minLon) * earthRadius * Math.cos((bounds.minLat + bounds.maxLat) / 2) const heightDistance = bounds.maxHeight - bounds.minHeight // 计算网格数量 - 使用配置的网格尺寸 const gridCountX = Math.ceil(lonDistance / this.config.gridWidth) const gridCountY = Math.ceil(latDistance / this.config.gridHeight) const gridCountZ = Math.ceil(heightDistance / this.config.gridDepth) const totalGridCount = gridCountX * gridCountY * gridCountZ if (this.config.enableLogging) { console.log(`计划生成网格数量: ${gridCountX} x ${gridCountY} x ${gridCountZ} = ${totalGridCount}`) console.log(`网格单元尺寸: 宽${this.config.gridWidth}m x 高${this.config.gridHeight}m x 深${this.config.gridDepth}m`) console.log(`实际距离: 经度${lonDistance.toFixed(1)}m x 纬度${latDistance.toFixed(1)}m x 高度${heightDistance.toFixed(1)}m`) console.log(`垂直方向: 高度范围${heightDistance.toFixed(1)}m ÷ 网格深度${this.config.gridDepth}m = ${gridCountZ}层网格`) } // 性能保护:检查网格数量是否超出限制 if (totalGridCount > this.config.maxGridCount) { console.warn(`网格数量 ${totalGridCount} 超过最大限制 ${this.config.maxGridCount},将进行优化处理`) // 自动调整网格尺寸 const scaleFactor = Math.cbrt(totalGridCount / this.config.maxGridCount) const adjustedGridSize = this.config.gridSize * scaleFactor console.log(`自动调整网格尺寸从 ${this.config.gridSize}m 到 ${adjustedGridSize.toFixed(1)}m`) // 重新计算网格数量 const newGridCountX = Math.ceil(lonDistance / adjustedGridSize) const newGridCountY = Math.ceil(latDistance / adjustedGridSize) const newGridCountZ = Math.ceil(heightDistance / adjustedGridSize) if (this.config.enableLogging) { console.log(`调整后网格数量: ${newGridCountX} x ${newGridCountY} x ${newGridCountZ} = ${newGridCountX * newGridCountY * newGridCountZ}`) } // 使用调整后的参数 return this.createGridWithCustomSize(bounds, newGridCountX, newGridCountY, newGridCountZ, adjustedGridSize) } // 正常情况下生成网格 return this.createGridWithCustomSize(bounds, gridCountX, gridCountY, gridCountZ, this.config.gridSize) } // ================================ // 使用自定义尺寸创建网格 // ================================ async createGridWithCustomSize (bounds, gridCountX, gridCountY, gridCountZ, gridSize) { let occupiedCount = 0 let freeCount = 0 // 批处理参数 const batchSize = this.config.enableBatching ? 100 : 1 let batchCount = 0 // 生成网格立方体 for (let i = 0; i < gridCountX; i++) { for (let j = 0; j < gridCountY; j++) { for (let k = 0; k < gridCountZ; k++) { const gridCenter = this.calculateGridCenter(bounds, i, j, k, gridCountX, gridCountY, gridCountZ) const isOccupied = await this.checkOccupancy(gridCenter) // 计算网格索引号 (i, j, k) const gridIndex = { x: i, y: j, z: k } // 根据配置决定是否显示 let shouldCreate = true if (this.config.showOccupiedOnly && !isOccupied) { shouldCreate = false } if (this.config.showFreeOnly && isOccupied) { shouldCreate = false } if (shouldCreate) { this.createGridCube(gridCenter, isOccupied, gridSize, gridIndex) } // 统计 if (isOccupied) { occupiedCount++ } else { freeCount++ } // 批处理优化:每处理一定数量后让出控制权 batchCount++ if (this.config.enableBatching && batchCount >= batchSize) { batchCount = 0 await new Promise(resolve => setTimeout(resolve, 1)) // 让出1ms给浏览器 } } } } if (this.config.enableLogging) { this.gridParams.hasGrid = true console.log(`网格生成完成 - 占用: ${occupiedCount}, 空闲: ${freeCount}, 总计: ${this.gridEntities.length}`) } } // ================================ // 计算网格中心点 // ================================ calculateGridCenter (bounds, i, j, k, gridCountX, gridCountY, gridCountZ) { // 计算网格中心的地理坐标 const lonStep = (bounds.maxLon - bounds.minLon) / gridCountX const latStep = (bounds.maxLat - bounds.minLat) / gridCountY const heightStep = (bounds.maxHeight - bounds.minHeight) / gridCountZ const lon = bounds.minLon + (i + 0.5) * lonStep const lat = bounds.minLat + (j + 0.5) * latStep const height = bounds.minHeight + (k + 0.5) * heightStep return { longitude: lon, latitude: lat, height: height } } // ================================ // 检查占用状态 - 基于网格范围内的叶子瓦片节点及其外包盒求交 // ================================ async checkOccupancy (gridCenter) { if (!this.tileset) { return false // 没有瓦片集,默认未占用 } // 计算网格单元的边界范围 const gridBounds = this.calculateGridBounds(gridCenter) // 收集网格范围内的所有叶子瓦片节点 const leafTilesInGrid = [] this.findLeafTilesInGridBounds(this.tileset.root, gridBounds, leafTilesInGrid) console.log(this.tileset.root, gridBounds, 'this.tileset.root') if (leafTilesInGrid.length === 0) { return false // 没有叶子瓦片,标记为未占用 } // 使用叶子节点的外包盒与网格单元进行精确求交计算 const hasIntersection = this.checkGridTileIntersections(gridBounds, leafTilesInGrid) if (hasIntersection) { console.log(`网格单元 [${Cesium.Math.toDegrees(gridCenter.longitude).toFixed(6)}, ${Cesium.Math.toDegrees(gridCenter.latitude).toFixed(6)}, ${gridCenter.height.toFixed(1)}] 与 ${leafTilesInGrid.length} 个叶子瓦片相交`) } return hasIntersection } // ================================ // 递归查找网格范围内的叶子瓦片节点 // ================================ findLeafTilesInGridBounds (tile, gridBounds, leafTiles) { if (!tile) { return } // 检查是否为叶子节点 const isLeafNode = !tile.children || tile.children.length === 0 // 首先检查当前瓦片是否与网格边界相交 const intersects = this.isTileIntersectingGridBounds(tile, gridBounds) if (!intersects) { return // 如果当前瓦片不与网格相交,则跳过其所有子节点 } if (isLeafNode) { // 只有叶子节点才添加到结果中 leafTiles.push(tile) } else { // 中间节点与网格相交,继续遍历其子节点 for (let i = 0; i < tile.children.length; i++) { this.findLeafTilesInGridBounds(tile.children[i], gridBounds, leafTiles) } } } // ================================ // 检查瓦片是否与网格边界相交(简化版本,用于预筛选) // ================================ isTileIntersectingGridBounds (tile, gridBounds) { // 获取瓦片的边界体信息 let boundingVolume = null if (tile.boundingVolume) { boundingVolume = tile.boundingVolume } else if (tile._boundingVolume) { boundingVolume = tile._boundingVolume } else if (tile.contentBoundingVolume) { boundingVolume = tile.contentBoundingVolume } if (!boundingVolume) { return false // 没有边界体信息 } try { // 简化的边界检测,用于快速预筛选 let tileBounds = null if (boundingVolume instanceof Cesium.OrientedBoundingBox) { tileBounds = this.getSimpleBoundsFromOBB(boundingVolume) } else if (boundingVolume instanceof Cesium.BoundingSphere) { tileBounds = this.getSimpleBoundsFromSphere(boundingVolume) } else if (boundingVolume.orientedBoundingBox) { tileBounds = this.getSimpleBoundsFromOBB(boundingVolume.orientedBoundingBox) } else if (boundingVolume.boundingSphere) { tileBounds = this.getSimpleBoundsFromSphere(boundingVolume.boundingSphere) } else if (boundingVolume._orientedBoundingBox) { tileBounds = this.getSimpleBoundsFromOBB(boundingVolume._orientedBoundingBox) } else if (boundingVolume._boundingSphere) { tileBounds = this.getSimpleBoundsFromSphere(boundingVolume._boundingSphere) } if (!tileBounds) { return false } // 简单的3D AABB相交检测 const tolerance = 0.0001 const intersects = !( gridBounds.maxLon < tileBounds.minLon - tolerance || gridBounds.minLon > tileBounds.maxLon + tolerance || gridBounds.maxLat < tileBounds.minLat - tolerance || gridBounds.minLat > tileBounds.maxLat + tolerance || gridBounds.maxHeight < tileBounds.minHeight - tolerance || gridBounds.minHeight > tileBounds.maxHeight + tolerance ) return intersects } catch (error) { console.warn('预筛选瓦片边界体时出错:', error) return true // 出错时保守地返回true,让后续精确计算处理 } } // ================================ // 从OrientedBoundingBox获取简化边界(用于预筛选) // ================================ getSimpleBoundsFromOBB (obb) { const center = obb.center const halfAxes = obb.halfAxes // 计算边界盒的大致范围 const xAxis = new Cesium.Cartesian3() const yAxis = new Cesium.Cartesian3() const zAxis = new Cesium.Cartesian3() Cesium.Matrix3.getColumn(halfAxes, 0, xAxis) Cesium.Matrix3.getColumn(halfAxes, 1, yAxis) Cesium.Matrix3.getColumn(halfAxes, 2, zAxis) // 估算边界盒的最大扩展 const maxExtent = Math.max( Cesium.Cartesian3.magnitude(xAxis), Cesium.Cartesian3.magnitude(yAxis), Cesium.Cartesian3.magnitude(zAxis) ) // 将中心转换为地理坐标 const centerCartographic = Cesium.Cartographic.fromCartesian(center) const centerLon = Cesium.Math.toDegrees(centerCartographic.longitude) const centerLat = Cesium.Math.toDegrees(centerCartographic.latitude) const centerHeight = centerCartographic.height // 估算经纬度范围 const deltaLon = maxExtent / (111320 * Math.cos(centerCartographic.latitude)) const deltaLat = maxExtent / 110540 return { minLon: centerLon - deltaLon, maxLon: centerLon + deltaLon, minLat: centerLat - deltaLat, maxLat: centerLat + deltaLat, minHeight: centerHeight - maxExtent, maxHeight: centerHeight + maxExtent } } // ================================ // 从BoundingSphere获取简化边界(用于预筛选) // ================================ getSimpleBoundsFromSphere (sphere) { const center = sphere.center const radius = sphere.radius // 将球心转换为地理坐标 const centerCartographic = Cesium.Cartographic.fromCartesian(center) const centerLon = Cesium.Math.toDegrees(centerCartographic.longitude) const centerLat = Cesium.Math.toDegrees(centerCartographic.latitude) const centerHeight = centerCartographic.height // 估算经纬度范围 const deltaLon = radius / (111320 * Math.cos(centerCartographic.latitude)) const deltaLat = radius / 110540 return { minLon: centerLon - deltaLon, maxLon: centerLon + deltaLon, minLat: centerLat - deltaLat, maxLat: centerLat + deltaLat, minHeight: centerHeight - radius, maxHeight: centerHeight + radius } } // ================================ // 检查网格与叶子瓦片的精确求交 // ================================ checkGridTileIntersections (gridBounds, leafTiles) { for (const tile of leafTiles) { if (this.isGridIntersectingTileBoundingBox(gridBounds, tile)) { return true // 找到一个相交的叶子瓦片即可确定占用状态 } } return false } // ================================ // 检查网格是否与瓦片外包盒相交(精确计算) // ================================ isGridIntersectingTileBoundingBox (gridBounds, tile) { // 获取瓦片的边界体信息 let boundingVolume = null if (tile.boundingVolume) { boundingVolume = tile.boundingVolume } else if (tile._boundingVolume) { boundingVolume = tile._boundingVolume } else if (tile.contentBoundingVolume) { boundingVolume = tile.contentBoundingVolume } if (!boundingVolume) { return false // 没有边界体信息 } try { // 获取瓦片边界框的8个顶点(轴对齐) const tileVertices = this.getTileBoundingBoxVertices(boundingVolume) if (!tileVertices) { return false } // 将网格边界转换为轴对齐边界框的8个顶点 const gridVertices = this.getGridBoundingBoxVertices(gridBounds) // 执行3D AABB求交检测 return this.checkAABBIntersection(gridVertices, tileVertices) } catch (error) { console.warn('处理瓦片边界体时出错:', error) return false } } // ================================ // 获取瓦片边界框的8个顶点(参考FlightPathTiles方法) // ================================ getTileBoundingBoxVertices (boundingVolume) { let center = null let halfAxes = null // 处理不同类型的边界体 if (boundingVolume instanceof Cesium.OrientedBoundingBox) { center = boundingVolume.center halfAxes = boundingVolume.halfAxes } else if (boundingVolume.orientedBoundingBox) { center = boundingVolume.orientedBoundingBox.center halfAxes = boundingVolume.orientedBoundingBox.halfAxes } else if (boundingVolume._orientedBoundingBox) { center = boundingVolume._orientedBoundingBox.center halfAxes = boundingVolume._orientedBoundingBox.halfAxes } else if (boundingVolume instanceof Cesium.BoundingSphere) { return this.getBoundingSphereVertices(boundingVolume) } else if (boundingVolume.boundingSphere) { return this.getBoundingSphereVertices(boundingVolume.boundingSphere) } else if (boundingVolume._boundingSphere) { return this.getBoundingSphereVertices(boundingVolume._boundingSphere) } if (!center || !halfAxes) { return null } return this.calculateBoundingBoxVertices(center, halfAxes) } // ================================ // 计算边界框的8个顶点(轴对齐,参考FlightPathTiles方法) // ================================ calculateBoundingBoxVertices (center, halfAxes) { // 计算所有8个顶点,然后找到轴对齐的边界框 const tempVertices = [] const directions = [ [-1, -1, -1], [-1, -1, 1], [-1, 1, -1], [-1, 1, 1], [1, -1, -1], [1, -1, 1], [1, 1, -1], [1, 1, 1] ] // 提取三个轴向的向量 const xAxis = new Cesium.Cartesian3() const yAxis = new Cesium.Cartesian3() const zAxis = new Cesium.Cartesian3() Cesium.Matrix3.getColumn(halfAxes, 0, xAxis) Cesium.Matrix3.getColumn(halfAxes, 1, yAxis) Cesium.Matrix3.getColumn(halfAxes, 2, zAxis) // 计算原始的8个顶点 directions.forEach(dir => { const vertex = Cesium.Cartesian3.clone(center) Cesium.Cartesian3.add(vertex, Cesium.Cartesian3.multiplyByScalar(xAxis, dir[0], new Cesium.Cartesian3()), vertex) Cesium.Cartesian3.add(vertex, Cesium.Cartesian3.multiplyByScalar(yAxis, dir[1], new Cesium.Cartesian3()), vertex) Cesium.Cartesian3.add(vertex, Cesium.Cartesian3.multiplyByScalar(zAxis, dir[2], new Cesium.Cartesian3()), vertex) tempVertices.push(vertex) }) // 转换为地理坐标,找到边界范围 let minLon = Infinity, maxLon = -Infinity let minLat = Infinity, maxLat = -Infinity let minHeight = Infinity, maxHeight = -Infinity tempVertices.forEach(vertex => { const cartographic = Cesium.Cartographic.fromCartesian(vertex) 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) }) // 创建轴对齐的边界框顶点(平行于地面) const vertices = [ // 底面4个顶点 Cesium.Cartesian3.fromDegrees(minLon, minLat, minHeight), // 0: 左下后 Cesium.Cartesian3.fromDegrees(maxLon, minLat, minHeight), // 1: 右下后 Cesium.Cartesian3.fromDegrees(minLon, maxLat, minHeight), // 2: 左上后 Cesium.Cartesian3.fromDegrees(maxLon, maxLat, minHeight), // 3: 右上后 // 顶面4个顶点 Cesium.Cartesian3.fromDegrees(minLon, minLat, maxHeight), // 4: 左下前 Cesium.Cartesian3.fromDegrees(maxLon, minLat, maxHeight), // 5: 右下前 Cesium.Cartesian3.fromDegrees(minLon, maxLat, maxHeight), // 6: 左上前 Cesium.Cartesian3.fromDegrees(maxLon, maxLat, maxHeight) // 7: 右上前 ] return vertices } // ================================ // 处理球体边界的情况 // ================================ getBoundingSphereVertices (sphere) { const center = sphere.center const radius = sphere.radius // 将球心转换为地理坐标 const centerCartographic = Cesium.Cartographic.fromCartesian(center) const centerLon = Cesium.Math.toDegrees(centerCartographic.longitude) const centerLat = Cesium.Math.toDegrees(centerCartographic.latitude) const centerHeight = centerCartographic.height // 估算经纬度范围(简化处理) const deltaLon = radius / (111320 * Math.cos(centerCartographic.latitude)) // 经度差 const deltaLat = radius / 110540 // 纬度差 // 创建轴对齐的边界框顶点 const vertices = [ // 底面4个顶点 Cesium.Cartesian3.fromDegrees(centerLon - deltaLon, centerLat - deltaLat, centerHeight - radius), Cesium.Cartesian3.fromDegrees(centerLon + deltaLon, centerLat - deltaLat, centerHeight - radius), Cesium.Cartesian3.fromDegrees(centerLon - deltaLon, centerLat + deltaLat, centerHeight - radius), Cesium.Cartesian3.fromDegrees(centerLon + deltaLon, centerLat + deltaLat, centerHeight - radius), // 顶面4个顶点 Cesium.Cartesian3.fromDegrees(centerLon - deltaLon, centerLat - deltaLat, centerHeight + radius), Cesium.Cartesian3.fromDegrees(centerLon + deltaLon, centerLat - deltaLat, centerHeight + radius), Cesium.Cartesian3.fromDegrees(centerLon - deltaLon, centerLat + deltaLat, centerHeight + radius), Cesium.Cartesian3.fromDegrees(centerLon + deltaLon, centerLat + deltaLat, centerHeight + radius) ] return vertices } // ================================ // 获取网格边界框的8个顶点 // ================================ getGridBoundingBoxVertices (gridBounds) { const vertices = [ // 底面4个顶点 Cesium.Cartesian3.fromDegrees(gridBounds.minLon, gridBounds.minLat, gridBounds.minHeight), Cesium.Cartesian3.fromDegrees(gridBounds.maxLon, gridBounds.minLat, gridBounds.minHeight), Cesium.Cartesian3.fromDegrees(gridBounds.minLon, gridBounds.maxLat, gridBounds.minHeight), Cesium.Cartesian3.fromDegrees(gridBounds.maxLon, gridBounds.maxLat, gridBounds.minHeight), // 顶面4个顶点 Cesium.Cartesian3.fromDegrees(gridBounds.minLon, gridBounds.minLat, gridBounds.maxHeight), Cesium.Cartesian3.fromDegrees(gridBounds.maxLon, gridBounds.minLat, gridBounds.maxHeight), Cesium.Cartesian3.fromDegrees(gridBounds.minLon, gridBounds.maxLat, gridBounds.maxHeight), Cesium.Cartesian3.fromDegrees(gridBounds.maxLon, gridBounds.maxLat, gridBounds.maxHeight) ] return vertices } // ================================ // 检查两个轴对齐边界框(AABB)是否相交 // ================================ checkAABBIntersection (vertices1, vertices2) { // 将顶点转换为地理坐标进行比较 const bounds1 = this.getVerticesBounds(vertices1) const bounds2 = this.getVerticesBounds(vertices2) // 3D AABB相交检测 const tolerance = 0.0001 const intersects = !( bounds1.maxLon < bounds2.minLon - tolerance || bounds1.minLon > bounds2.maxLon + tolerance || bounds1.maxLat < bounds2.minLat - tolerance || bounds1.minLat > bounds2.maxLat + tolerance || bounds1.maxHeight < bounds2.minHeight - tolerance || bounds1.minHeight > bounds2.maxHeight + tolerance ) return intersects } // ================================ // 从顶点计算边界范围 // ================================ getVerticesBounds (vertices) { let minLon = Infinity, maxLon = -Infinity let minLat = Infinity, maxLat = -Infinity let minHeight = Infinity, maxHeight = -Infinity vertices.forEach(vertex => { const cartographic = Cesium.Cartographic.fromCartesian(vertex) 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 { minLon: minLon, maxLon: maxLon, minLat: minLat, maxLat: maxLat, minHeight: minHeight, maxHeight: maxHeight } } // ================================ // 计算网格单元的边界范围 // ================================ calculateGridBounds (gridCenter) { const gridLon = Cesium.Math.toDegrees(gridCenter.longitude) const gridLat = Cesium.Math.toDegrees(gridCenter.latitude) const gridHeight = gridCenter.height // 计算网格单元的边界范围 const earthRadius = 6371000 const halfSize = this.gridSize / 2 // 计算经纬度的半范围 const deltaLon = halfSize / (earthRadius * Math.cos(gridCenter.latitude)) const deltaLat = halfSize / earthRadius return { minLon: gridLon - Cesium.Math.toDegrees(deltaLon), maxLon: gridLon + Cesium.Math.toDegrees(deltaLon), minLat: gridLat - Cesium.Math.toDegrees(deltaLat), maxLat: gridLat + Cesium.Math.toDegrees(deltaLat), minHeight: gridHeight - halfSize, maxHeight: gridHeight + halfSize } } // ================================ // 创建网格单元的边界框 // ================================ createGridBoundingBox (gridCenter) { // 将地理坐标转换为笛卡尔坐标 const centerCartesian = Cesium.Cartesian3.fromRadians( gridCenter.longitude, gridCenter.latitude, gridCenter.height ) // 创建半扩展向量 const halfSize = this.gridSize / 2 const halfExtents = new Cesium.Cartesian3(halfSize, halfSize, halfSize) return { center: centerCartesian, halfExtents: halfExtents } } // ================================ // 检查两个边界框是否相交 // ================================ checkBoundingBoxIntersection (box1, box2) { // 计算两个边界框的最小和最大点 const box1Min = Cesium.Cartesian3.subtract(box1.center, box1.halfExtents, new Cesium.Cartesian3()) const box1Max = Cesium.Cartesian3.add(box1.center, box1.halfExtents, new Cesium.Cartesian3()) const box2Min = Cesium.Cartesian3.subtract(box2.center, box2.halfExtents, new Cesium.Cartesian3()) const box2Max = Cesium.Cartesian3.add(box2.center, box2.halfExtents, new Cesium.Cartesian3()) // 检查在三个轴上是否都有重叠 return (box1Min.x <= box2Max.x && box1Max.x >= box2Min.x && box1Min.y <= box2Max.y && box1Max.y >= box2Min.y && box1Min.z <= box2Max.z && box1Max.z >= box2Min.z) } // ================================ // 创建网格立方体 - 使用配置的颜色和尺寸 // ================================ createGridCube (gridCenter, isOccupied, customGridSize = null, gridIndex = null) { // 将地理坐标转换为笛卡尔坐标 const position = Cesium.Cartesian3.fromRadians( gridCenter.longitude, gridCenter.latitude, gridCenter.height ) // 使用配置的颜色和透明度 const color = isOccupied ? (this.config.occupiedColor || Cesium.Color.RED.withAlpha(this.config.occupiedAlpha)) : (this.config.freeColor || Cesium.Color.GREEN.withAlpha(this.config.freeAlpha)) const outlineColor = isOccupied ? this.config.occupiedOutlineColor : this.config.freeOutlineColor // 使用自定义尺寸或配置的网格尺寸 const gridSize = customGridSize || this.config.gridSize const dimensions = new Cesium.Cartesian3( this.config.gridWidth || gridSize, this.config.gridHeight || gridSize, this.config.gridDepth || gridSize ) // 创建立方体实体 const entity = this.viewer.entities.add({ position: position, show: isOccupied ? this.gridParams.showOccupancy : this.gridParams.showIdle, box: { dimensions: dimensions, material: color, outline: true, outlineColor: outlineColor, outlineWidth: this.config.outlineWidth }, // 添加占用状态标记,用于统计 properties: { isOccupied: isOccupied, gridType: isOccupied ? 'occupied' : 'free', createdAt: new Date().toISOString(), gridIndex: gridIndex // 存储网格索引号 } }) this.gridEntities.push(entity) if (isOccupied && this.config.enableLogging) { const indexStr = gridIndex ? `索引[${gridIndex.x},${gridIndex.y},${gridIndex.z}] ` : '' console.log(`创建占用网格 ${indexStr}在位置: [${Cesium.Math.toDegrees(gridCenter.longitude).toFixed(6)}, ${Cesium.Math.toDegrees(gridCenter.latitude).toFixed(6)}, ${gridCenter.height.toFixed(1)}]`) } } // ================================ // 清除网格 // ================================ clearGrid () { // 移除所有网格实体 this.gridEntities.forEach(entity => { this.viewer.entities.remove(entity) }) this.gridEntities = [] // 清除瓦片边界框数据 this.tilesBoundingBoxes = [] console.log('已清除3D占用网格') } // ================================ // 切换网格显示 // ================================ toggleGridVisibility (visible) { this.gridEntities.forEach(entity => { entity.show = visible }) console.log(`3D占用网格${visible ? '显示' : '隐藏'}`) } // ================================ // 获取网格统计信息 - 使用配置参数 // ================================ getGridStatistics () { const totalGrids = this.gridEntities.length const occupiedGrids = this.gridEntities.filter(entity => entity.properties && entity.properties.isOccupied ).length const freeGrids = totalGrids - occupiedGrids const occupancyRate = totalGrids > 0 ? Math.round((occupiedGrids / totalGrids) * 100) : 0 if (this.config.enableLogging) { console.log(`网格统计: 总计${totalGrids}, 占用${occupiedGrids}, 空闲${freeGrids}, 占用率${occupancyRate}%`) console.log(`网格配置: 尺寸${this.config.gridWidth}x${this.config.gridHeight}x${this.config.gridDepth}m, 扩展${this.config.heightExtension}m(垂直)/${this.config.widthExtension}m(水平)`) } return { total: totalGrids, occupied: occupiedGrids, free: freeGrids, occupancyRate: occupancyRate, config: this.getConfig() } } // ================================ // 获取占用网格数量 // ================================ getOccupiedGridCount () { return this.gridEntities.filter(entity => entity.properties && entity.properties.isOccupied ).length } // 只显示占用 sheGridDisplay () { this.gridEntities.forEach(entity => { const isOccupied = entity.properties.isOccupied?._value entity.show = isOccupied ? this.gridParams.showOccupancy : this.gridParams.showIdle }) } // ================================ // 将OrientedBoundingBox转换为轴对齐边界框 // ================================ orientedBoundingBoxToAxisAligned (obb) { const center = obb.center const halfAxes = obb.halfAxes // 计算8个顶点 const vertices = [] const directions = [ [-1, -1, -1], [-1, -1, 1], [-1, 1, -1], [-1, 1, 1], [1, -1, -1], [1, -1, 1], [1, 1, -1], [1, 1, 1] ] // 提取三个轴向的向量 const xAxis = new Cesium.Cartesian3() const yAxis = new Cesium.Cartesian3() const zAxis = new Cesium.Cartesian3() Cesium.Matrix3.getColumn(halfAxes, 0, xAxis) Cesium.Matrix3.getColumn(halfAxes, 1, yAxis) Cesium.Matrix3.getColumn(halfAxes, 2, zAxis) directions.forEach(dir => { const vertex = Cesium.Cartesian3.clone(center) Cesium.Cartesian3.add(vertex, Cesium.Cartesian3.multiplyByScalar(xAxis, dir[0], new Cesium.Cartesian3()), vertex) Cesium.Cartesian3.add(vertex, Cesium.Cartesian3.multiplyByScalar(yAxis, dir[1], new Cesium.Cartesian3()), vertex) Cesium.Cartesian3.add(vertex, Cesium.Cartesian3.multiplyByScalar(zAxis, dir[2], new Cesium.Cartesian3()), vertex) vertices.push(vertex) }) // 转换为地理坐标,找到边界范围 let minLon = Infinity, maxLon = -Infinity let minLat = Infinity, maxLat = -Infinity let minHeight = Infinity, maxHeight = -Infinity vertices.forEach(vertex => { const cartographic = Cesium.Cartographic.fromCartesian(vertex) 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 { center: center, minLon: minLon, maxLon: maxLon, minLat: minLat, maxLat: maxLat, minHeight: minHeight, maxHeight: maxHeight } } // ================================ // 将BoundingSphere转换为轴对齐边界框 // ================================ sphereToAxisAligned (sphere) { const center = sphere.center const radius = sphere.radius // 将球心转换为地理坐标 const centerCartographic = Cesium.Cartographic.fromCartesian(center) const centerLon = Cesium.Math.toDegrees(centerCartographic.longitude) const centerLat = Cesium.Math.toDegrees(centerCartographic.latitude) const centerHeight = centerCartographic.height // 估算经纬度范围(简化处理) const deltaLon = radius / (111320 * Math.cos(centerCartographic.latitude)) // 经度差 const deltaLat = radius / 110540 // 纬度差 return { center: center, minLon: centerLon - deltaLon, maxLon: centerLon + deltaLon, minLat: centerLat - deltaLat, maxLat: centerLat + deltaLat, minHeight: centerHeight - radius, maxHeight: centerHeight + radius } } } export default OccupancyGrid src/views/gridManagement/GridSettings/PathPlanning.js
New file @@ -0,0 +1,1058 @@ // ================================ // A*航线规划模块 - 基于3D栅格索引 // ================================ import * as Cesium from 'cesium' class PathPlanning { constructor(viewer, occupancyGrid) { this.viewer = viewer; this.occupancyGrid = occupancyGrid; this.pathEntities = []; this.isEnabled = false; // 路径配置 this.config = { pathColor: Cesium.Color.YELLOW.withAlpha(0.8), // 路径颜色(黄色) pathOutlineColor: Cesium.Color.ORANGE, // 路径边框颜色 startPointColor: Cesium.Color.BLUE.withAlpha(0.9), // 起点颜色(蓝色) endPointColor: Cesium.Color.BLUE.withAlpha(0.9), // 终点颜色(蓝色) startEndOutlineColor: Cesium.Color.DARKBLUE, // 起点终点边框颜色 outlineWidth: 2, // 边框宽度 enableDiagonal: true, // 是否允许对角线移动(3D中为26连通性) verticalWeight: 1.2 // 垂直移动权重(略高于水平移动) }; // 显示控制 this.showOnlyPath = false; // 是否只显示路径单元 this.hasActivePath = false; // 是否有活跃的路径 // 3D栅格数据结构 this.grid3D = null; // 3维数组存储占用状态 this.gridDimensions = { width: 0, height: 0, depth: 0 }; // 栅格尺寸 this.gridEntityMap = new Map(); // 栅格索引(字符串)到实体的映射 this.indexToEntityMap = new Map(); // 栅格索引对象到实体的映射 // A*算法相关 this.openList = []; // 开放列表 this.closedList = []; // 关闭列表 } // ================================ // 启用/禁用路径规划功能 // ================================ setEnabled(enabled) { this.isEnabled = enabled; if (!enabled) { this.clearPath(); } } // ================================ // A*路径规划主函数 - 完全基于3D栅格索引 // ================================ async planPath() { if (!this.isEnabled || !this.occupancyGrid.gridEntities.length) { console.warn('路径规划未启用或网格未生成'); return; } try { console.log('开始基于3D栅格索引的A*路径规划...'); // 1. 构建3D栅格索引结构和占用状态数组 this.build3DGridStructure(); // 2. 通过最近邻查找获取航线起点和终点对应的3D栅格索引 const { startIndex, endIndex } = await this.findWaypointGridIndices(); if (!startIndex || !endIndex) { console.warn('无法找到航线起点或终点对应的栅格索引'); alert('无法找到航线起点或终点对应的栅格索引!'); return; } console.log('起点栅格索引:', startIndex); console.log('终点栅格索引:', endIndex); console.log('3D栅格维度:', this.gridDimensions); // 3. 首先显示起点和终点(蓝色) console.log('首先显示起点和终点...'); this.visualizeStartEndPoints(startIndex, endIndex); // 等待一小段时间让用户看到起点和终点 await new Promise(resolve => setTimeout(resolve, 1000)); // 4. 在三维数组上执行A*路径规划 console.log('开始执行A*路径规划算法...'); const pathIndices = this.aStarOnGrid3D(startIndex, endIndex); if (pathIndices && pathIndices.length > 0) { // 5. 通过索引找到对应的3D占用网格单元,复制并修改颜色(包含起点终点的蓝色) this.visualizePathByGridCopy(pathIndices, startIndex, endIndex); console.log(`路径规划成功,路径长度:${pathIndices.length}个栅格索引`); // 更新UI this.updatePathInfo(pathIndices); } else { console.warn('未找到可行路径'); alert('未找到从起点到终点的可行路径!'); // 即使没找到路径,也保持起点终点的显示 } } catch (error) { console.error('路径规划错误:', error); alert('路径规划过程中发生错误,请查看控制台日志。'); } } // ================================ // 构建3D栅格索引结构和占用状态数组 - 直接使用网格实体properties数据 // ================================ build3DGridStructure() { console.log('开始构建3D栅格索引结构,直接使用网格实体properties数据...'); if (!this.occupancyGrid.gridEntities.length) { throw new Error('没有可用的占用网格实体'); } // 第一步:分析所有网格实体,从properties中提取gridIndex,确定栅格维度 let minX = Infinity, minY = Infinity, minZ = Infinity; let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity; const validEntities = []; // 遍历所有网格实体,提取gridIndex信息 for (const entity of this.occupancyGrid.gridEntities) { // 直接从properties中获取gridIndex if (entity.properties && entity.properties.gridIndex) { const gridIndex = entity.properties.gridIndex.getValue(); if (gridIndex && typeof gridIndex.x === 'number' && typeof gridIndex.y === 'number' && typeof gridIndex.z === 'number') { // 更新维度范围 minX = Math.min(minX, gridIndex.x); maxX = Math.max(maxX, gridIndex.x); minY = Math.min(minY, gridIndex.y); maxY = Math.max(maxY, gridIndex.y); minZ = Math.min(minZ, gridIndex.z); maxZ = Math.max(maxZ, gridIndex.z); validEntities.push(entity); } else { console.warn('网格实体properties中的gridIndex格式不正确:', gridIndex); } } else { console.warn('网格实体缺少gridIndex属性'); } } if (validEntities.length === 0) { throw new Error('没有找到包含有效gridIndex的网格实体'); } // 计算栅格维度(基于实际的索引范围) this.gridDimensions = { width: maxX - minX + 1, height: maxY - minY + 1, depth: maxZ - minZ + 1 }; console.log(`从properties提取的3D栅格维度: ${this.gridDimensions.width} x ${this.gridDimensions.height} x ${this.gridDimensions.depth}`); console.log(`索引范围: X[${minX}, ${maxX}], Y[${minY}, ${maxY}], Z[${minZ}, ${maxZ}]`); // 第二步:初始化3D占用状态数组 this.grid3D = new Array(this.gridDimensions.width); for (let x = 0; x < this.gridDimensions.width; x++) { this.grid3D[x] = new Array(this.gridDimensions.height); for (let y = 0; y < this.gridDimensions.height; y++) { this.grid3D[x][y] = new Array(this.gridDimensions.depth); for (let z = 0; z < this.gridDimensions.depth; z++) { this.grid3D[x][y][z] = { occupied: false, entity: null, exists: false }; } } } // 第三步:直接使用实体properties数据填充3D数组 this.gridEntityMap.clear(); this.indexToEntityMap.clear(); let mappedCount = 0; let occupiedCount = 0; let freeCount = 0; // 记录索引偏移量,将原始索引映射到数组索引 this.indexOffset = { x: minX, y: minY, z: minZ }; for (const entity of validEntities) { const gridIndex = entity.properties.gridIndex.getValue(); const isOccupied = entity.properties.isOccupied ? entity.properties.isOccupied.getValue() : false; // 转换为数组索引(相对于最小值的偏移) const arrayX = gridIndex.x - minX; const arrayY = gridIndex.y - minY; const arrayZ = gridIndex.z - minZ; // 确保索引在有效范围内 if (arrayX >= 0 && arrayX < this.gridDimensions.width && arrayY >= 0 && arrayY < this.gridDimensions.height && arrayZ >= 0 && arrayZ < this.gridDimensions.depth) { // 存储到3D数组 this.grid3D[arrayX][arrayY][arrayZ] = { occupied: isOccupied, entity: entity, exists: true, originalIndex: { x: gridIndex.x, y: gridIndex.y, z: gridIndex.z } // 保存原始索引 }; // 建立映射关系 const gridKey = `${arrayX},${arrayY},${arrayZ}`; this.gridEntityMap.set(gridKey, entity); this.indexToEntityMap.set(`${arrayX}_${arrayY}_${arrayZ}`, entity); mappedCount++; if (isOccupied) { occupiedCount++; } else { freeCount++; } } else { console.warn(`网格索引超出范围: 原始(${gridIndex.x}, ${gridIndex.y}, ${gridIndex.z}) -> 数组(${arrayX}, ${arrayY}, ${arrayZ})`); } } console.log(`3D栅格结构构建完成:`); console.log(`- 栅格维度: ${this.gridDimensions.width} x ${this.gridDimensions.height} x ${this.gridDimensions.depth}`); console.log(`- 索引偏移: (${this.indexOffset.x}, ${this.indexOffset.y}, ${this.indexOffset.z})`); console.log(`- 映射实体数: ${mappedCount}`); console.log(`- 占用栅格: ${occupiedCount}`); console.log(`- 空闲栅格: ${freeCount}`); console.log(`- 占用率: ${mappedCount > 0 ? ((occupiedCount / mappedCount) * 100).toFixed(1) : 0}%`); } // ================================ // 通过最近邻查找获取航线起点和终点对应的3D栅格索引 // ================================ async findWaypointGridIndices() { console.log('开始通过最近邻查找航线起点和终点对应的栅格索引...'); // 获取航线航点 const waypoints = this.getFlightWaypoints(); if (!waypoints || waypoints.length < 2) { console.warn('航线航点不足,无法确定起点和终点'); return { startIndex: null, endIndex: null }; } const startWaypoint = waypoints[0]; const endWaypoint = waypoints[waypoints.length - 1]; console.log('航线起点位置:', startWaypoint.position); console.log('航线终点位置:', endWaypoint.position); // 使用最近邻搜索找到对应的栅格索引 const startIndex = this.findNearestGridIndex(startWaypoint.position); const endIndex = this.findNearestGridIndex(endWaypoint.position); if (!startIndex) { console.error('无法找到航线起点对应的栅格索引'); return { startIndex: null, endIndex: null }; } if (!endIndex) { console.error('无法找到航线终点对应的栅格索引'); return { startIndex: null, endIndex: null }; } // 验证找到的栅格索引是否可通行 if (this.grid3D[startIndex.x][startIndex.y][startIndex.z].occupied) { console.warn('起点栅格被占用,尝试寻找附近的空闲栅格...'); const freeStartIndex = this.findNearestFreeGrid(startIndex); if (freeStartIndex) { console.log(`找到起点附近的空闲栅格: (${freeStartIndex.x}, ${freeStartIndex.y}, ${freeStartIndex.z})`); startIndex.x = freeStartIndex.x; startIndex.y = freeStartIndex.y; startIndex.z = freeStartIndex.z; } else { console.error('起点附近没有可通行的栅格'); return { startIndex: null, endIndex: null }; } } if (this.grid3D[endIndex.x][endIndex.y][endIndex.z].occupied) { console.warn('终点栅格被占用,尝试寻找附近的空闲栅格...'); const freeEndIndex = this.findNearestFreeGrid(endIndex); if (freeEndIndex) { console.log(`找到终点附近的空闲栅格: (${freeEndIndex.x}, ${freeEndIndex.y}, ${freeEndIndex.z})`); endIndex.x = freeEndIndex.x; endIndex.y = freeEndIndex.y; endIndex.z = freeEndIndex.z; } else { console.error('终点附近没有可通行的栅格'); return { startIndex: null, endIndex: null }; } } console.log(`成功找到起点栅格索引: (${startIndex.x}, ${startIndex.y}, ${startIndex.z})`); console.log(`成功找到终点栅格索引: (${endIndex.x}, ${endIndex.y}, ${endIndex.z})`); return { startIndex, endIndex }; } // ================================ // 最近邻搜索:找到距离指定世界坐标最近的栅格索引 - 直接使用properties数据 // ================================ findNearestGridIndex(targetPosition) { let nearestIndex = null; let minDistance = Infinity; let nearestEntity = null; // 直接遍历所有网格实体,使用properties中的gridIndex for (const entity of this.occupancyGrid.gridEntities) { if (entity.properties && entity.properties.gridIndex && entity.position) { try { // 获取栅格实体的位置 const entityPosition = entity.position.getValue(this.viewer.clock.currentTime); // 计算距离 const distance = Cesium.Cartesian3.distance(targetPosition, entityPosition); if (distance < minDistance) { minDistance = distance; nearestEntity = entity; // 直接从properties获取gridIndex,并转换为数组索引 const originalIndex = entity.properties.gridIndex.getValue(); nearestIndex = { x: originalIndex.x - this.indexOffset.x, y: originalIndex.y - this.indexOffset.y, z: originalIndex.z - this.indexOffset.z, original: originalIndex // 保存原始索引以便调试 }; } } catch (error) { console.warn('处理网格实体时出错:', error); } } } if (nearestIndex) { console.log(`找到最近栅格索引: 数组索引(${nearestIndex.x}, ${nearestIndex.y}, ${nearestIndex.z}), 原始索引(${nearestIndex.original.x}, ${nearestIndex.original.y}, ${nearestIndex.original.z}), 距离: ${minDistance.toFixed(2)}m`); // 验证找到的索引是否有效 if (!this.isValidGridIndex(nearestIndex)) { console.error('找到的栅格索引无效'); return null; } // 移除original属性,只返回数组索引 return { x: nearestIndex.x, y: nearestIndex.y, z: nearestIndex.z }; } else { console.warn('未找到最近的栅格索引'); return null; } } // ================================ // 寻找指定栅格索引附近的空闲(可通行)栅格 // ================================ findNearestFreeGrid(centerIndex) { const searchRadius = 3; // 搜索半径 for (let radius = 1; radius <= searchRadius; radius++) { for (let dx = -radius; dx <= radius; dx++) { for (let dy = -radius; dy <= radius; dy++) { for (let dz = -radius; dz <= radius; dz++) { const x = centerIndex.x + dx; const y = centerIndex.y + dy; const z = centerIndex.z + dz; if (this.isValidGridIndex({ x, y, z })) { const cell = this.grid3D[x][y][z]; if (cell.exists && !cell.occupied) { return { x, y, z }; } } } } } } return null; // 未找到空闲栅格 } // ================================ // 获取航线航点 // ================================ getFlightWaypoints() { // 尝试从UAVApp获取航线航点 if (window.uavApp && window.uavApp.waypoints && window.uavApp.waypoints.length > 0) { return window.uavApp.waypoints; } console.warn('未找到航线航点,将无法进行基于航线的路径规划'); return []; } // ================================ // 在三维数组上执行A*路径规划算法 // ================================ aStarOnGrid3D(startIndex, endIndex) { console.log('开始在三维数组上执行A*路径规划算法...'); console.log(`起点: (${startIndex.x}, ${startIndex.y}, ${startIndex.z})`); console.log(`终点: (${endIndex.x}, ${endIndex.y}, ${endIndex.z})`); // 清理算法数据结构 this.openList = []; this.closedList = []; // 创建起始节点 const startNode = { x: startIndex.x, y: startIndex.y, z: startIndex.z, g: 0, h: this.calculateHeuristic(startIndex, endIndex), f: 0, parent: null }; startNode.f = startNode.g + startNode.h; this.openList.push(startNode); let iterations = 0; const maxIterations = 50000; let verticalMoves = 0; while (this.openList.length > 0 && iterations < maxIterations) { iterations++; // 找到f值最小的节点 this.openList.sort((a, b) => a.f - b.f); const currentNode = this.openList.shift(); // 如果到达终点 if (currentNode.x === endIndex.x && currentNode.y === endIndex.y && currentNode.z === endIndex.z) { console.log(`A*算法完成,迭代次数:${iterations}`); console.log(`垂直移动次数:${verticalMoves}`); return this.reconstructPath(currentNode); } // 将当前节点移到关闭列表 this.closedList.push(currentNode); // 获取邻居节点 const neighbors = this.getNeighbors(currentNode); for (const neighbor of neighbors) { // 跳过已在关闭列表中的节点 if (this.isInClosedList(neighbor)) { continue; } // 检查是否可通行 if (!this.isTraversable(neighbor.x, neighbor.y, neighbor.z)) { continue; } // 计算移动成本 const moveCost = this.getMoveCost(currentNode, neighbor); const tentativeG = currentNode.g + moveCost; // 统计垂直移动 if (neighbor.z !== currentNode.z) { verticalMoves++; } // 检查是否已在开放列表中 const existingNode = this.findInOpenList(neighbor); if (existingNode) { if (tentativeG < existingNode.g) { existingNode.g = tentativeG; existingNode.h = this.calculateHeuristic(neighbor, endIndex); existingNode.f = existingNode.g + existingNode.h; existingNode.parent = currentNode; } } else { const newNode = { x: neighbor.x, y: neighbor.y, z: neighbor.z, g: tentativeG, h: this.calculateHeuristic(neighbor, endIndex), f: 0, parent: currentNode }; newNode.f = newNode.g + newNode.h; this.openList.push(newNode); } } // 每1000次迭代输出一次进度 if (iterations % 1000 === 0) { const openCount = this.openList.length; const closedCount = this.closedList.length; console.log(`A*算法进度: ${iterations}次迭代, 开放列表: ${openCount}, 关闭列表: ${closedCount}`); } } console.warn(`A*算法未找到路径,迭代次数:${iterations}`); return null; } // ================================ // 重构路径 // ================================ reconstructPath(endNode) { const path = []; let currentNode = endNode; while (currentNode) { path.unshift({ x: currentNode.x, y: currentNode.y, z: currentNode.z }); currentNode = currentNode.parent; } console.log(`路径重构完成,路径长度: ${path.length}个节点`); return path; } // ================================ // 计算启发式函数(3D曼哈顿距离) // ================================ calculateHeuristic(index1, index2) { const dx = Math.abs(index1.x - index2.x); const dy = Math.abs(index1.y - index2.y); const dz = Math.abs(index1.z - index2.z); // 使用3D曼哈顿距离,给垂直移动适当权重 return dx + dy + (dz * this.config.verticalWeight); } // ================================ // 获取邻居节点(3D:26连通性) // ================================ getNeighbors(currentNode) { const neighbors = []; // 3D搜索:包括所有26个方向的邻居 for (let dx = -1; dx <= 1; dx++) { for (let dy = -1; dy <= 1; dy++) { for (let dz = -1; dz <= 1; dz++) { if (dx === 0 && dy === 0 && dz === 0) continue; // 如果不允许对角线移动,只使用6连通性 if (!this.config.enableDiagonal) { const isDirectional = (Math.abs(dx) + Math.abs(dy) + Math.abs(dz)) === 1; if (!isDirectional) continue; } const newX = currentNode.x + dx; const newY = currentNode.y + dy; const newZ = currentNode.z + dz; // 检查边界 if (this.isValidGridIndex({ x: newX, y: newY, z: newZ })) { neighbors.push({ x: newX, y: newY, z: newZ }); } } } } return neighbors; } // ================================ // A*算法辅助方法 // ================================ isInClosedList(node) { return this.closedList.some(n => n.x === node.x && n.y === node.y && n.z === node.z ); } findInOpenList(node) { return this.openList.find(n => n.x === node.x && n.y === node.y && n.z === node.z ); } // ================================ // 检查位置是否可通行 // ================================ isTraversable(x, y, z) { if (!this.isValidGridIndex({ x, y, z })) { return false; } const cell = this.grid3D[x][y][z]; // 如果该位置不存在网格实体,则不可通行 if (!cell.exists) { return false; } // 如果有实体但未被占用,可通行 return !cell.occupied; } // ================================ // 计算移动成本 // ================================ getMoveCost(fromNode, toNode) { const dx = Math.abs(toNode.x - fromNode.x); const dy = Math.abs(toNode.y - fromNode.y); const dz = Math.abs(toNode.z - fromNode.z); // 计算基础移动成本 let baseCost; if (dx && dy && dz) { baseCost = Math.sqrt(3); // 3D对角线移动 } else if ((dx && dy) || (dx && dz) || (dy && dz)) { baseCost = Math.sqrt(2); // 2D对角线移动 } else { baseCost = 1; // 直线移动 } // 如果是垂直移动,应用垂直权重 if (dz > 0) { baseCost *= this.config.verticalWeight; } return baseCost; } // ================================ // 通过索引找到对应的3D占用网格单元,复制并修改为黄色 - 使用properties数据 // ================================ visualizePathByGridCopy(pathIndices, startIndex, endIndex) { console.log('开始通过复制网格单元可视化路径...'); console.log('路径索引序列:', pathIndices.map(p => `(${p.x},${p.y},${p.z})`).join(' -> ')); console.log(`路径总长度: ${pathIndices.length}个栅格索引`); // 清除之前的路径 this.clearPath(); let copiedCount = 0; let missingCount = 0; for (let i = 0; i < pathIndices.length; i++) { const gridIndex = pathIndices[i]; // 从3D数组中获取对应的网格单元 const cell = this.grid3D[gridIndex.x][gridIndex.y][gridIndex.z]; if (cell.exists && cell.entity) { // 获取原始网格实体的信息 const sourceEntity = cell.entity; const sourcePosition = sourceEntity.position.getValue(this.viewer.clock.currentTime); const sourceDimensions = sourceEntity.box.dimensions.getValue(); // 获取原始索引(用于显示和命名) const originalIndex = cell.originalIndex || { x: gridIndex.x + this.indexOffset.x, y: gridIndex.y + this.indexOffset.y, z: gridIndex.z + this.indexOffset.z }; // 判断是否为起点或终点 const isStartPoint = (gridIndex.x === startIndex.x && gridIndex.y === startIndex.y && gridIndex.z === startIndex.z); const isEndPoint = (gridIndex.x === endIndex.x && gridIndex.y === endIndex.y && gridIndex.z === endIndex.z); // 根据点的类型选择颜色和名称 let entityColor, outlineColor, entityName, pointType; if (isStartPoint) { entityColor = this.config.startPointColor; outlineColor = this.config.startEndOutlineColor; entityName = `StartPoint_${originalIndex.x}_${originalIndex.y}_${originalIndex.z}`; pointType = '起点'; } else if (isEndPoint) { entityColor = this.config.endPointColor; outlineColor = this.config.startEndOutlineColor; entityName = `EndPoint_${originalIndex.x}_${originalIndex.y}_${originalIndex.z}`; pointType = '终点'; } else { entityColor = this.config.pathColor; outlineColor = this.config.pathOutlineColor; entityName = `PathGrid_${originalIndex.x}_${originalIndex.y}_${originalIndex.z}`; pointType = '路径'; } // 复制网格单元,修改颜色 const pathEntity = this.viewer.entities.add({ name: entityName, position: sourcePosition, box: { dimensions: sourceDimensions, material: entityColor, outline: true, outlineColor: outlineColor, outlineWidth: this.config.outlineWidth }, properties: { isPathGrid: true, gridIndex: originalIndex, // 使用原始索引 arrayIndex: { x: gridIndex.x, y: gridIndex.y, z: gridIndex.z }, // 保存数组索引 sourceEntityId: sourceEntity.id, pathStep: copiedCount, pointType: pointType } }); this.pathEntities.push(pathEntity); copiedCount++; console.log(`复制网格单元 数组索引[${gridIndex.x}, ${gridIndex.y}, ${gridIndex.z}] 原始索引[${originalIndex.x}, ${originalIndex.y}, ${originalIndex.z}] -> ${pointType}网格`); } else { missingCount++; console.warn(`网格索引 [${gridIndex.x}, ${gridIndex.y}, ${gridIndex.z}] 对应的网格单元不存在`); } } // 设置路径状态 this.hasActivePath = copiedCount > 0; // 如果启用了只显示路径模式,隐藏其他网格 if (this.showOnlyPath && this.hasActivePath) { this.updateGridVisibility(); } console.log(`路径可视化完成:`); console.log(`- 成功复制网格: ${copiedCount}个`); console.log(`- 缺失网格: ${missingCount}个`); // 显示起点和终点的原始索引 const startOriginalIndex = this.grid3D[startIndex.x][startIndex.y][startIndex.z].originalIndex || { x: startIndex.x + this.indexOffset.x, y: startIndex.y + this.indexOffset.y, z: startIndex.z + this.indexOffset.z }; const endOriginalIndex = this.grid3D[endIndex.x][endIndex.y][endIndex.z].originalIndex || { x: endIndex.x + this.indexOffset.x, y: endIndex.y + this.indexOffset.y, z: endIndex.z + this.indexOffset.z }; console.log(`- 起点: 数组索引(${startIndex.x}, ${startIndex.y}, ${startIndex.z}) 原始索引(${startOriginalIndex.x}, ${startOriginalIndex.y}, ${startOriginalIndex.z}) -> 蓝色`); console.log(`- 终点: 数组索引(${endIndex.x}, ${endIndex.y}, ${endIndex.z}) 原始索引(${endOriginalIndex.x}, ${endOriginalIndex.y}, ${endOriginalIndex.z}) -> 蓝色`); if (missingCount > 0) { console.warn(`⚠️ 有${missingCount}个路径索引对应的网格单元缺失`); } // 统计路径的3D特征 this.analyzePathCharacteristics(pathIndices); } // ================================ // 分析路径特征 // ================================ analyzePathCharacteristics(pathIndices) { if (!pathIndices || pathIndices.length === 0) { return; } console.log('=== 路径特征分析 ==='); // 计算路径的空间范围 const xValues = pathIndices.map(p => p.x); const yValues = pathIndices.map(p => p.y); const zValues = pathIndices.map(p => p.z); const xRange = [Math.min(...xValues), Math.max(...xValues)]; const yRange = [Math.min(...yValues), Math.max(...yValues)]; const zRange = [Math.min(...zValues), Math.max(...zValues)]; console.log(`X轴范围: [${xRange[0]}, ${xRange[1]}], 跨度: ${xRange[1] - xRange[0]}`); console.log(`Y轴范围: [${yRange[0]}, ${yRange[1]}], 跨度: ${yRange[1] - yRange[0]}`); console.log(`Z轴范围: [${zRange[0]}, ${zRange[1]}], 跨度: ${zRange[1] - zRange[0]}`); // 统计移动类型 let verticalMoves = 0; let horizontalMoves = 0; let diagonalMoves = 0; for (let i = 1; i < pathIndices.length; i++) { const prev = pathIndices[i - 1]; const curr = pathIndices[i]; const dx = Math.abs(curr.x - prev.x); const dy = Math.abs(curr.y - prev.y); const dz = Math.abs(curr.z - prev.z); if (dz > 0) verticalMoves++; if (dx > 0 || dy > 0) horizontalMoves++; if ((dx && dy) || (dx && dz) || (dy && dz)) diagonalMoves++; } const totalMoves = pathIndices.length - 1; console.log(`总移动步数: ${totalMoves}`); console.log(`垂直移动: ${verticalMoves} (${(verticalMoves/totalMoves*100).toFixed(1)}%)`); console.log(`水平移动: ${horizontalMoves} (${(horizontalMoves/totalMoves*100).toFixed(1)}%)`); console.log(`对角移动: ${diagonalMoves} (${(diagonalMoves/totalMoves*100).toFixed(1)}%)`); // 检查3D路径特征 const hasVerticalVariation = (zRange[1] - zRange[0]) > 0; if (hasVerticalVariation) { console.log('✅ 路径包含3D垂直变化,成功进行了立体路径规划'); } else { console.log('ℹ️ 路径没有垂直变化,为平面路径'); } console.log('=== 分析完成 ==='); } // ================================ // 清除路径 // ================================ clearPath() { // 从场景中移除路径网格实体 for (const pathEntity of this.pathEntities) { if (this.viewer.entities.contains(pathEntity)) { this.viewer.entities.remove(pathEntity); } } this.pathEntities = []; this.hasActivePath = false; // 重置路径状态 // 恢复所有网格的显示 // this.updateGridVisibility(); console.log('路径已清除,所有路径网格(包括起点、终点)已从场景中移除'); } // ================================ // 更新路径信息UI // ================================ updatePathInfo(pathIndices) { const pathLengthElement = document.getElementById('pathLength'); const pathStatusElement = document.getElementById('pathStatus'); if (pathLengthElement) { pathLengthElement.textContent = pathIndices.length; } if (pathStatusElement) { pathStatusElement.textContent = '规划完成'; } } // ================================ // 设置只显示路径模式 // ================================ setShowOnlyPath(showOnly) { this.showOnlyPath = showOnly; this.updateGridVisibility(); console.log(`只显示路径模式: ${showOnly ? '启用' : '禁用'}`); } // ================================ // 更新网格显示状态 // ================================ updateGridVisibility() { if (!this.occupancyGrid.gridEntities.length) { return; } // 控制原始占用网格的显示 for (const entity of this.occupancyGrid.gridEntities) { if (entity.show !== undefined) { if (this.showOnlyPath && this.hasActivePath) { // 只显示路径模式:隐藏原始网格 entity.show = false; } else { // 正常显示模式:显示所有原始网格 entity.show = true; } } } // 控制路径网格的显示(黄色路径网格始终显示,除非被清除) for (const pathEntity of this.pathEntities) { if (pathEntity.show !== undefined) { pathEntity.show = true; // 路径网格始终显示 } } const pathGridCount = this.pathEntities.length; const originalGridCount = this.occupancyGrid.gridEntities.length; console.log(`网格显示状态已更新: ${this.showOnlyPath ? '只显示路径' : '显示全部'}`); console.log(`当前显示: 原始网格 ${this.showOnlyPath && this.hasActivePath ? 0 : originalGridCount} 个, 路径网格 ${pathGridCount} 个`); } // ================================ // 验证栅格索引是否有效 // ================================ isValidGridIndex(index) { return index && typeof index.x === 'number' && typeof index.y === 'number' && typeof index.z === 'number' && index.x >= 0 && index.x < this.gridDimensions.width && index.y >= 0 && index.y < this.gridDimensions.height && index.z >= 0 && index.z < this.gridDimensions.depth; } // ================================ // 首先显示起点和终点(蓝色) // ================================ visualizeStartEndPoints(startIndex, endIndex) { console.log('开始显示起点和终点...'); // 清除之前的路径(如果有) this.clearPath(); // 显示起点(蓝色) this.createPointVisualization(startIndex, 'start'); // 显示终点(蓝色) this.createPointVisualization(endIndex, 'end'); console.log(`起点和终点已显示为蓝色:`); console.log(`- 起点: (${startIndex.x}, ${startIndex.y}, ${startIndex.z})`); console.log(`- 终点: (${endIndex.x}, ${endIndex.y}, ${endIndex.z})`); } // ================================ // 创建单个点的可视化(起点或终点) - 使用properties数据 // ================================ createPointVisualization(gridIndex, pointType) { // 从3D数组中获取对应的网格单元 const cell = this.grid3D[gridIndex.x][gridIndex.y][gridIndex.z]; if (cell.exists && cell.entity) { // 获取原始网格实体的信息 const sourceEntity = cell.entity; const sourcePosition = sourceEntity.position.getValue(this.viewer.clock.currentTime); const sourceDimensions = sourceEntity.box.dimensions.getValue(); // 获取原始索引(用于显示和命名) const originalIndex = cell.originalIndex || { x: gridIndex.x + this.indexOffset.x, y: gridIndex.y + this.indexOffset.y, z: gridIndex.z + this.indexOffset.z }; // 根据点类型设置属性 let entityColor, outlineColor, entityName, pointTypeChinese; if (pointType === 'start') { entityColor = this.config.startPointColor; outlineColor = this.config.startEndOutlineColor; entityName = `StartPoint_${originalIndex.x}_${originalIndex.y}_${originalIndex.z}`; pointTypeChinese = '起点'; } else if (pointType === 'end') { entityColor = this.config.endPointColor; outlineColor = this.config.startEndOutlineColor; entityName = `EndPoint_${originalIndex.x}_${originalIndex.y}_${originalIndex.z}`; pointTypeChinese = '终点'; } // 创建点的可视化实体 const pointEntity = this.viewer.entities.add({ name: entityName, position: sourcePosition, box: { dimensions: sourceDimensions, material: entityColor, outline: true, outlineColor: outlineColor, outlineWidth: this.config.outlineWidth }, properties: { isPathGrid: true, gridIndex: originalIndex, // 使用原始索引 arrayIndex: { x: gridIndex.x, y: gridIndex.y, z: gridIndex.z }, // 保存数组索引 sourceEntityId: sourceEntity.id, pointType: pointTypeChinese } }); this.pathEntities.push(pointEntity); console.log(`创建${pointTypeChinese}可视化: 数组索引[${gridIndex.x}, ${gridIndex.y}, ${gridIndex.z}] 原始索引[${originalIndex.x}, ${originalIndex.y}, ${originalIndex.z}]`); } else { console.warn(`网格索引 [${gridIndex.x}, ${gridIndex.y}, ${gridIndex.z}] 对应的网格单元不存在,无法创建${pointType}可视化`); } } // ================================ // 设置路径实体透明度 // ================================ setPathAlpha(alpha) { if (alpha < 0.1 || alpha > 1.0) { console.warn('透明度值应该在0.1到1.0之间'); return; } console.log(`设置路径透明度为: ${alpha}`); // 更新配置中的颜色透明度 this.config.pathColor = Cesium.Color.YELLOW.withAlpha(alpha); this.config.startPointColor = Cesium.Color.BLUE.withAlpha(alpha); this.config.endPointColor = Cesium.Color.BLUE.withAlpha(alpha); // 更新所有现有的路径实体透明度 let updatedCount = 0; for (const pathEntity of this.pathEntities) { if (pathEntity.box && pathEntity.box.material) { try { // 获取实体的类型 const pointType = pathEntity.properties && pathEntity.properties.pointType ? pathEntity.properties.pointType.getValue() : '路径'; // 根据实体类型设置对应的颜色 let newColor; if (pointType === '起点' || pointType === '终点') { newColor = Cesium.Color.BLUE.withAlpha(alpha); } else { newColor = Cesium.Color.YELLOW.withAlpha(alpha); } // 更新实体的材质颜色 pathEntity.box.material = newColor; updatedCount++; } catch (error) { console.warn('更新路径实体透明度时出错:', error); } } } console.log(`成功更新${updatedCount}个路径实体的透明度`); } // ================================ // 获取当前路径透明度 // ================================ getPathAlpha() { // 从配置中获取当前透明度 if (this.config.pathColor && this.config.pathColor.alpha !== undefined) { return this.config.pathColor.alpha; } return 0.8; // 默认透明度 } } // ================================ // 导出模块(如果使用模块系统) // ================================ if (typeof module !== 'undefined' && module.exports) { module.exports = PathPlanning; } export default PathPlanning src/views/gridManagement/gridManagement.vue
New file @@ -0,0 +1,623 @@ <template> <div class="gridManagement"> <div class="search-box"> <el-form :model="params" inline> <div style="display: flex;justify-content: space-between"> <div> <el-form-item label="网格名称:"> <el-input v-model="params.gridName" placeholder="请输入网格名称" clearable /> </el-form-item> </div> <div> <el-form-item> <el-button type="primary" @click="getList">搜索</el-button> <el-button @click="cancelSearch">取消</el-button> </el-form-item> </div> </div> </el-form> <div> <el-button type="primary" icon="el-icon-plus" @click="handleAdd">新增网格</el-button> </div> </div> <div class="mange-table"> <el-table border :data="tableList" class="custom-header"> <el-table-column label="序号" type="index" width="60"></el-table-column> <el-table-column prop="grid_name" label="网格名称" align="center" show-overflow-tooltip></el-table-column> <el-table-column prop="start_point" label="起点" align="center" show-overflow-tooltip></el-table-column> <el-table-column prop="end_point" label="终点" align="center" show-overflow-tooltip></el-table-column> <el-table-column prop="create_time" label="创建时间" align="center"></el-table-column> <el-table-column prop="nick_name" label="创建人" align="center"></el-table-column> <el-table-column label="操作" width="180" align="center"> <template #default="scope"> <!-- <el-button icon="el-icon-view" type="text" @click="handleDetail(scope.row)">查看</el-button> --> <el-button icon="el-icon-edit" type="text" @click="handleEdit(scope.row)">编辑</el-button> <el-button icon="el-icon-delete" type="text" @click="handleDelete(scope.row)">删除</el-button> </template> </el-table-column> </el-table> </div> <div class="pagination"> <el-pagination class="ztzf-pagination" popper-class="custom-pagination-dropdown" background :page-sizes="[10, 20, 30, 40, 50, 100]" :size="size" v-model:current-page="params.current" v-model:page-size="params.size" layout="total, sizes, prev, pager, next, jumper" :total="total" @size-change="handleSizeChange" @current-change="handleCurrentChange" /> </div> </div> <el-dialog class="ztzf-dialog" append-to-body v-model="isShowEditView" :title="titleTxt" :width="pxToRem(1000)" :close-on-click-modal="false" :destroy-on-close="true" @close="handleClose"> <div class="content-edit"> <el-form ref="ruleFormRef" :model="editParams" :rules="rules" inline> <el-form-item label="网格名称" prop="grid_name"> <el-input v-model="editParams.grid_name" /> </el-form-item> </el-form> <div class="map-container"> <div id="GridManagementMap" class="ztzf-cesium"></div> </div> <div class="btns"> <el-button type="primary" @click="meshAnalysis"><el-icon><Location /></el-icon>开启网格</el-button> <el-button type="primary" @click="clearGrid"><el-icon><DeleteLocation /></el-icon>清除网格</el-button> <el-button v-if="titleTxt === '新增'" type="primary" @click="submit(ruleFormRef)"><el-icon><CirclePlus /></el-icon>确认</el-button> <el-button v-else type="primary" @click="submit(ruleFormRef)"><el-icon><CircleCheck /></el-icon>修改</el-button> <el-button @click="isShowEditView = false"><el-icon><CircleClose /></el-icon>取消</el-button> </div> <div v-show="showContextMenu" class="context-menu" :style="contextMenuStyle"> <div class="menu-item" @click="clearAllPoints">清空网格</div> </div> </div> </el-dialog> </template> <script setup> import { airGridPage, airGridUpdate, airGridAdd, airGridDelete } from '@/api/airspace/airspace'; import { ElMessage, ElMessageBox, ElLoading } from 'element-plus'; import * as Cesium from 'cesium'; import { PublicCesium } from '@/utils/cesium/publicCesium' import OccupancyGrid from './GridSettings/OccupancyGrid' import PathPlanning from './GridSettings/PathPlanning' import newStartPoint from '@/assets/images/newStartPoint.png' import { cartesian3Convert,getLnglatAltitude } from '@/utils/cesium/mapUtil' import { ArrowLineMaterialProperty } from '@/utils/cesium/Material' import { getPolyLine } from '@/views/RoutePlan/PointAirLine/pointWayLineUtils' let arrowLineMaterialProperty = new ArrowLineMaterialProperty({ color: new Cesium.Color(128 / 255, 215 / 255, 255 / 255, 1), directionColor: new Cesium.Color(1, 1, 1, 1), outlineColor: new Cesium.Color(1, 1, 1, 1), outlineWidth: 0, speed: 5, }) const total = ref(0) const params = ref({ current: 1, size: 10, gridName: '', }); let titleTxt = ref('新增') let tableList = ref([]) let isShowView = ref(false) let rowView = ref({}) const showContextMenu = ref(false) const contextMenuStyle = ref({ left: '0px', top: '0px', }) let isShowEditView = ref(false) const ruleFormRef = ref() let editParams = ref({ id: '', grid_name: '', start_point: '', end_point: '', }) const rules = reactive({ grid_name: [ { required: true, message: '请输入网格名称', trigger: 'blur', }, ], }) function cancelSearch() { params.value = { id: '', gridName: '', } params.value.current = 1 getList() } function getList() { airGridPage(params.value).then(res => { tableList.value = res.data.data.records || [] total.value = res.data.data.total || 0 }) } function handleDelete (row) { ElMessageBox.confirm('确定删除吗?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning', }) .then(() => { airGridDelete(row.id).then(res => { ElMessage.success('删除成功') getList() }).catch(error => { ElMessage.error('删除失败'); }); }) .catch(() => { ElMessage({ type: 'info', message: '已取消删除', }) }); } async function handleAdd() { titleTxt.value = '新增' editParams.value.grid_name = '' isShowEditView.value = true await nextTick() initMap() } async function handleEdit(row) { titleTxt.value = '编辑' isShowEditView.value = true editParams.value = { ...row } const startPoint = row.start_point.split(',') const endPoint = row.end_point.split(',') const height = row.grid_height.split(',') pointList.value.push({ longitude: Number(startPoint[0]), latitude: Number(startPoint[1]), height: Number(height[0]), }) pointList.value.push({ longitude: Number(endPoint[0]), latitude: Number(endPoint[1]), height: Number(height[0]), }) await nextTick() initMap() } function handleSizeChange(val) { params.value.size = val getList() } function handleCurrentChange(val) { params.value.current = val getList() } async function submit(formValidate) { if (!formValidate) return await formValidate.validate((valid, fields) => { if (valid) { if (pointList.value.length === 0) { ElMessage.warning('请先添加起点和终点') return } const params = { id: editParams.value.id, grid_name: editParams.value.grid_name, start_point: `${pointList.value[0].longitude},${pointList.value[0].latitude}`, end_point: `${pointList.value[1].longitude},${pointList.value[1].latitude}`, grid_height: `${pointList.value[0].height},${pointList.value[1].height}`, } if (titleTxt.value === '新增') { airGridAdd(params).then(res => { isShowEditView.value = false ElMessage.success('新增成功') getList() }) } else { airGridUpdate(params).then(res => { isShowEditView.value = false ElMessage.success('操作成功') getList() }) } } }) } let publicCesiumInstance = null let viewer = null const viewInstance = shallowRef(null); let handler = null let occupancyGrid = null // 地图 const initMap = async () => { if (!document.getElementById('GridManagementMap')) { return } publicCesiumInstance = new PublicCesium({ dom: 'GridManagementMap', flyToContour: false, flatMode: false, terrain: true, layerMode: 4, contour: true, }) viewer = publicCesiumInstance.getViewer() viewInstance.value = publicCesiumInstance; viewer.scene.globe.depthTestAgainstTerrain = true; // viewer.flyTo({latitude: 28.624613214568868,longitude:115.85669891599704}, 4, 65) viewer?.camera.flyTo({ destination: Cesium.Cartesian3.fromDegrees(115.85669891599704, 28.624613214568868, 1000), duration: 1, orientation: { heading: Cesium.Math.toRadians(0.0), pitch: Cesium.Math.toRadians(-90.0), roll: 0.0, }, }); addCustomLayers(tilesTreeData.value.title, tilesTreeData.value) occupancyGrid = new OccupancyGrid(viewer, gridConfig, gridParams) // 添加事件 handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas) handler.setInputAction(singleClickEvent, Cesium.ScreenSpaceEventType.LEFT_CLICK) handler.setInputAction(click => { // 获取鼠标在页面上的坐标 const mousePosition = { x: event.clientX, y: event.clientY, } showContextMenu.value = true contextMenuStyle.value = { left: mousePosition.x + 'px', top: mousePosition.y + 'px', position: 'fixed', // 确保使用固定定位 zIndex: 9999, // 确保菜单显示在最上层 } }, Cesium.ScreenSpaceEventType.RIGHT_CLICK) cesiumContextMenu(false) if (titleTxt.value === '编辑') { ShowTwoPoints() // meshAnalysis() } } // 倾斜摄影 const tilesTreeData = ref( { id: '1', label: '空间大厦', type: 'layer-map', title: 'kjds_3Dtile', mapType: 'Cesium3DTile', src: '/3Dtile/kjds/tileset.json', }, ) const gridParams = reactive({ hasGrid: false, //生成了网格 hasRoute: false, //生成了航线 showIdle: true, //显示占用网格 showOccupancy: true, //显示空闲网格 }) // 初始化占用网格 - 使用自定义配置 const gridConfig = { // 网格尺寸配置 gridSize: 6, gridWidth: 12, gridHeight: 12, gridDepth: 12, // 区域扩展配置(确保4层网格生成) heightExtension: 72, // 向上向下各扩展120米,确保4层网格(总高度240米,4×50=200米,留有余量) widthExtension: 12, // 水平方向扩展100米 // 颜色配置 occupiedColor: Cesium.Color.RED.withAlpha(0.05), freeColor: Cesium.Color.GREEN.withAlpha(0.05), occupiedOutlineColor: Cesium.Color.DARKRED, freeOutlineColor: Cesium.Color.DARKGREEN, outlineWidth: 1, // 性能配置100000 maxGridCount: 500000, enableBatching: true, // 显示配置 showOccupiedOnly: false, showFreeOnly: false, enableLogging: true, } const addCustomLayers = async (layerName, options) => { if (options.mapType === 'arcgis') { let imageryProvider = new Cesium.WebMapTileServiceImageryProvider({ url: options.src, layer: options.title, style: 'default', format: 'image/png', tileMatrixSetID: 'default', tilingScheme: new Cesium.GeographicTilingScheme(), tileMatrixLabels: [ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', ], }) viewer?.imageryLayers.addImageryProvider(imageryProvider) } if (options.mapType === 'Cesium3DTile') { const tileset = await Cesium.Cesium3DTileset.fromUrl(options.src) viewer?.scene.primitives.add(tileset) } } let pointList = ref([]) async function singleClickEvent(movement) { cesiumContextMenu(false) if (pointList.value.length === 2) { ElMessage.warning('最多只能添加两个点,可以先清空') return } // 先清除point- viewer.entities.removeAll() const { longitude, latitude } = cartesian3Convert(viewer.scene.pickPosition(movement.position), viewer) let result = await getLnglatAltitude(longitude, latitude, viewer) let height = result.height + 60 const point = { longitude, latitude, height } pointList.value.push(point) ShowTwoPoints() } // 生成两个点 function ShowTwoPoints() { pointList.value.map((item, index) => { const position = Cesium.Cartesian3.fromDegrees( Number(item.longitude), Number(item.latitude), Number(item.height) ) viewer.entities.add({ id: `point-${index}`, position: position, billboard: { image: new Cesium.ConstantProperty(newStartPoint), width: 40, height: 40, }, }) viewer?.entities.add({ id: 'placement-' + index, position: position, polyline: getPolyLine(viewer, { value: pointList.value }, index), }) }) const position1 = Cesium.Cartesian3.fromDegrees( Number(pointList.value[0].longitude), Number(pointList.value[0].latitude), Number(pointList.value[0].height) ) const position2= Cesium.Cartesian3.fromDegrees( Number(pointList.value[1].longitude), Number(pointList.value[1].latitude), Number(pointList.value[1].height) ) // 获取两点在屏幕上的位置 // const screenPos1 = Cesium.SceneTransforms?.wgs84ToWindowCoordinates?.(viewer.scene, position1) // || viewer.scene.cartesianToCanvasCoordinates(position1); // const screenPos2 = Cesium.SceneTransforms?.wgs84ToWindowCoordinates?.(viewer.scene, position2) // || viewer.scene.cartesianToCanvasCoordinates(position2); // // // 生成屏幕矩形 // const boundingRect = new Cesium.BoundingRectangle( // Math.min(screenPos1.x, screenPos2.x), // Math.min(screenPos1.y, screenPos2.y), // Math.abs(screenPos1.x - screenPos2.x), // Math.abs(screenPos1.y - screenPos2.y) // ); // const center = Cesium.Cartesian3.midpoint(screenPos1, screenPos2, new Cesium.Cartesian3()); // const angle = Cesium.Math.PI_OVER_FOUR; // 45度 // viewer.entities.add({ // position: center, // rectangle: { // coordinates: boundingRect, // material: Cesium.Color.BLUE.withAlpha(0.5), // rotation: angle // 弧度制旋转 // } // }); viewer.entities.add({ id: `line`, polyline: { width: 2, eyeOffset: new Cesium.Cartesian3(0, 0, -6), material: arrowLineMaterialProperty, clampToGround: false, positions: [position1, position2], }, }) } // 禁用浏览器默认行为处理 const preventDefault = event => { event.preventDefault() return } const cesiumContextMenu = (isAdd = true) => { let cesium = document.getElementById('GridManagementMap') if (!cesium) return if (isAdd) { cesium.addEventListener('contextmenu', preventDefault) } else { cesium.removeEventListener('contextmenu', preventDefault) } } // 生成网格 function meshAnalysis() { if (pointList.value.length !== 2) { ElMessage.warning('请先添加起点和终点') return } const pointList1 = pointList.value.map(i => { const cartographic = Cesium.Cartographic.fromDegrees(i.longitude, i.latitude, i.height) const { x, y, z } = Cesium.Cartesian3.fromRadians( cartographic.longitude, cartographic.latitude, cartographic.height ) return { ...i, position: { x, y, z } } }) window.uavApp = { waypoints: pointList1, } occupancyGrid.generateOccupancyGridWithTiles(pointList1) } // 清除网格 function clearGrid() { occupancyGrid.clearGrid() gridParams.hasGrid = false } const handleClose = () => { // 清除editParams editParams.value = { id: '', grid_name: '', grid_height: '', start_point: '', end_point: '', } pointList.value = [] publicCesiumInstance?.viewerDestroy() publicCesiumInstance = null viewer = null isShowEditView.value = false viewer?.scene.primitives.remove(tilesTreeData.value.title) cesiumContextMenu(false) } function clearAllPoints() { showContextMenu.value = false pointList.value = [] viewer?.entities.removeAll() clearGrid() } onMounted(() => { getList() }) </script> <style lang="scss" scoped> .gridManagement { height: 0; flex: 1; padding: 20px; margin: 0 10px 10px 10px; background-color: #ffffff; // padding: 10px 20px; border-radius: 5px; display: flex; flex-direction: column; .search-box { margin-top: 20px; height: 80px; } :deep(.el-input) { .el-input__wrapper { width: 200px; } } // 表格 .mange-table { height: 0; flex: 1; margin-top: 18px; overflow: auto; } :deep(.el-pagination) { display: flex; justify-content: right; } :deep(.el-pagination button) { background: center center no-repeat none !important; color: #8eb8ea !important; } } .content-edit { height: 550px; .map-container { height: 440px; #GridManagementMap { width: 100%; height: 100%; } } .btns { margin-top: 16px; display: flex; justify-content: center; } .context-menu { position: fixed; background: #0f1929; border: 1px solid #51a8ff; border-radius: 4px; padding: 4px 0; z-index: 9999; min-width: 100px; // 设置最小宽度 box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.3); .menu-item { padding: 8px 16px; color: #d3e9ff; cursor: pointer; &:hover { background: rgba(81, 168, 255, 0.1); } } } } </style> vite.config.mjs
@@ -5,7 +5,7 @@ // https://vitejs.dev/config/ export default ({ mode, command }) => { const env = loadEnv(mode, process.cwd()) const { VITE_APP_ENV, VITE_APP_BASE, VITE_APP_URL } = env const { VITE_APP_ENV, VITE_APP_BASE, VITE_APP_URL, VITE_APP_MAP_TILE_URL } = env // 判断是打生产环境包 const isProd = VITE_APP_ENV === 'production' @@ -43,7 +43,13 @@ target: VITE_APP_URL, changeOrigin: true, rewrite: path => path.replace(/^\/api/, ''), } }, '/3Dtile': { target: VITE_APP_MAP_TILE_URL, changeOrigin: true, rewrite: path => path.replace(/^\/3Dtile/, ''), }, }, }, assetsInclude: ['**/*.gltf'],