chore: add earth hud backup and icon assets

This commit is contained in:
linkong
2026-03-27 14:30:12 +08:00
parent b0058edf17
commit 7ec9586f7a
16 changed files with 1168 additions and 0 deletions

View File

@@ -0,0 +1,435 @@
/* base.css - 公共基础样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #0a0a1a;
color: #fff;
overflow: hidden;
}
#container {
position: relative;
width: 100vw;
height: 100vh;
}
#container.dragging {
cursor: grabbing;
}
/* Bottom Dock */
#right-toolbar-group {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
z-index: 200;
}
#right-toolbar-group,
#info-panel,
#coordinates-display,
#legend,
#earth-stats {
transition:
top 0.45s ease,
right 0.45s ease,
bottom 0.45s ease,
left 0.45s ease,
transform 0.45s ease,
box-shadow 0.45s ease;
}
/* Zoom Toolbar - Right side, vertical */
#zoom-toolbar {
position: relative;
bottom: auto;
right: auto;
display: flex;
flex-direction: row;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
#zoom-toolbar .zoom-percent {
font-size: 0.75rem;
font-weight: 600;
color: #4db8ff;
min-width: 30px;
display: inline-block;
text-align: center;
cursor: pointer;
padding: 2px 4px;
border-radius: 3px;
transition: all 0.2s ease;
}
#zoom-toolbar .zoom-percent:hover {
background: rgba(77, 184, 255, 0.2);
box-shadow: 0 0 10px rgba(77, 184, 255, 0.3);
}
#zoom-toolbar .zoom-btn {
width: 28px;
height: 28px;
min-width: 28px;
border: none;
border-radius: 50%;
background: rgba(77, 184, 255, 0.2);
color: #4db8ff;
font-size: 14px;
font-weight: bold;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
padding: 0;
margin: 0;
flex: 0 0 auto;
box-sizing: border-box;
position: relative;
}
#zoom-toolbar .zoom-btn:hover {
background: rgba(77, 184, 255, 0.4);
transform: scale(1.1);
box-shadow: 0 0 10px rgba(77, 184, 255, 0.5);
}
#zoom-toolbar #reset-view svg {
width: 18px;
height: 18px;
stroke: currentColor;
stroke-width: 1.8;
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
}
#zoom-toolbar .zoom-percent {
position: relative;
}
#zoom-toolbar .tooltip {
position: absolute;
bottom: calc(100% + 12px);
left: 50%;
transform: translateX(-50%);
background: rgba(10, 10, 30, 0.95);
color: #fff;
padding: 6px 12px;
border-radius: 6px;
font-size: 12px;
white-space: nowrap;
opacity: 0;
visibility: hidden;
transition: all 0.2s ease;
border: 1px solid rgba(77, 184, 255, 0.4);
pointer-events: none;
z-index: 100;
}
#zoom-toolbar .zoom-btn:hover .tooltip,
#zoom-toolbar .zoom-percent:hover .tooltip {
opacity: 1;
visibility: visible;
}
#zoom-toolbar .tooltip::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border: 6px solid transparent;
border-top-color: rgba(77, 184, 255, 0.4);
}
#loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 1.2rem;
color: #4db8ff;
z-index: 100;
text-align: center;
background-color: rgba(10, 10, 30, 0.95);
padding: 30px;
border-radius: 10px;
border: 1px solid #4db8ff;
box-shadow: 0 0 30px rgba(77,184,255,0.3);
}
#loading-spinner {
border: 4px solid rgba(77, 184, 255, 0.3);
border-top: 4px solid #4db8ff;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 15px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error-message {
color: #ff4444;
margin-top: 10px;
font-size: 0.9rem;
display: none;
padding: 10px;
background-color: rgba(255, 68, 68, 0.1);
border-radius: 5px;
border-left: 3px solid #ff4444;
}
.terrain-controls {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.slider-container {
margin-bottom: 10px;
}
.slider-label {
display: flex;
justify-content: space-between;
margin-bottom: 5px;
font-size: 0.9rem;
}
input[type="range"] {
width: 100%;
height: 8px;
-webkit-appearance: none;
background: rgba(0, 102, 204, 0.3);
border-radius: 4px;
outline: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: #4db8ff;
cursor: pointer;
box-shadow: 0 0 10px #4db8ff;
}
.status-message {
position: absolute;
top: 20px;
right: 260px;
background-color: rgba(10, 10, 30, 0.85);
border-radius: 10px;
padding: 10px 15px;
z-index: 10;
box-shadow: 0 0 20px rgba(0, 150, 255, 0.3);
border: 1px solid rgba(0, 150, 255, 0.2);
font-size: 0.9rem;
display: none;
backdrop-filter: blur(5px);
}
.status-message.success {
color: #44ff44;
border-left: 3px solid #44ff44;
}
.status-message.warning {
color: #ffff44;
border-left: 3px solid #ffff44;
}
.status-message.error {
color: #ff4444;
border-left: 3px solid #ff4444;
}
.tooltip {
position: absolute;
background-color: rgba(10, 10, 30, 0.95);
border: 1px solid #4db8ff;
border-radius: 5px;
padding: 5px 10px;
font-size: 0.8rem;
color: #fff;
pointer-events: none;
z-index: 100;
box-shadow: 0 0 10px rgba(77, 184, 255, 0.3);
display: none;
user-select: none;
}
/* Control Toolbar - Stellarium/Star Walk style */
#control-toolbar {
position: relative;
bottom: auto;
right: auto;
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
background: rgba(10, 10, 30, 0.9);
border-radius: 999px;
padding: 10px 14px;
border: 1px solid rgba(77, 184, 255, 0.3);
box-shadow: 0 0 20px rgba(77, 184, 255, 0.2);
transition: all 0.3s ease;
}
.toolbar-items {
display: flex;
gap: 6px;
align-items: center;
flex-wrap: nowrap;
}
.toolbar-divider {
width: 1px;
height: 28px;
background: rgba(77, 184, 255, 0.28);
flex-shrink: 0;
}
.toolbar-btn {
position: relative;
width: 28px;
height: 28px;
border: none;
border-radius: 50%;
background: rgba(77, 184, 255, 0.15);
color: #4db8ff;
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
box-sizing: border-box;
padding: 0;
margin: 0;
}
.toolbar-btn:hover {
background: rgba(77, 184, 255, 0.35);
transform: scale(1.1);
box-shadow: 0 0 15px rgba(77, 184, 255, 0.5);
}
.toolbar-btn:active {
transform: scale(0.95);
}
.toolbar-btn.active {
background: rgba(77, 184, 255, 0.4);
box-shadow: 0 0 10px rgba(77, 184, 255, 0.4) inset;
}
.toolbar-btn .icon {
display: inline-flex;
align-items: center;
justify-content: center;
}
.toolbar-btn svg {
width: 18px;
height: 18px;
stroke: currentColor;
stroke-width: 2.1;
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
}
#rotate-toggle .icon-play,
#rotate-toggle.is-stopped .icon-pause {
display: none;
}
#rotate-toggle.is-stopped .icon-play {
display: inline-flex;
}
#container.layout-expanded #info-panel {
top: 20px;
left: 20px;
transform: translate(calc(-100% + 20px), calc(-100% + 20px));
}
#container.layout-expanded #coordinates-display {
top: 20px;
right: 20px;
transform: translate(calc(100% - 20px), calc(-100% + 20px));
}
#container.layout-expanded #legend {
left: 20px;
bottom: 20px;
transform: translate(calc(-100% + 20px), calc(100% - 20px));
}
#container.layout-expanded #earth-stats {
right: 20px;
bottom: 20px;
transform: translate(calc(100% - 20px), calc(100% - 20px));
}
#container.layout-expanded #right-toolbar-group {
bottom: 20px;
transform: translateX(-50%);
}
.toolbar-btn .tooltip {
position: absolute;
bottom: 50px;
left: 50%;
transform: translateX(-50%);
background: rgba(10, 10, 30, 0.95);
color: #fff;
padding: 6px 12px;
border-radius: 6px;
font-size: 12px;
white-space: nowrap;
opacity: 0;
visibility: hidden;
transition: all 0.2s ease;
border: 1px solid rgba(77, 184, 255, 0.4);
pointer-events: none;
z-index: 100;
}
.toolbar-btn:hover .tooltip {
opacity: 1;
visibility: visible;
bottom: 52px;
}
.toolbar-btn .tooltip::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border: 6px solid transparent;
border-top-color: rgba(77, 184, 255, 0.4);
}

