const satellite = require('satellite.js'); const fs = require('fs'); const path = require('path'); const TLE_CACHE_PATH = path.join(__dirname, 'TLE_cache.json'); const TLE_URL = 'https://celestrak.org/NORAD/elements/gp.php?GROUP=active&FORMAT=tle'; const TLE_REFRESH_INTERVAL = 60 * 60 * 1000; let tleCache = {}; let lastFetchTime = 0; function deg2rad(deg) { return deg * (Math.PI / 180); } function rad2deg(rad) { return rad * (180 / Math.PI); } function loadTleCache() { try { if (fs.existsSync(TLE_CACHE_PATH)) { const data = fs.readFileSync(TLE_CACHE_PATH, 'utf-8'); tleCache = JSON.parse(data); } } catch (e) { console.error('Failed to load TLE cache:', e.message); tleCache = {}; } } function saveTleCache() { try { fs.writeFileSync(TLE_CACHE_PATH, JSON.stringify(tleCache, null, 2)); } catch (e) { console.error('Failed to save TLE cache:', e.message); } } loadTleCache(); async function fetchTleData() { try { const response = await fetch(TLE_URL); if (!response.ok) throw new Error(`HTTP ${response.status}`); const text = await response.text(); const lines = text.split('\n').filter(l => l.trim()); const newCache = {}; for (let i = 0; i + 2 < lines.length; i += 3) { const name = lines[i].trim().replace(/^0 /, ''); const line1 = lines[i + 1].trim(); const line2 = lines[i + 2].trim(); if (line1.startsWith('1 ') && line2.startsWith('2 ')) { newCache[name] = { name, line1, line2 }; } } tleCache = newCache; lastFetchTime = Date.now(); saveTleCache(); console.log(`Fetched ${Object.keys(newCache).length} TLE records`); return newCache; } catch (e) { console.error('TLE fetch failed:', e.message); return tleCache; } } async function ensureTleFresh() { if (Date.now() - lastFetchTime > TLE_REFRESH_INTERVAL) { await fetchTleData(); } } function propagateSatellite(tleRecord, date) { try { const satrec = satellite.twoline2satrec(tleRecord.line1, tleRecord.line2); const positionAndVelocity = satellite.propagate(satrec, date); if (!positionAndVelocity.position) return null; const positionEci = positionAndVelocity.position; const gmst = satellite.gstime(date); const geodetic = satellite.eciToGeodetic(positionEci, gmst); const lat = rad2deg(geodetic.latitude); const lng = rad2deg(geodetic.longitude); const alt = geodetic.height; const earthRadius = 6371; const footprintRadius = earthRadius * Math.acos(earthRadius / (earthRadius + alt)); return { lat, lng, alt: alt * 1000, footprintRadius: footprintRadius * 1000 }; } catch (e) { return null; } } function getActiveSatellitesState(date) { const names = Object.keys(tleCache); const state = {}; for (const name of names) { const result = propagateSatellite(tleCache[name], date); if (result) { state[name] = result; } } return state; } function getRandomSatelliteState(date) { const names = Object.keys(tleCache); if (names.length === 0) return null; const name = names[Math.floor(Math.random() * names.length)]; const result = propagateSatellite(tleCache[name], date); if (!result) return null; return { name, ...result }; } function getSatelliteState(name, date) { if (!tleCache[name]) return null; const result = propagateSatellite(tleCache[name], date); if (!result) return null; return { name, ...result }; } function haversineDistance(lat1, lng1, lat2, lng2) { const R = 6371000; const dLat = deg2rad(lat2 - lat1); const dLng = deg2rad(lng2 - lng1); const a = Math.sin(dLat / 2) ** 2 + Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.sin(dLng / 2) ** 2; const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; } function scoreFromDistance(distanceMeters) { if (distanceMeters < 1000) return 10000; if (distanceMeters < 10000) return Math.round(10000 - (distanceMeters / 10000) * 2000); if (distanceMeters < 100000) return Math.round(8000 - (distanceMeters / 100000) * 3000); if (distanceMeters < 500000) return Math.round(5000 - (distanceMeters / 500000) * 3000); return Math.max(0, Math.round(2000 - (distanceMeters / 20000000) * 2000)); } module.exports = { fetchTleData, ensureTleFresh, propagateSatellite, getActiveSatellitesState, getRandomSatelliteState, getSatelliteState, haversineDistance, scoreFromDistance, tleCache, };