吉安感知网项目-前端
shuishen
2026-01-16 efb3b5e84ed4493f82f30ed3c3dcdff0fd5a8083
feat:任务工单后台地图
7 files modified
6 files added
1 files deleted
4069 ■■■■ changed files
applications/task-work-order/src/utils/cesium/DrawPolygon.js 803 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/utils/cesium/Material/index.js 18 ●●●● patch | view | raw | blame | history
applications/task-work-order/src/utils/cesium/common.js 8 ●●●● patch | view | raw | blame | history
applications/task-work-order/src/utils/cesium/compressImage.js 55 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/utils/cesium/compressImage2.js 46 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/utils/cesium/createRouteLine.js 923 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/utils/cesium/eventBus.js 5 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/utils/cesium/frustum/CesiumVideoFrustum.js 712 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/utils/cesium/frustum/CoordinateTranslate.js 229 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/utils/cesium/kmz.js 10 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/utils/cesium/mapUtil.js 207 ●●●● patch | view | raw | blame | history
applications/task-work-order/src/utils/cesium/publicCesium.js 168 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/utils/cesium/use-kmz-tsa.js 675 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/utils/cesium/useBoundary.js 210 ●●●● patch | view | raw | blame | history
applications/task-work-order/src/utils/cesium/DrawPolygon.js
New file
@@ -0,0 +1,803 @@
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()
    }
}
applications/task-work-order/src/utils/cesium/Material/index.js
@@ -54,6 +54,7 @@
    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
@@ -64,6 +65,9 @@
    this.opacity = options.opacity || 0.5
    this.alphaPower = options.alphaPower || 1.5
    this.speed = options.speed || 5
    this.xRepeatCount = options.xRepeatCount || 1
  }
  get isConstant () {
@@ -132,15 +136,17 @@
      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);
@@ -150,7 +156,7 @@
                  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) {
@@ -182,6 +188,8 @@
    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
  }
@@ -191,7 +199,8 @@
      (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))
    )
  }
}
@@ -200,6 +209,7 @@
  color: Cesium.createPropertyDescriptor('color'),
  opacity: Cesium.createPropertyDescriptor('opacity'),
  speed: Cesium.createPropertyDescriptor('speed'),
  xRepeatCount: Cesium.createPropertyDescriptor('xRepeatCount'),
})
applications/task-work-order/src/utils/cesium/common.js
@@ -131,22 +131,22 @@
    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
applications/task-work-order/src/utils/cesium/compressImage.js
New file
@@ -0,0 +1,55 @@
/**
 * @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)
    })
}
applications/task-work-order/src/utils/cesium/compressImage2.js
New file
@@ -0,0 +1,46 @@
/*
 * @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
    })
}
applications/task-work-order/src/utils/cesium/createRouteLine.js
@@ -2,17 +2,16 @@
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'
@@ -23,61 +22,21 @@
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) {
@@ -95,10 +54,108 @@
    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) {
@@ -119,10 +176,9 @@
    /**
     * 初始化viewer
     * @param {*} viewer
     * @param {*} viewer
     */
    initCreateRoute (viewer) {
        indexLog = 0
        this.viewer = viewer
    }
