fix: polish earth legend and info panel interactions
This commit is contained in:
@@ -7,6 +7,33 @@ This project follows the repository versioning rule:
|
|||||||
- `feature` -> `+0.1.0`
|
- `feature` -> `+0.1.0`
|
||||||
- `bugfix` -> `+0.0.1`
|
- `bugfix` -> `+0.0.1`
|
||||||
|
|
||||||
|
## 0.21.6
|
||||||
|
|
||||||
|
Released: 2026-03-27
|
||||||
|
|
||||||
|
### Highlights
|
||||||
|
|
||||||
|
- Refined the Earth page interaction loop with object-driven legend switching, clearer selection feedback, and cleaner HUD copy/layout behavior.
|
||||||
|
- Improved the Earth info surfaces so status toasts, info-card interactions, and title/subtitle presentation feel more intentional and easier to scan.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added click-to-copy support for info-card labels so clicking a field label copies the matching field value.
|
||||||
|
- Added runtime-generated legend content for cables and satellites based on current Earth data and selection state.
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
|
||||||
|
- Improved Earth legend behavior so selected cables and selected satellite categories are promoted to the top of the legend list.
|
||||||
|
- Improved legend overflow handling by constraining the visible list and using scroll for additional entries.
|
||||||
|
- Improved info-panel heading layout with centered title/subtitle styling and better subtitle hierarchy.
|
||||||
|
- Improved status-message behavior with replayable slide-in notifications when messages change in quick succession.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed info-card content spacing by targeting the actual `#info-card-content` node instead of a non-matching class selector.
|
||||||
|
- Fixed cable legend generation so it follows backend-returned cable names and colors instead of stale hard-coded placeholder categories.
|
||||||
|
- Fixed reset-view and legend-related HUD behaviors so selection and legend state stay in sync when users interact with real Earth objects.
|
||||||
|
|
||||||
## 0.21.5
|
## 0.21.5
|
||||||
|
|
||||||
Released: 2026-03-27
|
Released: 2026-03-27
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
## Current Version
|
## Current Version
|
||||||
|
|
||||||
- `main` 当前主线历史推导到:`0.16.5`
|
- `main` 当前主线历史推导到:`0.16.5`
|
||||||
- `dev` 当前开发分支历史推导到:`0.21.3`
|
- `dev` 当前开发分支历史推导到:`0.21.6`
|
||||||
|
|
||||||
## Timeline
|
## Timeline
|
||||||
|
|
||||||
@@ -66,6 +66,9 @@
|
|||||||
| `0.21.1` | bugfix | `dev` | `pending` | polish Earth toolbar controls, icons, and loading copy |
|
| `0.21.1` | bugfix | `dev` | `pending` | polish Earth toolbar controls, icons, and loading copy |
|
||||||
| `0.21.2` | bugfix | `dev` | `pending` | redesign Earth HUD with liquid-glass controls, dynamic legend switching, and info-card interaction polish |
|
| `0.21.2` | bugfix | `dev` | `pending` | redesign Earth HUD with liquid-glass controls, dynamic legend switching, and info-card interaction polish |
|
||||||
| `0.21.3` | bugfix | `dev` | `30a29a6e` | harden `planet.sh` startup controls, add selective restart and interactive user creation |
|
| `0.21.3` | bugfix | `dev` | `30a29a6e` | harden `planet.sh` startup controls, add selective restart and interactive user creation |
|
||||||
|
| `0.21.4` | bugfix | `dev` | `7ec9586f` | add Earth HUD backup snapshots and icon assets |
|
||||||
|
| `0.21.5` | bugfix | `dev` | `a761dfc5` | refine Earth legend item presentation |
|
||||||
|
| `0.21.6` | bugfix | `dev` | `pending` | improve Earth legend generation, info-card interactions, and HUD messaging polish |
|
||||||
|
|
||||||
## Maintenance Commits Not Counted as Version Bumps
|
## Maintenance Commits Not Counted as Version Bumps
|
||||||
|
|
||||||
|
|||||||
@@ -14,14 +14,34 @@
|
|||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
color: #4db8ff;
|
color: #4db8ff;
|
||||||
text-shadow: 0 0 10px rgba(77, 184, 255, 0.5);
|
text-shadow: 0 0 10px rgba(77, 184, 255, 0.5);
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#info-panel .subtitle {
|
#info-panel .subtitle {
|
||||||
color: #aaa;
|
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
font-size: 0.9rem;
|
|
||||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
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 {
|
#info-panel .cable-info {
|
||||||
|
|||||||
@@ -19,6 +19,24 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
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 {
|
#legend .legend-item {
|
||||||
|
|||||||
@@ -33,7 +33,10 @@
|
|||||||
<div id="container">
|
<div id="container">
|
||||||
<div id="info-panel">
|
<div id="info-panel">
|
||||||
<h1>智能星球计划</h1>
|
<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 id="info-card" class="info-card" style="display: none;">
|
||||||
<div class="info-card-header">
|
<div class="info-card-header">
|
||||||
@@ -90,6 +93,12 @@
|
|||||||
</span>
|
</span>
|
||||||
<span class="tooltip">显示卫星</span>
|
<span class="tooltip">显示卫星</span>
|
||||||
</button>
|
</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="显示/隐藏线缆">
|
<button id="toggle-cables" class="toolbar-btn floating-btn liquid-glass-surface active" title="显示/隐藏线缆">
|
||||||
<span class="icon" aria-hidden="true">
|
<span class="icon" aria-hidden="true">
|
||||||
<span class="material-symbols-rounded">cable</span>
|
<span class="material-symbols-rounded">cable</span>
|
||||||
@@ -194,6 +203,10 @@
|
|||||||
<span class="stats-label">卫星:</span>
|
<span class="stats-label">卫星:</span>
|
||||||
<span class="stats-value" id="satellite-count">0 颗</span>
|
<span class="stats-value" id="satellite-count">0 颗</span>
|
||||||
</div>
|
</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">
|
<div class="stats-item">
|
||||||
<span class="stats-label">视角距离:</span>
|
<span class="stats-label">视角距离:</span>
|
||||||
<span class="stats-value" id="camera-distance">300 km</span>
|
<span class="stats-value" id="camera-distance">300 km</span>
|
||||||
@@ -207,7 +220,7 @@
|
|||||||
<div id="loading">
|
<div id="loading">
|
||||||
<div id="loading-spinner"></div>
|
<div id="loading-spinner"></div>
|
||||||
<div id="loading-title">正在初始化全球态势数据...</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>
|
||||||
<div id="status-message" class="status-message" style="display: none;"></div>
|
<div id="status-message" class="status-message" style="display: none;"></div>
|
||||||
<div id="tooltip" class="tooltip"></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 { latLonToVector3 } from "./utils.js";
|
||||||
import { updateEarthStats, showStatusMessage } from "./ui.js";
|
import { updateEarthStats, showStatusMessage } from "./ui.js";
|
||||||
import { showInfoCard } from "./info-card.js";
|
import { showInfoCard } from "./info-card.js";
|
||||||
import { setLegendMode } from "./legend.js";
|
import { setLegendItems, setLegendMode } from "./legend.js";
|
||||||
|
|
||||||
export let cableLines = [];
|
export let cableLines = [];
|
||||||
export let landingPoints = [];
|
export let landingPoints = [];
|
||||||
@@ -57,7 +57,11 @@ function getCableColor(properties) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cableName =
|
const cableName =
|
||||||
properties.Name || properties.cableName || properties.shortname || "";
|
properties.Name ||
|
||||||
|
properties.name ||
|
||||||
|
properties.cableName ||
|
||||||
|
properties.shortname ||
|
||||||
|
"";
|
||||||
if (cableName.includes("Americas II")) {
|
if (cableName.includes("Americas II")) {
|
||||||
return CABLE_COLORS["Americas II"];
|
return CABLE_COLORS["Americas II"];
|
||||||
}
|
}
|
||||||
@@ -91,11 +95,17 @@ function createCableLine(points, color, properties) {
|
|||||||
properties.cable_id ||
|
properties.cable_id ||
|
||||||
properties.id ||
|
properties.id ||
|
||||||
properties.Name ||
|
properties.Name ||
|
||||||
|
properties.name ||
|
||||||
Math.random().toString(36);
|
Math.random().toString(36);
|
||||||
cableLine.userData = {
|
cableLine.userData = {
|
||||||
type: "cable",
|
type: "cable",
|
||||||
cableId,
|
cableId,
|
||||||
name: properties.Name || properties.cableName || "Unknown",
|
name:
|
||||||
|
properties.Name ||
|
||||||
|
properties.name ||
|
||||||
|
properties.cableName ||
|
||||||
|
properties.shortname ||
|
||||||
|
"Unknown",
|
||||||
owner: properties.owner || properties.owners || "-",
|
owner: properties.owner || properties.owners || "-",
|
||||||
status: properties.status || "-",
|
status: properties.status || "-",
|
||||||
length: properties.length || "-",
|
length: properties.length || "-",
|
||||||
@@ -382,6 +392,7 @@ export async function loadLandingPoints(scene, earthObj) {
|
|||||||
|
|
||||||
export function handleCableClick(cable) {
|
export function handleCableClick(cable) {
|
||||||
lockedCable = cable;
|
lockedCable = cable;
|
||||||
|
setLegendItems("cables", getCableLegendItems());
|
||||||
|
|
||||||
const data = cable.userData;
|
const data = cable.userData;
|
||||||
setLegendMode("cables");
|
setLegendMode("cables");
|
||||||
@@ -399,12 +410,51 @@ export function handleCableClick(cable) {
|
|||||||
|
|
||||||
export function clearCableSelection() {
|
export function clearCableSelection() {
|
||||||
lockedCable = null;
|
lockedCable = null;
|
||||||
|
setLegendItems("cables", getCableLegendItems());
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCableLines() {
|
export function getCableLines() {
|
||||||
return cableLines;
|
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) {
|
export function getCablesById(cableId) {
|
||||||
return cableIdMap.get(cableId) || [];
|
return cableIdMap.get(cableId) || [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export const EARTH_CONFIG = {
|
|||||||
export const PATHS = {
|
export const PATHS = {
|
||||||
cablesApi: '/api/v1/visualization/geo/cables',
|
cablesApi: '/api/v1/visualization/geo/cables',
|
||||||
landingPointsApi: '/api/v1/visualization/geo/landing-points',
|
landingPointsApi: '/api/v1/visualization/geo/landing-points',
|
||||||
|
bgpApi: '/api/v1/visualization/geo/bgp-anomalies',
|
||||||
geoJSON: './geo.json',
|
geoJSON: './geo.json',
|
||||||
landingPointsStatic: './landing-point-geo.geojson',
|
landingPointsStatic: './landing-point-geo.geojson',
|
||||||
};
|
};
|
||||||
@@ -69,6 +70,37 @@ export const SATELLITE_CONFIG = {
|
|||||||
dotOpacityMax: 1.0
|
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 = {
|
export const PREDICTED_ORBIT_CONFIG = {
|
||||||
sampleInterval: 10,
|
sampleInterval: 10,
|
||||||
opacity: 0.8
|
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,
|
getSatelliteCount,
|
||||||
} from "./satellites.js";
|
} from "./satellites.js";
|
||||||
import { toggleCables, getShowCables } from "./cables.js";
|
import { toggleCables, getShowCables } from "./cables.js";
|
||||||
|
import { toggleBGP, getShowBGP, getBGPCount } from "./bgp.js";
|
||||||
|
|
||||||
export let autoRotate = true;
|
export let autoRotate = true;
|
||||||
export let zoomLevel = 1.0;
|
export let zoomLevel = 1.0;
|
||||||
@@ -293,6 +294,7 @@ function setupTerrainControls() {
|
|||||||
const infoTrigger = document.getElementById("info-trigger");
|
const infoTrigger = document.getElementById("info-trigger");
|
||||||
const terrainBtn = document.getElementById("toggle-terrain");
|
const terrainBtn = document.getElementById("toggle-terrain");
|
||||||
const satellitesBtn = document.getElementById("toggle-satellites");
|
const satellitesBtn = document.getElementById("toggle-satellites");
|
||||||
|
const bgpBtn = document.getElementById("toggle-bgp");
|
||||||
const trailsBtn = document.getElementById("toggle-trails");
|
const trailsBtn = document.getElementById("toggle-trails");
|
||||||
const cablesBtn = document.getElementById("toggle-cables");
|
const cablesBtn = document.getElementById("toggle-cables");
|
||||||
const layoutBtn = document.getElementById("layout-toggle");
|
const layoutBtn = document.getElementById("layout-toggle");
|
||||||
@@ -337,6 +339,22 @@ function setupTerrainControls() {
|
|||||||
showStatusMessage(showSats ? "卫星已显示" : "卫星已隐藏", "info");
|
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 () {
|
bindListener(trailsBtn, "click", function () {
|
||||||
const isActive = this.classList.contains("active");
|
const isActive = this.classList.contains("active");
|
||||||
const nextShowTrails = !isActive;
|
const nextShowTrails = !isActive;
|
||||||
|
|||||||
@@ -30,6 +30,25 @@ const CARD_CONFIG = {
|
|||||||
{ key: 'apogee', label: '远地点', unit: 'km' }
|
{ 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: {
|
supercomputer: {
|
||||||
icon: '🖥️',
|
icon: '🖥️',
|
||||||
title: '超算详情',
|
title: '超算详情',
|
||||||
|
|||||||
@@ -1,25 +1,21 @@
|
|||||||
const LEGEND_MODES = {
|
const LEGEND_MODES = {
|
||||||
cables: {
|
cables: {
|
||||||
title: "线缆图例",
|
title: "线缆图例",
|
||||||
items: [
|
|
||||||
{ color: "#ff4444", label: "Americas II" },
|
|
||||||
{ color: "#44ff44", label: "AU Aleutian A" },
|
|
||||||
{ color: "#4444ff", label: "AU Aleutian B" },
|
|
||||||
{ color: "#ffff44", label: "其他电缆" },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
satellites: {
|
satellites: {
|
||||||
title: "卫星图例",
|
title: "卫星图例",
|
||||||
items: [
|
},
|
||||||
{ color: "#4db8ff", label: "卫星本体" },
|
bgp: {
|
||||||
{ color: "#9be7ff", label: "卫星轨迹" },
|
title: "BGP观测图例",
|
||||||
{ color: "#7dffb3", label: "悬停高亮" },
|
|
||||||
{ color: "#ffd166", label: "选中目标" },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let currentLegendMode = "cables";
|
let currentLegendMode = "cables";
|
||||||
|
let legendItemsByMode = {
|
||||||
|
cables: [],
|
||||||
|
satellites: [],
|
||||||
|
bgp: [],
|
||||||
|
};
|
||||||
|
|
||||||
export function initLegend() {
|
export function initLegend() {
|
||||||
renderLegend(currentLegendMode);
|
renderLegend(currentLegendMode);
|
||||||
@@ -27,7 +23,6 @@ export function initLegend() {
|
|||||||
|
|
||||||
export function setLegendMode(mode) {
|
export function setLegendMode(mode) {
|
||||||
const nextMode = LEGEND_MODES[mode] ? mode : "cables";
|
const nextMode = LEGEND_MODES[mode] ? mode : "cables";
|
||||||
if (nextMode === currentLegendMode) return;
|
|
||||||
currentLegendMode = nextMode;
|
currentLegendMode = nextMode;
|
||||||
renderLegend(currentLegendMode);
|
renderLegend(currentLegendMode);
|
||||||
}
|
}
|
||||||
@@ -36,12 +31,25 @@ export function getLegendMode() {
|
|||||||
return currentLegendMode;
|
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) {
|
function renderLegend(mode) {
|
||||||
const legend = document.getElementById("legend");
|
const legend = document.getElementById("legend");
|
||||||
if (!legend) return;
|
if (!legend) return;
|
||||||
|
|
||||||
const config = LEGEND_MODES[mode] || LEGEND_MODES.cables;
|
const config = LEGEND_MODES[mode] || LEGEND_MODES.cables;
|
||||||
const itemsHtml = config.items
|
const items = legendItemsByMode[mode] || [];
|
||||||
|
const itemsHtml = items
|
||||||
.map(
|
.map(
|
||||||
(item) => `
|
(item) => `
|
||||||
<div class="legend-item">
|
<div class="legend-item">
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
handleCableClick,
|
handleCableClick,
|
||||||
clearCableSelection,
|
clearCableSelection,
|
||||||
getCableLines,
|
getCableLines,
|
||||||
|
getCableLegendItems,
|
||||||
getCableState,
|
getCableState,
|
||||||
setCableState,
|
setCableState,
|
||||||
clearAllCableStates,
|
clearAllCableStates,
|
||||||
@@ -44,6 +45,9 @@ import {
|
|||||||
updateSatellitePositions,
|
updateSatellitePositions,
|
||||||
toggleSatellites,
|
toggleSatellites,
|
||||||
getShowSatellites,
|
getShowSatellites,
|
||||||
|
getSatelliteLegendItems,
|
||||||
|
setSelectedSatelliteLegend,
|
||||||
|
clearSelectedSatelliteLegend,
|
||||||
getSatelliteCount,
|
getSatelliteCount,
|
||||||
selectSatellite,
|
selectSatellite,
|
||||||
getSatellitePoints,
|
getSatellitePoints,
|
||||||
@@ -60,6 +64,18 @@ import {
|
|||||||
resetSatelliteState,
|
resetSatelliteState,
|
||||||
clearSatelliteData,
|
clearSatelliteData,
|
||||||
} from "./satellites.js";
|
} from "./satellites.js";
|
||||||
|
import {
|
||||||
|
loadBGPAnomalies,
|
||||||
|
getBGPMarkers,
|
||||||
|
getBGPLegendItems,
|
||||||
|
getBGPCount,
|
||||||
|
getShowBGP,
|
||||||
|
clearBGPSelection,
|
||||||
|
setBGPMarkerState,
|
||||||
|
updateBGPVisualState,
|
||||||
|
clearBGPData,
|
||||||
|
toggleBGP,
|
||||||
|
} from "./bgp.js";
|
||||||
import {
|
import {
|
||||||
setupControls,
|
setupControls,
|
||||||
getAutoRotate,
|
getAutoRotate,
|
||||||
@@ -74,7 +90,12 @@ import {
|
|||||||
hideInfoCard,
|
hideInfoCard,
|
||||||
setInfoCardNoBorder,
|
setInfoCardNoBorder,
|
||||||
} from "./info-card.js";
|
} from "./info-card.js";
|
||||||
import { initLegend, setLegendMode } from "./legend.js";
|
import {
|
||||||
|
initLegend,
|
||||||
|
setLegendMode,
|
||||||
|
refreshLegend,
|
||||||
|
setLegendItems,
|
||||||
|
} from "./legend.js";
|
||||||
|
|
||||||
export let scene;
|
export let scene;
|
||||||
export let camera;
|
export let camera;
|
||||||
@@ -86,6 +107,7 @@ let previousMousePosition = { x: 0, y: 0 };
|
|||||||
let targetRotation = { x: 0, y: 0 };
|
let targetRotation = { x: 0, y: 0 };
|
||||||
let inertialVelocity = { x: 0, y: 0 };
|
let inertialVelocity = { x: 0, y: 0 };
|
||||||
let hoveredCable = null;
|
let hoveredCable = null;
|
||||||
|
let hoveredBGP = null;
|
||||||
let hoveredSatellite = null;
|
let hoveredSatellite = null;
|
||||||
let hoveredSatelliteIndex = null;
|
let hoveredSatelliteIndex = null;
|
||||||
let lockedSatellite = null;
|
let lockedSatellite = null;
|
||||||
@@ -110,6 +132,7 @@ const interactionMouse = new THREE.Vector2();
|
|||||||
const scratchCameraToEarth = new THREE.Vector3();
|
const scratchCameraToEarth = new THREE.Vector3();
|
||||||
const scratchCableCenter = new THREE.Vector3();
|
const scratchCableCenter = new THREE.Vector3();
|
||||||
const scratchCableDirection = new THREE.Vector3();
|
const scratchCableDirection = new THREE.Vector3();
|
||||||
|
const scratchBGPDirection = new THREE.Vector3();
|
||||||
|
|
||||||
const cleanupFns = [];
|
const cleanupFns = [];
|
||||||
const DRAG_ROTATION_FACTOR = 0.005;
|
const DRAG_ROTATION_FACTOR = 0.005;
|
||||||
@@ -169,6 +192,7 @@ function disposeSceneObject(object) {
|
|||||||
|
|
||||||
function clearRuntimeSelection() {
|
function clearRuntimeSelection() {
|
||||||
hoveredCable = null;
|
hoveredCable = null;
|
||||||
|
hoveredBGP = null;
|
||||||
hoveredSatellite = null;
|
hoveredSatellite = null;
|
||||||
hoveredSatelliteIndex = null;
|
hoveredSatelliteIndex = null;
|
||||||
lockedObject = null;
|
lockedObject = null;
|
||||||
@@ -176,14 +200,17 @@ function clearRuntimeSelection() {
|
|||||||
lockedSatellite = null;
|
lockedSatellite = null;
|
||||||
lockedSatelliteIndex = null;
|
lockedSatelliteIndex = null;
|
||||||
setLockedSatelliteIndex(null);
|
setLockedSatelliteIndex(null);
|
||||||
|
clearSelectedSatelliteLegend();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearLockedObject() {
|
export function clearLockedObject() {
|
||||||
hidePredictedOrbit();
|
hidePredictedOrbit();
|
||||||
clearAllCableStates();
|
clearAllCableStates();
|
||||||
clearCableSelection();
|
clearCableSelection();
|
||||||
|
clearBGPSelection();
|
||||||
setSatelliteRingState(null, "none", null);
|
setSatelliteRingState(null, "none", null);
|
||||||
clearRuntimeSelection();
|
clearRuntimeSelection();
|
||||||
|
setLegendItems("satellites", getSatelliteLegendItems());
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSameCable(cable1, cable2) {
|
function isSameCable(cable1, cable2) {
|
||||||
@@ -213,6 +240,8 @@ function showSatelliteInfo(props) {
|
|||||||
const perigee = (6371 * (1 - ecc)).toFixed(0);
|
const perigee = (6371 * (1 - ecc)).toFixed(0);
|
||||||
const apogee = (6371 * (1 + ecc)).toFixed(0);
|
const apogee = (6371 * (1 + ecc)).toFixed(0);
|
||||||
|
|
||||||
|
setSelectedSatelliteLegend(props);
|
||||||
|
setLegendItems("satellites", getSatelliteLegendItems());
|
||||||
setLegendMode("satellites");
|
setLegendMode("satellites");
|
||||||
showInfoCard("satellite", {
|
showInfoCard("satellite", {
|
||||||
name: props?.name || "-",
|
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() {
|
function applyCableVisualState() {
|
||||||
const allCables = getCableLines();
|
const allCables = getCableLines();
|
||||||
const pulse = (Math.sin(Date.now() * CABLE_CONFIG.pulseSpeed) + 1) * 0.5;
|
const pulse = (Math.sin(Date.now() * CABLE_CONFIG.pulseSpeed) + 1) * 0.5;
|
||||||
@@ -248,7 +295,8 @@ function applyCableVisualState() {
|
|||||||
default:
|
default:
|
||||||
if (
|
if (
|
||||||
(lockedObjectType === "cable" && lockedObject) ||
|
(lockedObjectType === "cable" && lockedObject) ||
|
||||||
(lockedObjectType === "satellite" && lockedSatellite)
|
(lockedObjectType === "satellite" && lockedSatellite) ||
|
||||||
|
(lockedObjectType === "bgp" && lockedObject)
|
||||||
) {
|
) {
|
||||||
cable.material.opacity = CABLE_CONFIG.otherOpacity;
|
cable.material.opacity = CABLE_CONFIG.otherOpacity;
|
||||||
const origColor = cable.userData.originalColor;
|
const origColor = cable.userData.originalColor;
|
||||||
@@ -289,6 +337,7 @@ function updateStatsSummary() {
|
|||||||
cableCount: getCableLines().length,
|
cableCount: getCableLines().length,
|
||||||
landingPointCount:
|
landingPointCount:
|
||||||
document.getElementById("landing-point-count")?.textContent || 0,
|
document.getElementById("landing-point-count")?.textContent || 0,
|
||||||
|
bgpAnomalyCount: `${getBGPCount()} 条`,
|
||||||
terrainOn: getShowTerrain(),
|
terrainOn: getShowTerrain(),
|
||||||
textureQuality: "8K 卫星图",
|
textureQuality: "8K 卫星图",
|
||||||
});
|
});
|
||||||
@@ -337,6 +386,9 @@ export function init() {
|
|||||||
addLights();
|
addLights();
|
||||||
initInfoCard();
|
initInfoCard();
|
||||||
initLegend();
|
initLegend();
|
||||||
|
setLegendItems("cables", getCableLegendItems());
|
||||||
|
setLegendItems("satellites", getSatelliteLegendItems());
|
||||||
|
setLegendItems("bgp", getBGPLegendItems());
|
||||||
const earthObj = createEarth(scene);
|
const earthObj = createEarth(scene);
|
||||||
targetRotation = {
|
targetRotation = {
|
||||||
x: earthObj.rotation.x,
|
x: earthObj.rotation.x,
|
||||||
@@ -400,8 +452,8 @@ async function loadData(showWhiteSphere = false) {
|
|||||||
setLoadingMessage(
|
setLoadingMessage(
|
||||||
showWhiteSphere ? "正在刷新全球态势数据..." : "正在初始化全球态势数据...",
|
showWhiteSphere ? "正在刷新全球态势数据..." : "正在初始化全球态势数据...",
|
||||||
showWhiteSphere
|
showWhiteSphere
|
||||||
? "重新同步卫星、海底光缆与登陆点数据"
|
? "重新同步卫星、海底光缆、登陆点与BGP异常数据"
|
||||||
: "同步卫星、海底光缆与登陆点数据",
|
: "同步卫星、海底光缆、登陆点与BGP异常数据",
|
||||||
);
|
);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
clearLockedObject();
|
clearLockedObject();
|
||||||
@@ -434,6 +486,22 @@ async function loadData(showWhiteSphere = false) {
|
|||||||
}
|
}
|
||||||
return satelliteCount;
|
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) {
|
if (loadToken !== currentLoadToken) {
|
||||||
@@ -451,6 +519,9 @@ async function loadData(showWhiteSphere = false) {
|
|||||||
if (results[2].status === "rejected") {
|
if (results[2].status === "rejected") {
|
||||||
errors.push({ label: "卫星", reason: results[2].reason });
|
errors.push({ label: "卫星", reason: results[2].reason });
|
||||||
}
|
}
|
||||||
|
if (results[3].status === "rejected") {
|
||||||
|
errors.push({ label: "BGP异常", reason: results[3].reason });
|
||||||
|
}
|
||||||
|
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
const errorMessage = buildLoadErrorMessage(errors);
|
const errorMessage = buildLoadErrorMessage(errors);
|
||||||
@@ -462,6 +533,10 @@ async function loadData(showWhiteSphere = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateStatsSummary();
|
updateStatsSummary();
|
||||||
|
setLegendItems("cables", getCableLegendItems());
|
||||||
|
setLegendItems("satellites", getSatelliteLegendItems());
|
||||||
|
setLegendItems("bgp", getBGPLegendItems());
|
||||||
|
refreshLegend();
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
isDataLoading = 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) {
|
function onMouseMove(event) {
|
||||||
const earth = getEarth();
|
const earth = getEarth();
|
||||||
if (!earth) return;
|
if (!earth) return;
|
||||||
@@ -551,6 +638,10 @@ function onMouseMove(event) {
|
|||||||
|
|
||||||
const frontCables = getFrontFacingCables(getCableLines());
|
const frontCables = getFrontFacingCables(getCableLines());
|
||||||
const cableIntersects = interactionRaycaster.intersectObjects(frontCables);
|
const cableIntersects = interactionRaycaster.intersectObjects(frontCables);
|
||||||
|
const frontFacingBGPMarkers = getFrontFacingBGPMarkers(getBGPMarkers());
|
||||||
|
const bgpIntersects = getShowBGP()
|
||||||
|
? interactionRaycaster.intersectObjects(frontFacingBGPMarkers)
|
||||||
|
: [];
|
||||||
|
|
||||||
let hoveredSat = null;
|
let hoveredSat = null;
|
||||||
let hoveredSatIndexFromIntersect = 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 (
|
if (
|
||||||
hoveredCable &&
|
hoveredCable &&
|
||||||
(!cableIntersects.length ||
|
(!cableIntersects.length ||
|
||||||
@@ -589,7 +690,16 @@ function onMouseMove(event) {
|
|||||||
hoveredSatelliteIndex = null;
|
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;
|
const cable = cableIntersects[0].object;
|
||||||
hoveredCable = cable;
|
hoveredCable = cable;
|
||||||
if (!isSameCable(cable, lockedObject)) {
|
if (!isSameCable(cable, lockedObject)) {
|
||||||
@@ -613,6 +723,8 @@ function onMouseMove(event) {
|
|||||||
}
|
}
|
||||||
showSatelliteInfo(hoveredSat.properties);
|
showSatelliteInfo(hoveredSat.properties);
|
||||||
setInfoCardNoBorder(true);
|
setInfoCardNoBorder(true);
|
||||||
|
} else if (lockedObjectType === "bgp" && lockedObject) {
|
||||||
|
showBGPInfo(lockedObject);
|
||||||
} else if (lockedObjectType === "cable" && lockedObject) {
|
} else if (lockedObjectType === "cable" && lockedObject) {
|
||||||
showCableInfo(lockedObject);
|
showCableInfo(lockedObject);
|
||||||
} else if (lockedObjectType === "satellite" && lockedSatellite) {
|
} else if (lockedObjectType === "satellite" && lockedSatellite) {
|
||||||
@@ -686,10 +798,31 @@ function onClick(event) {
|
|||||||
const cableIntersects = interactionRaycaster.intersectObjects(
|
const cableIntersects = interactionRaycaster.intersectObjects(
|
||||||
getFrontFacingCables(getCableLines()),
|
getFrontFacingCables(getCableLines()),
|
||||||
);
|
);
|
||||||
|
const frontFacingBGPMarkers = getFrontFacingBGPMarkers(getBGPMarkers());
|
||||||
|
const bgpIntersects = getShowBGP()
|
||||||
|
? interactionRaycaster.intersectObjects(frontFacingBGPMarkers)
|
||||||
|
: [];
|
||||||
const satIntersects = getShowSatellites()
|
const satIntersects = getShowSatellites()
|
||||||
? interactionRaycaster.intersectObject(getSatellitePoints())
|
? 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()) {
|
if (cableIntersects.length > 0 && getShowCables()) {
|
||||||
clearLockedObject();
|
clearLockedObject();
|
||||||
|
|
||||||
@@ -816,10 +949,15 @@ function animate() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
applyCableVisualState();
|
applyCableVisualState();
|
||||||
|
updateBGPVisualState(lockedObjectType, lockedObject);
|
||||||
|
|
||||||
if (lockedObjectType === "cable" && lockedObject) {
|
if (lockedObjectType === "cable" && lockedObject) {
|
||||||
applyLandingPointVisualState(lockedObject.userData.name, false);
|
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);
|
applyLandingPointVisualState(null, true);
|
||||||
} else {
|
} else {
|
||||||
resetLandingPointVisualState();
|
resetLandingPointVisualState();
|
||||||
@@ -864,6 +1002,7 @@ export function destroy() {
|
|||||||
|
|
||||||
clearLockedObject();
|
clearLockedObject();
|
||||||
clearCableData(getEarth());
|
clearCableData(getEarth());
|
||||||
|
clearBGPData(getEarth());
|
||||||
resetSatelliteState();
|
resetSatelliteState();
|
||||||
clearUiState();
|
clearUiState();
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ let lockedSatelliteIndex = null;
|
|||||||
let hoveredSatelliteIndex = null;
|
let hoveredSatelliteIndex = null;
|
||||||
let positionUpdateAccumulator = 0;
|
let positionUpdateAccumulator = 0;
|
||||||
let satelliteCapacity = 0;
|
let satelliteCapacity = 0;
|
||||||
|
let selectedSatelliteLegendKey = null;
|
||||||
|
|
||||||
const TRAIL_LENGTH = SATELLITE_CONFIG.trailLength;
|
const TRAIL_LENGTH = SATELLITE_CONFIG.trailLength;
|
||||||
const DOT_TEXTURE_SIZE = 32;
|
const DOT_TEXTURE_SIZE = 32;
|
||||||
@@ -33,10 +34,95 @@ const scratchToSatellite = new THREE.Vector3();
|
|||||||
|
|
||||||
export let breathingPhase = 0;
|
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) {
|
export function updateBreathingPhase(deltaTime = 16) {
|
||||||
breathingPhase += SATELLITE_CONFIG.breathingSpeed * (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) {
|
function disposeMaterial(material) {
|
||||||
if (!material) return;
|
if (!material) return;
|
||||||
if (Array.isArray(material)) {
|
if (Array.isArray(material)) {
|
||||||
|
|||||||
@@ -85,12 +85,15 @@ export function updateZoomDisplay(zoomLevel, distance) {
|
|||||||
export function updateEarthStats(stats) {
|
export function updateEarthStats(stats) {
|
||||||
const cableCountEl = document.getElementById("cable-count");
|
const cableCountEl = document.getElementById("cable-count");
|
||||||
const landingPointCountEl = document.getElementById("landing-point-count");
|
const landingPointCountEl = document.getElementById("landing-point-count");
|
||||||
|
const bgpAnomalyCountEl = document.getElementById("bgp-anomaly-count");
|
||||||
const terrainStatusEl = document.getElementById("terrain-status");
|
const terrainStatusEl = document.getElementById("terrain-status");
|
||||||
const textureQualityEl = document.getElementById("texture-quality");
|
const textureQualityEl = document.getElementById("texture-quality");
|
||||||
|
|
||||||
if (cableCountEl) cableCountEl.textContent = stats.cableCount || 0;
|
if (cableCountEl) cableCountEl.textContent = stats.cableCount || 0;
|
||||||
if (landingPointCountEl)
|
if (landingPointCountEl)
|
||||||
landingPointCountEl.textContent = stats.landingPointCount || 0;
|
landingPointCountEl.textContent = stats.landingPointCount || 0;
|
||||||
|
if (bgpAnomalyCountEl)
|
||||||
|
bgpAnomalyCountEl.textContent = stats.bgpAnomalyCount || 0;
|
||||||
if (terrainStatusEl)
|
if (terrainStatusEl)
|
||||||
terrainStatusEl.textContent = stats.terrainOn ? "开启" : "关闭";
|
terrainStatusEl.textContent = stats.terrainOn ? "开启" : "关闭";
|
||||||
if (textureQualityEl)
|
if (textureQualityEl)
|
||||||
|
|||||||
Reference in New Issue
Block a user