路逸思 发表于 昨天 10:41

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 of this.positions.entries()) {
            let samePos = false; // 第1个点和最后一个点位置是否相同
            if (this.positions.length > 0) {
                let lastPos1 = this.positions;
                let lastPos2 = this.positions;
                let distance = Cesium.Cartesian3.distance(Cesium.Cartesian3.fromDegrees(lastPos1, lastPos1, lastPos1),
                  Cesium.Cartesian3.fromDegrees(lastPos2, lastPos2, lastPos2));
                if (distance < 5) {
                  samePos = true;
                }
            }

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

    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: ,
            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: , // 默认值
            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 = ;
                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 of this.positions.entries()) {
            if (i < this.positions.length) {
                let distance = Cesium.Cartesian3.distance(Cesium.Cartesian3.fromDegrees(lng, lat, 0), Cesium.Cartesian3.fromDegrees(pos, pos, 0));
                if (distance < 10) {
                  this.finishLabels(i + 1);
                }
            }
      }
      // 指定点位回调
      for (let of this.positions.entries()) {
            let distance = Cesium.Cartesian3.distance(Cesium.Cartesian3.fromDegrees(lng, lat, 0), Cesium.Cartesian3.fromDegrees(pos, pos, 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([, ]);
                }
            })();

      }
    }

    /**
   * 计算正方形在三维空间中的四个顶点坐标
   */
    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, pos, angle, step);
      let result = { center: pos, positions: [] };
      for (let i = 0; i < r.length; i++) {
            const cartographic = Cesium.Cartographic.fromCartesian(r);
            let position = ;
            result.positions.push(position);
      }
      return result;
    }

    UavInspection.prototype.getRectangleVertices = function (pos1, pos2, step) {
      let mid1 = Cesium.Cartesian3.fromDegrees(pos1, pos1, 0);
      let mid2 = Cesium.Cartesian3.fromDegrees(pos2, pos2, 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 ;
      });
    }

    UavInspection.prototype.divideSegment = function (pos1, pos2, step) {
      let p1 = Cesium.Cartesian3.fromDegrees(pos1, pos1, 0);
      let p2 = Cesium.Cartesian3.fromDegrees(pos2, pos2, 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 = ;
            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;
            let pos2 = positions;
            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 of this.segmentPositions.entries()) {
                let distance1 = Cesium.Cartesian3.distance(Cesium.Cartesian3.fromDegrees(pos.pos1, pos.pos1, 0), Cesium.Cartesian3.fromDegrees(pos.pos2, pos.pos2, 0));
                let distance2 = Cesium.Cartesian3.distance(Cesium.Cartesian3.fromDegrees(lng, lat, 0), Cesium.Cartesian3.fromDegrees(pos.pos1, pos.pos1, 0));
                let distance3 = Cesium.Cartesian3.distance(Cesium.Cartesian3.fromDegrees(lng, lat, 0), Cesium.Cartesian3.fromDegrees(pos.pos2, pos.pos2, 0));
                if (distance1 - (distance2 + distance3) > -0.1) {
                  index = i;
                }
            }

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

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

    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, pos1);
      const point2 = Cesium.Cartographic.fromDegrees(pos2, pos2);

      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);
    }
}效果截图

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


来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: Cesium 无人机巡检三维效果实现