Files
web-Iot/src/components/Map.jsx
2026-05-22 08:03:30 +08:00

1317 lines
34 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 } from 'react';
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';
import gecaocar from '../assets/images/gecaocar.png';
import arrowImg from '../assets/images/arrow.png';
import iotImg from '../assets/images/iot.png';
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";
export default function CesiumMap({ realtimePosition, pathPoints = [], devices = [], onCameraClick, focusLocation, points = [], onPathChange, from, onClearDraw }) {
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 trackLineRef = useRef(null);
const realtimeEntityRef = useRef(null);
const realtimeLineRef = useRef(null);
const realtimeTrackRef = useRef([]);
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: {
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,
}
});
}
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 (trackLineRef.current) {
viewer.entities.remove(trackLineRef.current);
}
if (!points || points.length === 0) return;
const validPoints = points.filter(p => {
const lon = Number(p.lon);
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.lon),
Number(point.lat)
)
);
const trackLine = viewer.entities.add({
polyline: {
positions: positions,
width: 3,
material: new Cesium.PolylineDashMaterialProperty({
color: Cesium?.Color.BLUE,
dashLength: 16,
gapColor: Cesium?.Color.TRANSPARENT,
}),
clampToGround: true,
disableDepthTestDistance: Number.POSITIVE_INFINITY,
},
});
trackLineRef.current = trackLine;
// ======================
// 🚀 非实时模式 -> 自动飞到轨迹
// ======================
console.log('=== drawTrackLine ===', { points, validPoints, isRealtimeMode });
if (!isRealtimeMode) {
let minLon = 180, maxLon = -180;
let minLat = 90, maxLat = -90;
validPoints.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() !== "track") return;
// 单个点
if (validPoints.length === 1) {
console.log('=== drawTrackLine1111 ===', { points, validPoints, isRealtimeMode });
const p = validPoints[0];
viewer.scene.requestRender();
viewer.camera.flyTo({
destination: Cesium.Cartesian3.fromDegrees(
p.lon,
p.lat,
800
),
duration: 1.2
});
viewer.scene.requestRender();
} else {
console.log('=== drawTrackLine2222 ===', { points, validPoints, isRealtimeMode });
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]);
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() {
const baseUrl = "http://192.168.2.59:7000/geoserver";
//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) {
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`;
console.log("addLayerFromList", wmsUrl, workspace, layerName, index)
addWmsLayerToCesium(wmsUrl, workspace, layerName, index);
}
function getLayerBoundsAndFit(wmsUrl, layerName) {
console.log('=== 获取图层范围开始 ===');
console.log('参数:', { wmsUrl, layerName });
const getCapabilitiesUrl = `${wmsUrl}?service=WMS&version=1.1.1&request=GetCapabilities`;
console.log('GetCapabilities URL:', getCapabilitiesUrl);
fetch(getCapabilitiesUrl)
.then(response => response.text())
.then(xmlText => {
console.log('GetCapabilities响应:', 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 = [];
console.log(wmsUrl, workspace, layerName, index, "wmsUrl, workspace, layerName, index")
//if (index == 2) {
// flyToLayer(layerIdentifier, baseUrl);
//}
//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) {
console.log('定位到图层范围:', layerIdentifier);
if (!viewerRef.current) return;
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');
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'));
const rectangle = window.Cesium.Rectangle.fromDegrees(minx, miny, maxx, maxy);
viewerRef.current.camera.flyTo({
destination: rectangle,
duration: 2
});
}
break;
}
}
});
}
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),
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;
try { fetchPublishedLayers() } catch (e) { }
return () => {
if (lastFlightPromiseRef.current) viewer.camera.cancelFlight();
};
}, []);
useEffect(() => {
// 禁用聚焦飞行
}, [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), []);
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),
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];
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) => {
if (from !== "device-control") 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 };
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();
// =============================
// 判断实时数据是否合法
// =============================
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;
// 恢复所有设备显示
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;
}
// ====================================
// 解析实时坐标
// ====================================
const lon = Number(realtimePosition.lon);
const lat = Number(realtimePosition.lat);
const position = Cesium.Cartesian3.fromDegrees(lon, lat, 1);
// ====================================
// 轨迹点去抖动(防止重复点)
// ====================================
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.00001) {
shouldAdd = false;
}
}
if (shouldAdd) {
realtimeLastPointRef.current = { lon, lat };
realtimeCartesianRef.current.push(
Cesium.Cartesian3.fromDegrees(lon, lat)
);
}
// ====================================
// 限制轨迹长度(防止内存爆)
// ====================================
if (realtimeCartesianRef.current.length > 2000) {
realtimeCartesianRef.current.shift();
}
// ====================================
// 第一次进入 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;
});
}
// ====================================
// 创建实时小车(只创建一次)
// ====================================
if (!realtimeCarEntityRef.current) {
realtimeCarEntityRef.current = viewer.entities.add({
position: position,
billboard: {
image: carImg,
width: 60,
height: 60,
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
disableDepthTestDistance: Number.POSITIVE_INFINITY,
}
});
} else {
realtimeCarEntityRef.current.position = position;
}
// ====================================
// 创建轨迹线(只创建一次)
// ====================================
if (!realtimeLineEntityRef.current) {
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
}
});
}
if (!hasFlownRef.current) {
hasFlownRef.current = true;
if (getCameraMode() !== "realtime") return;
viewer.camera.flyTo({
destination: Cesium.Cartesian3.fromDegrees(
lon,
lat,
1200
),
orientation: {
heading: 0,
pitch: Cesium.Math.toRadians(-90),
roll: 0
},
duration: 1.2
});
}
viewer.scene.requestRender();
}, [realtimePosition]);
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"
}}
/>
</>;
}