View File

@@ -0,0 +1,421 @@
// controls.js - Zoom, rotate and toggle controls
import { CONFIG, EARTH_CONFIG } from "./constants.js";
import { updateZoomDisplay, showStatusMessage } from "./ui.js";
import { toggleTerrain } from "./earth.js";
import { reloadData, clearLockedObject } from "./main.js";
import {
toggleSatellites,
toggleTrails,
getShowSatellites,
getSatelliteCount,
} from "./satellites.js";
import { toggleCables, getShowCables } from "./cables.js";
export let autoRotate = true;
export let zoomLevel = 1.0;
export let showTerrain = false;
export let isDragging = false;
export let layoutExpanded = false;
let earthObj = null;
let listeners = [];
let cleanupFns = [];
function bindListener(element, eventName, handler, options) {
if (!element) return;
element.addEventListener(eventName, handler, options);
listeners.push(() =>
element.removeEventListener(eventName, handler, options),
);
}
function resetCleanup() {
cleanupFns.forEach((cleanup) => cleanup());
cleanupFns = [];
listeners.forEach((cleanup) => cleanup());
listeners = [];
}
export function setupControls(camera, renderer, scene, earth) {
resetCleanup();
earthObj = earth;
setupZoomControls(camera);
setupWheelZoom(camera, renderer);
setupRotateControls(camera, earth);
setupTerrainControls();
}
function setupZoomControls(camera) {
let zoomInterval = null;
let holdTimeout = null;
let startTime = 0;
const HOLD_THRESHOLD = 150;
const LONG_PRESS_TICK = 50;
const CLICK_STEP = 10;
const MIN_PERCENT = CONFIG.minZoom * 100;
const MAX_PERCENT = CONFIG.maxZoom * 100;
function doZoomStep(direction) {
let currentPercent = Math.round(zoomLevel * 100);
let newPercent =
direction > 0 ? currentPercent + CLICK_STEP : currentPercent - CLICK_STEP;
if (newPercent > MAX_PERCENT) newPercent = MAX_PERCENT;
if (newPercent < MIN_PERCENT) newPercent = MIN_PERCENT;
zoomLevel = newPercent / 100;
applyZoom(camera);
}
function doContinuousZoom(direction) {
let currentPercent = Math.round(zoomLevel * 100);
let newPercent = direction > 0 ? currentPercent + 1 : currentPercent - 1;
if (newPercent > MAX_PERCENT) newPercent = MAX_PERCENT;
if (newPercent < MIN_PERCENT) newPercent = MIN_PERCENT;
zoomLevel = newPercent / 100;
applyZoom(camera);
}
function startContinuousZoom(direction) {
doContinuousZoom(direction);
zoomInterval = window.setInterval(() => {
doContinuousZoom(direction);
}, LONG_PRESS_TICK);
}
function stopZoom() {
if (zoomInterval) {
clearInterval(zoomInterval);
zoomInterval = null;
}
if (holdTimeout) {
clearTimeout(holdTimeout);
holdTimeout = null;
}
}
function handleMouseDown(direction) {
startTime = Date.now();
stopZoom();
holdTimeout = window.setTimeout(() => {
startContinuousZoom(direction);
}, HOLD_THRESHOLD);
}
function handleMouseUp(direction) {
const heldTime = Date.now() - startTime;
stopZoom();
if (heldTime < HOLD_THRESHOLD) {
doZoomStep(direction);
}
}
cleanupFns.push(stopZoom);
const zoomIn = document.getElementById("zoom-in");
const zoomOut = document.getElementById("zoom-out");
const zoomValue = document.getElementById("zoom-value");
bindListener(zoomIn, "mousedown", () => handleMouseDown(1));
bindListener(zoomIn, "mouseup", () => handleMouseUp(1));
bindListener(zoomIn, "mouseleave", stopZoom);
bindListener(zoomIn, "touchstart", (e) => {
e.preventDefault();
handleMouseDown(1);
});
bindListener(zoomIn, "touchend", () => handleMouseUp(1));
bindListener(zoomOut, "mousedown", () => handleMouseDown(-1));
bindListener(zoomOut, "mouseup", () => handleMouseUp(-1));
bindListener(zoomOut, "mouseleave", stopZoom);
bindListener(zoomOut, "touchstart", (e) => {
e.preventDefault();
handleMouseDown(-1);
});
bindListener(zoomOut, "touchend", () => handleMouseUp(-1));
bindListener(zoomValue, "click", () => {
const startZoomVal = zoomLevel;
const targetZoom = 1.0;
const startDistance = CONFIG.defaultCameraZ / startZoomVal;
const targetDistance = CONFIG.defaultCameraZ / targetZoom;
animateValue(
0,
1,
600,
(progress) => {
const ease = 1 - Math.pow(1 - progress, 3);
zoomLevel = startZoomVal + (targetZoom - startZoomVal) * ease;
camera.position.z = CONFIG.defaultCameraZ / zoomLevel;
const distance =
startDistance + (targetDistance - startDistance) * ease;
updateZoomDisplay(zoomLevel, distance.toFixed(0));
},
() => {
zoomLevel = 1.0;
showStatusMessage("缩放已重置到100%", "info");
},
);
});
}
function setupWheelZoom(camera, renderer) {
bindListener(
renderer?.domElement,
"wheel",
(e) => {
e.preventDefault();
if (e.deltaY < 0) {
zoomLevel = Math.min(zoomLevel + 0.1, CONFIG.maxZoom);
} else {
zoomLevel = Math.max(zoomLevel - 0.1, CONFIG.minZoom);
}
applyZoom(camera);
},
{ passive: false },
);
}
function applyZoom(camera) {
camera.position.z = CONFIG.defaultCameraZ / zoomLevel;
const distance = camera.position.z.toFixed(0);
updateZoomDisplay(zoomLevel, distance);
}
function animateValue(start, end, duration, onUpdate, onComplete) {
const startTime = performance.now();
function update(currentTime) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
const easeProgress = 1 - Math.pow(1 - progress, 3);
const current = start + (end - start) * easeProgress;
onUpdate(current);
if (progress < 1) {
requestAnimationFrame(update);
} else if (onComplete) {
onComplete();
}
}
requestAnimationFrame(update);
}
export function resetView(camera) {
if (!earthObj) return;
function animateToView(targetLat, targetLon, targetRotLon) {
const latRot = (targetLat * Math.PI) / 180;
const targetRotX =
EARTH_CONFIG.tiltRad + latRot * EARTH_CONFIG.latCoefficient;
const targetRotY = -((targetRotLon * Math.PI) / 180);
const startRotX = earthObj.rotation.x;
const startRotY = earthObj.rotation.y;
const startZoom = zoomLevel;
const targetZoom = 1.0;
animateValue(
0,
1,
800,
(progress) => {
const ease = 1 - Math.pow(1 - progress, 3);
earthObj.rotation.x = startRotX + (targetRotX - startRotX) * ease;
earthObj.rotation.y = startRotY + (targetRotY - startRotY) * ease;
zoomLevel = startZoom + (targetZoom - startZoom) * ease;
camera.position.z = CONFIG.defaultCameraZ / zoomLevel;
updateZoomDisplay(zoomLevel, camera.position.z.toFixed(0));
},
() => {
zoomLevel = 1.0;
showStatusMessage("视角已重置", "info");
},
);
}
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(pos) =>
animateToView(
pos.coords.latitude,
pos.coords.longitude,
-pos.coords.longitude,
),
() =>
animateToView(
EARTH_CONFIG.chinaLat,
EARTH_CONFIG.chinaLon,
EARTH_CONFIG.chinaRotLon,
),
{ timeout: 5000, enableHighAccuracy: false },
);
} else {
animateToView(
EARTH_CONFIG.chinaLat,
EARTH_CONFIG.chinaLon,
EARTH_CONFIG.chinaRotLon,
);
}
clearLockedObject();
}
function setupRotateControls(camera) {
const rotateBtn = document.getElementById("rotate-toggle");
const resetViewBtn = document.getElementById("reset-view");
bindListener(rotateBtn, "click", () => {
const isRotating = toggleAutoRotate();
showStatusMessage(isRotating ? "自动旋转已开启" : "自动旋转已暂停", "info");
});
updateRotateUI();
bindListener(resetViewBtn, "click", () => {
resetView(camera);
});
}
function setupTerrainControls() {
const container = document.getElementById("container");
const terrainBtn = document.getElementById("toggle-terrain");
const satellitesBtn = document.getElementById("toggle-satellites");
const trailsBtn = document.getElementById("toggle-trails");
const cablesBtn = document.getElementById("toggle-cables");
const layoutBtn = document.getElementById("layout-toggle");
const reloadBtn = document.getElementById("reload-data");
if (trailsBtn) {
trailsBtn.classList.add("active");
const tooltip = trailsBtn.querySelector(".tooltip");
if (tooltip) tooltip.textContent = "隐藏轨迹";
}
bindListener(terrainBtn, "click", function () {
showTerrain = !showTerrain;
toggleTerrain(showTerrain);
this.classList.toggle("active", showTerrain);
const tooltip = this.querySelector(".tooltip");
if (tooltip) tooltip.textContent = showTerrain ? "隐藏地形" : "显示地形";
const terrainStatus = document.getElementById("terrain-status");
if (terrainStatus)
terrainStatus.textContent = showTerrain ? "开启" : "关闭";
showStatusMessage(showTerrain ? "地形已显示" : "地形已隐藏", "info");
});
bindListener(satellitesBtn, "click", function () {
const showSats = !getShowSatellites();
if (!showSats) {
clearLockedObject();
}
toggleSatellites(showSats);
this.classList.toggle("active", showSats);
const tooltip = this.querySelector(".tooltip");
if (tooltip) tooltip.textContent = showSats ? "隐藏卫星" : "显示卫星";
const satelliteCountEl = document.getElementById("satellite-count");
if (satelliteCountEl)
satelliteCountEl.textContent = getSatelliteCount() + " 颗";
showStatusMessage(showSats ? "卫星已显示" : "卫星已隐藏", "info");
});
bindListener(trailsBtn, "click", function () {
const isActive = this.classList.contains("active");
const nextShowTrails = !isActive;
toggleTrails(nextShowTrails);
this.classList.toggle("active", nextShowTrails);
const tooltip = this.querySelector(".tooltip");
if (tooltip) tooltip.textContent = nextShowTrails ? "隐藏轨迹" : "显示轨迹";
showStatusMessage(nextShowTrails ? "轨迹已显示" : "轨迹已隐藏", "info");
});
bindListener(cablesBtn, "click", function () {
const showNextCables = !getShowCables();
if (!showNextCables) {
clearLockedObject();
}
toggleCables(showNextCables);
this.classList.toggle("active", showNextCables);
const tooltip = this.querySelector(".tooltip");
if (tooltip) tooltip.textContent = showNextCables ? "隐藏线缆" : "显示线缆";
showStatusMessage(showNextCables ? "线缆已显示" : "线缆已隐藏", "info");
});
bindListener(reloadBtn, "click", async () => {
await reloadData();
});
bindListener(layoutBtn, "click", () => {
const expanded = toggleLayoutExpanded(container);
showStatusMessage(expanded ? "布局已最大化" : "布局已恢复", "info");
});
updateLayoutUI(container);
}
export function teardownControls() {
resetCleanup();
}
export function getAutoRotate() {
return autoRotate;
}
function updateRotateUI() {
const btn = document.getElementById("rotate-toggle");
if (btn) {
btn.classList.toggle("active", autoRotate);
btn.classList.toggle("is-stopped", !autoRotate);
const tooltip = btn.querySelector(".tooltip");
if (tooltip) tooltip.textContent = autoRotate ? "暂停旋转" : "开始旋转";
}
}
export function setAutoRotate(value) {
autoRotate = value;
updateRotateUI();
}
export function toggleAutoRotate() {
autoRotate = !autoRotate;
updateRotateUI();
clearLockedObject();
return autoRotate;
}
export function getZoomLevel() {
return zoomLevel;
}
export function getShowTerrain() {
return showTerrain;
}
function updateLayoutUI(container) {
if (container) {
container.classList.toggle("layout-expanded", layoutExpanded);
}
const btn = document.getElementById("layout-toggle");
if (btn) {
btn.classList.toggle("active", layoutExpanded);
const tooltip = btn.querySelector(".tooltip");
const nextLabel = layoutExpanded ? "恢复布局" : "最大化布局";
btn.title = nextLabel;
if (tooltip) tooltip.textContent = nextLabel;
}
}
function toggleLayoutExpanded(container) {
layoutExpanded = !layoutExpanded;
updateLayoutUI(container);
return layoutExpanded;
}

