Cesium 无人机巡检三维效果实现

uavInspection.js代码

/**
 * 无人机巡检
 */

import * as mars3d from "mars3d"
import * as Cesium from 'mars3d-cesium'
import * as turf from "@turf/turf"

import { getMap } from '@/components/mars3dMap/js/index.js'
import { getHeightByLngLat } from '@/components/mars3dMap/js/utils.js'
import GlowLineMaterialProperty from '@/components/mars3dMap/js/material/GlowLineMaterialProperty.js'

import iamge from "@/components/mars3dMap/images/路径撒点.png";
import iamge2 from "@/components/mars3dMap/images/路径撒点2.png";

let graphicLayer;

let uavInspectionMap = new Map();

let pointCallback; // 无人机经过点位回调

let UavInspection = (function () {

    /**
     * 无人机巡检
     */
    function UavInspection(id, positions) {
        this.id = id;
        this.positions = positions || [];
        this.graphicMap = new Map();
        this.step = 20;
        this.callbackPointSet = new Set(); // 回调点位集合
        if (positions && positions.length > 0) {
            this.segmentPositions = this.divideLine(positions, this.step);
        }

        if (!graphicLayer) {
            let map = getMap();
            graphicLayer = new mars3d.layer.GraphicLayer()
            map.addLayer(graphicLayer);
        }
    }

    UavInspection.prototype.finishLabels = function (index) {
        for (let i = 1; i <= index; i++) {
            this.finishLabel(i);
        }
    }

    UavInspection.prototype.finishLabel = function (index) {
        let id = `label-${index}`;
        if (this.graphicMap.has(id)) {
            let data = this.graphicMap.get(id);
            data.graphic.setStyle({ image: iamge2 });
        }
    }

    UavInspection.prototype.addLabels = function () {
        for (let [i, pos] of this.positions.entries()) {
            let samePos = false; // 第1个点和最后一个点位置是否相同
            if (this.positions.length > 0) {
                let lastPos1 = this.positions[0];
                let lastPos2 = this.positions[this.positions.length - 1];
                let distance = Cesium.Cartesian3.distance(Cesium.Cartesian3.fromDegrees(lastPos1[0], lastPos1[1], lastPos1[2]),
                    Cesium.Cartesian3.fromDegrees(lastPos2[0], lastPos2[1], lastPos2[2]));
                if (distance < 5) {
                    samePos = true;
                }
            }

            if (i < (samePos ? this.positions.length - 1 : this.positions.length)) {
                this.addLabel(i + 1, pos[0], pos[1], pos[2]);
            }
        }
    }

    UavInspection.prototype.addLabel = function (index, lng, lat, height) {
        let id = `label-${index}`;
        let data;
        if (this.graphicMap.has(id)) {
            return;
        } else {
            data = { id: id, type: 'label' };
            this.graphicMap.set(id, data);
        }

        data.graphic = new mars3d.graphic.BillboardEntity({
            id: id,
            position: [lng, lat, height],
            style: {
                image: iamge,
                scale: 0.5,
                horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
                verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
                clampToGround: false,
                pixelOffsetY: 30,
                label: {
                    text: `${index}`,
                    pixelOffsetY: -31,
                    visibleDepth: false
                }
            },
        })
        graphicLayer.addGraphic(data.graphic);
    }

    UavInspection.prototype.clear = function () {
        for (let id of this.graphicMap.keys()) {
            let data = this.graphicMap.get(id);
            this.graphicMap.delete(id);
            graphicLayer.removeGraphic(data.graphic);
            data.graphic = undefined;
            if (data.type == 'drone') {
                data.positionProperty = undefined;
            }
        }
        this.callbackPointSet.clear();
    }

    UavInspection.prototype.createPath = function () {
        return { // 实时轨迹显示
            show: true,
            leadTime: 0, // 飞机将要经过的路径,路径存在的时间
            trailTime: 60, // 飞机已经经过的路径,路径存在的时间
            width: 8, // 线宽度
            resolution: 1,
            // color: 'rgba(255, 193, 37, 1)',
            /* material: new Cesium.PolylineGlowMaterialProperty({
                glowPower: 0.25, // 轨迹线的发光强度
                color: new Cesium.Color(255 / 255, 193 / 255, 37 / 255, 1) // 颜色
            }) */
            material: new GlowLineMaterialProperty({
                color: new Cesium.Color(255 / 255, 255 / 255, 0 / 255, 1),
                power: 0.2
            })
        };
    }

    UavInspection.prototype.createModel = function () {
        return new mars3d.graphic.ModelEntity({
            position: [0, 0, 0], // 默认值
            style: {
                url: 'gltf/四旋翼无人机1.glb',
                scale: 6,
                minimumPixelSize: 100,
                heading: 0
            },
            path: this.createPath()
        });
    }

    UavInspection.prototype.createDrone = function () {
        let id = this.id;
        let data;
        if (this.graphicMap.has(id)) {
            return;
        } else {
            data = { id: id, type: 'drone' };
            this.graphicMap.set(id, data);
            data.positionProperty = new Cesium.SampledPositionProperty();
            data.positionProperty.forwardExtrapolationType = Cesium.ExtrapolationType.HOLD; // 后续时段保持末位置
        }

        if (!data.graphic) {
            data.graphic = this.createModel();
            graphicLayer.addGraphic(data.graphic);
            data.graphic.position = data.positionProperty;
            this.addDashLine();
        }
    }

    UavInspection.prototype.updateDronePosition = function (lng, lat, height) {
        let id = this.id;
        if (this.graphicMap.has(id)) {
            let data = this.graphicMap.get(id);
            if (data.graphic) {
                this.lastTime = new Date().getTime();
                data.position = [lng, lat, height];
                let position = Cesium.Cartesian3.fromDegrees(lng, lat, height);
                let delay = 2000; // 延迟(单位:毫秒)
                let time = Cesium.JulianDate.fromDate(new Date(new Date().getTime() + delay));
                data.positionProperty.addSample(time, position);
                setTimeout(() => {
                    this.updateDashLine(lng, lat, height);
                    this.drawBlocks(lng, lat);
                }, delay);
            }
        }
        // 无人机经纬标签时标签变色
        for (let [i, pos] of this.positions.entries()) {
            if (i < this.positions.length) {
                let distance = Cesium.Cartesian3.distance(Cesium.Cartesian3.fromDegrees(lng, lat, 0), Cesium.Cartesian3.fromDegrees(pos[0], pos[1], 0));
                if (distance < 10) {
                    this.finishLabels(i + 1);
                }
            }
        }
        // 指定点位回调
        for (let [i, pos] of this.positions.entries()) {
            let distance = Cesium.Cartesian3.distance(Cesium.Cartesian3.fromDegrees(lng, lat, 0), Cesium.Cartesian3.fromDegrees(pos[0], pos[1], 0));
            if (distance < 10) {
                if (pointCallback && !this.callbackPointSet.has(i + 1)) {
                    let map = getMap();
                    const screenPoint = map.scene.cartesianToCanvasCoordinates(Cesium.Cartesian3.fromDegrees(lng, lat, height));
                    this.callbackPointSet.add(i + 1);
                    setTimeout(() => {
                        pointCallback(i + 1, screenPoint);
                    }, 100);
                }
            }
        }
    }

    UavInspection.prototype.addDashLine = function () {
        let id = `dashline-${this.id}`;
        let data;
        if (this.graphicMap.has(id)) {
            return;
        } else {
            data = { id: id, type: 'dashline' };
            this.graphicMap.set(id, data);
        }

        if (!data.graphic) {
            data.graphic = new mars3d.graphic.PolylineEntity({
                positions: undefined,
                style: {
                    width: 2,
                    clampToGround: false,
                    materialType: mars3d.MaterialType.PolylineDash,
                    materialOptions: {
                        color: '#ff0000',
                        dashLength: 16.0
                    }
                }
            })
            graphicLayer.addGraphic(data.graphic)
        }
    }

    UavInspection.prototype.updateDashLine = function (lng, lat, height) {
        let id = `dashline-${this.id}`;
        if (this.graphicMap.has(id)) {
            let data = this.graphicMap.get(id);
            (async () => {
                let h = await getHeightByLngLat(getMap(), lng, lat);
                if (data.graphic) {
                    data.graphic.setCallbackPositions([[lng, lat, height], [lng, lat, h]]);
                }
            })();

        }
    }

    /**
     * 计算正方形在三维空间中的四个顶点坐标
     */
    UavInspection.prototype.calculateSquareVertices = function (lng, lat, angle, step) {
        let centerPoint = Cesium.Cartesian3.fromDegrees(lng, lat, 0);

        // 获取中心点处的ENU坐标系基向量(东、北、天)
        const matrix = Cesium.Transforms.eastNorthUpToFixedFrame(centerPoint);
        const east = new Cesium.Cartesian3();
        const north = new Cesium.Cartesian3();
        Cesium.Matrix4.getColumn(matrix, 0, east);  // 东方向单位向量
        Cesium.Matrix4.getColumn(matrix, 1, north); // 北方向单位向量

        const halfLen = step / 2;
        const angleRad = Cesium.Math.toRadians(angle);  // 转为弧度

        // 1. 计算目标方向向量(θ方向)在ENU坐标系的投影分量
        const cosTheta = Math.cos(angleRad);
        const sinTheta = Math.sin(angleRad);

        // 计算主方向单位向量 (基于东和北分量)
        const dirX = cosTheta;
        const dirY = -sinTheta; // 负号用于处理顺时针旋转

        // 构造三维空间中的方向向量
        const directionVector = new Cesium.Cartesian3();
        Cesium.Cartesian3.multiplyByScalar(east, dirX, directionVector);
        Cesium.Cartesian3.add(
            directionVector,
            Cesium.Cartesian3.multiplyByScalar(north, dirY, new Cesium.Cartesian3()),
            directionVector
        );

        // 2. 计算垂直方向向量 (θ+90°) 的分量
        const perpX = -sinTheta;
        const perpY = -cosTheta;

        // 构造三维空间中的垂直向量
        const perpendicularVector = new Cesium.Cartesian3();
        Cesium.Cartesian3.multiplyByScalar(east, perpX, perpendicularVector);
        Cesium.Cartesian3.add(
            perpendicularVector,
            Cesium.Cartesian3.multiplyByScalar(north, perpY, new Cesium.Cartesian3()),
            perpendicularVector
        );

        // 3. 计算四个顶点
        const result = [];
        const scratch = new Cesium.Cartesian3();

        // 顶点计算函数 (避免重复代码)
        const calculatePoint = (xFactor, yFactor) => {
            const point = scratch;
            // point = centerPoint + (directionVector * xFactor * halfLen) + (perpendicularVector * yFactor * halfLen)
            Cesium.Cartesian3.multiplyByScalar(directionVector, xFactor * halfLen, point);
            Cesium.Cartesian3.add(
                point,
                Cesium.Cartesian3.multiplyByScalar(perpendicularVector, yFactor * halfLen, new Cesium.Cartesian3()),
                point
            );
            return Cesium.Cartesian3.add(centerPoint, point, new Cesium.Cartesian3());
        };

        // 生成4个顶点(顺时针顺序)
        result.push(calculatePoint(1, -1));  // 起点:右下
        result.push(calculatePoint(1, 1));  // 右上
        result.push(calculatePoint(-1, 1));  // 左上
        result.push(calculatePoint(-1, -1));  // 左下

        return result;
    }

    UavInspection.prototype.getSquareVertices = function (pos, pos1, pos2, step) {
        let angle = this.calculateBearingFromEast(pos1, pos2);
        let r = this.calculateSquareVertices(pos[0], pos[1], angle, step);
        let result = { center: pos, positions: [] };
        for (let i = 0; i < r.length; i++) {
            const cartographic = Cesium.Cartographic.fromCartesian(r[i]);
            let position = [Cesium.Math.toDegrees(cartographic.longitude), Cesium.Math.toDegrees(cartographic.latitude), cartographic.height];
            result.positions.push(position);
        }
        return result;
    }

    UavInspection.prototype.getRectangleVertices = function (pos1, pos2, step) {
        let mid1 = Cesium.Cartesian3.fromDegrees(pos1[0], pos1[1], 0);
        let mid2 = Cesium.Cartesian3.fromDegrees(pos2[0], pos2[1], 0);

        const { Cartesian3, Ellipsoid, Cartographic } = Cesium;
        const ellipsoid = Ellipsoid.WGS84;

        // 计算中点连线方向向量
        const v1 = Cartesian3.subtract(mid2, mid1, new Cartesian3());
        const distance = Cartesian3.magnitude(v1);
        Cartesian3.normalize(v1, v1);

        // 计算垂直方向向量
        const center = Cartesian3.midpoint(mid1, mid2, new Cartesian3());
        const normal = ellipsoid.geodeticSurfaceNormal(center, new Cartesian3());
        const v2 = Cartesian3.cross(normal, v1, new Cartesian3());
        Cartesian3.normalize(v2, v2);

        // 缩放垂直向量到指定长度
        const halfWidth = Cartesian3.multiplyByScalar(v2, step / 2.0, new Cartesian3());

        // 计算四个顶点的笛卡尔坐标
        const cornersCartesian = [
            Cartesian3.add(mid1, Cartesian3.negate(halfWidth, new Cartesian3()), new Cartesian3()), // SW
            Cartesian3.add(mid1, halfWidth, new Cartesian3()),  // NW
            Cartesian3.add(mid2, halfWidth, new Cartesian3()),   // NE
            Cartesian3.add(mid2, Cartesian3.negate(halfWidth, new Cartesian3()), new Cartesian3())  // SE
        ];

        // 转换为经纬度(弧度)坐标
        const cornersRadians = cornersCartesian.map(cartesian => {
            return Cartographic.fromCartesian(cartesian);
        });

        // 转换为度数
        return cornersRadians.map(rad => {
            return [Cesium.Math.toDegrees(rad.longitude), Cesium.Math.toDegrees(rad.latitude), rad.height];
        });
    }

    UavInspection.prototype.divideSegment = function (pos1, pos2, step) {
        let p1 = Cesium.Cartesian3.fromDegrees(pos1[0], pos1[1], 0);
        let p2 = Cesium.Cartesian3.fromDegrees(pos2[0], pos2[1], 0);

        let result = [];
        let lastPos = pos1;
        let currentPos;
        let distance = Cesium.Cartesian3.distance(p1, p2);
        let d = step;
        while (d < distance - 0.1) {
            let t = d / distance;

            const interpolatedCartesian = new Cesium.Cartesian3();
            Cesium.Cartesian3.lerp(p1, p2, t, interpolatedCartesian);

            const cartographic = Cesium.Cartographic.fromCartesian(interpolatedCartesian);
            currentPos = [Cesium.Math.toDegrees(cartographic.longitude), Cesium.Math.toDegrees(cartographic.latitude), cartographic.height];
            result.push({
                pos1: lastPos,
                pos2: currentPos,
                positions: this.getRectangleVertices(lastPos, currentPos, step)
            });
            lastPos = currentPos;

            d += step;
        }
        result.push({
            pos1: currentPos,
            pos2: pos2,
            positions: this.getRectangleVertices(currentPos, pos2, step)
        });
        return result;
    }

    UavInspection.prototype.divideLine = function (positions, step) {
        let result = [];
        for (let i = 0; i < positions.length - 1; i++) {
            let pos1 = positions[i];
            let pos2 = positions[i + 1];
            let array = this.divideSegment(pos1, pos2, step);
            result.push(...array);
        }
        return result.slice(0, -1);
    }

    UavInspection.prototype.drawBlocks = function (lng, lat) {
        if (this.segmentPositions && this.segmentPositions.length > 0) {
            let index = -1;
            for (let [i, pos] of this.segmentPositions.entries()) {
                let distance1 = Cesium.Cartesian3.distance(Cesium.Cartesian3.fromDegrees(pos.pos1[0], pos.pos1[1], 0), Cesium.Cartesian3.fromDegrees(pos.pos2[0], pos.pos2[1], 0));
                let distance2 = Cesium.Cartesian3.distance(Cesium.Cartesian3.fromDegrees(lng, lat, 0), Cesium.Cartesian3.fromDegrees(pos.pos1[0], pos.pos1[1], 0));
                let distance3 = Cesium.Cartesian3.distance(Cesium.Cartesian3.fromDegrees(lng, lat, 0), Cesium.Cartesian3.fromDegrees(pos.pos2[0], pos.pos2[1], 0));
                if (distance1 - (distance2 + distance3) > -0.1) {
                    index = i;
                }
            }

            for (let i = 0; i <= index; i++) {
                let pos = this.segmentPositions[i];
                let pos2
                if (i + 1 < this.segmentPositions.length) {
                    pos2 = this.segmentPositions[i + 1];
                } else {
                    pos2 = this.segmentPositions[i - 1];
                }

                this.drawBlock(i, pos);
            }
        } else {
            if (!this.lastBlockPos) {
                this.lastBlockPos = [lng, lat, 0];
                this.blockIndex = 0;
            } else {
                let distance = Cesium.Cartesian3.distance(Cesium.Cartesian3.fromDegrees(this.lastBlockPos[0], this.lastBlockPos[1], 0), Cesium.Cartesian3.fromDegrees(lng, lat, 0));
                if (distance >= this.step) {
                    let pos2 = [lng, lat, 0];
                    let vertices = this.getRectangleVertices(this.lastBlockPos, pos2, this.step);
                    this.drawBlock(this.blockIndex++, { positions: vertices });
                    this.lastBlockPos = [lng, lat, 0];
                }
            }
        }
    }

    UavInspection.prototype.drawBlock = function (i, pos) {
        let id = `block-${i}`;
        let data;
        if (this.graphicMap.has(id)) {
            return;
        } else {
            data = { id: id, type: 'block' };
            this.graphicMap.set(id, data);
        }

        if (!data.graphic) {
            data.graphic = new mars3d.graphic.PolygonPrimitive({
                positions: pos.positions,
                style: {
                    color: "#ff0000",
                    opacity: 0.3,
                    clampToGround: true
                }
            })
            graphicLayer.addGraphic(data.graphic)
        }
    }

    /**
     * 计算两点间方向(正东为0°,顺时针到360°)
     * @returns {number} 方向角度(0°到360°,正东方向为0°)
     */
    UavInspection.prototype.calculateBearingFromEast = function (pos1, pos2) {
        const point1 = Cesium.Cartographic.fromDegrees(pos1[0], pos1[1]);
        const point2 = Cesium.Cartographic.fromDegrees(pos2[0], pos2[1]);

        const lon1 = point1.longitude;
        const lat1 = point1.latitude;
        const lon2 = point2.longitude;
        const lat2 = point2.latitude;

        // 经度差(考虑跨日期变更线)
        const dLon = lon2 - lon1;

        // 计算Y分量(与正北相关)
        const y = Math.sin(dLon) * Math.cos(lat2);

        // 计算X分量(与正北相关)
        const x = Math.cos(lat1) * Math.sin(lat2) -
            Math.sin(lat1) * Math.cos(lat2) * Math.cos(dLon);

        // 计算初始方位角(0°为正北,顺时针)
        const bearingRad = Math.atan2(y, x);
        let bearingDeg = Cesium.Math.toDegrees(bearingRad);

        // 转换为0°~360°范围
        if (bearingDeg < 0) {
            bearingDeg += 360;
        }

        // 坐标系转换:正北0° → 正东0°
        // 转换公式: (bearingDeg + 90) % 360
        return (bearingDeg + 90) % 360;
    }

    return UavInspection;
})()

