// 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 getSatellitePoints() { return satellitePoints; }