View File

@@ -0,0 +1,227 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>智能星球计划 - 现实层宇宙全息感知</title>
<script type="importmap">
{
"imports": {
"three": "https://esm.sh/three@0.128.0",
"simplex-noise": "https://esm.sh/simplex-noise@4.0.1",
"satellite.js": "https://esm.sh/satellite.js@5.0.0"
}
}
</script>
<link rel="stylesheet" href="css/base.css">
<link rel="stylesheet" href="css/info-panel.css">
<link rel="stylesheet" href="css/coordinates-display.css">
<link rel="stylesheet" href="css/legend.css">
<link rel="stylesheet" href="css/earth-stats.css">
</head>
<body>
<div id="container">
<div id="info-panel">
<h1>智能星球计划</h1>
<div class="subtitle">现实层宇宙全息感知系统 | 卫星 · 海底光缆 · 算力基础设施</div>
<div id="info-card" class="info-card" style="display: none;">
<div class="info-card-header">
<span class="info-card-icon" id="info-card-icon">🛰️</span>
<h3 id="info-card-title">详情</h3>
</div>
<div id="info-card-content"></div>
</div>
<div id="error-message" class="error-message"></div>
</div>
<div id="right-toolbar-group">
<div id="control-toolbar">
<div class="toolbar-items">
<button id="layout-toggle" class="toolbar-btn" title="最大化布局">
<span class="icon" aria-hidden="true">
<svg viewBox="0 0 24 24">
<path d="M9 9H5V5"></path>
<path d="M15 9h4V5"></path>
<path d="M9 15H5v4"></path>
<path d="M15 15h4v4"></path>
<path d="M5 5l5 5"></path>
<path d="M19 5l-5 5"></path>
<path d="M5 19l5-5"></path>
<path d="M19 19l-5-5"></path>
</svg>
</span>
<span class="tooltip">最大化布局</span>
</button>
<button id="rotate-toggle" class="toolbar-btn" title="自动旋转">
<span class="icon rotate-icon icon-pause" aria-hidden="true">
<svg viewBox="0 0 24 24">
<path d="M9 6v12"></path>
<path d="M15 6v12"></path>
</svg>
</span>
<span class="icon rotate-icon icon-play" aria-hidden="true">
<svg viewBox="0 0 24 24">
<path d="M8 6.5v11l9-5.5z" fill="currentColor" stroke="none"></path>
</svg>
</span>
<span class="tooltip">自动旋转</span>
</button>
<button id="toggle-cables" class="toolbar-btn active" title="显示/隐藏线缆">
<span class="icon" aria-hidden="true">
<svg viewBox="0 0 24 24">
<circle cx="12" cy="12" r="6.5"></circle>
<path d="M5.8 12h12.4"></path>
<path d="M12 5.8a8.5 8.5 0 0 1 0 12.4"></path>
<path d="M8 16c2-1.8 6-1.8 8 0"></path>
</svg>
</span>
<span class="tooltip">隐藏线缆</span>
</button>
<button id="toggle-terrain" class="toolbar-btn" title="显示/隐藏地形">
<span class="icon" aria-hidden="true">
<svg viewBox="0 0 24 24">
<path d="M3 18h18"></path>
<path d="M4.5 18l5-7 3 4 3.5-6 3.5 9"></path>
<path d="M11 18l2-3 1.5 2"></path>
</svg>
</span>
<span class="tooltip">显示/隐藏地形</span>
</button>
<button id="toggle-satellites" class="toolbar-btn" title="显示/隐藏卫星">
<span class="icon" aria-hidden="true">
<svg viewBox="0 0 24 24">
<rect x="10" y="10" width="4" height="4" rx="0.8"></rect>
<rect x="4" y="9" width="4" height="6" rx="0.8"></rect>
<rect x="16" y="9" width="4" height="6" rx="0.8"></rect>
<path d="M8 12h2"></path>
<path d="M14 12h2"></path>
<path d="M12 8V6"></path>
<path d="M11 6h2"></path>
<path d="M12 14v4"></path>
<path d="M10 18h4"></path>
</svg>
</span>
<span class="tooltip">显示卫星</span>
</button>
<button id="toggle-trails" class="toolbar-btn active" title="显示/隐藏轨迹">
<span class="icon" aria-hidden="true">
<svg viewBox="0 0 24 24">
<path d="M5 17h7"></path>
<path d="M7 13.5h8"></path>
<path d="M10 10h6"></path>
<circle cx="17.5" cy="8.5" r="2.2" fill="currentColor" stroke="none"></circle>
<path d="M15.8 10.2l2.8-2.8"></path>
</svg>
</span>
<span class="tooltip">隐藏轨迹</span>
</button>
<button id="reload-data" class="toolbar-btn" title="重新加载数据">
<span class="icon" aria-hidden="true">
<svg viewBox="0 0 24 24">
<path d="M20 5v5h-5"></path>
<path d="M20 10a8 8 0 1 0 2 5"></path>
</svg>
</span>
<span class="tooltip">重新加载数据</span>
</button>
</div>
<div class="toolbar-divider" aria-hidden="true"></div>
<div id="zoom-toolbar">
<button id="zoom-out" class="zoom-btn" title="缩小"><span class="tooltip">缩小</span></button>
<span id="zoom-value" class="zoom-percent" title="重置缩放到100%">100%<span class="tooltip">重置缩放到100%</span></span>
<button id="zoom-in" class="zoom-btn" title="放大">+<span class="tooltip">放大</span></button>
<button id="reset-view" class="zoom-btn" title="重置视角">
<svg viewBox="0 0 24 24" aria-hidden="true">
<circle cx="12" cy="12" r="5"></circle>
<path d="M12 3v4"></path>
<path d="M12 17v4"></path>
<path d="M3 12h4"></path>
<path d="M17 12h4"></path>
<circle cx="12" cy="12" r="1.5" fill="currentColor" stroke="none"></circle>
</svg>
<span class="tooltip">重置视角</span>
</button>
</div>
</div>
</div>
<div id="coordinates-display">
<h3 style="color:#4db8ff; margin-bottom:8px; font-size:1.1rem;">坐标信息</h3>
<div class="coord-item">
<span class="coord-label">经度:</span>
<span id="longitude-value" class="coord-value">0.00°</span>
</div>
<div class="coord-item">
<span class="coord-label">纬度:</span>
<span id="latitude-value" class="coord-value">0.00°</span>
</div>
<div id="zoom-level">缩放: 1.0x</div>
<div class="mouse-coords" id="mouse-coords">鼠标位置: 无</div>
</div>
<div id="legend">
<h3 style="color:#4db8ff; margin-bottom:10px; font-size:1.1rem;">图例</h3>
<div class="legend-item">
<div class="legend-color" style="background-color: #ff4444;"></div>
<span>Americas II</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background-color: #44ff44;"></div>
<span>AU Aleutian A</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background-color: #4444ff;"></div>
<span>AU Aleutian B</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background-color: #ffff44;"></div>
<span>其他电缆</span>
</div>
</div>
<div id="earth-stats">
<h3 style="color:#4db8ff; margin-bottom:10px; font-size:1.1rem;">地球信息</h3>
<div class="stats-item">
<span class="stats-label">电缆系统:</span>
<span class="stats-value" id="cable-count">0个</span>
</div>
<div class="stats-item">
<span class="stats-label">状态:</span>
<span class="stats-value" id="cable-status-summary">-</span>
</div>
<div class="stats-item">
<span class="stats-label">登陆点:</span>
<span class="stats-value" id="landing-point-count">0个</span>
</div>
<div class="stats-item">
<span class="stats-label">地形:</span>
<span class="stats-value" id="terrain-status">开启</span>
</div>
<div class="stats-item">
<span class="stats-label">卫星:</span>
<span class="stats-value" id="satellite-count">0 颗</span>
</div>
<div class="stats-item">
<span class="stats-label">视角距离:</span>
<span class="stats-value" id="camera-distance">300 km</span>
</div>
<div class="stats-item">
<span class="stats-label">纹理质量:</span>
<span class="stats-value" id="texture-quality">8K 卫星图</span>
</div>
</div>
<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>
<div id="status-message" class="status-message" style="display: none;"></div>
<div id="tooltip" class="tooltip"></div>
</div>
<script type="module" src="js/main.js"></script>
</body>
</html>

