feat(earth): add predicted orbit display for locked satellites
- Calculate orbital period from meanMotion - Generate predicted orbit points with 10s sampling - Show complete orbit line when satellite is locked - Hide orbit when satellite is unlocked - Color gradient: bright (current) to dark (end) - Fix TLE epoch format issue with fallback circle orbit - Add visibility change handler to clear trails on page hide - Fix satellite count display after loading - Merge predicted-orbit plan into single file
This commit is contained in:
@@ -60,6 +60,11 @@ export const SATELLITE_CONFIG = {
|
||||
apiPath: '/api/v1/visualization/geo/satellites'
|
||||
};
|
||||
|
||||
export const PREDICTED_ORBIT_CONFIG = {
|
||||
sampleInterval: 10,
|
||||
opacity: 0.8
|
||||
};
|
||||
|
||||
export const GRID_CONFIG = {
|
||||
latitudeStep: 10,
|
||||
longitudeStep: 30,
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
} from './ui.js';
|
||||
import { createEarth, createClouds, createTerrain, createStars, createGridLines, toggleTerrain, getEarth } from './earth.js';
|
||||
import { loadGeoJSONFromPath, loadLandingPoints, handleCableClick, clearCableSelection, getCableLines, getCablesById, lockedCable as cableLocked, getCableState, setCableState, clearAllCableStates, applyLandingPointVisualState, resetLandingPointVisualState, getAllLandingPoints, getShowCables } from './cables.js';
|
||||
import { createSatellites, loadSatellites, updateSatellitePositions, toggleSatellites, toggleTrails, getShowSatellites, selectSatellite, getSatelliteData, getSatellitePoints, setSatelliteRingState, updateLockedRingPosition, updateHoverRingPosition, getSatellitePositions } from './satellites.js';
|
||||
import { createSatellites, loadSatellites, updateSatellitePositions, toggleSatellites, toggleTrails, getShowSatellites, getSatelliteCount, selectSatellite, getSatelliteData, getSatellitePoints, setSatelliteRingState, updateLockedRingPosition, updateHoverRingPosition, getSatellitePositions, showPredictedOrbit, hidePredictedOrbit } from './satellites.js';
|
||||
import { setupControls, getAutoRotate, getShowTerrain, zoomLevel, setAutoRotate, toggleAutoRotate, resetView } from './controls.js';
|
||||
import { initInfoCard, showInfoCard, hideInfoCard, getCurrentType, setInfoCardNoBorder } from './info-card.js';
|
||||
|
||||
@@ -34,6 +34,7 @@ let dragStartTime = 0;
|
||||
let isLongDrag = false;
|
||||
|
||||
export function clearLockedObject() {
|
||||
hidePredictedOrbit();
|
||||
hoveredCable = null;
|
||||
hoveredSatellite = null;
|
||||
hoveredSatelliteIndex = null;
|
||||
@@ -202,6 +203,7 @@ async function loadData(showWhiteSphere = false) {
|
||||
(async () => {
|
||||
const satCount = await loadSatellites();
|
||||
console.log(`卫星数据加载完成: ${satCount} 颗`);
|
||||
document.getElementById('satellite-count').textContent = satCount + ' 颗';
|
||||
updateSatellitePositions();
|
||||
console.log('卫星位置已更新');
|
||||
toggleSatellites(true);
|
||||
@@ -438,6 +440,7 @@ function onClick(event, camera, renderer) {
|
||||
lockedObjectType = 'satellite';
|
||||
lockedSatellite = sat;
|
||||
lockedSatelliteIndex = index;
|
||||
showPredictedOrbit(sat);
|
||||
setAutoRotate(false);
|
||||
|
||||
const satPositions = getSatellitePositions();
|
||||
|
||||
@@ -18,7 +18,6 @@ let lockedRingSprite = null;
|
||||
const SATELLITE_API = SATELLITE_CONFIG.apiPath + '?limit=' + SATELLITE_CONFIG.maxCount;
|
||||
const MAX_SATELLITES = SATELLITE_CONFIG.maxCount;
|
||||
const TRAIL_LENGTH = SATELLITE_CONFIG.trailLength;
|
||||
const TRAIL_MAX_AGE_MS = 5000; // 5 seconds
|
||||
const DOT_TEXTURE_SIZE = 32;
|
||||
|
||||
function createCircularDotTexture() {
|
||||
@@ -136,15 +135,26 @@ function computeSatellitePosition(satellite, time) {
|
||||
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();
|
||||
// Simplified epoch calculation
|
||||
let epochDate = epoch && epoch.length >= 10 ? new Date(epoch) : time;
|
||||
const epochYear = epochDate.getUTCFullYear() % 100;
|
||||
const startOfYear = new Date(Date.UTC(epochDate.getUTCFullYear(), 0, 1));
|
||||
const dayOfYear = Math.floor((epochDate - startOfYear) / 86400000) + 1;
|
||||
const msOfDay = epochDate.getUTCHours() * 3600000 + epochDate.getUTCMinutes() * 60000 + epochDate.getUTCSeconds() * 1000 + epochDate.getUTCMilliseconds();
|
||||
const dayFraction = msOfDay / 86400000;
|
||||
const epochStr = String(epochYear).padStart(2, '0') + String(dayOfYear).padStart(3, '0') + '.' + dayFraction.toFixed(8).substring(2);
|
||||
|
||||
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`;
|
||||
// Format eccentricity as "0.0001652" (7 chars after decimal)
|
||||
const eccStr = '0' + eccentricity.toFixed(7);
|
||||
const tleLine1 = `1 ${noradId.toString().padStart(5)}U 00001A ${epochStr} .00000000 00000-0 00000-0 0 9999`;
|
||||
const tleLine2 = `2 ${noradId.toString().padStart(5)} ${raan.toFixed(4).padStart(8)} ${inclination.toFixed(4).padStart(8)} ${eccStr.substring(1)} ${argOfPerigee.toFixed(4).padStart(8)} ${meanAnomaly.toFixed(4).padStart(8)} ${meanMotion.toFixed(8).padStart(11)} 0 9999`;
|
||||
|
||||
console.log('[DEBUG computeSat] TLE1:', tleLine1);
|
||||
console.log('[DEBUG computeSat] TLE2:', tleLine2);
|
||||
const satrec = twoline2satrec(tleLine1, tleLine2);
|
||||
console.log('[DEBUG computeSat] satrec.error:', satrec?.error, 'satrec.no:', satrec?.no);
|
||||
if (!satrec || satrec.error) {
|
||||
console.log('[DEBUG computeSat] returning null due to satrec error');
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -209,7 +219,7 @@ export async function loadSatellites() {
|
||||
satelliteData = data.features || [];
|
||||
|
||||
console.log(`Loaded ${satelliteData.length} satellites`);
|
||||
return satelliteData;
|
||||
return satelliteData.length;
|
||||
} catch (error) {
|
||||
console.error('Failed to load satellites:', error);
|
||||
return [];
|
||||
@@ -245,10 +255,10 @@ export function updateSatellitePositions(deltaTime = 0) {
|
||||
|
||||
satellitePositions[i].current.copy(pos);
|
||||
|
||||
satellitePositions[i].trail.push({ pos: pos.clone(), time: Date.now() });
|
||||
satellitePositions[i].trail = satellitePositions[i].trail.filter(
|
||||
p => Date.now() - p.time < TRAIL_MAX_AGE_MS
|
||||
);
|
||||
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;
|
||||
@@ -282,7 +292,7 @@ export function updateSatellitePositions(deltaTime = 0) {
|
||||
const trailIdx = (i * TRAIL_LENGTH + j) * 3;
|
||||
|
||||
if (j < trail.length) {
|
||||
const t = trail[j].pos;
|
||||
const t = trail[j];
|
||||
trailPositions[trailIdx] = t.x;
|
||||
trailPositions[trailIdx + 1] = t.y;
|
||||
trailPositions[trailIdx + 2] = t.z;
|
||||
@@ -455,3 +465,99 @@ export function initSatelliteScene(scene, earth) {
|
||||
sceneRef = scene;
|
||||
earthObjRef = earth;
|
||||
}
|
||||
|
||||
let predictedOrbitLine = null;
|
||||
|
||||
function calculateOrbitalPeriod(meanMotion) {
|
||||
return 86400 / meanMotion;
|
||||
}
|
||||
|
||||
function calculatePredictedOrbit(satellite, periodSeconds, sampleInterval = 10) {
|
||||
const points = [];
|
||||
const samples = Math.ceil(periodSeconds / sampleInterval);
|
||||
const now = new Date();
|
||||
|
||||
// Full orbit: from now to now+period (complete circle forward)
|
||||
for (let i = 0; i <= samples; i++) {
|
||||
const time = new Date(now.getTime() + i * sampleInterval * 1000);
|
||||
const pos = computeSatellitePosition(satellite, time);
|
||||
if (pos) points.push(pos);
|
||||
}
|
||||
|
||||
// If we don't have enough points, use fallback orbit
|
||||
if (points.length < samples * 0.5) {
|
||||
points.length = 0;
|
||||
const radius = CONFIG.earthRadius + 5;
|
||||
const noradId = satellite.properties?.norad_cat_id || 0;
|
||||
const inclination = satellite.properties?.inclination || 53;
|
||||
const raan = satellite.properties?.raan || 0;
|
||||
const meanAnomaly = satellite.properties?.mean_anomaly || 0;
|
||||
|
||||
for (let i = 0; i <= samples; i++) {
|
||||
const theta = (i / samples) * Math.PI * 2;
|
||||
const phi = (inclination * Math.PI / 180);
|
||||
const x = radius * Math.sin(phi) * Math.cos(theta + raan * Math.PI / 180);
|
||||
const y = radius * Math.cos(phi);
|
||||
const z = radius * Math.sin(phi) * Math.sin(theta + raan * Math.PI / 180);
|
||||
points.push(new THREE.Vector3(x, y, z));
|
||||
}
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
export function showPredictedOrbit(satellite) {
|
||||
hidePredictedOrbit();
|
||||
|
||||
const props = satellite.properties;
|
||||
const meanMotion = props?.mean_motion || 15;
|
||||
const periodSeconds = calculateOrbitalPeriod(meanMotion);
|
||||
console.log('[DEBUG] meanMotion:', meanMotion, 'periodSeconds:', periodSeconds);
|
||||
|
||||
// Test current time
|
||||
const now = new Date();
|
||||
const testPos = computeSatellitePosition(satellite, now);
|
||||
console.log('[DEBUG] testPos (now):', testPos);
|
||||
|
||||
const points = calculatePredictedOrbit(satellite, periodSeconds);
|
||||
console.log('[DEBUG] points.length:', points.length);
|
||||
|
||||
if (points.length < 2) return;
|
||||
|
||||
const positions = new Float32Array(points.length * 3);
|
||||
const colors = new Float32Array(points.length * 3);
|
||||
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
positions[i * 3] = points[i].x;
|
||||
positions[i * 3 + 1] = points[i].y;
|
||||
positions[i * 3 + 2] = points[i].z;
|
||||
|
||||
const t = i / (points.length - 1);
|
||||
colors[i * 3] = 1 - t * 0.4;
|
||||
colors[i * 3 + 1] = 1 - t * 0.6;
|
||||
colors[i * 3 + 2] = t;
|
||||
}
|
||||
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
||||
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
||||
|
||||
const material = new THREE.LineBasicMaterial({
|
||||
vertexColors: true,
|
||||
transparent: true,
|
||||
opacity: 0.8,
|
||||
blending: THREE.AdditiveBlending
|
||||
});
|
||||
|
||||
predictedOrbitLine = new THREE.Line(geometry, material);
|
||||
earthObjRef.add(predictedOrbitLine);
|
||||
}
|
||||
|
||||
export function hidePredictedOrbit() {
|
||||
if (predictedOrbitLine) {
|
||||
earthObjRef.remove(predictedOrbitLine);
|
||||
predictedOrbitLine.geometry.dispose();
|
||||
predictedOrbitLine.material.dispose();
|
||||
predictedOrbitLine = null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user