7 files modified
6 files added
1 files deleted
| New file |
| | |
| | | import * as Cesium from 'cesium' |
| | | import * as turf from '@turf/turf' |
| | | import { boxTransformScale } from '@/utils/turfFunc' |
| | | import { ElMessage } from 'element-plus' |
| | | import { flyVisual, getPointPositionsHeight } from '@/utils/cesium/mapUtil' |
| | | |
| | | /** |
| | | * 多边形绘制与编辑工具类 |
| | | * 功能: |
| | | * - 绘制多边形 |
| | | * - 拖动编辑端点 |
| | | * - 删除端点、删除整个多边形 |
| | | * - 多边形自交检查(避免非法几何) |
| | | * - 外部订阅/通知机制 |
| | | */ |
| | | export class DrawPolygon { |
| | | constructor() { |
| | | // 图斑预览模式标记 |
| | | this.isPureSpotPreview = false |
| | | // 是否删除测区 |
| | | this.isDeleteTheArea = true |
| | | //是否可以编辑图斑 |
| | | this.isPreviewMode = true |
| | | // Cesium 视图对象 |
| | | this.viewer = null |
| | | // 当前绘制的多边形 |
| | | this.curPolygon = null |
| | | // 绘制模式标识 |
| | | this.drawingMode = false |
| | | // 编辑模式标识 |
| | | this.editingMode = false |
| | | // 是否正在拖拽端点 |
| | | this.isDragging = false |
| | | // 当前拖拽的点实体 |
| | | this.draggedEntity = null |
| | | // 多边形实体 |
| | | this.polygonEntity = null |
| | | // 存储端点的 DataSource |
| | | this.editPolygonDataSource = null |
| | | this.editPolygonPointDataSource = null |
| | | // 存储中点(边中点)的 DataSource:用于编辑态插入新端点 |
| | | this.editPolygonMidPointDataSource = null |
| | | // 鼠标事件处理器 |
| | | this.handler = null |
| | | // 右键菜单 DOM |
| | | this.menuPopup = null |
| | | // 被右键选中的点 |
| | | this.delPolygonPoint = null |
| | | // 是否显示警告提示(自交) |
| | | this.isShowWaringTip = false |
| | | // 当前拖拽点是否合法 |
| | | this.currentDragPointIsValid = false |
| | | // 当前拖拽点的坐标 |
| | | this.currentDragPointPosition = null |
| | | |
| | | // 事件回调函数绑定 this |
| | | this.handleLeftDown = this.handleLeftDown.bind(this) |
| | | this.handleLeftUp = this.handleLeftUp.bind(this) |
| | | this.handleMouseMove = this.handleMouseMove.bind(this) |
| | | this.handleLeftClick = this.handleLeftClick.bind(this) |
| | | this.handleRightClick = this.handleRightClick.bind(this) |
| | | |
| | | this.delPolygon = this.delPolygon.bind(this) |
| | | this.delPoint = this.delPoint.bind(this) |
| | | // 外部订阅者 |
| | | this.listeners = [] |
| | | } |
| | | |
| | | // 实体命名常量 |
| | | static ENTITY_NAMES = { |
| | | POLYGON: '区域-面', |
| | | POINT: '区域-端点', |
| | | // 编辑态中点(用于快速插入新端点) |
| | | MID_POINT: '区域-中点', |
| | | POLYGON_ID: 'planar-route-polygon', |
| | | POINT_ID_PREFIX: 'planar-route-point', |
| | | MID_POINT_ID_PREFIX: 'planar-route-mid-point', |
| | | } |
| | | |
| | | // 颜色常量 |
| | | static COLORS = { |
| | | DEFAULT_POLYGON: Cesium.Color.fromBytes(45, 140, 240, 99), // 默认面颜色 |
| | | DEFAULT_LINE: Cesium.Color.fromBytes(45, 140, 240, 255), // 默认边界线颜色 |
| | | ERROR_POLYGON: Cesium.Color.fromCssColorString('rgba(255, 0, 0, .3)'), // 错误(自交)面颜色 |
| | | ERROR_LINE: Cesium.Color.fromCssColorString('rgba(255, 0, 0, 1)'), // 错误(自交)线颜色 |
| | | } |
| | | |
| | | // ============ 发布订阅机制 ============ |
| | | |
| | | // 外部订阅数据变化 |
| | | subscribe(key, listener) { |
| | | this.listeners.push({ key, listener }) |
| | | } |
| | | |
| | | // 通知订阅者 |
| | | notify(key, data) { |
| | | this.listeners.filter(subscriber => subscriber.key === key).forEach(subscriber => subscriber.listener(data)) |
| | | } |
| | | |
| | | // ============ 绘制相关 ============ |
| | | // 编辑图斑 |
| | | editThePatch(data) { |
| | | this.isPreviewMode = data |
| | | // 关闭编辑能力时隐藏端点/中点,并清空中点,避免残留交互 |
| | | if (!this.isPreviewMode) { |
| | | this.editPolygonPointDataSource && (this.editPolygonPointDataSource.entities.show = false) |
| | | this.editPolygonMidPointDataSource && (this.editPolygonMidPointDataSource.entities.show = false) |
| | | this.editPolygonMidPointDataSource?.entities?.removeAll?.() |
| | | return |
| | | } |
| | | |
| | | if (this.editingMode) { |
| | | this.editPolygonPointDataSource && (this.editPolygonPointDataSource.entities.show = true) |
| | | this.editPolygonMidPointDataSource && (this.editPolygonMidPointDataSource.entities.show = true) |
| | | this.rebuildEditPoints() |
| | | this.rebuildMidPoints() |
| | | } |
| | | } |
| | | // 删除测区 |
| | | deleteTheArea(data) { |
| | | this.isDeleteTheArea = data |
| | | } |
| | | |
| | | // 开始绘制 |
| | | startDrawing() { |
| | | this.drawingMode = true |
| | | this.curPolygon = new Cesium.PolygonHierarchy() |
| | | |
| | | // 如果还没有 DataSource,就新建一个 |
| | | if (!this.editPolygonDataSource) { |
| | | this.editPolygonDataSource = new Cesium.CustomDataSource('editPolygonDataSource') |
| | | this.viewer?.dataSources.add(this.editPolygonDataSource) |
| | | } |
| | | |
| | | if (!this.editPolygonPointDataSource) { |
| | | this.editPolygonPointDataSource = new Cesium.CustomDataSource('editPolygonPointDataSource') |
| | | this.viewer?.dataSources.add(this.editPolygonPointDataSource) |
| | | } |
| | | |
| | | // 中点数据源:编辑模式下用于“边上插点” |
| | | if (!this.editPolygonMidPointDataSource) { |
| | | this.editPolygonMidPointDataSource = new Cesium.CustomDataSource('editPolygonMidPointDataSource') |
| | | this.viewer?.dataSources.add(this.editPolygonMidPointDataSource) |
| | | } |
| | | |
| | | // 清空之前的点 |
| | | this.editPolygonDataSource?.entities.removeAll() |
| | | this.editPolygonPointDataSource?.entities.removeAll() |
| | | this.editPolygonMidPointDataSource?.entities.removeAll() |
| | | } |
| | | |
| | | // 创建多边形实体(含边界线) |
| | | createPolygonEntity() { |
| | | this.polygonEntity = this.editPolygonDataSource.entities.add({ |
| | | name: DrawPolygon.ENTITY_NAMES.POLYGON, |
| | | id: DrawPolygon.ENTITY_NAMES.POLYGON_ID, |
| | | polygon: { |
| | | hierarchy: new Cesium.CallbackProperty(() => this.curPolygon, false), |
| | | material: DrawPolygon.COLORS.DEFAULT_POLYGON, |
| | | outline: false, |
| | | outlineWidth: 2, |
| | | heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, |
| | | }, |
| | | polyline: { |
| | | width: 2, |
| | | material: DrawPolygon.COLORS.DEFAULT_LINE, |
| | | clampToGround: true, |
| | | positions: new Cesium.CallbackProperty( |
| | | () => [...this.curPolygon.positions, this.curPolygon.positions[0]], // 闭合线 |
| | | false |
| | | ), |
| | | }, |
| | | }) |
| | | } |
| | | |
| | | // 创建端点实体 |
| | | createPointEntity(position, isAdd) { |
| | | const pointIndex = isAdd ? this.curPolygon.positions.length - 2 : this.curPolygon.positions.length - 1 |
| | | |
| | | this.editPolygonPointDataSource.entities.add({ |
| | | name: DrawPolygon.ENTITY_NAMES.POINT, |
| | | id: `${DrawPolygon.ENTITY_NAMES.POINT_ID_PREFIX}${pointIndex}`, |
| | | position: position.clone(), |
| | | point: { |
| | | pixelSize: 14, |
| | | color: Cesium.Color.WHITE, |
| | | heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, |
| | | disableDepthTestDistance: Number.POSITIVE_INFINITY, |
| | | }, |
| | | customData: { |
| | | ind: pointIndex, // 索引记录 |
| | | }, |
| | | }) |
| | | } |
| | | |
| | | createMidPointEntity(startInd, position) { |
| | | // 使用 CallbackProperty 让中点位置随端点拖拽实时计算 |
| | | // startInd 表示该中点属于边:(startInd) -> (startInd + 1) |
| | | const updatePosition = () => { |
| | | const positions = this.curPolygon?.positions |
| | | if (!positions || positions.length < 2) return position |
| | | |
| | | const n = positions.length |
| | | const p1 = positions[startInd] |
| | | const p2 = positions[(startInd + 1) % n] |
| | | if (!p1 || !p2) return position |
| | | |
| | | return Cesium.Cartesian3.midpoint(p1, p2, new Cesium.Cartesian3()) |
| | | } |
| | | |
| | | this.editPolygonMidPointDataSource.entities.add({ |
| | | name: DrawPolygon.ENTITY_NAMES.MID_POINT, |
| | | id: `${DrawPolygon.ENTITY_NAMES.MID_POINT_ID_PREFIX}${startInd}`, |
| | | position: new Cesium.CallbackProperty(updatePosition, false), |
| | | point: { |
| | | pixelSize: 10, |
| | | color: Cesium.Color.fromCssColorString('rgba(255, 255, 255, .7)'), |
| | | heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, |
| | | disableDepthTestDistance: Number.POSITIVE_INFINITY, |
| | | }, |
| | | customData: { |
| | | startInd, |
| | | }, |
| | | }) |
| | | } |
| | | |
| | | rebuildEditPoints() { |
| | | // 插入/删除端点后:端点实体 id 与 customData.ind 需要全量重建,保证索引与 positions 一致 |
| | | if (!this.isPreviewMode) return |
| | | if (!this.editPolygonPointDataSource || !this.curPolygon?.positions) return |
| | | |
| | | this.editPolygonPointDataSource.entities.removeAll() |
| | | this.curPolygon.positions.forEach((position, index) => { |
| | | this.editPolygonPointDataSource.entities.add({ |
| | | name: DrawPolygon.ENTITY_NAMES.POINT, |
| | | id: `${DrawPolygon.ENTITY_NAMES.POINT_ID_PREFIX}${index}`, |
| | | position: position.clone(), |
| | | point: { |
| | | pixelSize: 14, |
| | | color: Cesium.Color.WHITE, |
| | | heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, |
| | | disableDepthTestDistance: Number.POSITIVE_INFINITY, |
| | | }, |
| | | customData: { |
| | | ind: index, |
| | | }, |
| | | }) |
| | | }) |
| | | } |
| | | |
| | | rebuildMidPoints() { |
| | | // 仅在“顶点数量变化/切换编辑态”时维护中点实体数量 |
| | | // 拖拽过程中中点位置由 CallbackProperty 自动更新,不需要重建 |
| | | if (!this.isPreviewMode) return |
| | | if (!this.editPolygonMidPointDataSource || !this.curPolygon?.positions) return |
| | | |
| | | const positions = this.curPolygon.positions |
| | | const n = positions.length |
| | | const entities = this.editPolygonMidPointDataSource.entities |
| | | |
| | | if (!this.editingMode || n < 2) { |
| | | entities.removeAll() |
| | | return |
| | | } |
| | | |
| | | const neededIds = new Set() |
| | | for (let i = 0; i < n; i += 1) { |
| | | const id = `${DrawPolygon.ENTITY_NAMES.MID_POINT_ID_PREFIX}${i}` |
| | | neededIds.add(id) |
| | | // 缺少则补齐:保证每条边都有一个中点实体 |
| | | if (!entities.getById(id)) { |
| | | const p1 = positions[i] |
| | | const p2 = positions[(i + 1) % n] |
| | | if (!p1 || !p2) continue |
| | | const mid = Cesium.Cartesian3.midpoint(p1, p2, new Cesium.Cartesian3()) |
| | | this.createMidPointEntity(i, mid) |
| | | } |
| | | } |
| | | |
| | | entities.values.slice().forEach(entity => { |
| | | if (entity?.name !== DrawPolygon.ENTITY_NAMES.MID_POINT) return |
| | | // 多余则移除:例如删除端点导致边数量减少 |
| | | if (!neededIds.has(entity.id)) { |
| | | entities.remove(entity) |
| | | } |
| | | }) |
| | | } |
| | | |
| | | insertPointFromMidPoint(midPointEntity) { |
| | | // 点击中点:在对应边上插入一个新端点(白点),并立即进入拖拽态 |
| | | if (!this.isPreviewMode) return |
| | | if (!this.editingMode) return |
| | | if (!midPointEntity?.customData) return |
| | | |
| | | const startInd = midPointEntity.customData.startInd |
| | | // 中点位置可能是 CallbackProperty,需要取当前时刻的值 |
| | | const positionProperty = midPointEntity.position |
| | | const entityPosition = positionProperty?.getValue |
| | | ? positionProperty.getValue(Cesium.JulianDate.now()) |
| | | : positionProperty |
| | | if (!entityPosition) return |
| | | |
| | | // 插入到 startInd 与 startInd+1 之间 |
| | | const insertIndex = Math.min(Math.max(startInd + 1, 0), this.curPolygon.positions.length) |
| | | this.curPolygon.positions.splice(insertIndex, 0, entityPosition.clone ? entityPosition.clone() : entityPosition) |
| | | |
| | | this.rebuildEditPoints() |
| | | this.rebuildMidPoints() |
| | | |
| | | const newPointEntity = this.editPolygonPointDataSource?.entities?.getById( |
| | | `${DrawPolygon.ENTITY_NAMES.POINT_ID_PREFIX}${insertIndex}` |
| | | ) |
| | | if (newPointEntity) { |
| | | this.isDragging = true |
| | | this.draggedEntity = newPointEntity |
| | | this.currentDragPointPosition = this.curPolygon.positions[insertIndex] |
| | | this.disableMapControl() |
| | | } |
| | | this.notify('getPolygonPositions', this.curPolygon.positions) |
| | | } |
| | | // 清除不在范围内的点 |
| | | removeLastInvalidPoint() { |
| | | const posLen = this.curPolygon.positions.length |
| | | if (posLen === 0) return |
| | | |
| | | let targetPointId = '' |
| | | let removeCount = 0 |
| | | |
| | | if (posLen === 2) { |
| | | removeCount = 2 |
| | | targetPointId = `${DrawPolygon.ENTITY_NAMES.POINT_ID_PREFIX}0` |
| | | } else if (posLen > 2) { |
| | | removeCount = 1 |
| | | const pointIndex = posLen - 2 |
| | | targetPointId = `${DrawPolygon.ENTITY_NAMES.POINT_ID_PREFIX}${pointIndex}` |
| | | } |
| | | |
| | | this.curPolygon.positions.splice(posLen - removeCount, removeCount) |
| | | |
| | | const invalidPoint = this.editPolygonPointDataSource.entities.getById(targetPointId) |
| | | if (invalidPoint) { |
| | | this.editPolygonPointDataSource.entities.remove(invalidPoint) |
| | | } |
| | | |
| | | if (this.curPolygon.positions.length === 0 && this.polygonEntity) { |
| | | this.editPolygonDataSource.entities.remove(this.polygonEntity) |
| | | this.polygonEntity = null |
| | | } |
| | | } |
| | | // 添加一个点 |
| | | addPosition(position, isAdd = true) { |
| | | // 第一个点要重复压入一次,形成动态绘制效果 |
| | | if (this.curPolygon.positions.length === 0 && isAdd) { |
| | | this.curPolygon.positions.push(position.clone()) |
| | | } |
| | | |
| | | this.curPolygon.positions.push(position.clone()) |
| | | |
| | | // 如果没有实体则创建 |
| | | if (!this.polygonEntity) { |
| | | this.createPolygonEntity() |
| | | } |
| | | |
| | | // 创建端点实体 |
| | | if (this.isPreviewMode) { |
| | | this.createPointEntity(position, isAdd) |
| | | } |
| | | |
| | | this.notify('getPoints', position) |
| | | } |
| | | |
| | | // ============ 鼠标事件 ============ |
| | | |
| | | // 鼠标左键按下(选中端点拖动) |
| | | handleLeftDown(movement) { |
| | | if (!this.editingMode) return |
| | | |
| | | const pickedEntity = this.viewer.scene.pick(movement.position)?.id |
| | | const isPoint = pickedEntity?.name === DrawPolygon.ENTITY_NAMES.POINT |
| | | const isMidPoint = pickedEntity?.name === DrawPolygon.ENTITY_NAMES.MID_POINT |
| | | |
| | | if (pickedEntity && isPoint) { |
| | | this.isDragging = true |
| | | this.draggedEntity = pickedEntity |
| | | this.currentDragPointPosition = this.curPolygon.positions[this.draggedEntity?.customData.ind] |
| | | this.disableMapControl() // 禁止地图交互 |
| | | return |
| | | } |
| | | |
| | | if (pickedEntity && isMidPoint) { |
| | | this.insertPointFromMidPoint(pickedEntity) |
| | | } |
| | | } |
| | | |
| | | // 鼠标左键抬起(拖拽结束) |
| | | handleLeftUp() { |
| | | if (!(this.editingMode && this.curPolygon?.positions && this.draggedEntity)) return |
| | | |
| | | if (this.currentDragPointIsValid && this.isDragging) { |
| | | // 更新点位置 |
| | | this.draggedEntity.position = this.currentDragPointPosition |
| | | this.curPolygon.positions[this.draggedEntity?.customData.ind] = this.currentDragPointPosition |
| | | |
| | | // 校验多边形是否自交 |
| | | if (!this.curDragPointIsValid(this.curPolygon.positions)) { |
| | | this.isShowWaringTip = true |
| | | this.currentDragPointIsValid = true |
| | | this.updatePolygonAppearance(DrawPolygon.COLORS.ERROR_POLYGON, DrawPolygon.COLORS.ERROR_LINE) |
| | | } else { |
| | | this.isShowWaringTip = false |
| | | this.currentDragPointIsValid = false |
| | | this.updatePolygonAppearance(DrawPolygon.COLORS.DEFAULT_POLYGON, DrawPolygon.COLORS.DEFAULT_LINE) |
| | | } |
| | | |
| | | this.notify('getShowWaringTip', this.isShowWaringTip) |
| | | } |
| | | |
| | | // 拖拽结束,恢复交互 |
| | | if (this.isDragging) { |
| | | this.notify('getPolygonPositions', this.curPolygon.positions) |
| | | this.isDragging = false |
| | | this.draggedEntity = null |
| | | this.enableMapControl() |
| | | this.rebuildMidPoints() |
| | | } |
| | | } |
| | | |
| | | // 鼠标移动 |
| | | handleMouseMove(movement) { |
| | | if (!this.drawingMode && !this.editingMode) return |
| | | |
| | | const cartesian = this.viewer.scene.pickPosition(movement.endPosition) |
| | | if (!cartesian) return |
| | | |
| | | // 编辑模式下,拖拽点实时更新 |
| | | if (this.editingMode && this.draggedEntity?.customData) { |
| | | this.draggedEntity.position = cartesian |
| | | this.curPolygon.positions[this.draggedEntity?.customData.ind] = cartesian |
| | | } |
| | | |
| | | // 绘制模式下,实时更新最后一个点 |
| | | if (this.drawingMode && this.polygonEntity && this.curPolygon.positions.length >= 1) { |
| | | this.curPolygon.positions.pop() |
| | | this.curPolygon.positions.push(cartesian) |
| | | } |
| | | |
| | | // 实时检查是否自交 |
| | | if ( |
| | | (this.editingMode && this.draggedEntity?.customData) || |
| | | (this.drawingMode && this.polygonEntity && this.curPolygon.positions.length >= 3) |
| | | ) { |
| | | if (!this.curDragPointIsValid(this.curPolygon.positions)) { |
| | | this.isShowWaringTip = true |
| | | this.currentDragPointIsValid = true |
| | | this.updatePolygonAppearance(DrawPolygon.COLORS.ERROR_POLYGON, DrawPolygon.COLORS.ERROR_LINE) |
| | | } else { |
| | | this.isShowWaringTip = false |
| | | this.currentDragPointIsValid = false |
| | | this.updatePolygonAppearance(DrawPolygon.COLORS.DEFAULT_POLYGON, DrawPolygon.COLORS.DEFAULT_LINE) |
| | | } |
| | | |
| | | this.notify('getShowWaringTip', this.isShowWaringTip) |
| | | } |
| | | } |
| | | |
| | | // 鼠标左键点击 |
| | | handleLeftClick(click) { |
| | | this.removeMenuPopup() |
| | | |
| | | const pickedAllEntity = this.viewer.scene.drillPick(click.position).filter(i => i.id) |
| | | const isStartPoint = pickedAllEntity.find(i => i.id.name === '起飞点') |
| | | const isPolygonPoint = pickedAllEntity.find(i => i.id.name === DrawPolygon.ENTITY_NAMES.POINT) |
| | | const isPolygon = pickedAllEntity.find(i => i.id.name === DrawPolygon.ENTITY_NAMES.POLYGON) |
| | | |
| | | if (isStartPoint) return |
| | | |
| | | // 如果不是绘制模式 |
| | | if (!this.drawingMode) { |
| | | if (!isPolygon) { |
| | | this.editingMode = false |
| | | this.editPolygonPointDataSource.entities.show = false |
| | | this.editPolygonMidPointDataSource && (this.editPolygonMidPointDataSource.entities.show = false) |
| | | return |
| | | } |
| | | |
| | | if (isPolygon) { |
| | | this.editingMode = true |
| | | this.editPolygonPointDataSource.entities.show = true |
| | | this.editPolygonMidPointDataSource && (this.editPolygonMidPointDataSource.entities.show = true) |
| | | this.rebuildMidPoints() |
| | | } |
| | | return |
| | | } |
| | | |
| | | // 点击闭合多边形 |
| | | if (this.curPolygon.positions.length < 4 && isPolygonPoint) { |
| | | return |
| | | } |
| | | |
| | | if (this.curPolygon.positions.length >= 4 && isPolygonPoint) { |
| | | if (!this.drawingMode) return |
| | | |
| | | this.finishDrawing() |
| | | return |
| | | } |
| | | |
| | | // 添加新的点 |
| | | const cartesian = this.viewer.scene.pickPosition(click.position) |
| | | if (!cartesian) return |
| | | |
| | | let arr = [...this.curPolygon.positions] |
| | | arr.pop() |
| | | // 校验多边形是否自交 |
| | | if (arr.length > 2 && !this.curDragPointIsValid([...arr, cartesian])) { |
| | | ElMessage.warning('测区不支持交叉绘制,请重新绘制') |
| | | return |
| | | } |
| | | |
| | | this.addPosition(cartesian) |
| | | } |
| | | |
| | | // 鼠标右键点击(弹出菜单) |
| | | handleRightClick(click) { |
| | | const that = this |
| | | if (that.drawingMode) return |
| | | |
| | | that.removeMenuPopup() |
| | | |
| | | const pickedAllEntity = that.viewer.scene.drillPick(click.position).filter(i => i.id) |
| | | const isPolygon = pickedAllEntity.find(i => i.id.name === DrawPolygon.ENTITY_NAMES.POLYGON) |
| | | const isEditPoint = pickedAllEntity.find(i => i.id.name === DrawPolygon.ENTITY_NAMES.POINT) |
| | | const { |
| | | position: { x, y }, |
| | | } = click |
| | | |
| | | let pickedEntity, tooltipEvent, menuType |
| | | if (isEditPoint) { |
| | | pickedEntity = isEditPoint |
| | | tooltipEvent = that.delPoint |
| | | menuType = 'edit-point' |
| | | } else if (isPolygon) { |
| | | pickedEntity = isPolygon |
| | | tooltipEvent = that.delPolygon |
| | | menuType = 'polygon' |
| | | } |
| | | |
| | | if (pickedEntity && this.isDeleteTheArea) { |
| | | that.delPolygonPoint = pickedEntity.id |
| | | that.menuPopup = that.createMenuPopup(menuType) |
| | | that.menuPopup.style.transform = `translate3d(${x - 10}px, ${y - 10}px, 0)` |
| | | that.viewer.container.appendChild(that.menuPopup) |
| | | that.menuPopup.addEventListener('click', tooltipEvent) |
| | | } |
| | | } |
| | | |
| | | // ============ 删除相关 ============ |
| | | |
| | | // 删除所有实体 |
| | | removeEntities() { |
| | | if (this.editPolygonDataSource) { |
| | | this.editPolygonDataSource.entities.removeAll() |
| | | this.editPolygonDataSource = null |
| | | } |
| | | |
| | | if (this.editPolygonPointDataSource) { |
| | | this.editPolygonPointDataSource.entities.removeAll() |
| | | this.editPolygonPointDataSource = null |
| | | } |
| | | |
| | | if (this.editPolygonMidPointDataSource) { |
| | | this.editPolygonMidPointDataSource.entities.removeAll() |
| | | this.editPolygonMidPointDataSource = null |
| | | } |
| | | this.editingMode = false |
| | | this.polygonEntity = null |
| | | this.curPolygon = null |
| | | } |
| | | |
| | | // 完成绘制 |
| | | finishDrawing() { |
| | | this.curPolygon.positions.pop() |
| | | |
| | | if (this.curPolygon.positions.length >= 3) { |
| | | this.drawingMode = false |
| | | this.editingMode = true |
| | | this.editPolygonPointDataSource.entities.show = true |
| | | this.editPolygonMidPointDataSource && (this.editPolygonMidPointDataSource.entities.show = true) |
| | | this.rebuildMidPoints() |
| | | |
| | | if (!this.curDragPointIsValid(this.curPolygon.positions)) { |
| | | this.isShowWaringTip = true |
| | | this.currentDragPointIsValid = true |
| | | this.updatePolygonAppearance(DrawPolygon.COLORS.ERROR_POLYGON, DrawPolygon.COLORS.ERROR_LINE) |
| | | } else { |
| | | this.isShowWaringTip = false |
| | | this.currentDragPointIsValid = false |
| | | this.updatePolygonAppearance(DrawPolygon.COLORS.DEFAULT_POLYGON, DrawPolygon.COLORS.DEFAULT_LINE) |
| | | } |
| | | |
| | | this.notify('getShowWaringTip', this.isShowWaringTip) |
| | | this.notify('getPolygonPositions', this.curPolygon.positions) |
| | | } |
| | | } |
| | | |
| | | // 删除多边形 |
| | | delPolygon() { |
| | | this.removeEntities() |
| | | this.removeMenuPopup() |
| | | this.notify('getPolygonPositions', []) |
| | | this.startDrawing() |
| | | } |
| | | // 删除图斑 |
| | | delSpot() { |
| | | this.removeEntities() |
| | | this.isPreviewMode = true |
| | | this.startDrawing() |
| | | } |
| | | // 删除端点 |
| | | delPoint() { |
| | | if (this.curPolygon.positions.length <= 3) { |
| | | this.removeMenuPopup() |
| | | return ElMessage.warning('端点不可少于3个') |
| | | } |
| | | if (!this.delPolygonPoint) return |
| | | |
| | | this.curPolygon.positions.splice(this.delPolygonPoint.customData.ind, 1) |
| | | this.removeMenuPopup() |
| | | this.rebuildEditPoints() |
| | | this.rebuildMidPoints() |
| | | |
| | | this.notify('getPolygonPositions', this.curPolygon.positions) |
| | | } |
| | | |
| | | // ============ 工具方法 ============ |
| | | |
| | | // 创建右键菜单 |
| | | createMenuPopup(type = 'polygon') { |
| | | const menuPopupVBox = document.createElement('div') |
| | | menuPopupVBox.id = 'planarPolygonEdit' |
| | | menuPopupVBox.className = 'planar-polygon-edit-tooltip' |
| | | |
| | | const menuPopup = document.createElement('div') |
| | | menuPopup.id = 'planarPolygonEditMenu' |
| | | menuPopup.className = 'planar-polygon-edit-menu' |
| | | |
| | | const menuItems = |
| | | type === 'polygon' |
| | | ? [{ title: '删除测区', class: 'del-planar-polygon' }] |
| | | : [{ title: '删除端点', class: 'del-planar-point' }] |
| | | |
| | | menuItems.forEach(item => { |
| | | const titleDiv = document.createElement('div') |
| | | titleDiv.innerText = item.title |
| | | titleDiv.className = item.class |
| | | menuPopup.appendChild(titleDiv) |
| | | }) |
| | | this.isPreviewMode = true |
| | | menuPopupVBox.appendChild(menuPopup) |
| | | return menuPopupVBox |
| | | } |
| | | |
| | | // 移除菜单 |
| | | removeMenuPopup() { |
| | | const that = this |
| | | if (that.menuPopup) { |
| | | that.menuPopup.removeEventListener('click', that.delPolygon) |
| | | that.menuPopup.removeEventListener('click', that.delPoint) |
| | | that.viewer.container.removeChild(that.menuPopup) |
| | | that.menuPopup = null |
| | | } |
| | | that.delPolygonPoint = null |
| | | } |
| | | |
| | | // 更新多边形样式(正常/错误) |
| | | updatePolygonAppearance(polygonColor, lineColor) { |
| | | this.polygonEntity.polygon.material = polygonColor |
| | | this.polygonEntity.polyline.material = lineColor |
| | | } |
| | | |
| | | // 禁用地图交互 |
| | | disableMapControl() { |
| | | const controller = this.viewer.scene.screenSpaceCameraController |
| | | controller.enableRotate = false |
| | | controller.enableTranslate = false |
| | | controller.enableZoom = false |
| | | } |
| | | |
| | | // 启用地图交互 |
| | | enableMapControl() { |
| | | const controller = this.viewer.scene.screenSpaceCameraController |
| | | controller.enableRotate = true |
| | | controller.enableTranslate = true |
| | | controller.enableZoom = true |
| | | } |
| | | |
| | | // 检查多边形是否自交 |
| | | curDragPointIsValid(positions) { |
| | | if (positions.length < 3) return true |
| | | |
| | | const cartographics = Cesium.Ellipsoid.WGS84.cartesianArrayToCartographicArray(positions) |
| | | const latLngPoints = cartographics.map(cartographic => [ |
| | | Cesium.Math.toDegrees(cartographic.longitude), |
| | | Cesium.Math.toDegrees(cartographic.latitude), |
| | | ]) |
| | | |
| | | // 用 turf.js 检查自交 |
| | | const poly = turf.polygon([[...latLngPoints, latLngPoints[0]]]) |
| | | const intersections = turf.kinks(poly) |
| | | |
| | | return intersections.features.length === 0 |
| | | } |
| | | // 初始化已有多边形 |
| | | async initPolygon(viewer, positions, isPurePreview = false) { |
| | | this.initHandler(viewer) |
| | | this.isPureSpotPreview = isPurePreview |
| | | this.startDrawing() |
| | | let newPosition = positions.map(item => { |
| | | return Cesium.Cartesian3.fromDegrees(Number(item.lng), Number(item.lat), Number(item?.height || 0)) |
| | | }) |
| | | // 预览航线的时候调用 |
| | | if (!this.isPureSpotPreview) { |
| | | this.notify('getPolygonPositions', newPosition) |
| | | } |
| | | |
| | | newPosition.forEach(item => { |
| | | this.addPosition(item, false) |
| | | }) |
| | | |
| | | // 视角飞入区域 |
| | | const newBox = boxTransformScale( |
| | | positions.map(item => [item.lng, item.lat]), |
| | | 5 |
| | | ) |
| | | viewer.camera.flyTo({ |
| | | destination: Cesium.Rectangle.fromDegrees(...newBox), |
| | | offset: new Cesium.HeadingPitchRange(0, Cesium.Math.toRadians(-90), 0), |
| | | duration: 0.5, |
| | | }) |
| | | |
| | | let pointList = await getPointPositionsHeight(positions, viewer) |
| | | flyVisual({ positionsData: pointList.map(item => [item.lng, item.lat, item.ASL]), viewer }) |
| | | |
| | | this.drawingMode = false |
| | | this.editingMode = true |
| | | this.editPolygonPointDataSource && (this.editPolygonPointDataSource.entities.show = true) |
| | | this.editPolygonMidPointDataSource && (this.editPolygonMidPointDataSource.entities.show = true) |
| | | this.rebuildMidPoints() |
| | | } |
| | | |
| | | // 初始化事件处理器 |
| | | initHandler(viewer) { |
| | | this.viewer = viewer |
| | | this.startDrawing() |
| | | |
| | | if (!this.handler) { |
| | | this.handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas) |
| | | |
| | | // 注册鼠标事件 |
| | | const events = [ |
| | | [Cesium.ScreenSpaceEventType.LEFT_DOWN, this.handleLeftDown], |
| | | [Cesium.ScreenSpaceEventType.LEFT_UP, this.handleLeftUp], |
| | | [Cesium.ScreenSpaceEventType.MOUSE_MOVE, this.handleMouseMove], |
| | | [Cesium.ScreenSpaceEventType.LEFT_CLICK, this.handleLeftClick], |
| | | [Cesium.ScreenSpaceEventType.RIGHT_CLICK, this.handleRightClick], |
| | | ] |
| | | |
| | | events.forEach(([type, handler]) => { |
| | | this.handler.setInputAction(handler, type) |
| | | }) |
| | | } |
| | | } |
| | | |
| | | // 移除事件处理器 |
| | | removeHandler() { |
| | | if (this.handler) { |
| | | const eventTypes = [ |
| | | Cesium.ScreenSpaceEventType.LEFT_DOWN, |
| | | Cesium.ScreenSpaceEventType.LEFT_UP, |
| | | Cesium.ScreenSpaceEventType.MOUSE_MOVE, |
| | | Cesium.ScreenSpaceEventType.LEFT_CLICK, |
| | | Cesium.ScreenSpaceEventType.RIGHT_CLICK, |
| | | ] |
| | | |
| | | eventTypes.forEach(type => { |
| | | this.handler.removeInputAction(type) |
| | | }) |
| | | |
| | | this.handler = null |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 销毁实例,释放资源 |
| | | */ |
| | | destroy() { |
| | | if (!this.viewer) return |
| | | |
| | | this.removeMenuPopup() |
| | | this.removeEntities() |
| | | this.removeHandler() |
| | | this.enableMapControl() |
| | | } |
| | | } |
| | |
| | | this._opacity = undefined |
| | | this._alphaPower = undefined |
| | | this._speed = undefined |
| | | this._xRepeatCount = undefined |
| | | |
| | | this.color = options.color || Cesium.Color.fromBytes(0, 255, 255, 255) |
| | | this.glowPower = .25 |
| | |
| | | this.opacity = options.opacity || 0.5 |
| | | this.alphaPower = options.alphaPower || 1.5 |
| | | this.speed = options.speed || 5 |
| | | this.xRepeatCount = options.xRepeatCount || 1 |
| | | |
| | | |
| | | } |
| | | |
| | | get isConstant () { |
| | |
| | | color: new Cesium.Color(1.0, 1.0, 0.0, 0.7), |
| | | bgColor: new Cesium.Color(0.0, 1.0, 0.0, 0.0), |
| | | speed: 5, |
| | | globalAlpha: 1.0 |
| | | globalAlpha: 1.0, |
| | | xRepeatCount: 1.0 |
| | | }, |
| | | source: ` |
| | | uniform vec4 bgColor; |
| | | uniform vec4 color; |
| | | uniform float speed; |
| | | uniform float globalAlpha; |
| | | uniform float xRepeatCount; // 新增的 uniform 在着色器中 |
| | | |
| | | czm_material czm_getMaterial(czm_materialInput materialInput) { |
| | | czm_material czm_getMaterial(czm_materialInput materialInput) { |
| | | czm_material material = czm_getDefaultMaterial(materialInput); |
| | | vec2 st = materialInput.st; |
| | | float time = fract(czm_frameNumber * speed / 1000.0); |
| | |
| | | colorMars3D = vec3(1.0); |
| | | } |
| | | |
| | | material.alpha = color.a * 1.5 * smoothstep(0.0, 1.0, fract(st.s - time)); |
| | | material.alpha = color.a * 1.5 * smoothstep(0.0, 1.0, fract(st.s * xRepeatCount - time)); // 使用 xRepeatCount 来调整纹理重复 |
| | | material.diffuse = max(colorMars3D.rgb * material.alpha, colorMars3D.rgb); |
| | | |
| | | if (material.alpha < bgColor.a) { |
| | |
| | | result.color = Cesium.Property.getValueOrUndefined(this._color, time) |
| | | result.globalAlpha = Cesium.Property.getValueOrUndefined(this._opacity, time) |
| | | result.speed = Cesium.Property.getValueOrUndefined(this._speed, time) |
| | | result.xRepeatCount = Cesium.Property.getValueOrUndefined(this._xRepeatCount, time) |
| | | |
| | | return result |
| | | } |
| | | |
| | |
| | | (other instanceof LineTrailMaterial && |
| | | Cesium.Property.equals(this._color, other._color) && |
| | | Cesium.Property.equals(this._opacity, other._opacity) && |
| | | Cesium.Property.equals(this._speed, other._speed)) |
| | | Cesium.Property.equals(this._speed, other._speed) && |
| | | Cesium.Property.equals(this._xRepeatCount, other._xRepeatCount)) |
| | | ) |
| | | } |
| | | } |
| | |
| | | color: Cesium.createPropertyDescriptor('color'), |
| | | opacity: Cesium.createPropertyDescriptor('opacity'), |
| | | speed: Cesium.createPropertyDescriptor('speed'), |
| | | xRepeatCount: Cesium.createPropertyDescriptor('xRepeatCount'), |
| | | }) |
| | | |
| | | |
| | |
| | | let newLat = lat |
| | | |
| | | switch (direction) { |
| | | case 'W': // 向前(heading 方向) |
| | | case 'KeyW': // 向前(heading 方向) |
| | | newLon += moveStep * Math.sin(headingRad) |
| | | newLat += moveStep * Math.cos(headingRad) |
| | | break |
| | | |
| | | case 'S': // 向后(反方向) |
| | | case 'KeyS': // 向后(反方向) |
| | | newLon -= moveStep * Math.sin(headingRad) |
| | | newLat -= moveStep * Math.cos(headingRad) |
| | | break |
| | | |
| | | case 'D': // 向右(heading + 90°) |
| | | case 'KeyD': // 向右(heading + 90°) |
| | | newLon += moveStep * Math.cos(headingRad) |
| | | newLat -= moveStep * Math.sin(headingRad) |
| | | break |
| | | |
| | | case 'A': // 向左(heading - 90°) |
| | | case 'KeyA': // 向左(heading - 90°) |
| | | newLon -= moveStep * Math.cos(headingRad) |
| | | newLat += moveStep * Math.sin(headingRad) |
| | | break |
| New file |
| | |
| | | /** |
| | | * @description: 图片压缩 |
| | | * @param {*} src 图片地址 |
| | | * @param {*} quality 压缩质量 |
| | | * @param {*} maxWidth 最大宽度 |
| | | * @param {*} callback 回调函数 |
| | | * @return {*} |
| | | */ |
| | | function compressImage(src, quality, maxWidth, callback) { |
| | | const img = new Image() |
| | | img.setAttribute('crossOrigin', 'Anonymous') |
| | | img.onload = function () { |
| | | const canvas = document.createElement('canvas') |
| | | const ctx = canvas.getContext('2d') |
| | | let width = img.width |
| | | let height = img.height |
| | | |
| | | if (width > maxWidth) { |
| | | height *= maxWidth / width |
| | | width = maxWidth |
| | | } |
| | | |
| | | canvas.width = width |
| | | canvas.height = height |
| | | ctx.drawImage(img, 0, 0, width, height) |
| | | |
| | | const newData = canvas.toDataURL('image/jpeg', quality) |
| | | // 使用atob()将base64转换为二进制字符串 |
| | | const binaryString = window.atob(newData.split(',')[1]) |
| | | const mimeString = newData.split(',')[0].split(':')[1].split(';')[0] |
| | | // 创建Blob对象 |
| | | const array = [] |
| | | for (let i = 0; i < binaryString.length; i++) { |
| | | array.push(binaryString.charCodeAt(i)) |
| | | } |
| | | const blob = new Blob([new Uint8Array(array)], { type: mimeString }) |
| | | |
| | | callback(blob) |
| | | } |
| | | img.src = src |
| | | |
| | | return src |
| | | } |
| | | |
| | | export default function imageCompress(src, quality, maxWidth) { |
| | | // 使用例子 |
| | | return compressImage(src, quality, maxWidth, function (blob) { |
| | | // 创建一个新的图片URL |
| | | const imageUrl = URL.createObjectURL(blob) |
| | | // 可以在这里使用imageUrl,比如设置为img元素的src |
| | | // document.getElementById('your-image-element').src = imageUrl; |
| | | // 当不需要这个URL的时候,释放它 |
| | | URL.revokeObjectURL(imageUrl) |
| | | }) |
| | | } |
| New file |
| | |
| | | /* |
| | | * @Author: GuLiMmo 2820890765@qq.com |
| | | * @Date: 2024-05-21 10:14:11 |
| | | * @LastEditors: GuLiMmo 2820890765@qq.com |
| | | * @LastEditTime: 2024-05-21 10:20:34 |
| | | * @FilePath: /bigScreen/src/utils/cesium/compressImage2.js |
| | | * @Description: |
| | | * Copyright (c) 2024 by GuLiMmo, All Rights Reserved. |
| | | */ |
| | | export default function compressImage( |
| | | src, |
| | | maxWidth, |
| | | maxHeight, |
| | | outputFormat = 'image/jpeg', |
| | | quality = 0.7 |
| | | ) { |
| | | return new Promise((resolve, reject) => { |
| | | const image = new Image() |
| | | image.setAttribute('crossOrigin', 'Anonymous') |
| | | image.src = src |
| | | image.onload = () => { |
| | | let width = image.width |
| | | let height = image.height |
| | | |
| | | if (width > maxWidth) { |
| | | height *= maxWidth / width |
| | | width = maxWidth |
| | | } |
| | | |
| | | if (height > maxHeight) { |
| | | width *= maxHeight / height |
| | | height = maxHeight |
| | | } |
| | | |
| | | const canvas = document.createElement('canvas') |
| | | canvas.width = width |
| | | canvas.height = height |
| | | const ctx = canvas.getContext('2d') |
| | | ctx.drawImage(image, 0, 0, width, height) |
| | | |
| | | const newDataUrl = canvas.toDataURL(outputFormat, quality) |
| | | resolve(newDataUrl) |
| | | } |
| | | image.onerror = reject |
| | | }) |
| | | } |
| | |
| | | import { Decimal } from 'decimal.js' |
| | | import _, { cloneDeep, throttle } from 'lodash' |
| | | import { v4 as uuidv4 } from 'uuid' |
| | | |
| | | import { render } from 'vue' |
| | | |
| | | import { analysisPointLineKmz, handlePointListForKmz } from '@/views/newRoutePlan/Waypoint/pointWayLineUtils' |
| | | import HistoryDronePopup from '@/components/DeviceJobDetails/HistoryDronePopup.vue' |
| | | |
| | | import { getNewPolygonData } from '@/views/RoutePlan/PlanarAirLine/planarRouteUtils' |
| | | import { |
| | | getNewPolygonData, |
| | | } from '@/views/newRoutePlan/routeUtils' |
| | | |
| | | import { analyzeKmzFile, removeTextKey, XMLToJSON } from '@/utils/cesium/kmz' |
| | | |
| | | import { ArrowLineMaterialProperty, LineTrailMaterial, PolylineGlowMaterial, } from '@/utils/cesium/Material' |
| | | |
| | | import { ArrowLineMaterialProperty, LineTrailMaterial, PolylineGlowMaterial } from '@/utils/cesium/Material' |
| | | import CreateFrustum from '@/utils/cesium/frustum/CreateFrustum' |
| | | import { ElMessage } from 'element-plus' |
| | | |
| | |
| | | import aircraftGltf from '@/assets/gltf/aircraft.gltf' |
| | | import newNumPoint from '@/assets/images/newStartPoint.png' |
| | | |
| | | import { |
| | | getPolyLine, |
| | | } from '@/views/RoutePlan/PointAirLine/pointWayLineUtils' |
| | | import { getPolyLine } from '@/views/newRoutePlan/Waypoint/pointWayLineUtils' |
| | | import store from '@/store/index' |
| | | import newStartPoint from '@/assets/images/newStartPoint.png' |
| | | import newStartPointMore from '@/assets/images/newStartPointMore.png' |
| | | import { |
| | | arrowLineMaterialProperty, arrowLineMaterialPropertyGray, arrowLineMaterialPropertyGrayT, |
| | | arrowLineMaterialPropertyGreen, |
| | | arrowLineMaterialPropertyOrange, |
| | | } from '@/hooks/useMorePointLine/useMorePointLine' |
| | | |
| | | 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 |
| | | }) |
| | | |
| | | let arrowLineMaterialPropertyOrange = new ArrowLineMaterialProperty({ |
| | | color: new Cesium.Color(255 / 255, 185 / 255, 58 / 255, 1), |
| | | directionColor: new Cesium.Color(1, 1, 1, 1), |
| | | outlineColor: new Cesium.Color(1, 1, 1, 1), |
| | | outlineWidth: 0, |
| | | speed: 5, |
| | | }) |
| | | |
| | | let arrowLineMaterialPropertyGreen = new ArrowLineMaterialProperty({ |
| | | color: new Cesium.Color(6 / 255, 217 / 255, 87 / 255, 1), |
| | | directionColor: new Cesium.Color(1, 1, 1, 1), |
| | | outlineColor: new Cesium.Color(1, 1, 1, 1), |
| | | outlineWidth: 0, |
| | | speed: 5, |
| | | }) |
| | | |
| | | // 记录index |
| | | let indexLog = 0 |
| | | // 颜色 |
| | | function getLineColor(lineNumber) { |
| | | // 颜色顺序: 蓝(0), 黄(1), 绿(2) |
| | | const colors = [arrowLineMaterialProperty, arrowLineMaterialPropertyOrange, arrowLineMaterialPropertyGreen] |
| | | // 计算颜色索引 (从0开始) |
| | | const colorIndex = (lineNumber - 1) % 3 |
| | | // 返回对应颜色 |
| | | return colors[colorIndex] |
| | | } |
| | | |
| | | let runningLineMaterial = new LineTrailMaterial({ |
| | | color: Cesium.Color.fromCssColorString('#1FFF69'), |
| | | opacity: 1, |
| | | speed: 10, |
| | | }) |
| | | let runningTextColor = Cesium.Color.fromCssColorString('#1FFF69') |
| | | let runningTextOffset = new Cesium.Cartesian2(0, -32) |
| | | |
| | | let pendingLineMaterial = new PolylineGlowMaterial({ |
| | | color: Cesium.Color.fromCssColorString('#FFB81D'), |
| | | }) |
| | | let pendingTextColor = Cesium.Color.fromCssColorString('#FFB81D') |
| | | let pendingTextOffset = new Cesium.Cartesian2(0, 32) |
| | | |
| | | let MapPopUpBox = HistoryDronePopup |
| | | |
| | | function getZoomFactor (camerasInfo, type) { |
| | |
| | | |
| | | return zoom_factor |
| | | } |
| | | // 记录index |
| | | let indexLog = 0 |
| | | // 颜色 |
| | | function getPendingExecutionLineColor (lineNumber) { |
| | | // 颜色顺序: 蓝(灰), 更灰 |
| | | const colors = [arrowLineMaterialPropertyGray, arrowLineMaterialPropertyGrayT] |
| | | // 计算颜色索引 (从0开始) |
| | | const colorIndex = (lineNumber - 1) % 2 |
| | | // 返回对应颜色 |
| | | return colors[colorIndex] |
| | | } |
| | | |
| | | function getLineColor (lineNumber) { |
| | | // 颜色顺序: 蓝(0), 黄(1), 绿(2) |
| | | const colors = [arrowLineMaterialProperty, arrowLineMaterialPropertyOrange, arrowLineMaterialPropertyGreen] |
| | | // 计算颜色索引 (从0开始) |
| | | const colorIndex = (lineNumber - 1) % 3 |
| | | // 返回对应颜色 |
| | | return colors[colorIndex] |
| | | } |
| | | |
| | | function getBase64Image (imgUrl) { |
| | | const img = new Image() |
| | | const canvas = document.createElement('canvas') |
| | | const ctx = canvas.getContext('2d') |
| | | |
| | | return new Promise(resolve => { |
| | | img.onload = function () { |
| | | canvas.width = img.width |
| | | canvas.height = img.height |
| | | ctx.drawImage(img, 0, 0) |
| | | const base64 = canvas.toDataURL('image/png') |
| | | resolve(base64) |
| | | } |
| | | img.src = imgUrl |
| | | }) |
| | | } |
| | | async function createCombinedSVG (index, billboardImageUrl) { |
| | | const base64Image = await getBase64Image(billboardImageUrl) |
| | | const svg = ` |
| | | <svg width="100" height="100" xmlns="http://www.w3.org/2000/svg"> |
| | | <image href="${base64Image}" x="10" y="10" width="80" height="80"/> |
| | | <text x="50" y="54" font-size="26" fill="white" text-anchor="middle" dominant-baseline="middle"> |
| | | ${index + 1} |
| | | </text> |
| | | </svg> |
| | | ` |
| | | return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}` |
| | | } |
| | | |
| | | function findHeightByCoordinates (data, targetLng, targetLat) { |
| | | // 展平二维数组,然后查找第一个匹配的点 |
| | | const allPoints = data.flat() |
| | | const point = allPoints.find(point => |
| | | point.longitude === targetLng && point.latitude === targetLat |
| | | ) |
| | | |
| | | return point ? point.height : null |
| | | } |
| | | |
| | | function renderingMachineNest (options) { |
| | | const { viewer, dockTransformPosition, dockPosition, data } = options |
| | | viewer.entities.add({ |
| | | name: 'work-drone-route-point-dock', |
| | | position: dockTransformPosition, |
| | | billboard: { |
| | | image: endingOnlineImg, |
| | | width: 50, |
| | | height: 50, |
| | | disableDepthTestDistance: Number.POSITIVE_INFINITY, |
| | | }, |
| | | customData: { |
| | | data, |
| | | }, |
| | | }) |
| | | viewer.entities.add({ |
| | | name: 'work-drone-route--point-polyline', |
| | | position: dockTransformPosition, |
| | | polyline: getPolyLine(viewer, { value: [dockPosition] }, 0), |
| | | }) |
| | | } |
| | | |
| | | function renderingStartingPoint (options) { |
| | | const { viewer, startPosition, data } = options |
| | | viewer.entities.add({ |
| | | name: 'work-drone-route-point-start', |
| | | position: startPosition, |
| | | billboard: { |
| | | image: rwqfdImg, |
| | | width: 50, |
| | | height: 50, |
| | | disableDepthTestDistance: Number.POSITIVE_INFINITY, |
| | | }, |
| | | customData: { |
| | | data, |
| | | }, |
| | | }) |
| | | } |
| | | |
| | | export default class CreateRouteLine { |
| | | /** |
| | | * |
| | | * |
| | | * @param {*} options 参数 |
| | | */ |
| | | constructor(options) { |
| | |
| | | |
| | | /** |
| | | * 初始化viewer |
| | | * @param {*} viewer |
| | | * @param {*} viewer |
| | | */ |
| | | initCreateRoute (viewer) { |
| | | indexLog = 0 |
| | | this.viewer = viewer |
| | | } |
| | | |
| | |
| | | const currentWaypointIndex = output?.ext['current_waypoint_index'] |
| | | |
| | | this.updataMultiNestData(sn, { |
| | | currentWaypointIndex |
| | | currentWaypointIndex, |
| | | }) |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 请求航线文件数据并解析处理 |
| | | * 请求航线文件数据并绘制解析处理 |
| | | * @param {*} data 包含文件url等信息 |
| | | * @param {*} dronePosition 无人机位置 |
| | | * @returns |
| | | * @param {*} dockPosition 无人机位置 |
| | | * @returns |
| | | */ |
| | | async parsingFiles (data, dronePosition = null, isShowDock = false, isShowPointBillboard = true) { |
| | | async parsingFiles (data, dockPosition = null, isShowDock = false, isShowPointBillboard = true) { |
| | | console.log(data,'888') |
| | | const { url, wayline_type, device_sn } = data |
| | | |
| | | if (!url) return dronePosition != null ? { |
| | | dronePosition: [{ lng: dronePosition.longitude, lat: dronePosition.latitude, alt: dronePosition.height }] |
| | | } : {} |
| | | |
| | | indexLog = 0 |
| | | if (!url) |
| | | return dockPosition != null |
| | | ? { |
| | | dronePosition: [{ lng: dockPosition.longitude, lat: dockPosition.latitude, alt: dockPosition.height }], |
| | | } |
| | | : {} |
| | | this.updataMultiNestData(device_sn) |
| | | |
| | | const res = await analyzeKmzFile(`${url}?_t=${new Date().getTime()}`) |
| | |
| | | const missionConfig = removeTextKey(waylinesXmlJson.missionConfig) |
| | | const waylinesXMLObj = removeTextKey(waylinesXmlJson.Folder) |
| | | |
| | | if (templateType === "mapping3d") { |
| | | if (templateType === 'mapping3d') { |
| | | if (!waylinesXMLObj[0].Placemark.length) return ElMessage.error('没有航线点位') |
| | | } else { |
| | | if (!waylinesXMLObj.Placemark.length) return ElMessage.error('没有航线点位') |
| | | } |
| | | |
| | | if ([2, 4, 5, 6, 7, 10].includes(Number(wayline_type))) { |
| | | if (templateType === "mapping3d") { |
| | | if ([2, 4, 5, 7, 10].includes(Number(wayline_type))) { |
| | | if (templateType === 'mapping3d') { |
| | | let morePolygonData = waylinesXMLObj.map(i => ({ |
| | | data: i.Placemark |
| | | data: i.Placemark, |
| | | })) |
| | | |
| | | let newPolygonData = getNewPolygonData(morePolygonData) |
| | |
| | | waylinesXMLObj[0], |
| | | missionConfig, |
| | | templateXmlJson, |
| | | dronePosition, |
| | | dockPosition, |
| | | data, |
| | | startPoint, |
| | | isShowDock, |
| | |
| | | waylinesXMLObj, |
| | | missionConfig, |
| | | templateXmlJson, |
| | | dronePosition, |
| | | dockPosition, |
| | | data, |
| | | startPoint, |
| | | isShowDock, |
| | |
| | | return this.drawPointRoute( |
| | | waylinesXMLObj, |
| | | missionConfig, |
| | | dronePosition, |
| | | dockPosition, |
| | | data, |
| | | startPoint, |
| | | isShowDock, |
| | |
| | | } |
| | | } |
| | | |
| | | // 拆分航线 |
| | | async splitWayLine (data, dronePosition = null, isShowDock = false, isShowPointBillboard = true) { |
| | | const { |
| | | pointPlacemark, |
| | | polygonList, |
| | | pointList, |
| | | templateType, |
| | | startPoint, |
| | | execute_height_mode, |
| | | auto_flight_speed, |
| | | take_off_security_height, |
| | | buffer_distance_meters, |
| | | missionConfig, |
| | | waylinesXMLObjR, |
| | | } = await analysisPointLineKmz(`${data.domain_url}${data.object_key}`) |
| | | const { positionArray, filePositions } = this.disposeData( |
| | | waylinesXMLObjR, |
| | | missionConfig, |
| | | null, |
| | | data, |
| | | startPoint, |
| | | 0 |
| | | ) |
| | | let bigPointList = handlePointListForKmz({ |
| | | placemark: pointPlacemark, |
| | | startPoint: startPoint, |
| | | execute_height_mode: execute_height_mode, |
| | | auto_flight_speed: auto_flight_speed, |
| | | }) |
| | | // 路径线 |
| | | let entityConfig |
| | | bigPointList.shift() |
| | | let flyPositions = bigPointList.map(i => [Number(i.longitude), Number(i.latitude), Number(i.height)]) |
| | | // 渲染多个点 |
| | | let firstTopPosition = null |
| | | let resultPosotions = [] |
| | | await Promise.all(data.wayline_file_list.map(async (item, index) => { |
| | | // 小航线渲染线 |
| | | const kmzUrl = import.meta.env.VITE_APP_AIRLINE_URL + item.object_key |
| | | const { pointList } = await analysisPointLineKmz(kmzUrl) |
| | | resultPosotions.push(pointList) |
| | | })).then(res => { |
| | | // 目的是区分大航线点属于哪条小航线 |
| | | bigPointList.forEach(bigObj => { |
| | | let foundIndex = -1 |
| | | // 遍历 resultPositions 查找匹配项 |
| | | resultPosotions.some((subArray, subIndex) => { |
| | | const found = subArray.some(obj => |
| | | obj.latitude === bigObj.latitude && |
| | | obj.longitude === bigObj.longitude |
| | | ) |
| | | if (found) { |
| | | foundIndex = subIndex |
| | | return true // 找到就停止搜索 |
| | | } |
| | | return false |
| | | }) |
| | | // 如果找到了,给 bigPointList 的对象添加标记 |
| | | if (foundIndex !== -1) { |
| | | bigObj.log = foundIndex // 或 bigObj.index = foundIndex |
| | | } |
| | | }) |
| | | console.log(bigPointList, '查看结果值') // 现在 bigPointList 会有 log 属性 |
| | | bigPointList.map(async (i, index) => { |
| | | firstTopPosition = i |
| | | const position = Cesium.Cartesian3.fromDegrees(Number(i.longitude), Number(i.latitude), Number(findHeightByCoordinates(resultPosotions, i.longitude, i.latitude))) |
| | | const combinedImage = await createCombinedSVG(index, i.log % 2 ? newStartPoint : newStartPointMore) |
| | | |
| | | entityConfig = { |
| | | name: 'work-drone-route-point-split', |
| | | position: position, |
| | | billboard: { |
| | | disableDepthTestDistance: Number.POSITIVE_INFINITY, |
| | | image: combinedImage, |
| | | width: 50, |
| | | height: 50, |
| | | verticalOrigin: Cesium.VerticalOrigin.CENTER, |
| | | horizontalOrigin: Cesium.HorizontalOrigin.CENTER, |
| | | } |
| | | } |
| | | |
| | | // 只在第一个点添加 label |
| | | if (index === 0) { |
| | | entityConfig.label = { |
| | | text: data.task_name || '', |
| | | font: 'bold 16px sans-serif', |
| | | fillColor: data.status === 2 ? runningTextColor : pendingTextColor, |
| | | pixelOffset: data.status === 2 ? runningTextOffset : pendingTextOffset, |
| | | style: Cesium.LabelStyle.FILL_AND_OUTLINE, |
| | | outlineWidth: 2, |
| | | outlineColor: Cesium.Color.BLACK, |
| | | disableDepthTestDistance: Number.POSITIVE_INFINITY, |
| | | } |
| | | } |
| | | |
| | | this.viewer.entities.add(entityConfig) |
| | | }) |
| | | }) |
| | | |
| | | // 渲染航线 |
| | | data.wayline_file_list.map(async (item, index) => { |
| | | // 小航线渲染线 |
| | | const kmzUrl = import.meta.env.VITE_APP_AIRLINE_URL + item.object_key |
| | | const { pointList } = await analysisPointLineKmz(kmzUrl) |
| | | // if (curRouteLineData.value.droneData && splitLines.length === 1) { |
| | | // // 把pointList的第一个点替换为无人机位置 |
| | | // pointList[0] = curRouteLineData.value.droneData[0] |
| | | // } |
| | | if (take_off_security_height + pointList[0].height > firstTopPosition.height) { |
| | | // 安全高度+机巢高度 > 第一个点高度 |
| | | const positionTest = { |
| | | longitude: pointList[0].longitude, |
| | | latitude: pointList[0].latitude, |
| | | height: take_off_security_height + pointList[0].height, |
| | | } |
| | | |
| | | const positionTest1 = { |
| | | longitude: pointList[1].longitude, |
| | | latitude: pointList[1].latitude, |
| | | height: take_off_security_height + pointList[0].height, |
| | | } |
| | | pointList.splice(1, 0, positionTest) |
| | | // 往数组中插入元素 |
| | | pointList.splice(2, 0, positionTest1) |
| | | // console.log(pointList, 'pointList') |
| | | } else if (take_off_security_height + pointList[0].height < firstTopPosition.height) { |
| | | const positionTest = { |
| | | longitude: pointList[0].longitude, |
| | | latitude: pointList[0].latitude, |
| | | height: firstTopPosition.height, |
| | | } |
| | | pointList.splice(1, 0, positionTest) |
| | | } |
| | | const routePositions = pointList.map(i => |
| | | Cesium.Cartesian3.fromDegrees(Number(i.longitude), Number(i.latitude), Number(i.height)) |
| | | ) |
| | | |
| | | this.viewer.entities.add({ |
| | | name: 'work-drone-route-line-split', |
| | | polyline: { |
| | | width: 4, |
| | | positions: routePositions, |
| | | material: data.status === 1 ? arrowLineMaterialPropertyGray : getLineColor(index + 1), |
| | | clampToGround: false, |
| | | }, |
| | | }) |
| | | }) |
| | | // 落点线 |
| | | ; (bigPointList || [])?.forEach((item, index) => { |
| | | const topPosition = Cesium.Cartesian3.fromDegrees( |
| | | Number(item.longitude), |
| | | Number(item.latitude), |
| | | Number(item.height) |
| | | ) |
| | | let setting = { |
| | | name: 'work-drone-route--planar-polyline-split', |
| | | position: topPosition, |
| | | polyline: getPolyLine(this.viewer, { value: bigPointList }, index), |
| | | } |
| | | this.viewer.entities.add(setting) |
| | | }) |
| | | return { |
| | | entity: entityConfig, |
| | | routeLinePositions: positionArray, |
| | | filePositions: |
| | | dronePosition != null |
| | | ? [{ lng: dronePosition.longitude, lat: dronePosition.latitude, alt: dronePosition.height }, ...filePositions] |
| | | : filePositions, |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 绘制面状航线 |
| | | * @param {*} lineObj 航线文件中的 |
| | | * @param {*} missionConfig 航线文件中的 |
| | | * @param {*} templateXmlJson 航线文件中的 |
| | | * @param {*} dronePosition 无人机位置 |
| | | * @param {*} data |
| | | * @param {*} startPoint |
| | | * @param {*} isShowPointBillboard 是否显示,航线起飞点,终点等标注 |
| | | * @returns |
| | | * @param {*} dockPosition 无人机位置 |
| | | * @param {*} data |
| | | * @param {*} startPoint |
| | | * @returns |
| | | */ |
| | | async drawPlanarRoute (lineObj, missionConfig, templateXmlJson, dronePosition, data, startPoint, isShowDock, isShowPointBillboard, wayline_type) { |
| | | const { positionArray, filePositions } = this.disposeData(lineObj, missionConfig, dronePosition, data, startPoint, wayline_type) |
| | | async drawPlanarRoute ( |
| | | lineObj, |
| | | missionConfig, |
| | | templateXmlJson, |
| | | dockPosition, |
| | | data, |
| | | startPoint, |
| | | isShowDock, |
| | | isShowPointBillboard, |
| | | wayline_type |
| | | ) { |
| | | const { positionArray, filePositions } = this.disposeData( |
| | | lineObj, |
| | | missionConfig, |
| | | dockPosition, |
| | | data, |
| | | startPoint, |
| | | wayline_type |
| | | ) |
| | | const { device_sn } = data |
| | | |
| | | const droneTransformPosition = Cesium.Cartesian3.fromDegrees( |
| | | Number(dronePosition.longitude), |
| | | Number(dronePosition.latitude), |
| | | Number(dronePosition.height), |
| | | const dockTransformPosition = Cesium.Cartesian3.fromDegrees( |
| | | Number(dockPosition.longitude), |
| | | Number(dockPosition.latitude), |
| | | Number(dockPosition.height) |
| | | ) |
| | | |
| | | let coordArr = null |
| | | |
| | | const placemark = templateXmlJson.Folder?.Placemark |
| | | // 取出点位 |
| | | const coordinates = |
| | | placemark.Polygon?.outerBoundaryIs.LinearRing.coordinates?.[ |
| | | '#text' |
| | | ]?.split('\n') || [] |
| | | |
| | | const coordinates = placemark.Polygon?.outerBoundaryIs.LinearRing.coordinates?.['#text']?.split('\n') || [] |
| | | // 数组转换 |
| | | coordArr = coordinates.map((coordinate) => |
| | | coordArr = coordinates.map(coordinate => |
| | | coordinate |
| | | .replace(/\s+/g, '') |
| | | .split(',') |
| | | .map((v) => Number(v)), |
| | | .map(v => Number(v)) |
| | | ) |
| | | |
| | | // 获取当前经纬度绝对高度 |
| | | let newCoordArr = coordArr.map(item => ([item[0], item[1], 0])) |
| | | |
| | | let newCoordArr = coordArr.map(item => [item[0], item[1], 0]) |
| | | let planarRouteEntity = this.viewer.entities.add({ |
| | | name: 'work-drone-route-planar-polyline', |
| | | |
| | | polyline: { |
| | | width: 3, |
| | | positions: positionArray, |
| | |
| | | }, |
| | | |
| | | customData: { |
| | | data |
| | | } |
| | | data, |
| | | }, |
| | | }) |
| | | |
| | | let droppointPositions = (filePositions || [])?.map(i => ({ |
| | | longitude: Number(i.lng), |
| | | latitude: Number(i.lat), |
| | | height: Number(i.alt), |
| | | })); |
| | | })) |
| | | |
| | | // 落点线 |
| | | (droppointPositions || [])?.forEach((item, index) => { |
| | | const topPosition = Cesium.Cartesian3.fromDegrees( |
| | | Number(item.longitude), |
| | | Number(item.latitude), |
| | | Number(item.height) |
| | | ) |
| | | let setting = { |
| | | name: 'work-drone-route-planar-polyline', |
| | | position: topPosition, |
| | | polyline: getPolyLine(this.viewer, { value: droppointPositions }, index), |
| | | } |
| | | this.viewer.entities.add(setting) |
| | | }) |
| | | // 落点线 |
| | | ; (droppointPositions || [])?.forEach((item, index) => { |
| | | const topPosition = Cesium.Cartesian3.fromDegrees( |
| | | Number(item.longitude), |
| | | Number(item.latitude), |
| | | Number(item.height) |
| | | ) |
| | | let setting = { |
| | | name: 'work-drone-route--planar-polyline', |
| | | position: topPosition, |
| | | polyline: getPolyLine(this.viewer, { value: droppointPositions }, index), |
| | | } |
| | | this.viewer.entities.add(setting) |
| | | }) |
| | | |
| | | // 面状航线第一个点突出展示 |
| | | isShowPointBillboard && this.startingIncreasePoint(positionArray) |
| | |
| | | name: 'work-drone-route-planar-border', |
| | | |
| | | polyline: { |
| | | positions: Cesium.Cartesian3.fromDegreesArrayHeights( |
| | | cloneCoordArr.flat(), |
| | | ), |
| | | positions: Cesium.Cartesian3.fromDegreesArrayHeights(cloneCoordArr.flat()), |
| | | width: 4, |
| | | material: new Cesium.PolylineOutlineMaterialProperty({ |
| | | color: new Cesium.Color.fromBytes(74, 138, 233), |
| | |
| | | // 绘制面状地块-------------------- |
| | | this.viewer.entities.add({ |
| | | name: 'work-drone-route-planar-polygon', |
| | | |
| | | polygon: { |
| | | hierarchy: Cesium.Cartesian3.fromDegreesArrayHeights( |
| | | newCoordArr.flat(), |
| | | ), |
| | | hierarchy: Cesium.Cartesian3.fromDegreesArrayHeights(newCoordArr.flat()), |
| | | material: new Cesium.Color.fromBytes(75, 159, 221, 100), |
| | | outline: true, |
| | | outlineColor: new Cesium.Color.fromBytes(35, 85, 216, 255), |
| | |
| | | |
| | | label: { |
| | | text: data.job_name || '', |
| | | font: 'bold 16px YouSheBiaoTiHei', |
| | | font: 'bold 16px sans-serif', |
| | | fillColor: Cesium.Color.WHITE, |
| | | pixelOffset: new Cesium.Cartesian2(0, -50), |
| | | style: Cesium.LabelStyle.FILL_AND_OUTLINE, |
| | |
| | | }) |
| | | } else { |
| | | // 显示机巢位置 |
| | | if (isShowDock) { |
| | | // 起点 |
| | | this.viewer.entities.add({ |
| | | name: 'work-drone-route-point-dock', |
| | | |
| | | position: droneTransformPosition, |
| | | |
| | | billboard: { |
| | | image: endingOnlineImg, |
| | | width: 50, |
| | | height: 50, |
| | | disableDepthTestDistance: Number.POSITIVE_INFINITY, |
| | | }, |
| | | |
| | | customData: { |
| | | data |
| | | } |
| | | }) |
| | | |
| | | this.viewer.entities.add({ |
| | | name: 'work-drone-route--point-polyline', |
| | | position: droneTransformPosition, |
| | | polyline: getPolyLine(this.viewer, { value: [dronePosition] }, 0), |
| | | }) |
| | | } |
| | | |
| | | const startPosition = Cesium.Cartesian3.fromDegrees( |
| | | Number(filePositions[0].lng), |
| | | Number(filePositions[0].lat), |
| | | Number(filePositions[0].alt) |
| | | ) |
| | | |
| | | isShowDock && renderingMachineNest({ |
| | | viewer: this.viewer, |
| | | dockTransformPosition, |
| | | dockPosition, |
| | | data |
| | | }) |
| | | // 起点 |
| | | isShowPointBillboard && this.viewer.entities.add({ |
| | | name: 'work-drone-route-point-start', |
| | | |
| | | position: startPosition, |
| | | |
| | | billboard: { |
| | | image: rwqfdImg, |
| | | width: 50, |
| | | height: 50, |
| | | disableDepthTestDistance: Number.POSITIVE_INFINITY, |
| | | }, |
| | | |
| | | customData: { |
| | | data |
| | | } |
| | | isShowPointBillboard && renderingStartingPoint({ |
| | | viewer: this.viewer, |
| | | startPosition: Cesium.Cartesian3.fromDegrees( |
| | | Number(filePositions[0].lng), |
| | | Number(filePositions[0].lat), |
| | | Number(filePositions[0].alt) |
| | | ), |
| | | data |
| | | }) |
| | | } |
| | | |
| | | return { |
| | | entity: planarRouteEntity, |
| | | routeLinePositions: positionArray, |
| | | filePositions: dronePosition != null ? [ |
| | | { lng: dronePosition.longitude, lat: dronePosition.latitude, alt: dronePosition.height }, |
| | | ...filePositions |
| | | ] : filePositions |
| | | filePositions: |
| | | dockPosition != null |
| | | ? [{ lng: dockPosition.longitude, lat: dockPosition.latitude, alt: dockPosition.height }, ...filePositions] |
| | | : filePositions, |
| | | } |
| | | } |
| | | |
| | |
| | | |
| | | let setting = { |
| | | name: 'work-drone-route-planar-start', |
| | | |
| | | position: positions, |
| | | |
| | | billboard: { |
| | | image: this.planarBillboard('#2D8CF0'), |
| | | pixelOffset: new Cesium.Cartesian2(146, 72), |
| | |
| | | * 绘制点航线 |
| | | * @param {*} lineObj 航线文件中的 |
| | | * @param {*} missionConfig 航线文件中的 |
| | | * @param {*} dronePosition 无人机位置 |
| | | * @param {*} dockPosition 无人机位置 |
| | | * @param {*} data 任务信息,航线地址等 |
| | | * @param {*} startPoint |
| | | * @param {*} startPoint |
| | | * @param {*} isShowDock 是否显示机巢位置 |
| | | * @param {*} isShowPointBillboard 是否显示,航线起飞点,终点等标注 |
| | | * @returns |
| | | * @returns |
| | | */ |
| | | async drawPointRoute (lineObj, missionConfig, dronePosition, data, startPoint, isShowDock, isShowPointBillboard, wayline_type) { |
| | | const { positionArray, filePositions } = this.disposeData(lineObj, missionConfig, dronePosition, data, startPoint, wayline_type) |
| | | |
| | | const droneTransformPosition = Cesium.Cartesian3.fromDegrees( |
| | | Number(dronePosition.longitude), |
| | | Number(dronePosition.latitude), |
| | | Number(dronePosition.height), |
| | | async drawPointRoute ( |
| | | lineObj, |
| | | missionConfig, |
| | | dockPosition, |
| | | data, |
| | | startPoint, |
| | | isShowDock, |
| | | isShowPointBillboard, |
| | | wayline_type |
| | | ) { |
| | | const { positionArray, filePositions } = this.disposeData( |
| | | lineObj, |
| | | missionConfig, |
| | | dockPosition, |
| | | data, |
| | | startPoint, |
| | | wayline_type |
| | | ) |
| | | |
| | | const dockTransformPosition = Cesium.Cartesian3.fromDegrees( |
| | | Number(dockPosition.longitude), |
| | | Number(dockPosition.latitude), |
| | | Number(dockPosition.height) |
| | | ) |
| | | // 路径线 |
| | | let polyline |
| | | |
| | | |
| | | indexLog = indexLog + 1 |
| | | |
| | | if (this.type === 'clusterScheduling') { |
| | | filePositions.forEach((item, index) => { |
| | | let position = Cesium.Cartesian3.fromDegrees(item.lng, item.lat, item.alt) |
| | | |
| | | if (index === 0) { |
| | | this.viewer.entities.add({ |
| | | name: 'work-drone-route-point-start', |
| | | position, |
| | | |
| | | billboard: { |
| | | image: rwqfdImg, |
| | | outlineWidth: 0, |
| | |
| | | height: 50, |
| | | disableDepthTestDistance: Number.POSITIVE_INFINITY, |
| | | }, |
| | | |
| | | label: { |
| | | text: data.job_name || '', |
| | | font: 'bold 16px YouSheBiaoTiHei', |
| | | font: 'bold 16px sans-serif', |
| | | fillColor: data.type === 2 ? runningTextColor : pendingTextColor, |
| | | pixelOffset: data.type === 2 ? runningTextOffset : pendingTextOffset, |
| | | style: Cesium.LabelStyle.FILL_AND_OUTLINE, |
| | |
| | | } else if (index === filePositions.length - 1) { |
| | | this.viewer.entities.add({ |
| | | name: 'work-drone-route-point-end', |
| | | |
| | | position, |
| | | |
| | | billboard: { |
| | | image: newEndPointImg, |
| | | outlineWidth: 0, |
| | |
| | | } else { |
| | | this.viewer.entities.add({ |
| | | name: 'work-drone-route-point-approach', |
| | | |
| | | position, |
| | | |
| | | billboard: { |
| | | image: newNumPoint, |
| | | width: 50, |
| | |
| | | }) |
| | | } |
| | | }) |
| | | |
| | | polyline = this.viewer.entities.add({ |
| | | name: 'work-drone-route-point-polyline', |
| | | |
| | | polyline: { |
| | | width: 4, |
| | | positions: positionArray, |
| | | material: data.type === 2 ? runningLineMaterial : pendingLineMaterial, |
| | | material: data.type === 2 ? getLineColor(indexLog) : arrowLineMaterialPropertyGray, |
| | | clampToGround: false, |
| | | } |
| | | }, |
| | | }) |
| | | } else { |
| | | // 显示机巢位置 |
| | | if (isShowDock) { |
| | | // 起点 |
| | | this.viewer.entities.add({ |
| | | name: 'work-drone-route-point-dock', |
| | | |
| | | position: droneTransformPosition, |
| | | |
| | | billboard: { |
| | | image: endingOnlineImg, |
| | | width: 50, |
| | | height: 50, |
| | | disableDepthTestDistance: Number.POSITIVE_INFINITY, |
| | | }, |
| | | |
| | | customData: { |
| | | data |
| | | } |
| | | }) |
| | | |
| | | this.viewer.entities.add({ |
| | | name: 'work-drone-route--point-polyline', |
| | | position: droneTransformPosition, |
| | | polyline: getPolyLine(this.viewer, { value: [dronePosition] }, 0), |
| | | }) |
| | | } |
| | | |
| | | const startPosition = Cesium.Cartesian3.fromDegrees( |
| | | Number(filePositions[0].lng), |
| | | Number(filePositions[0].lat), |
| | | Number(filePositions[0].alt) |
| | | ) |
| | | |
| | | isShowDock && renderingMachineNest({ |
| | | viewer: this.viewer, |
| | | dockTransformPosition, |
| | | dockPosition |
| | | }) |
| | | // 起点 |
| | | isShowPointBillboard && this.viewer.entities.add({ |
| | | name: 'work-drone-route-point-start', |
| | | |
| | | position: startPosition, |
| | | |
| | | billboard: { |
| | | image: rwqfdImg, |
| | | width: 50, |
| | | height: 50, |
| | | disableDepthTestDistance: Number.POSITIVE_INFINITY, |
| | | }, |
| | | |
| | | customData: { |
| | | data |
| | | } |
| | | isShowPointBillboard && renderingStartingPoint({ |
| | | viewer: this.viewer, |
| | | startPosition: Cesium.Cartesian3.fromDegrees( |
| | | Number(filePositions[0].lng), |
| | | Number(filePositions[0].lat), |
| | | Number(filePositions[0].alt) |
| | | ), |
| | | data |
| | | }) |
| | | |
| | | // 终点 |
| | | isShowPointBillboard && this.viewer.entities.add({ |
| | | name: 'work-drone-route-point-end', |
| | | |
| | | position: positionArray[positionArray.length - 1], |
| | | |
| | | billboard: { |
| | | image: new Cesium.ConstantProperty(endPointImg), |
| | | width: 30, |
| | | height: 30, |
| | | verticalOrigin: Cesium.VerticalOrigin.BOTTOM, // 底部对齐 |
| | | }, |
| | | }) |
| | | |
| | | isShowPointBillboard && |
| | | this.viewer.entities.add({ |
| | | name: 'work-drone-route-point-end', |
| | | position: positionArray[positionArray.length - 1], |
| | | billboard: { |
| | | image: new Cesium.ConstantProperty(endPointImg), |
| | | width: 30, |
| | | height: 30, |
| | | verticalOrigin: Cesium.VerticalOrigin.BOTTOM, // 底部对齐 |
| | | }, |
| | | }) |
| | | console.log(data.status, '失败') |
| | | polyline = this.viewer.entities.add({ |
| | | name: 'work-drone-route-point-polyline', |
| | | |
| | | polyline: { |
| | | width: 4, |
| | | positions: positionArray, |
| | | material: getLineColor(indexLog), |
| | | material: data.status === 5 ? arrowLineMaterialPropertyGray : getLineColor(indexLog), |
| | | clampToGround: false, |
| | | }, |
| | | |
| | | customData: { |
| | | data |
| | | } |
| | | data, |
| | | }, |
| | | }) |
| | | } |
| | | |
| | |
| | | longitude: Number(i.lng), |
| | | latitude: Number(i.lat), |
| | | height: Number(i.alt), |
| | | })); |
| | | })) |
| | | |
| | | // 落点线 |
| | | (droppointPositions || [])?.forEach((item, index) => { |
| | | const topPosition = Cesium.Cartesian3.fromDegrees( |
| | | Number(item.longitude), |
| | | Number(item.latitude), |
| | | Number(item.height) |
| | | ) |
| | | // 落点线 |
| | | ; (droppointPositions || [])?.forEach((item, index) => { |
| | | const topPosition = Cesium.Cartesian3.fromDegrees( |
| | | Number(item.longitude), |
| | | Number(item.latitude), |
| | | Number(item.height) |
| | | ) |
| | | |
| | | let setting = { |
| | | name: 'work-drone-route-point-polyline', |
| | | position: topPosition, |
| | | polyline: getPolyLine(this.viewer, { value: droppointPositions }, index), |
| | | } |
| | | this.viewer.entities.add(setting) |
| | | }) |
| | | let setting = { |
| | | name: 'work-drone-route--point-polyline', |
| | | position: topPosition, |
| | | polyline: getPolyLine(this.viewer, { value: droppointPositions }, index), |
| | | } |
| | | this.viewer.entities.add(setting) |
| | | }) |
| | | |
| | | return { |
| | | entity: polyline, |
| | | routeLinePositions: positionArray, |
| | | filePositions: dronePosition != null ? [ |
| | | { lng: dronePosition.longitude, lat: dronePosition.latitude, alt: dronePosition.height }, |
| | | ...filePositions |
| | | ] : filePositions, |
| | | filePositions: |
| | | dockPosition !== null |
| | | ? [{ lng: dockPosition.longitude, lat: dockPosition.latitude, alt: dockPosition.height }, ...filePositions] |
| | | : filePositions, |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 通用处理数据 |
| | | * @param {*} lineObj |
| | | * @param {*} missionConfig |
| | | * @param {*} dronePosition |
| | | * @param {*} startPoint |
| | | * @param {*} lineObj |
| | | * @param {*} missionConfig |
| | | * @param {*} dronePosition |
| | | * @param {*} startPoint |
| | | * @returns {} {positionArray: 带拼接点位置, filePositions:光航线点位置} |
| | | */ |
| | | disposeData (lineObj, missionConfig, dronePosition, data, startPoint, wayline_type) { |
| | | indexLog = indexLog + 1 |
| | | const { device_sn } = data |
| | | |
| | | const executeHeightMode = lineObj.executeHeightMode === "WGS84" |
| | | const executeHeightMode = lineObj.executeHeightMode === 'WGS84' |
| | | |
| | | let filePositions = lineObj.Placemark.map(item => { |
| | | const [lng, lat] = item.Point.coordinates.split(',') |
| | |
| | | let safetyHeight |
| | | |
| | | if (executeHeightMode) { |
| | | let startHeight = new Decimal(missionConfig.takeOffSecurityHeight) |
| | | .plus(dronePosition.height).toNumber() |
| | | let startHeight = new Decimal(missionConfig.takeOffSecurityHeight).plus(dronePosition?.height || 0).toNumber() |
| | | |
| | | safetyHeight = startHeight < filePositions[0].alt ? filePositions[0].alt : startHeight |
| | | } else { |
| | | safetyHeight = missionConfig.takeOffSecurityHeight < filePositions[0].alt ? filePositions[0].alt : missionConfig.takeOffSecurityHeight |
| | | safetyHeight = |
| | | missionConfig.takeOffSecurityHeight < filePositions[0].alt |
| | | ? filePositions[0].alt |
| | | : missionConfig.takeOffSecurityHeight |
| | | } |
| | | |
| | | let safetyPositions = [] |
| | |
| | | }) |
| | | |
| | | this.updataMultiNestData(device_sn, { |
| | | currentRoutePositions |
| | | currentRoutePositions, |
| | | }) |
| | | |
| | | if ([0].includes(wayline_type)) { |
| | | let startHeight = executeHeightMode ? filePositions[0].alt : filePositions[0].alt + (dronePosition != null ? dronePosition.height : 0) |
| | | let endHeight = executeHeightMode ? filePositions[1].alt : filePositions[1].alt + (dronePosition != null ? dronePosition.height : 0) |
| | | let startHeight = executeHeightMode |
| | | ? filePositions[0].alt |
| | | : filePositions[0].alt + (dronePosition != null ? dronePosition.height : 0) |
| | | let endHeight = executeHeightMode |
| | | ? filePositions[1].alt |
| | | : filePositions[1].alt + (dronePosition != null ? dronePosition.height : 0) |
| | | |
| | | this.updataMultiNestData(device_sn, { |
| | | showPopupPosition: Cesium.Cartesian3.midpoint( |
| | | Cesium.Cartesian3.fromDegrees(filePositions[0].lng, filePositions[0].lat, startHeight), |
| | | Cesium.Cartesian3.fromDegrees(filePositions[1].lng, filePositions[1].lat, endHeight), |
| | | new Cesium.Cartesian3()) |
| | | new Cesium.Cartesian3() |
| | | ), |
| | | }) |
| | | } else { |
| | | let startHeight = executeHeightMode ? safetyPositions[1].alt : safetyPositions[1].alt + (dronePosition != null ? dronePosition.height : 0) |
| | | let endHeight = executeHeightMode ? safetyPositions[2].alt : safetyPositions[2].alt + (dronePosition != null ? dronePosition.height : 0) |
| | | let startHeight = executeHeightMode |
| | | ? safetyPositions[1].alt |
| | | : safetyPositions[1].alt + (dronePosition != null ? dronePosition.height : 0) |
| | | let endHeight = executeHeightMode |
| | | ? safetyPositions[2].alt |
| | | : safetyPositions[2].alt + (dronePosition != null ? dronePosition.height : 0) |
| | | |
| | | this.updataMultiNestData(device_sn, { |
| | | showPopupPosition: Cesium.Cartesian3.midpoint( |
| | | Cesium.Cartesian3.fromDegrees(safetyPositions[1].lng, safetyPositions[1].lat, startHeight), |
| | | Cesium.Cartesian3.fromDegrees(safetyPositions[2].lng, safetyPositions[2].lat, endHeight), |
| | | new Cesium.Cartesian3()) |
| | | new Cesium.Cartesian3() |
| | | ), |
| | | }) |
| | | } |
| | | |
| | |
| | | |
| | | return { |
| | | ...item, |
| | | alt: Number(height) |
| | | alt: Number(height), |
| | | } |
| | | }) |
| | | }), |
| | | } |
| | | } |
| | | |
| | |
| | | return i?.name && i?.name.includes('work-drone-route-') |
| | | }) |
| | | |
| | | Array.isArray(entities) && entities.forEach(item => { |
| | | this.viewer?.entities.remove(item) |
| | | }) |
| | | Array.isArray(entities) && |
| | | entities.forEach(item => { |
| | | this.viewer?.entities.remove(item) |
| | | }) |
| | | } |
| | | |
| | | // 当前无人机视椎体相关 |
| | |
| | | ['wide', 'ir', 'zoom'].includes(curDroneData?.camera_type) |
| | | ) { |
| | | if (curDroneData.camera_type === 'wide') { |
| | | fov = 60 |
| | | fov = 65 |
| | | } else { |
| | | const diffFocal = zoomFactor.value[curDroneData.camera_type].max - getZoomFactor(host?.cameras?.[0], curDroneData.camera_type) |
| | | |
| | | fov = (60 / zoomFactor.value[curDroneData.camera_type].max) * (diffFocal === 0 ? 1 : diffFocal) |
| | | const cameraMultiplier = getZoomFactor(host?.cameras?.[0], curDroneData.camera_type) |
| | | switch (cameraMultiplier) { |
| | | case 2: fov = 30; break |
| | | case 3: fov = 19; break |
| | | case 4: fov = 15; break |
| | | case 5: fov = 13; break |
| | | case 6: fov = 11; break |
| | | case 7: fov = 9; break |
| | | case 8: fov = 8.34; break |
| | | case 9: fov = 7.68; break |
| | | case 10: fov = 6.9; break |
| | | case 14: fov = 5; break |
| | | default: fov = 55.946 * Math.exp(-0.4504 * cameraMultiplier) + 6.323 |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | pitch: 0, |
| | | heading: attitude_head, |
| | | |
| | | isShowVideoPlan: this.isShowVideoPlan |
| | | }) |
| | | isShowVideoPlan: this.isShowVideoPlan, |
| | | }), |
| | | }) |
| | | } |
| | | } |
| | |
| | | |
| | | if (device_sn && live_status) { |
| | | this.updataMultiNestData(device_sn, { |
| | | camera_type: live_status?.[0].video_type || 'wide' |
| | | camera_type: live_status?.[0].video_type || 'wide', |
| | | }) |
| | | } |
| | | } |
| | |
| | | } |
| | | } |
| | | |
| | | // 设置当前无人机视椎体信息面板 |
| | | setFrustumInfoDetails (data) { |
| | | const { |
| | | dock_sn, |
| | | distance_to_airport, |
| | | distance_to_next_point, |
| | | estimated_time_to_next_point, |
| | | plane_mileage |
| | | } = data |
| | | const { dock_sn, distance_to_airport, distance_to_next_point, estimated_time_to_next_point, plane_mileage } = data |
| | | |
| | | if (dock_sn) { |
| | | const aircraftEntity = this.viewer?.entities.values.find(i => { |
| | |
| | | } |
| | | |
| | | aircraftEntity.label = new Cesium.LabelGraphics({ |
| | | text: `离机场平面距离:${distance_to_airport}m\n离下个航点平面距离:${Math.round(distance_to_next_point)}m\n到达下个航点剩余时间:${arrivalTime}\n航线总平面里程:${Math.round(plane_mileage)}m`, |
| | | text: `离机场平面距离:${distance_to_airport}m\n离下个航点平面距离:${Math.round( |
| | | distance_to_next_point |
| | | )}m\n到达下个航点剩余时间:${arrivalTime}\n航线总平面里程:${Math.round(plane_mileage)}m`, |
| | | font: '13px monospace', |
| | | showBackground: true, |
| | | horizontalOrigin: Cesium.HorizontalOrigin.CENTER, |
| | |
| | | |
| | | return |
| | | } |
| | | |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | // 设置无人机模型 |
| | | setAircraftGltf (host) { |
| | | const parent_sn = host?.parent_sn |
| | | const attitude_head = Number(host?.payloads?.[0]?.gimbal_yaw) - 90 || 0 |
| | | |
| | | if (parent_sn) { |
| | | const aircraftEntity = this.viewer?.entities.values.find(i => { |
| | |
| | | }) |
| | | |
| | | const position = Cesium.Cartesian3.fromDegrees(host?.longitude, host?.latitude, host?.height) |
| | | // ✅ 新增:Heading / Pitch / Roll(单位:度 -> 弧度) |
| | | const heading = Cesium.Math.toRadians(attitude_head) // 航向角 |
| | | const pitch = Cesium.Math.toRadians(0) // 俯仰角 |
| | | const roll = Cesium.Math.toRadians(0) // 横滚角 |
| | | |
| | | // ✅ 新增:构造 HeadingPitchRoll 和 四元数 |
| | | const hpr = new Cesium.HeadingPitchRoll(heading, pitch, roll) |
| | | const orientation = Cesium.Transforms.headingPitchRollQuaternion(position, hpr) |
| | | |
| | | if (aircraftEntity) { |
| | | aircraftEntity.position = new Cesium.ConstantPositionProperty(position) |
| | | aircraftEntity.orientation = new Cesium.ConstantProperty(orientation) |
| | | |
| | | return |
| | | } |
| | |
| | | this.viewer?.entities.add({ |
| | | name: `aircraft-glf-${parent_sn}`, |
| | | position, |
| | | orientation, |
| | | label: {}, |
| | | model: { |
| | | uri: aircraftGltf, // 或 .glb |
| | | scale: 1.0, // 缩放比例 |
| | | scale: 64, // 缩放比例 |
| | | minimumPixelSize: 64, // 最小像素尺寸(保证模型远处可见) |
| | | maximumScale: 128, // 最大缩放(可选) |
| | | maximumScale: 64, // 最大缩放(可选) |
| | | }, |
| | | }) |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 移除无人机模型 |
| | | removeAircraftGltf (host) { |
| | | const parent_sn = host?.parent_sn |
| | | |
| | | if (parent_sn) { |
| | | const aircraftEntity = this.viewer?.entities.values.find(i => { |
| | | return i?.name && i?.name.includes(`aircraft-glf-${parent_sn}`) |
| | | }) |
| | | |
| | | if (aircraftEntity) { |
| | | this.viewer?.entities.remove(aircraftEntity) |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 移除所有无人机视椎体 |
| | | removeViewInfoFrustum () { |
| | | this.multiNestData.forEach(item => { |
| | | if ('viewInfoFrustum' in item && item.viewInfoFrustum) { |
| | |
| | | return i?.name && i?.name.includes(`aircraft-glf-${parent_sn}`) |
| | | }) |
| | | |
| | | Array.isArray(entities) && entities.forEach(item => { |
| | | this.viewer?.entities.remove(item) |
| | | }) |
| | | Array.isArray(entities) && |
| | | entities.forEach(item => { |
| | | this.viewer?.entities.remove(item) |
| | | }) |
| | | } |
| | | } |
| | | |
| | |
| | | return i?.name && i?.name.includes(`aircraft-glf-`) |
| | | }) |
| | | |
| | | Array.isArray(entities) && entities.forEach(item => { |
| | | this.viewer?.entities.remove(item) |
| | | }) |
| | | Array.isArray(entities) && |
| | | entities.forEach(item => { |
| | | this.viewer?.entities.remove(item) |
| | | }) |
| | | |
| | | this.multiNestData.length > 0 && (this.multiNestData.forEach(item => { |
| | | item.viewInfoFrustum?.clear() |
| | | })) |
| | | this.multiNestData.length > 0 && |
| | | this.multiNestData.forEach(item => { |
| | | item.viewInfoFrustum?.clear() |
| | | }) |
| | | } |
| | | |
| | | /** |
| | |
| | | this.multiNestData[isHaveInd] = { |
| | | ...this.multiNestData[isHaveInd], |
| | | |
| | | ...data |
| | | ...data, |
| | | } |
| | | } else { |
| | | this.multiNestData.push({ |
| | | device_sn, |
| | | ...data |
| | | ...data, |
| | | }) |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 根据机巢sn获取当前机巢对应的相关信息 |
| | | * @param {*} device_sn |
| | | * @param {*} device_sn |
| | | */ |
| | | getCurDroneData (device_sn) { |
| | | const isHave = this.multiNestData.find(item => item.device_sn === device_sn) |
| | |
| | | return { |
| | | ...item, |
| | | show: true, |
| | | uuid: uuidv4() |
| | | uuid: uuidv4(), |
| | | } |
| | | }) |
| | | this.viewer.scene.postRender.addEventListener(that.labelBoxRenderHandler) |
| | | } |
| | | |
| | | showSingleDetailMapPopup (data) { |
| | | this.detailsMapPopupData.forEach(i => i.job_id === data.job_id && i.device_sn === data.device_sn && (i.show = true)) |
| | | this.detailsMapPopupData.forEach( |
| | | i => i?.job_id === data?.job_id && i?.device_sn === data?.device_sn && (i.show = true) |
| | | ) |
| | | } |
| | | |
| | | getLabelDom (data) { |
| | |
| | | tooltipContainer.style.position = 'absolute' |
| | | tooltipContainer.style.transform = 'translate(-50%,10%)' |
| | | tooltipContainer.style.pointerEvents = 'none' |
| | | document.querySelector('.content-map-popups')?.append(tooltipContainer) |
| | | document.querySelector('.content-map-popups').append(tooltipContainer) |
| | | render(vNode, tooltipContainer) |
| | | return tooltipContainer |
| | | } |
| | | |
| | | labelBoxRender () { |
| | | this.detailsMapPopupData.filter(i => i.show === true).forEach(item => { |
| | | const { showPopupPosition } = this.getCurDroneData(item.device_sn) |
| | | this.detailsMapPopupData |
| | | .filter(i => i.show === true) |
| | | .forEach(item => { |
| | | const { showPopupPosition } = this.getCurDroneData(item.device_sn) |
| | | |
| | | let dom = document.querySelector(`[id="${item.uuid}"]`) |
| | | let dom = document.querySelector(`[id="${item.uuid}"]`) |
| | | |
| | | if (!dom) { |
| | | dom = this.getLabelDom(item) |
| | | } |
| | | if (!dom) { |
| | | dom = this.getLabelDom(item) |
| | | } |
| | | |
| | | const screenPosition = this.viewer?.scene.cartesianToCanvasCoordinates(showPopupPosition) |
| | | if (screenPosition) { |
| | | dom.style.left = `${screenPosition.x}px` |
| | | dom.style.top = `${screenPosition.y}px` |
| | | dom.style.display = 'block' |
| | | } |
| | | }) |
| | | const screenPosition = this.viewer?.scene.cartesianToCanvasCoordinates(showPopupPosition) |
| | | if (screenPosition) { |
| | | dom.style.left = `${screenPosition.x}px` |
| | | dom.style.top = `${screenPosition.y}px` |
| | | dom.style.display = 'block' |
| | | } |
| | | }) |
| | | } |
| | | |
| | | // 移除弹框标签 |
| | |
| | | const that = this |
| | | that.viewer?.scene.postRender.removeEventListener(that.labelBoxRenderHandler) |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * @Author : yuan |
| | | * @Date : 2025-08-16 17:01:46 |
| | | * @LastEditors : yuan |
| | | * @LastEditTime : 2025-09-26 16:27:20 |
| | | * @FilePath : \src\utils\cesium\frustum\CesiumVideoFrustum.js |
| | | * @Description : |
| | | * Copyright 2025 OBKoro1, All Rights Reserved. |
| | | * 2025-08-16 17:01:46 |
| | | */ |
| | | import * as Cesium from 'cesium' |
| | | |
| | | var videoShed3dShader = ` |
| | | uniform float ztzf_frustum_opacity; |
| | | |
| | | uniform sampler2D ztzf_frustum_videoTexture; |
| | | uniform sampler2D ztzf_frustum_maskTexture; |
| | | uniform vec4 ztzf_frustum_hiddenAreaColor; |
| | | |
| | | uniform sampler2D shadowMap_texture; |
| | | uniform mat4 shadowMap_matrix; |
| | | uniform vec4 shadowMap_lightPositionEC; |
| | | uniform vec4 shadowMap_texelSizeDepthBias; |
| | | uniform vec4 shadowMap_normalOffsetScale; |
| | | uniform bool ztzf_frustum_flipx; |
| | | uniform bool ztzf_frustum_flipy; |
| | | |
| | | // 新增边框参数 |
| | | uniform vec4 ztzf_frustum_borderColor; // 边框颜色(RGBA) |
| | | uniform float ztzf_frustum_borderWidth; // 边框宽度(0.0-1.0) |
| | | |
| | | uniform sampler2D colorTexture; |
| | | uniform sampler2D depthTexture; |
| | | in vec2 v_textureCoordinates; |
| | | |
| | | vec4 toEye(in vec2 uv, in float depth) { |
| | | vec2 xy = vec2((uv.x * 2.0 - 1.0), (uv.y * 2.0 - 1.0)); |
| | | vec4 posInCamera = czm_inverseProjection * vec4(xy, depth, 1.0); |
| | | posInCamera = posInCamera / posInCamera.w; |
| | | return posInCamera; |
| | | } |
| | | |
| | | float getDepthMars3D(in vec4 depth) { |
| | | float z_window = czm_unpackDepth(depth); |
| | | z_window = czm_reverseLogDepth(z_window); |
| | | float n_range = czm_depthRange.near; |
| | | float f_range = czm_depthRange.far; |
| | | return (2.0 * z_window - n_range - f_range) / (f_range - n_range); |
| | | } |
| | | float _czm_sampleShadowMap(sampler2D shadowMap, vec2 uv) { |
| | | return texture(shadowMap, uv).r; |
| | | } |
| | | float _czm_shadowDepthCompare(sampler2D shadowMap, vec2 uv, float depth) { |
| | | return step(depth, _czm_sampleShadowMap(shadowMap, uv)); |
| | | } |
| | | float _czm_shadowVisibility(sampler2D shadowMap, czm_shadowParameters shadowParameters) { |
| | | float depthBias = shadowParameters.depthBias; |
| | | float depth = shadowParameters.depth; |
| | | float nDotL = shadowParameters.nDotL; |
| | | float normalShadingSmooth = shadowParameters.normalShadingSmooth; |
| | | float darkness = shadowParameters.darkness; |
| | | vec2 uv = shadowParameters.texCoords; |
| | | depth -= depthBias; |
| | | vec2 texelStepSize = shadowParameters.texelStepSize; |
| | | float radius = 1.0; |
| | | float dx0 = -texelStepSize.x * radius; |
| | | float dy0 = -texelStepSize.y * radius; |
| | | float dx1 = texelStepSize.x * radius; |
| | | float dy1 = texelStepSize.y * radius; |
| | | float visibility = (_czm_shadowDepthCompare(shadowMap, uv, depth) + |
| | | _czm_shadowDepthCompare(shadowMap, uv + vec2(dx0, dy0), depth) + |
| | | _czm_shadowDepthCompare(shadowMap, uv + vec2(0.0, dy0), depth) + |
| | | _czm_shadowDepthCompare(shadowMap, uv + vec2(dx1, dy0), depth) + |
| | | _czm_shadowDepthCompare(shadowMap, uv + vec2(dx0, 0.0), depth) + |
| | | _czm_shadowDepthCompare(shadowMap, uv + vec2(dx1, 0.0), depth) + |
| | | _czm_shadowDepthCompare(shadowMap, uv + vec2(dx0, dy1), depth) + |
| | | _czm_shadowDepthCompare(shadowMap, uv + vec2(0.0, dy1), depth) + |
| | | _czm_shadowDepthCompare(shadowMap, uv + vec2(dx1, dy1), depth)) * (1.0 / 9.0); |
| | | return visibility; |
| | | } |
| | | |
| | | vec3 pointProjectOnPlane(in vec3 planeNormal, in vec3 planeOrigin, in vec3 point) { |
| | | vec3 v01 = point - planeOrigin; |
| | | float d = dot(planeNormal, v01); |
| | | return (point - planeNormal * d); |
| | | } |
| | | float ptm(vec3 pt) { |
| | | return sqrt(pt.x * pt.x + pt.y * pt.y + pt.z * pt.z); |
| | | } |
| | | |
| | | void main() { |
| | | const float PI = 3.141592653589793; |
| | | vec4 color = texture(colorTexture, v_textureCoordinates); |
| | | vec4 currD = texture(depthTexture, v_textureCoordinates); |
| | | |
| | | float depth = getDepthMars3D(currD); |
| | | vec4 positionEC = toEye(v_textureCoordinates, depth); |
| | | vec3 normalEC = vec3(1.0); |
| | | czm_shadowParameters shadowParameters; |
| | | shadowParameters.texelStepSize = shadowMap_texelSizeDepthBias.xy; |
| | | shadowParameters.depthBias = shadowMap_texelSizeDepthBias.z; |
| | | shadowParameters.normalShadingSmooth = shadowMap_texelSizeDepthBias.w; |
| | | shadowParameters.darkness = shadowMap_normalOffsetScale.w; |
| | | shadowParameters.depthBias *= max(depth * 0.01, 1.0); |
| | | |
| | | vec3 directionEC = normalize(positionEC.xyz - shadowMap_lightPositionEC.xyz); |
| | | float nDotL = clamp(dot(normalEC, -directionEC), 0.0, 1.0); |
| | | vec4 shadowPosition = shadowMap_matrix * positionEC; |
| | | shadowPosition /= shadowPosition.w; |
| | | if(any(lessThan(shadowPosition.xyz, vec3(0.0))) || any(greaterThan(shadowPosition.xyz, vec3(1.0)))) { |
| | | out_FragColor = color; |
| | | return; |
| | | } |
| | | |
| | | shadowParameters.texCoords = shadowPosition.xy; |
| | | shadowParameters.depth = shadowPosition.z; |
| | | shadowParameters.nDotL = nDotL; |
| | | float visibility = _czm_shadowVisibility(shadowMap_texture, shadowParameters); |
| | | |
| | | //视频投射 |
| | | if(visibility == 1.0) { |
| | | if(ztzf_frustum_flipx){ |
| | | shadowPosition.x = shadowPosition.x + (0.5 - shadowPosition.x) * 2.0; |
| | | } |
| | | if(ztzf_frustum_flipy){ |
| | | shadowPosition.y = shadowPosition.y + (0.5 - shadowPosition.y) * 2.0; |
| | | } |
| | | |
| | | vec4 videoColor = texture(ztzf_frustum_videoTexture, shadowPosition.xy); |
| | | vec4 maskColor = texture(ztzf_frustum_maskTexture, shadowPosition.xy); |
| | | videoColor *= maskColor; |
| | | |
| | | // 计算到边缘的距离 |
| | | float distToEdge = min( |
| | | min(shadowPosition.x, 1.0 - shadowPosition.x), |
| | | min(shadowPosition.y, 1.0 - shadowPosition.y) |
| | | ); |
| | | |
| | | // 混合视频颜色和背景 |
| | | vec4 finalColor = mix(color, vec4(videoColor.xyz, 1.0), ztzf_frustum_opacity * videoColor.a); |
| | | |
| | | // 添加边框效果 |
| | | if(distToEdge < ztzf_frustum_borderWidth && videoColor.a > 0.0) { |
| | | float borderFactor = smoothstep(0.0, ztzf_frustum_borderWidth, distToEdge); |
| | | finalColor = mix(ztzf_frustum_borderColor, finalColor, borderFactor); |
| | | } |
| | | |
| | | out_FragColor = finalColor; |
| | | } else { |
| | | if(abs(shadowPosition.z - 0.0) < 0.01) { |
| | | return; |
| | | } |
| | | out_FragColor = vec4(mix(color.rgb, ztzf_frustum_hiddenAreaColor.rgb, ztzf_frustum_hiddenAreaColor.a), ztzf_frustum_hiddenAreaColor.a); |
| | | } |
| | | } |
| | | ` |
| | | |
| | | class CesiumVideoFrustum { |
| | | constructor(viewer, param) { |
| | | this.param = param |
| | | |
| | | var option = this._initCameraParam() |
| | | |
| | | if (option || (option = {}), this.viewer = viewer, this._cameraPosition = option.cameraPosition, this._position = option.position, |
| | | this.type = option.type, this._alpha = option.alpha || 0, this.element = option.element, this.color = option.color, |
| | | this._debugFrustum = Cesium.defaultValue(option.debugFrustum, !0), this._aspectRatio = option.aspectRatio || this._getWinWidHei(), |
| | | this._camerafov = option.fov || Cesium.Math.toDegrees(this.viewer.scene.camera.frustum.fov), |
| | | this._isShadowMap = option.isShadowMap || false, |
| | | this.texture = option.texture || new Cesium.Texture({ |
| | | context: this.viewer.scene.context, |
| | | source: { |
| | | width: 1, |
| | | height: 1, |
| | | arrayBufferView: new Uint8Array([255, 255, 255, 255]) |
| | | }, |
| | | flipY: !1 |
| | | }), this.defaultShow = Cesium.defaultValue(option.show, !0), !this.cameraPosition || !this.position) return void console.log('初始化失败:请确认相机位置与视点位置正确!') |
| | | |
| | | this.activeVideo() |
| | | this._createShadowMap() |
| | | this._getOrientation() |
| | | this._addCameraFrustum() |
| | | this._addPostProcess() |
| | | this.viewer.scene.primitives.add(this) |
| | | } |
| | | |
| | | get alpha () { |
| | | return this._alpha |
| | | } |
| | | |
| | | set alpha (e) { |
| | | this._alpha = e |
| | | this._changeViewPos() |
| | | } |
| | | |
| | | get aspectRatio () { |
| | | return this._aspectRatio |
| | | } |
| | | |
| | | set aspectRatio (e) { |
| | | this._aspectRatio = e |
| | | this._changeVideoWidHei() |
| | | } |
| | | |
| | | get debugFrustum () { |
| | | return this._debugFrustum |
| | | } |
| | | |
| | | set debugFrustum (e) { |
| | | this._debugFrustum = e |
| | | this.cameraFrustum.show = e |
| | | this.cameraFrustumOutline.show = e |
| | | } |
| | | |
| | | get fov () { |
| | | return this._camerafov |
| | | } |
| | | |
| | | set fov (e) { |
| | | this._camerafov = e |
| | | this._changeCameraFov() |
| | | } |
| | | |
| | | get cameraPosition () { |
| | | return this._cameraPosition |
| | | } |
| | | |
| | | set cameraPosition (e) { |
| | | e && (this._cameraPosition = e, this._changeCameraPos()) |
| | | } |
| | | |
| | | get isShadowMap () { |
| | | return this._isShadowMap |
| | | } |
| | | |
| | | set isShadowMap (e) { |
| | | this._isShadowMap = e |
| | | this._changeCameraIsShadowMap() |
| | | } |
| | | |
| | | get position () { |
| | | return this._position |
| | | } |
| | | |
| | | set position (e) { |
| | | e && (this._position = e, this._changeViewPos()) |
| | | } |
| | | |
| | | get params () { |
| | | var t = {} |
| | | return t.element = this.element, |
| | | t.position = this.position, |
| | | t.cameraPosition = this.cameraPosition, |
| | | t.fov = this.fov, |
| | | t.isShadowMap = this.isShadowMap, |
| | | t.aspectRatio = this.aspectRatio, |
| | | t.alpha = this.alpha, |
| | | t.debugFrustum = this.debugFrustum, |
| | | t.show = this.show |
| | | } |
| | | |
| | | get show () { |
| | | return this.defaultShow |
| | | } |
| | | |
| | | set show (e) { |
| | | this.defaultShow = Boolean(e), |
| | | this._switchShow() |
| | | } |
| | | |
| | | isDestroyed () { |
| | | return false |
| | | } |
| | | |
| | | _initCameraParam () { |
| | | var cameraPosition = Cesium.Cartesian3.fromDegrees(this.param.position.x * 1, this.param.position.y * 1, this.param.position.z * 1) |
| | | |
| | | const hpr = Cesium.HeadingPitchRoll.fromDegrees(this.param.rotation.y * 1, this.param.rotation.x * 1, 0) |
| | | const orientation = Cesium.Transforms.headingPitchRollQuaternion(cameraPosition, hpr) |
| | | |
| | | let matrix = Cesium.Matrix3.fromQuaternion( |
| | | orientation, |
| | | new Cesium.Matrix3() |
| | | ) |
| | | |
| | | let direction = Cesium.Matrix3.getColumn(matrix, 2, new Cesium.Cartesian3()) |
| | | |
| | | let frontDirect = Cesium.Cartesian3.multiplyByScalar( |
| | | direction, |
| | | 250, |
| | | new Cesium.Cartesian3() |
| | | ) |
| | | let position = Cesium.Cartesian3.add(cameraPosition, frontDirect, new Cesium.Cartesian3()) |
| | | |
| | | return { |
| | | cameraPosition: cameraPosition, |
| | | position: position, |
| | | alpha: this.param.alpha, |
| | | near: this.param.near, |
| | | fov: this.param.fov, |
| | | isShadowMap: this.param.isShadowMap, |
| | | debugFrustum: this.param.debugFrustum, |
| | | aspectRatio: this.param.aspectRatio, |
| | | element: this.param.element, |
| | | } |
| | | } |
| | | |
| | | _changeAlpha (e) { |
| | | this.alpha = e |
| | | } |
| | | |
| | | _changeAspectRatio (e) { |
| | | this.aspectRatio = e |
| | | } |
| | | |
| | | _changeRotation (e) { |
| | | if (e) { |
| | | this.param.rotation = e |
| | | var option = this._initCameraParam() |
| | | this.position = option.position |
| | | } |
| | | } |
| | | |
| | | _changeCameraPosition (e) { |
| | | if (e) { |
| | | this.param.position = e |
| | | var option = this._initCameraParam() |
| | | this.cameraPosition = option.cameraPosition |
| | | } |
| | | } |
| | | |
| | | _changeFov (e) { |
| | | if (e) { |
| | | this.param.fov = e |
| | | var option = this._initCameraParam() |
| | | this.fov = option.fov |
| | | } |
| | | } |
| | | |
| | | _changeIsShadowMap (e) { |
| | | this.param.isShadowMap = e |
| | | var option = this._initCameraParam() |
| | | this.isShadowMap = option.isShadowMap |
| | | } |
| | | |
| | | _changeFar (e) { |
| | | if (e) { |
| | | this.param.far = e |
| | | var option = this._initCameraParam() |
| | | this.position = option.position |
| | | } |
| | | } |
| | | |
| | | _changeNear (e) { |
| | | if (e) { |
| | | this.param.near = e |
| | | this.near = this.param.near |
| | | this._changeCameraPos() |
| | | } |
| | | } |
| | | |
| | | _getWinWidHei () { |
| | | var viewer = this.viewer.scene |
| | | return viewer.canvas.clientWidth / viewer.canvas.clientHeight |
| | | } |
| | | |
| | | _changeCameraFov () { |
| | | this.postProcess && this.viewer.scene.postProcessStages.remove(this.postProcess) |
| | | |
| | | this.viewer.scene.primitives.remove(this.cameraFrustum), |
| | | this.viewer.scene.primitives.remove(this.cameraFrustumOutline), |
| | | this._createShadowMap(this.cameraPosition, this.position), |
| | | this._getOrientation(), |
| | | this._addCameraFrustum(), |
| | | this._addPostProcess() |
| | | } |
| | | |
| | | _changeCameraIsShadowMap () { |
| | | this.postProcess && this.viewer.scene.postProcessStages.remove(this.postProcess) |
| | | |
| | | this.viewShadowMap && this.viewShadowMap.destroy() |
| | | this.viewShadowMap = null |
| | | |
| | | this.viewer.scene.primitives.remove(this.cameraFrustum), |
| | | this.viewer.scene.primitives.remove(this.cameraFrustumOutline), |
| | | this._createShadowMap(this.cameraPosition, this.position), |
| | | this._getOrientation(), |
| | | this._addCameraFrustum(), |
| | | this._addPostProcess() |
| | | } |
| | | |
| | | _changeVideoWidHei () { |
| | | this.postProcess && this.viewer.scene.postProcessStages.remove(this.postProcess) |
| | | |
| | | this.viewer.scene.primitives.remove(this.cameraFrustum), |
| | | this.viewer.scene.primitives.remove(this.cameraFrustumOutline), |
| | | this._createShadowMap(this.cameraPosition, this.position), |
| | | this._getOrientation(), |
| | | this._addCameraFrustum(), |
| | | this._addPostProcess() |
| | | } |
| | | |
| | | _changeCameraPos () { |
| | | this.postProcess && this.viewer.scene.postProcessStages.remove(this.postProcess) |
| | | |
| | | this.viewShadowMap && this.viewShadowMap.destroy() |
| | | this.viewShadowMap = null |
| | | |
| | | this.viewer.scene.primitives.remove(this.cameraFrustum), |
| | | this.viewer.scene.primitives.remove(this.cameraFrustumOutline), |
| | | // this.cameraFrustum.destroy(), |
| | | this._createShadowMap(this.cameraPosition, this.position), |
| | | this._getOrientation(), |
| | | this._addCameraFrustum(), |
| | | this._addPostProcess() |
| | | } |
| | | |
| | | _changeViewPos () { |
| | | this.postProcess && this.viewer.scene.postProcessStages.remove(this.postProcess) |
| | | |
| | | this.viewShadowMap && this.viewShadowMap.destroy() |
| | | this.viewShadowMap = null |
| | | |
| | | this.viewer.scene.primitives.remove(this.cameraFrustum), |
| | | this.viewer.scene.primitives.remove(this.cameraFrustumOutline), |
| | | // this.cameraFrustum.destroy(), |
| | | this._createShadowMap(this.cameraPosition, this.position), |
| | | this._getOrientation(), |
| | | this._addCameraFrustum(), |
| | | this._addPostProcess() |
| | | } |
| | | |
| | | _switchShow () { |
| | | this.show ? !this.postProcess && this._addPostProcess() : (this.postProcess && this.viewer.scene.postProcessStages.remove(this.postProcess), delete this.postProcess, this.postProcess = null), |
| | | this.cameraFrustum.show = this.show, |
| | | this.cameraFrustumOutline.show = this.show |
| | | } |
| | | |
| | | activeVideo () { |
| | | var video = this.element, |
| | | that = this |
| | | if (video) { |
| | | var viewer = this.viewer |
| | | this.activeVideoListener || (this.activeVideoListener = function () { |
| | | that.videoTexture && that.videoTexture.destroy(), |
| | | that.videoTexture = new Cesium.Texture({ |
| | | context: viewer.scene.context, |
| | | source: video, |
| | | width: 1, |
| | | height: 1, |
| | | pixelFormat: Cesium.PixelFormat.RGBA, |
| | | pixelDatatype: Cesium.PixelDatatype.UNSIGNED_BYTE |
| | | }) |
| | | }), |
| | | viewer.clock.onTick.addEventListener(this.activeVideoListener) |
| | | } |
| | | } |
| | | |
| | | locate () { |
| | | var cameraPosition = Cesium.clone(this.cameraPosition), |
| | | position = Cesium.clone(this.position) |
| | | this.viewer.Camera.position = cameraPosition, |
| | | this.viewer.camera.direction = Cesium.Cartesian3.subtract(position, cameraPosition, new Cesium.Cartesian3(0, 0, 0)), |
| | | this.viewer.camera.up = Cesium.Cartesian3.normalize(cameraPosition, new Cesium.Cartesian3(0, 0, 0)) |
| | | } |
| | | |
| | | update (e) { |
| | | this.viewShadowMap && this.viewer.scene.frameState.shadowMaps.push(this.viewShadowMap) // *重点* 多投影 |
| | | } |
| | | |
| | | _createShadowMap () { |
| | | var e = Cesium.Cartesian3.fromDegrees(this.param.position.x * 1, this.param.position.y * 1, this.param.position.z * 1) |
| | | var t = this.position |
| | | var i = this.viewer.scene |
| | | var a = new Cesium.Camera(i) |
| | | a.position = e |
| | | |
| | | this.customFrustum = new Cesium.PerspectiveFrustum({ |
| | | fov: Cesium.Math.toRadians(this.fov), |
| | | aspectRatio: this.aspectRatio, |
| | | near: this.near, |
| | | far: 250 |
| | | }) |
| | | |
| | | a.frustum = new Cesium.PerspectiveFrustum({ |
| | | fov: Cesium.Math.toRadians(this.fov), |
| | | aspectRatio: this.aspectRatio, |
| | | near: this.near, |
| | | far: 250 |
| | | }) |
| | | |
| | | let curOrientation = {} |
| | | |
| | | const headingPitchRoll = new Cesium.HeadingPitchRoll( |
| | | Cesium.Math.toRadians(this.param.rotation.y, 0), |
| | | Cesium.Math.toRadians(this.param.rotation.x, 0), |
| | | Cesium.Math.toRadians(0), |
| | | ) |
| | | |
| | | curOrientation['heading'] = headingPitchRoll['heading'] |
| | | curOrientation['pitch'] = headingPitchRoll['pitch'] |
| | | curOrientation['roll'] = headingPitchRoll['roll'] |
| | | |
| | | let curDestination = {} |
| | | |
| | | curDestination.destination = e |
| | | curDestination.orientation = curOrientation |
| | | |
| | | a.setView(curDestination) |
| | | |
| | | var n = Cesium.Cartesian3.distance(t, e) |
| | | this.viewDis = n |
| | | |
| | | if (!this.isShadowMap) return |
| | | |
| | | this.viewShadowMap = new Cesium.ShadowMap({ |
| | | lightCamera: a, |
| | | enable: !1, |
| | | isPointLight: !1, |
| | | isSpotLight: !0, |
| | | cascadesEnabled: !1, |
| | | context: i.context, |
| | | pointLightRadius: n, |
| | | }) |
| | | } |
| | | |
| | | _getOrientation () { |
| | | const hpr = Cesium.HeadingPitchRoll.fromDegrees(180 + this.param.rotation.y * 1, 0, 90 - this.param.rotation.x * 1 || 0) |
| | | const orientation = Cesium.Transforms.headingPitchRollQuaternion(this.cameraPosition, hpr) |
| | | |
| | | return this.orientation = orientation, |
| | | orientation |
| | | } |
| | | |
| | | _addCameraFrustum () { |
| | | var e = this |
| | | |
| | | this.cameraFrustum = new Cesium.Primitive({ |
| | | geometryInstances: new Cesium.GeometryInstance({ |
| | | geometry: new Cesium.FrustumGeometry({ |
| | | origin: e.cameraPosition, |
| | | orientation: e.orientation, |
| | | frustum: e.customFrustum, |
| | | _drawNearPlane: !0 |
| | | }), |
| | | attributes: { |
| | | color: Cesium.ColorGeometryInstanceAttribute.fromColor( |
| | | Cesium.Color.fromBytes(0, 213, 144, 20) |
| | | ), |
| | | } |
| | | }), |
| | | releaseGeometryInstances: false, |
| | | appearance: new Cesium.PerInstanceColorAppearance({ |
| | | translucent: true, |
| | | flat: !0, |
| | | closed: true, |
| | | }), |
| | | asynchronous: false, |
| | | show: this.debugFrustum && this.show |
| | | }) |
| | | |
| | | this.cameraFrustumOutline = new Cesium.Primitive({ |
| | | geometryInstances: new Cesium.GeometryInstance({ |
| | | geometry: new Cesium.FrustumOutlineGeometry({ |
| | | origin: e.cameraPosition, |
| | | orientation: e.orientation, |
| | | frustum: e.customFrustum, |
| | | _drawNearPlane: !0 |
| | | }), |
| | | attributes: { |
| | | color: Cesium.ColorGeometryInstanceAttribute.fromColor( |
| | | Cesium.Color.fromBytes(0, 213, 144, 255) |
| | | ), |
| | | } |
| | | }), |
| | | appearance: new Cesium.PerInstanceColorAppearance({ |
| | | closed: true, |
| | | flat: true, |
| | | translucent: true |
| | | }), |
| | | asynchronous: false, |
| | | show: this.debugFrustum && this.show |
| | | }) |
| | | |
| | | this.viewer.scene.primitives.add(this.cameraFrustum) |
| | | this.viewer.scene.primitives.add(this.cameraFrustumOutline) |
| | | } |
| | | |
| | | _addPostProcess () { |
| | | if (!this.isShadowMap) return |
| | | |
| | | var e = this, |
| | | t = videoShed3dShader |
| | | |
| | | const i = e.viewShadowMap['_primitiveBias'] |
| | | |
| | | this.postProcess = new Cesium.PostProcessStage({ |
| | | fragmentShader: t, |
| | | uniforms: { |
| | | ztzf_frustum_videoTexture: function () { |
| | | return e.videoTexture |
| | | }, |
| | | |
| | | ztzf_frustum_maskTexture: function () { |
| | | return new Cesium['Texture']({ |
| | | 'context': e.viewer.scene.context, |
| | | 'source': { |
| | | 'width': 0x1, |
| | | 'height': 0x1, |
| | | 'arrayBufferView': new Uint8Array([255, 255, 255, 255]) |
| | | }, |
| | | 'flipY': ![] |
| | | }) |
| | | }, |
| | | |
| | | ztzf_frustum_opacity: function () { |
| | | return e.alpha |
| | | }, |
| | | |
| | | ztzf_frustum_hiddenAreaColor: () => { |
| | | return new Cesium.Color(0, 0, 0, 0.5) |
| | | }, |
| | | |
| | | shadowMap_texture: function () { |
| | | if (!e.viewShadowMap || !e.viewShadowMap['_shadowMapTexture'] || e.viewShadowMap['_shadowMapTexture']['isDestroyed']()) { |
| | | return new Cesium['Texture']({ |
| | | 'context': e.viewer.scene.context, |
| | | 'source': { |
| | | 'width': 0x1, |
| | | 'height': 0x1, |
| | | 'arrayBufferView': new Uint8Array([0x0, 0x0, 0x0, 0x0]) |
| | | }, |
| | | 'flipY': ![] |
| | | }) |
| | | } |
| | | |
| | | return e.viewShadowMap._shadowMapTexture |
| | | }, |
| | | |
| | | shadowMap_matrix: function () { |
| | | return e.viewShadowMap._shadowMapMatrix |
| | | }, |
| | | |
| | | shadowMap_lightPositionEC: function () { |
| | | return e.viewShadowMap._lightPositionEC |
| | | }, |
| | | |
| | | shadowMap_texelSizeDepthBias: function () { |
| | | var t = new Cesium.Cartesian2 |
| | | return t.x = 1 / e.viewShadowMap._textureSize.x, |
| | | t.y = 1 / e.viewShadowMap._textureSize.y, |
| | | Cesium.Cartesian4.fromElements(t.x, t.y, i.depthBias, i.normalShadingSmooth, this.combinedUniforms1) |
| | | }, |
| | | |
| | | shadowMap_normalOffsetScale: function () { |
| | | return Cesium.Cartesian4.fromElements(i.normalOffsetScale, e.viewShadowMap._distance, e.viewShadowMap.maximumDistance, e.viewShadowMap._darkness, this.combinedUniforms2) |
| | | }, |
| | | |
| | | ztzf_frustum_flipx: () => { |
| | | return false |
| | | }, |
| | | |
| | | ztzf_frustum_flipy: () => { |
| | | return false |
| | | }, |
| | | |
| | | ztzf_frustum_borderColor: () => { |
| | | return new Cesium.Color(0 / 255, 213 / 255, 144 / 255, 255 / 255) |
| | | }, |
| | | |
| | | ztzf_frustum_borderWidth: () => { |
| | | return 0.01 |
| | | }, |
| | | } |
| | | }), |
| | | this.viewer.scene.postProcessStages.add(this.postProcess) |
| | | } |
| | | |
| | | destroy () { |
| | | this.viewer.scene.postProcessStages.remove(this.postProcess), |
| | | this.viewer.scene.primitives.remove(this.cameraFrustum), |
| | | this.viewer.scene.primitives.remove(this.cameraFrustumOutline), |
| | | //this._videoEle && this._videoEle.parentNode.removeChild(this._videoEle), |
| | | this.activeVideoListener && this.viewer.clock.onTick.removeEventListener(this.activeVideoListener), |
| | | this.activeVideoListener && delete this.activeVideoListener, |
| | | delete this.postProcess, |
| | | delete this.viewShadowMap, |
| | | delete this.color, |
| | | delete this.viewDis, |
| | | delete this.cameraPosition, |
| | | delete this.position, |
| | | delete this.alpha, |
| | | delete this._camerafov, |
| | | delete this._isShadowMap, |
| | | delete this._cameraPosition, |
| | | delete this.videoTexture, |
| | | delete this.cameraFrustum, |
| | | delete this.cameraFrustumOutline, |
| | | delete this._videoEle, |
| | | delete this._debugFrustum, |
| | | delete this._position, |
| | | delete this._aspectRatio, |
| | | delete this.element, |
| | | delete this.orientation, |
| | | delete this.texture, |
| | | delete this.videoId, |
| | | this.viewer.scene.primitives.remove(this), |
| | | delete this.viewer |
| | | } |
| | | } |
| | | |
| | | export default CesiumVideoFrustum |
| New file |
| | |
| | | /* |
| | | * @Author : yuan |
| | | * @Date : 2025-08-15 14:06:49 |
| | | * @LastEditors : yuan |
| | | * @LastEditTime : 2025-08-15 14:06:59 |
| | | * @FilePath : \src\utils\cesium\frustum\CoordinateTranslate.js |
| | | * @Description : |
| | | * Copyright 2025 OBKoro1, All Rights Reserved. |
| | | * 2025-08-15 14:06:49 |
| | | */ |
| | | let ECEF = (function () { |
| | | var _ = function () { |
| | | this.PI = 3.141592653589793238 |
| | | this.a = 6378137.0 |
| | | this.b = 6356752.3142 |
| | | this.f = (this.a - this.b) / this.a |
| | | this.e_sq = this.f * (2.0 - this.f) |
| | | this.ee = 0.00669437999013 |
| | | this.WGSF = 1 / 298.257223563 |
| | | this.WGSe2 = this.WGSF * (2 - this.WGSF) |
| | | this.WGSa = 6378137.00000 |
| | | this.EPSILON = 1.0e-12 |
| | | } |
| | | _.prototype.CalculateCoordinates = function (point, azimuth, elevation, distance) { |
| | | var vertical_height = distance * Math.sin(2 * this.PI / 360 * elevation)//垂直高度 |
| | | var horizontal_distance = distance * Math.cos(2 * this.PI / 360 * elevation)//水平距离 |
| | | if (azimuth > 360) azimuth = azimuth % 360 |
| | | if (azimuth < 0) azimuth = 360 + (azimuth % 360) |
| | | |
| | | var point1 = this.lonLat2WebMercator(point) |
| | | var lnglat = null |
| | | |
| | | var x_length, y_length |
| | | if (azimuth <= 90) {//第四象限 |
| | | x_length = horizontal_distance * Math.cos(2 * this.PI / 360 * azimuth) |
| | | y_length = horizontal_distance * Math.sin(2 * this.PI / 360 * azimuth) |
| | | lnglat = { |
| | | x: point1.x + x_length, |
| | | y: point1.y - y_length |
| | | } |
| | | } else if (azimuth > 90 && azimuth <= 180) {//第三象限 |
| | | x_length = horizontal_distance * Math.sin(2 * this.PI / 360 * (azimuth - 90)) |
| | | y_length = horizontal_distance * Math.cos(2 * this.PI / 360 * (azimuth - 90)) |
| | | lnglat = { |
| | | x: point1.x - x_length, |
| | | y: point1.y - y_length |
| | | } |
| | | } else if (azimuth > 180 && azimuth <= 270) {//第二象限 |
| | | x_length = horizontal_distance * Math.cos(2 * this.PI / 360 * (azimuth - 180)) |
| | | y_length = horizontal_distance * Math.sin(2 * this.PI / 360 * (azimuth - 180)) |
| | | lnglat = { |
| | | x: point1.x - x_length, |
| | | y: point1.y + y_length |
| | | } |
| | | } else {//第一象限 |
| | | x_length = horizontal_distance * Math.sin(2 * this.PI / 360 * (azimuth - 270)) |
| | | y_length = horizontal_distance * Math.cos(2 * this.PI / 360 * (azimuth - 270)) |
| | | lnglat = { |
| | | x: point1.x + x_length, |
| | | y: point1.y + y_length |
| | | } |
| | | } |
| | | lnglat = this.webMercator2LonLat(lnglat) |
| | | return { |
| | | lng: lnglat.x, |
| | | lat: lnglat.y, |
| | | height: vertical_height |
| | | } |
| | | } |
| | | /* |
| | | *经纬度转Web墨卡托 |
| | | *@lonLat 经纬度 |
| | | */ |
| | | _.prototype.lonLat2WebMercator = function (lonLat) { |
| | | let x = lonLat.x * this.a / 180 |
| | | let y = Math.log(Math.tan((90 + lonLat.y) * this.PI / 360)) / (this.PI / 180) |
| | | y = y * this.a / 180 |
| | | return { |
| | | x: x, |
| | | y: y |
| | | } |
| | | } |
| | | |
| | | /* |
| | | *Web墨卡托转经纬度 |
| | | *@mercator 平面坐标 |
| | | */ |
| | | _.prototype.webMercator2LonLat = function (mercator) { |
| | | let x = mercator.x / this.a * 180 |
| | | let y = mercator.y / this.a * 180 |
| | | y = 180 / this.PI * (2 * (Math.exp(y * this.PI / 180)) - this.PI / 2) |
| | | return { |
| | | x: x, |
| | | y: y |
| | | } |
| | | } |
| | | |
| | | _.prototype.get_atan = function (z, y) { |
| | | let x |
| | | if (z == 0) { |
| | | x = this.PI / 2 |
| | | } else { |
| | | if (y == 0) { |
| | | x = this.PI |
| | | } else { |
| | | x = Math.atan(Math.abs(y / z)) |
| | | if ((y > 0) && (z < 0)) { |
| | | x = this.PI - x |
| | | } else if ((y < 0) && (z < 0)) { |
| | | x = this.PI + x |
| | | } else if ((y < 0) && (z > 0)) { |
| | | x = 2 * this.M_PI - x |
| | | } |
| | | } |
| | | } |
| | | return x |
| | | } |
| | | //WGS84转ECEF坐标系 |
| | | _.prototype.ConvertLLAToXYZ = function (LLACoor) { |
| | | let lon = this.PI / 180 * LLACoor.longitude |
| | | let lat = this.PI / 180 * LLACoor.latitude |
| | | let H = LLACoor.altitude |
| | | let N0 = this.a / Math.sqrt(1.0 - this.ee * Math.sin(lat) * Math.sin(lat)) |
| | | let x = (N0 + H) * Math.cos(lat) * Math.cos(lon) |
| | | let y = (N0 + H) * Math.cos(lat) * Math.sin(lon) |
| | | let z = (N0 * (1.0 - this.ee) + H) * Math.sin(lat) |
| | | return { |
| | | x: x, |
| | | y: y, |
| | | z: z |
| | | } |
| | | } |
| | | |
| | | //ECEF坐标系转WGS84 |
| | | _.prototype.ConvertXYZToLLA = function (XYZCoor) { |
| | | let longitude = this.get_atan(XYZCoor.x, XYZCoor.y) |
| | | if (longitude < 0) { |
| | | longitude = longitude + this.PI |
| | | } |
| | | let latitude = this.get_atan(Math.sqrt(XYZCoor.x * XYZCoor.x + XYZCoor.y * XYZCoor.y), XYZCoor.z) |
| | | |
| | | let W = Math.sqrt(1 - this.WGSe2 * Math.sin(latitude) * Math.sin(latitude)) |
| | | let N = this.WGSa / W |
| | | let B1 |
| | | do { |
| | | B1 = latitude |
| | | W = Math.sqrt(1 - this.WGSe2 * Math.sin(B1) * Math.sin(B1)) |
| | | N = this.WGSa / W |
| | | latitude = this.get_atan(Math.sqrt(XYZCoor.x * XYZCoor.x + XYZCoor.y * XYZCoor.y), (XYZCoor.z + N * this.WGSe2 * Math.sin(B1))) |
| | | } |
| | | while (Math.abs(latitude - B1) > this.EPSILON) |
| | | |
| | | var altitude = Math.sqrt(XYZCoor.x * XYZCoor.x + XYZCoor.y * XYZCoor.y) / Math.cos(latitude) - this.WGSa / Math.sqrt(1 - this.WGSe2 * Math.sin(latitude) * Math.sin(latitude)) |
| | | |
| | | return { |
| | | longitude: longitude * 180 / this.PI, |
| | | latitude: latitude * 180 / this.PI, |
| | | altitude: altitude |
| | | } |
| | | } |
| | | /*北东天坐标系转WGS84 |
| | | @ a A点坐标 |
| | | @ p 相对参数,距离、方位角、仰角 |
| | | */ |
| | | // 俯视角pitch -elevation |
| | | //航向角heading(yaw) -azimuth |
| | | _.prototype.enu_to_ecef = function (a, p) { |
| | | //距离 |
| | | let distance = p.distance |
| | | //方位角 |
| | | let azimuth = p.azimuth |
| | | //仰角 |
| | | let elevation = p.elevation |
| | | |
| | | let zUp = elevation >= 0 ? distance * Math.sin(this.PI / 180 * elevation) : (-1) * distance * Math.sin(this.PI / 180 * Math.abs(elevation)) |
| | | |
| | | let d = distance * Math.cos(this.PI / 180 * Math.abs(elevation)) |
| | | let xEast |
| | | let yNorth |
| | | if (azimuth <= 90) { |
| | | xEast = d * Math.sin(this.PI / 180 * azimuth) |
| | | yNorth = d * Math.cos(this.PI / 180 * azimuth) |
| | | } else if (azimuth > 90 && azimuth < 180) { |
| | | xEast = d * Math.cos(this.PI / 180 * (azimuth - 90)) |
| | | yNorth = (-1) * d * Math.sin(this.PI / 180 * (azimuth - 90)) |
| | | } else if (azimuth > 180 && azimuth < 270) { |
| | | xEast = (-1) * d * Math.sin(this.PI / 180 * (azimuth - 180)) |
| | | yNorth = (-1) * d * Math.cos(this.PI / 180 * (azimuth - 180)) |
| | | } else { |
| | | xEast = (-1) * d * Math.sin(this.PI / 180 * (360 - azimuth)) |
| | | yNorth = d * Math.cos(this.PI / 180 * (360 - azimuth)) |
| | | } |
| | | |
| | | let lamb = this.radians(a.latitude) |
| | | let phi = this.radians(a.longitude) |
| | | let h0 = a.altitude |
| | | |
| | | let s = Math.sin(lamb) |
| | | let N = this.a / Math.sqrt(1.0 - this.e_sq * s * s) |
| | | |
| | | let sin_lambda = Math.sin(lamb) |
| | | let cos_lambda = Math.cos(lamb) |
| | | |
| | | let sin_phi = Math.sin(phi) |
| | | let cos_phi = Math.cos(phi) |
| | | |
| | | let x0 = (h0 + N) * cos_lambda * cos_phi |
| | | let y0 = (h0 + N) * cos_lambda * sin_phi |
| | | let z0 = (h0 + (1 - this.e_sq) * N) * sin_lambda |
| | | |
| | | let t = cos_lambda * zUp - sin_lambda * yNorth |
| | | |
| | | let zd = sin_lambda * zUp + cos_lambda * yNorth |
| | | let xd = cos_phi * t - sin_phi * xEast |
| | | let yd = sin_phi * t + cos_phi * xEast |
| | | |
| | | return this.ConvertXYZToLLA({ |
| | | x: xd + x0, |
| | | y: yd + y0, |
| | | z: zd + z0 |
| | | }) |
| | | } |
| | | _.prototype.radians = function (degree) { |
| | | return this.PI / 180 * degree |
| | | } |
| | | return _ |
| | | })() |
| | | |
| | | export default ECEF |
| | |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 递归处理对象,移除 #text 键并返回其值,同时处理数值字符串到数字的转换 |
| | | * @param {any} obj - 需要处理的对象或值 |
| | | * @returns {any} 处理后的对象或值 |
| | | */ |
| | | export function removeTextKey(obj) { |
| | | if (typeof obj !== 'object' || obj === null) { |
| | | // 如果是数值字符串,转换为数值 |
| | |
| | | } |
| | | |
| | | if (Array.isArray(obj)) { |
| | | // 递归处理数组中的每个元素 |
| | | return obj.map(item => removeTextKey(item)); |
| | | } |
| | | |
| | | if (Object.prototype.hasOwnProperty.call(obj, '#text')) { |
| | | // 如果 #text 的值是数值字符串,转换为数值 |
| | | // 如果存在 #text 属性,返回其值(如果是数值字符串则转换为数字) |
| | | const textValue = obj['#text']; |
| | | return typeof textValue === 'string' && /^-?\d+(\.\d+)?$/.test(textValue) ? Number(textValue) : textValue; |
| | | } |
| | | |
| | | // 递归处理对象属性 |
| | | const newObj = {}; |
| | | for (const key in obj) { |
| | | if (Object.prototype.hasOwnProperty.call(obj, key)) { |
| | | // 如果属性值是空对象,则设为空字符串;否则递归处理 |
| | | newObj[key] = Object.keys(obj[key]).length === 0 ? '' : removeTextKey(obj[key]); |
| | | } |
| | | } |
| | |
| | | import * as Cesium from 'cesium' |
| | | import * as turf from '@turf/turf' |
| | | const { VITE_APP_BASE, VITE_APP_ENV, VITE_APP_REGION_URL } = import.meta.env |
| | | import store from '@/store' |
| | | |
| | | const { VITE_APP_BASE, VITE_APP_ENV, VITE_APP_REGION_URL } = import.meta.env |
| | | export const getLngLatDistance = (lat1, lng1, lat2, lng2) => { |
| | | const radLat1 = (lat1 * Math.PI) / 180.0 |
| | | const radLat2 = (lat2 * Math.PI) / 180.0 |
| | |
| | | const newArr = arr.sort((a, b) => a.distance - b.distance) |
| | | return newArr[0] |
| | | } |
| | | // 获取多边形内部数据 |
| | | export const samplePolygonInterior = (positions, samplingDistance) => { |
| | | if (!positions.length) return [] |
| | | let minLng = Infinity, |
| | | maxLng = -Infinity |
| | | let minLat = Infinity, |
| | | maxLat = -Infinity |
| | | positions.forEach(pos => { |
| | | minLng = Math.min(minLng, pos.lng) |
| | | maxLng = Math.max(maxLng, pos.lng) |
| | | minLat = Math.min(minLat, pos.lat) |
| | | maxLat = Math.max(maxLat, pos.lat) |
| | | }) |
| | | |
| | | // 将 Cesium 多边形格式转为 Turf 格式 |
| | | const turfPolygonCoords = positions.map(pos => [pos.lng, pos.lat]) |
| | | turfPolygonCoords.push([positions[0].lng, positions[0].lat]) // 闭合多边形 |
| | | const turfPolygon = turf.polygon([turfPolygonCoords]) |
| | | |
| | | // 生成网格点并判断是否在多边形内 |
| | | const sampledPoints = [] |
| | | for (let lng = minLng; lng <= maxLng; lng += samplingDistance) { |
| | | for (let lat = minLat; lat <= maxLat; lat += samplingDistance) { |
| | | // 创建 Turf 点 |
| | | const turfPoint = turf.point([lng, lat]) |
| | | // Turf 原生方法判断点是否在多边形内 |
| | | if (turf.booleanPointInPolygon(turfPoint, turfPolygon)) { |
| | | sampledPoints.push({ |
| | | lng: Number(lng.toFixed(6)), |
| | | lat: Number(lat.toFixed(6)), |
| | | height: 0, |
| | | }) |
| | | } |
| | | } |
| | | } |
| | | |
| | | return sampledPoints |
| | | } |
| | | // 获取多边形面内的最高点(相对地形高度) |
| | | export const getPolygonHighestPoint = async (positions, viewer, samplingDistance = 0.0002) => { |
| | | try { |
| | | const interiorPoints = samplePolygonInterior(positions, samplingDistance, viewer) |
| | | const allPoints = [...positions, ...interiorPoints] |
| | | // 批量获取所有点的地形高度 |
| | | const pointsWithTerrain = await getPointPositionsHeight(allPoints, viewer) |
| | | // console.log('所有点的地形高度', pointsWithTerrain) |
| | | |
| | | // 地形高度(ASL)最高的点 |
| | | const highestTerrainPoint = pointsWithTerrain.reduce( |
| | | (max, curr) => (curr.ASL > max.ASL ? curr : max), |
| | | pointsWithTerrain[0] |
| | | ) |
| | | |
| | | // console.log('地形最高点信息:', highestTerrainPoint) |
| | | return highestTerrainPoint |
| | | } catch (error) { |
| | | console.error('获取地形最高点失败:', error) |
| | | return null |
| | | } |
| | | } |
| | | |
| | | // 获取当前经纬度地形数据 |
| | | export const getLnglatAltitude = (longitude, latitude, viewer) => { |
| | |
| | | return new Promise((resolve, reject) => { |
| | | if (!data || !data.length) { |
| | | resolve([]) |
| | | |
| | | return |
| | | } |
| | | |
| | | // 假设 viewer 已经初始化并且 terrainProvider 是有效的 |
| | | const terrainProvider = viewer?.terrainProvider |
| | | |
| | | // 创建 Cartographic 对象 |
| | | const cartographics = data.map(item => { |
| | | const { lng, lat } = item |
| | | |
| | | return Cesium.Cartographic.fromDegrees(Number(lng), Number(lat)) |
| | | }) |
| | | |
| | | // 获取地形数据的Promise |
| | | const promise = Cesium.sampleTerrainMostDetailed(terrainProvider, cartographics) |
| | | |
| | | // 使用 Cesium.when 处理 Promise |
| | | promise |
| | | .then(function (updatedPositions) { |
| | | // updatedPositions 是一个数组,包含更新后的 Cartographic 对象 |
| | | // 是一个数组,包含更新后的 Cartographic 对象 |
| | | const newPosition = updatedPositions.map((item, index) => { |
| | | const longitude = Cesium.Math.toDegrees(item.longitude) |
| | | const latitude = Cesium.Math.toDegrees(item.latitude) |
| | | |
| | | let pointData = { |
| | | ...data[index], |
| | | longitude, |
| | |
| | | ASL: Number(item.height), |
| | | customHeight: Number(item.height), |
| | | } |
| | | |
| | | if (droneHeight) pointData.TH = Number(item.height) + Number(droneHeight) |
| | | |
| | | return pointData |
| | | }) |
| | | |
| | | resolve(newPosition) |
| | | // 在这里,你可以使用 height 值进行后续操作 |
| | | }) |
| | | .catch(function (error) { |
| | | // console.error('获取高程时发生错误:', error); |
| | | reject(error) |
| | | console.log('获取高程时发生错误:', error) |
| | | resolve(data.map(item => ({ ...item, ASL: 0, customHeight: 0, longitude: item.lng, latitude: item.lat }))) |
| | | }) |
| | | }) |
| | | } |
| | |
| | | if (!Array.isArray(positionsData) || positionsData.length === 0) return |
| | | |
| | | // 如果是一个点,加两个点方便后续生成外包围盒 |
| | | if (positionsData.length === 1){ |
| | | const [lon,lat,height] = positionsData[0] |
| | | positionsData = [[lon+0.001,lat+0.001,height],[lon-0.001,lat-0.001,height],[lon,lat,height]] |
| | | if (positionsData.length === 1) { |
| | | const [lon, lat, height = 0] = positionsData[0] |
| | | positionsData = [ |
| | | [lon + 0.001, lat + 0.001, height], |
| | | [lon - 0.001, lat - 0.001, height], |
| | | [lon, lat, height], |
| | | ] |
| | | } |
| | | const positions = positionsData.map(([lon, lat, height]) => |
| | | const positions = positionsData.map(([lon, lat, height = 0]) => |
| | | Cesium.Cartesian3.fromDegrees(Number(lon), Number(lat), Number(height || 0)) |
| | | ) |
| | | |
| | |
| | | range: boundingSphere.radius * multiple, |
| | | }, |
| | | complete: () => { |
| | | // 飞行完成后检查相机高度是否小于最高点,如果小于直接飞到最高点 |
| | | // 飞行完成后检查相机高度是否小于最高点 |
| | | const cameraHeight = Cesium.Cartographic.fromCartesian(viewer.camera.position).height |
| | | if (cameraHeight < maxHeight) { |
| | | viewer.camera.flyTo({ |
| | |
| | | |
| | | // 辅助函数:创建旋转文字的画布 |
| | | export function createRotatedTextCanvas(time) { |
| | | const prefix = '预计' |
| | | const prefix = '预计到达' |
| | | const suffix = '分钟' |
| | | const canvas = document.createElement('canvas') |
| | | const context = canvas.getContext('2d') |
| | |
| | | context.font = 'bold 16px Source Han Sans CN' |
| | | // 测量各部分文本宽度 |
| | | const prefixWidth = context.measureText(prefix).width |
| | | const timeWidth = context.measureText(time).width |
| | | let timeWidth = context.measureText(time).width |
| | | const timeWidthMore = `${time >= 10 ? 6 : 0}` |
| | | timeWidth = timeWidth + Number(timeWidthMore) |
| | | const suffixWidth = context.measureText(suffix).width |
| | | const totalWidth = prefixWidth + timeWidth + suffixWidth |
| | | const textHeight = 20 // 估算高度 |
| | |
| | | context.textAlign = 'left' // 改为左对齐,方便分段绘制 |
| | | // 清除画布 |
| | | context.clearRect(0, 0, canvas.width, canvas.height) |
| | | // 绘制“预计”(白色) |
| | | // 绘制“预计到达”(白色) |
| | | context.fillStyle = '#FFFFFF' |
| | | context.fillText(prefix, 10, 15) |
| | | // 绘制时间(蓝色) |
| | | context.font = 'bold 30px Source Han Sans CN' |
| | | context.font = 'bold 24px Source Han Sans CN' |
| | | context.fillStyle = '#1EE7E7' // 亮蓝色 |
| | | context.fillText(time, 10 + prefixWidth, 12) |
| | | // 设置宽度 |
| | | context.textBaseline = 'middle' |
| | | context.fillText(time, 10 + prefixWidth, 14) |
| | | // 绘制“分钟”(白色) |
| | | context.font = 'bold 16px Source Han Sans CN' |
| | | context.fillStyle = '#FFFFFF' |
| | | context.fillText(suffix, 20 + prefixWidth + timeWidth, 15) |
| | | context.fillText(suffix, 14 + prefixWidth + timeWidth, 15) |
| | | |
| | | return canvas |
| | | } |
| | | |
| | | |
| | | export const saveCurrentCameraPosition = viewer => { |
| | | const position = viewer?.camera.position.clone() // 相机位置(Cartesian3) |
| | | const heading = viewer?.camera.heading // 航向角(弧度) |
| | | const pitch = viewer?.camera.pitch // 俯仰角(弧度) |
| | | const roll = viewer?.camera.roll // 翻滚角(弧度) |
| | | store.commit('setCameraPosition', { |
| | | duration: 0, |
| | | destination: position, |
| | | orientation: { heading, pitch, roll }, |
| | | }) |
| | | } |
| | | |
| | | // 连接地面线 |
| | | export function getDockPolyLine(data, viewer) { |
| | | if (!data.longitude || !data.latitude) return {} |
| | | return { |
| | | positions: new Cesium.CallbackProperty(() => { |
| | | const pointPosition = Cesium.Cartesian3.fromDegrees(+data.longitude, +data.latitude, +data.height) |
| | | if (!pointPosition) return [] |
| | | // 获取点的位置 |
| | | const cartographic = Cesium.Cartographic.fromCartesian(pointPosition) |
| | | // 获取地形高度 |
| | | const terrainHeight = viewer.scene.globe.getHeight(cartographic) || 0 |
| | | // 创建地面点位置 |
| | | const groundPosition = Cesium.Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, terrainHeight) |
| | | return [pointPosition, groundPosition] |
| | | }, false), |
| | | width: 0.5, |
| | | material: Cesium.Color.WHITE, |
| | | } |
| | | } |
| | | |
| | | // 飞向一个机巢中心 |
| | | export const flyDockCenter = (viewer, info) => { |
| | | const { longitude = 115, latitude = 28, height = 0 } = info |
| | | const position = Cesium.Cartesian3.fromDegrees(+longitude, +latitude, +height) |
| | | const droneEntity = viewer?.entities.add({ position: position, point: { pixelSize: 0 } }) |
| | | viewer?.flyTo(droneEntity, { |
| | | offset: new Cesium.HeadingPitchRange(0, Cesium.Math.toRadians(-60), 22000), |
| | | duration: 0.5, |
| | | complete: () => { |
| | | viewer?.entities.remove(droneEntity) |
| | | }, |
| | | }) |
| | | } |
| | | |
| | | // 在固定的经度上获得高度 |
| | | export function getHeightOnFixedLonLat(ray, pointParams, ellipsoid = Cesium.Ellipsoid.WGS84) { |
| | | const { longitude: lon, latitude: lat } = pointParams |
| | | // 1. 地表点(高度=0) |
| | | const p0 = Cesium.Cartesian3.fromDegrees(lon, lat, 0, ellipsoid) |
| | | |
| | | // 2. 该点处的法线方向(up 向量,单位向量) |
| | | const up = ellipsoid.geodeticSurfaceNormal(p0, new Cesium.Cartesian3()) |
| | | |
| | | // 3. 射线起点和方向 |
| | | const r0 = ray.origin // 相机位置 |
| | | const rd = ray.direction // 射线方向(已归一化?不一定,但不影响) |
| | | |
| | | // 4. 解:求直线 L(t) = p0 + t * up 与射线 R(s) = r0 + s * rd 的最近点 |
| | | // 使用向量公式求两条直线的最近点参数 t |
| | | const w0 = Cesium.Cartesian3.subtract(p0, r0, new Cesium.Cartesian3()) |
| | | |
| | | const a = Cesium.Cartesian3.dot(up, up) // = 1(因为 up 是单位向量) |
| | | const b = Cesium.Cartesian3.dot(up, rd) |
| | | const c = Cesium.Cartesian3.dot(rd, rd) // = |rd|^2 |
| | | const d = Cesium.Cartesian3.dot(up, w0) |
| | | const e = Cesium.Cartesian3.dot(rd, w0) |
| | | |
| | | const denom = a * c - b * b |
| | | let t |
| | | |
| | | if (Math.abs(denom) < Cesium.Math.EPSILON10) { |
| | | // 两直线平行,取 t = 0 |
| | | t = 0 |
| | | } else { |
| | | t = (b * e - c * d) / denom |
| | | } |
| | | |
| | | // 5. 得到高度线上对应的点 |
| | | const resultPoint = Cesium.Cartesian3.add( |
| | | p0, |
| | | Cesium.Cartesian3.multiplyByScalar(up, t, new Cesium.Cartesian3()), |
| | | new Cesium.Cartesian3() |
| | | ) |
| | | |
| | | // 6. 转回 Cartographic 获取精确高度 |
| | | const carto = Cesium.Cartographic.fromCartesian(resultPoint, ellipsoid) |
| | | return { |
| | | cartesian: resultPoint, |
| | | height: carto.height, |
| | | longitude: Cesium.Math.toDegrees(carto.longitude), |
| | | latitude: Cesium.Math.toDegrees(carto.latitude), |
| | | } |
| | | } |
| | |
| | | * @param {boolean} [options.flyToContour=false] - 是否飞行到轮廓,默认为 false |
| | | * @param {number} [options.multiple=1.4] - 范围倍数 |
| | | * @param {number} [options.dockOptions={}] - 机巢显示 |
| | | * @param {array} [options.terrainLoadCallback] - 地形加载回调 |
| | | * @param {array} [options.boundaryChange] - 边界改变事件 |
| | | * @param {array} [options.boundaryColor] - 边界颜色 |
| | | * @param {array} [options.dockRangeType] - 机巢范围类型 1显示覆盖圆;2显示覆盖圈 |
| | |
| | | dom, |
| | | flatMode = true, |
| | | terrain = false, |
| | | layerMode = 0, |
| | | layerMode = 17, |
| | | contour = true, |
| | | flyToContour = false, |
| | | multiple = 1.4, |
| | | |
| | | dockOptions = {}, |
| | | |
| | | boundaryColor = '#7FFFD4', |
| | | boundaryChange, |
| | | terrainLoadCallback, |
| | | boundaryColor = '#7FFFD4', |
| | | dockRangeType = 1, |
| | | useDockHeight = false |
| | | } = options |
| | |
| | | baseLayer: false, |
| | | fullscreenButton: false, |
| | | } |
| | | |
| | | // 尝试解决出现多个id相同的dom |
| | | const domIdList = document.querySelectorAll(`#${dom}`) |
| | | if (domIdList.length>1) domIdList[1].remove() |
| | | |
| | | this.viewer = new Viewer(dom, viewerOptions) |
| | | this.viewerDom = dom |
| | |
| | | this.viewer?.cesiumWidget.screenSpaceEventHandler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK) // 禁用双击 |
| | | |
| | | if (terrain) { |
| | | // 正则:第一位非0,第二位任意数字,后10位都是0 |
| | | const re = /^[1-9]\d0{10}$/ |
| | | |
| | | let result = null |
| | | |
| | | // 1️⃣先判断 areaCode |
| | | if (re.test(store.state.user.userInfo.detail.areaCode)) { |
| | | result = store.state.user.userInfo.detail.areaCode.slice(0, 6) |
| | | } else { |
| | | // 2️⃣如果不满足,从 ancestors 里找 |
| | | const matchedItems = store.state.user.userInfo.detail.ancestors |
| | | .split(',') |
| | | .filter(item => re.test(item)) // 过滤出符合条件的 |
| | | |
| | | // 取第一个符合条件的前6位(可以改成 last one 看需求) |
| | | if (matchedItems.length > 0) { |
| | | result = matchedItems[0].slice(0, 6) |
| | | } |
| | | } |
| | | |
| | | try { |
| | | Cesium.CesiumTerrainProvider.fromUrl(`${import.meta.env.VITE_APP_TERRAIN_URL}${result}`, { |
| | | requestVertexNormals: true, // 启用地形法线增强立体感 |
| | | requestWaterMask: true, // 启用水体遮罩效果 |
| | | }).then(terrainProvider => { |
| | | this.viewer.terrainProvider = terrainProvider |
| | | }) |
| | | const noTerrainMechanism = ['1962779164650135554'] |
| | | if (noTerrainMechanism.includes(store.state.user.userInfo?.deptId)) { |
| | | // 使用Cesium World Terrain |
| | | Cesium.createWorldTerrainAsync({ |
| | | requestWaterMask: false, // 请求水域效果 |
| | | requestVertexNormals: true // 请求光照和地形法线 |
| | | }).then(terrainProvider => { |
| | | this.viewer.terrainProvider = terrainProvider |
| | | terrainLoadCallback?.() |
| | | }) |
| | | } else { |
| | | /*// 正则:第一位非0,第二位任意数字,后10位都是0 |
| | | const re = /^[1-9]\d0{10}$/ |
| | | |
| | | let result = null |
| | | |
| | | // 1️⃣先判断 areaCode |
| | | if (re.test(store.state.user.userInfo.detail.areaCode)) { |
| | | result = store.state.user.userInfo.detail.areaCode.slice(0, 6) |
| | | } else { |
| | | // 2️⃣如果不满足,从 ancestors 里找 |
| | | const matchedItems = store.state.user.userInfo.detail.ancestors |
| | | .split(',') |
| | | .filter(item => re.test(item)) // 过滤出符合条件的 |
| | | |
| | | // 取第一个符合条件的前6位(可以改成 last one 看需求) |
| | | if (matchedItems.length > 0) { |
| | | result = matchedItems[0].slice(0, 6) |
| | | } |
| | | } |
| | | |
| | | // 使用公司地形 |
| | | Cesium.CesiumTerrainProvider.fromUrl(`${import.meta.env.VITE_APP_TERRAIN_URL}${result}`, { |
| | | requestVertexNormals: true, // 启用地形法线增强立体感 |
| | | requestWaterMask: true, // 启用水体遮罩效果 |
| | | }).then(terrainProvider => { |
| | | this.viewer.terrainProvider = terrainProvider |
| | | terrainLoadCallback?.() |
| | | })*/ |
| | | } |
| | | } catch (error) { |
| | | console.error('地形加载失败:', error) |
| | | } |
| | |
| | | this.switchFlatMode(flatMode) |
| | | this.switchContour(contour) |
| | | flyToContour && this.flyToContour(contour) |
| | | |
| | | if (Cesium.FeatureDetection.supportsImageRenderingPixelated()) { |
| | | let dpr = window.devicePixelRatio |
| | | while (dpr >= 2.0) dpr /= 2.0 // 避免过高缩放导致模糊 |
| | | this.viewer.resolutionScale = dpr // 设置分辨率缩放比例 |
| | | } |
| | | } |
| | | |
| | | getViewer () { |
| | |
| | | |
| | | // 销毁viewer |
| | | viewerDestroy (removeDom = true) { |
| | | if (!this.viewer) return |
| | | // // 👇 关键:先停止所有动态行为 |
| | | // this.viewer.clock.shouldAnimate = false; |
| | | // this.viewer.scene.requestRenderMode = false; |
| | | // // 取消所有可能的 pending 渲染 |
| | | // if (this.viewer.cesiumWidget) { |
| | | // this.viewer.cesiumWidget.render = () => {}; // 空函数覆盖 |
| | | // } |
| | | // // 清理实体和图层 |
| | | // this.viewer?.entities.removeAll(); |
| | | // this.viewer?.imageryLayers.removeAll(); |
| | | // // 清理数据源 |
| | | // try { |
| | | // const dataSources = this.viewer.dataSources; |
| | | // while (dataSources.length > 0) { |
| | | // dataSources.remove(dataSources.get(0)); |
| | | // } |
| | | // } catch (e) { |
| | | // console.warn('清理数据源时出错:', e); |
| | | // } |
| | | // // 清理场景中的图元 |
| | | // try { |
| | | // this.viewer?.scene.primitives.removeAll(); |
| | | // } catch (e) { |
| | | // console.warn('清理图元时出错:', e); |
| | | // } |
| | | // // 获取webgl上下文 |
| | | // let gl = this.viewer?.scene.context._originalGLContext |
| | | // if (gl) { |
| | | // try { |
| | | // gl.canvas.width = 1 |
| | | // gl.canvas.height = 1 |
| | | // gl.getExtension("WEBGL_lose_context").loseContext() |
| | | // } catch (e) { |
| | | // console.warn('释放WebGL上下文时出错:', e) |
| | | // } |
| | | // gl = null |
| | | // } |
| | | this.viewer?.destroy() // 销毁Viewer实例 |
| | | this.viewer = null |
| | | smallMpaCenterDistance = null |
| | | |
| | | if (this.viewer) { |
| | | this.viewer?.entities.removeAll() |
| | | this.viewer?.imageryLayers.removeAll() |
| | | this.viewer?.dataSources.removeAll() |
| | | |
| | | |
| | | // try { |
| | | // this.viewer?.scene?.primitives?.removeAll() |
| | | // this.viewer?.entities?.removeAll() |
| | | // } catch (e) { |
| | | // console.warn('清理资源时出错:', e) |
| | | |
| | | // console.log(this.viewer, 1111111111111) |
| | | // } |
| | | |
| | | // 获取webgl上下文 |
| | | let gl = this.viewer?.scene.context._originalGLContext |
| | | gl.canvas.width = 1 |
| | | gl.canvas.height = 1 |
| | | gl.getExtension("WEBGL_lose_context").loseContext() |
| | | gl = null |
| | | this.viewer?.destroy() // 销毁Viewer实例 |
| | | this.viewer = null |
| | | var cesiumContainer = document.getElementById(this.viewerDom) |
| | | if (cesiumContainer && removeDom) { |
| | | cesiumContainer.remove() // 移除与地图相关的DOM元素 |
| | | this.viewerDom = null |
| | | } |
| | | var cesiumContainer = document.getElementById(this.viewerDom) |
| | | if (cesiumContainer && removeDom) { |
| | | cesiumContainer.remove() // 移除与地图相关的DOM元素 |
| | | this.viewerDom = null |
| | | } |
| | | } |
| | | |
| | | setShowDock (sns) { |
| | | this.boundary?.setShowDock(sns) |
| | | } |
| | | |
| | | setDockCoverColor (sns) { |
| | | this.boundary?.setDockCoverColor(sns) |
| | | } |
| | | // 切换轮廓显示 |
| | | async switchContour (open) { |
| | | if (open) { |
| | | await this.boundary?.openContour() |
| | | |
| | | } else { |
| | | this.boundary?.closeContour() |
| | | } |
| | | } |
| | | // // 飞向轮廓居中 |
| | | // 飞向轮廓居中 |
| | | flyToContour () { |
| | | this.boundary?.flyToBoundary() |
| | | } |
| | |
| | | this.globalBaseMapLayers?.forEach(item => { |
| | | if (item.mapLayer) item.mapLayer.show = false |
| | | }) |
| | | // store.state.common.mapSetting.mode = mode |
| | | store.state.common.mapSetting.mode = mode |
| | | // 标准地图(天地图矢量)加载 |
| | | if (mode === 0) { |
| | | mapLayers.push( |
| | |
| | | this.viewer?.imageryLayers.lowerToBottom(find.mapLayer) |
| | | }) |
| | | } |
| | | |
| | | // 设置或重置滤镜 |
| | | |
| | | setCurrentLayerFilter (data, mode) { |
| | | if (mode === 17) { |
| | | data.mapLayer.brightness = 0.6 // 亮度(默认值) |
| New file |
| | |
| | | import _ from 'lodash' |
| | | import JSZIP from 'jszip' |
| | | import { JSONToXML } from './kmz' |
| | | import { getDateFromTimestamp } from '@/utils/date' |
| | | |
| | | export default function kmzFile () { |
| | | // 云台信息 |
| | | const droneModel = { |
| | | 52: '0', |
| | | 53: '1', |
| | | 66: '0', |
| | | 67: '1', |
| | | 80: '0', |
| | | 81: '1', |
| | | } |
| | | // 创建图斑航线 |
| | | const create = async (waylineBasicInfo, lnglats, spotList) => { |
| | | try { |
| | | const date = new Date() |
| | | const username = window.localStorage.getItem('bs_username') |
| | | const updateTime = date.getTime() |
| | | const fileName = '图斑航线规划_' + date.getTime() |
| | | // 无人机机型、云台型号、航线高度 |
| | | const { droneEnumValue, payloadEnumValue, height } = waylineBasicInfo |
| | | const defaultHeight = height || 100 |
| | | // 起始点位 (参考点位-具体以机场为准) |
| | | const startPoint = lnglats[0] |
| | | const takeOffPoint = [startPoint[1], startPoint[0], 0].join(',') |
| | | // template.xml对象模版 |
| | | const fileTemplate = { |
| | | author: { '#text': username }, |
| | | createTime: { '#text': updateTime }, |
| | | updateTime: { '#text': updateTime }, |
| | | missionConfig: { |
| | | // 飞向首航点模式 |
| | | flyToWaylineMode: { '#text': 'safely' }, |
| | | // 航线结束动作 |
| | | finishAction: { '#text': 'goHome' }, |
| | | // 失控是否继续执行航线 |
| | | exitOnRCLost: { '#text': 'goContinue' }, |
| | | // 失控动作类型 |
| | | executeRCLostAction: { '#text': 'goBack' }, |
| | | // 安全起飞高度 |
| | | takeOffSecurityHeight: { '#text': 20 }, |
| | | // 参考起飞点 |
| | | takeOffRefPoint: { '#text': takeOffPoint }, |
| | | // 参考起飞点绝对高度 |
| | | takeOffRefPointAGLHeight: { '#text': 0 }, |
| | | // 全局航线过渡速度 |
| | | globalTransitionalSpeed: { '#text': 10 }, |
| | | // 全局返航高度 |
| | | globalRTHHeight: { '#text': defaultHeight }, |
| | | // 无人机参数 |
| | | droneInfo: { |
| | | droneEnumValue: { '#text': droneEnumValue }, |
| | | droneSubEnumValue: { |
| | | '#text': droneModel[payloadEnumValue], |
| | | }, |
| | | }, |
| | | // 云台参数 |
| | | payloadInfo: { |
| | | payloadEnumValue: { '#text': payloadEnumValue }, |
| | | payloadSubEnumValue: { |
| | | '#text': droneModel[payloadEnumValue], |
| | | }, |
| | | payloadPositionIndex: { '#text': 0 }, |
| | | }, |
| | | }, |
| | | Folder: { |
| | | // 预定义模板类型 - 航点飞行 |
| | | templateType: { '#text': 'waypoint' }, |
| | | // 是否使用全局航线过渡速度 |
| | | useGlobalTransitionalSpeed: { '#text': 0 }, |
| | | templateId: { '#text': 0 }, |
| | | // 坐标系参数信息 |
| | | waylineCoordinateSysParam: { |
| | | // 经纬度坐标系 - 固定值 |
| | | coordinateMode: { '#text': 'WGS84' }, |
| | | // 航点高程参考平面 - 相对高度 |
| | | heightMode: { '#text': 'relativeToStartPoint' }, |
| | | }, |
| | | // 全局航线飞行速度 |
| | | autoFlightSpeed: { '#text': 10 }, |
| | | // 云台俯仰角控制模式 |
| | | gimbalPitchMode: { '#text': 'usePointSetting' }, |
| | | // 全局高度 |
| | | globalHeight: { '#text': '100' }, |
| | | // 全局偏航角模式参数 - 可使用当前默认值 |
| | | globalWaypointHeadingParam: { |
| | | waypointHeadingMode: { '#text': 'followWayline' }, |
| | | waypointHeadingAngle: { '#text': 0 }, |
| | | waypointPoiPoint: { |
| | | '#text': '0.000000,0.000000,0.000000', |
| | | }, |
| | | waypointHeadingPathMode: { '#text': 'followBadArc' }, |
| | | waypointHeadingPoiIndex: { '#text': 0 }, |
| | | }, |
| | | // 全局航点类型 - 直线飞行,飞行器到点停 |
| | | globalWaypointTurnMode: { |
| | | '#text': 'toPointAndStopWithDiscontinuityCurvature', |
| | | }, |
| | | // 全局航段轨迹是否尽量贴合直线 |
| | | globalUseStraightLine: { '#text': 1 }, |
| | | // 航点点位信息 - 位置、事件等 |
| | | Placemark: [], |
| | | // payloadParam: { |
| | | // payloadPositionIndex: { '#text': 0 }, |
| | | // focusMode: { '#text': 'firstPoint' }, |
| | | // meteringMode: { '#text': 'average' }, |
| | | // returnMode: { '#text': 'singleReturnFirst' }, |
| | | // samplingRate: { '#text': '240000' }, |
| | | // scanningMode: { '#text': 'repetitive' }, |
| | | // imageFormat: { '#text': 'wide,zoom' }, |
| | | // }, |
| | | }, |
| | | } |
| | | // 航点创建 |
| | | const polygonNo = { |
| | | no: '', |
| | | index: 0, |
| | | } |
| | | lnglats.forEach((lnglat, index) => { |
| | | const obj = spotList.find(spot => spot.dkfw.find(item => _.isEqual(item, lnglat))) |
| | | let fileSuffix = '' |
| | | if (obj) { |
| | | if (polygonNo.no === obj.dkbh) { |
| | | polygonNo.index++ |
| | | } else { |
| | | polygonNo.no = obj.dkbh |
| | | polygonNo.index = 1 |
| | | } |
| | | fileSuffix = `图斑航线拍摄(${obj.dkbh}-${polygonNo.index})` |
| | | } else { |
| | | fileSuffix = '图斑航线拍摄(起点)' |
| | | } |
| | | const placemark = { |
| | | Point: { |
| | | // 航点经纬度 |
| | | coordinates: { '#text': `${lnglat[0]},${lnglat[1]}` }, |
| | | }, |
| | | // 航点index |
| | | index: { '#text': index }, |
| | | // 全局航线高度(椭球高) |
| | | ellipsoidHeight: { '#text': defaultHeight }, |
| | | // 全局航线高度 (EGM96海拔高/相对起飞点高度/AGL相对地面高度) |
| | | height: { '#text': defaultHeight }, |
| | | // 航点飞行速度 |
| | | waypointSpeed: { '#text': 10 }, |
| | | // 沿航线方向 - 可使用当前默认值 |
| | | waypointHeadingParam: { |
| | | waypointHeadingMode: { '#text': 'followWayline' }, |
| | | waypointHeadingAngle: { '#text': 0 }, |
| | | waypointPoiPoint: { |
| | | '#text': '0.000000,0.000000,0.000000', |
| | | }, |
| | | waypointHeadingPathMode: { '#text': 'followBadArc' }, |
| | | waypointHeadingPoiIndex: { '#text': 0 }, |
| | | }, |
| | | // 航点类型 |
| | | waypointTurnParam: { |
| | | waypointTurnMode: { |
| | | '#text': 'toPointAndStopWithDiscontinuityCurvature', |
| | | }, |
| | | waypointTurnDampingDist: { '#text': 0.2 }, |
| | | }, |
| | | // 是否全局参数 |
| | | useGlobalHeight: { '#text': 1 }, |
| | | useGlobalSpeed: { '#text': 1 }, |
| | | useGlobalHeadingParam: { '#text': 1 }, |
| | | useGlobalTurnParam: { '#text': 1 }, |
| | | useStraightLine: { '#text': 1 }, |
| | | // 事件组 |
| | | actionGroup: { |
| | | actionGroupId: { '#text': index }, |
| | | actionGroupStartIndex: { '#text': index }, |
| | | actionGroupEndIndex: { '#text': index }, |
| | | actionGroupMode: { '#text': 'sequence' }, |
| | | actionTrigger: { |
| | | actionTriggerType: { '#text': 'reachPoint' }, |
| | | }, |
| | | // 单个事件 - 如果是多个事件action可改成数组 action: [{}] |
| | | action: { |
| | | actionId: { '#text': 0 }, |
| | | actionActuatorFunc: { '#text': 'takePhoto' }, |
| | | actionActuatorFuncParam: { |
| | | fileSuffix: { '#text': fileSuffix }, |
| | | payloadPositionIndex: { '#text': '0' }, |
| | | useGlobalPayloadLensIndex: { '#text': '1' }, |
| | | }, |
| | | }, |
| | | }, |
| | | } |
| | | fileTemplate.Folder.Placemark.push(placemark) |
| | | }) |
| | | const fileWaylines = waylinesContext(fileTemplate) |
| | | |
| | | return { |
| | | fileName, |
| | | template: fileTemplate, |
| | | waylines: fileWaylines, |
| | | } |
| | | } catch (error) { |
| | | console.log(error) |
| | | return false |
| | | } |
| | | } |
| | | |
| | | // 创建正常航线 |
| | | const createNoramlWaylines = (waylineBasicInfo, lnglats) => { |
| | | try { |
| | | const date = new Date() |
| | | const username = window.localStorage.getItem('bs_username') |
| | | const updateTime = date.getTime() |
| | | const { year, month, day, hours, minutes } = getDateFromTimestamp(updateTime) |
| | | const fileName = '新建航线_' + year + month + day + hours + minutes |
| | | // 无人机机型、云台型号、航线高度 |
| | | const { droneEnumValue, payloadEnumValue, height, waylineName } = waylineBasicInfo |
| | | const defaultHeight = height || 100 |
| | | // 起始点位 (参考点位-具体以机场为准) |
| | | const startPoint = lnglats[0] |
| | | const takeOffPoint = [startPoint.latitude, startPoint.longitude, startPoint.height].join(',') |
| | | // template.xml对象模版 |
| | | const fileTemplate = { |
| | | author: { '#text': username }, |
| | | createTime: { '#text': updateTime }, |
| | | updateTime: { '#text': updateTime }, |
| | | missionConfig: { |
| | | // 飞向首航点模式 |
| | | flyToWaylineMode: { '#text': 'safely' }, |
| | | // 航线结束动作 |
| | | finishAction: { '#text': 'goHome' }, |
| | | // 失控是否继续执行航线 |
| | | exitOnRCLost: { '#text': 'goContinue' }, |
| | | // 失控动作类型 |
| | | executeRCLostAction: { '#text': 'goBack' }, |
| | | // 安全起飞高度 |
| | | takeOffSecurityHeight: { '#text': 20 }, |
| | | // 参考起飞点 |
| | | takeOffRefPoint: { '#text': takeOffPoint }, |
| | | // 参考起飞点绝对高度 |
| | | takeOffRefPointAGLHeight: { '#text': 0 }, |
| | | // 全局航线过渡速度 |
| | | globalTransitionalSpeed: { '#text': 10 }, |
| | | // 全局返航高度 |
| | | globalRTHHeight: { '#text': defaultHeight }, |
| | | // 无人机参数 |
| | | droneInfo: { |
| | | droneEnumValue: { '#text': droneEnumValue }, |
| | | droneSubEnumValue: { |
| | | '#text': droneModel[payloadEnumValue], |
| | | }, |
| | | }, |
| | | // 是否自动绕行(暂未验证M30T机型使用此参数后会不会无法起飞) |
| | | autoRerouteInfo: { |
| | | missionAutoRerouteMode: { '#text': 1 }, |
| | | transitionalAutoRerouteMode: { '#text': 1 }, |
| | | }, |
| | | // 云台参数 |
| | | payloadInfo: { |
| | | payloadEnumValue: { '#text': payloadEnumValue }, |
| | | payloadSubEnumValue: { |
| | | '#text': droneModel[payloadEnumValue], |
| | | }, |
| | | payloadPositionIndex: { '#text': 0 }, |
| | | }, |
| | | }, |
| | | Folder: { |
| | | // 预定义模板类型 - 航点飞行 |
| | | templateType: { '#text': 'waypoint' }, |
| | | // 是否使用全局航线过渡速度 |
| | | useGlobalTransitionalSpeed: { '#text': 0 }, |
| | | templateId: { '#text': 0 }, |
| | | // 坐标系参数信息 |
| | | waylineCoordinateSysParam: { |
| | | // 经纬度坐标系 - 固定值 |
| | | coordinateMode: { '#text': 'WGS84' }, |
| | | // 航点高程参考平面 - 相对高度 |
| | | heightMode: { '#text': 'relativeToStartPoint' }, |
| | | }, |
| | | // 全局航线飞行速度 |
| | | autoFlightSpeed: { '#text': 10 }, |
| | | // 云台俯仰角控制模式 |
| | | gimbalPitchMode: { '#text': 'usePointSetting' }, |
| | | // 全局高度 |
| | | globalHeight: { '#text': '100' }, |
| | | // 全局偏航角模式参数 - 可使用当前默认值 |
| | | globalWaypointHeadingParam: { |
| | | waypointHeadingMode: { '#text': 'followWayline' }, |
| | | waypointHeadingAngle: { '#text': 0 }, |
| | | waypointPoiPoint: { |
| | | '#text': '0.000000,0.000000,0.000000', |
| | | }, |
| | | waypointHeadingPathMode: { '#text': 'followBadArc' }, |
| | | waypointHeadingPoiIndex: { '#text': 0 }, |
| | | }, |
| | | // 全局航点类型 - 直线飞行,飞行器到点停 |
| | | globalWaypointTurnMode: { |
| | | '#text': 'toPointAndStopWithDiscontinuityCurvature', |
| | | }, |
| | | // 全局航段轨迹是否尽量贴合直线 |
| | | globalUseStraightLine: { '#text': 1 }, |
| | | // 航点点位信息 - 位置、事件等 |
| | | Placemark: [], |
| | | payloadParam: { |
| | | payloadPositionIndex: { '#text': 0 }, |
| | | focusMode: { '#text': 'firstPoint' }, |
| | | meteringMode: { '#text': 'average' }, |
| | | returnMode: { '#text': 'singleReturnFirst' }, |
| | | samplingRate: { '#text': '240000' }, |
| | | scanningMode: { '#text': 'repetitive' }, |
| | | imageFormat: { '#text': 'wide,zoom' }, |
| | | }, |
| | | }, |
| | | } |
| | | lnglats.forEach((lnglat, index) => { |
| | | const placemark = { |
| | | Point: { |
| | | // 航点经纬度 |
| | | coordinates: { |
| | | '#text': `${lnglat.longitude},${lnglat.latitude}`, |
| | | }, |
| | | }, |
| | | // 航点index |
| | | index: { '#text': index }, |
| | | // 全局航线高度(椭球高) |
| | | ellipsoidHeight: { '#text': lnglat.height }, |
| | | // 全局航线高度 (EGM96海拔高/相对起飞点高度/AGL相对地面高度) |
| | | height: { '#text': lnglat.height }, |
| | | // 航点飞行速度 |
| | | waypointSpeed: { '#text': lnglat?.speed || 10 }, |
| | | // 沿航线方向 - 可使用当前默认值 |
| | | waypointHeadingParam: { |
| | | waypointHeadingMode: { '#text': 'followWayline' }, |
| | | waypointHeadingAngle: { '#text': 0 }, |
| | | waypointPoiPoint: { |
| | | '#text': '0.000000,0.000000,0.000000', |
| | | }, |
| | | waypointHeadingPathMode: { '#text': 'followBadArc' }, |
| | | waypointHeadingPoiIndex: { '#text': 0 }, |
| | | }, |
| | | // 航点类型 |
| | | waypointTurnParam: { |
| | | waypointTurnMode: { |
| | | '#text': 'toPointAndStopWithDiscontinuityCurvature', |
| | | }, |
| | | waypointTurnDampingDist: { '#text': 0.2 }, |
| | | }, |
| | | // 是否全局参数 |
| | | useGlobalHeight: { '#text': 1 }, |
| | | useGlobalSpeed: { '#text': 0 }, |
| | | useGlobalHeadingParam: { '#text': 1 }, |
| | | useGlobalTurnParam: { '#text': 1 }, |
| | | useStraightLine: { '#text': 1 }, |
| | | } |
| | | if (lnglat?.actions) { |
| | | const actionGroup = { |
| | | actionGroupId: { '#text': index }, |
| | | actionGroupStartIndex: { '#text': index }, |
| | | actionGroupEndIndex: { '#text': index }, |
| | | actionGroupMode: { '#text': 'sequence' }, |
| | | actionTrigger: { |
| | | actionTriggerType: { '#text': 'reachPoint' }, |
| | | }, |
| | | // 单个事件 - 如果是多个事件action可改成数组 action: [{}] |
| | | // action: [{ |
| | | // actionId: { '#text': 0 }, |
| | | // actionActuatorFunc: { '#text': '' }, |
| | | // actionActuatorFuncParam: {}, |
| | | // }], |
| | | action: [], |
| | | } |
| | | lnglat?.actions.forEach((action, index) => { |
| | | const singleAction = { |
| | | actionId: { '#text': index }, |
| | | actionActuatorFunc: { '#text': '' }, |
| | | actionActuatorFuncParam: {}, |
| | | } |
| | | // actionGroup.action['actionActuatorFunc'] = { '#text': action.key } |
| | | // action.params.forEach(p => { |
| | | // if (p.value || p.value !== '') { |
| | | // actionGroup.action['actionActuatorFuncParam'][p.key] = { '#text': p.value } |
| | | // } |
| | | // }) |
| | | singleAction['actionActuatorFunc'] = { |
| | | '#text': action.key, |
| | | } |
| | | action.params.forEach(p => { |
| | | if (p.value || p.value !== '') { |
| | | singleAction['actionActuatorFuncParam'][p.key] = { |
| | | '#text': p.value, |
| | | } |
| | | } |
| | | }) |
| | | if (lnglat?.actions.length > 1) { |
| | | actionGroup.action.push(singleAction) |
| | | } else { |
| | | actionGroup.action = singleAction |
| | | } |
| | | }) |
| | | placemark.actionGroup = actionGroup |
| | | } |
| | | fileTemplate.Folder.Placemark.push(placemark) |
| | | }) |
| | | const fileWaylines = waylinesContext(fileTemplate) |
| | | return { |
| | | fileName: waylineName || fileName, |
| | | template: fileTemplate, |
| | | waylines: fileWaylines, |
| | | } |
| | | } catch (error) { |
| | | console.log(error) |
| | | return error |
| | | } |
| | | } |
| | | |
| | | // 创建面状航线 |
| | | const createPlanarWayline = (waylineBasicInfo, lnglats) => { } |
| | | |
| | | // 创建点状航线 |
| | | const createPointWayLines = (waylineBasicInfo, points, settingInfo, dockPosition) => { |
| | | try { |
| | | const { droneEnumValue, payloadEnumValue, waylineName } = waylineBasicInfo |
| | | |
| | | const { |
| | | imageFormat, |
| | | heightMode, |
| | | globalHeight, |
| | | autoFlightSpeed, |
| | | gimbalPitchMode, |
| | | waypointTurnMode, |
| | | waypointHeadingMode, |
| | | finishAction, |
| | | globalTransitionalSpeed, |
| | | } = settingInfo |
| | | |
| | | const date = new Date() |
| | | const username = window.localStorage.getItem('bs_username') |
| | | const updateTime = date.getTime() |
| | | const { year, month, day, hours, minutes } = getDateFromTimestamp(updateTime) |
| | | const fileName = '新建航线_' + year + month + day + hours + minutes |
| | | // 无人机机型、云台型号、航线高度 |
| | | |
| | | const defaultHeight = 100 |
| | | // 起始点位 (参考点位-具体以机场为准) |
| | | const takeOffPoint = [dockPosition.latitude, dockPosition.longitude, 0].join(',') |
| | | |
| | | // template.xml对象模版 |
| | | const fileTemplate = { |
| | | author: { '#text': username }, |
| | | createTime: { '#text': updateTime }, |
| | | updateTime: { '#text': updateTime }, |
| | | missionConfig: { |
| | | // 飞向首航点模式 |
| | | flyToWaylineMode: { '#text': 'safely' }, |
| | | // 航线结束动作 |
| | | finishAction: { '#text': finishAction || 'goHome' }, |
| | | // 失控是否继续执行航线 |
| | | exitOnRCLost: { '#text': 'executeLostAction' }, |
| | | // 失控动作类型 |
| | | executeRCLostAction: { '#text': 'goBack' }, |
| | | // 安全起飞高度 |
| | | takeOffSecurityHeight: { '#text': 120 }, |
| | | // 参考起飞点 |
| | | takeOffRefPoint: { '#text': takeOffPoint }, |
| | | // 参考起飞点绝对高度 |
| | | takeOffRefPointAGLHeight: { '#text': 0 }, |
| | | // 全局航线过渡速度 |
| | | globalTransitionalSpeed: { '#text': globalTransitionalSpeed || 10 }, |
| | | // 全局返航高度 |
| | | globalRTHHeight: { '#text': defaultHeight }, |
| | | // 无人机参数 |
| | | droneInfo: { |
| | | droneEnumValue: { '#text': droneEnumValue }, |
| | | droneSubEnumValue: { |
| | | '#text': droneModel[payloadEnumValue], |
| | | }, |
| | | }, |
| | | // 是否自动绕行(暂未验证M30T机型使用此参数后会不会无法起飞) |
| | | autoRerouteInfo: { |
| | | missionAutoRerouteMode: { '#text': 1 }, |
| | | transitionalAutoRerouteMode: { '#text': 1 }, |
| | | }, |
| | | // 云台参数 |
| | | payloadInfo: { |
| | | payloadEnumValue: { '#text': payloadEnumValue }, |
| | | payloadSubEnumValue: { |
| | | '#text': droneModel[payloadEnumValue], |
| | | }, |
| | | payloadPositionIndex: { '#text': 0 }, |
| | | }, |
| | | }, |
| | | Folder: { |
| | | // 预定义模板类型 - 航点飞行 |
| | | templateType: { '#text': 'waypoint' }, |
| | | // 是否使用全局航线过渡速度 |
| | | useGlobalTransitionalSpeed: { '#text': 0 }, |
| | | templateId: { '#text': 0 }, |
| | | // 坐标系参数信息 |
| | | waylineCoordinateSysParam: { |
| | | // 经纬度坐标系 - 固定值 |
| | | coordinateMode: { '#text': 'WGS84' }, |
| | | // 航点高程参考平面 - 相对高度 |
| | | heightMode: heightMode, |
| | | }, |
| | | // 全局航线飞行速度 |
| | | autoFlightSpeed: { '#text': autoFlightSpeed || 10 }, |
| | | // 云台俯仰角控制模式 |
| | | gimbalPitchMode: { '#text': gimbalPitchMode || 'manual' }, |
| | | // 全局高度 |
| | | globalHeight: { '#text': globalHeight || '100' }, |
| | | // 全局偏航角模式参数 - 可使用当前默认值 |
| | | globalWaypointHeadingParam: { |
| | | waypointHeadingMode: { '#text': waypointHeadingMode || 'followWayline' }, |
| | | waypointHeadingAngle: { '#text': 0 }, |
| | | waypointPoiPoint: { |
| | | '#text': '0.000000,0.000000,0.000000', |
| | | }, |
| | | waypointHeadingPathMode: { '#text': 'followBadArc' }, |
| | | waypointHeadingPoiIndex: { '#text': 0 }, |
| | | }, |
| | | // 全局航点类型 - 直线飞行,飞行器到点停 |
| | | globalWaypointTurnMode: { |
| | | '#text': waypointTurnMode || 'toPointAndStopWithDiscontinuityCurvature', |
| | | }, |
| | | // 全局航段轨迹是否尽量贴合直线 |
| | | globalUseStraightLine: { '#text': 1 }, |
| | | // 航点点位信息 - 位置、事件等 |
| | | Placemark: [], |
| | | payloadParam: { |
| | | payloadPositionIndex: { '#text': 0 }, |
| | | focusMode: { '#text': 'firstPoint' }, |
| | | meteringMode: { '#text': 'average' }, |
| | | returnMode: { '#text': 'singleReturnFirst' }, |
| | | samplingRate: { '#text': '240000' }, |
| | | scanningMode: { '#text': 'repetitive' }, |
| | | imageFormat: imageFormat, |
| | | }, |
| | | }, |
| | | } |
| | | points.length && |
| | | points.forEach((detail, ind) => { |
| | | const { Point, index, height, ellipsoidHeight, actionGroup, waypointSpeed } = detail |
| | | |
| | | const placemark = { |
| | | Point, |
| | | // 航点index |
| | | index, |
| | | // 全局航线高度(椭球高) |
| | | ellipsoidHeight, |
| | | // 全局航线高度 (EGM96海拔高/相对起飞点高度/AGL相对地面高度) |
| | | height, |
| | | // 航点飞行速度 |
| | | waypointSpeed: { '#text': waypointSpeed || 10 }, |
| | | // 沿航线方向 - 可使用当前默认值 |
| | | waypointHeadingParam: { |
| | | waypointHeadingMode: { |
| | | '#text': waypointHeadingMode || 'followWayline', |
| | | }, |
| | | waypointHeadingAngle: { '#text': 0 }, |
| | | waypointPoiPoint: { |
| | | '#text': '0.000000,0.000000,0.000000', |
| | | }, |
| | | waypointHeadingPathMode: { '#text': 'followBadArc' }, |
| | | waypointHeadingPoiIndex: { '#text': 0 }, |
| | | }, |
| | | // 航点类型 |
| | | waypointTurnParam: { |
| | | waypointTurnMode: { |
| | | '#text': waypointTurnMode || 'toPointAndStopWithDiscontinuityCurvature', |
| | | }, |
| | | waypointTurnDampingDist: { '#text': 0.2 }, |
| | | }, |
| | | // 是否全局参数 |
| | | useGlobalHeight: { '#text': 1 }, |
| | | useGlobalSpeed: { '#text': 0 }, |
| | | useGlobalHeadingParam: { '#text': 1 }, |
| | | useGlobalTurnParam: { '#text': 1 }, |
| | | useStraightLine: { '#text': 1 }, |
| | | } |
| | | if (actionGroup) { |
| | | placemark.actionGroup = actionGroup |
| | | } |
| | | |
| | | fileTemplate.Folder.Placemark.push(placemark) |
| | | }) |
| | | |
| | | const fileWaylines = waylinesContext(fileTemplate) |
| | | return { |
| | | fileName: waylineName || fileName, |
| | | template: fileTemplate, |
| | | waylines: fileWaylines, |
| | | } |
| | | } catch (error) { |
| | | console.log(error) |
| | | return error |
| | | } |
| | | } |
| | | |
| | | const save = async fileInfo => { |
| | | const JsZip = new JSZIP() |
| | | const { fileName, template, waylines } = fileInfo |
| | | const templateXml = JSONToXML(template, '', true) |
| | | const waylinesWpml = JSONToXML(waylines, '', true) |
| | | JsZip.file('wpmz/template.kml', templateXml) |
| | | JsZip.file('wpmz/waylines.wpml', waylinesWpml) |
| | | const fileBlob = await JsZip.generateAsync({ type: 'blob' }) |
| | | // saveAs(kmzFile, `${fileName}.kmz`) |
| | | return fileBlob |
| | | } |
| | | |
| | | const waylinesContext = templateJson => { |
| | | const waylinesObj = { |
| | | // 标准模版 |
| | | missionConfig: { |
| | | flyToWaylineMode: null, |
| | | finishAction: null, |
| | | exitOnRCLost: null, |
| | | executeRCLostAction: null, |
| | | takeOffSecurityHeight: null, |
| | | globalTransitionalSpeed: null, |
| | | globalRTHHeight: null, |
| | | droneInfo: null, |
| | | payloadInfo: null, |
| | | }, |
| | | Folder: { |
| | | templateId: null, |
| | | executeHeightMode: null, |
| | | waylineId: null, |
| | | autoFlightSpeed: null, |
| | | /* |
| | | waylines.wpml和template.xml的Placemark通用 |
| | | 不同点: |
| | | waylineId = templateId |
| | | ellipsoidHeight、 height 替换成 executeHeight |
| | | */ |
| | | Placemark: null, |
| | | }, |
| | | } |
| | | Object.keys(waylinesObj).forEach(key => { |
| | | if (Object.prototype.toString.call(waylinesObj[key]) === '[object Object]') { |
| | | Object.keys(waylinesObj[key]).forEach(item => { |
| | | waylinesObj[key][item] = templateJson[key][item] |
| | | if (item === 'executeHeightMode') { |
| | | waylinesObj[key][item] = templateJson[key].waylineCoordinateSysParam.heightMode |
| | | } |
| | | if (item === 'waylineId') { |
| | | waylinesObj[key][item] = templateJson[key].templateId |
| | | } |
| | | if (item === 'Placemark') { |
| | | const placemarks = _.cloneDeep(templateJson[key][item]) |
| | | placemarks.forEach(placemark => { |
| | | placemark.executeHeight = { |
| | | '#text': placemark.ellipsoidHeight?.['#text'] || '', |
| | | } |
| | | delete placemark.ellipsoidHeight |
| | | delete placemark.height |
| | | }) |
| | | waylinesObj[key][item] = placemarks |
| | | } |
| | | }) |
| | | } else { |
| | | waylinesObj[key] = templateJson[key] |
| | | } |
| | | }) |
| | | return waylinesObj |
| | | } |
| | | |
| | | return { |
| | | create, |
| | | createNoramlWaylines, |
| | | createPlanarWayline, |
| | | createPointWayLines, |
| | | save, |
| | | } |
| | | } |
| | |
| | | import { useStore } from 'vuex' |
| | | import { getDeviceRegion, getDeviceRegionCount } from '@/api/home/aggregation' |
| | | import userStore from '@/store/modules/user' |
| | | import { areaCodeToArr, getContourByCode } from '@/utils/cesium/mapUtil' |
| | | // import { getEllipse } from '@/hooks/useSingleDroneMap/useSingleDroneMap' |
| | | import { areaCodeToArr, getContourByCode, getDockPolyLine, getLnglatAltitude } from '@/utils/cesium/mapUtil' |
| | | import getBaseConfig from '@/buildConfig/config' |
| | | import { getDroneStatusImage } from '@/utils/stateToImageMap/drone' |
| | | import { |
| | | GroundCirclePrimitiveManager |
| | | } from '@/utils/mapUtils' |
| | | import { GroundCirclePrimitiveManager } from '@/utils/mapUtils' |
| | | |
| | | import lowBattery from '@/assets/images/aiNowFly/low-battery.svg' |
| | | import mediumBattery from '@/assets/images/aiNowFly/medium-battery.svg' |
| | | import fullBattery from '@/assets/images/aiNowFly/full-charge.svg' |
| | | |
| | | const { VITE_APP_BASE, VITE_APP_ENV, VITE_APP_REGION_URL } = import.meta.env |
| | | // const { singleDockSystem } = getBaseConfig() |
| | | const { singleDockSystem } = getBaseConfig() |
| | | |
| | | |
| | | |
| | | /** |
| | | * 使用通用的边界 |
| | |
| | | * @param options |
| | | */ |
| | | export const useBoundary = (viewer, options = {}) => { |
| | | const { multiple = 1.4, dockOptions = {}, boundaryChange, boundaryColor, dockRangeType, useDockHeight = false } = options |
| | | const { scrollShowDock = false, showDock = false } = dockOptions |
| | | const { |
| | | multiple = 1.4, |
| | | dockOptions = {}, |
| | | boundaryChange, |
| | | boundaryColor, |
| | | dockRangeType, |
| | | useDockHeight = false, |
| | | } = options |
| | | const { scrollShowDock = false, showDock = false, showDockNameAndBattery = false } = dockOptions |
| | | |
| | | const manager = new GroundCirclePrimitiveManager() |
| | | manager.init(viewer) |
| | |
| | | } |
| | | |
| | | // 获取设备列表 |
| | | async function getDeviceRegionFun () { |
| | | async function getDeviceRegionFun() { |
| | | const res = await getDeviceRegion({ areaCode: selectedAreaCode }) |
| | | return res?.data?.data || [] |
| | | } |
| | |
| | | return await res.json() |
| | | } |
| | | |
| | | function getBase64Image(imgUrl) { |
| | | const img = new Image() |
| | | const canvas = document.createElement('canvas') |
| | | const ctx = canvas.getContext('2d') |
| | | |
| | | return new Promise(resolve => { |
| | | img.onload = function () { |
| | | canvas.width = img.width |
| | | canvas.height = img.height |
| | | ctx.drawImage(img, 0, 0) |
| | | const base64 = canvas.toDataURL('image/png') |
| | | resolve(base64) |
| | | } |
| | | img.src = imgUrl |
| | | }) |
| | | } |
| | | |
| | | function batteryLevelSvg(batteryNum) { |
| | | if (batteryNum <= 30) { |
| | | return lowBattery |
| | | } else if (batteryNum > 30 && batteryNum <= 60) { |
| | | return mediumBattery |
| | | } else { |
| | | return fullBattery |
| | | } |
| | | } |
| | | |
| | | async function nestSVG(nickname, batteryImgUrl, batteryNum) { |
| | | let txt = nickname.length < 4 ? 130 : nickname.length * 38 |
| | | let tranx = nickname.length < 4 ? 86 : nickname.length * 26 + 3 |
| | | const batteryBase64Image = await getBase64Image(batteryImgUrl) |
| | | const svg = ` |
| | | <svg width="${txt}" height="100" xmlns="http://www.w3.org/2000/svg"> |
| | | <g transform="translate(${tranx}, 10)"> |
| | | <!-- 电池图标 --> |
| | | <image href="${batteryBase64Image}" x="0" y="2" width="12" height="13"/> |
| | | <!-- 电量值 --> |
| | | <text x="12" y="10" font-size="14" fill="${ |
| | | batteryNum <= 30 ? '#FF604B' : batteryNum >= 60 ? '#40FF5C' : '#00FFF2' |
| | | }" text-anchor="start" dominant-baseline="middle" font-weight="bolder" font-family="Source Han Sans CN" text-rendering="geometricPrecision"> |
| | | ${batteryNum}% |
| | | </text> |
| | | </g> |
| | | </svg> |
| | | ` |
| | | return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}` |
| | | } |
| | | |
| | | // 无人机散点渲染 |
| | | const droneSplashed = () => { |
| | | let showDockList = showDockSnList === null |
| | | ? _.cloneDeep(initDockList) |
| | | : initDockList.filter(item => showDockSnList.includes(item.device_sn)) |
| | | manager.removeAll() |
| | | let showDockList = |
| | | showDockSnList === null |
| | | ? _.cloneDeep(initDockList) |
| | | : initDockList.filter(item => showDockSnList.includes(item.device_sn)) |
| | | |
| | | showDockList.forEach((item, index) => { |
| | | showDockList.forEach(async (item, index) => { |
| | | if (item.status === 'OFFLINE') return |
| | | let polyline = {} |
| | | |
| | | if (dockRangeType === 2) { |
| | | manager.addCircleOutline({ |
| | | data: { |
| | | lng: item.longitude, |
| | | lat: item.latitude |
| | | lat: item.latitude, |
| | | }, |
| | | frameColor: Cesium.Color.fromCssColorString('#7FFFD4') |
| | | frameColor: Cesium.Color.fromCssColorString('#7FFFD4'), |
| | | }) |
| | | } else { |
| | | manager.addCircle({ |
| | | data: { |
| | | lng: item.longitude, |
| | | lat: item.latitude |
| | | lat: item.latitude, |
| | | }, |
| | | materialColor: Cesium.Color.CORNFLOWERBLUE.withAlpha(0.3) |
| | | materialColor: item.color ? item.color : Cesium.Color.CORNFLOWERBLUE.withAlpha(0.3), |
| | | }) |
| | | } |
| | | |
| | | if (useDockHeight) { |
| | | polyline = { |
| | | positions: new Cesium.CallbackProperty(() => { |
| | | const pointPosition = Cesium.Cartesian3.fromDegrees(+item.longitude, +item.latitude, +item.height) |
| | | if (!pointPosition) return [] |
| | | // 获取点的位置 |
| | | const cartographic = Cesium.Cartographic.fromCartesian(pointPosition) |
| | | // 获取地形高度 |
| | | const terrainHeight = viewer.scene.globe.getHeight(cartographic) || 0 |
| | | // 创建地面点位置 |
| | | const groundPosition = Cesium.Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, terrainHeight) |
| | | |
| | | return [pointPosition, groundPosition] |
| | | }, false), |
| | | width: 0.5, |
| | | material: Cesium.Color.WHITE, |
| | | } |
| | | polyline = getDockPolyLine(item, viewer) |
| | | } |
| | | |
| | | const batteryImgUrl = batteryLevelSvg(item.capacity_percent) |
| | | const combinedImage = await nestSVG(item.nickname, batteryImgUrl, item.capacity_percent) |
| | | let pointObj = {} |
| | | try { |
| | | pointObj = await getLnglatAltitude(item.longitude, item.latitude, viewer) |
| | | } catch (e) {} |
| | | const position = Cesium.Cartesian3.fromDegrees( |
| | | +item.longitude, |
| | | +item.latitude, |
| | | useDockHeight ? +item.height : pointObj?.height || 0 |
| | | ) |
| | | if (showDockNameAndBattery) { |
| | | // 带电量 |
| | | dockSource.entities.add({ |
| | | position, |
| | | billboard: { |
| | | // new Cesium.ConstantProperty(pointData.status === 'OFFLINE' ? endingImg : unSelectedOnline), |
| | | image: combinedImage, |
| | | }, |
| | | }) |
| | | } |
| | | dockSource.entities.add({ |
| | | position: Cesium.Cartesian3.fromDegrees(+item.longitude, +item.latitude, !useDockHeight ? 0 : +item.height), |
| | | label: { |
| | | text: item.nickname, |
| | | font: 'bold 16px Source Han Sans CN', |
| | | fillColor: Cesium.Color.WHITE, |
| | | outlineColor: Cesium.Color.BLACK, |
| | | outlineWidth: 2, |
| | | style: Cesium.LabelStyle.FILL_AND_OUTLINE, |
| | | verticalOrigin: Cesium.VerticalOrigin.BOTTOM, |
| | | pixelOffset: new Cesium.Cartesian2(0, -24), |
| | | }, |
| | | position, |
| | | polyline, |
| | | billboard: { |
| | | image: getDroneStatusImage(item.status), |
| | | width: 60, |
| | | height: 60, |
| | | eyeOffset: new Cesium.Cartesian3(0, 0, -5) |
| | | disableDepthTestDistance: Number.POSITIVE_INFINITY, |
| | | eyeOffset: new Cesium.Cartesian3(0, 0, -5), |
| | | }, |
| | | }) |
| | | }) |
| | |
| | | positions: positions, |
| | | width: 5, // 直接设置宽度 |
| | | clampToGround: true, |
| | | material: Cesium.Color.fromCssColorString(boundaryColor) |
| | | material: Cesium.Color.fromCssColorString(boundaryColor), |
| | | }, |
| | | polygon, |
| | | }) |
| | |
| | | positions: positions, |
| | | width: 1, // 直接设置宽度 |
| | | clampToGround: true, |
| | | material: Cesium.Color.fromCssColorString(boundaryColor) |
| | | material: Cesium.Color.fromCssColorString(boundaryColor), |
| | | }, |
| | | }) |
| | | }) |
| | |
| | | // 打开边界 |
| | | const openContour = async () => { |
| | | const areaCode = selectedAreaCode || userAreaCode |
| | | |
| | | viewer.scene.postRender.removeEventListener(determineScaling) |
| | | if (!areaCode) return |
| | | const hierarchy = areaCodeToArr(areaCode.slice(0, 6)) |
| | |
| | | const outlineGJson = await getContourByCode(areaCode.slice(0, 6)) |
| | | scalingJudgment.forEach(item => item.show && (item.outline = outlineGJson)) |
| | | renderOutline(scalingJudgment[(hierarchy.length - 3) * -1]) |
| | | } catch (e) { } |
| | | viewer.scene.postRender.addEventListener(determineScaling) |
| | | } catch (e) {} |
| | | try { |
| | | viewer?.scene?.postRender?.addEventListener(determineScaling) |
| | | }catch (e) { |
| | | |
| | | } |
| | | } |
| | | |
| | | // 关闭边界 |
| | | function closeContour () { |
| | | function closeContour() { |
| | | removeAllEntities() |
| | | viewer.scene.postRender.removeEventListener(determineScaling) |
| | | } |
| | | |
| | | // 飞向边界 |
| | | async function flyToBoundary () { |
| | | async function flyToBoundary() { |
| | | // 单机巢系统 |
| | | if (singleDockSystem) { |
| | | if (!initDockList.length) { |
| | | initDockList = await getDeviceRegionFun() |
| | | } |
| | | viewer?.camera.flyTo({ |
| | | destination: Cesium.Cartesian3.fromDegrees(initDockList[0].longitude, initDockList[0].latitude, 24000), |
| | | destination: Cesium.Cartesian3.fromDegrees(initDockList[0].longitude, initDockList[0].latitude, 22000), |
| | | duration: 0, |
| | | }) |
| | | return |
| | |
| | | const dataSource = await Cesium.GeoJsonDataSource.load(gJson) |
| | | if (!dataSource) return |
| | | viewer.dataSources.add(dataSource) |
| | | |
| | | // 获取多边形边界所有点 |
| | | let positionList = [] |
| | | dataSource.entities.values.forEach(function (entity) { |
| | | if (entity.polygon) { |
| | | // 获取多边形的边界球 |
| | |
| | | let cartographic = Cesium.Cartographic.fromCartesian(item) |
| | | let lng = Cesium.Math.toDegrees(cartographic.longitude) // 经度 |
| | | let lat = Cesium.Math.toDegrees(cartographic.latitude) // 纬度 |
| | | |
| | | return [_.round(lng, 6), _.round(lat, 6)] |
| | | }) |
| | | |
| | | const newBox = boxTransformScale(curPolygonPosition, multiple) |
| | | |
| | | viewer.camera.flyTo({ |
| | | destination: Cesium.Rectangle.fromDegrees(...newBox), |
| | | offset: new Cesium.HeadingPitchRange(0, Cesium.Math.toRadians(-90), 0), |
| | | duration: 0.5, |
| | | }) |
| | | positionList = positionList.concat(curPolygonPosition) |
| | | } |
| | | }) |
| | | const newBox = boxTransformScale(positionList, multiple) |
| | | viewer.camera.flyTo({ |
| | | destination: Cesium.Rectangle.fromDegrees(...newBox), |
| | | offset: new Cesium.HeadingPitchRange(0, Cesium.Math.toRadians(-90), 0), |
| | | duration: 0.5, |
| | | }) |
| | | |
| | | dataSource.entities.values.forEach(entity => { |
| | |
| | | * 设置要显示的机巢列表 |
| | | * @param dockSns - 要显示的机巢设备序列号数组。如果为 undefined,则显示所有已初始化的机巢;如果为空数组,则不显示任何机巢。 |
| | | */ |
| | | function setShowDock (dockSns) { |
| | | function setShowDock(dockSns) { |
| | | if (dockSns === undefined) { |
| | | showDockSnList = null |
| | | } else if (dockSns.length === 0) { |
| | |
| | | } else { |
| | | showDockSnList = dockSns |
| | | } |
| | | |
| | | removeDockCover() |
| | | |
| | | if (active === '县') droneSplashed() |
| | | if (!showDock) return |
| | | if (scrollShowDock ? active === '县' : true) { |
| | | droneSplashed() |
| | | } |
| | | } |
| | | |
| | | function setDockCoverColor(arr) { |
| | | initDockList.forEach(item => { |
| | | arr.forEach(dock => { |
| | | item.color = dock.device_sn === item.device_sn ? dock.color : null |
| | | }) |
| | | }) |
| | | if (scrollShowDock ? active === '县' : true) { |
| | | droneSplashed() |
| | | } |
| | | } |
| | | onBeforeUnmount(() => { |
| | | manager.destroy() |
| | | }) |
| | | |
| | | |
| | | return { |
| | | openContour, |
| | | closeContour, |
| | | flyToBoundary, |
| | | setShowDock |
| | | setShowDock, |
| | | setDockCoverColor, |
| | | } |
| | | } |