Spaces:
Sleeping
Sleeping
| 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, | |
| }; | |