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