fix: stabilize earth bgp geography and rendering

This commit is contained in:
linkong
2026-04-02 15:36:20 +08:00
parent 07e4f519a1
commit e5fec8ba3d
26 changed files with 1788 additions and 137 deletions

View File

@@ -2,7 +2,7 @@
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" href="data:," />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>智能星球计划</title>
</head>

View File

@@ -1,12 +1,12 @@
{
"name": "planet-frontend",
"version": "0.22.8",
"version": "0.22.10",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "planet-frontend",
"version": "0.22.8",
"version": "0.22.10",
"dependencies": {
"@ant-design/icons": "^5.2.6",
"antd": "^5.12.5",
@@ -16,7 +16,9 @@
"react-dom": "^18.2.0",
"react-resizable": "^3.1.3",
"react-router-dom": "^6.21.0",
"simplex-noise": "^4.0.1",
"socket.io-client": "^4.7.2",
"three": "^0.160.0",
"zustand": "^4.4.7"
},
"devDependencies": {
@@ -3009,6 +3011,12 @@
"semver": "bin/semver.js"
}
},
"node_modules/simplex-noise": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/simplex-noise/-/simplex-noise-4.0.3.tgz",
"integrity": "sha512-qSE2I4AngLQG7BXqoZj51jokT4WUXe8mOBrvfOXpci8+6Yu44+/dD5zqDpOx3Ux792eamTd2lLcI8jqFntk/lg==",
"license": "MIT"
},
"node_modules/socket.io-client": {
"version": "4.8.3",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz",
@@ -3059,6 +3067,12 @@
"integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==",
"license": "MIT"
},
"node_modules/three": {
"version": "0.160.1",
"resolved": "https://registry.npmjs.org/three/-/three-0.160.1.tgz",
"integrity": "sha512-Bgl2wPJypDOZ1stAxwfWAcJ0WQf7QzlptsxkjYiURPz+n5k4RBDLsq+6f9Y75TYxn6aHLcWz+JNmwTOXWrQTBQ==",
"license": "MIT"
},
"node_modules/throttle-debounce": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "planet-frontend",
"version": "0.22.9",
"version": "0.22.10",
"private": true,
"dependencies": {
"@ant-design/icons": "^5.2.6",

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

View File

@@ -13,7 +13,9 @@ let showBGP = true;
let totalAnomalyCount = 0;
let totalIncidentCount = 0;
let textureCache = null;
let eventRingTextureCache = null;
let collectorTextureCache = null;
const eventTextureCache = new Map();
let activeEventOverlay = null;
let activeCollectorOverlayContext = null;
const relativeTimeFormatter = new Intl.RelativeTimeFormat("zh-CN", {
@@ -60,6 +62,29 @@ function getMarkerTexture() {
return textureCache;
}
function getEventRingTexture() {
if (eventRingTextureCache) return eventRingTextureCache;
const canvas = document.createElement("canvas");
canvas.width = 128;
canvas.height = 128;
const context = canvas.getContext("2d");
if (!context) {
eventRingTextureCache = new THREE.Texture(canvas);
return eventRingTextureCache;
}
context.clearRect(0, 0, 128, 128);
context.strokeStyle = "rgba(255,255,255,0.98)";
context.lineWidth = 6;
context.beginPath();
context.arc(64, 64, 44, 0, Math.PI * 2);
context.stroke();
eventRingTextureCache = new THREE.CanvasTexture(canvas);
return eventRingTextureCache;
}
function getCollectorTexture() {
if (collectorTextureCache) return collectorTextureCache;
@@ -102,6 +127,121 @@ function getCollectorTexture() {
return collectorTextureCache;
}
function getEventSymbolKind(anomalyType) {
const value = String(anomalyType || "").toLowerCase();
if (value.includes("origin")) return "triangle";
if (value.includes("withdraw")) return "exclamation";
if (value.includes("specific") || value.includes("burst")) return "burst";
if (value.includes("flap")) return "wave";
if (value.includes("leak")) return "leak";
return "dot";
}
function drawTriangleSymbol(context) {
context.beginPath();
context.moveTo(64, 18);
context.lineTo(110, 106);
context.lineTo(18, 106);
context.closePath();
context.fill();
}
function drawExclamationSymbol(context) {
context.beginPath();
context.roundRect(52, 22, 24, 62, 12);
context.fill();
context.beginPath();
context.arc(64, 102, 10, 0, Math.PI * 2);
context.fill();
}
function drawWaveSymbol(context) {
context.lineWidth = 12;
context.lineCap = "round";
context.beginPath();
context.moveTo(18, 76);
context.bezierCurveTo(34, 46, 46, 46, 64, 76);
context.bezierCurveTo(80, 106, 94, 106, 110, 76);
context.stroke();
}
function drawBurstSymbol(context) {
context.lineWidth = 10;
context.lineCap = "round";
for (let index = 0; index < 6; index += 1) {
const angle = (Math.PI * 2 * index) / 6;
const inner = 26;
const outer = 48;
context.beginPath();
context.moveTo(64 + Math.cos(angle) * inner, 64 + Math.sin(angle) * inner);
context.lineTo(64 + Math.cos(angle) * outer, 64 + Math.sin(angle) * outer);
context.stroke();
}
context.beginPath();
context.arc(64, 64, 16, 0, Math.PI * 2);
context.fill();
}
function drawLeakSymbol(context) {
context.lineWidth = 10;
context.lineCap = "round";
context.beginPath();
context.moveTo(28, 96);
context.lineTo(64, 28);
context.lineTo(100, 96);
context.stroke();
context.beginPath();
context.moveTo(40, 82);
context.lineTo(64, 54);
context.lineTo(88, 82);
context.stroke();
}
function drawDotSymbol(context) {
context.beginPath();
context.arc(64, 64, 28, 0, Math.PI * 2);
context.fill();
}
function getEventTexture(anomalyType) {
const kind = getEventSymbolKind(anomalyType);
if (eventTextureCache.has(kind)) return eventTextureCache.get(kind);
const canvas = document.createElement("canvas");
canvas.width = 128;
canvas.height = 128;
const context = canvas.getContext("2d");
if (!context) {
const fallback = new THREE.Texture(canvas);
eventTextureCache.set(kind, fallback);
return fallback;
}
context.clearRect(0, 0, 128, 128);
context.fillStyle = "rgba(255,255,255,0.96)";
context.strokeStyle = "rgba(255,255,255,0.96)";
context.shadowBlur = 0;
context.lineJoin = "round";
if (kind === "triangle") {
drawTriangleSymbol(context);
} else if (kind === "exclamation") {
drawExclamationSymbol(context);
} else if (kind === "wave") {
drawWaveSymbol(context);
} else if (kind === "burst") {
drawBurstSymbol(context);
} else if (kind === "leak") {
drawLeakSymbol(context);
} else {
drawDotSymbol(context);
}
const texture = new THREE.CanvasTexture(canvas);
eventTextureCache.set(kind, texture);
return texture;
}
function normalizeSeverity(severity) {
const value = String(severity || "").trim().toLowerCase();
@@ -934,9 +1074,14 @@ function createCollectorMarker(markerData) {
function createAnomalyMarker(markerData) {
const sprite = new THREE.Sprite(
createSpriteMaterial({
new THREE.SpriteMaterial({
map: getEventTexture(markerData.incident_type || markerData.anomaly_type),
color: getSeverityColor(markerData.severity),
transparent: true,
opacity: BGP_CONFIG.opacity.normal,
depthWrite: false,
depthTest: true,
blending: THREE.NormalBlending,
}),
);
@@ -960,12 +1105,45 @@ function createAnomalyMarker(markerData) {
...markerData,
};
const ringA = new THREE.Sprite(
new THREE.SpriteMaterial({
map: getEventRingTexture(),
color: getSeverityColor(markerData.severity),
transparent: true,
opacity: 0,
depthWrite: false,
depthTest: true,
blending: THREE.AdditiveBlending,
}),
);
ringA.scale.setScalar(baseScale * BGP_CONFIG.eventRingScaleA);
ringA.position.set(0, 0, -0.01);
sprite.add(ringA);
const ringB = new THREE.Sprite(
new THREE.SpriteMaterial({
map: getEventRingTexture(),
color: getSeverityColor(markerData.severity),
transparent: true,
opacity: 0,
depthWrite: false,
depthTest: true,
blending: THREE.AdditiveBlending,
}),
);
ringB.scale.setScalar(baseScale * BGP_CONFIG.eventRingScaleB);
ringB.position.set(0, 0, -0.02);
sprite.add(ringB);
sprite.userData.ringA = ringA;
sprite.userData.ringB = ringB;
anomalyMarkers.push(sprite);
bgpGroup.add(sprite);
}
function dedupeAnomalies(features) {
const latestByCollector = new Map();
const latestByLocation = new Map();
features.forEach((feature) => {
const data = buildAnomalyFeatureData(feature);
@@ -976,22 +1154,30 @@ function dedupeAnomalies(features) {
(activeEventCountByCollector.get(data.collector) || 0) + 1,
);
const dedupeKey = `${data.collector}|${data.latitude.toFixed(4)}|${data.longitude.toFixed(4)}`;
const previous = latestByCollector.get(dedupeKey);
const dedupeKey = `${data.latitude.toFixed(3)}|${data.longitude.toFixed(3)}`;
const previous = latestByLocation.get(dedupeKey);
const currentTime = data.created_at_raw
? new Date(data.created_at_raw).getTime()
: 0;
const previousTime = previous?.created_at_raw
? new Date(previous.created_at_raw).getTime()
: 0;
const currentSeverity = getSeverityScale(data.severity);
const previousSeverity = previous ? getSeverityScale(previous.severity) : 0;
if (!previous || currentTime >= previousTime) {
latestByCollector.set(dedupeKey, data);
if (
!previous ||
currentSeverity > previousSeverity ||
(currentSeverity === previousSeverity && currentTime >= previousTime)
) {
latestByLocation.set(dedupeKey, data);
}
});
return Array.from(latestByCollector.values())
return Array.from(latestByLocation.values())
.sort((a, b) => {
const severityDiff = getSeverityScale(b.severity) - getSeverityScale(a.severity);
if (severityDiff !== 0) return severityDiff;
const timeA = a.created_at_raw ? new Date(a.created_at_raw).getTime() : 0;
const timeB = b.created_at_raw ? new Date(b.created_at_raw).getTime() : 0;
return timeB - timeA;
@@ -1000,7 +1186,7 @@ function dedupeAnomalies(features) {
}
function dedupeIncidents(features) {
const latestByKey = new Map();
const latestByLocation = new Map();
features.forEach((feature) => {
const data = buildIncidentFeatureData(feature);
@@ -1013,22 +1199,30 @@ function dedupeIncidents(features) {
);
});
const dedupeKey = String(data.incident_key || data.id);
const previous = latestByKey.get(dedupeKey);
const dedupeKey = `${data.latitude.toFixed(3)}|${data.longitude.toFixed(3)}`;
const previous = latestByLocation.get(dedupeKey);
const currentTime = data.created_at_raw
? new Date(data.created_at_raw).getTime()
: 0;
const previousTime = previous?.created_at_raw
? new Date(previous.created_at_raw).getTime()
: 0;
const currentSeverity = getSeverityScale(data.severity);
const previousSeverity = previous ? getSeverityScale(previous.severity) : 0;
if (!previous || currentTime >= previousTime) {
latestByKey.set(dedupeKey, data);
if (
!previous ||
currentSeverity > previousSeverity ||
(currentSeverity === previousSeverity && currentTime >= previousTime)
) {
latestByLocation.set(dedupeKey, data);
}
});
return Array.from(latestByKey.values())
return Array.from(latestByLocation.values())
.sort((a, b) => {
const severityDiff = getSeverityScale(b.severity) - getSeverityScale(a.severity);
if (severityDiff !== 0) return severityDiff;
const timeA = a.created_at_raw ? new Date(a.created_at_raw).getTime() : 0;
const timeB = b.created_at_raw ? new Date(b.created_at_raw).getTime() : 0;
return timeB - timeA;
@@ -1046,25 +1240,40 @@ function applyCollectorCounts() {
export async function loadBGPAnomalies(scene, earth) {
clearBGPData(earth);
const [collectorsResponse, incidentsResponse, anomaliesResponse] = await Promise.all([
fetch(PATHS.bgpCollectorsApi),
fetch(`${PATHS.bgpIncidentsApi}?limit=${BGP_CONFIG.defaultFetchLimit}`),
fetch(`${PATHS.bgpApi}?limit=${BGP_CONFIG.defaultFetchLimit}`),
]);
const collectorsResponse = await fetch(PATHS.bgpCollectorsApi);
if (!collectorsResponse.ok) {
throw new Error(`BGP collectors HTTP ${collectorsResponse.status}`);
}
if (!incidentsResponse.ok) {
throw new Error(`BGP incidents HTTP ${incidentsResponse.status}`);
let anomaliesPayload = { type: "FeatureCollection", features: [], count: 0 };
try {
const anomaliesResponse = await fetch(
`${PATHS.bgpApi}?limit=${BGP_CONFIG.defaultFetchLimit}`,
{ signal: AbortSignal.timeout(5000) },
);
if (!anomaliesResponse.ok) {
throw new Error(`BGP anomalies HTTP ${anomaliesResponse.status}`);
}
anomaliesPayload = await anomaliesResponse.json();
} catch (error) {
console.warn("BGP anomalies unavailable, falling back to collectors only:", error);
}
if (!anomaliesResponse.ok) {
throw new Error(`BGP anomalies HTTP ${anomaliesResponse.status}`);
let incidentsPayload = { type: "FeatureCollection", features: [], count: 0 };
try {
const incidentsResponse = await fetch(
`${PATHS.bgpIncidentsApi}?limit=${BGP_CONFIG.defaultFetchLimit}`,
{ signal: AbortSignal.timeout(5000) },
);
if (!incidentsResponse.ok) {
throw new Error(`BGP incidents HTTP ${incidentsResponse.status}`);
}
incidentsPayload = await incidentsResponse.json();
} catch (error) {
console.warn("BGP incidents unavailable, falling back to anomalies:", error);
}
const collectorsPayload = await collectorsResponse.json();
const incidentsPayload = await incidentsResponse.json();
const anomaliesPayload = await anomaliesResponse.json();
const collectorFeatures = Array.isArray(collectorsPayload?.features)
? collectorsPayload.features
: [];
@@ -1232,28 +1441,59 @@ export function updateBGPVisualState(lockedObjectType, lockedObject, camera) {
let scale = marker.userData.baseScale;
let opacity = BGP_CONFIG.opacity.normal;
let markerColor = marker.userData.baseColor || getSeverityColor(marker.userData.severity);
const isIncidentMarker = marker.userData.source === "bgp_incident";
let ringBaseOpacity = isIncidentMarker
? BGP_CONFIG.eventRingOpacity
: BGP_CONFIG.eventRingOpacity * 0.45;
if (isLocked || isLinkedCollectorLocked) {
scale *= 1 + BGP_CONFIG.lockedPulseAmplitude * pulse;
opacity =
BGP_CONFIG.opacity.lockedMin +
(BGP_CONFIG.opacity.lockedMax - BGP_CONFIG.opacity.lockedMin) * pulse;
ringBaseOpacity *= 1.2;
} else if (isHovered) {
scale *= BGP_CONFIG.hoverScale;
opacity = BGP_CONFIG.opacity.hover;
ringBaseOpacity *= 1.05;
} else if (isOtherLocked) {
scale *= BGP_CONFIG.dimmedScale;
opacity = 0.1;
markerColor = 0x7d8ca3;
ringBaseOpacity = 0.02;
} else {
scale *= 1 + BGP_CONFIG.normalPulseAmplitude * pulse;
opacity = BGP_CONFIG.opacity.normal;
opacity = isIncidentMarker ? 0.7 : 0.62;
}
marker.scale.setScalar(scale);
marker.material.color.setHex(markerColor);
marker.material.opacity = opacity;
marker.visible = showBGP;
const ringPhaseA = (now * BGP_CONFIG.eventRingSpeed + marker.userData.pulseOffset) % 1;
const applyRingState = (ring, phase, maxScale) => {
if (!ring) return;
const progress = Math.max(0, Math.min(1, phase));
const minScale = 1.28;
const desiredWorldScale =
marker.userData.baseScale * (minScale + progress * (maxScale - minScale));
const parentScale = Math.max(scale, 0.0001);
const localRingScale = desiredWorldScale / parentScale;
const fadeIn = Math.max(0, Math.min(1, (progress - 0.08) / 0.14));
const fadeOut = 1 - progress;
const visibility = fadeIn * fadeOut;
ring.scale.setScalar(localRingScale);
ring.material.color.setHex(markerColor);
ring.material.opacity = showBGP ? ringBaseOpacity * visibility : 0;
ring.visible = showBGP;
};
applyRingState(marker.userData.ringA, ringPhaseA, BGP_CONFIG.eventRingScaleA);
if (marker.userData.ringB) {
marker.userData.ringB.material.opacity = 0;
marker.userData.ringB.visible = false;
}
});
}
@@ -1368,42 +1608,9 @@ export function showBGPEventOverlay(marker, earth) {
typeof region?.longitude === "number",
);
if (validRegions.length === 0) return;
const averageLatitude =
validRegions.reduce((sum, region) => sum + region.latitude, 0) /
validRegions.length;
const averageLongitude =
validRegions.reduce((sum, region) => sum + region.longitude, 0) /
validRegions.length;
const hubPosition = latLonToVector3(
averageLatitude,
averageLongitude,
CONFIG.earthRadius + BGP_CONFIG.eventHubAltitudeOffset,
);
const hub = createOverlaySprite({
color: BGP_CONFIG.eventHubColor,
opacity: 0.95,
scale: BGP_CONFIG.eventHubScale,
});
hub.position.copy(hubPosition);
hub.renderOrder = 6;
bgpOverlayGroup.add(hub);
const overlayItems = [hub];
const overlayItems = [];
validRegions.forEach((region) => {
const regionPosition = latLonToVector3(
region.latitude,
region.longitude,
CONFIG.earthRadius + BGP_CONFIG.collectorAltitudeOffset + 0.3,
);
const link = createArcLine(regionPosition, hubPosition, BGP_CONFIG.linkColor);
link.renderOrder = 4;
bgpOverlayGroup.add(link);
overlayItems.push(link);
const halo = createOverlaySprite({
color: BGP_CONFIG.regionColor,
opacity: 0.24,

View File

@@ -138,6 +138,10 @@ export const BGP_CONFIG = {
eventHubColor: 0x8af5ff,
linkColor: 0x54d2ff,
regionColor: 0x2dd4bf,
eventRingScaleA: 2.5,
eventRingScaleB: 3.4,
eventRingOpacity: 0.5,
eventRingSpeed: 0.001,
collectorHaloScale: 11.5,
collectorPulseHaloScale: 16.5,
collectorCoverageHaloScale: 22.5

View File

@@ -104,7 +104,7 @@ export function createClouds(scene, earthObj) {
earthObj.add(clouds);
textureLoader.load(
'https://threejs.org/examples/textures/planets/earth_clouds_1024.png',
'./assets/earth_clouds_1024.png',
function(texture) {
material.map = texture;
material.needsUpdate = true;