View File

@@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="6.75" stroke="#4DB8FF" stroke-width="2.1"/>
<path d="M5.75 12H18.25" stroke="#4DB8FF" stroke-width="2.1" stroke-linecap="round"/>
<path d="M12 5.8C14.7 7.75 14.7 16.25 12 18.2" stroke="#4DB8FF" stroke-width="2.1" stroke-linecap="round"/>
<path d="M8 16C9.95 14.2 14.05 14.2 16 16" stroke="#4DB8FF" stroke-width="2.1" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 480 B

View File

@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="8" stroke="#4DB8FF" stroke-width="2.2"/>
<path d="M12 10V16" stroke="#4DB8FF" stroke-width="2.2" stroke-linecap="round"/>
<circle cx="12" cy="7.25" r="1.25" fill="#4DB8FF"/>
</svg>

After

Width:  |  Height:  |  Size: 310 B

View File

@@ -0,0 +1,10 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 9L5 5" stroke="#4DB8FF" stroke-width="2.2" stroke-linecap="round"/>
<path d="M5 8V5H8" stroke="#4DB8FF" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15 9L19 5" stroke="#4DB8FF" stroke-width="2.2" stroke-linecap="round"/>
<path d="M16 5H19V8" stroke="#4DB8FF" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 15L5 19" stroke="#4DB8FF" stroke-width="2.2" stroke-linecap="round"/>
<path d="M5 16V19H8" stroke="#4DB8FF" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15 15L19 19" stroke="#4DB8FF" stroke-width="2.2" stroke-linecap="round"/>
<path d="M16 19H19V16" stroke="#4DB8FF" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 871 B

