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

1314 lines
34 KiB
React
Raw Normal View History

2026-05-12 08:34:13 +08:00
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 = '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`;
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 = [];
//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"
}}
/>
</>;
}