Skip to content

Commit cf5a43e

Browse files
Merge pull request #35 from sebastiankrll/fix/all
Fix/all
2 parents d58b755 + a96bc44 commit cf5a43e

17 files changed

Lines changed: 211 additions & 241 deletions

File tree

apps/api/src/index.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import { validateCallsign, validateICAO, validateNumber, validateString } from "
99

1010
rdsConnect();
1111

12+
const WEATHER_CACHE_DURATION = 10 * 60 * 1000;
13+
const cachedWeathers = new Map<string, { timestamp: number; rawMetar: string; rawTaf: string }>();
14+
1215
const limiter = rateLimit({
1316
windowMs: 60_000,
1417
max: 60,
@@ -32,6 +35,7 @@ app.use(limiter);
3235
const asyncHandler =
3336
(fn: (req: express.Request, res: express.Response, next: express.NextFunction) => Promise<void> | Promise<any>) =>
3437
(req: express.Request, res: express.Response, next: express.NextFunction) => {
38+
// console.log(`Incoming request: ${req.method} ${req.originalUrl}`);
3539
Promise.resolve(fn(req, res, next)).catch(next);
3640
};
3741

@@ -206,6 +210,50 @@ app.get(
206210
}),
207211
);
208212

213+
app.get(
214+
"/data/weather/:icao",
215+
asyncHandler(async (req, res) => {
216+
const icao = validateICAO(req.params.icao).toUpperCase();
217+
218+
const cachedWeather = cachedWeathers.get(icao);
219+
const now = Date.now();
220+
221+
if (cachedWeather && now - cachedWeather.timestamp < WEATHER_CACHE_DURATION) {
222+
res.json({ metar: cachedWeather.rawMetar, taf: cachedWeather.rawTaf });
223+
return;
224+
}
225+
226+
const metarResponse = await fetch(`https://aviationweather.gov/api/data/metar?ids=${icao}&format=json`);
227+
const tafResponse = await fetch(`https://aviationweather.gov/api/data/taf?ids=${icao}&format=json`);
228+
229+
if (!metarResponse.ok && !tafResponse.ok) {
230+
res.status(404).json({ error: "Weather data not found" });
231+
return;
232+
}
233+
234+
const safeParse = (text: string): any[] => {
235+
try {
236+
const parsed = JSON.parse(text);
237+
return Array.isArray(parsed) ? parsed : [];
238+
} catch (_err) {
239+
return [];
240+
}
241+
};
242+
243+
const metarText = await metarResponse.text();
244+
const tafText = await tafResponse.text();
245+
const metarData = safeParse(metarText);
246+
const tafData = safeParse(tafText);
247+
248+
const rawMetar = metarData.length > 0 && metarData[0]?.rawOb ? metarData[0].rawOb : "";
249+
const rawTaf = tafData.length > 0 && (tafData[0]?.rawTAF || tafData[0]?.rawOb) ? tafData[0].rawTAF || tafData[0].rawOb : "";
250+
251+
cachedWeathers.set(icao, { timestamp: now, rawMetar, rawTaf });
252+
253+
res.json({ metar: rawMetar, taf: rawTaf });
254+
}),
255+
);
256+
209257
app.get(
210258
"/data/controllers/:callsigns",
211259
asyncHandler(async (req, res) => {

apps/ingestion/src/airport.ts

Lines changed: 0 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,10 @@
1-
import { promisify } from "node:util";
2-
import * as zlib from "node:zlib";
31
import type { AirportDelta, AirportLong, AirportShort, PilotLong } from "@sr24/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;
322

333
let cached: AirportLong[] = [];
344
let updated: AirportShort[] = [];
355
let added: Required<AirportShort>[] = [];
366

377
export async function mapAirports(pilotsLong: PilotLong[]): Promise<AirportLong[]> {
38-
await updateWeather();
39-
408
const airportRecord: Record<string, AirportLong> = {};
419
const routeRecord: Record<string, Map<string, number>> = {};
4210

@@ -179,8 +147,6 @@ function initAirportRecord(icao: string): AirportLong {
179147
arr_traffic: { traffic_count: 0, average_delay: 0, flights_delayed: 0 },
180148
busiest: { departure: "-", arrival: "-" },
181149
unique: { departures: 0, arrivals: 0 },
182-
metar: getMetar(icao),
183-
taf: getTaf(icao),
184150
};
185151
}
186152

@@ -199,72 +165,3 @@ function calculateArrivalDelay(pilot: PilotLong): number {
199165

200166
return Math.min(Math.max(delay_min, 0), 120);
201167
}
202-
203-
const gunzip = promisify(zlib.gunzip);
204-
let metarCache: Map<string, string> = new Map();
205-
let tafCache: Map<string, string> = new Map();
206-
let lastWeatherFetch = 0;
207-
208-
async function fetchWeather(url: string): Promise<MetarXML | TafXML> {
209-
const response = await axios.get<Buffer>(url, {
210-
responseType: "arraybuffer",
211-
});
212-
213-
const decompressed = await gunzip(response.data);
214-
const xml = decompressed.toString("utf-8");
215-
216-
const parsed = (await parseStringPromise(xml)) as MetarXML | TafXML;
217-
218-
return parsed;
219-
}
220-
221-
async function updateWeather(): Promise<void> {
222-
if (Date.now() - lastWeatherFetch < WEATHER_FETCH_INTERVAL) {
223-
return;
224-
}
225-
lastWeatherFetch = Date.now();
226-
227-
try {
228-
const parsedMetar = (await fetchWeather(METAR_URL)) as MetarXML;
229-
const parsedTaf = (await fetchWeather(TAF_URL)) as TafXML;
230-
231-
const metars = parsedMetar?.response?.data?.[0]?.METAR || [];
232-
const tafs = parsedTaf?.response?.data?.[0]?.TAF || [];
233-
234-
const newMetarCache = new Map<string, string>();
235-
const newTafCache = new Map<string, string>();
236-
237-
for (const metar of metars) {
238-
const icao = metar.station_id?.[0];
239-
const raw = metar.raw_text?.[0];
240-
241-
if (icao && raw) {
242-
newMetarCache.set(icao, raw);
243-
}
244-
}
245-
246-
for (const taf of tafs) {
247-
const icao = taf.station_id?.[0];
248-
const raw = taf.raw_text?.[0];
249-
250-
if (icao && raw) {
251-
newTafCache.set(icao, raw);
252-
}
253-
}
254-
255-
metarCache = newMetarCache;
256-
tafCache = newTafCache;
257-
258-
// console.log(`✅ Updated ${metarCache.size} METAR entries and ${tafCache.size} TAF entries`);
259-
} catch (error) {
260-
console.error("❌ Error fetching weather data:", error instanceof Error ? error.message : error);
261-
}
262-
}
263-
264-
export function getMetar(icao: string): string | null {
265-
return metarCache.get(icao) || null;
266-
}
267-
268-
export function getTaf(icao: string): string | null {
269-
return tafCache.get(icao) || null;
270-
}

apps/ingestion/src/pilot.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,11 @@ function mapPilotTimes(current: PilotLong, cache: PilotLong | undefined, vatsimP
369369
stop_counter = 0;
370370
}
371371

372+
// Moving, @"Taxi In", past scheduled on blocks
373+
if (cache.times.state !== "Taxi In" && on_block.getTime() < now.getTime()) {
374+
on_block = new Date(now.getTime() + TAXI_TIME_MS);
375+
}
376+
372377
// Not moving, @"Taxi In"
373378
if (current.groundspeed === 0 && cache.times.state === "Taxi In") {
374379
if (stop_counter > 5) {

apps/web/components/Map/utils/events.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,10 @@ export function updateOverlays(): void {
218218
}
219219

220220
async function updateOverlay(feature: Feature<Point>, overlay: Overlay): Promise<void> {
221-
if (!feature || !overlay) return;
221+
if (!feature || !overlay) {
222+
resetMap(true);
223+
return;
224+
};
222225

223226
const geom = feature.getGeometry();
224227
const coords = geom?.getCoordinates();

apps/web/components/Map/utils/pilotFeatures.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type { PilotProperties } from "@/types/ol";
88
import { pilotMainSource } from "./dataLayers";
99
import { getMapView } from "./init";
1010
import { initTrackFeatures } from "./trackFeatures";
11+
import { resetMap } from "./events";
1112

1213
interface RBushPilotFeature {
1314
minX: number;
@@ -114,6 +115,11 @@ export function updatePilotFeatures(delta: PilotDelta): void {
114115
}
115116
}
116117

118+
if (highlightedPilot && !pilotMap.has(highlightedPilot)) {
119+
highlightedPilot = null;
120+
resetMap(true);
121+
}
122+
117123
pilotRBush.clear();
118124
pilotRBush.load(items);
119125
}

apps/web/components/Map/utils/trackFeatures.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ interface Cached {
1212
coords: [number, number];
1313
index: number;
1414
stroke?: Stroke;
15+
timestamp?: number;
1516
}
1617

18+
const STALE_MS = 60 * 1000;
1719
const cached: Cached = {
1820
id: null,
1921
coords: [0, 0],
@@ -54,6 +56,7 @@ export async function initTrackFeatures(id: string | null): Promise<void> {
5456
trackSource.clear();
5557
trackSource.addFeatures(trackFeatures);
5658
cached.id = id;
59+
cached.timestamp = Date.now();
5760
}
5861

5962
export async function updateTrackFeatures(delta: PilotDelta): Promise<void> {
@@ -63,6 +66,11 @@ export async function updateTrackFeatures(delta: PilotDelta): Promise<void> {
6366
if (!cached.id || !pilot) return;
6467
if (pilot.latitude === undefined || pilot.longitude === undefined) return;
6568

69+
if (Date.now() - (cached.timestamp || 0) > STALE_MS) {
70+
await initTrackFeatures(cached.id);
71+
return;
72+
}
73+
6674
const trackFeature = new Feature({
6775
geometry: new LineString([cached.coords, [pilot.longitude, pilot.latitude]].map((coord) => fromLonLat(coord))),
6876
type: "track",

apps/web/components/Panels/Airport/AirportConnections.tsx

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { AirportLong } from "@sr24/types/vatsim";
22
import { getDelayColor } from "./AirportStatus";
33

4-
export function AirportConnections({ airport }: { airport: AirportLong }) {
4+
export function AirportConnections({ airport }: { airport: AirportLong | undefined }) {
55
return (
66
<div className="panel-sub-container sep">
77
<div className="panel-section-title">
@@ -19,46 +19,46 @@ export function AirportConnections({ airport }: { airport: AirportLong }) {
1919
<div className="panel-sub-container airport-connections">
2020
<div className="panel-data-item">
2121
<p>Total</p>
22-
<p>{airport.dep_traffic.traffic_count}</p>
22+
<p>{airport?.dep_traffic.traffic_count || 0}</p>
2323
</div>
2424
<div className="panel-data-item">
2525
<p>Delayed</p>
26-
<p>{airport.dep_traffic.flights_delayed}</p>
26+
<p>{airport?.dep_traffic.flights_delayed || 0}</p>
2727
</div>
2828
<div className="panel-data-item">
2929
<p>Avg. Delay</p>
30-
<p className={getDelayColor(airport.dep_traffic.average_delay)}>{`${airport.dep_traffic.average_delay} min`}</p>
30+
<p className={airport ? getDelayColor(airport.dep_traffic.average_delay) : ""}>{`${airport?.dep_traffic.average_delay || 0} min`}</p>
3131
</div>
3232
<div className="panel-data-item">
3333
<p>Busiest Route</p>
34-
<p>{airport.busiest.departure}</p>
34+
<p>{airport?.busiest.departure || "N/A"}</p>
3535
</div>
3636
<div className="panel-data-item">
3737
<p>Unique Connections</p>
38-
<p>{airport.unique.departures}</p>
38+
<p>{airport?.unique.departures || 0}</p>
3939
</div>
4040
</div>
4141
<div className="panel-data-separator">Arrivals</div>
4242
<div className="panel-sub-container airport-connections">
4343
<div className="panel-data-item">
4444
<p>Total</p>
45-
<p>{airport.arr_traffic.traffic_count}</p>
45+
<p>{airport?.arr_traffic.traffic_count || 0}</p>
4646
</div>
4747
<div className="panel-data-item">
4848
<p>Delayed</p>
49-
<p>{airport.arr_traffic.flights_delayed}</p>
49+
<p>{airport?.arr_traffic.flights_delayed || 0}</p>
5050
</div>
5151
<div className="panel-data-item">
5252
<p>Avg. Delay</p>
53-
<p className={getDelayColor(airport.arr_traffic.average_delay)}>{`${airport.arr_traffic.average_delay} min`}</p>
53+
<p className={airport ? getDelayColor(airport.arr_traffic.average_delay) : ""}>{`${airport?.arr_traffic.average_delay || 0} min`}</p>
5454
</div>
5555
<div className="panel-data-item">
5656
<p>Busiest Route</p>
57-
<p>{airport.busiest.arrival}</p>
57+
<p>{airport?.busiest.arrival || "N/A"}</p>
5858
</div>
5959
<div className="panel-data-item">
6060
<p>Unique Connections</p>
61-
<p>{airport.unique.arrivals}</p>
61+
<p>{airport?.unique.arrivals || 0}</p>
6262
</div>
6363
</div>
6464
</div>

0 commit comments

Comments
 (0)