function createUavInspection(id, positions) {
    if (!uavInspectionMap.has(id)) {
        let uavInspection = new UavInspection(id, positions);
        uavInspection.createDrone();
        uavInspection.addLabels();
        uavInspectionMap.set(id, uavInspection);
    }
}

function uavInspectionExists(id) {
    return uavInspectionMap.has(id);
}

function updateUavInspectionPosition(id, lng, lat, height) {
    if (uavInspectionMap.has(id)) {
        let uavInspection = uavInspectionMap.get(id);
        uavInspection.updateDronePosition(lng, lat, height);
    }
}

function clearUavInspections() {
    for (let id of uavInspectionMap.keys()) {
        let uavInspection = uavInspectionMap.get(id);
        uavInspectionMap.delete(id);
        uavInspection.clear();
    }
    graphicLayer = undefined;
}

// 定时清理
setInterval(() => {
    for (let id of uavInspectionMap.keys()) {
        let uavInspection = uavInspectionMap.get(id);
        if (new Date().getTime() - uavInspection.lastTime > 20 * 1000) {
            uavInspectionMap.delete(id);
            uavInspection.clear();
        }
    }
}, 1000);

// 无人机经过点位回调
function registerPointCallback(callback) {
    pointCallback = callback;
}

export { uavInspectionExists, createUavInspection, updateUavInspectionPosition, clearUavInspections, registerPointCallback }

