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:
321
frontend/public/earth/js/satellites.js
Normal file
321
frontend/public/earth/js/satellites.js
Normal file
@@ -0,0 +1,321 @@
|
||||
// satellites.js - Satellite visualization module with real SGP4 positions and animations
|
||||
|
||||
import * as THREE from 'three';
|
||||
import { twoline2satrec, sgp4, propagate, degreesToRadians, radiansToDegrees, eciToGeodetic } from 'satellite.js';
|
||||
import { CONFIG } from './constants.js';
|
||||
|
||||
let satellitePoints = null;
|
||||
let satelliteTrails = null;
|
||||
let satelliteData = [];
|
||||
let showSatellites = false;
|
||||
let showTrails = true;
|
||||
let animationTime = 0;
|
||||
let selectedSatellite = null;
|
||||
let satellitePositions = [];
|
||||
|
||||
const SATELLITE_API = '/api/v1/visualization/geo/satellites?limit=2000';
|
||||
const MAX_SATELLITES = 500;
|
||||
const TRAIL_LENGTH = 30;
|
||||
|
||||
export function createSatellites(scene, earthObj) {
|
||||
const positions = new Float32Array(MAX_SATELLITES * 3);
|
||||
const colors = new Float32Array(MAX_SATELLITES * 3);
|
||||
|
||||
const pointsGeometry = new THREE.BufferGeometry();
|
||||
pointsGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
||||
pointsGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
||||
|
||||
const pointsMaterial = new THREE.PointsMaterial({
|
||||
size: 3,
|
||||
vertexColors: true,
|
||||
transparent: true,
|
||||
opacity: 0.9,
|
||||
sizeAttenuation: true
|
||||
});
|
||||
|
||||
satellitePoints = new THREE.Points(pointsGeometry, pointsMaterial);
|
||||
satellitePoints.visible = false;
|
||||
satellitePoints.userData = { type: 'satellitePoints' };
|
||||
earthObj.add(satellitePoints);
|
||||
|
||||
const trailPositions = new Float32Array(MAX_SATELLITES * TRAIL_LENGTH * 3);
|
||||
const trailColors = new Float32Array(MAX_SATELLITES * TRAIL_LENGTH * 3);
|
||||
|
||||
const trailGeometry = new THREE.BufferGeometry();
|
||||
trailGeometry.setAttribute('position', new THREE.BufferAttribute(trailPositions, 3));
|
||||
trailGeometry.setAttribute('color', new THREE.BufferAttribute(trailColors, 3));
|
||||
|
||||
const trailMaterial = new THREE.LineBasicMaterial({
|
||||
vertexColors: true,
|
||||
transparent: true,
|
||||
opacity: 0.3,
|
||||
blending: THREE.AdditiveBlending
|
||||
});
|
||||
|
||||
satelliteTrails = new THREE.LineSegments(trailGeometry, trailMaterial);
|
||||
satelliteTrails.visible = false;
|
||||
satelliteTrails.userData = { type: 'satelliteTrails' };
|
||||
earthObj.add(satelliteTrails);
|
||||
|
||||
satellitePositions = [];
|
||||
for (let i = 0; i < MAX_SATELLITES; i++) {
|
||||
satellitePositions.push({
|
||||
current: new THREE.Vector3(),
|
||||
trail: []
|
||||
});
|
||||
}
|
||||
|
||||
return satellitePoints;
|
||||
}
|
||||
|
||||
function computeSatellitePosition(satellite, time) {
|
||||
try {
|
||||
const props = satellite.properties;
|
||||
if (!props || !props.norad_cat_id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const noradId = props.norad_cat_id;
|
||||
const inclination = props.inclination || 53;
|
||||
const raan = props.raan || 0;
|
||||
const eccentricity = props.eccentricity || 0.0001;
|
||||
const argOfPerigee = props.arg_of_perigee || 0;
|
||||
const meanAnomaly = props.mean_anomaly || 0;
|
||||
const meanMotion = props.mean_motion || 15;
|
||||
const epoch = props.epoch || '';
|
||||
|
||||
const year = epoch && epoch.length >= 4 ? parseInt(epoch.substring(0, 4)) : time.getUTCFullYear();
|
||||
const month = epoch && epoch.length >= 7 ? parseInt(epoch.substring(5, 7)) : time.getUTCMonth() + 1;
|
||||
const day = epoch && epoch.length >= 10 ? parseInt(epoch.substring(8, 10)) : time.getUTCDate();
|
||||
|
||||
const tleLine1 = `1 ${String(noradId).padStart(5, '0')}U 00001A ${year}${String(month).padStart(2, '0')}${String(day).padStart(2, '0')}.00000000 .00000000 00000-0 00000-0 0 9999`;
|
||||
const tleLine2 = `2 ${String(noradId).padStart(5, '0')} ${String(raan.toFixed(4)).padStart(8, ' ')} ${String(inclination.toFixed(4)).padStart(8, ' ')} ${String(eccentricity.toFixed(7)).replace('0.', '.')} ${String(argOfPerigee.toFixed(4)).padStart(8, ' ')} ${String(meanAnomaly.toFixed(4)).padStart(8, ' ')} ${String(meanMotion.toFixed(8)).padStart(11, ' ')} 0 9999`;
|
||||
|
||||
const satrec = twoline2satrec(tleLine1, tleLine2);
|
||||
if (!satrec || satrec.error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const positionAndVelocity = propagate(satrec, time);
|
||||
if (!positionAndVelocity || !positionAndVelocity.position) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const x = positionAndVelocity.position.x;
|
||||
const y = positionAndVelocity.position.y;
|
||||
const z = positionAndVelocity.position.z;
|
||||
|
||||
if (!x || !y || !z) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const r = Math.sqrt(x * x + y * y + z * z);
|
||||
const earthRadius = 6371;
|
||||
const displayRadius = CONFIG.earthRadius * (earthRadius / 6371) * 1.05;
|
||||
|
||||
const scale = displayRadius / r;
|
||||
|
||||
return new THREE.Vector3(x * scale, y * scale, z * scale);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function generateFallbackPosition(satellite, index, total) {
|
||||
const radius = CONFIG.earthRadius + 5;
|
||||
|
||||
const noradId = satellite.properties?.norad_cat_id || index;
|
||||
const inclination = satellite.properties?.inclination || 53;
|
||||
const raan = satellite.properties?.raan || 0;
|
||||
const meanAnomaly = satellite.properties?.mean_anomaly || 0;
|
||||
|
||||
const hash = String(noradId).split('').reduce((a, b) => a + b.charCodeAt(0), 0);
|
||||
const randomOffset = (hash % 1000) / 1000;
|
||||
|
||||
const normalizedIndex = index / total;
|
||||
const theta = normalizedIndex * Math.PI * 2 * 10 + (raan * Math.PI / 180);
|
||||
const phi = (inclination * Math.PI / 180) + (meanAnomaly * Math.PI / 180 * 0.1);
|
||||
|
||||
const adjustedPhi = Math.abs(phi % Math.PI);
|
||||
const adjustedTheta = theta + randomOffset * Math.PI * 2;
|
||||
|
||||
const x = radius * Math.sin(adjustedPhi) * Math.cos(adjustedTheta);
|
||||
const y = radius * Math.cos(adjustedPhi);
|
||||
const z = radius * Math.sin(adjustedPhi) * Math.sin(adjustedTheta);
|
||||
|
||||
return new THREE.Vector3(x, y, z);
|
||||
}
|
||||
|
||||
export async function loadSatellites() {
|
||||
try {
|
||||
const response = await fetch(SATELLITE_API);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
satelliteData = data.features || [];
|
||||
|
||||
console.log(`Loaded ${satelliteData.length} satellites`);
|
||||
return satelliteData;
|
||||
} catch (error) {
|
||||
console.error('Failed to load satellites:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function updateSatellitePositions(deltaTime = 0) {
|
||||
if (!satellitePoints || satelliteData.length === 0) return;
|
||||
|
||||
animationTime += deltaTime * 0.001;
|
||||
|
||||
const positions = satellitePoints.geometry.attributes.position.array;
|
||||
const colors = satellitePoints.geometry.attributes.color.array;
|
||||
|
||||
const trailPositions = satelliteTrails.geometry.attributes.position.array;
|
||||
const trailColors = satelliteTrails.geometry.attributes.color.array;
|
||||
|
||||
const baseTime = new Date();
|
||||
const count = Math.min(satelliteData.length, 500);
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const satellite = satelliteData[i];
|
||||
const props = satellite.properties;
|
||||
|
||||
const timeOffset = (i / count) * 2 * Math.PI * 0.1;
|
||||
const adjustedTime = new Date(baseTime.getTime() + timeOffset * 1000 * 60 * 10);
|
||||
|
||||
let pos = computeSatellitePosition(satellite, adjustedTime);
|
||||
|
||||
if (!pos) {
|
||||
pos = generateFallbackPosition(satellite, i, count);
|
||||
}
|
||||
|
||||
satellitePositions[i].current.copy(pos);
|
||||
|
||||
satellitePositions[i].trail.push(pos.clone());
|
||||
if (satellitePositions[i].trail.length > TRAIL_LENGTH) {
|
||||
satellitePositions[i].trail.shift();
|
||||
}
|
||||
|
||||
positions[i * 3] = pos.x;
|
||||
positions[i * 3 + 1] = pos.y;
|
||||
positions[i * 3 + 2] = pos.z;
|
||||
|
||||
const inclination = props?.inclination || 53;
|
||||
const name = props?.name || '';
|
||||
const isStarlink = name.includes('STARLINK');
|
||||
const isGeo = inclination > 20 && inclination < 30;
|
||||
const isIridium = name.includes('IRIDIUM');
|
||||
|
||||
let r, g, b;
|
||||
if (isStarlink) {
|
||||
r = 0.0; g = 0.9; b = 1.0;
|
||||
} else if (isGeo) {
|
||||
r = 1.0; g = 0.8; b = 0.0;
|
||||
} else if (isIridium) {
|
||||
r = 1.0; g = 0.5; b = 0.0;
|
||||
} else if (inclination > 50 && inclination < 70) {
|
||||
r = 0.0; g = 1.0; b = 0.3;
|
||||
} else {
|
||||
r = 1.0; g = 1.0; b = 1.0;
|
||||
}
|
||||
|
||||
colors[i * 3] = r;
|
||||
colors[i * 3 + 1] = g;
|
||||
colors[i * 3 + 2] = b;
|
||||
|
||||
const trail = satellitePositions[i].trail;
|
||||
for (let j = 0; j < TRAIL_LENGTH; j++) {
|
||||
const trailIdx = (i * TRAIL_LENGTH + j) * 3;
|
||||
|
||||
if (j < trail.length) {
|
||||
const t = trail[j];
|
||||
trailPositions[trailIdx] = t.x;
|
||||
trailPositions[trailIdx + 1] = t.y;
|
||||
trailPositions[trailIdx + 2] = t.z;
|
||||
|
||||
const alpha = j / trail.length;
|
||||
trailColors[trailIdx] = r * alpha;
|
||||
trailColors[trailIdx + 1] = g * alpha;
|
||||
trailColors[trailIdx + 2] = b * alpha;
|
||||
} else {
|
||||
trailPositions[trailIdx] = 0;
|
||||
trailPositions[trailIdx + 1] = 0;
|
||||
trailPositions[trailIdx + 2] = 0;
|
||||
trailColors[trailIdx] = 0;
|
||||
trailColors[trailIdx + 1] = 0;
|
||||
trailColors[trailIdx + 2] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = count; i < 2000; i++) {
|
||||
positions[i * 3] = 0;
|
||||
positions[i * 3 + 1] = 0;
|
||||
positions[i * 3 + 2] = 0;
|
||||
|
||||
for (let j = 0; j < TRAIL_LENGTH; j++) {
|
||||
const trailIdx = (i * TRAIL_LENGTH + j) * 3;
|
||||
trailPositions[trailIdx] = 0;
|
||||
trailPositions[trailIdx + 1] = 0;
|
||||
trailPositions[trailIdx + 2] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
satellitePoints.geometry.attributes.position.needsUpdate = true;
|
||||
satellitePoints.geometry.attributes.color.needsUpdate = true;
|
||||
satellitePoints.geometry.setDrawRange(0, count);
|
||||
|
||||
satelliteTrails.geometry.attributes.position.needsUpdate = true;
|
||||
satelliteTrails.geometry.attributes.color.needsUpdate = true;
|
||||
}
|
||||
|
||||
export function toggleSatellites(visible) {
|
||||
showSatellites = visible;
|
||||
if (satellitePoints) {
|
||||
satellitePoints.visible = visible;
|
||||
}
|
||||
if (satelliteTrails) {
|
||||
satelliteTrails.visible = visible && showTrails;
|
||||
}
|
||||
}
|
||||
|
||||
export function toggleTrails(visible) {
|
||||
showTrails = visible;
|
||||
if (satelliteTrails) {
|
||||
satelliteTrails.visible = visible && showSatellites;
|
||||
}
|
||||
}
|
||||
|
||||
export function getShowSatellites() {
|
||||
return showSatellites;
|
||||
}
|
||||
|
||||
export function getSatelliteCount() {
|
||||
return satelliteData.length;
|
||||
}
|
||||
|
||||
export function getSatelliteAt(index) {
|
||||
if (index >= 0 && index < satelliteData.length) {
|
||||
return satelliteData[index];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getSatelliteData() {
|
||||
return satelliteData;
|
||||
}
|
||||
|
||||
export function selectSatellite(index) {
|
||||
selectedSatellite = index;
|
||||
return getSatelliteAt(index);
|
||||
}
|
||||
|
||||
export function getSelectedSatellite() {
|
||||
return selectedSatellite;
|
||||
}
|
||||
|
||||
export function getSatellitePoints() {
|
||||
return satellitePoints;
|
||||
}
|
||||
Reference in New Issue
Block a user