fix: 修复3D地球坐标映射多个严重bug
## Bug修复详情 ### 1. 致命错误:球面距离计算 (calculateDistance) - 问题:使用勾股定理计算经纬度距离,在球体表面完全错误 - 修复:改用Haversine公式计算球面大圆距离 - 影响:赤道1度=111km,极地1度=19km,原计算误差巨大 ### 2. 经度范围规范化 (vector3ToLatLon) - 问题:Math.atan2返回[-180°,180°],转换后可能超出标准范围 - 修复:添加while循环规范化到[-180, 180]区间 - 影响:避免本初子午线附近返回360°的异常值 ### 3. 屏幕坐标转换支持非全屏 (screenToEarthCoords) - 问题:假设Canvas永远全屏,非全屏时点击偏移严重 - 修复:新增domElement参数,使用getBoundingClientRect()计算相对坐标 - 影响:嵌入式3D地球组件也能精准拾取 ### 4. 地球旋转时经纬度映射错误 - 问题:Raycaster返回世界坐标,未考虑地球自转 - 修复:使用earth.worldToLocal()转换到本地坐标空间 - 影响:地球旋转时经纬度显示正确跟随 ## 新增功能 - CelesTrak卫星数据采集器 - Space-Track卫星数据采集器 - 卫星可视化模块(500颗,实时SGP4轨道计算) - 海底光缆悬停显示info-card - 统一info-card组件 - 工具栏按钮(Stellarium风格) - 缩放控制(百分比显示) - Docker volume映射(代码热更新) ## 文件变更 - utils.js: 坐标转换核心逻辑修复 - satellites.js: 新增卫星可视化 - cables.js: 悬停交互支持 - main.js: 悬停/锁定逻辑 - controls.js: 工具栏UI - info-card.js: 统一卡片组件 - docker-compose.yml: volume映射 - restart.sh: 简化重启脚本
This commit is contained in:
144
frontend/public/earth/js/controls.js
vendored
144
frontend/public/earth/js/controls.js
vendored
@@ -3,6 +3,8 @@
|
||||
import { CONFIG } from './constants.js';
|
||||
import { updateZoomDisplay, showStatusMessage } from './ui.js';
|
||||
import { toggleTerrain } from './earth.js';
|
||||
import { reloadData } from './main.js';
|
||||
import { toggleSatellites, toggleTrails, getShowSatellites, getSatelliteCount } from './satellites.js';
|
||||
|
||||
export let autoRotate = true;
|
||||
export let zoomLevel = 1.0;
|
||||
@@ -20,20 +22,86 @@ export function setupControls(camera, renderer, scene, earth) {
|
||||
}
|
||||
|
||||
function setupZoomControls(camera) {
|
||||
document.getElementById('zoom-in').addEventListener('click', () => {
|
||||
zoomLevel = Math.min(zoomLevel + 0.5, CONFIG.maxZoom);
|
||||
applyZoom(camera);
|
||||
});
|
||||
let zoomInterval = null;
|
||||
let lastDirection = 0;
|
||||
let isSnapped = false;
|
||||
|
||||
document.getElementById('zoom-out').addEventListener('click', () => {
|
||||
zoomLevel = Math.max(zoomLevel - 0.5, CONFIG.minZoom);
|
||||
applyZoom(camera);
|
||||
});
|
||||
const MIN_PERCENT = CONFIG.minZoom * 100;
|
||||
const MAX_PERCENT = CONFIG.maxZoom * 100;
|
||||
|
||||
document.getElementById('zoom-reset').addEventListener('click', () => {
|
||||
zoomLevel = 1.0;
|
||||
function adjustZoom(direction) {
|
||||
const currentPercent = Math.round(zoomLevel * 100);
|
||||
let newPercent;
|
||||
|
||||
if (direction > 0) {
|
||||
if (!isSnapped || currentPercent % 10 !== 0) {
|
||||
newPercent = Math.ceil(currentPercent / 10) * 10;
|
||||
if (newPercent <= currentPercent) newPercent = currentPercent + 10;
|
||||
isSnapped = true;
|
||||
} else {
|
||||
newPercent = currentPercent + 10;
|
||||
}
|
||||
} else {
|
||||
if (!isSnapped || currentPercent % 10 !== 0) {
|
||||
newPercent = Math.floor(currentPercent / 10) * 10;
|
||||
if (newPercent >= currentPercent) newPercent = currentPercent - 10;
|
||||
isSnapped = true;
|
||||
} else {
|
||||
newPercent = currentPercent - 10;
|
||||
}
|
||||
}
|
||||
|
||||
if (newPercent > MAX_PERCENT) newPercent = MAX_PERCENT;
|
||||
if (newPercent < MIN_PERCENT) newPercent = MIN_PERCENT;
|
||||
|
||||
zoomLevel = newPercent / 100;
|
||||
applyZoom(camera);
|
||||
showStatusMessage('缩放已重置', 'info');
|
||||
}
|
||||
|
||||
function startZoom(direction) {
|
||||
isSnapped = false;
|
||||
lastDirection = direction;
|
||||
adjustZoom(direction);
|
||||
zoomInterval = setInterval(() => {
|
||||
adjustZoom(direction);
|
||||
}, 150);
|
||||
}
|
||||
|
||||
function stopZoom() {
|
||||
if (zoomInterval) {
|
||||
clearInterval(zoomInterval);
|
||||
zoomInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('zoom-in').addEventListener('mousedown', () => startZoom(1));
|
||||
document.getElementById('zoom-in').addEventListener('mouseup', stopZoom);
|
||||
document.getElementById('zoom-in').addEventListener('mouseleave', stopZoom);
|
||||
document.getElementById('zoom-in').addEventListener('touchstart', (e) => { e.preventDefault(); startZoom(1); });
|
||||
document.getElementById('zoom-in').addEventListener('touchend', stopZoom);
|
||||
|
||||
document.getElementById('zoom-out').addEventListener('mousedown', () => startZoom(-1));
|
||||
document.getElementById('zoom-out').addEventListener('mouseup', stopZoom);
|
||||
document.getElementById('zoom-out').addEventListener('mouseleave', stopZoom);
|
||||
document.getElementById('zoom-out').addEventListener('touchstart', (e) => { e.preventDefault(); startZoom(-1); });
|
||||
document.getElementById('zoom-out').addEventListener('touchend', stopZoom);
|
||||
|
||||
document.getElementById('zoom-value').addEventListener('click', function() {
|
||||
const startZoom = zoomLevel;
|
||||
const targetZoom = 1.0;
|
||||
const startDistance = CONFIG.defaultCameraZ / startZoom;
|
||||
const targetDistance = CONFIG.defaultCameraZ / targetZoom;
|
||||
|
||||
animateValue(0, 1, 600, (progress) => {
|
||||
const ease = 1 - Math.pow(1 - progress, 3);
|
||||
zoomLevel = startZoom + (targetZoom - startZoom) * ease;
|
||||
camera.position.z = CONFIG.defaultCameraZ / zoomLevel;
|
||||
const distance = startDistance + (targetDistance - startDistance) * ease;
|
||||
updateZoomDisplay(zoomLevel, distance.toFixed(0));
|
||||
}, () => {
|
||||
zoomLevel = 1.0;
|
||||
showStatusMessage('缩放已重置到100%', 'info');
|
||||
});
|
||||
});
|
||||
|
||||
const slider = document.getElementById('zoom-slider');
|
||||
@@ -111,13 +179,12 @@ export function resetView(camera) {
|
||||
}
|
||||
|
||||
function setupRotateControls(camera, earth) {
|
||||
document.getElementById('rotate-toggle').addEventListener('click', () => {
|
||||
document.getElementById('rotate-toggle').addEventListener('click', function() {
|
||||
toggleAutoRotate();
|
||||
const isOn = autoRotate;
|
||||
showStatusMessage(isOn ? '自动旋转已开启' : '自动旋转已暂停', 'info');
|
||||
showStatusMessage(autoRotate ? '自动旋转已开启' : '自动旋转已暂停', 'info');
|
||||
});
|
||||
|
||||
document.getElementById('reset-view').addEventListener('click', () => {
|
||||
document.getElementById('reset-view').addEventListener('click', function() {
|
||||
if (!earthObj) return;
|
||||
|
||||
const startRotX = earthObj.rotation.x;
|
||||
@@ -143,18 +210,45 @@ function setupRotateControls(camera, earth) {
|
||||
}
|
||||
|
||||
function setupTerrainControls() {
|
||||
document.getElementById('toggle-terrain').addEventListener('click', () => {
|
||||
document.getElementById('toggle-terrain').addEventListener('click', function() {
|
||||
showTerrain = !showTerrain;
|
||||
toggleTerrain(showTerrain);
|
||||
const btn = document.getElementById('toggle-terrain');
|
||||
btn.textContent = showTerrain ? '隐藏地形' : '显示地形';
|
||||
this.classList.toggle('active', showTerrain);
|
||||
this.querySelector('.tooltip').textContent = showTerrain ? '隐藏地形' : '显示地形';
|
||||
document.getElementById('terrain-status').textContent = showTerrain ? '开启' : '关闭';
|
||||
showStatusMessage(showTerrain ? '地形已显示' : '地形已隐藏', 'info');
|
||||
});
|
||||
|
||||
document.getElementById('reload-data').addEventListener('click', () => {
|
||||
showStatusMessage('重新加载数据...', 'info');
|
||||
window.location.reload();
|
||||
document.getElementById('toggle-satellites').addEventListener('click', function() {
|
||||
const showSats = !getShowSatellites();
|
||||
toggleSatellites(showSats);
|
||||
this.classList.toggle('active', showSats);
|
||||
this.querySelector('.tooltip').textContent = showSats ? '隐藏卫星' : '显示卫星';
|
||||
document.getElementById('satellite-count').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');
|
||||
});
|
||||
|
||||
document.getElementById('reload-data').addEventListener('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');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function setupMouseControls(camera, renderer) {
|
||||
@@ -192,7 +286,9 @@ export function setAutoRotate(value) {
|
||||
autoRotate = value;
|
||||
const btn = document.getElementById('rotate-toggle');
|
||||
if (btn) {
|
||||
btn.textContent = autoRotate ? '暂停旋转' : '开始旋转';
|
||||
btn.classList.toggle('active', value);
|
||||
const tooltip = btn.querySelector('.tooltip');
|
||||
if (tooltip) tooltip.textContent = value ? '暂停旋转' : '自动旋转';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,7 +296,9 @@ export function toggleAutoRotate() {
|
||||
autoRotate = !autoRotate;
|
||||
const btn = document.getElementById('rotate-toggle');
|
||||
if (btn) {
|
||||
btn.textContent = autoRotate ? '暂停旋转' : '开始旋转';
|
||||
btn.classList.toggle('active', autoRotate);
|
||||
const tooltip = btn.querySelector('.tooltip');
|
||||
if (tooltip) tooltip.textContent = autoRotate ? '暂停旋转' : '自动旋转';
|
||||
}
|
||||
if (window.clearLockedCable) {
|
||||
window.clearLockedCable();
|
||||
|
||||
Reference in New Issue
Block a user