release: bump version to 0.41.1
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
19
frontend/public/earth/assets/icons/bgp-collector.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
||||
<!-- BGP route collector marker. Outer ring + Material access_point icon. -->
|
||||
<!-- States: idle opacity=0.74, hover/locked use brighter blend (color controlled externally) -->
|
||||
|
||||
<!-- Outer ring -->
|
||||
<circle cx="64" cy="64" r="22" fill="none" stroke="rgba(111,160,197,0.34)" stroke-width="1.2"/>
|
||||
|
||||
<!-- access_point icon: 24x24 path scaled 4x and offset to (16,16) in 128x128 canvas space -->
|
||||
<g transform="translate(16 16) scale(4 4)"
|
||||
fill="rgba(214,224,233,0.88)"
|
||||
stroke="rgba(64,106,136,0.74)"
|
||||
stroke-width="0.9"
|
||||
stroke-linejoin="round"
|
||||
stroke-linecap="round">
|
||||
<path d="M4.93 4.93A9.97 9.97 0 0 0 2 12c0 2.76 1.12 5.26 2.93 7.07l1.41-1.41A7.94 7.94 0 0 1 4 12c0-2.21.89-4.22 2.34-5.66zm14.14 0l-1.41 1.41A7.96 7.96 0 0 1 20 12c0 2.22-.89 4.22-2.34 5.66l1.41 1.41A9.97 9.97 0 0 0 22 12c0-2.76-1.12-5.26-2.93-7.07M7.76 7.76A5.98 5.98 0 0 0 6 12c0 1.65.67 3.15 1.76 4.24l1.41-1.41A4 4 0 0 1 8 12c0-1.11.45-2.11 1.17-2.83zm8.48 0l-1.41 1.41A4 4 0 0 1 16 12c0 1.11-.45 2.11-1.17 2.83l1.41 1.41A5.98 5.98 0 0 0 18 12c0-1.65-.67-3.15-1.76-4.24M12 10a2 2 0 0 0-2 2a2 2 0 0 0 2 2a2 2 0 0 0 2-2a2 2 0 0 0-2-2"/>
|
||||
<!-- Center dot override -->
|
||||
<circle cx="12" cy="12" r="0.85" fill="rgba(222,231,239,0.72)" stroke="none"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
13
frontend/public/earth/assets/icons/bgp-event-burst.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
||||
<!-- BGP specific/burst anomaly symbol: 6 radial spokes (r 26→48) + center dot (r=16). -->
|
||||
<!-- Spoke endpoints calculated as: inner=64+cos(angle)*26, outer=64+cos(angle)*48 for 6 angles -->
|
||||
<g stroke="currentColor" stroke-width="10" stroke-linecap="round">
|
||||
<line x1="90" y1="64" x2="112" y2="64"/>
|
||||
<line x1="77" y1="86.5" x2="88" y2="105.6"/>
|
||||
<line x1="51" y1="86.5" x2="40" y2="105.6"/>
|
||||
<line x1="38" y1="64" x2="16" y2="64"/>
|
||||
<line x1="51" y1="41.5" x2="40" y2="22.4"/>
|
||||
<line x1="77" y1="41.5" x2="88" y2="22.4"/>
|
||||
</g>
|
||||
<circle cx="64" cy="64" r="16" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 714 B |
4
frontend/public/earth/assets/icons/bgp-event-dot.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
||||
<!-- BGP generic event symbol: filled circle. -->
|
||||
<circle cx="64" cy="64" r="28" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 177 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
||||
<!-- BGP withdraw event symbol: exclamation mark (rounded bar + dot). -->
|
||||
<rect x="52" y="22" width="24" height="62" rx="12" fill="currentColor"/>
|
||||
<circle cx="64" cy="102" r="10" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 277 B |
7
frontend/public/earth/assets/icons/bgp-event-leak.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
||||
<!-- BGP route leak symbol: two nested open triangular outlines (outer + inner chevron). -->
|
||||
<g stroke="currentColor" stroke-width="10" stroke-linecap="round" stroke-linejoin="round" fill="none">
|
||||
<polyline points="28,96 64,28 100,96"/>
|
||||
<polyline points="40,82 64,54 88,82"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 364 B |
4
frontend/public/earth/assets/icons/bgp-event-ring.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
||||
<!-- Ring overlay for BGP event markers. Stroked circle, no fill. -->
|
||||
<circle cx="64" cy="64" r="44" fill="none" stroke="rgba(255,255,255,0.98)" stroke-width="6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 238 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
||||
<!-- BGP origin anomaly symbol: upward triangle. -->
|
||||
<polygon points="64,18 110,106 18,106" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 188 B |
4
frontend/public/earth/assets/icons/bgp-event-wave.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
||||
<!-- BGP flap event symbol: zigzag/wave (filled W shape, closed). -->
|
||||
<polygon points="14,100 38,26 64,100 90,26 114,100" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 218 B |
12
frontend/public/earth/assets/icons/bgp-glow-dot.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
||||
<!-- BGP collector base glow: radial gradient dot. Inner r=8 fully opaque, fades to transparent at r=56. -->
|
||||
<defs>
|
||||
<radialGradient id="bgp-glow" cx="64" cy="64" r="56" fx="64" fy="64" fr="8" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="white" stop-opacity="1"/>
|
||||
<stop offset="24%" stop-color="white" stop-opacity="0.92"/>
|
||||
<stop offset="58%" stop-color="white" stop-opacity="0.35"/>
|
||||
<stop offset="100%" stop-color="white" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<circle cx="64" cy="64" r="56" fill="url(#bgp-glow)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 653 B |
16
frontend/public/earth/assets/icons/compute-gpu-cluster.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
||||
<!-- GPU cluster marker: database/cylinder stack icon. -->
|
||||
<!-- Color: #2dd4bf (teal) per COMPUTE_CENTER_CONFIG.colors.gpu_cluster -->
|
||||
<!-- States: normal, estimated (adds a "?" badge drawn separately at canvas level) -->
|
||||
<!-- Outer cylinder: top ellipse cap + side rect + bottom half-ellipse -->
|
||||
<!-- Inner groove ring: smaller cylinder shape overlaid at same color (subtle shape layering) -->
|
||||
<g fill="#2dd4bf">
|
||||
<rect x="46" y="46" width="36" height="28"/>
|
||||
<ellipse cx="64" cy="46" rx="18" ry="8"/>
|
||||
<path d="M 82,74 A 18,8 0 0,1 46,74 Z"/>
|
||||
|
||||
<rect x="52" y="58" width="24" height="6"/>
|
||||
<ellipse cx="64" cy="58" rx="12" ry="4.5"/>
|
||||
<path d="M 76,64 A 12,4.5 0 0,1 52,64 Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 787 B |
10
frontend/public/earth/assets/icons/compute-supercomputer.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
||||
<!-- Supercomputer marker: flat-screen monitor with neck and base stand. -->
|
||||
<!-- Color: #38bdf8 (sky-blue) per COMPUTE_CENTER_CONFIG.colors.supercomputer -->
|
||||
<!-- States: normal, estimated (adds a "?" badge drawn separately at canvas level) -->
|
||||
<g fill="#38bdf8">
|
||||
<rect x="40" y="42" width="48" height="30" rx="7"/>
|
||||
<rect x="58" y="74" width="12" height="8" rx="3"/>
|
||||
<rect x="50" y="84" width="28" height="5" rx="2.5"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 520 B |
10
frontend/public/earth/assets/icons/marker-landing-point.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="100 50 600 680">
|
||||
<!-- Location pin with circular cutout. Coordinates in 1000-unit space (canvas scale: size/1000). -->
|
||||
<!-- States: color via fill attribute — default white for 3D canvas, orange (#ffaa00) for normal, white for hover/locked -->
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
fill="currentColor"
|
||||
d="M400 704 C386 704 375 697 367 684 L173 378 C117 290 144 173 229 111 C278 75 337 57 400 57 C463 57 522 75 571 111 C656 173 683 290 627 378 L433 684 C425 697 414 704 400 704 Z
|
||||
M400 320 m-86 0 a86 86 0 1 0 172 0 a86 86 0 1 0 -172 0"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 611 B |
@@ -20,7 +20,56 @@ export let lockedCable = null;
|
||||
let cableIdMap = new Map();
|
||||
let cableStates = new Map();
|
||||
let cablesVisible = true;
|
||||
let landingPointGeometry = null;
|
||||
let landingPointTexture = null;
|
||||
const _lpWorldPos = new THREE.Vector3();
|
||||
|
||||
function createLandingPointTexture() {
|
||||
const size = CABLE_CONFIG.landingPoint.textureSize;
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
const ctx = canvas.getContext("2d");
|
||||
const iconPath = new Path2D(
|
||||
[
|
||||
"M400 704",
|
||||
"C386 704 375 697 367 684",
|
||||
"L173 378",
|
||||
"C117 290 144 173 229 111",
|
||||
"C278 75 337 57 400 57",
|
||||
"C463 57 522 75 571 111",
|
||||
"C656 173 683 290 627 378",
|
||||
"L433 684",
|
||||
"C425 697 414 704 400 704",
|
||||
"Z",
|
||||
].join(" "),
|
||||
);
|
||||
|
||||
ctx.clearRect(0, 0, size, size);
|
||||
ctx.save();
|
||||
ctx.translate(size * 0.12, size * 0.02);
|
||||
ctx.scale(size / 1000, size / 1000);
|
||||
|
||||
ctx.fillStyle = "#ffffff";
|
||||
ctx.fill(iconPath);
|
||||
|
||||
ctx.globalCompositeOperation = "destination-out";
|
||||
ctx.beginPath();
|
||||
ctx.arc(400, 320, 86, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
|
||||
const texture = new THREE.CanvasTexture(canvas);
|
||||
texture.colorSpace = THREE.SRGBColorSpace;
|
||||
texture.needsUpdate = true;
|
||||
return texture;
|
||||
}
|
||||
|
||||
function getLandingPointTexture() {
|
||||
if (!landingPointTexture) {
|
||||
landingPointTexture = createLandingPointTexture();
|
||||
}
|
||||
return landingPointTexture;
|
||||
}
|
||||
|
||||
function clamp(value, min, max) {
|
||||
return Math.min(max, Math.max(min, value));
|
||||
@@ -49,7 +98,7 @@ function disposeMaterial(material) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (material.map) {
|
||||
if (material.map && !material.userData?.sharedMap) {
|
||||
material.map.dispose();
|
||||
}
|
||||
material.dispose();
|
||||
@@ -69,6 +118,22 @@ function disposeObject(object, parent) {
|
||||
}
|
||||
}
|
||||
|
||||
function setLandingPointMaterialState(point, { color, opacity, emissive, emissiveIntensity }) {
|
||||
point.material.color.set(color);
|
||||
point.material.opacity = opacity;
|
||||
if (point.material.emissive && emissive !== undefined) {
|
||||
point.material.emissive.setHex(emissive);
|
||||
}
|
||||
if ("emissiveIntensity" in point.material && emissiveIntensity !== undefined) {
|
||||
point.material.emissiveIntensity = emissiveIntensity;
|
||||
}
|
||||
}
|
||||
|
||||
function setLandingPointScale(point, heightScale) {
|
||||
const aspect = CABLE_CONFIG.landingPoint.iconAspectRatio;
|
||||
point.scale.set(heightScale * aspect, heightScale, 1);
|
||||
}
|
||||
|
||||
function getCableColor(properties) {
|
||||
if (properties.color) {
|
||||
if (
|
||||
@@ -357,13 +422,6 @@ export async function loadLandingPoints(scene, earthObj, options = {}) {
|
||||
|
||||
clearLandingPoints(earthObj);
|
||||
|
||||
if (!landingPointGeometry) {
|
||||
landingPointGeometry = new THREE.SphereGeometry(
|
||||
CABLE_CONFIG.landingPoint.radius,
|
||||
CABLE_CONFIG.landingPoint.widthSegments,
|
||||
CABLE_CONFIG.landingPoint.heightSegments,
|
||||
);
|
||||
}
|
||||
let validCount = 0;
|
||||
|
||||
for (const feature of data.features) {
|
||||
@@ -396,29 +454,35 @@ export async function loadLandingPoints(scene, earthObj, options = {}) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const sphere = new THREE.Mesh(
|
||||
landingPointGeometry,
|
||||
new THREE.MeshStandardMaterial({
|
||||
const marker = new THREE.Sprite(
|
||||
new THREE.SpriteMaterial({
|
||||
map: getLandingPointTexture(),
|
||||
color: CABLE_CONFIG.landingPoint.color,
|
||||
emissive: CABLE_CONFIG.landingPoint.emissive,
|
||||
emissiveIntensity: CABLE_CONFIG.landingPoint.emissiveIntensity,
|
||||
transparent: true,
|
||||
opacity: CABLE_CONFIG.landingPoint.opacity,
|
||||
depthTest: true,
|
||||
depthWrite: false,
|
||||
}),
|
||||
);
|
||||
sphere.position.copy(position);
|
||||
sphere.userData = {
|
||||
marker.material.userData.sharedMap = true;
|
||||
marker.renderOrder = CABLE_CONFIG.landingPoint.renderOrder;
|
||||
marker.center.set(
|
||||
CABLE_CONFIG.landingPoint.anchorX,
|
||||
CABLE_CONFIG.landingPoint.anchorY,
|
||||
);
|
||||
marker.position.copy(position);
|
||||
marker.userData = {
|
||||
type: "landingPoint",
|
||||
name: properties.name || "未知登陆站",
|
||||
cableNames: properties.cable_names || [],
|
||||
country: properties.country || "未知国家",
|
||||
status: properties.status || "Unknown",
|
||||
baseScale: CABLE_CONFIG.landingPoint.baseScale,
|
||||
sharedGeometry: true,
|
||||
};
|
||||
setLandingPointScale(marker, CABLE_CONFIG.landingPoint.baseScale);
|
||||
|
||||
earthObj.add(sphere);
|
||||
landingPoints.push(sphere);
|
||||
earthObj.add(marker);
|
||||
landingPoints.push(marker);
|
||||
validCount++;
|
||||
}
|
||||
|
||||
@@ -533,6 +597,18 @@ export function getAllLandingPoints() {
|
||||
return landingPoints;
|
||||
}
|
||||
|
||||
// Hide pins within ~3° of the limb (normalised dot < 0.05) to prevent the
|
||||
// "half-clipped-into-globe" look caused by depthTest clipping the billboard
|
||||
// against closer earth-surface geometry near the horizon.
|
||||
const _FACING_DOT_MIN_SQ = 0.05 * 0.05;
|
||||
|
||||
function isFacingCamera(lp, camera) {
|
||||
lp.getWorldPosition(_lpWorldPos);
|
||||
const d = _lpWorldPos.dot(camera.position);
|
||||
if (d <= 0) return false;
|
||||
return d * d > _FACING_DOT_MIN_SQ * _lpWorldPos.lengthSq() * camera.position.lengthSq();
|
||||
}
|
||||
|
||||
export function applyLandingPointVisualState(lockedCableName, dimAll = false, camera = null) {
|
||||
const pulse =
|
||||
(Math.sin(Date.now() * CABLE_CONFIG.landingPointVisual.pulseSpeed) + 1) * 0.5;
|
||||
@@ -544,58 +620,65 @@ export function applyLandingPointVisualState(lockedCableName, dimAll = false, ca
|
||||
: [];
|
||||
|
||||
landingPoints.forEach((lp) => {
|
||||
lp.visible = cablesVisible && (camera ? isFacingCamera(lp, camera) : true);
|
||||
const isRelated =
|
||||
!dimAll &&
|
||||
Array.isArray(lp.userData.cableNames) &&
|
||||
lp.userData.cableNames.some((name) => relatedNames.includes(name));
|
||||
|
||||
if (isRelated) {
|
||||
lp.material.color.setHex(0xffd27a);
|
||||
lp.material.emissive.setHex(0x7a4a00);
|
||||
lp.material.emissiveIntensity =
|
||||
CABLE_CONFIG.landingPointVisual.related.emissiveIntensityBase +
|
||||
0.2 +
|
||||
pulse * (CABLE_CONFIG.landingPointVisual.related.emissiveIntensityPulse + 0.2);
|
||||
lp.material.opacity =
|
||||
Math.max(
|
||||
setLandingPointMaterialState(lp, {
|
||||
color: 0xffd27a,
|
||||
emissive: 0x7a4a00,
|
||||
emissiveIntensity:
|
||||
CABLE_CONFIG.landingPointVisual.related.emissiveIntensityBase +
|
||||
0.2 +
|
||||
pulse * (CABLE_CONFIG.landingPointVisual.related.emissiveIntensityPulse + 0.2),
|
||||
opacity: Math.max(
|
||||
0.92,
|
||||
CABLE_CONFIG.landingPointVisual.related.opacityBase +
|
||||
pulse * CABLE_CONFIG.landingPointVisual.related.opacityPulse,
|
||||
);
|
||||
),
|
||||
});
|
||||
const distanceScale = getLandingPointDistanceScale(lp, camera);
|
||||
const baseScale = lp.userData?.baseScale || CABLE_CONFIG.landingPoint.baseScale;
|
||||
lp.scale.setScalar(
|
||||
setLandingPointScale(
|
||||
lp,
|
||||
(CABLE_CONFIG.landingPointVisual.related.scaleBase +
|
||||
pulse * CABLE_CONFIG.landingPointVisual.related.scalePulse) *
|
||||
baseScale *
|
||||
distanceScale,
|
||||
distanceScale,
|
||||
);
|
||||
} else {
|
||||
const dimColor = CABLE_CONFIG.landingPointVisual.dimmed.colorRGB;
|
||||
const r = dimColor.r * brightness;
|
||||
const g = dimColor.g * brightness;
|
||||
const b = dimColor.b * brightness;
|
||||
lp.material.color.setRGB(r / 255, g / 255, b / 255);
|
||||
lp.material.emissive.setHex(CABLE_CONFIG.landingPointVisual.dimmed.emissive);
|
||||
lp.material.emissiveIntensity =
|
||||
CABLE_CONFIG.landingPointVisual.dimmed.emissiveIntensity;
|
||||
lp.material.opacity = CABLE_CONFIG.landingPointVisual.dimmed.opacity;
|
||||
setLandingPointMaterialState(lp, {
|
||||
color: new THREE.Color(r / 255, g / 255, b / 255),
|
||||
emissive: CABLE_CONFIG.landingPointVisual.dimmed.emissive,
|
||||
emissiveIntensity: CABLE_CONFIG.landingPointVisual.dimmed.emissiveIntensity,
|
||||
opacity: CABLE_CONFIG.landingPointVisual.dimmed.opacity,
|
||||
});
|
||||
const distanceScale = getLandingPointDistanceScale(lp, camera);
|
||||
const baseScale = lp.userData?.baseScale || CABLE_CONFIG.landingPoint.baseScale;
|
||||
lp.scale.setScalar(baseScale * distanceScale);
|
||||
setLandingPointScale(lp, baseScale * distanceScale);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function resetLandingPointVisualState(camera = null) {
|
||||
landingPoints.forEach((lp) => {
|
||||
lp.material.color.setHex(CABLE_CONFIG.landingPoint.color);
|
||||
lp.material.emissive.setHex(CABLE_CONFIG.landingPoint.emissive);
|
||||
lp.material.emissiveIntensity = CABLE_CONFIG.landingPoint.emissiveIntensity;
|
||||
lp.material.opacity = CABLE_CONFIG.landingPoint.opacity;
|
||||
lp.visible = cablesVisible && (camera ? isFacingCamera(lp, camera) : true);
|
||||
setLandingPointMaterialState(lp, {
|
||||
color: CABLE_CONFIG.landingPoint.color,
|
||||
emissive: CABLE_CONFIG.landingPoint.emissive,
|
||||
emissiveIntensity: CABLE_CONFIG.landingPoint.emissiveIntensity,
|
||||
opacity: CABLE_CONFIG.landingPoint.opacity,
|
||||
});
|
||||
const distanceScale = getLandingPointDistanceScale(lp, camera);
|
||||
const baseScale = lp.userData?.baseScale || CABLE_CONFIG.landingPoint.baseScale;
|
||||
lp.scale.setScalar(baseScale * distanceScale);
|
||||
setLandingPointScale(lp, baseScale * distanceScale);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -201,6 +201,7 @@ export const PATHS = {
|
||||
bgpApi: '/api/v1/visualization/geo/bgp-anomalies',
|
||||
bgpIncidentsApi: '/api/v1/visualization/geo/bgp-incidents',
|
||||
bgpCollectorsApi: '/api/v1/visualization/geo/bgp-collectors',
|
||||
earthSummaryApi: '/api/v1/visualization/geo/summary',
|
||||
earthClientLogsApi: '/api/v1/system/logs/earth-client',
|
||||
};
|
||||
|
||||
@@ -259,15 +260,17 @@ export const CABLE_CONFIG = {
|
||||
renderOrder: 1,
|
||||
},
|
||||
landingPoint: {
|
||||
altitudeOffset: 0.1,
|
||||
radius: 0.4,
|
||||
widthSegments: 16,
|
||||
heightSegments: 16,
|
||||
baseScale: 2.5,
|
||||
altitudeOffset: 0.48,
|
||||
textureSize: 256,
|
||||
iconAspectRatio: 0.82,
|
||||
anchorX: 0.52,
|
||||
anchorY: 0.276,
|
||||
baseScale: 12,
|
||||
color: 0xffaa00,
|
||||
emissive: 0x442200,
|
||||
emissiveIntensity: 0.5,
|
||||
opacity: 1.0,
|
||||
renderOrder: 4.5,
|
||||
},
|
||||
landingPointSizeStabilization: {
|
||||
enabled: true,
|
||||
@@ -277,7 +280,7 @@ export const CABLE_CONFIG = {
|
||||
},
|
||||
landingPointVisual: {
|
||||
pulseSpeed: 0.003,
|
||||
dimBrightness: 0.3,
|
||||
dimBrightness: 0.62,
|
||||
related: {
|
||||
emissiveIntensityBase: 0.5,
|
||||
emissiveIntensityPulse: 0.5,
|
||||
@@ -287,10 +290,10 @@ export const CABLE_CONFIG = {
|
||||
scalePulse: 0.3,
|
||||
},
|
||||
dimmed: {
|
||||
colorRGB: { r: 255, g: 170, b: 0 },
|
||||
emissive: 0x000000,
|
||||
emissiveIntensity: 0,
|
||||
opacity: 0.3,
|
||||
colorRGB: { r: 180, g: 116, b: 28 },
|
||||
emissive: 0x3a2200,
|
||||
emissiveIntensity: 0.18,
|
||||
opacity: 0.78,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
5
frontend/public/earth/js/controls.js
vendored
@@ -915,7 +915,10 @@ function syncEarthSettingsStateFromRuntime() {
|
||||
const scope = getSettingsViewportScope();
|
||||
|
||||
nextSettings.shared = getCurrentSharedSettingsSnapshot();
|
||||
nextSettings.views[scope].panelVisibility = getCurrentPanelVisibilitySnapshot();
|
||||
// panelVisibility is maintained in earthSettingsState via setHudPanelVisibility.
|
||||
// Do not re-snapshot from DOM here: transient hides (e.g. closeTransientMobileOverlays)
|
||||
// change the DOM without going through setHudPanelVisibility and would corrupt the
|
||||
// user's persisted preference.
|
||||
earthSettingsState = nextSettings;
|
||||
return nextSettings;
|
||||
}
|
||||
|
||||
@@ -260,6 +260,7 @@ let calloutConnector = null;
|
||||
let cruiseBGPAdapter = null;
|
||||
let cruiseNewsAdapter = null;
|
||||
let cruiseSequencer = null;
|
||||
let earthStatsSummary = null;
|
||||
let activeDragPointerId = null;
|
||||
let activeTouchPoints = new Map();
|
||||
let pinchGesture = null;
|
||||
@@ -1246,6 +1247,61 @@ function getBGPStatusText(bgpResult) {
|
||||
return "当前无活跃事件";
|
||||
}
|
||||
|
||||
function toCount(value) {
|
||||
const count = Number(value);
|
||||
return Number.isFinite(count) ? count : 0;
|
||||
}
|
||||
|
||||
function formatBGPStatusFromSummary(summary) {
|
||||
if (!summary) return "-";
|
||||
if (summary.bgpIncidentCount > 0) {
|
||||
return `${summary.bgpIncidentCount} 起活跃事件`;
|
||||
}
|
||||
if (summary.bgpAnomalyCount > 0) {
|
||||
return `${summary.bgpAnomalyCount} 条活跃异常`;
|
||||
}
|
||||
return "当前无活跃事件";
|
||||
}
|
||||
|
||||
function applyEarthStatsSummary(summary) {
|
||||
if (!summary) return;
|
||||
updateEarthStats({
|
||||
cableCount: `${summary.cableCount}个`,
|
||||
landingPointCount: `${summary.landingPointCount}个`,
|
||||
satelliteCount: `${summary.satelliteCount} 颗`,
|
||||
computeCenterCount: `${summary.computeCenterCount} 个`,
|
||||
bgpAnomalyCount: `${summary.bgpEventCount} 起`,
|
||||
bgpCollectorCount: `${summary.bgpCollectorCount} 个`,
|
||||
bgpStatusSummary: formatBGPStatusFromSummary(summary),
|
||||
terrainOn: getShowTerrain(),
|
||||
textureQuality: "8K 卫星图",
|
||||
});
|
||||
}
|
||||
|
||||
async function loadEarthStatsSummary() {
|
||||
try {
|
||||
const response = await fetch(PATHS.earthSummaryApi);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Earth summary HTTP ${response.status}`);
|
||||
}
|
||||
const payload = await response.json();
|
||||
const stats = payload?.stats || {};
|
||||
earthStatsSummary = {
|
||||
cableCount: toCount(stats.cable_count),
|
||||
landingPointCount: toCount(stats.landing_point_count),
|
||||
satelliteCount: toCount(stats.satellite_count),
|
||||
computeCenterCount: toCount(stats.compute_center_count),
|
||||
bgpEventCount: toCount(stats.bgp_event_count),
|
||||
bgpIncidentCount: toCount(stats.bgp_incident_count),
|
||||
bgpAnomalyCount: toCount(stats.bgp_anomaly_count),
|
||||
bgpCollectorCount: toCount(stats.bgp_collector_count),
|
||||
};
|
||||
applyEarthStatsSummary(earthStatsSummary);
|
||||
} catch (error) {
|
||||
console.warn("全球态势聚合统计加载失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function updateComputeCenterHud(computeCenterResult) {
|
||||
const computeBtn = document.getElementById("toggle-compute-centers");
|
||||
if (computeBtn) {
|
||||
@@ -1885,7 +1941,8 @@ function updateSatelliteToggleUi(enabled, satelliteCount = getSatelliteCount())
|
||||
});
|
||||
}
|
||||
|
||||
setEarthStatValue("satellite-count", `${satelliteCount} 颗`);
|
||||
const resolvedCount = satelliteCount || earthStatsSummary?.satelliteCount || 0;
|
||||
setEarthStatValue("satellite-count", `${resolvedCount} 颗`);
|
||||
}
|
||||
|
||||
function updateCableToggleUi(enabled) {
|
||||
@@ -1898,8 +1955,11 @@ function updateCableToggleUi(enabled) {
|
||||
});
|
||||
}
|
||||
|
||||
setEarthStatValue("cable-count", `${getCableLines().length}个`);
|
||||
setEarthStatValue("landing-point-count", `${getLandingPoints().length}个`);
|
||||
const cableCount = getCableLines().length || earthStatsSummary?.cableCount || 0;
|
||||
const landingPointCount =
|
||||
getLandingPoints().length || earthStatsSummary?.landingPointCount || 0;
|
||||
setEarthStatValue("cable-count", `${cableCount}个`);
|
||||
setEarthStatValue("landing-point-count", `${landingPointCount}个`);
|
||||
}
|
||||
|
||||
async function ensureCablesEnabled() {
|
||||
@@ -2015,13 +2075,25 @@ function disableSatellites() {
|
||||
}
|
||||
|
||||
function updateStatsSummary() {
|
||||
const cableCount = getCableLines().length || earthStatsSummary?.cableCount || 0;
|
||||
const landingPointCount =
|
||||
getLandingPoints().length || earthStatsSummary?.landingPointCount || 0;
|
||||
const satelliteCount = getSatelliteCount() || earthStatsSummary?.satelliteCount || 0;
|
||||
const computeCenterCount =
|
||||
getComputeCenterCount() || earthStatsSummary?.computeCenterCount || 0;
|
||||
const bgpEventCount = getBGPCount() || earthStatsSummary?.bgpEventCount || 0;
|
||||
const bgpCollectorCount =
|
||||
getBGPCollectorCount() || earthStatsSummary?.bgpCollectorCount || 0;
|
||||
updateEarthStats({
|
||||
cableCount: getCableLines().length,
|
||||
landingPointCount: getLandingPoints().length,
|
||||
computeCenterCount: `${getComputeCenterCount()} 个`,
|
||||
bgpAnomalyCount: `${getBGPCount()} 条`,
|
||||
bgpCollectorCount: `${getBGPCollectorCount()} 个`,
|
||||
bgpStatusSummary: getBGPStatusSummary(),
|
||||
cableCount: `${cableCount}个`,
|
||||
landingPointCount: `${landingPointCount}个`,
|
||||
satelliteCount: `${satelliteCount} 颗`,
|
||||
computeCenterCount: `${computeCenterCount} 个`,
|
||||
bgpAnomalyCount: `${bgpEventCount} 起`,
|
||||
bgpCollectorCount: `${bgpCollectorCount} 个`,
|
||||
bgpStatusSummary: getBGPCount()
|
||||
? getBGPStatusSummary()
|
||||
: formatBGPStatusFromSummary(earthStatsSummary),
|
||||
terrainOn: getShowTerrain(),
|
||||
textureQuality: "8K 卫星图",
|
||||
});
|
||||
@@ -2269,6 +2341,11 @@ async function loadData() {
|
||||
await yieldFrame(18);
|
||||
if (loadToken !== currentLoadToken) { isDataLoading = false; return; }
|
||||
|
||||
setLoadingMessage("正在读取全球态势统计...");
|
||||
await loadEarthStatsSummary();
|
||||
if (loadToken !== currentLoadToken) { isDataLoading = false; return; }
|
||||
await yieldFrame(12);
|
||||
|
||||
const errors = [];
|
||||
|
||||
// Step 1 — Earth texture
|
||||
|
||||
@@ -185,14 +185,22 @@ export function updateZoomDisplay(zoomLevel, distance) {
|
||||
|
||||
// Update earth stats
|
||||
export function updateEarthStats(stats) {
|
||||
setEarthStatValue("cable-count", String(stats.cableCount || 0));
|
||||
setEarthStatValue("landing-point-count", String(stats.landingPointCount || 0));
|
||||
setEarthStatValue("compute-center-count", String(stats.computeCenterCount || 0));
|
||||
setEarthStatValue("bgp-anomaly-count", String(stats.bgpAnomalyCount || 0));
|
||||
setEarthStatValue("bgp-collector-count", String(stats.bgpCollectorCount || 0));
|
||||
setEarthStatValue("bgp-status-summary", stats.bgpStatusSummary || "-");
|
||||
setEarthStatValue("terrain-status", stats.terrainOn ? "开启" : "关闭");
|
||||
setEarthStatValue("texture-quality", stats.textureQuality || "8K 卫星图");
|
||||
const has = (key) => Object.prototype.hasOwnProperty.call(stats, key);
|
||||
if (has("cableCount")) setEarthStatValue("cable-count", String(stats.cableCount || 0));
|
||||
if (has("landingPointCount")) {
|
||||
setEarthStatValue("landing-point-count", String(stats.landingPointCount || 0));
|
||||
}
|
||||
if (has("satelliteCount")) setEarthStatValue("satellite-count", String(stats.satelliteCount || 0));
|
||||
if (has("computeCenterCount")) {
|
||||
setEarthStatValue("compute-center-count", String(stats.computeCenterCount || 0));
|
||||
}
|
||||
if (has("bgpAnomalyCount")) setEarthStatValue("bgp-anomaly-count", String(stats.bgpAnomalyCount || 0));
|
||||
if (has("bgpCollectorCount")) {
|
||||
setEarthStatValue("bgp-collector-count", String(stats.bgpCollectorCount || 0));
|
||||
}
|
||||
if (has("bgpStatusSummary")) setEarthStatValue("bgp-status-summary", stats.bgpStatusSummary || "-");
|
||||
if (has("terrainOn")) setEarthStatValue("terrain-status", stats.terrainOn ? "开启" : "关闭");
|
||||
if (has("textureQuality")) setEarthStatValue("texture-quality", stats.textureQuality || "8K 卫星图");
|
||||
}
|
||||
|
||||
// Show/hide loading via status message
|
||||
|
||||