@@ -138,24 +194,27 @@
            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()}`)
@@ -178,16 +237,16 @@
        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)
@@ -214,7 +273,7 @@
                    waylinesXMLObj[0],
                    missionConfig,
                    templateXmlJson,
                    dronePosition,
                    dockPosition,
                    data,
                    startPoint,
                    isShowDock,
@@ -227,7 +286,7 @@
                waylinesXMLObj,
                missionConfig,
                templateXmlJson,
                dronePosition,
                dockPosition,
                data,
                startPoint,
                isShowDock,
@@ -238,7 +297,7 @@
            return this.drawPointRoute(
                waylinesXMLObj,
                missionConfig,
                dronePosition,
                dockPosition,
                data,
                startPoint,
                isShowDock,
@@ -248,50 +307,231 @@
        }
    }
    // 拆分航线
    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,
@@ -300,30 +540,30 @@
            },
            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)
@@ -340,9 +580,7 @@
            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),
@@ -357,11 +595,8 @@
        // 绘制面状地块--------------------
        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),
@@ -384,7 +619,7 @@
                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,
@@ -395,64 +630,31 @@
            })
        } 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,
        }
    }
@@ -472,9 +674,7 @@
        let setting = {
            name: 'work-drone-route-planar-start',
            position: positions,
            billboard: {
                image: this.planarBillboard('#2D8CF0'),
                pixelOffset: new Cesium.Cartesian2(146, 72),
@@ -489,35 +689,48 @@
     * 绘制点航线
     * @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,
@@ -525,10 +738,9 @@
                            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,
@@ -540,9 +752,7 @@
                } else if (index === filePositions.length - 1) {
                    this.viewer.entities.add({
                        name: 'work-drone-route-point-end',
                        position,
                        billboard: {
                            image: newEndPointImg,
                            outlineWidth: 0,
@@ -556,9 +766,7 @@
                } else {
                    this.viewer.entities.add({
                        name: 'work-drone-route-point-approach',
                        position,
                        billboard: {
                            image: newNumPoint,
                            width: 50,
@@ -577,96 +785,56 @@
                    })
                }
            })
            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,
                },
            })
        }
@@ -674,47 +842,45 @@
            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(',')
@@ -730,12 +896,14 @@
        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 = []
@@ -773,28 +941,38 @@
        })
        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()
                ),
            })
        }
@@ -805,9 +983,9 @@
                return {
                    ...item,
                    alt: Number(height)
                    alt: Number(height),
                }
            })
            }),
        }
    }
@@ -819,9 +997,10 @@
            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)
            })
    }
    // 当前无人机视椎体相关
@@ -847,11 +1026,22 @@
                ['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
                    }
                }
            }
@@ -871,8 +1061,8 @@
                    pitch: 0,
                    heading: attitude_head,
                    isShowVideoPlan: this.isShowVideoPlan
                })
                    isShowVideoPlan: this.isShowVideoPlan,
                }),
            })
        }
    }
@@ -882,7 +1072,7 @@
        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',
            })
        }
    }
@@ -897,14 +1087,9 @@
        }
    }
    // 设置当前无人机视椎体信息面板
    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 => {
@@ -924,7 +1109,9 @@
                }
                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,
@@ -936,7 +1123,6 @@
                return
            }
        }
    }
@@ -954,8 +1140,10 @@
        }
    }
    // 设置无人机模型
    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 => {
@@ -963,9 +1151,18 @@
            })
            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
            }
@@ -974,18 +1171,35 @@
                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) {
@@ -1010,9 +1224,10 @@
                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)
                })
        }
    }
@@ -1021,13 +1236,15 @@
            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()
            })
    }
    /**
@@ -1043,19 +1260,19 @@
            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)
@@ -1071,14 +1288,16 @@
            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) {
@@ -1090,28 +1309,30 @@
        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'
                }
            })
    }
    // 移除弹框标签
@@ -1137,4 +1358,4 @@
        const that = this
        that.viewer?.scene.postRender.removeEventListener(that.labelBoxRenderHandler)
    }
}
}
applications/task-work-order/src/utils/cesium/eventBus.js
File was deleted
applications/task-work-order/src/utils/cesium/frustum/CesiumVideoFrustum.js
New file
@@ -0,0 +1,712 @@
/*
 * @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
applications/task-work-order/src/utils/cesium/frustum/CoordinateTranslate.js
New file
@@ -0,0 +1,229 @@
/*
 * @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
applications/task-work-order/src/utils/cesium/kmz.js
@@ -209,6 +209,11 @@
}
/**
 * 递归处理对象,移除 #text 键并返回其值,同时处理数值字符串到数字的转换
 * @param {any} obj - 需要处理的对象或值
 * @returns {any} 处理后的对象或值
 */
export function removeTextKey(obj) {
    if (typeof obj !== 'object' || obj === null) {
        // 如果是数值字符串,转换为数值
@@ -219,18 +224,21 @@
    }
    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]);
        }
    }
applications/task-work-order/src/utils/cesium/mapUtil.js
@@ -1,7 +1,8 @@
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
@@ -190,6 +191,66 @@
    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) => {
@@ -285,31 +346,24 @@
    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,
@@ -317,18 +371,14 @@
                        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 })))
            })
    })
}
@@ -410,11 +460,15 @@
    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))
    )
@@ -439,7 +493,7 @@
            range: boundingSphere.radius * multiple,
        },
        complete: () => {
            // 飞行完成后检查相机高度是否小于最高点,如果小于直接飞到最高点
            // 飞行完成后检查相机高度是否小于最高点
            const cameraHeight = Cesium.Cartographic.fromCartesian(viewer.camera.position).height
            if (cameraHeight < maxHeight) {
                viewer.camera.flyTo({
@@ -529,7 +583,7 @@
// 辅助函数:创建旋转文字的画布
export function createRotatedTextCanvas(time) {
    const prefix = '预计'
    const prefix = '预计到达'
    const suffix = '分钟'
    const canvas = document.createElement('canvas')
    const context = canvas.getContext('2d')
@@ -537,7 +591,9 @@
    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 // 估算高度
@@ -550,17 +606,116 @@
    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),
    }
}
applications/task-work-order/src/utils/cesium/publicCesium.js
@@ -70,6 +70,7 @@
 * @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显示覆盖圈
@@ -92,15 +93,14 @@
            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
@@ -120,6 +120,10 @@
            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
@@ -143,33 +147,47 @@
        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)
            }
@@ -180,6 +198,12 @@
        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 () {
@@ -188,53 +212,69 @@
    // 销毁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()
    }
@@ -257,7 +297,7 @@
        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(
@@ -324,9 +364,7 @@
            this.viewer?.imageryLayers.lowerToBottom(find.mapLayer)
        })
    }
    // 设置或重置滤镜
    setCurrentLayerFilter (data, mode) {
        if (mode === 17) {
            data.mapLayer.brightness = 0.6 // 亮度(默认值)
applications/task-work-order/src/utils/cesium/use-kmz-tsa.js
New file
@@ -0,0 +1,675 @@
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,
    }
}
applications/task-work-order/src/utils/cesium/useBoundary.js
@@ -6,19 +6,17 @@
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()
/**
 * 使用通用的边界
@@ -26,8 +24,15 @@
 * @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)
@@ -56,7 +61,7 @@
    }
    // 获取设备列表
    async function getDeviceRegionFun () {
    async function getDeviceRegionFun() {
        const res = await getDeviceRegion({ areaCode: selectedAreaCode })
        return res?.data?.data || []
    }
@@ -100,60 +105,127 @@
        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),
                },
            })
        })
@@ -186,7 +258,7 @@
                            positions: positions,
                            width: 5, // 直接设置宽度
                            clampToGround: true,
                            material: Cesium.Color.fromCssColorString(boundaryColor)
                            material: Cesium.Color.fromCssColorString(boundaryColor),
                        },
                        polygon,
                    })
@@ -205,7 +277,7 @@
                        positions: positions,
                        width: 1, // 直接设置宽度
                        clampToGround: true,
                        material: Cesium.Color.fromCssColorString(boundaryColor)
                        material: Cesium.Color.fromCssColorString(boundaryColor),
                    },
                })
            })
@@ -217,7 +289,6 @@
    // 打开边界
    const openContour = async () => {
        const areaCode = selectedAreaCode || userAreaCode
        viewer.scene.postRender.removeEventListener(determineScaling)
        if (!areaCode) return
        const hierarchy = areaCodeToArr(areaCode.slice(0, 6))
@@ -258,25 +329,29 @@
            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
@@ -286,7 +361,8 @@
        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) {
                // 获取多边形的边界球
@@ -295,18 +371,16 @@
                    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 => {
@@ -320,7 +394,7 @@
     * 设置要显示的机巢列表
     * @param dockSns - 要显示的机巢设备序列号数组。如果为 undefined,则显示所有已初始化的机巢;如果为空数组,则不显示任何机巢。
     */
    function setShowDock (dockSns) {
    function setShowDock(dockSns) {
        if (dockSns === undefined) {
            showDockSnList = null
        } else if (dockSns.length === 0) {
@@ -328,21 +402,31 @@
        } 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,
    }
}