2026-06-15 20:54:41 +08:00
|
|
|
|
import React, { useEffect, useRef, useState, useMemo, useImperativeHandle, forwardRef, useCallback } from 'react';
|
2026-05-12 08:34:13 +08:00
|
|
|
|
import carImg from '../assets/images/car.png';
|
|
|
|
|
|
import inspectImg from "../assets/images/inspectCar.png"
|
|
|
|
|
|
import planeImg from '../assets/images/plane.png';
|
|
|
|
|
|
import cameraImg from '../assets/images/camera.png';
|
|
|
|
|
|
import droneInspectImg from '../assets/images/xunjiancar.png';
|
2026-06-11 17:02:23 +08:00
|
|
|
|
import gecocar from '../assets/images/gecaocar.png';
|
2026-05-12 08:34:13 +08:00
|
|
|
|
import arrowImg from '../assets/images/arrow.png';
|
|
|
|
|
|
import iotImg from '../assets/images/iot.png';
|
2026-05-22 09:32:36 +08:00
|
|
|
|
import envConfig from '../../env';
|
2026-06-11 17:02:23 +08:00
|
|
|
|
import { TracePoint, TPAction, TPMode } from '../lib/TracePoint/tracePoint';
|
2026-05-22 09:32:36 +08:00
|
|
|
|
|
2026-05-12 08:34:13 +08:00
|
|
|
|
|
|
|
|
|
|
const CYAN_ALARM_COLOR = new window.Cesium.Color(
|
|
|
|
|
|
34 / 255,
|
|
|
|
|
|
211 / 255,
|
|
|
|
|
|
238 / 255,
|
|
|
|
|
|
1
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const CYAN_GLOW_COLOR = new window.Cesium.Color(
|
|
|
|
|
|
34 / 255,
|
|
|
|
|
|
211 / 255,
|
|
|
|
|
|
238 / 255,
|
|
|
|
|
|
1
|
|
|
|
|
|
);
|
|
|
|
|
|
window.Cesium.Ion.defaultAccessToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJmNjk0NjI1MC0wZGYyLTQ1YzItYjJmZi0wY2M5ODEwODllYzIiLCJpZCI6Mzk3MTQxLCJpYXQiOjE3NzI1MjkwMjV9.U6qmakhDgXJVXwLFyWzSxAUOFZRrDkIsRUPP6CCdnKg";
|
|
|
|
|
|
|
2026-06-15 20:54:41 +08:00
|
|
|
|
const CesiumMap = React.forwardRef(({ ifClear, ifClearPlanLine, ifClearNavLine, finishPoint, drawRealTime, realtimePosition, realtimeType, pathPoints = [], devices = [], onCameraClick, focusLocation, points = [], markedPoints = [], onPathChange, from, onClearDraw }, ref) => {
|
2026-05-12 08:34:13 +08:00
|
|
|
|
const containerRef = useRef(null);
|
|
|
|
|
|
const viewerRef = useRef(null);
|
|
|
|
|
|
const tooltipRef = useRef(null);
|
|
|
|
|
|
const hasFlownRef = useRef(false);
|
|
|
|
|
|
const eventHandlerRef = useRef(null);
|
|
|
|
|
|
const entityMapRef = useRef(new Map());
|
|
|
|
|
|
const alarmMapRef = useRef(new Map());
|
|
|
|
|
|
const iotMapRef = useRef(new Map());
|
2026-06-11 17:02:23 +08:00
|
|
|
|
const planLineRef = useRef(null); // points 规划航线
|
|
|
|
|
|
const navLineRef = useRef(null); // 完成点实时轨迹
|
2026-06-15 20:54:41 +08:00
|
|
|
|
const markedPointsRef = useRef([]); // 标记点实体
|
2026-05-12 08:34:13 +08:00
|
|
|
|
|
|
|
|
|
|
const realtimeEntityRef = useRef(null);
|
|
|
|
|
|
const realtimeLineRef = useRef(null);
|
|
|
|
|
|
const realtimeTrackRef = useRef([]);
|
|
|
|
|
|
|
2026-06-11 17:02:23 +08:00
|
|
|
|
const tracePointRef = useRef(new TracePoint());
|
|
|
|
|
|
const lastFinishPointRef = useRef(null);
|
|
|
|
|
|
const isNavigationModeRef = useRef(false);
|
|
|
|
|
|
const followCarEntityRef = useRef(null); // 跟随小车的实例
|
|
|
|
|
|
|
2026-05-12 08:34:13 +08:00
|
|
|
|
const isCameraFlyingRef = useRef(false);
|
|
|
|
|
|
const lastFlightPromiseRef = useRef(null);
|
|
|
|
|
|
|
|
|
|
|
|
const [drawPoints, setDrawPoints] = useState([]);
|
|
|
|
|
|
const drawLineRef = useRef(null);
|
|
|
|
|
|
const pointEntitiesRef = useRef([]);
|
|
|
|
|
|
|
|
|
|
|
|
const movingPathRef = useRef([]);
|
|
|
|
|
|
const movingIndexRef = useRef(0);
|
|
|
|
|
|
const isMovingRef = useRef(false);
|
|
|
|
|
|
const moveSpeedRef = useRef(0.02);
|
|
|
|
|
|
const movingEntityRef = useRef(null);
|
|
|
|
|
|
const realtimeCartesianRef = useRef([]); // 直接存 Cartesian3(性能最高)
|
|
|
|
|
|
const realtimeLineEntityRef = useRef(null);
|
|
|
|
|
|
const realtimeCarEntityRef = useRef(null);
|
|
|
|
|
|
const realtimeLastPointRef = useRef(null);
|
|
|
|
|
|
const realtimeHiddenRef = useRef(false);
|
|
|
|
|
|
|
|
|
|
|
|
const yellowCircleCanvas = useMemo(
|
|
|
|
|
|
() => createYellowCircleCanvas(30),
|
|
|
|
|
|
[]
|
|
|
|
|
|
);
|
|
|
|
|
|
const glowCircleCanvas = useMemo(
|
|
|
|
|
|
() => createGlowCircleCanvas(45),
|
|
|
|
|
|
[]
|
|
|
|
|
|
);
|
|
|
|
|
|
const glowBlinkColor = useMemo(
|
|
|
|
|
|
() =>
|
|
|
|
|
|
createGlowBlinkColor({
|
|
|
|
|
|
minAlpha: 0.25,
|
|
|
|
|
|
maxAlpha: 0.75,
|
|
|
|
|
|
speed: 1.1
|
|
|
|
|
|
}),
|
|
|
|
|
|
[]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const clearDrawingPath = () => {
|
|
|
|
|
|
const viewer = viewerRef.current;
|
|
|
|
|
|
if (!viewer) return;
|
|
|
|
|
|
|
|
|
|
|
|
pointEntitiesRef.current.forEach(p => {
|
|
|
|
|
|
viewer.entities.removeById(p);
|
|
|
|
|
|
});
|
|
|
|
|
|
pointEntitiesRef.current = [];
|
|
|
|
|
|
|
|
|
|
|
|
if (drawLineRef.current) {
|
|
|
|
|
|
viewer.entities.remove(drawLineRef.current);
|
|
|
|
|
|
drawLineRef.current = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setDrawPoints([]);
|
|
|
|
|
|
onPathChange && onPathChange([]);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const updateDrawPath = (newPoints) => {
|
|
|
|
|
|
const viewer = viewerRef.current;
|
|
|
|
|
|
if (!viewer) return;
|
|
|
|
|
|
|
|
|
|
|
|
pointEntitiesRef.current.forEach(id => {
|
|
|
|
|
|
viewer.entities.removeById(id);
|
|
|
|
|
|
});
|
|
|
|
|
|
pointEntitiesRef.current = [];
|
|
|
|
|
|
|
|
|
|
|
|
newPoints.forEach((p, i) => {
|
|
|
|
|
|
const id = `draw-point-${i}`;
|
|
|
|
|
|
viewer.entities.add({
|
|
|
|
|
|
id,
|
|
|
|
|
|
position: window.Cesium.Cartesian3.fromDegrees(p.lon, p.lat),
|
|
|
|
|
|
point: {
|
2026-05-22 08:03:30 +08:00
|
|
|
|
color: window.Cesium?.Color.SKYBLUE,
|
2026-05-12 08:34:13 +08:00
|
|
|
|
pixelSize: 10,
|
|
|
|
|
|
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
|
|
|
|
|
},
|
|
|
|
|
|
label: {
|
|
|
|
|
|
text: `${i + 1}`,
|
|
|
|
|
|
font: '12px sans-serif',
|
|
|
|
|
|
pixelOffset: new window.Cesium.Cartesian2(0, -15),
|
2026-05-22 08:03:30 +08:00
|
|
|
|
fillColor: window.Cesium?.Color.WHITE
|
2026-05-12 08:34:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
pointEntitiesRef.current.push(id);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (drawLineRef.current) {
|
|
|
|
|
|
viewer.entities.remove(drawLineRef.current);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (newPoints.length >= 2) {
|
|
|
|
|
|
const positions = newPoints.map(p => {
|
|
|
|
|
|
return window.Cesium.Cartesian3.fromDegrees(p.lon, p.lat);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
drawLineRef.current = viewer.entities.add({
|
|
|
|
|
|
polyline: {
|
|
|
|
|
|
positions: positions,
|
|
|
|
|
|
width: 4,
|
2026-05-22 08:03:30 +08:00
|
|
|
|
material: window.Cesium?.Color.SKYBLUE,
|
2026-05-12 08:34:13 +08:00
|
|
|
|
clampToGround: true,
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onPathChange && onPathChange(newPoints);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const calculateDirection = (point1, point2) => {
|
|
|
|
|
|
const lon1 = point1.lon;
|
|
|
|
|
|
const lat1 = point1.lat;
|
|
|
|
|
|
const lon2 = point2.lon;
|
|
|
|
|
|
const lat2 = point2.lat;
|
|
|
|
|
|
const dLon = lon2 - lon1;
|
|
|
|
|
|
const dLat = lat2 - lat1;
|
|
|
|
|
|
const angle = Math.atan2(dLat, dLon);
|
|
|
|
|
|
return angle;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const drawTrackLine = (points) => {
|
|
|
|
|
|
const viewer = viewerRef.current;
|
|
|
|
|
|
if (!viewer) return;
|
|
|
|
|
|
|
|
|
|
|
|
const Cesium = window.Cesium;
|
|
|
|
|
|
|
|
|
|
|
|
// ======================
|
|
|
|
|
|
// 是否实时模式
|
|
|
|
|
|
// ======================
|
|
|
|
|
|
const isRealtimeMode = !!(
|
|
|
|
|
|
realtimePosition?.lon &&
|
|
|
|
|
|
realtimePosition?.lat &&
|
|
|
|
|
|
!isNaN(Number(realtimePosition.lon)) &&
|
|
|
|
|
|
!isNaN(Number(realtimePosition.lat))
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-06-11 17:02:23 +08:00
|
|
|
|
if (planLineRef.current) {
|
|
|
|
|
|
viewer.entities.remove(planLineRef.current);
|
2026-05-12 08:34:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-01 09:12:08 +08:00
|
|
|
|
// 2. 传入的 points 本身为空 → 清空轨迹退出
|
|
|
|
|
|
if (!points || points.length === 0) {
|
|
|
|
|
|
const viewer = viewerRef.current;
|
|
|
|
|
|
if (viewer) {
|
|
|
|
|
|
try {
|
2026-06-11 17:02:23 +08:00
|
|
|
|
if (planLineRef.current) {
|
|
|
|
|
|
viewer.entities.remove(planLineRef.current);
|
|
|
|
|
|
planLineRef.current = null;
|
2026-06-01 09:12:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (e) { }
|
|
|
|
|
|
viewer.scene.requestRender(); // 强制刷新!
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-05-12 08:34:13 +08:00
|
|
|
|
const validPoints = points.filter(p => {
|
2026-06-04 11:49:20 +08:00
|
|
|
|
|
|
|
|
|
|
const lon = Number(p.lng);
|
2026-05-12 08:34:13 +08:00
|
|
|
|
const lat = Number(p.lat);
|
|
|
|
|
|
return (
|
|
|
|
|
|
!isNaN(lon) &&
|
|
|
|
|
|
!isNaN(lat) &&
|
|
|
|
|
|
Math.abs(lon) < 180 &&
|
|
|
|
|
|
Math.abs(lat) < 90 &&
|
|
|
|
|
|
lon !== 0 &&
|
|
|
|
|
|
lat !== 0
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (validPoints.length === 0) return;
|
|
|
|
|
|
|
|
|
|
|
|
const positions = validPoints.map(point =>
|
|
|
|
|
|
Cesium.Cartesian3.fromDegrees(
|
2026-06-04 11:49:20 +08:00
|
|
|
|
Number(point.lng),
|
2026-05-12 08:34:13 +08:00
|
|
|
|
Number(point.lat)
|
|
|
|
|
|
)
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const trackLine = viewer.entities.add({
|
|
|
|
|
|
polyline: {
|
|
|
|
|
|
positions: positions,
|
2026-06-01 10:53:52 +08:00
|
|
|
|
width: 2,
|
2026-06-11 17:02:23 +08:00
|
|
|
|
// 规划航线用橙色,和实时轨迹区分
|
|
|
|
|
|
material: Cesium.Color.ORANGE.withAlpha(0.8),
|
2026-05-12 08:34:13 +08:00
|
|
|
|
clampToGround: true,
|
|
|
|
|
|
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-11 17:02:23 +08:00
|
|
|
|
planLineRef.current = trackLine;
|
2026-05-12 08:34:13 +08:00
|
|
|
|
|
|
|
|
|
|
// ======================
|
|
|
|
|
|
// 🚀 非实时模式 -> 自动飞到轨迹
|
|
|
|
|
|
// ======================
|
|
|
|
|
|
if (!isRealtimeMode) {
|
|
|
|
|
|
|
|
|
|
|
|
let minLon = 180, maxLon = -180;
|
|
|
|
|
|
let minLat = 90, maxLat = -90;
|
|
|
|
|
|
|
|
|
|
|
|
validPoints.forEach(p => {
|
2026-06-04 11:49:20 +08:00
|
|
|
|
minLon = Math.min(minLon, p.lng);
|
|
|
|
|
|
maxLon = Math.max(maxLon, p.lng);
|
2026-05-12 08:34:13 +08:00
|
|
|
|
minLat = Math.min(minLat, p.lat);
|
|
|
|
|
|
maxLat = Math.max(maxLat, p.lat);
|
|
|
|
|
|
});
|
|
|
|
|
|
if (getCameraMode() !== "track") return;
|
|
|
|
|
|
|
|
|
|
|
|
// 单个点
|
|
|
|
|
|
if (validPoints.length === 1) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const p = validPoints[0];
|
|
|
|
|
|
viewer.scene.requestRender();
|
|
|
|
|
|
viewer.camera.flyTo({
|
|
|
|
|
|
destination: Cesium.Cartesian3.fromDegrees(
|
2026-06-04 11:49:20 +08:00
|
|
|
|
p.lng,
|
2026-05-12 08:34:13 +08:00
|
|
|
|
p.lat,
|
|
|
|
|
|
800
|
|
|
|
|
|
),
|
|
|
|
|
|
duration: 1.2
|
|
|
|
|
|
});
|
|
|
|
|
|
viewer.scene.requestRender();
|
|
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const rectangle = Cesium.Rectangle.fromDegrees(
|
|
|
|
|
|
minLon,
|
|
|
|
|
|
minLat,
|
|
|
|
|
|
maxLon,
|
|
|
|
|
|
maxLat
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
viewer.scene.requestRender();
|
|
|
|
|
|
|
|
|
|
|
|
viewer.camera.flyTo({
|
|
|
|
|
|
destination: rectangle,
|
|
|
|
|
|
duration: 1.2
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
viewer.scene.requestRender();
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (viewerRef.current) {
|
|
|
|
|
|
drawTrackLine(points);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [points]);
|
|
|
|
|
|
|
2026-06-15 20:54:41 +08:00
|
|
|
|
// 渲染绿色标记点
|
|
|
|
|
|
const drawMarkedPoints = (points) => {
|
|
|
|
|
|
const viewer = viewerRef.current;
|
|
|
|
|
|
if (!viewer) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 先清空之前的标记点
|
|
|
|
|
|
markedPointsRef.current.forEach(id => {
|
|
|
|
|
|
viewer.entities.removeById(id);
|
|
|
|
|
|
});
|
|
|
|
|
|
markedPointsRef.current = [];
|
|
|
|
|
|
|
|
|
|
|
|
if (!points || points.length === 0) return;
|
|
|
|
|
|
|
|
|
|
|
|
points.forEach((p, i) => {
|
|
|
|
|
|
const id = `marked-point-${i}`;
|
|
|
|
|
|
viewer.entities.add({
|
|
|
|
|
|
id,
|
|
|
|
|
|
position: window.Cesium.Cartesian3.fromDegrees(p.lon, p.lat),
|
|
|
|
|
|
point: {
|
|
|
|
|
|
color: window.Cesium.Color.GREEN,
|
|
|
|
|
|
pixelSize: 15,
|
|
|
|
|
|
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
|
|
|
|
|
},
|
|
|
|
|
|
label: {
|
|
|
|
|
|
text: `${i + 1}`,
|
|
|
|
|
|
font: '14px sans-serif',
|
|
|
|
|
|
pixelOffset: new window.Cesium.Cartesian2(0, -20),
|
|
|
|
|
|
fillColor: window.Cesium.Color.WHITE,
|
|
|
|
|
|
outlineColor: window.Cesium.Color.BLACK,
|
|
|
|
|
|
outlineWidth: 2,
|
|
|
|
|
|
style: window.Cesium.LabelStyle.FILL_AND_OUTLINE,
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
markedPointsRef.current.push(id);
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (viewerRef.current) {
|
|
|
|
|
|
drawMarkedPoints(markedPoints);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [markedPoints]);
|
|
|
|
|
|
|
2026-05-12 08:34:13 +08:00
|
|
|
|
function showRobotTooltip(tooltip, robot, position) {
|
|
|
|
|
|
const statusMap = {
|
|
|
|
|
|
1: { text: "工作中", color: "#10b981" },
|
|
|
|
|
|
2: { text: "空闲", color: "#f59e0b" },
|
|
|
|
|
|
3: { text: "离线", color: "#f87171" }
|
|
|
|
|
|
};
|
|
|
|
|
|
let status = statusMap[robot.feStatus] || statusMap[1];
|
|
|
|
|
|
|
|
|
|
|
|
tooltip.style.display = "block";
|
|
|
|
|
|
tooltip.style.left = position.x + 12 + "px";
|
|
|
|
|
|
tooltip.style.top = position.y + 12 + "px";
|
|
|
|
|
|
|
|
|
|
|
|
let deviceValuesHTML = '';
|
|
|
|
|
|
if (robot.devicevalue && robot.devicevalue.length > 0) {
|
|
|
|
|
|
deviceValuesHTML = robot.devicevalue.map((device) => `
|
|
|
|
|
|
<div style="margin-bottom:4px;"><strong>${device.name}:</strong> ${device.value}${device.unit}</div>
|
|
|
|
|
|
`).join('');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
tooltip.innerHTML = `
|
|
|
|
|
|
<div style="font-weight:600;margin-bottom:6px;">🤖 ${robot.deviceAlias || robot.serialNumber || robot.name}</div>
|
|
|
|
|
|
${deviceValuesHTML ? `<div style="margin-top:8px;"><strong>设备数据:</strong>${deviceValuesHTML}</div>` : ''}
|
|
|
|
|
|
<div>经度:${Number(robot.lastRunningStatus?.longitude).toFixed(6)}</div>
|
|
|
|
|
|
<div>纬度:${Number(robot.lastRunningStatus?.latitude).toFixed(6)}</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function fetchPublishedLayers() {
|
2026-05-22 09:32:36 +08:00
|
|
|
|
const baseUrl = envConfig.geoserver_url;
|
2026-05-20 15:48:44 +08:00
|
|
|
|
//const baseUrl = 'https://gis.caasrobot.com/geoserver';
|
2026-05-12 08:34:13 +08:00
|
|
|
|
const getCapabilitiesUrl = `${baseUrl}/wms?service=WMS&version=1.1.1&request=GetCapabilities`;
|
|
|
|
|
|
|
|
|
|
|
|
fetch(getCapabilitiesUrl)
|
|
|
|
|
|
.then(response => response.text())
|
|
|
|
|
|
.then(xmlText => {
|
|
|
|
|
|
const parser = new DOMParser();
|
|
|
|
|
|
const xmlDoc = parser.parseFromString(xmlText, 'text/xml');
|
|
|
|
|
|
const layerElements = xmlDoc.getElementsByTagName('Layer');
|
|
|
|
|
|
const uniqueLayerNames = new Set();
|
|
|
|
|
|
const publishedLayers = [];
|
|
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < layerElements.length; i++) {
|
|
|
|
|
|
const layer = layerElements[i];
|
|
|
|
|
|
const nameElement = layer.getElementsByTagName('Name')[0];
|
|
|
|
|
|
const titleElement = layer.getElementsByTagName('Title')[0];
|
|
|
|
|
|
if (nameElement && nameElement.textContent) {
|
|
|
|
|
|
const layerName = nameElement.textContent;
|
|
|
|
|
|
if (layerName.includes(':') && !uniqueLayerNames.has(layerName)) {
|
|
|
|
|
|
uniqueLayerNames.add(layerName);
|
|
|
|
|
|
publishedLayers.push({
|
|
|
|
|
|
name: layerName,
|
|
|
|
|
|
title: titleElement ? titleElement.textContent : layerName
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
displayPublishedLayers(publishedLayers, baseUrl);
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(error => console.error('获取图层失败:', error));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function displayPublishedLayers(publishedLayers, baseUrl) {
|
2026-06-17 21:05:35 +08:00
|
|
|
|
//console.log('已发布的图层:', publishedLayers)
|
2026-05-12 08:34:13 +08:00
|
|
|
|
publishedLayers.forEach((layer, index) => {
|
|
|
|
|
|
const [workspace, layerName] = layer.name.split(':');
|
|
|
|
|
|
if (workspace && layerName) {
|
|
|
|
|
|
addLayerFromList(layer.name, baseUrl, index);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function addLayerFromList(layerFullName, baseUrl, index) {
|
|
|
|
|
|
const [workspace, layerName] = layerFullName.split(':');
|
|
|
|
|
|
if (!workspace || !layerName) return;
|
|
|
|
|
|
const wmsUrl = `${baseUrl}/wms`;
|
|
|
|
|
|
addWmsLayerToCesium(wmsUrl, workspace, layerName, index);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getLayerBoundsAndFit(wmsUrl, layerName) {
|
|
|
|
|
|
console.log('=== 获取图层范围开始 ===');
|
|
|
|
|
|
|
|
|
|
|
|
const getCapabilitiesUrl = `${wmsUrl}?service=WMS&version=1.1.1&request=GetCapabilities`;
|
|
|
|
|
|
|
|
|
|
|
|
fetch(getCapabilitiesUrl)
|
|
|
|
|
|
.then(response => response.text())
|
|
|
|
|
|
.then(xmlText => {
|
|
|
|
|
|
const parser = new DOMParser();
|
|
|
|
|
|
const xmlDoc = parser.parseFromString(xmlText, 'text/xml');
|
|
|
|
|
|
const layerElements = xmlDoc.getElementsByTagName('Layer');
|
|
|
|
|
|
let layerBbox = null;
|
|
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < layerElements.length; i++) {
|
|
|
|
|
|
const layer = layerElements[i];
|
|
|
|
|
|
const nameElement = layer.getElementsByTagName('Name')[0];
|
|
|
|
|
|
|
|
|
|
|
|
if (nameElement && nameElement.textContent === layerName) {
|
|
|
|
|
|
const bboxElement = layer.getElementsByTagName('BoundingBox')[0];
|
|
|
|
|
|
if (bboxElement) {
|
|
|
|
|
|
const minx = parseFloat(bboxElement.getAttribute('minx'));
|
|
|
|
|
|
const miny = parseFloat(bboxElement.getAttribute('miny'));
|
|
|
|
|
|
const maxx = parseFloat(bboxElement.getAttribute('maxx'));
|
|
|
|
|
|
const maxy = parseFloat(bboxElement.getAttribute('maxy'));
|
|
|
|
|
|
|
|
|
|
|
|
layerBbox = [minx, miny, maxx, maxy];
|
|
|
|
|
|
console.log('找到图层范围:', layerBbox);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (layerBbox) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const bounds = new AMap.Bounds(
|
|
|
|
|
|
new AMap.LngLat(layerBbox[0], layerBbox[1]),
|
|
|
|
|
|
new AMap.LngLat(layerBbox[2], layerBbox[3])
|
|
|
|
|
|
);
|
|
|
|
|
|
map.setFitView(bounds, 15, [50, 50, 50, 50]);
|
|
|
|
|
|
console.log('地图已定位到图层范围');
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('地图定位失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.warn('未找到图层范围,使用默认位置');
|
|
|
|
|
|
map.setCenter([120.565, 31.453]);
|
|
|
|
|
|
map.setZoom(10);
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(error => {
|
|
|
|
|
|
console.error('获取图层范围失败:', error);
|
|
|
|
|
|
map.setCenter([120.565, 31.453]);
|
|
|
|
|
|
map.setZoom(10);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
const getCameraMode = () => {
|
|
|
|
|
|
const hasRealtime =
|
|
|
|
|
|
realtimePosition?.lon &&
|
|
|
|
|
|
realtimePosition?.lat &&
|
|
|
|
|
|
!isNaN(Number(realtimePosition.lon)) &&
|
|
|
|
|
|
!isNaN(Number(realtimePosition.lat));
|
|
|
|
|
|
|
|
|
|
|
|
const hasTrack = points && points.length > 0;
|
|
|
|
|
|
|
|
|
|
|
|
if (hasRealtime) return "realtime";
|
|
|
|
|
|
if (hasTrack) return "track";
|
|
|
|
|
|
return "devices";
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
function addWmsLayerToCesium(wmsUrl, workspace, layerName, index) {
|
|
|
|
|
|
const layerIdentifier = `${workspace}:${layerName}`;
|
|
|
|
|
|
let baseUrl = wmsUrl;
|
|
|
|
|
|
const cesiumLayers = [];
|
2026-06-24 15:47:52 +08:00
|
|
|
|
if (index == 2 && from == "other") {
|
2026-06-17 21:05:35 +08:00
|
|
|
|
flyToLayer(layerIdentifier, baseUrl, 100);
|
|
|
|
|
|
}
|
2026-05-12 08:34:13 +08:00
|
|
|
|
//getLayerBoundsAndFit(wmsUrl, layerName);
|
|
|
|
|
|
|
|
|
|
|
|
if (baseUrl.includes('/gwc/service/wms')) {
|
|
|
|
|
|
baseUrl = baseUrl.replace('/gwc/service/wms', '');
|
|
|
|
|
|
} else if (baseUrl.includes('/gwc/service/wmts')) {
|
|
|
|
|
|
baseUrl = baseUrl.replace('/gwc/service/wmts', '');
|
|
|
|
|
|
} else if (baseUrl.includes('/wms?')) {
|
|
|
|
|
|
baseUrl = baseUrl.split('/wms?')[0];
|
|
|
|
|
|
} else if (baseUrl.includes('/wms')) {
|
|
|
|
|
|
baseUrl = baseUrl.split('/wms')[0];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const imageryProvider = new window.Cesium.WebMapServiceImageryProvider({
|
|
|
|
|
|
url: `${baseUrl}/gwc/service/wms`,
|
|
|
|
|
|
layers: layerIdentifier,
|
|
|
|
|
|
parameters: {
|
|
|
|
|
|
service: 'WMS',
|
|
|
|
|
|
version: '1.1.1',
|
|
|
|
|
|
request: 'GetMap',
|
|
|
|
|
|
transparent: true,
|
|
|
|
|
|
format: 'image/png',
|
|
|
|
|
|
srs: 'EPSG:4326',
|
|
|
|
|
|
width: 256,
|
|
|
|
|
|
height: 256,
|
|
|
|
|
|
styles: ''
|
|
|
|
|
|
},
|
|
|
|
|
|
tilingScheme: new window.Cesium.GeographicTilingScheme(),
|
|
|
|
|
|
tileWidth: 256,
|
|
|
|
|
|
tileHeight: 256,
|
|
|
|
|
|
maximumLevel: 21,
|
|
|
|
|
|
minimumLevel: 1,
|
|
|
|
|
|
credit: new window.Cesium.Credit('GeoServer GWC WMS'),
|
|
|
|
|
|
usePreCachedTilesIfAvailable: true,
|
|
|
|
|
|
enablePickFeatures: false,
|
|
|
|
|
|
maximumRequests: 15,
|
|
|
|
|
|
retryAttempts: 2
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const layer = viewerRef.current.imageryLayers.addImageryProvider(imageryProvider);
|
|
|
|
|
|
cesiumLayers.push(layer);
|
|
|
|
|
|
} catch (gwcWmsError) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const imageryProvider = new window.Cesium.WebMapServiceImageryProvider({
|
|
|
|
|
|
url: `${baseUrl}/${workspace}/wms`,
|
|
|
|
|
|
layers: layerIdentifier,
|
|
|
|
|
|
parameters: {
|
|
|
|
|
|
service: 'WMS',
|
|
|
|
|
|
version: '1.1.1',
|
|
|
|
|
|
request: 'GetMap',
|
|
|
|
|
|
transparent: true,
|
|
|
|
|
|
format: 'image/png',
|
|
|
|
|
|
srs: 'EPSG:3857',
|
|
|
|
|
|
width: 256,
|
|
|
|
|
|
height: 256,
|
|
|
|
|
|
styles: ''
|
|
|
|
|
|
},
|
|
|
|
|
|
tilingScheme: new window.Cesium.WebMercatorTilingScheme(),
|
|
|
|
|
|
tileWidth: 256,
|
|
|
|
|
|
tileHeight: 256,
|
|
|
|
|
|
maximumLevel: 18,
|
|
|
|
|
|
minimumLevel: 5,
|
|
|
|
|
|
credit: new window.Cesium.Credit('GeoServer WMS'),
|
|
|
|
|
|
usePreCachedTilesIfAvailable: true,
|
|
|
|
|
|
enablePickFeatures: false,
|
|
|
|
|
|
maximumRequests: 10,
|
|
|
|
|
|
retryAttempts: 2
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const layer = viewerRef.current.imageryLayers.addImageryProvider(imageryProvider);
|
|
|
|
|
|
cesiumLayers.push(layer);
|
|
|
|
|
|
} catch (wmsError) {
|
|
|
|
|
|
const imageryProvider = new window.Cesium.WebMapServiceImageryProvider({
|
|
|
|
|
|
url: `${baseUrl}/gwc/service/wms`,
|
|
|
|
|
|
layers: layerIdentifier,
|
|
|
|
|
|
parameters: {
|
|
|
|
|
|
service: 'WMS',
|
|
|
|
|
|
version: '1.1.1',
|
|
|
|
|
|
request: 'GetMap',
|
|
|
|
|
|
transparent: true,
|
|
|
|
|
|
format: 'image/png',
|
|
|
|
|
|
srs: 'EPSG:4326',
|
|
|
|
|
|
width: 256,
|
|
|
|
|
|
height: 256,
|
|
|
|
|
|
styles: ''
|
|
|
|
|
|
},
|
|
|
|
|
|
tilingScheme: new window.Cesium.GeographicTilingScheme(),
|
|
|
|
|
|
tileWidth: 256,
|
|
|
|
|
|
tileHeight: 256,
|
|
|
|
|
|
maximumLevel: 21,
|
|
|
|
|
|
minimumLevel: 1,
|
|
|
|
|
|
credit: new window.Cesium.Credit('GeoServer GWC WMS'),
|
|
|
|
|
|
usePreCachedTilesIfAvailable: true,
|
|
|
|
|
|
enablePickFeatures: false,
|
|
|
|
|
|
maximumRequests: 15,
|
|
|
|
|
|
retryAttempts: 2
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const layer = viewerRef.current.imageryLayers.addImageryProvider(imageryProvider);
|
|
|
|
|
|
cesiumLayers.push(layer);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-17 21:05:35 +08:00
|
|
|
|
function flyToLayer(layerIdentifier, baseUrl, flyHeight = 1000) {
|
|
|
|
|
|
//console.log('开始获取图层范围', layerIdentifier);
|
2026-05-12 08:34:13 +08:00
|
|
|
|
const url = `${baseUrl}/wms?service=WMS&version=1.1.1&request=GetCapabilities`;
|
|
|
|
|
|
|
|
|
|
|
|
fetch(url)
|
|
|
|
|
|
.then(res => res.text())
|
|
|
|
|
|
.then(xmlText => {
|
|
|
|
|
|
const xml = new DOMParser().parseFromString(xmlText, 'text/xml');
|
|
|
|
|
|
const layerElements = xml.getElementsByTagName('Layer');
|
2026-06-17 21:05:35 +08:00
|
|
|
|
let targetBbox = null;
|
2026-05-12 08:34:13 +08:00
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < layerElements.length; i++) {
|
|
|
|
|
|
const layer = layerElements[i];
|
|
|
|
|
|
const nameElement = layer.getElementsByTagName('Name')[0];
|
|
|
|
|
|
|
|
|
|
|
|
if (nameElement && nameElement.textContent === layerIdentifier) {
|
|
|
|
|
|
const bboxElement = layer.getElementsByTagName('BoundingBox')[0];
|
|
|
|
|
|
if (bboxElement) {
|
|
|
|
|
|
const minx = parseFloat(bboxElement.getAttribute('minx'));
|
|
|
|
|
|
const miny = parseFloat(bboxElement.getAttribute('miny'));
|
|
|
|
|
|
const maxx = parseFloat(bboxElement.getAttribute('maxx'));
|
|
|
|
|
|
const maxy = parseFloat(bboxElement.getAttribute('maxy'));
|
2026-06-17 21:05:35 +08:00
|
|
|
|
targetBbox = [minx, miny, maxx, maxy];
|
|
|
|
|
|
console.log('读取到图层bbox', targetBbox);
|
2026-05-12 08:34:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-06-17 21:05:35 +08:00
|
|
|
|
|
|
|
|
|
|
if (!targetBbox) {
|
|
|
|
|
|
console.warn('该图层未找到BoundingBox,无法飞行', layerIdentifier);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const [minx, miny, maxx, maxy] = targetBbox;
|
|
|
|
|
|
// 取图层中心点
|
|
|
|
|
|
const centerLon = (minx + maxx) / 2;
|
|
|
|
|
|
const centerLat = (miny + maxy) / 2;
|
|
|
|
|
|
|
|
|
|
|
|
viewerRef.current.camera.flyTo({
|
|
|
|
|
|
// 固定高度,第三个参数就是高度(单位米)
|
|
|
|
|
|
destination: window.Cesium.Cartesian3.fromDegrees(centerLon, centerLat, flyHeight),
|
|
|
|
|
|
orientation: {
|
|
|
|
|
|
heading: 0,
|
|
|
|
|
|
pitch: window.Cesium.Math.toRadians(-90), // 垂直俯视
|
|
|
|
|
|
roll: 0
|
|
|
|
|
|
},
|
|
|
|
|
|
duration: 2
|
|
|
|
|
|
});
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(err => {
|
|
|
|
|
|
console.error('获取WMS Capabilities失败', err);
|
2026-05-12 08:34:13 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getRobotEntityId(robot, index) {
|
|
|
|
|
|
if (robot.id != null) return `robot-${robot.id}`;
|
|
|
|
|
|
if (robot.name) return `robot-${robot.name}`;
|
|
|
|
|
|
return `robot-index-${index}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function createGlowCircleCanvas(size = 40, innerAlpha = 0.8) {
|
|
|
|
|
|
const canvas = document.createElement("canvas");
|
|
|
|
|
|
canvas.width = Math.max(1, Number(size) || 40);
|
|
|
|
|
|
canvas.height = Math.max(1, Number(size) || 40);
|
|
|
|
|
|
const ctx = canvas.getContext("2d");
|
|
|
|
|
|
const r = Math.max(1, Number(size) || 40) / 2;
|
|
|
|
|
|
const gradient = ctx.createRadialGradient(r, r, r * 0.2, r, r, r);
|
|
|
|
|
|
gradient.addColorStop(0, `rgba(255,255,255,${innerAlpha})`);
|
|
|
|
|
|
gradient.addColorStop(0.4, `rgba(255,255,255,0.6)`);
|
|
|
|
|
|
gradient.addColorStop(0.7, `rgba(255,255,255,0.3)`);
|
|
|
|
|
|
gradient.addColorStop(1, `rgba(255,255,25,0)`);
|
|
|
|
|
|
ctx.fillStyle = gradient;
|
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
|
ctx.arc(r, r, r, 0, Math.PI * 2);
|
|
|
|
|
|
ctx.fill();
|
|
|
|
|
|
return canvas;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function createGlowBlinkColor({
|
|
|
|
|
|
minAlpha = 0.2,
|
|
|
|
|
|
maxAlpha = 0.9,
|
|
|
|
|
|
speed = 1.2
|
|
|
|
|
|
} = {}) {
|
|
|
|
|
|
const start = Date.now();
|
|
|
|
|
|
return new window.Cesium.CallbackProperty(() => {
|
|
|
|
|
|
const t = (Date.now() - start) / 1000;
|
|
|
|
|
|
const alpha = minAlpha + (maxAlpha - minAlpha) * (0.5 + 0.5 * Math.sin(t * speed * Math.PI * 2));
|
|
|
|
|
|
return CYAN_GLOW_COLOR.withAlpha(alpha);
|
|
|
|
|
|
}, false);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function createRobotEntity(viewer, robot, index) {
|
|
|
|
|
|
const raw = robot?.lastRunningStatus || {};
|
|
|
|
|
|
const parsed = parseLngLat(raw.longitude, raw.latitude);
|
|
|
|
|
|
if (!parsed) return null;
|
|
|
|
|
|
const { longitude, latitude } = parsed;
|
|
|
|
|
|
const baseId = getRobotEntityId(robot, index);
|
|
|
|
|
|
viewer.entities.removeById(baseId);
|
|
|
|
|
|
viewer.entities.removeById(baseId + "_alarm");
|
|
|
|
|
|
const position = window.Cesium.Cartesian3.fromDegrees(longitude, latitude, 0);
|
|
|
|
|
|
|
|
|
|
|
|
viewer.entities.add({
|
|
|
|
|
|
id: baseId + "_alarm",
|
|
|
|
|
|
position,
|
|
|
|
|
|
billboard: {
|
|
|
|
|
|
image: createGlowCircleCanvas(45),
|
|
|
|
|
|
width: 45,
|
|
|
|
|
|
height: 45,
|
|
|
|
|
|
verticalOrigin: window.Cesium.VerticalOrigin.BOTTOM,
|
|
|
|
|
|
pixelOffset: new window.Cesium.Cartesian2(0, -6),
|
|
|
|
|
|
color: createGlowBlinkColor({
|
|
|
|
|
|
minAlpha: 0.25,
|
|
|
|
|
|
maxAlpha: 0.75,
|
|
|
|
|
|
speed: 1.1
|
|
|
|
|
|
}),
|
|
|
|
|
|
disableDepthTestDistance: Number.POSITIVE_INFINITY
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const entity = viewer.entities.add({
|
|
|
|
|
|
id: baseId,
|
|
|
|
|
|
position,
|
|
|
|
|
|
billboard: {
|
|
|
|
|
|
image: carImg,
|
|
|
|
|
|
width: 60,
|
|
|
|
|
|
height: 60,
|
|
|
|
|
|
verticalOrigin: window.Cesium.VerticalOrigin.BOTTOM,
|
|
|
|
|
|
disableDepthTestDistance: Number.POSITIVE_INFINITY
|
|
|
|
|
|
},
|
|
|
|
|
|
label: {
|
|
|
|
|
|
text: robot.name,
|
|
|
|
|
|
font: "14px sans-serif",
|
|
|
|
|
|
pixelOffset: new window.Cesium.Cartesian2(0, -40),
|
2026-05-22 08:03:30 +08:00
|
|
|
|
fillColor: window.Cesium?.Color.WHITE,
|
2026-05-12 08:34:13 +08:00
|
|
|
|
showBackground: true,
|
2026-05-22 08:03:30 +08:00
|
|
|
|
backgroundColor: window.Cesium?.Color.BLACK.withAlpha(0.6)
|
2026-05-12 08:34:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
entity.robotData = robot;
|
|
|
|
|
|
return entity;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!containerRef.current) return;
|
|
|
|
|
|
if (viewerRef.current) return;
|
|
|
|
|
|
|
|
|
|
|
|
const viewer = new window.Cesium.Viewer(containerRef.current, {
|
|
|
|
|
|
animation: false,
|
|
|
|
|
|
timeline: false,
|
|
|
|
|
|
baseLayerPicker: true,
|
|
|
|
|
|
geocoder: false,
|
|
|
|
|
|
homeButton: false,
|
|
|
|
|
|
sceneModePicker: false,
|
|
|
|
|
|
navigationHelpButton: false,
|
|
|
|
|
|
fullscreenButton: false,
|
|
|
|
|
|
infoBox: false,
|
|
|
|
|
|
imageryProvider: new window.Cesium.UrlTemplateImageryProvider({
|
|
|
|
|
|
url: "https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
|
|
|
|
|
|
fileExtension: "jpg"
|
|
|
|
|
|
})
|
|
|
|
|
|
});
|
|
|
|
|
|
viewerRef.current = viewer;
|
2026-05-22 08:03:30 +08:00
|
|
|
|
viewer.scene.globe.baseColor = window.Cesium?.Color.BLACK;
|
2026-05-12 08:34:13 +08:00
|
|
|
|
viewer.scene.globe.depthTestAgainstTerrain = false;
|
|
|
|
|
|
viewer.scene.requestRenderMode = true; // 👈 加这个(只在需要时渲染)
|
|
|
|
|
|
viewer.scene.globe.enableLighting = false;
|
|
|
|
|
|
|
2026-05-22 14:36:31 +08:00
|
|
|
|
//viewer.camera.flyTo({
|
|
|
|
|
|
// // 中国中心大致坐标 + 合适高度(200万米)
|
|
|
|
|
|
// destination: window.Cesium.Cartesian3.fromDegrees(103.84, 31.15, 2000000),
|
|
|
|
|
|
// orientation: {
|
|
|
|
|
|
// heading: window.Cesium.Math.toRadians(0),
|
|
|
|
|
|
// pitch: window.Cesium.Math.toRadians(-90),
|
|
|
|
|
|
// roll: 0
|
|
|
|
|
|
// },
|
|
|
|
|
|
// duration: 0 // 初始化直接到位,无需动画
|
|
|
|
|
|
//});
|
2026-05-22 14:14:30 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-05-12 08:34:13 +08:00
|
|
|
|
try { fetchPublishedLayers() } catch (e) { }
|
|
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
if (lastFlightPromiseRef.current) viewer.camera.cancelFlight();
|
|
|
|
|
|
};
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
2026-05-28 16:28:10 +08:00
|
|
|
|
const focusEntityRef = useRef(null);
|
|
|
|
|
|
|
2026-05-12 08:34:13 +08:00
|
|
|
|
useEffect(() => {
|
2026-06-05 15:57:36 +08:00
|
|
|
|
// 如果有 realtimePosition,不显示 focusEntity
|
|
|
|
|
|
const hasRealtime =
|
|
|
|
|
|
realtimePosition?.lon &&
|
|
|
|
|
|
realtimePosition?.lat &&
|
|
|
|
|
|
!isNaN(Number(realtimePosition.lon)) &&
|
|
|
|
|
|
!isNaN(Number(realtimePosition.lat));
|
|
|
|
|
|
|
|
|
|
|
|
if (!viewerRef.current || !focusLocation || hasRealtime) {
|
2026-05-28 16:28:10 +08:00
|
|
|
|
if (focusEntityRef.current) {
|
|
|
|
|
|
viewerRef.current?.entities.remove(focusEntityRef.current);
|
|
|
|
|
|
focusEntityRef.current = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-05-28 15:17:00 +08:00
|
|
|
|
const viewer = viewerRef.current;
|
2026-05-28 16:28:10 +08:00
|
|
|
|
const Cesium = window.Cesium;
|
|
|
|
|
|
|
|
|
|
|
|
// 选择图片
|
|
|
|
|
|
let image = carImg;
|
|
|
|
|
|
if (focusLocation.type === "plane" || focusLocation.type === "drone") {
|
|
|
|
|
|
image = planeImg;
|
|
|
|
|
|
} else if (focusLocation.type === "robot") {
|
|
|
|
|
|
image = droneInspectImg;
|
|
|
|
|
|
} else if (focusLocation.type === "mower") {
|
|
|
|
|
|
image = carImg;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果已经有 focusEntity,先移除
|
|
|
|
|
|
if (focusEntityRef.current) {
|
|
|
|
|
|
viewer.entities.remove(focusEntityRef.current);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 添加新的 focusEntity
|
|
|
|
|
|
focusEntityRef.current = viewer.entities.add({
|
|
|
|
|
|
position: Cesium.Cartesian3.fromDegrees(Number(focusLocation.lon), Number(focusLocation.lat), 0),
|
|
|
|
|
|
billboard: {
|
|
|
|
|
|
image: image,
|
|
|
|
|
|
width: 60,
|
|
|
|
|
|
height: 60,
|
|
|
|
|
|
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
|
|
|
|
|
|
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
|
|
|
|
|
},
|
|
|
|
|
|
label: {
|
|
|
|
|
|
text: focusLocation.name,
|
|
|
|
|
|
font: "12px sans-serif",
|
|
|
|
|
|
pixelOffset: new Cesium.Cartesian2(0, -80),
|
|
|
|
|
|
fillColor: Cesium.Color.YELLOW,
|
|
|
|
|
|
showBackground: true,
|
|
|
|
|
|
backgroundColor: Cesium.Color.BLACK.withAlpha(0.6)
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2026-05-28 15:17:00 +08:00
|
|
|
|
|
|
|
|
|
|
viewer.camera.flyTo({
|
2026-05-28 16:28:10 +08:00
|
|
|
|
destination: Cesium.Cartesian3.fromDegrees(Number(focusLocation.lon), Number(focusLocation.lat), focusLocation.height || 800),
|
2026-05-28 15:17:00 +08:00
|
|
|
|
orientation: {
|
2026-05-28 16:28:10 +08:00
|
|
|
|
heading: Cesium.Math.toRadians(0.0),
|
|
|
|
|
|
pitch: Cesium.Math.toRadians(-90.0),
|
2026-05-28 15:17:00 +08:00
|
|
|
|
roll: 0.0,
|
|
|
|
|
|
},
|
|
|
|
|
|
duration: 1.5
|
|
|
|
|
|
});
|
2026-05-12 08:34:13 +08:00
|
|
|
|
}, [focusLocation]);
|
|
|
|
|
|
|
|
|
|
|
|
function flyToRobots(viewer, robots) {
|
|
|
|
|
|
if (!robots || robots.length === 0) return;
|
|
|
|
|
|
|
|
|
|
|
|
let west = 180, south = 90, east = -180, north = -90;
|
|
|
|
|
|
|
|
|
|
|
|
robots.forEach(r => {
|
|
|
|
|
|
const lon = Number(r?.lastRunningStatus?.longitude);
|
|
|
|
|
|
const lat = Number(r?.lastRunningStatus?.latitude);
|
|
|
|
|
|
|
|
|
|
|
|
west = Math.min(west, lon);
|
|
|
|
|
|
south = Math.min(south, lat);
|
|
|
|
|
|
east = Math.max(east, lon);
|
|
|
|
|
|
north = Math.max(north, lat);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const rectangle = window.Cesium.Rectangle.fromDegrees(west, south, east, north);
|
|
|
|
|
|
viewer.camera.flyTo({ destination: rectangle, duration: 1.5 });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function parseLngLat(lon, lat) {
|
|
|
|
|
|
const longitude = Number(lon);
|
|
|
|
|
|
const latitude = Number(lat);
|
|
|
|
|
|
if (Number.isFinite(longitude) && Number.isFinite(latitude)) {
|
|
|
|
|
|
return { longitude, latitude };
|
|
|
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function createYellowCircleDataUrl(size = 30) {
|
|
|
|
|
|
const canvas = document.createElement('canvas');
|
|
|
|
|
|
canvas.width = size;
|
|
|
|
|
|
canvas.height = size;
|
|
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
|
|
ctx.clearRect(0, 0, size, size);
|
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
|
ctx.arc(size / 2, size / 2, size / 2 - 1, 0, Math.PI * 2);
|
|
|
|
|
|
ctx.fillStyle = 'rgba(255, 215, 0, 1)';
|
|
|
|
|
|
ctx.fill();
|
|
|
|
|
|
return canvas.toDataURL();
|
|
|
|
|
|
}
|
|
|
|
|
|
const yellowCircleImg = useMemo(() => createYellowCircleDataUrl(30), []);
|
|
|
|
|
|
|
2026-06-12 16:12:06 +08:00
|
|
|
|
// 监听 drawRealTime 变化,每次开始执行任务时重置为定位模式
|
2026-06-15 13:57:41 +08:00
|
|
|
|
useEffect(() => {
|
2026-06-12 16:12:06 +08:00
|
|
|
|
if (drawRealTime) {
|
|
|
|
|
|
// 开始执行任务时:重置为定位模式,清空之前的轨迹数据
|
|
|
|
|
|
tracePointRef.current.setMode(TPMode.LOCATION);
|
|
|
|
|
|
isNavigationModeRef.current = false;
|
|
|
|
|
|
lastFinishPointRef.current = null;
|
|
|
|
|
|
|
|
|
|
|
|
// 清空导航相关的实体
|
|
|
|
|
|
const viewer = viewerRef.current;
|
|
|
|
|
|
if (viewer) {
|
|
|
|
|
|
if (navLineRef.current) {
|
|
|
|
|
|
viewer.entities.remove(navLineRef.current);
|
|
|
|
|
|
navLineRef.current = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (followCarEntityRef.current) {
|
|
|
|
|
|
viewer.entities.remove(followCarEntityRef.current);
|
|
|
|
|
|
followCarEntityRef.current = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
viewer.scene.requestRender();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [drawRealTime]);
|
|
|
|
|
|
|
2026-06-11 17:02:23 +08:00
|
|
|
|
// 处理完成点
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!finishPoint || !finishPoint.lng || !finishPoint.lat || !drawRealTime) {
|
|
|
|
|
|
// 没有 finishPoint 时,移除跟随小车
|
|
|
|
|
|
const viewer = viewerRef.current;
|
2026-06-15 13:57:41 +08:00
|
|
|
|
if (viewer && followCarEntityRef.current) {
|
2026-06-11 17:02:23 +08:00
|
|
|
|
viewer.entities.remove(followCarEntityRef.current);
|
|
|
|
|
|
followCarEntityRef.current = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const point = { lng: finishPoint.lng, lat: finishPoint.lat };
|
|
|
|
|
|
|
|
|
|
|
|
// 如果是定位模式且还没有导航模式,则切换到导航模式
|
|
|
|
|
|
if (!isNavigationModeRef.current && drawRealTime) {
|
|
|
|
|
|
tracePointRef.current.setMode(TPMode.NAVIGATION);
|
|
|
|
|
|
isNavigationModeRef.current = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
tracePointRef.current.upsert(point, TPAction.ADD);
|
|
|
|
|
|
lastFinishPointRef.current = finishPoint;
|
|
|
|
|
|
|
|
|
|
|
|
// 更新轨迹绘制
|
|
|
|
|
|
updateTraceLine();
|
|
|
|
|
|
|
|
|
|
|
|
// 更新或添加跟随小车
|
|
|
|
|
|
}, [finishPoint, drawRealTime]);
|
|
|
|
|
|
|
|
|
|
|
|
// 更新跟随小车
|
|
|
|
|
|
const updateFollowCar = (cartesianPos, heading = 0) => {
|
|
|
|
|
|
const viewer = viewerRef.current;
|
|
|
|
|
|
if (!viewer || !cartesianPos) return;
|
|
|
|
|
|
const Cesium = window.Cesium;
|
|
|
|
|
|
|
|
|
|
|
|
const hpr = new Cesium.HeadingPitchRoll(heading, 0, 0);
|
|
|
|
|
|
const orientation = Cesium.Transforms.headingPitchRollQuaternion(cartesianPos, hpr);
|
|
|
|
|
|
|
|
|
|
|
|
if (followCarEntityRef.current) {
|
|
|
|
|
|
followCarEntityRef.current.position = cartesianPos;
|
|
|
|
|
|
followCarEntityRef.current.orientation = orientation;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
followCarEntityRef.current = viewer.entities.add({
|
|
|
|
|
|
position: cartesianPos,
|
|
|
|
|
|
orientation: orientation,
|
|
|
|
|
|
billboard: {
|
|
|
|
|
|
image: carImg,
|
|
|
|
|
|
width: 50,
|
|
|
|
|
|
height: 50,
|
|
|
|
|
|
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
|
|
|
|
|
|
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
|
|
|
|
|
color: Cesium.Color.YELLOW.withAlpha(0.8),
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const updateTraceLine = (clear = false) => {
|
|
|
|
|
|
const viewer = viewerRef.current;
|
|
|
|
|
|
if (!viewer) return;
|
|
|
|
|
|
const Cesium = window.Cesium;
|
|
|
|
|
|
|
|
|
|
|
|
// 主动清空 或 轨迹数据为空:统一清理
|
|
|
|
|
|
if (clear) {
|
|
|
|
|
|
tracePointRef.current.reset?.();
|
|
|
|
|
|
lastFinishPointRef.current = null;
|
|
|
|
|
|
isNavigationModeRef.current = false;
|
|
|
|
|
|
realtimeCartesianRef.current = [];
|
|
|
|
|
|
realtimeLastPointRef.current = null;
|
|
|
|
|
|
|
|
|
|
|
|
if (navLineRef.current) {
|
|
|
|
|
|
viewer.entities.remove(navLineRef.current);
|
|
|
|
|
|
navLineRef.current = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (followCarEntityRef.current) {
|
|
|
|
|
|
viewer.entities.remove(followCarEntityRef.current);
|
|
|
|
|
|
followCarEntityRef.current = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (realtimeLineEntityRef.current) {
|
|
|
|
|
|
viewer.entities.remove(realtimeLineEntityRef.current);
|
|
|
|
|
|
realtimeLineEntityRef.current = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
viewer.scene.requestRender();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const tracePoints = tracePointRef.current.getTracePoint();
|
|
|
|
|
|
if (!tracePoints || tracePoints.length === 0) {
|
|
|
|
|
|
if (navLineRef.current) {
|
|
|
|
|
|
viewer.entities.remove(navLineRef.current);
|
|
|
|
|
|
navLineRef.current = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (followCarEntityRef.current) {
|
|
|
|
|
|
viewer.entities.remove(followCarEntityRef.current);
|
|
|
|
|
|
followCarEntityRef.current = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
viewer.scene.requestRender();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const validPoints = tracePoints.filter(p => {
|
|
|
|
|
|
const lng = Number(p.lng);
|
|
|
|
|
|
const lat = Number(p.lat);
|
|
|
|
|
|
return !isNaN(lng) && !isNaN(lat) && Math.abs(lng) < 180 && Math.abs(lat) < 90 && lng !== 0 && lat !== 0;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (validPoints.length === 0) {
|
|
|
|
|
|
if (followCarEntityRef.current) {
|
|
|
|
|
|
viewer.entities.remove(followCarEntityRef.current);
|
|
|
|
|
|
followCarEntityRef.current = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const positions = validPoints.map(point =>
|
|
|
|
|
|
Cesium.Cartesian3.fromDegrees(Number(point.lng), Number(point.lat))
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 更新轨迹线
|
|
|
|
|
|
if (navLineRef.current) {
|
|
|
|
|
|
navLineRef.current.polyline.positions = positions;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
navLineRef.current = viewer.entities.add({
|
|
|
|
|
|
polyline: {
|
|
|
|
|
|
positions: positions,
|
|
|
|
|
|
width: 2,
|
|
|
|
|
|
material: Cesium.Color.BLUE.withAlpha(0.9),
|
|
|
|
|
|
clampToGround: true,
|
|
|
|
|
|
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 计算小车朝向
|
|
|
|
|
|
let heading = 0;
|
|
|
|
|
|
const lastIdx = positions.length - 1;
|
|
|
|
|
|
if (lastIdx > 0) {
|
|
|
|
|
|
const prevCart = positions[lastIdx - 1];
|
|
|
|
|
|
const currCart = positions[lastIdx];
|
|
|
|
|
|
const prevCartographic = Cesium.Cartographic.fromCartesian(prevCart);
|
|
|
|
|
|
const currCartographic = Cesium.Cartographic.fromCartesian(currCart);
|
|
|
|
|
|
|
|
|
|
|
|
const dLon = currCartographic.longitude - prevCartographic.longitude;
|
|
|
|
|
|
const dLat = currCartographic.latitude - prevCartographic.latitude;
|
|
|
|
|
|
heading = Math.atan2(dLat, dLon);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 直接传 Cartesian3,无需二次转换
|
|
|
|
|
|
//updateFollowCar(positions[lastIdx], heading);
|
|
|
|
|
|
|
|
|
|
|
|
viewer.scene.requestRender();
|
|
|
|
|
|
};
|
2026-06-12 16:12:06 +08:00
|
|
|
|
// 清空规划航线(使用上一次的 ifClearPlanLine 作为比较,避免重复触发)
|
|
|
|
|
|
const prevIfClearPlanLine = useRef(0);
|
2026-06-15 13:57:41 +08:00
|
|
|
|
useEffect(() => {
|
2026-06-12 16:12:06 +08:00
|
|
|
|
if (ifClearPlanLine !== prevIfClearPlanLine.current && ifClearPlanLine) {
|
2026-06-11 17:02:23 +08:00
|
|
|
|
const viewer = viewerRef.current;
|
|
|
|
|
|
if (viewer) {
|
|
|
|
|
|
if (planLineRef.current) {
|
|
|
|
|
|
viewer.entities.remove(planLineRef.current);
|
|
|
|
|
|
planLineRef.current = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
viewer.scene.requestRender();
|
|
|
|
|
|
}
|
2026-06-12 16:12:06 +08:00
|
|
|
|
prevIfClearPlanLine.current = ifClearPlanLine;
|
2026-06-11 17:02:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
}, [ifClearPlanLine]);
|
|
|
|
|
|
|
|
|
|
|
|
// 清空实时轨迹
|
2026-06-12 16:12:06 +08:00
|
|
|
|
const prevIfClearNavLine = useRef(0);
|
2026-06-11 17:02:23 +08:00
|
|
|
|
useEffect(() => {
|
2026-06-15 13:57:41 +08:00
|
|
|
|
if (ifClearNavLine !== prevIfClearNavLine.current && ifClearNavLine) {
|
2026-06-11 17:02:23 +08:00
|
|
|
|
updateTraceLine(true);
|
2026-06-12 16:12:06 +08:00
|
|
|
|
prevIfClearNavLine.current = ifClearNavLine;
|
2026-06-11 17:02:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
}, [ifClearNavLine]);
|
|
|
|
|
|
|
|
|
|
|
|
// 兼容旧 ifClear:同时清空两类
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (ifClear) {
|
|
|
|
|
|
const viewer = viewerRef.current;
|
|
|
|
|
|
if (viewer) {
|
|
|
|
|
|
if (planLineRef.current) {
|
|
|
|
|
|
viewer.entities.remove(planLineRef.current);
|
|
|
|
|
|
planLineRef.current = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
updateTraceLine(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [ifClear])
|
|
|
|
|
|
|
2026-05-12 08:34:13 +08:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
|
|
|
|
|
|
const viewer = viewerRef.current;
|
|
|
|
|
|
if (!viewer || !window.Cesium) return;
|
|
|
|
|
|
|
|
|
|
|
|
const Cesium = window.Cesium;
|
|
|
|
|
|
|
|
|
|
|
|
const isRealtimeMode =
|
|
|
|
|
|
realtimePosition?.lon &&
|
|
|
|
|
|
realtimePosition?.lat &&
|
|
|
|
|
|
!isNaN(Number(realtimePosition.lon)) &&
|
|
|
|
|
|
!isNaN(Number(realtimePosition.lat));
|
|
|
|
|
|
|
|
|
|
|
|
const nextIds = new Set();
|
|
|
|
|
|
|
|
|
|
|
|
const validPositions = [];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
devices.forEach((device, index) => {
|
|
|
|
|
|
|
|
|
|
|
|
const raw = device?.lastRunningStatus || {};
|
|
|
|
|
|
const parsed = parseLngLat(raw.longitude, raw.latitude);
|
|
|
|
|
|
if (!parsed) return;
|
|
|
|
|
|
|
|
|
|
|
|
const { longitude, latitude } = parsed;
|
|
|
|
|
|
|
|
|
|
|
|
validPositions.push({ lon: longitude, lat: latitude });
|
|
|
|
|
|
|
|
|
|
|
|
const id = getRobotEntityId(device, index);
|
|
|
|
|
|
nextIds.add(id);
|
|
|
|
|
|
|
|
|
|
|
|
const isIOT = device.from == "IOT";
|
|
|
|
|
|
|
|
|
|
|
|
const position = Cesium.Cartesian3.fromDegrees(
|
|
|
|
|
|
longitude,
|
|
|
|
|
|
latitude,
|
|
|
|
|
|
1
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// ==============================
|
|
|
|
|
|
// 图标选择
|
|
|
|
|
|
// ==============================
|
|
|
|
|
|
|
|
|
|
|
|
let image = carImg;
|
|
|
|
|
|
let width = 60;
|
|
|
|
|
|
let height = 60;
|
|
|
|
|
|
const carsize = device?.productId == 138 ? 40 : 60;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (isIOT) {
|
|
|
|
|
|
|
|
|
|
|
|
image = device?.type === "camera" ? cameraImg : iotImg;
|
|
|
|
|
|
|
|
|
|
|
|
width = device?.type === "camera" ? 40 : 20;
|
|
|
|
|
|
height = device?.type === "camera" ? 40 : 20;
|
|
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
|
|
|
|
image =
|
|
|
|
|
|
device?.type === "plane" ? planeImg : // 飞机
|
|
|
|
|
|
device?.type === "drone" ? planeImg : // 无人机
|
|
|
|
|
|
device?.type === "mower" ? carImg : // 割草
|
|
|
|
|
|
device?.type === "robot" ? droneInspectImg : // 巡检
|
|
|
|
|
|
carImg; // 默认车
|
|
|
|
|
|
|
|
|
|
|
|
width =
|
|
|
|
|
|
device?.type === "plane" ||
|
|
|
|
|
|
device?.type === "drone" ||
|
|
|
|
|
|
device?.type === "drone-inspect"
|
|
|
|
|
|
? 70
|
|
|
|
|
|
: carsize;
|
|
|
|
|
|
|
|
|
|
|
|
height =
|
|
|
|
|
|
device?.type === "plane" ||
|
|
|
|
|
|
device?.type === "drone" ||
|
|
|
|
|
|
device?.type === "drone-inspect"
|
|
|
|
|
|
? 70
|
|
|
|
|
|
: carsize;
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ==============================
|
|
|
|
|
|
// entity 获取 / 创建
|
|
|
|
|
|
// ==============================
|
|
|
|
|
|
|
|
|
|
|
|
let entity = entityMapRef.current.get(id);
|
|
|
|
|
|
|
|
|
|
|
|
if (!entity) {
|
|
|
|
|
|
|
|
|
|
|
|
entity = viewer.entities.add({
|
|
|
|
|
|
|
|
|
|
|
|
id,
|
|
|
|
|
|
|
|
|
|
|
|
position,
|
|
|
|
|
|
|
|
|
|
|
|
billboard: {
|
|
|
|
|
|
image,
|
|
|
|
|
|
width,
|
|
|
|
|
|
height,
|
|
|
|
|
|
verticalOrigin: window.Cesium.VerticalOrigin.BOTTOM,
|
|
|
|
|
|
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
|
|
|
|
|
show: !isIOT
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
label: {
|
|
|
|
|
|
text: device.name || "",
|
|
|
|
|
|
font: "12px sans-serif",
|
|
|
|
|
|
pixelOffset: new Cesium.Cartesian2(0, -40),
|
2026-05-22 08:03:30 +08:00
|
|
|
|
fillColor: Cesium?.Color.WHITE,
|
2026-05-12 08:34:13 +08:00
|
|
|
|
showBackground: true,
|
2026-05-22 08:03:30 +08:00
|
|
|
|
backgroundColor: Cesium?.Color.BLACK.withAlpha(0.6)
|
2026-05-12 08:34:13 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
properties: {
|
|
|
|
|
|
robot: device
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
entityMapRef.current.set(id, entity);
|
|
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
|
|
|
|
// ==============================
|
|
|
|
|
|
// 更新 entity
|
|
|
|
|
|
// ==============================
|
|
|
|
|
|
|
|
|
|
|
|
entity.position = position;
|
|
|
|
|
|
|
|
|
|
|
|
entity.billboard.image = image;
|
|
|
|
|
|
entity.billboard.width = width;
|
|
|
|
|
|
entity.billboard.height = height;
|
|
|
|
|
|
|
|
|
|
|
|
entity.label.text = device.name || "";
|
|
|
|
|
|
|
|
|
|
|
|
entity.properties.robot = device;
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ==============================
|
|
|
|
|
|
// 删除不存在设备
|
|
|
|
|
|
// ==============================
|
|
|
|
|
|
|
|
|
|
|
|
entityMapRef.current.forEach((entity, id) => {
|
|
|
|
|
|
|
|
|
|
|
|
if (!nextIds.has(id)) {
|
|
|
|
|
|
|
|
|
|
|
|
viewer.entities.remove(entity);
|
|
|
|
|
|
|
|
|
|
|
|
entityMapRef.current.delete(id);
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ==============================
|
|
|
|
|
|
// 请求渲染
|
|
|
|
|
|
// ==============================
|
|
|
|
|
|
|
|
|
|
|
|
viewer.scene.requestRender();
|
|
|
|
|
|
|
|
|
|
|
|
// ==============================
|
|
|
|
|
|
// 自动 flyTo
|
|
|
|
|
|
// ==============================
|
|
|
|
|
|
|
|
|
|
|
|
if (!isRealtimeMode && validPositions.length > 0) {
|
|
|
|
|
|
|
|
|
|
|
|
let minLon = 180,
|
|
|
|
|
|
maxLon = -180,
|
|
|
|
|
|
minLat = 90,
|
|
|
|
|
|
maxLat = -90;
|
|
|
|
|
|
|
|
|
|
|
|
validPositions.forEach(p => {
|
|
|
|
|
|
|
|
|
|
|
|
minLon = Math.min(minLon, p.lon);
|
|
|
|
|
|
maxLon = Math.max(maxLon, p.lon);
|
|
|
|
|
|
minLat = Math.min(minLat, p.lat);
|
|
|
|
|
|
maxLat = Math.max(maxLat, p.lat);
|
|
|
|
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
if (getCameraMode() !== "devices") return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (validPositions.length === 1) {
|
|
|
|
|
|
|
|
|
|
|
|
const p = validPositions[0];
|
|
|
|
|
|
|
|
|
|
|
|
viewer.camera.flyTo({
|
|
|
|
|
|
destination: Cesium.Cartesian3.fromDegrees(
|
|
|
|
|
|
p.lon,
|
|
|
|
|
|
p.lat,
|
|
|
|
|
|
800
|
|
|
|
|
|
),
|
|
|
|
|
|
duration: 1.2
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
|
|
|
|
const rectangle = Cesium.Rectangle.fromDegrees(
|
|
|
|
|
|
minLon,
|
|
|
|
|
|
minLat,
|
|
|
|
|
|
maxLon,
|
|
|
|
|
|
maxLat
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
viewer.camera.flyTo({
|
|
|
|
|
|
destination: rectangle,
|
|
|
|
|
|
duration: 1.2
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}, [devices, realtimePosition]);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!viewerRef.current || pathPoints.length < 2) return;
|
|
|
|
|
|
const viewer = viewerRef.current;
|
|
|
|
|
|
movingPathRef.current = [...points];
|
|
|
|
|
|
movingIndexRef.current = 0;
|
|
|
|
|
|
isMovingRef.current = true;
|
|
|
|
|
|
|
|
|
|
|
|
let targetEntity = null;
|
|
|
|
|
|
for (let [id, ent] of entityMapRef.current) {
|
|
|
|
|
|
targetEntity = ent;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!targetEntity) return;
|
|
|
|
|
|
movingEntityRef.current = targetEntity;
|
|
|
|
|
|
|
|
|
|
|
|
const animateMove = () => {
|
|
|
|
|
|
if (!isMovingRef.current) return;
|
|
|
|
|
|
const path = movingPathRef.current;
|
|
|
|
|
|
const idx = movingIndexRef.current;
|
|
|
|
|
|
if (idx >= path.length - 1) {
|
|
|
|
|
|
isMovingRef.current = false;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const p1 = path[idx];
|
|
|
|
|
|
const p2 = path[idx + 1];
|
|
|
|
|
|
const step = moveSpeedRef.current;
|
|
|
|
|
|
const lon = p1.lon + (p2.lon - p1.lon) * step;
|
|
|
|
|
|
const lat = p1.lat + (p2.lat - p1.lat) * step;
|
|
|
|
|
|
const newPos = window.Cesium.Cartesian3.fromDegrees(lon, lat, 1);
|
|
|
|
|
|
targetEntity.position = newPos;
|
|
|
|
|
|
const heading = Math.atan2(p2.lat - p1.lat, p2.lon - p1.lon);
|
|
|
|
|
|
targetEntity.orientation = window.Cesium.Transforms.headingPitchRollQuaternion(
|
|
|
|
|
|
newPos,
|
|
|
|
|
|
new window.Cesium.HeadingPitchRoll(heading, 0, 0)
|
|
|
|
|
|
);
|
|
|
|
|
|
const distance = Math.hypot(lon - p2.lon, lat - p2.lat);
|
|
|
|
|
|
if (distance < 0.00005) {
|
|
|
|
|
|
movingIndexRef.current += 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
viewer.scene.requestRender();
|
|
|
|
|
|
requestAnimationFrame(animateMove);
|
|
|
|
|
|
};
|
|
|
|
|
|
animateMove();
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
isMovingRef.current = false;
|
|
|
|
|
|
};
|
|
|
|
|
|
}, [points]);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const viewer = viewerRef.current;
|
|
|
|
|
|
if (!viewer) return;
|
|
|
|
|
|
const handler = new window.Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
|
|
|
|
|
|
eventHandlerRef.current = handler;
|
|
|
|
|
|
|
|
|
|
|
|
handler.setInputAction((movement) => {
|
|
|
|
|
|
const picked = viewer.scene.pick(movement.endPosition);
|
|
|
|
|
|
if (window.Cesium.defined(picked) && picked.id?.properties?.robot) {
|
|
|
|
|
|
const robot = picked.id.properties.robot.getValue();
|
|
|
|
|
|
showRobotTooltip(tooltipRef.current, robot, movement.endPosition);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
tooltipRef.current.style.display = "none";
|
|
|
|
|
|
}
|
|
|
|
|
|
}, window.Cesium.ScreenSpaceEventType.MOUSE_MOVE);
|
|
|
|
|
|
|
|
|
|
|
|
handler.setInputAction((movement) => {
|
|
|
|
|
|
const picked = viewer.scene.pick(movement.position);
|
|
|
|
|
|
if (window.Cesium.defined(picked) && picked.id?.properties?.robot) {
|
|
|
|
|
|
const robot = picked.id.properties.robot.getValue();
|
|
|
|
|
|
robot.feStatus == 1 && onCameraClick?.(robot);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, window.Cesium.ScreenSpaceEventType.LEFT_CLICK);
|
|
|
|
|
|
|
|
|
|
|
|
handler.setInputAction((movement) => {
|
2026-06-05 15:57:36 +08:00
|
|
|
|
//if (from !== "device-control")
|
|
|
|
|
|
return;
|
2026-05-12 08:34:13 +08:00
|
|
|
|
const cartesian = viewer.camera.pickEllipsoid(movement.position, viewer.scene.globe.ellipsoid);
|
|
|
|
|
|
if (!cartesian) return;
|
|
|
|
|
|
const cartographic = window.Cesium.Cartographic.fromCartesian(cartesian);
|
|
|
|
|
|
const lon = window.Cesium.Math.toDegrees(cartographic.longitude);
|
|
|
|
|
|
const lat = window.Cesium.Math.toDegrees(cartographic.latitude);
|
|
|
|
|
|
const newPoint = { lon, lat };
|
|
|
|
|
|
const newPoints = [...drawPoints, newPoint];
|
|
|
|
|
|
setDrawPoints(newPoints);
|
|
|
|
|
|
updateDrawPath(newPoints);
|
|
|
|
|
|
}, window.Cesium.ScreenSpaceEventType.LEFT_CLICK);
|
|
|
|
|
|
|
|
|
|
|
|
if (onClearDraw) {
|
|
|
|
|
|
onClearDraw(clearDrawingPath);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
handler.destroy();
|
|
|
|
|
|
};
|
|
|
|
|
|
}, [drawPoints, from]);
|
|
|
|
|
|
|
|
|
|
|
|
//useEffect(() => {
|
|
|
|
|
|
// const viewer = viewerRef.current;
|
|
|
|
|
|
// if (!viewer) return;
|
|
|
|
|
|
// const tick = viewer.clock.onTick.addEventListener(() => {
|
|
|
|
|
|
// viewer.scene.requestRender();
|
|
|
|
|
|
// });
|
|
|
|
|
|
// return () => {
|
|
|
|
|
|
// viewer.clock.onTick.removeEventListener(tick);
|
|
|
|
|
|
// };
|
|
|
|
|
|
//}, []);
|
|
|
|
|
|
|
|
|
|
|
|
function createYellowCircleCanvas(size = 40) {
|
|
|
|
|
|
const canvas = document.createElement("canvas");
|
|
|
|
|
|
canvas.width = size; canvas.height = size;
|
|
|
|
|
|
const ctx = canvas.getContext("2d");
|
|
|
|
|
|
const r = size / 2;
|
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
|
ctx.arc(r, r, r, 0, Math.PI * 2);
|
|
|
|
|
|
ctx.fillStyle = "#facc15";
|
|
|
|
|
|
ctx.fill();
|
|
|
|
|
|
return canvas;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ======================
|
|
|
|
|
|
// ✅ 永久不闪烁 · 实时轨迹终极版
|
|
|
|
|
|
// ======================
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
|
|
|
|
|
|
const viewer = viewerRef.current;
|
|
|
|
|
|
if (!viewer || !window.Cesium) return;
|
|
|
|
|
|
|
|
|
|
|
|
const Cesium = window.Cesium;
|
|
|
|
|
|
|
|
|
|
|
|
viewer.scene.requestRender();
|
|
|
|
|
|
|
2026-06-11 17:02:23 +08:00
|
|
|
|
// =============================
|
|
|
|
|
|
// 判断是否是无人机模式
|
|
|
|
|
|
// =============================
|
|
|
|
|
|
const isDroneMode = realtimeType === 'plane' || realtimeType === 'drone' || devices.some(d => d.type === 'plane' || d.type === 'drone');
|
|
|
|
|
|
|
2026-05-12 08:34:13 +08:00
|
|
|
|
// =============================
|
|
|
|
|
|
// 判断实时数据是否合法
|
|
|
|
|
|
// =============================
|
|
|
|
|
|
|
|
|
|
|
|
const hasRealtime =
|
|
|
|
|
|
realtimePosition &&
|
|
|
|
|
|
realtimePosition.lon &&
|
|
|
|
|
|
realtimePosition.lat &&
|
|
|
|
|
|
!isNaN(Number(realtimePosition.lon)) &&
|
|
|
|
|
|
!isNaN(Number(realtimePosition.lat));
|
|
|
|
|
|
|
|
|
|
|
|
// ====================================
|
|
|
|
|
|
// 情况1:没有实时数据 -> 清理所有 realtime
|
|
|
|
|
|
// ====================================
|
|
|
|
|
|
|
|
|
|
|
|
if (!hasRealtime) {
|
|
|
|
|
|
|
|
|
|
|
|
if (realtimeCarEntityRef.current) {
|
|
|
|
|
|
viewer.entities.remove(realtimeCarEntityRef.current);
|
|
|
|
|
|
realtimeCarEntityRef.current = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-11 17:02:23 +08:00
|
|
|
|
// 移除实时轨迹线
|
2026-05-12 08:34:13 +08:00
|
|
|
|
if (realtimeLineEntityRef.current) {
|
|
|
|
|
|
viewer.entities.remove(realtimeLineEntityRef.current);
|
|
|
|
|
|
realtimeLineEntityRef.current = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
realtimeCartesianRef.current = [];
|
|
|
|
|
|
realtimeLastPointRef.current = null;
|
|
|
|
|
|
realtimeHiddenRef.current = false;
|
|
|
|
|
|
hasFlownRef.current = false;
|
|
|
|
|
|
|
|
|
|
|
|
// 恢复所有设备显示
|
|
|
|
|
|
entityMapRef.current.forEach(e => {
|
|
|
|
|
|
if (e.billboard) e.billboard.show = true;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
alarmMapRef.current.forEach(e => {
|
|
|
|
|
|
if (e.billboard) e.billboard.show = true;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
iotMapRef.current.forEach(e => {
|
|
|
|
|
|
if (e.billboard) e.billboard.show = true;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
viewer.scene.requestRender();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-05 15:57:36 +08:00
|
|
|
|
// ====================================
|
|
|
|
|
|
// 情况2:有实时数据 -> 隐藏 focusEntity
|
|
|
|
|
|
// ====================================
|
|
|
|
|
|
if (focusEntityRef.current) {
|
|
|
|
|
|
viewer.entities.remove(focusEntityRef.current);
|
|
|
|
|
|
focusEntityRef.current = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 08:34:13 +08:00
|
|
|
|
// ====================================
|
|
|
|
|
|
// 解析实时坐标
|
|
|
|
|
|
// ====================================
|
2026-06-05 15:57:36 +08:00
|
|
|
|
entityMapRef.current.forEach((entity) => {
|
|
|
|
|
|
if (entity.billboard) entity.billboard.show = false;
|
|
|
|
|
|
});
|
|
|
|
|
|
alarmMapRef.current.forEach((entity) => {
|
|
|
|
|
|
if (entity.billboard) entity.billboard.show = false;
|
|
|
|
|
|
});
|
|
|
|
|
|
iotMapRef.current.forEach((entity) => {
|
|
|
|
|
|
if (entity.billboard) entity.billboard.show = false;
|
|
|
|
|
|
});
|
2026-05-12 08:34:13 +08:00
|
|
|
|
|
2026-06-17 11:14:14 +08:00
|
|
|
|
const lon = Number(realtimePosition.lon || finishPoint.lng);
|
|
|
|
|
|
const lat = Number(realtimePosition.lat || finishPoint.lat);
|
2026-05-12 08:34:13 +08:00
|
|
|
|
|
|
|
|
|
|
const position = Cesium.Cartesian3.fromDegrees(lon, lat, 1);
|
|
|
|
|
|
|
|
|
|
|
|
// ====================================
|
2026-06-11 17:02:23 +08:00
|
|
|
|
// 无人机模式:保持原来的逻辑
|
2026-05-12 08:34:13 +08:00
|
|
|
|
// ====================================
|
2026-06-15 13:57:41 +08:00
|
|
|
|
if (isDroneMode || from == "deviceStatus") {
|
2026-06-11 17:02:23 +08:00
|
|
|
|
// 轨迹点去抖动(防止重复点)
|
|
|
|
|
|
const last = realtimeLastPointRef.current;
|
|
|
|
|
|
let shouldAdd = true;
|
|
|
|
|
|
|
|
|
|
|
|
if (last) {
|
|
|
|
|
|
const dx = last.lon - lon;
|
|
|
|
|
|
const dy = last.lat - lat;
|
|
|
|
|
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
|
|
if (dist < 0.0000001) {
|
|
|
|
|
|
shouldAdd = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-12 08:34:13 +08:00
|
|
|
|
|
2026-06-11 17:02:23 +08:00
|
|
|
|
if (shouldAdd && drawRealTime) {
|
|
|
|
|
|
realtimeLastPointRef.current = { lon, lat };
|
|
|
|
|
|
realtimeCartesianRef.current.push(
|
|
|
|
|
|
Cesium.Cartesian3.fromDegrees(lon, lat)
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-05-12 08:34:13 +08:00
|
|
|
|
|
2026-06-11 17:02:23 +08:00
|
|
|
|
// 第一次进入 realtime -> 隐藏所有设备
|
|
|
|
|
|
if (!realtimeHiddenRef.current) {
|
|
|
|
|
|
realtimeHiddenRef.current = true;
|
2026-05-12 08:34:13 +08:00
|
|
|
|
|
2026-06-11 17:02:23 +08:00
|
|
|
|
entityMapRef.current.forEach(e => {
|
|
|
|
|
|
if (e.billboard) e.billboard.show = false;
|
|
|
|
|
|
});
|
2026-05-12 08:34:13 +08:00
|
|
|
|
|
2026-06-11 17:02:23 +08:00
|
|
|
|
alarmMapRef.current.forEach(e => {
|
|
|
|
|
|
if (e.billboard) e.billboard.show = false;
|
|
|
|
|
|
});
|
2026-05-12 08:34:13 +08:00
|
|
|
|
|
2026-06-11 17:02:23 +08:00
|
|
|
|
iotMapRef.current.forEach(e => {
|
|
|
|
|
|
if (e.billboard) e.billboard.show = false;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-05-12 08:34:13 +08:00
|
|
|
|
|
2026-06-11 17:02:23 +08:00
|
|
|
|
// 无人机图标
|
2026-06-15 13:57:41 +08:00
|
|
|
|
const img = realtimeType === 'plane' || realtimeType === 'drone' ? planeImg : carImg;
|
2026-06-11 17:02:23 +08:00
|
|
|
|
const imgWidth = 70;
|
|
|
|
|
|
const imgHeight = 70;
|
|
|
|
|
|
|
|
|
|
|
|
// 如果有航向角,设置旋转
|
|
|
|
|
|
let rotation = 0;
|
|
|
|
|
|
if (realtimePosition?.heading !== undefined && !isNaN(Number(realtimePosition.heading))) {
|
|
|
|
|
|
let headingDeg = Number(realtimePosition.heading);
|
|
|
|
|
|
rotation = Cesium.Math.toRadians(headingDeg);
|
2026-05-12 08:34:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 13:57:41 +08:00
|
|
|
|
//rotation = Cesium.Math.toRadians(-3.17);
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-11 17:02:23 +08:00
|
|
|
|
if (!realtimeCarEntityRef.current) {
|
|
|
|
|
|
realtimeCarEntityRef.current = viewer.entities.add({
|
|
|
|
|
|
position: position,
|
|
|
|
|
|
billboard: {
|
|
|
|
|
|
image: img,
|
|
|
|
|
|
width: imgWidth,
|
|
|
|
|
|
height: imgHeight,
|
|
|
|
|
|
verticalOrigin: Cesium.VerticalOrigin.CENTER,
|
|
|
|
|
|
rotation: rotation,
|
|
|
|
|
|
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
realtimeCarEntityRef.current.position = position;
|
|
|
|
|
|
realtimeCarEntityRef.current.billboard.image = img;
|
|
|
|
|
|
realtimeCarEntityRef.current.billboard.width = imgWidth;
|
|
|
|
|
|
realtimeCarEntityRef.current.billboard.height = imgHeight;
|
|
|
|
|
|
realtimeCarEntityRef.current.billboard.rotation = rotation;
|
|
|
|
|
|
}
|
2026-05-12 08:34:13 +08:00
|
|
|
|
|
2026-06-11 17:02:23 +08:00
|
|
|
|
// 无人机模式的实时轨迹线
|
|
|
|
|
|
if (drawRealTime) {
|
|
|
|
|
|
if (realtimeLineEntityRef.current) {
|
|
|
|
|
|
// 已存在,CallbackProperty 会自动更新
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const callbackPositions = new Cesium.CallbackProperty(() => {
|
|
|
|
|
|
return realtimeCartesianRef.current;
|
|
|
|
|
|
}, false);
|
|
|
|
|
|
|
|
|
|
|
|
realtimeLineEntityRef.current = viewer.entities.add({
|
|
|
|
|
|
polyline: {
|
|
|
|
|
|
positions: callbackPositions,
|
|
|
|
|
|
width: 5,
|
|
|
|
|
|
material: new Cesium.PolylineGlowMaterialProperty({
|
|
|
|
|
|
glowPower: 0.25,
|
|
|
|
|
|
color: Cesium?.Color.LIME
|
|
|
|
|
|
}),
|
|
|
|
|
|
clampToGround: true,
|
|
|
|
|
|
disableDepthTestDistance: Number.POSITIVE_INFINITY
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 不绘制实时轨迹,移除线
|
|
|
|
|
|
if (realtimeLineEntityRef.current) {
|
|
|
|
|
|
viewer.entities.remove(realtimeLineEntityRef.current);
|
|
|
|
|
|
realtimeLineEntityRef.current = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
realtimeCartesianRef.current = [];
|
|
|
|
|
|
}
|
2026-05-12 08:34:13 +08:00
|
|
|
|
|
2026-06-11 17:02:23 +08:00
|
|
|
|
// 无人机模式不需要完成点逻辑,直接返回
|
|
|
|
|
|
if (!hasFlownRef.current) {
|
|
|
|
|
|
hasFlownRef.current = true;
|
|
|
|
|
|
if (getCameraMode() !== "realtime") return;
|
2026-05-12 08:34:13 +08:00
|
|
|
|
|
2026-06-11 17:02:23 +08:00
|
|
|
|
viewer.camera.flyTo({
|
|
|
|
|
|
destination: Cesium.Cartesian3.fromDegrees(lon, lat, 30),
|
|
|
|
|
|
orientation: {
|
|
|
|
|
|
heading: 0,
|
|
|
|
|
|
pitch: Cesium.Math.toRadians(-90),
|
|
|
|
|
|
roll: 0
|
|
|
|
|
|
},
|
|
|
|
|
|
duration: 1.2
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-05-12 08:34:13 +08:00
|
|
|
|
|
2026-06-11 17:02:23 +08:00
|
|
|
|
viewer.scene.requestRender();
|
|
|
|
|
|
return;
|
2026-05-12 08:34:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ====================================
|
2026-06-11 17:02:23 +08:00
|
|
|
|
// 机器人模式:使用 TracePoint 逻辑
|
2026-05-12 08:34:13 +08:00
|
|
|
|
// ====================================
|
2026-06-11 17:02:23 +08:00
|
|
|
|
const point = { lng: lon, lat: lat };
|
2026-05-12 08:34:13 +08:00
|
|
|
|
|
2026-06-11 17:02:23 +08:00
|
|
|
|
// 如果是定位模式且 drawRealTime 为 false,保持定位模式
|
|
|
|
|
|
if (!drawRealTime) {
|
|
|
|
|
|
tracePointRef.current.setMode(TPMode.LOCATION);
|
|
|
|
|
|
isNavigationModeRef.current = false;
|
|
|
|
|
|
}
|
2026-05-12 08:34:13 +08:00
|
|
|
|
|
2026-06-11 17:02:23 +08:00
|
|
|
|
// 更新当前点
|
|
|
|
|
|
tracePointRef.current.upsert(point, TPAction.UPDATE);
|
|
|
|
|
|
|
|
|
|
|
|
// 更新轨迹绘制
|
|
|
|
|
|
updateTraceLine();
|
2026-05-12 08:34:13 +08:00
|
|
|
|
|
2026-06-11 17:02:23 +08:00
|
|
|
|
// 第一次进入 realtime -> 隐藏所有设备
|
2026-05-12 08:34:13 +08:00
|
|
|
|
if (!realtimeHiddenRef.current) {
|
|
|
|
|
|
|
|
|
|
|
|
realtimeHiddenRef.current = true;
|
|
|
|
|
|
|
|
|
|
|
|
entityMapRef.current.forEach(e => {
|
|
|
|
|
|
if (e.billboard) e.billboard.show = false;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
alarmMapRef.current.forEach(e => {
|
|
|
|
|
|
if (e.billboard) e.billboard.show = false;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
iotMapRef.current.forEach(e => {
|
|
|
|
|
|
if (e.billboard) e.billboard.show = false;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-11 17:02:23 +08:00
|
|
|
|
// 机器人图标
|
|
|
|
|
|
const img = carImg;
|
|
|
|
|
|
const imgWidth = 60;
|
|
|
|
|
|
const imgHeight = 60;
|
2026-06-01 17:14:55 +08:00
|
|
|
|
|
|
|
|
|
|
// 如果有航向角,设置旋转
|
|
|
|
|
|
let rotation = 0;
|
|
|
|
|
|
if (realtimePosition?.heading !== undefined && !isNaN(Number(realtimePosition.heading))) {
|
|
|
|
|
|
let headingDeg = Number(realtimePosition.heading);
|
2026-06-05 15:57:36 +08:00
|
|
|
|
rotation = Cesium.Math.toRadians(headingDeg);
|
2026-06-01 17:14:55 +08:00
|
|
|
|
}
|
2026-05-12 08:34:13 +08:00
|
|
|
|
|
2026-06-01 17:14:55 +08:00
|
|
|
|
if (!realtimeCarEntityRef.current) {
|
2026-05-12 08:34:13 +08:00
|
|
|
|
realtimeCarEntityRef.current = viewer.entities.add({
|
|
|
|
|
|
position: position,
|
|
|
|
|
|
billboard: {
|
2026-06-01 17:14:55 +08:00
|
|
|
|
image: img,
|
|
|
|
|
|
width: imgWidth,
|
|
|
|
|
|
height: imgHeight,
|
|
|
|
|
|
verticalOrigin: Cesium.VerticalOrigin.CENTER,
|
|
|
|
|
|
rotation: rotation,
|
2026-05-12 08:34:13 +08:00
|
|
|
|
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
realtimeCarEntityRef.current.position = position;
|
2026-06-01 17:14:55 +08:00
|
|
|
|
realtimeCarEntityRef.current.billboard.image = img;
|
|
|
|
|
|
realtimeCarEntityRef.current.billboard.width = imgWidth;
|
|
|
|
|
|
realtimeCarEntityRef.current.billboard.height = imgHeight;
|
|
|
|
|
|
realtimeCarEntityRef.current.billboard.rotation = rotation;
|
2026-05-12 08:34:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-11 17:02:23 +08:00
|
|
|
|
// 机器人模式只在导航模式下显示额外的实时轨迹线
|
|
|
|
|
|
if (drawRealTime && isNavigationModeRef.current) {
|
|
|
|
|
|
// 轨迹点去抖动(防止重复点)
|
|
|
|
|
|
const last = realtimeLastPointRef.current;
|
|
|
|
|
|
let shouldAdd = true;
|
|
|
|
|
|
|
|
|
|
|
|
if (last) {
|
|
|
|
|
|
const dx = last.lon - lon;
|
|
|
|
|
|
const dy = last.lat - lat;
|
|
|
|
|
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
|
|
//if (dist < 0.0000001) {
|
|
|
|
|
|
// shouldAdd = false;
|
|
|
|
|
|
//}
|
|
|
|
|
|
}
|
2026-05-12 08:34:13 +08:00
|
|
|
|
|
2026-06-11 17:02:23 +08:00
|
|
|
|
//if (shouldAdd) {
|
|
|
|
|
|
// realtimeLastPointRef.current = { lon, lat };
|
|
|
|
|
|
// realtimeCartesianRef.current.push(
|
|
|
|
|
|
// Cesium.Cartesian3.fromDegrees(lon, lat)
|
|
|
|
|
|
// );
|
|
|
|
|
|
//}
|
|
|
|
|
|
|
|
|
|
|
|
//if (realtimeLineEntityRef.current) {
|
|
|
|
|
|
// // 已存在,CallbackProperty 会自动更新
|
|
|
|
|
|
//} else {
|
|
|
|
|
|
// const callbackPositions = new Cesium.CallbackProperty(() => {
|
|
|
|
|
|
// return realtimeCartesianRef.current;
|
|
|
|
|
|
// }, false);
|
|
|
|
|
|
|
|
|
|
|
|
// realtimeLineEntityRef.current = viewer.entities.add({
|
|
|
|
|
|
// polyline: {
|
|
|
|
|
|
// positions: callbackPositions,
|
|
|
|
|
|
// width: 5,
|
|
|
|
|
|
// material: new Cesium.PolylineGlowMaterialProperty({
|
|
|
|
|
|
// glowPower: 0.25,
|
|
|
|
|
|
// color: Cesium?.Color.LIME
|
|
|
|
|
|
// }),
|
|
|
|
|
|
// clampToGround: true,
|
|
|
|
|
|
// disableDepthTestDistance: Number.POSITIVE_INFINITY
|
|
|
|
|
|
// }
|
|
|
|
|
|
// });
|
|
|
|
|
|
//}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 移除实时轨迹线
|
|
|
|
|
|
if (realtimeLineEntityRef.current) {
|
|
|
|
|
|
viewer.entities.remove(realtimeLineEntityRef.current);
|
|
|
|
|
|
realtimeLineEntityRef.current = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
realtimeCartesianRef.current = [];
|
2026-05-12 08:34:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!hasFlownRef.current) {
|
|
|
|
|
|
|
|
|
|
|
|
hasFlownRef.current = true;
|
|
|
|
|
|
if (getCameraMode() !== "realtime") return;
|
|
|
|
|
|
|
|
|
|
|
|
viewer.camera.flyTo({
|
|
|
|
|
|
|
|
|
|
|
|
destination: Cesium.Cartesian3.fromDegrees(
|
|
|
|
|
|
lon,
|
|
|
|
|
|
lat,
|
2026-06-10 17:22:55 +08:00
|
|
|
|
30
|
2026-05-12 08:34:13 +08:00
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
orientation: {
|
|
|
|
|
|
heading: 0,
|
|
|
|
|
|
pitch: Cesium.Math.toRadians(-90),
|
|
|
|
|
|
roll: 0
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
duration: 1.2
|
|
|
|
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
viewer.scene.requestRender();
|
|
|
|
|
|
|
|
|
|
|
|
}, [realtimePosition]);
|
2026-06-15 20:54:41 +08:00
|
|
|
|
// 截图功能
|
|
|
|
|
|
const captureScreenshot = useCallback(() => {
|
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
|
const viewer = viewerRef.current;
|
|
|
|
|
|
if (!viewer) {
|
|
|
|
|
|
resolve(null);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 使用 Cesium 的截图功能
|
|
|
|
|
|
viewer.scene.render();
|
|
|
|
|
|
|
|
|
|
|
|
const canvas = viewer.scene.canvas;
|
|
|
|
|
|
const dataUrl = canvas.toDataURL('image/jpeg', 0.8);
|
|
|
|
|
|
resolve(dataUrl);
|
|
|
|
|
|
});
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
// 暴露 ref 方法
|
|
|
|
|
|
useImperativeHandle(ref, () => ({
|
|
|
|
|
|
captureScreenshot
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
2026-05-12 08:34:13 +08:00
|
|
|
|
return <>
|
|
|
|
|
|
<div ref={containerRef} className="w-full h-full bg-slate-900 overflow-hidden" />
|
|
|
|
|
|
<div
|
|
|
|
|
|
ref={tooltipRef}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: "absolute", display: "none", pointerEvents: "none",
|
|
|
|
|
|
background: "rgba(0,0,0,0.75)", color: "#fff", padding: "8px 10px",
|
|
|
|
|
|
borderRadius: "4px", fontSize: "12px", zIndex: 9999, whiteSpace: "nowrap"
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</>;
|
2026-06-15 20:54:41 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
export default CesiumMap;
|