Stabilize Earth module and fix satellite TLE handling

This commit is contained in:
linkong
2026-03-26 10:29:50 +08:00
parent 3fd6cbb6f7
commit ce5feba3b9
14 changed files with 2132 additions and 1069 deletions

View File

@@ -1,11 +1,16 @@
// 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';
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;
@@ -13,8 +18,26 @@ export let showTerrain = false;
export let isDragging = 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);
@@ -29,39 +52,40 @@ function setupZoomControls(camera) {
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;
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 = setInterval(() => {
zoomInterval = window.setInterval(() => {
doContinuousZoom(direction);
}, LONG_PRESS_TICK);
}
function stopZoom() {
if (zoomInterval) {
clearInterval(zoomInterval);
@@ -72,15 +96,15 @@ function setupZoomControls(camera) {
holdTimeout = null;
}
}
function handleMouseDown(direction) {
startTime = Date.now();
stopZoom();
holdTimeout = setTimeout(() => {
holdTimeout = window.setTimeout(() => {
startContinuousZoom(direction);
}, HOLD_THRESHOLD);
}
function handleMouseUp(direction) {
const heldTime = Date.now() - startTime;
stopZoom();
@@ -88,48 +112,72 @@ function setupZoomControls(camera) {
doZoomStep(direction);
}
}
document.getElementById('zoom-in').addEventListener('mousedown', () => handleMouseDown(1));
document.getElementById('zoom-in').addEventListener('mouseup', () => handleMouseUp(1));
document.getElementById('zoom-in').addEventListener('mouseleave', stopZoom);
document.getElementById('zoom-in').addEventListener('touchstart', (e) => { e.preventDefault(); handleMouseDown(1); });
document.getElementById('zoom-in').addEventListener('touchend', () => handleMouseUp(1));
document.getElementById('zoom-out').addEventListener('mousedown', () => handleMouseDown(-1));
document.getElementById('zoom-out').addEventListener('mouseup', () => handleMouseUp(-1));
document.getElementById('zoom-out').addEventListener('mouseleave', stopZoom);
document.getElementById('zoom-out').addEventListener('touchstart', (e) => { e.preventDefault(); handleMouseDown(-1); });
document.getElementById('zoom-out').addEventListener('touchend', () => handleMouseUp(-1));
document.getElementById('zoom-value').addEventListener('click', function() {
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');
});
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) {
renderer.domElement.addEventListener('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 });
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) {
@@ -140,149 +188,186 @@ function applyZoom(camera) {
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 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');
});
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 }
(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);
}
if (typeof window.clearLockedCable === 'function') {
window.clearLockedCable();
animateToView(
EARTH_CONFIG.chinaLat,
EARTH_CONFIG.chinaLon,
EARTH_CONFIG.chinaRotLon,
);
}
clearLockedObject();
}
function setupRotateControls(camera, earth) {
const rotateBtn = document.getElementById('rotate-toggle');
rotateBtn.addEventListener('click', function() {
function setupRotateControls(camera) {
const rotateBtn = document.getElementById("rotate-toggle");
const resetViewBtn = document.getElementById("reset-view");
bindListener(rotateBtn, "click", () => {
const isRotating = toggleAutoRotate();
showStatusMessage(isRotating ? '自动旋转已开启' : '自动旋转已暂停', 'info');
showStatusMessage(isRotating ? "自动旋转已开启" : "自动旋转已暂停", "info");
});
updateRotateUI();
document.getElementById('reset-view').addEventListener('click', function() {
bindListener(resetViewBtn, "click", () => {
resetView(camera);
});
}
function setupTerrainControls() {
document.getElementById('toggle-terrain').addEventListener('click', function() {
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 reloadBtn = document.getElementById("reload-data");
const toolbarToggle = document.getElementById("toolbar-toggle");
const toolbar = document.getElementById("control-toolbar");
bindListener(terrainBtn, "click", function () {
showTerrain = !showTerrain;
toggleTerrain(showTerrain);
this.classList.toggle('active', showTerrain);
this.querySelector('.tooltip').textContent = showTerrain ? '隐藏地形' : '显示地形';
document.getElementById('terrain-status').textContent = showTerrain ? '开启' : '关闭';
showStatusMessage(showTerrain ? '地形已显示' : '地形已隐藏', 'info');
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");
});
document.getElementById('toggle-satellites').addEventListener('click', function() {
bindListener(satellitesBtn, "click", function () {
const showSats = !getShowSatellites();
if (!showSats) {
clearLockedObject();
}
toggleSatellites(showSats);
this.classList.toggle('active', showSats);
this.querySelector('.tooltip').textContent = showSats ? '隐藏卫星' : '显示卫星';
document.getElementById('satellite-count').textContent = getSatelliteCount() + ' 颗';
showStatusMessage(showSats ? '卫星已显示' : '卫星已隐藏', 'info');
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");
});
document.getElementById('toggle-trails').addEventListener('click', function() {
const isActive = this.classList.contains('active');
const showTrails = !isActive;
toggleTrails(showTrails);
this.classList.toggle('active', showTrails);
this.querySelector('.tooltip').textContent = showTrails ? '隐藏轨迹' : '显示轨迹';
showStatusMessage(showTrails ? '轨迹已显示' : '轨迹已隐藏', '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");
});
document.getElementById('toggle-cables').addEventListener('click', function() {
const showCables = !getShowCables();
if (!showCables) {
bindListener(cablesBtn, "click", function () {
const showNextCables = !getShowCables();
if (!showNextCables) {
clearLockedObject();
}
toggleCables(showCables);
this.classList.toggle('active', showCables);
this.querySelector('.tooltip').textContent = showCables ? '隐藏线缆' : '显示线缆';
showStatusMessage(showCables ? '线缆已显示' : '线缆已隐藏', 'info');
toggleCables(showNextCables);
this.classList.toggle("active", showNextCables);
const tooltip = this.querySelector(".tooltip");
if (tooltip) tooltip.textContent = showNextCables ? "隐藏线缆" : "显示线缆";
showStatusMessage(showNextCables ? "线缆已显示" : "线缆已隐藏", "info");
});
document.getElementById('reload-data').addEventListener('click', async () => {
bindListener(reloadBtn, "click", async () => {
await reloadData();
showStatusMessage('数据已重新加载', 'success');
});
const toolbarToggle = document.getElementById('toolbar-toggle');
const toolbar = document.getElementById('control-toolbar');
if (toolbarToggle && toolbar) {
toolbarToggle.addEventListener('click', () => {
toolbar.classList.toggle('collapsed');
bindListener(toolbarToggle, "click", () => {
toolbar.classList.toggle("collapsed");
});
}
}
export function teardownControls() {
resetCleanup();
}
export function getAutoRotate() {
return autoRotate;
}
function updateRotateUI() {
const btn = document.getElementById('rotate-toggle');
const btn = document.getElementById("rotate-toggle");
if (btn) {
btn.classList.toggle('active', autoRotate);
btn.innerHTML = autoRotate ? '⏸️' : '▶️';
const tooltip = btn.querySelector('.tooltip');
if (tooltip) tooltip.textContent = autoRotate ? '暂停旋转' : '开始旋转';
btn.classList.toggle("active", autoRotate);
btn.innerHTML = autoRotate ? "⏸️" : "▶️";
const tooltip = btn.querySelector(".tooltip");
if (tooltip) tooltip.textContent = autoRotate ? "暂停旋转" : "开始旋转";
}
}
@@ -294,9 +379,7 @@ export function setAutoRotate(value) {
export function toggleAutoRotate() {
autoRotate = !autoRotate;
updateRotateUI();
if (window.clearLockedCable) {
window.clearLockedCable();
}
clearLockedObject();
return autoRotate;
}