1+ import { promisify } from "node:util" ;
2+ import * as zlib from "node:zlib" ;
13import type { AirportDelta , AirportLong , AirportShort , PilotLong } from "@sk/types/vatsim" ;
4+ import axios from "axios" ;
5+ import { parseStringPromise } from "xml2js" ;
6+
7+ interface MetarXML {
8+ response ?: {
9+ data ?: Array < {
10+ METAR ?: Array < {
11+ station_id ?: string [ ] ;
12+ raw_text ?: string [ ] ;
13+ } > ;
14+ } > ;
15+ } ;
16+ }
17+
18+ interface TafXML {
19+ response ?: {
20+ data ?: Array < {
21+ TAF ?: Array < {
22+ station_id ?: string [ ] ;
23+ raw_text ?: string [ ] ;
24+ } > ;
25+ } > ;
26+ } ;
27+ }
28+
29+ const METAR_URL = "https://aviationweather.gov/data/cache/metars.cache.xml.gz" ;
30+ const TAF_URL = "https://aviationweather.gov/data/cache/tafs.cache.xml.gz" ;
31+ const WEATHER_FETCH_INTERVAL = 600_000 ;
232
333let cached : AirportLong [ ] = [ ] ;
434let deleted : string [ ] = [ ] ;
535let updated : AirportShort [ ] = [ ] ;
636let added : AirportShort [ ] = [ ] ;
737
8- export function mapAirports ( pilotsLong : PilotLong [ ] ) : AirportLong [ ] {
38+ export async function mapAirports ( pilotsLong : PilotLong [ ] ) : Promise < AirportLong [ ] > {
39+ await updateWeather ( ) ;
40+
941 const airportRecord : Record < string , AirportLong > = { } ;
1042 const routeRecord : Record < string , Map < string , number > > = { } ;
1143
@@ -59,18 +91,38 @@ export function mapAirports(pilotsLong: PilotLong[]): AirportLong[] {
5991 const routes = routeRecord [ icao ] ;
6092 if ( ! routes ) continue ;
6193
62- let busiestRoute = "-" ;
63- let maxFlights = 0 ;
94+ let busiestDeparture = "-" ;
95+ let busiestArrival = "-" ;
96+ let busiestDepCount = 0 ;
97+ let busiestArrCount = 0 ;
98+ let uniqueDepartures = 0 ;
99+ let uniqueArrivals = 0 ;
64100
65101 routes . forEach ( ( count , route ) => {
66- if ( count > maxFlights ) {
67- maxFlights = count ;
68- busiestRoute = route ;
102+ const [ depIcao , arrIcao ] = route . split ( "-" ) ;
103+ if ( depIcao === icao ) {
104+ uniqueDepartures ++ ;
105+ if ( count > busiestDepCount ) {
106+ busiestDeparture = route ;
107+ busiestDepCount = count ;
108+ }
109+ } else if ( arrIcao === icao ) {
110+ uniqueArrivals ++ ;
111+ if ( count > busiestArrCount ) {
112+ busiestArrival = route ;
113+ busiestArrCount = count ;
114+ }
69115 }
70116 } ) ;
71117
72- airportRecord [ icao ] . busiest_route = busiestRoute ;
73- airportRecord [ icao ] . total_routes = routes . size ;
118+ airportRecord [ icao ] . busiest = {
119+ departure : busiestDeparture ,
120+ arrival : busiestArrival ,
121+ } ;
122+ airportRecord [ icao ] . unique = {
123+ departures : uniqueDepartures ,
124+ arrivals : uniqueArrivals ,
125+ } ;
74126 }
75127
76128 const airportsLong = Object . values ( airportRecord ) ;
@@ -109,8 +161,10 @@ function initAirportRecord(icao: string): AirportLong {
109161 icao : icao ,
110162 dep_traffic : { traffic_count : 0 , average_delay : 0 , flights_delayed : 0 } ,
111163 arr_traffic : { traffic_count : 0 , average_delay : 0 , flights_delayed : 0 } ,
112- busiest_route : "" ,
113- total_routes : 0 ,
164+ busiest : { departure : "-" , arrival : "-" } ,
165+ unique : { departures : 0 , arrivals : 0 } ,
166+ metar : getMetar ( icao ) ,
167+ taf : getTaf ( icao ) ,
114168 } ;
115169}
116170
@@ -129,3 +183,72 @@ function calculateArrivalDelay(pilot: PilotLong): number {
129183
130184 return Math . min ( Math . max ( delay_min , 0 ) , 120 ) ;
131185}
186+
187+ const gunzip = promisify ( zlib . gunzip ) ;
188+ let metarCache : Map < string , string > = new Map ( ) ;
189+ let tafCache : Map < string , string > = new Map ( ) ;
190+ let lastWeatherFetch = 0 ;
191+
192+ async function fetchWeather ( url : string ) : Promise < MetarXML | TafXML > {
193+ const response = await axios . get < Buffer > ( url , {
194+ responseType : "arraybuffer" ,
195+ } ) ;
196+
197+ const decompressed = await gunzip ( response . data ) ;
198+ const xml = decompressed . toString ( "utf-8" ) ;
199+
200+ const parsed = ( await parseStringPromise ( xml ) ) as MetarXML | TafXML ;
201+
202+ return parsed ;
203+ }
204+
205+ async function updateWeather ( ) : Promise < void > {
206+ if ( Date . now ( ) - lastWeatherFetch < WEATHER_FETCH_INTERVAL ) {
207+ return ;
208+ }
209+ lastWeatherFetch = Date . now ( ) ;
210+
211+ try {
212+ const parsedMetar = ( await fetchWeather ( METAR_URL ) ) as MetarXML ;
213+ const parsedTaf = ( await fetchWeather ( TAF_URL ) ) as TafXML ;
214+
215+ const metars = parsedMetar ?. response ?. data ?. [ 0 ] ?. METAR || [ ] ;
216+ const tafs = parsedTaf ?. response ?. data ?. [ 0 ] ?. TAF || [ ] ;
217+
218+ const newMetarCache = new Map < string , string > ( ) ;
219+ const newTafCache = new Map < string , string > ( ) ;
220+
221+ for ( const metar of metars ) {
222+ const icao = metar . station_id ?. [ 0 ] ;
223+ const raw = metar . raw_text ?. [ 0 ] ;
224+
225+ if ( icao && raw ) {
226+ newMetarCache . set ( icao , raw ) ;
227+ }
228+ }
229+
230+ for ( const taf of tafs ) {
231+ const icao = taf . station_id ?. [ 0 ] ;
232+ const raw = taf . raw_text ?. [ 0 ] ;
233+
234+ if ( icao && raw ) {
235+ newTafCache . set ( icao , raw ) ;
236+ }
237+ }
238+
239+ metarCache = newMetarCache ;
240+ tafCache = newTafCache ;
241+
242+ // console.log(`✅ Updated ${metarCache.size} METAR entries and ${tafCache.size} TAF entries`);
243+ } catch ( error ) {
244+ console . error ( "❌ Error fetching weather data:" , error instanceof Error ? error . message : error ) ;
245+ }
246+ }
247+
248+ export function getMetar ( icao : string ) : string | null {
249+ return metarCache . get ( icao ) || null ;
250+ }
251+
252+ export function getTaf ( icao : string ) : string | null {
253+ return tafCache . get ( icao ) || null ;
254+ }
0 commit comments