View File

@@ -0,0 +1,10 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 9.2L9.2 9.2L9.2 6" stroke="#4DB8FF" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18 9.2L14.8 9.2L14.8 6" stroke="#4DB8FF" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 14.8L9.2 14.8L9.2 18" stroke="#4DB8FF" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18 14.8L14.8 14.8L14.8 18" stroke="#4DB8FF" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 6L10 10" stroke="#4DB8FF" stroke-width="2.2" stroke-linecap="round"/>
<path d="M18 6L14 10" stroke="#4DB8FF" stroke-width="2.2" stroke-linecap="round"/>
<path d="M6 18L10 14" stroke="#4DB8FF" stroke-width="2.2" stroke-linecap="round"/>
<path d="M18 18L14 14" stroke="#4DB8FF" stroke-width="2.2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 927 B

View File

@@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 6.25V17.75" stroke="#4DB8FF" stroke-width="2.4" stroke-linecap="round"/>
<path d="M15 6.25V17.75" stroke="#4DB8FF" stroke-width="2.4" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 278 B

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.5 6.5L17.5 12L8.5 17.5V6.5Z" fill="#4DB8FF"/>
</svg>

After

Width:  |  Height:  |  Size: 163 B

View File

@@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.8 12.6C6.8 8.95 9.75 6 13.4 6C14.9 6 16.24 6.46 17.3 7.28" stroke="#4DB8FF" stroke-width="2.2" stroke-linecap="round"/>
<path d="M18.85 10.45C19.2 11.15 19.4 11.95 19.4 12.8C19.4 16.45 16.45 19.4 12.8 19.4C10.05 19.4 7.69 17.72 6.7 15.33" stroke="#4DB8FF" stroke-width="2.2" stroke-linecap="round"/>
<path d="M15.9 5.95H19.2V9.25" stroke="#4DB8FF" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M19.2 5.95L16.7 8.45" stroke="#4DB8FF" stroke-width="2.2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 631 B

