diff --git a/frontend/public/earth/_backup/dock-centered-20260326/base.css b/frontend/public/earth/_backup/dock-centered-20260326/base.css new file mode 100644 index 00000000..5337baa4 --- /dev/null +++ b/frontend/public/earth/_backup/dock-centered-20260326/base.css @@ -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); +} diff --git a/frontend/public/earth/_backup/dock-centered-20260326/controls.js b/frontend/public/earth/_backup/dock-centered-20260326/controls.js new file mode 100644 index 00000000..81333aa4 --- /dev/null +++ b/frontend/public/earth/_backup/dock-centered-20260326/controls.js @@ -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; +} diff --git a/frontend/public/earth/_backup/dock-centered-20260326/index.html b/frontend/public/earth/_backup/dock-centered-20260326/index.html new file mode 100644 index 00000000..476511b6 --- /dev/null +++ b/frontend/public/earth/_backup/dock-centered-20260326/index.html @@ -0,0 +1,227 @@ + + +
+ + +