// 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 = []; let hoverRingSprite = null; let lockedRingSprite = null; const SATELLITE_API = '/api/v1/visualization/geo/satellites?limit=2000'; const MAX_SATELLITES = 2000; const TRAIL_LENGTH = 30; const DOT_TEXTURE_SIZE = 32; function createCircularDotTexture() { const canvas = document.createElement('canvas'); canvas.width = DOT_TEXTURE_SIZE; canvas.height = DOT_TEXTURE_SIZE; const ctx = canvas.getContext('2d'); const center = DOT_TEXTURE_SIZE / 2; const radius = center - 2; const gradient = ctx.createRadialGradient(center, center, 0, center, center, radius); gradient.addColorStop(0, 'rgba(255, 255, 255, 1)'); gradient.addColorStop(0.5, 'rgba(255, 255, 255, 0.8)'); gradient.addColorStop(1, 'rgba(255, 255, 255, 0)'); ctx.fillStyle = gradient; ctx.beginPath(); ctx.arc(center, center, radius, 0, Math.PI * 2); ctx.fill(); const texture = new THREE.CanvasTexture(canvas); texture.needsUpdate = true; return texture; } function createRingTexture(innerRadius, outerRadius, color = '#ffffff') { const size = DOT_TEXTURE_SIZE * 2; const canvas = document.createElement('canvas'); canvas.width = size; canvas.height = size; const ctx = canvas.getContext('2d'); const center = size / 2; ctx.strokeStyle = color; ctx.lineWidth = 3; ctx.beginPath(); ctx.arc(center, center, (innerRadius + outerRadius) / 2, 0, Math.PI * 2); ctx.stroke(); const texture = new THREE.CanvasTexture(canvas); texture.needsUpdate = true; return texture; } export function createSatellites(scene, earthObj) { initSatelliteScene(scene, earthObj); const positions = new Float32Array(MAX_SATELLITES * 3); const colors = new Float32Array(MAX_SATELLITES * 3); const dotTexture = createCircularDotTexture(); 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: 1.5, map: dotTexture, vertexColors: true, transparent: true, opacity: 0.9, sizeAttenuation: true, alphaTest: 0.1 }); 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, MAX_SATELLITES); 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; } export function getSatellitePositions() { return satellitePositions; } let earthObjRef = null; let sceneRef = null; export function showHoverRing(position, isLocked = false) { if (!sceneRef || !earthObjRef) return; const ringTexture = createRingTexture(8, 12, isLocked ? '#ffcc00' : '#ffffff'); const spriteMaterial = new THREE.SpriteMaterial({ map: ringTexture, transparent: true, opacity: 0.8, depthTest: false }); const sprite = new THREE.Sprite(spriteMaterial); sprite.position.copy(position); sprite.scale.set(3, 3, 1); earthObjRef.add(sprite); if (isLocked) { if (lockedRingSprite) { earthObjRef.remove(lockedRingSprite); } lockedRingSprite = sprite; } else { if (hoverRingSprite) { earthObjRef.remove(hoverRingSprite); } hoverRingSprite = sprite; } return sprite; } export function hideHoverRings() { if (!earthObjRef) return; if (hoverRingSprite) { earthObjRef.remove(hoverRingSprite); hoverRingSprite = null; } } export function hideLockedRing() { if (!earthObjRef || !lockedRingSprite) return; earthObjRef.remove(lockedRingSprite); lockedRingSprite = null; } export function updateLockedRingPosition(position) { if (lockedRingSprite && position) { lockedRingSprite.position.copy(position); } } export function updateHoverRingPosition(position) { if (hoverRingSprite && position) { hoverRingSprite.position.copy(position); } } export function setSatelliteRingState(index, state, position) { switch (state) { case 'hover': hideHoverRings(); showHoverRing(position, false); break; case 'locked': hideHoverRings(); showHoverRing(position, true); break; case 'none': hideHoverRings(); hideLockedRing(); break; } } export function initSatelliteScene(scene, earth) { sceneRef = scene; earthObjRef = earth; }