View File

@@ -0,0 +1,8 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="5" stroke="#4DB8FF" stroke-width="2.2"/>
<path d="M12 3V6.5" stroke="#4DB8FF" stroke-width="2.2" stroke-linecap="round"/>
<path d="M12 17.5V21" stroke="#4DB8FF" stroke-width="2.2" stroke-linecap="round"/>
<path d="M3 12H6.5" stroke="#4DB8FF" stroke-width="2.2" stroke-linecap="round"/>
<path d="M17.5 12H21" stroke="#4DB8FF" stroke-width="2.2" stroke-linecap="round"/>
<circle cx="12" cy="12" r="1.45" fill="#4DB8FF"/>
</svg>

After

Width:  |  Height:  |  Size: 561 B

View File

@@ -0,0 +1,11 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="10" y="10" width="4" height="4" rx="0.8" stroke="#4DB8FF" stroke-width="2"/>
<rect x="4" y="9" width="4" height="6" rx="0.8" stroke="#4DB8FF" stroke-width="2"/>
<rect x="16" y="9" width="4" height="6" rx="0.8" stroke="#4DB8FF" stroke-width="2"/>
<path d="M8 12H10" stroke="#4DB8FF" stroke-width="2" stroke-linecap="round"/>
<path d="M14 12H16" stroke="#4DB8FF" stroke-width="2" stroke-linecap="round"/>
<path d="M12 8V6" stroke="#4DB8FF" stroke-width="2" stroke-linecap="round"/>
<path d="M10.75 6H13.25" stroke="#4DB8FF" stroke-width="2" stroke-linecap="round"/>
<path d="M12 14V18" stroke="#4DB8FF" stroke-width="2" stroke-linecap="round"/>
<path d="M10.25 18H13.75" stroke="#4DB8FF" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 858 B

