fix: polish earth legend and info panel interactions
This commit is contained in:
@@ -14,14 +14,34 @@
|
||||
margin-bottom: 5px;
|
||||
color: #4db8ff;
|
||||
text-shadow: 0 0 10px rgba(77, 184, 255, 0.5);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#info-panel .subtitle {
|
||||
color: #aaa;
|
||||
margin-bottom: 20px;
|
||||
font-size: 0.9rem;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
padding-bottom: 10px;
|
||||
padding-bottom: 12px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
#info-panel .subtitle-main {
|
||||
color: #d7e7f5;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.35;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
#info-panel .subtitle-meta {
|
||||
color: #8ea5bc;
|
||||
font-size: 0.74rem;
|
||||
line-height: 1.3;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
#info-panel .cable-info {
|
||||
|
||||
@@ -19,6 +19,24 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
max-height: 202px;
|
||||
overflow-y: auto;
|
||||
padding-right: 4px;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(160, 220, 255, 0.4) transparent;
|
||||
}
|
||||
|
||||
#legend .legend-list::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
#legend .legend-list::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#legend .legend-list::-webkit-scrollbar-thumb {
|
||||
background: linear-gradient(180deg, rgba(210, 237, 255, 0.28), rgba(110, 176, 255, 0.34));
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
#legend .legend-item {
|
||||
|
||||
@@ -33,7 +33,10 @@
|
||||
<div id="container">
|
||||
<div id="info-panel">
|
||||
<h1>智能星球计划</h1>
|
||||
<div class="subtitle">现实层宇宙全息感知系统 | 卫星 · 海底光缆 · 算力基础设施</div>
|
||||
<div class="subtitle">
|
||||
<span class="subtitle-main">现实层宇宙全息感知系统</span>
|
||||
<span class="subtitle-meta">卫星 · 海底光缆 · 算力基础设施</span>
|
||||
</div>
|
||||
|
||||
<div id="info-card" class="info-card" style="display: none;">
|
||||
<div class="info-card-header">
|
||||
@@ -90,6 +93,12 @@
|
||||
</span>
|
||||
<span class="tooltip">显示卫星</span>
|
||||
</button>
|
||||
<button id="toggle-bgp" class="toolbar-btn floating-btn liquid-glass-surface active" title="显示/隐藏BGP观测">
|
||||
<span class="icon" aria-hidden="true">
|
||||
<span class="material-symbols-rounded">radar</span>
|
||||
</span>
|
||||
<span class="tooltip">隐藏BGP观测</span>
|
||||
</button>
|
||||
<button id="toggle-cables" class="toolbar-btn floating-btn liquid-glass-surface active" title="显示/隐藏线缆">
|
||||
<span class="icon" aria-hidden="true">
|
||||
<span class="material-symbols-rounded">cable</span>
|
||||
@@ -194,6 +203,10 @@
|
||||
<span class="stats-label">卫星:</span>
|
||||
<span class="stats-value" id="satellite-count">0 颗</span>
|
||||
</div>
|
||||
<div class="stats-item">
|
||||
<span class="stats-label">BGP异常:</span>
|
||||
<span class="stats-value" id="bgp-anomaly-count">0 条</span>
|
||||
</div>
|
||||
<div class="stats-item">
|
||||
<span class="stats-label">视角距离:</span>
|
||||
<span class="stats-value" id="camera-distance">300 km</span>
|
||||
@@ -207,7 +220,7 @@
|
||||
<div id="loading">
|
||||
<div id="loading-spinner"></div>
|
||||
<div id="loading-title">正在初始化全球态势数据...</div>
|
||||
<div id="loading-subtitle" style="font-size:0.9rem; margin-top:10px; color:#aaa;">同步卫星、海底光缆与登陆点数据</div>
|
||||
<div id="loading-subtitle" style="font-size:0.9rem; margin-top:10px; color:#aaa;">同步卫星、海底光缆、登陆点与BGP异常数据</div>
|
||||
</div>
|
||||
<div id="status-message" class="status-message" style="display: none;"></div>
|
||||
<div id="tooltip" class="tooltip"></div>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { CONFIG, CABLE_COLORS, PATHS, CABLE_STATE } from "./constants.js";
|
||||
import { latLonToVector3 } from "./utils.js";
|
||||
import { updateEarthStats, showStatusMessage } from "./ui.js";
|
||||
import { showInfoCard } from "./info-card.js";
|
||||
import { setLegendMode } from "./legend.js";
|
||||
import { setLegendItems, setLegendMode } from "./legend.js";
|
||||
|
||||
export let cableLines = [];
|
||||
export let landingPoints = [];
|
||||
@@ -57,7 +57,11 @@ function getCableColor(properties) {
|
||||
}
|
||||
|
||||
const cableName =
|
||||
properties.Name || properties.cableName || properties.shortname || "";
|
||||
properties.Name ||
|
||||
properties.name ||
|
||||
properties.cableName ||
|
||||
properties.shortname ||
|
||||
"";
|
||||
if (cableName.includes("Americas II")) {
|
||||
return CABLE_COLORS["Americas II"];
|
||||
}
|
||||
@@ -91,11 +95,17 @@ function createCableLine(points, color, properties) {
|
||||
properties.cable_id ||
|
||||
properties.id ||
|
||||
properties.Name ||
|
||||
properties.name ||
|
||||
Math.random().toString(36);
|
||||
cableLine.userData = {
|
||||
type: "cable",
|
||||
cableId,
|
||||
name: properties.Name || properties.cableName || "Unknown",
|
||||
name:
|
||||
properties.Name ||
|
||||
properties.name ||
|
||||
properties.cableName ||
|
||||
properties.shortname ||
|
||||
"Unknown",
|
||||
owner: properties.owner || properties.owners || "-",
|
||||
status: properties.status || "-",
|
||||
length: properties.length || "-",
|
||||
@@ -382,6 +392,7 @@ export async function loadLandingPoints(scene, earthObj) {
|
||||
|
||||
export function handleCableClick(cable) {
|
||||
lockedCable = cable;
|
||||
setLegendItems("cables", getCableLegendItems());
|
||||
|
||||
const data = cable.userData;
|
||||
setLegendMode("cables");
|
||||
@@ -399,12 +410,51 @@ export function handleCableClick(cable) {
|
||||
|
||||
export function clearCableSelection() {
|
||||
lockedCable = null;
|
||||
setLegendItems("cables", getCableLegendItems());
|
||||
}
|
||||
|
||||
export function getCableLines() {
|
||||
return cableLines;
|
||||
}
|
||||
|
||||
export function getCableLegendItems() {
|
||||
const legendMap = new Map();
|
||||
|
||||
cableLines.forEach((cable) => {
|
||||
const color = cable.userData?.originalColor;
|
||||
const label = cable.userData?.name || "未知线缆";
|
||||
|
||||
if (typeof color === "number" && !legendMap.has(label)) {
|
||||
legendMap.set(label, {
|
||||
label,
|
||||
color: `#${color.toString(16).padStart(6, "0")}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (legendMap.size === 0) {
|
||||
return [{ label: "其他电缆", color: "#ffff44" }];
|
||||
}
|
||||
|
||||
const items = Array.from(legendMap.values()).sort((a, b) =>
|
||||
a.label.localeCompare(b.label, "zh-CN"),
|
||||
);
|
||||
|
||||
const selectedName = lockedCable?.userData?.name;
|
||||
if (!selectedName) {
|
||||
return items;
|
||||
}
|
||||
|
||||
const selectedIndex = items.findIndex((item) => item.label === selectedName);
|
||||
if (selectedIndex <= 0) {
|
||||
return items;
|
||||
}
|
||||
|
||||
const [selectedItem] = items.splice(selectedIndex, 1);
|
||||
items.unshift(selectedItem);
|
||||
return items;
|
||||
}
|
||||
|
||||
export function getCablesById(cableId) {
|
||||
return cableIdMap.get(cableId) || [];
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ export const EARTH_CONFIG = {
|
||||
export const PATHS = {
|
||||
cablesApi: '/api/v1/visualization/geo/cables',
|
||||
landingPointsApi: '/api/v1/visualization/geo/landing-points',
|
||||
bgpApi: '/api/v1/visualization/geo/bgp-anomalies',
|
||||
geoJSON: './geo.json',
|
||||
landingPointsStatic: './landing-point-geo.geojson',
|
||||
};
|
||||
@@ -69,6 +70,37 @@ export const SATELLITE_CONFIG = {
|
||||
dotOpacityMax: 1.0
|
||||
};
|
||||
|
||||
export const BGP_CONFIG = {
|
||||
defaultFetchLimit: 200,
|
||||
maxRenderedMarkers: 200,
|
||||
altitudeOffset: 1.2,
|
||||
baseScale: 6.2,
|
||||
hoverScale: 1.16,
|
||||
dimmedScale: 0.92,
|
||||
pulseSpeed: 0.0045,
|
||||
normalPulseAmplitude: 0.08,
|
||||
lockedPulseAmplitude: 0.28,
|
||||
opacity: {
|
||||
normal: 0.78,
|
||||
hover: 1.0,
|
||||
dimmed: 0.24,
|
||||
lockedMin: 0.65,
|
||||
lockedMax: 1.0
|
||||
},
|
||||
severityColors: {
|
||||
critical: 0xff4d4f,
|
||||
high: 0xff9f43,
|
||||
medium: 0xffd166,
|
||||
low: 0x4dabf7
|
||||
},
|
||||
severityScales: {
|
||||
critical: 1.18,
|
||||
high: 1.08,
|
||||
medium: 1.0,
|
||||
low: 0.94
|
||||
}
|
||||
};
|
||||
|
||||
export const PREDICTED_ORBIT_CONFIG = {
|
||||
sampleInterval: 10,
|
||||
opacity: 0.8
|
||||
|
||||
18
frontend/public/earth/js/controls.js
vendored
18
frontend/public/earth/js/controls.js
vendored
@@ -11,6 +11,7 @@ import {
|
||||
getSatelliteCount,
|
||||
} from "./satellites.js";
|
||||
import { toggleCables, getShowCables } from "./cables.js";
|
||||
import { toggleBGP, getShowBGP, getBGPCount } from "./bgp.js";
|
||||
|
||||
export let autoRotate = true;
|
||||
export let zoomLevel = 1.0;
|
||||
@@ -293,6 +294,7 @@ function setupTerrainControls() {
|
||||
const infoTrigger = document.getElementById("info-trigger");
|
||||
const terrainBtn = document.getElementById("toggle-terrain");
|
||||
const satellitesBtn = document.getElementById("toggle-satellites");
|
||||
const bgpBtn = document.getElementById("toggle-bgp");
|
||||
const trailsBtn = document.getElementById("toggle-trails");
|
||||
const cablesBtn = document.getElementById("toggle-cables");
|
||||
const layoutBtn = document.getElementById("layout-toggle");
|
||||
@@ -337,6 +339,22 @@ function setupTerrainControls() {
|
||||
showStatusMessage(showSats ? "卫星已显示" : "卫星已隐藏", "info");
|
||||
});
|
||||
|
||||
bindListener(bgpBtn, "click", function () {
|
||||
const showNextBGP = !getShowBGP();
|
||||
if (!showNextBGP) {
|
||||
clearLockedObject();
|
||||
}
|
||||
toggleBGP(showNextBGP);
|
||||
this.classList.toggle("active", showNextBGP);
|
||||
const tooltip = this.querySelector(".tooltip");
|
||||
if (tooltip) tooltip.textContent = showNextBGP ? "隐藏BGP观测" : "显示BGP观测";
|
||||
const bgpCountEl = document.getElementById("bgp-anomaly-count");
|
||||
if (bgpCountEl) {
|
||||
bgpCountEl.textContent = `${getBGPCount()} 条`;
|
||||
}
|
||||
showStatusMessage(showNextBGP ? "BGP观测已显示" : "BGP观测已隐藏", "info");
|
||||
});
|
||||
|
||||
bindListener(trailsBtn, "click", function () {
|
||||
const isActive = this.classList.contains("active");
|
||||
const nextShowTrails = !isActive;
|
||||
|
||||
@@ -30,6 +30,25 @@ const CARD_CONFIG = {
|
||||
{ key: 'apogee', label: '远地点', unit: 'km' }
|
||||
]
|
||||
},
|
||||
bgp: {
|
||||
icon: '📡',
|
||||
title: 'BGP异常详情',
|
||||
className: 'bgp',
|
||||
fields: [
|
||||
{ key: 'anomaly_type', label: '异常类型' },
|
||||
{ key: 'severity', label: '严重度' },
|
||||
{ key: 'status', label: '状态' },
|
||||
{ key: 'prefix', label: '前缀' },
|
||||
{ key: 'origin_asn', label: '原始 ASN' },
|
||||
{ key: 'new_origin_asn', label: '新 ASN' },
|
||||
{ key: 'confidence', label: '置信度' },
|
||||
{ key: 'collector', label: '采集器' },
|
||||
{ key: 'country', label: '国家' },
|
||||
{ key: 'city', label: '城市' },
|
||||
{ key: 'created_at', label: '创建时间' },
|
||||
{ key: 'summary', label: '摘要' }
|
||||
]
|
||||
},
|
||||
supercomputer: {
|
||||
icon: '🖥️',
|
||||
title: '超算详情',
|
||||
|
||||
@@ -1,25 +1,21 @@
|
||||
const LEGEND_MODES = {
|
||||
cables: {
|
||||
title: "线缆图例",
|
||||
items: [
|
||||
{ color: "#ff4444", label: "Americas II" },
|
||||
{ color: "#44ff44", label: "AU Aleutian A" },
|
||||
{ color: "#4444ff", label: "AU Aleutian B" },
|
||||
{ color: "#ffff44", label: "其他电缆" },
|
||||
],
|
||||
},
|
||||
satellites: {
|
||||
title: "卫星图例",
|
||||
items: [
|
||||
{ color: "#4db8ff", label: "卫星本体" },
|
||||
{ color: "#9be7ff", label: "卫星轨迹" },
|
||||
{ color: "#7dffb3", label: "悬停高亮" },
|
||||
{ color: "#ffd166", label: "选中目标" },
|
||||
],
|
||||
},
|
||||
bgp: {
|
||||
title: "BGP观测图例",
|
||||
},
|
||||
};
|
||||
|
||||
let currentLegendMode = "cables";
|
||||
let legendItemsByMode = {
|
||||
cables: [],
|
||||
satellites: [],
|
||||
bgp: [],
|
||||
};
|
||||
|
||||
export function initLegend() {
|
||||
renderLegend(currentLegendMode);
|
||||
@@ -27,7 +23,6 @@ export function initLegend() {
|
||||
|
||||
export function setLegendMode(mode) {
|
||||
const nextMode = LEGEND_MODES[mode] ? mode : "cables";
|
||||
if (nextMode === currentLegendMode) return;
|
||||
currentLegendMode = nextMode;
|
||||
renderLegend(currentLegendMode);
|
||||
}
|
||||
@@ -36,12 +31,25 @@ export function getLegendMode() {
|
||||
return currentLegendMode;
|
||||
}
|
||||
|
||||
export function refreshLegend() {
|
||||
renderLegend(currentLegendMode);
|
||||
}
|
||||
|
||||
export function setLegendItems(mode, items) {
|
||||
if (!LEGEND_MODES[mode]) return;
|
||||
legendItemsByMode[mode] = Array.isArray(items) ? items : [];
|
||||
if (mode === currentLegendMode) {
|
||||
renderLegend(currentLegendMode);
|
||||
}
|
||||
}
|
||||
|
||||
function renderLegend(mode) {
|
||||
const legend = document.getElementById("legend");
|
||||
if (!legend) return;
|
||||
|
||||
const config = LEGEND_MODES[mode] || LEGEND_MODES.cables;
|
||||
const itemsHtml = config.items
|
||||
const items = legendItemsByMode[mode] || [];
|
||||
const itemsHtml = items
|
||||
.map(
|
||||
(item) => `
|
||||
<div class="legend-item">
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
handleCableClick,
|
||||
clearCableSelection,
|
||||
getCableLines,
|
||||
getCableLegendItems,
|
||||
getCableState,
|
||||
setCableState,
|
||||
clearAllCableStates,
|
||||
@@ -44,6 +45,9 @@ import {
|
||||
updateSatellitePositions,
|
||||
toggleSatellites,
|
||||
getShowSatellites,
|
||||
getSatelliteLegendItems,
|
||||
setSelectedSatelliteLegend,
|
||||
clearSelectedSatelliteLegend,
|
||||
getSatelliteCount,
|
||||
selectSatellite,
|
||||
getSatellitePoints,
|
||||
@@ -60,6 +64,18 @@ import {
|
||||
resetSatelliteState,
|
||||
clearSatelliteData,
|
||||
} from "./satellites.js";
|
||||
import {
|
||||
loadBGPAnomalies,
|
||||
getBGPMarkers,
|
||||
getBGPLegendItems,
|
||||
getBGPCount,
|
||||
getShowBGP,
|
||||
clearBGPSelection,
|
||||
setBGPMarkerState,
|
||||
updateBGPVisualState,
|
||||
clearBGPData,
|
||||
toggleBGP,
|
||||
} from "./bgp.js";
|
||||
import {
|
||||
setupControls,
|
||||
getAutoRotate,
|
||||
@@ -74,7 +90,12 @@ import {
|
||||
hideInfoCard,
|
||||
setInfoCardNoBorder,
|
||||
} from "./info-card.js";
|
||||
import { initLegend, setLegendMode } from "./legend.js";
|
||||
import {
|
||||
initLegend,
|
||||
setLegendMode,
|
||||
refreshLegend,
|
||||
setLegendItems,
|
||||
} from "./legend.js";
|
||||
|
||||
export let scene;
|
||||
export let camera;
|
||||
@@ -86,6 +107,7 @@ let previousMousePosition = { x: 0, y: 0 };
|
||||
let targetRotation = { x: 0, y: 0 };
|
||||
let inertialVelocity = { x: 0, y: 0 };
|
||||
let hoveredCable = null;
|
||||
let hoveredBGP = null;
|
||||
let hoveredSatellite = null;
|
||||
let hoveredSatelliteIndex = null;
|
||||
let lockedSatellite = null;
|
||||
@@ -110,6 +132,7 @@ const interactionMouse = new THREE.Vector2();
|
||||
const scratchCameraToEarth = new THREE.Vector3();
|
||||
const scratchCableCenter = new THREE.Vector3();
|
||||
const scratchCableDirection = new THREE.Vector3();
|
||||
const scratchBGPDirection = new THREE.Vector3();
|
||||
|
||||
const cleanupFns = [];
|
||||
const DRAG_ROTATION_FACTOR = 0.005;
|
||||
@@ -169,6 +192,7 @@ function disposeSceneObject(object) {
|
||||
|
||||
function clearRuntimeSelection() {
|
||||
hoveredCable = null;
|
||||
hoveredBGP = null;
|
||||
hoveredSatellite = null;
|
||||
hoveredSatelliteIndex = null;
|
||||
lockedObject = null;
|
||||
@@ -176,14 +200,17 @@ function clearRuntimeSelection() {
|
||||
lockedSatellite = null;
|
||||
lockedSatelliteIndex = null;
|
||||
setLockedSatelliteIndex(null);
|
||||
clearSelectedSatelliteLegend();
|
||||
}
|
||||
|
||||
export function clearLockedObject() {
|
||||
hidePredictedOrbit();
|
||||
clearAllCableStates();
|
||||
clearCableSelection();
|
||||
clearBGPSelection();
|
||||
setSatelliteRingState(null, "none", null);
|
||||
clearRuntimeSelection();
|
||||
setLegendItems("satellites", getSatelliteLegendItems());
|
||||
}
|
||||
|
||||
function isSameCable(cable1, cable2) {
|
||||
@@ -213,6 +240,8 @@ function showSatelliteInfo(props) {
|
||||
const perigee = (6371 * (1 - ecc)).toFixed(0);
|
||||
const apogee = (6371 * (1 + ecc)).toFixed(0);
|
||||
|
||||
setSelectedSatelliteLegend(props);
|
||||
setLegendItems("satellites", getSatelliteLegendItems());
|
||||
setLegendMode("satellites");
|
||||
showInfoCard("satellite", {
|
||||
name: props?.name || "-",
|
||||
@@ -224,6 +253,24 @@ function showSatelliteInfo(props) {
|
||||
});
|
||||
}
|
||||
|
||||
function showBGPInfo(marker) {
|
||||
setLegendMode("bgp");
|
||||
showInfoCard("bgp", {
|
||||
anomaly_type: marker.userData.anomaly_type,
|
||||
severity: marker.userData.rawSeverity || marker.userData.severity,
|
||||
status: marker.userData.status,
|
||||
prefix: marker.userData.prefix,
|
||||
origin_asn: marker.userData.origin_asn,
|
||||
new_origin_asn: marker.userData.new_origin_asn,
|
||||
confidence: marker.userData.confidence,
|
||||
collector: marker.userData.collector,
|
||||
country: marker.userData.country,
|
||||
city: marker.userData.city,
|
||||
created_at: marker.userData.created_at,
|
||||
summary: marker.userData.summary,
|
||||
});
|
||||
}
|
||||
|
||||
function applyCableVisualState() {
|
||||
const allCables = getCableLines();
|
||||
const pulse = (Math.sin(Date.now() * CABLE_CONFIG.pulseSpeed) + 1) * 0.5;
|
||||
@@ -248,7 +295,8 @@ function applyCableVisualState() {
|
||||
default:
|
||||
if (
|
||||
(lockedObjectType === "cable" && lockedObject) ||
|
||||
(lockedObjectType === "satellite" && lockedSatellite)
|
||||
(lockedObjectType === "satellite" && lockedSatellite) ||
|
||||
(lockedObjectType === "bgp" && lockedObject)
|
||||
) {
|
||||
cable.material.opacity = CABLE_CONFIG.otherOpacity;
|
||||
const origColor = cable.userData.originalColor;
|
||||
@@ -289,6 +337,7 @@ function updateStatsSummary() {
|
||||
cableCount: getCableLines().length,
|
||||
landingPointCount:
|
||||
document.getElementById("landing-point-count")?.textContent || 0,
|
||||
bgpAnomalyCount: `${getBGPCount()} 条`,
|
||||
terrainOn: getShowTerrain(),
|
||||
textureQuality: "8K 卫星图",
|
||||
});
|
||||
@@ -337,6 +386,9 @@ export function init() {
|
||||
addLights();
|
||||
initInfoCard();
|
||||
initLegend();
|
||||
setLegendItems("cables", getCableLegendItems());
|
||||
setLegendItems("satellites", getSatelliteLegendItems());
|
||||
setLegendItems("bgp", getBGPLegendItems());
|
||||
const earthObj = createEarth(scene);
|
||||
targetRotation = {
|
||||
x: earthObj.rotation.x,
|
||||
@@ -400,8 +452,8 @@ async function loadData(showWhiteSphere = false) {
|
||||
setLoadingMessage(
|
||||
showWhiteSphere ? "正在刷新全球态势数据..." : "正在初始化全球态势数据...",
|
||||
showWhiteSphere
|
||||
? "重新同步卫星、海底光缆与登陆点数据"
|
||||
: "同步卫星、海底光缆与登陆点数据",
|
||||
? "重新同步卫星、海底光缆、登陆点与BGP异常数据"
|
||||
: "同步卫星、海底光缆、登陆点与BGP异常数据",
|
||||
);
|
||||
setLoading(true);
|
||||
clearLockedObject();
|
||||
@@ -434,6 +486,22 @@ async function loadData(showWhiteSphere = false) {
|
||||
}
|
||||
return satelliteCount;
|
||||
})(),
|
||||
(async () => {
|
||||
clearBGPData(earth);
|
||||
const bgpResult = await loadBGPAnomalies(scene, earth);
|
||||
toggleBGP(true);
|
||||
const bgpBtn = document.getElementById("toggle-bgp");
|
||||
if (bgpBtn) {
|
||||
bgpBtn.classList.add("active");
|
||||
const tooltip = bgpBtn.querySelector(".tooltip");
|
||||
if (tooltip) tooltip.textContent = "隐藏BGP观测";
|
||||
}
|
||||
const bgpCountEl = document.getElementById("bgp-anomaly-count");
|
||||
if (bgpCountEl) {
|
||||
bgpCountEl.textContent = `${bgpResult.totalCount} 条`;
|
||||
}
|
||||
return bgpResult;
|
||||
})(),
|
||||
]);
|
||||
|
||||
if (loadToken !== currentLoadToken) {
|
||||
@@ -451,6 +519,9 @@ async function loadData(showWhiteSphere = false) {
|
||||
if (results[2].status === "rejected") {
|
||||
errors.push({ label: "卫星", reason: results[2].reason });
|
||||
}
|
||||
if (results[3].status === "rejected") {
|
||||
errors.push({ label: "BGP异常", reason: results[3].reason });
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
const errorMessage = buildLoadErrorMessage(errors);
|
||||
@@ -462,6 +533,10 @@ async function loadData(showWhiteSphere = false) {
|
||||
}
|
||||
|
||||
updateStatsSummary();
|
||||
setLegendItems("cables", getCableLegendItems());
|
||||
setLegendItems("satellites", getSatelliteLegendItems());
|
||||
setLegendItems("bgp", getBGPLegendItems());
|
||||
refreshLegend();
|
||||
setLoading(false);
|
||||
isDataLoading = false;
|
||||
|
||||
@@ -524,6 +599,18 @@ function getFrontFacingCables(cableLines) {
|
||||
});
|
||||
}
|
||||
|
||||
function getFrontFacingBGPMarkers(markers) {
|
||||
const earth = getEarth();
|
||||
if (!earth) return markers;
|
||||
|
||||
scratchCameraToEarth.subVectors(camera.position, earth.position).normalize();
|
||||
|
||||
return markers.filter((marker) => {
|
||||
scratchBGPDirection.copy(marker.position).normalize();
|
||||
return scratchCameraToEarth.dot(scratchBGPDirection) > 0;
|
||||
});
|
||||
}
|
||||
|
||||
function onMouseMove(event) {
|
||||
const earth = getEarth();
|
||||
if (!earth) return;
|
||||
@@ -551,6 +638,10 @@ function onMouseMove(event) {
|
||||
|
||||
const frontCables = getFrontFacingCables(getCableLines());
|
||||
const cableIntersects = interactionRaycaster.intersectObjects(frontCables);
|
||||
const frontFacingBGPMarkers = getFrontFacingBGPMarkers(getBGPMarkers());
|
||||
const bgpIntersects = getShowBGP()
|
||||
? interactionRaycaster.intersectObjects(frontFacingBGPMarkers)
|
||||
: [];
|
||||
|
||||
let hoveredSat = null;
|
||||
let hoveredSatIndexFromIntersect = null;
|
||||
@@ -568,6 +659,16 @@ function onMouseMove(event) {
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
hoveredBGP &&
|
||||
(!bgpIntersects.length || bgpIntersects[0]?.object !== hoveredBGP)
|
||||
) {
|
||||
if (hoveredBGP !== lockedObject) {
|
||||
setBGPMarkerState(hoveredBGP, "normal");
|
||||
}
|
||||
hoveredBGP = null;
|
||||
}
|
||||
|
||||
if (
|
||||
hoveredCable &&
|
||||
(!cableIntersects.length ||
|
||||
@@ -589,7 +690,16 @@ function onMouseMove(event) {
|
||||
hoveredSatelliteIndex = null;
|
||||
}
|
||||
|
||||
if (cableIntersects.length > 0 && getShowCables()) {
|
||||
if (bgpIntersects.length > 0 && getShowBGP()) {
|
||||
const marker = bgpIntersects[0].object;
|
||||
hoveredBGP = marker;
|
||||
if (marker !== lockedObject) {
|
||||
setBGPMarkerState(marker, "hover");
|
||||
}
|
||||
showBGPInfo(marker);
|
||||
setInfoCardNoBorder(true);
|
||||
hideTooltip();
|
||||
} else if (cableIntersects.length > 0 && getShowCables()) {
|
||||
const cable = cableIntersects[0].object;
|
||||
hoveredCable = cable;
|
||||
if (!isSameCable(cable, lockedObject)) {
|
||||
@@ -613,6 +723,8 @@ function onMouseMove(event) {
|
||||
}
|
||||
showSatelliteInfo(hoveredSat.properties);
|
||||
setInfoCardNoBorder(true);
|
||||
} else if (lockedObjectType === "bgp" && lockedObject) {
|
||||
showBGPInfo(lockedObject);
|
||||
} else if (lockedObjectType === "cable" && lockedObject) {
|
||||
showCableInfo(lockedObject);
|
||||
} else if (lockedObjectType === "satellite" && lockedSatellite) {
|
||||
@@ -686,10 +798,31 @@ function onClick(event) {
|
||||
const cableIntersects = interactionRaycaster.intersectObjects(
|
||||
getFrontFacingCables(getCableLines()),
|
||||
);
|
||||
const frontFacingBGPMarkers = getFrontFacingBGPMarkers(getBGPMarkers());
|
||||
const bgpIntersects = getShowBGP()
|
||||
? interactionRaycaster.intersectObjects(frontFacingBGPMarkers)
|
||||
: [];
|
||||
const satIntersects = getShowSatellites()
|
||||
? interactionRaycaster.intersectObject(getSatellitePoints())
|
||||
: [];
|
||||
|
||||
if (bgpIntersects.length > 0 && getShowBGP()) {
|
||||
clearLockedObject();
|
||||
|
||||
const clickedMarker = bgpIntersects[0].object;
|
||||
setBGPMarkerState(clickedMarker, "locked");
|
||||
|
||||
lockedObject = clickedMarker;
|
||||
lockedObjectType = "bgp";
|
||||
setAutoRotate(false);
|
||||
showBGPInfo(clickedMarker);
|
||||
showStatusMessage(
|
||||
`已选择BGP异常: ${clickedMarker.userData.collector}`,
|
||||
"info",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cableIntersects.length > 0 && getShowCables()) {
|
||||
clearLockedObject();
|
||||
|
||||
@@ -816,10 +949,15 @@ function animate() {
|
||||
}
|
||||
|
||||
applyCableVisualState();
|
||||
updateBGPVisualState(lockedObjectType, lockedObject);
|
||||
|
||||
if (lockedObjectType === "cable" && lockedObject) {
|
||||
applyLandingPointVisualState(lockedObject.userData.name, false);
|
||||
} else if (lockedObjectType === "satellite" && lockedSatellite) {
|
||||
} else if (
|
||||
lockedObjectType === "satellite" && lockedSatellite
|
||||
) {
|
||||
applyLandingPointVisualState(null, true);
|
||||
} else if (lockedObjectType === "bgp" && lockedObject) {
|
||||
applyLandingPointVisualState(null, true);
|
||||
} else {
|
||||
resetLandingPointVisualState();
|
||||
@@ -864,6 +1002,7 @@ export function destroy() {
|
||||
|
||||
clearLockedObject();
|
||||
clearCableData(getEarth());
|
||||
clearBGPData(getEarth());
|
||||
resetSatelliteState();
|
||||
clearUiState();
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ let lockedSatelliteIndex = null;
|
||||
let hoveredSatelliteIndex = null;
|
||||
let positionUpdateAccumulator = 0;
|
||||
let satelliteCapacity = 0;
|
||||
let selectedSatelliteLegendKey = null;
|
||||
|
||||
const TRAIL_LENGTH = SATELLITE_CONFIG.trailLength;
|
||||
const DOT_TEXTURE_SIZE = 32;
|
||||
@@ -33,10 +34,95 @@ const scratchToSatellite = new THREE.Vector3();
|
||||
|
||||
export let breathingPhase = 0;
|
||||
|
||||
const SATELLITE_LEGEND_RULES = [
|
||||
{
|
||||
key: "starlink",
|
||||
label: "Starlink",
|
||||
color: "#00e6ff",
|
||||
match: (props) => (props?.name || "").includes("STARLINK"),
|
||||
},
|
||||
{
|
||||
key: "geo",
|
||||
label: "GEO / 倾角 20-30",
|
||||
color: "#ffcc00",
|
||||
match: (props) => {
|
||||
const inclination = props?.inclination || 53;
|
||||
return inclination > 20 && inclination < 30;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "iridium",
|
||||
label: "Iridium",
|
||||
color: "#ff8000",
|
||||
match: (props) => (props?.name || "").includes("IRIDIUM"),
|
||||
},
|
||||
{
|
||||
key: "mid-inclination",
|
||||
label: "倾角 50-70",
|
||||
color: "#00ff4d",
|
||||
match: (props) => {
|
||||
const inclination = props?.inclination || 53;
|
||||
return inclination > 50 && inclination < 70;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "other",
|
||||
label: "其他卫星",
|
||||
color: "#ffffff",
|
||||
match: () => true,
|
||||
},
|
||||
];
|
||||
|
||||
export function updateBreathingPhase(deltaTime = 16) {
|
||||
breathingPhase += SATELLITE_CONFIG.breathingSpeed * (deltaTime / 16);
|
||||
}
|
||||
|
||||
export function getSatelliteLegendItems() {
|
||||
const presentKeys = new Set();
|
||||
|
||||
satelliteData.forEach((satellite) => {
|
||||
const props = satellite?.properties || {};
|
||||
const rule = SATELLITE_LEGEND_RULES.find((item) => item.match(props));
|
||||
if (rule) {
|
||||
presentKeys.add(rule.key);
|
||||
}
|
||||
});
|
||||
|
||||
if (presentKeys.size === 0) {
|
||||
return SATELLITE_LEGEND_RULES.map(({ label, color }) => ({ label, color }));
|
||||
}
|
||||
|
||||
const items = SATELLITE_LEGEND_RULES
|
||||
.filter((item) => presentKeys.has(item.key))
|
||||
.map(({ key, label, color }) => ({ key, label, color }));
|
||||
|
||||
if (!selectedSatelliteLegendKey) {
|
||||
return items.map(({ label, color }) => ({ label, color }));
|
||||
}
|
||||
|
||||
const selectedIndex = items.findIndex(
|
||||
(item) => item.key === selectedSatelliteLegendKey,
|
||||
);
|
||||
|
||||
if (selectedIndex > 0) {
|
||||
const [selectedItem] = items.splice(selectedIndex, 1);
|
||||
items.unshift(selectedItem);
|
||||
}
|
||||
|
||||
return items.map(({ label, color }) => ({ label, color }));
|
||||
}
|
||||
|
||||
export function setSelectedSatelliteLegend(props) {
|
||||
const rule = SATELLITE_LEGEND_RULES.find((item) =>
|
||||
item.match(props || {}),
|
||||
);
|
||||
selectedSatelliteLegendKey = rule?.key || null;
|
||||
}
|
||||
|
||||
export function clearSelectedSatelliteLegend() {
|
||||
selectedSatelliteLegendKey = null;
|
||||
}
|
||||
|
||||
function disposeMaterial(material) {
|
||||
if (!material) return;
|
||||
if (Array.isArray(material)) {
|
||||
|
||||
@@ -85,12 +85,15 @@ export function updateZoomDisplay(zoomLevel, distance) {
|
||||
export function updateEarthStats(stats) {
|
||||
const cableCountEl = document.getElementById("cable-count");
|
||||
const landingPointCountEl = document.getElementById("landing-point-count");
|
||||
const bgpAnomalyCountEl = document.getElementById("bgp-anomaly-count");
|
||||
const terrainStatusEl = document.getElementById("terrain-status");
|
||||
const textureQualityEl = document.getElementById("texture-quality");
|
||||
|
||||
if (cableCountEl) cableCountEl.textContent = stats.cableCount || 0;
|
||||
if (landingPointCountEl)
|
||||
landingPointCountEl.textContent = stats.landingPointCount || 0;
|
||||
if (bgpAnomalyCountEl)
|
||||
bgpAnomalyCountEl.textContent = stats.bgpAnomalyCount || 0;
|
||||
if (terrainStatusEl)
|
||||
terrainStatusEl.textContent = stats.terrainOn ? "开启" : "关闭";
|
||||
if (textureQualityEl)
|
||||
|
||||
Reference in New Issue
Block a user