Files
web-Iot/src/components/Map.jsx

2151 lines
59 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect, useRef, useState, useMemo, useImperativeHandle, forwardRef, useCallback } from 'react';
import carImg from '../assets/images/carplane.png';
import inspectImg from "../assets/images/inspectCar.png"
import planeImg from '../assets/images/planeicon.png';
import cameraImg from '../assets/images/camera.png';
import droneInspectImg from '../assets/images/xunjiancar.png';
import gecocar from '../assets/images/gecaocar.png';
import arrowImg from '../assets/images/arrow.png';
import iotImg from '../assets/images/iot.png';
import envConfig from '../../env';
import { TracePoint, TPAction, TPMode } from '../lib/TracePoint/tracePoint';
import { useTranslation } from 'react-i18next';
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";
const CesiumMap = React.forwardRef(({ ifClear, ifClearPlanLine, ifClearNavLine, finishPoint, drawRealTime, realtimePosition, realtimeType, pathPoints = [], devices = [], onCameraClick, focusLocation, points = [], markedPoints = [], onPathChange, from, onClearDraw, onMarkPoint, routeMode = '', polyPoints, mapLayer }, ref) => {
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());
const planLineRef = useRef(null); // points 规划航线
const polygonRef = useRef(null); // coverage 区域多边形
const navLineRef = useRef(null); // 完成点实时轨迹
const markedPointsRef = useRef([]); // 标记点实体
const markedLineRef = useRef(null); // 标记点连线实体
const onMarkPointRef = useRef(onMarkPoint); // 总是指向最新的 onMarkPoint,避免闭包滞后
const onCameraClickRef = useRef(onCameraClick); // 同样处理,避免闭包滞后
const { t } = useTranslation();
const realtimeEntityRef = useRef(null);
const realtimeLineRef = useRef(null);
const realtimeTrackRef = useRef([]);
const tracePointRef = useRef(new TracePoint());
const lastFinishPointRef = useRef(null);
const isNavigationModeRef = useRef(false);
const followCarEntityRef = useRef(null); // 跟随小车的实例
const isCameraFlyingRef = useRef(false);
const lastFlightPromiseRef = useRef(null);
// ==============================
// 飞行优先级调度:优先级高的可打断低的;同级不重复起飞
// realtime(100) > focus(90) > devices(80) > track(70) > layer(10)
// ==============================
const lastFlightPriorityRef = useRef(-1);
const FLIGHT_PRIORITY = { realtime: 100, focus: 90, devices: 80, track: 70, layer: 10 };
const flyToView = (viewer, priority, config) => {
if (!viewer) return;
if (priority <= lastFlightPriorityRef.current) return; // 同级或更低不重复/覆盖;更高优先级才打断
lastFlightPriorityRef.current = priority;
try { viewer.camera.cancelFlight(); } catch (e) { /* 忽略取消异常 */ }
viewer.camera.flyTo(config);
};
const resetFlightPriority = () => { lastFlightPriorityRef.current = -1; };
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 focusEntityRef = useRef(null);
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: {
color: window.Cesium?.Color.SKYBLUE,
pixelSize: 10,
disableDepthTestDistance: Number.POSITIVE_INFINITY,
},
label: {
text: `${i + 1}`,
font: '12px sans-serif',
pixelOffset: new window.Cesium.Cartesian2(0, -15),
fillColor: window.Cesium?.Color.WHITE
}
});
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,
material: window.Cesium?.Color.SKYBLUE,
clampToGround: true,
}
});
}
console.log(newPoints, "newPoints")
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))
);
if (planLineRef.current) {
viewer.entities.remove(planLineRef.current);
}
// 2. 传入的 points 本身为空 → 清空轨迹退出
if (!points || points.length === 0) {
const viewer = viewerRef.current;
if (viewer) {
try {
if (planLineRef.current) {
viewer.entities.remove(planLineRef.current);
planLineRef.current = null;
}
} catch (e) { }
viewer.scene.requestRender(); // 强制刷新!
}
return;
}
const validPoints = points?.filter(p => {
const lon = Number(p.lng);
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(
Number(point.lng),
Number(point.lat)
)
);
const trackLine = viewer.entities.add({
polyline: {
positions: positions,
width: 2,
// 规划航线用橙色,和实时轨迹区分
material: Cesium.Color.ORANGE.withAlpha(0.8),
clampToGround: true,
disableDepthTestDistance: Number.POSITIVE_INFINITY,
},
});
planLineRef.current = trackLine;
// ======================
// 🚀 非实时模式 -> 自动飞到轨迹
// ======================
if (!isRealtimeMode) {
let minLon = 180, maxLon = -180;
let minLat = 90, maxLat = -90;
validPoints.forEach(p => {
minLon = Math.min(minLon, p.lng);
maxLon = Math.max(maxLon, p.lng);
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();
flyToView(viewer, FLIGHT_PRIORITY.track, {
destination: Cesium.Cartesian3.fromDegrees(
p.lng,
p.lat,
800
),
duration: 1.2
});
viewer.scene.requestRender();
} else {
const rectangle = Cesium.Rectangle.fromDegrees(
minLon,
minLat,
maxLon,
maxLat
);
viewer.scene.requestRender();
flyToView(viewer, FLIGHT_PRIORITY.track, {
destination: rectangle,
duration: 1.2
});
viewer.scene.requestRender();
}
}
};
const drawPolygon = (points) => {
const viewer = viewerRef.current;
if (!viewer) return;
const isRealtimeMode = !!(
realtimePosition?.lon &&
realtimePosition?.lat &&
!isNaN(Number(realtimePosition.lon)) &&
!isNaN(Number(realtimePosition.lat))
);
const Cesium = window.Cesium;
// 每次绘制区域前,先移除上一次绘制的地块
if (polygonRef.current) {
try {
viewer.entities.remove(polygonRef.current);
polygonRef.current = null;
} catch (e) { }
}
if (planLineRef.current) {
try {
viewer.entities.remove(planLineRef.current);
planLineRef.current = null;
} catch (e) { }
}
const validPoints = points?.filter(p => {
const lon = Number(p.lng);
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(
Number(point.lng),
Number(point.lat)
)
);
// coverage(区域覆盖)模式下以多边形 polygon 框绘制,否则为折线
const isCoverage = routeMode === 'coverage';
let trackLine;
if (isCoverage) {
trackLine = viewer.entities.add({
polygon: {
hierarchy: positions,
material: Cesium.Color.ORANGE.withAlpha(0.2),
outline: true,
outlineColor: Cesium.Color.ORANGE.withAlpha(0.8),
height: 0,
clampToGround: true,
disableDepthTestDistance: Number.POSITIVE_INFINITY,
},
});
} else {
trackLine = viewer.entities.add({
polyline: {
positions: positions,
width: 2,
material: Cesium.Color.ORANGE.withAlpha(0.8),
clampToGround: true,
disableDepthTestDistance: Number.POSITIVE_INFINITY,
},
});
}
polygonRef.current = trackLine;
// ======================
// 🚀 非实时模式 -> 自动飞到轨迹
// ======================
if (!isRealtimeMode) {
let minLon = 180, maxLon = -180;
let minLat = 90, maxLat = -90;
validPoints.forEach(p => {
minLon = Math.min(minLon, p.lng);
maxLon = Math.max(maxLon, p.lng);
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();
flyToView(viewer, FLIGHT_PRIORITY.track, {
destination: Cesium.Cartesian3.fromDegrees(
p.lng,
p.lat,
800
),
duration: 1.2
});
viewer.scene.requestRender();
} else {
const rectangle = Cesium.Rectangle.fromDegrees(
minLon,
minLat,
maxLon,
maxLat
);
viewer.scene.requestRender();
flyToView(viewer, FLIGHT_PRIORITY.track, {
destination: rectangle,
duration: 1.2
});
viewer.scene.requestRender();
}
}
};
useEffect(() => {
resetFlightPriority();
if (polyPoints && polyPoints.length > 0) {
drawPolygon(polyPoints)
} else {
// polyPoints 被清空时,移除已绘制的区域
const viewer = viewerRef.current;
if (viewer && polygonRef.current) {
try {
viewer.entities.remove(polygonRef.current);
polygonRef.current = null;
} catch (e) { }
}
}
}, [polyPoints])
useEffect(() => {
resetFlightPriority();
if (viewerRef.current) {
drawTrackLine(points);
}
}, [points]);
// 渲染标记点 + 连线(唯一系统,避免重复标记)
const drawMarkedPoints = (points) => {
const viewer = viewerRef.current;
if (!viewer) return;
// 先清空之前的标记点与连线
markedPointsRef.current.forEach(id => {
viewer.entities.removeById(id);
});
markedPointsRef.current = [];
if (markedLineRef.current) {
try {
viewer.entities.remove(markedLineRef.current);
} catch (e) { }
markedLineRef.current = null;
}
if (!points || points.length === 0) return;
// 先绘制连线:有 2 个及以上打点才连线
if (points.length >= 2) {
const linePositions = points.map(p =>
window.Cesium.Cartesian3.fromDegrees(Number(p.lon), Number(p.lat))
);
markedLineRef.current = viewer.entities.add({
polyline: {
positions: linePositions,
width: 4,
material: window.Cesium.Color.SKYBLUE,
clampToGround: true,
}
});
}
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]);
function showRobotTooltip(tooltip, robot, position, t) {
const statusMap = {
1: { text: t('common.working'), color: "#10b981" },
2: { text: t('devices.idle'), color: "#f59e0b" },
3: { text: t('devices.offline'), 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>${t('devices.deviceData')}:</strong>${deviceValuesHTML}</div>` : ''}
<div>${t('systemSetting.longitude')}:${Number(robot.lastRunningStatus?.longitude).toFixed(6)}</div>
<div>${t('systemSetting.latitude')}:${Number(robot.lastRunningStatus?.latitude).toFixed(6)}</div>
`;
}
const getCursorLngLat = (viewer, screenPos) => {
const cartesian = viewer.camera.pickEllipsoid(screenPos, viewer.scene.globe.ellipsoid);
if (!window.Cesium.defined(cartesian)) return { lon: NaN, lat: NaN };
const cartographic = window.Cesium.Cartographic.fromCartesian(cartesian);
return {
lon: window.Cesium.Math.toDegrees(cartographic.longitude),
lat: window.Cesium.Math.toDegrees(cartographic.latitude),
};
};
function showCoordinateTooltip(tooltip, position, t, lon, lat, robot) {
tooltip.style.display = "block";
tooltip.style.left = position.x + 12 + "px";
tooltip.style.top = position.y + 12 + "px";
tooltip.style.whiteSpace = "nowrap";
const lonStr = Number.isFinite(lon) ? lon.toFixed(6) : "--";
const latStr = Number.isFinite(lat) ? lat.toFixed(6) : "--";
if (robot) {
const statusMap = {
1: { text: t('common.working'), color: "#10b981" },
2: { text: t('devices.idle'), color: "#f59e0b" },
3: { text: t('devices.offline'), color: "#f87171" }
};
const status = statusMap[robot.feStatus] || statusMap[1];
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>${t('devices.deviceData')}:</strong>${deviceValuesHTML}</div>` : ''}
<div>${t('systemSetting.longitude')}:${lonStr}</div>
<div>${t('systemSetting.latitude')}:${latStr}</div>
`;
} else {
tooltip.innerHTML = `
<div style="font-weight:600;margin-bottom:4px;">📍 ${t('systemSetting.longitude')}:${lonStr}</div>
<div style="font-weight:600;">${t('systemSetting.latitude')}:${latStr}</div>
`;
}
}
function fetchPublishedLayers() {
const baseUrl = envConfig.geoserver_url;
//const baseUrl = 'https://gis.caasrobot.com/geoserver';
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) {
//console.log('已发布的图层:', publishedLayers)
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) || polyPoints?.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 = [];
//if (index == 2 && from == "other") {
// //flyToLayer(layerIdentifier, baseUrl, 100);
//}
//if (index == 0 && from == "other") {
// flyToLayer(layerIdentifier, baseUrl, 100);
//}
//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);
}
}
}
function flyToLayer(layerIdentifier, baseUrl, flyHeight = 100) {
//console.log('开始获取图层范围', layerIdentifier);
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');
let targetBbox = null;
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'));
targetBbox = [minx, miny, maxx, maxy];
console.log('读取到图层bbox', targetBbox);
}
break;
}
}
if (!targetBbox) {
console.warn('该图层未找到BoundingBox,无法飞行', layerIdentifier);
return;
}
const [minx, miny, maxx, maxy] = targetBbox;
// 取图层中心点
const centerLon = (minx + maxx) / 2;
const centerLat = (miny + maxy) / 2;
flyToView(viewerRef.current, FLIGHT_PRIORITY.layer, {
// 固定高度,第三个参数就是高度(单位米)
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);
});
}
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: 30,
height: 30,
verticalOrigin: window.Cesium.VerticalOrigin.BOTTOM,
disableDepthTestDistance: Number.POSITIVE_INFINITY
},
label: {
text: robot.name,
font: "14px sans-serif",
pixelOffset: new window.Cesium.Cartesian2(0, -40),
fillColor: window.Cesium?.Color.WHITE,
showBackground: true,
backgroundColor: window.Cesium?.Color.BLACK.withAlpha(0.6)
}
});
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;
viewer.scene.globe.baseColor = window.Cesium?.Color.BLACK;
viewer.scene.globe.depthTestAgainstTerrain = false;
viewer.scene.requestRenderMode = true; // 👈 加这个(只在需要时渲染)
viewer.scene.globe.enableLighting = false;
//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 // 初始化直接到位,无需动画
//});
try { fetchPublishedLayers() } catch (e) { }
return () => {
if (lastFlightPromiseRef.current) viewer.camera.cancelFlight();
};
}, []);
// 根据场站映射图层定位地图:切换场站时始终飞向该场站的图层
// 不再用可能滞后的 devices 判断(避免用上一场站的机器决策);
// 若场站有机器,由 devices 效果按更高优先级接管飞向机器
useEffect(() => {
if (!mapLayer || !viewerRef.current) return;
resetFlightPriority();
flyToLayer(mapLayer, envConfig.geoserver_url, 100);
}, [mapLayer]);
useEffect(() => {
// 如果有 realtimePosition,不显示 focusEntity
const hasRealtime =
realtimePosition?.lon &&
realtimePosition?.lat &&
!isNaN(Number(realtimePosition.lon)) &&
!isNaN(Number(realtimePosition.lat));
if (!viewerRef.current || !focusLocation || hasRealtime) {
if (focusEntityRef.current) {
viewerRef.current?.entities.remove(focusEntityRef.current);
focusEntityRef.current = null;
}
return;
}
const viewer = viewerRef.current;
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: 30,
height: 30,
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)
}
});
flyToView(viewer, FLIGHT_PRIORITY.focus, {
destination: Cesium.Cartesian3.fromDegrees(Number(focusLocation.lon), Number(focusLocation.lat), focusLocation.height || 800),
orientation: {
heading: Cesium.Math.toRadians(0.0),
pitch: Cesium.Math.toRadians(-90.0),
roll: 0.0,
},
duration: 1.5
});
}, [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);
flyToView(viewer, FLIGHT_PRIORITY.devices, { 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), []);
// 监听 drawRealTime 变化,每次开始执行任务时重置为定位模式
useEffect(() => {
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();
}
} else {
tracePointRef.current.setMode(TPMode.LOCATION);
isNavigationModeRef.current = false;
lastFinishPointRef.current = null;
}
}, [drawRealTime]);
// 处理完成点
useEffect(() => {
if (!finishPoint || !finishPoint.lng || !finishPoint.lat || !drawRealTime) {
// 没有 finishPoint 时,移除跟随小车
const viewer = viewerRef.current;
if (viewer && followCarEntityRef.current) {
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: 30,
height: 30,
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();
};
// 清空规划航线(使用上一次的 ifClearPlanLine 作为比较,避免重复触发)
const prevIfClearPlanLine = useRef(0);
useEffect(() => {
if (ifClearPlanLine !== prevIfClearPlanLine.current && ifClearPlanLine) {
const viewer = viewerRef.current;
if (viewer) {
if (planLineRef.current) {
viewer.entities.remove(planLineRef.current);
planLineRef.current = null;
}
if (polygonRef.current) {
viewer.entities.remove(polygonRef.current);
polygonRef.current = null;
}
viewer.scene.requestRender();
}
prevIfClearPlanLine.current = ifClearPlanLine;
}
}, [ifClearPlanLine]);
// 清空实时轨迹
const prevIfClearNavLine = useRef(0);
useEffect(() => {
if (ifClearNavLine !== prevIfClearNavLine.current && ifClearNavLine) {
updateTraceLine(true);
prevIfClearNavLine.current = ifClearNavLine;
}
}, [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])
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 = 30;
let height = 30;
const carsize = device?.productId == 138 ? 30 : 30;
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"
? 30
: carsize;
height =
device?.type === "plane" ||
device?.type === "drone" ||
device?.type === "drone-inspect"
? 30
: 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),
fillColor: Cesium?.Color.WHITE,
showBackground: true,
backgroundColor: Cesium?.Color.BLACK.withAlpha(0.6)
},
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];
flyToView(viewer, FLIGHT_PRIORITY.devices, {
destination: Cesium.Cartesian3.fromDegrees(
p.lon,
p.lat,
800
),
duration: 1.2
});
} else {
const rectangle = Cesium.Rectangle.fromDegrees(
minLon,
minLat,
maxLon,
maxLat
);
flyToView(viewer, FLIGHT_PRIORITY.devices, {
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]);
// 每次渲染都更新回调引用,确保单击处理器用最新版本(避免闭包滞后)
onMarkPointRef.current = onMarkPoint;
onCameraClickRef.current = onCameraClick;
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);
const pickedRobot = (window.Cesium.defined(picked) && picked.id?.properties?.robot) ? picked.id.properties.robot.getValue() : null;
const { lon, lat } = getCursorLngLat(viewer, movement.endPosition);
if (window.Cesium.defined(lon) && Number.isFinite(lon)) {
showCoordinateTooltip(tooltipRef.current, movement.endPosition, t, lon, lat, pickedRobot);
} else if (pickedRobot) {
showCoordinateTooltip(tooltipRef.current, movement.endPosition, t, NaN, NaN, pickedRobot);
} 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 && onCameraClickRef.current?.(robot);
}
}, window.Cesium.ScreenSpaceEventType.LEFT_CLICK);
handler.setInputAction((movement) => {
if (from !== "deviceStatus")
return;
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 };
onMarkPointRef.current?.({ lon, lat });
}, 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();
// =============================
// 判断是否是无人机模式
// =============================
const isDroneMode = realtimeType === 'plane' || realtimeType === 'drone' || devices.some(d => d.type === 'plane' || d.type === 'drone');
// =============================
// 判断实时数据是否合法
// =============================
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;
}
// 移除实时轨迹线
if (realtimeLineEntityRef.current) {
viewer.entities.remove(realtimeLineEntityRef.current);
realtimeLineEntityRef.current = null;
}
realtimeCartesianRef.current = [];
realtimeLastPointRef.current = null;
realtimeHiddenRef.current = false;
hasFlownRef.current = false;
resetFlightPriority();
// 恢复所有设备显示
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;
}
// ====================================
// 情况2:有实时数据 -> 隐藏 focusEntity
// ====================================
if (focusEntityRef.current) {
viewer.entities.remove(focusEntityRef.current);
focusEntityRef.current = null;
}
// ====================================
// 解析实时坐标
// ====================================
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;
});
const lon = Number(realtimePosition.lon || finishPoint.lng);
const lat = Number(realtimePosition.lat || finishPoint.lat);
const position = Cesium.Cartesian3.fromDegrees(lon, lat, 1);
// ====================================
// 无人机模式:保持原来的逻辑
// ====================================
if (isDroneMode || from == "deviceStatus" || from == "devicesTask") {
// 轨迹点去抖动(防止重复点)
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;
//}
}
if (shouldAdd && drawRealTime) {
realtimeLastPointRef.current = { lon, lat };
realtimeCartesianRef.current.push(
Cesium.Cartesian3.fromDegrees(lon, lat)
);
}
// 第一次进入 realtime -> 隐藏所有设备
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;
});
}
// 无人机图标
const img = realtimeType === 'plane' || realtimeType === 'drone' ? planeImg : carImg;
const imgWidth = 30;
const imgHeight = 30;
// 如果有航向角,设置旋转
let rotation = 0;
if (realtimePosition?.heading !== undefined && !isNaN(Number(realtimePosition.heading))) {
let headingDeg = Number(realtimePosition.heading);
rotation = Cesium.Math.toRadians(headingDeg);
}
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;
}
// 无人机模式的实时轨迹线
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 = [];
}
// 无人机模式不需要完成点逻辑,直接返回
if (!hasFlownRef.current) {
hasFlownRef.current = true;
if (getCameraMode() !== "realtime") return;
flyToView(viewer, FLIGHT_PRIORITY.realtime, {
destination: Cesium.Cartesian3.fromDegrees(lon, lat, 30),
orientation: {
heading: 0,
pitch: Cesium.Math.toRadians(-90),
roll: 0
},
duration: 1.2
});
}
viewer.scene.requestRender();
return;
}
// ====================================
// 机器人模式:使用 TracePoint 逻辑
// ====================================
const point = { lng: lon, lat: lat };
// 如果是定位模式且 drawRealTime 为 false,保持定位模式
if (!drawRealTime) {
tracePointRef.current.setMode(TPMode.LOCATION);
isNavigationModeRef.current = false;
}
// 更新当前点
tracePointRef.current.upsert(point, TPAction.UPDATE);
// 更新轨迹绘制
updateTraceLine();
// 第一次进入 realtime -> 隐藏所有设备
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;
});
}
// 机器人图标
const img = carImg;
const imgWidth = 30;
const imgHeight = 30;
// 如果有航向角,设置旋转
let rotation = 0;
if (realtimePosition?.heading !== undefined && !isNaN(Number(realtimePosition.heading))) {
let headingDeg = Number(realtimePosition.heading);
rotation = Cesium.Math.toRadians(headingDeg);
}
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;
}
// 机器人模式只在导航模式下显示额外的实时轨迹线
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;
//}
}
//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 = [];
}
if (!hasFlownRef.current) {
hasFlownRef.current = true;
if (getCameraMode() !== "realtime") return;
flyToView(viewer, FLIGHT_PRIORITY.realtime, {
destination: Cesium.Cartesian3.fromDegrees(
lon,
lat,
30
),
orientation: {
heading: 0,
pitch: Cesium.Math.toRadians(-90),
roll: 0
},
duration: 1.2
});
}
viewer.scene.requestRender();
}, [realtimePosition]);
// 截图功能
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
}));
return (
<div className="w-full h-full bg-slate-900 overflow-hidden relative">
<div ref={containerRef} className="w-full h-full 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: 99999,
whiteSpace: "nowrap",
}}
/>
</div>
);
});
export default CesiumMap;