View File

@@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="10.5" cy="10.5" r="5.75" stroke="#4DB8FF" stroke-width="2.2"/>
<path d="M15.2 15.2L19.25 19.25" stroke="#4DB8FF" stroke-width="2.2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 276 B

View File

@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 18H21" stroke="#4DB8FF" stroke-width="2.2" stroke-linecap="round"/>
<path d="M4.5 18L9.5 11L12.5 15L16 9L19.5 18" stroke="#4DB8FF" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.25 18L13.05 15.35L14.55 17.25" stroke="#4DB8FF" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 449 B

View File

@@ -0,0 +1,7 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 17H12" stroke="#4DB8FF" stroke-width="2.2" stroke-linecap="round"/>
<path d="M7 13.5H15" stroke="#4DB8FF" stroke-width="2.2" stroke-linecap="round"/>
<path d="M10 10H16" stroke="#4DB8FF" stroke-width="2.2" stroke-linecap="round"/>
<circle cx="17.5" cy="8.5" r="2.2" fill="#4DB8FF"/>
<path d="M15.8 10.2L18.55 7.45" stroke="#4DB8FF" stroke-width="2.2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 501 B

View File

@@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="10.5" cy="10.5" r="5.75" stroke="#4DB8FF" stroke-width="2.2"/>
<path d="M15.25 15.25L19.25 19.25" stroke="#4DB8FF" stroke-width="2.2" stroke-linecap="round"/>
<path d="M10.5 8V13" stroke="#4DB8FF" stroke-width="2.2" stroke-linecap="round"/>
<path d="M8 10.5H13" stroke="#4DB8FF" stroke-width="2.2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 446 B