Stabilize Earth module and fix satellite TLE handling
This commit is contained in:
355
frontend/public/earth/js/controls.js
vendored
355
frontend/public/earth/js/controls.js
vendored
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user