实时接收WebSocket数据,展示无人机、航线、地面色块

部分代码如下

import { uavInspectionExists, createUavInspection, updateUavInspectionPosition } from '@/components/mars3dMap/js/uavInspection.js'

async function processWebSocketData(json) {
    if (json.method && json.method == "FLIGHT" && json.data) {
        let data = json.data;
        let id = data.droneSN;
        let model = data.model; // 飞机型号
        let lng = data.longitude;
        let lat = data.latitude;
        let alt = data.altitude; // 海拔高度
        let heading = data.heading; // 朝向(度)
        let pitch = data.pitch; // 俯仰角(度)
        let roll = data.roll; // 翻转角(度)
        let groundSpeed = data.groundSpeed; // 地面速度,单位m/s

        if (!uavInspectionExists(id)) {
            let positions;
            if (id == config.droneSN) {
                positions = config.passingPoints;
            } else {
                positions = [];
            }
            createUavInspection(id, positions)
        }

        updateUavInspectionPosition(id, lng, lat, alt);
    }
}

效果截图

截图说明:由于测试笔记本比较卡,所以红虚线、红色块和无人机的位置不同步

来源链接:https://www.cnblogs.com/s0611163/p/18961871

© 版权声明
THE END
支持一下吧
点赞10 分享
评论 抢沙发
头像
请文明发言!
提交
头像

昵称

取消
昵称表情代码快捷回复

    暂无评论内容