// app/api/airport-data/route.ts import { NextResponse } from 'next/server'; import fetch from 'node-fetch'; import NodeCache from 'node-cache'; const cache = new NodeCache({ stdTTL: 60, checkperiod: 10 }); // Constants (these could be moved to a separate file if needed) const magneticVariation = 18; // West variation (Add to True Wind) const runways = { CYHZ: { "05": 53, "32": 323, "23": 233, "14": 143 }, CYFC: { "09": 87, "15": 148, "27": 268, "33": 328 }, CYQM: { "06": 61, "29": 286, "11": 106, "24": 241 }, CYSJ: { "23": 229, "05": 49, "14": 138, "32": 319 }, CYZX: { "08": 80, "12": 122, "26": 261, "30": 303 }, CYYG: { "03": 27, "21": 207, "10": 97, "28": 277 }, CYYT: { "28": 283, "10": 103, "16": 156, "34": 336 }, CYQX: { "21": 210, "03": 30, "13": 128, "31": 308 }, CYYR: { "08": 76, "15": 154, "26": 256, "33": 334 }, LFVP: { "08": 76, "26": 256 }, CYQI: { "06": 59, "15": 150, "24": 239, "33": 330 }, CYAY: { "10": 99, "28": 279 }, CYDF: { "25": 244, "07": 64 }, CYJT: { "27": 270, "09": 90 }, } as const; export async function GET(req: Request) { const url = new URL(req.url); const airportCode = url.searchParams.get('airportCode'); if (!airportCode) { return NextResponse.json({ error: 'Missing airportCode parameter' }, { status: 400 }); } // Check if data is cached const cachedData = cache.get(airportCode); if (cachedData) { console.log('Returning cached data'); return NextResponse.json(cachedData); } const apiUrl = `https://avwx.rest/api/metar/${airportCode}?token=vmtkb1D8Tuva2Jw2tXihWcKE3m2sfDJkySBZygVx82I`; try { const response = await fetch(apiUrl, { headers: { 'Accept': 'application/json', 'Authorization': 'Bearer vmtkb1D8Tuva2Jw2tXihWcKE3m2sfDJkySBZygVx82I', }, }); if (!response.ok) { return NextResponse.json({ error: `Error fetching data: ${response.statusText}` }, { status: response.status }); } const data = await response.json(); const windDirectionTrue = parseInt(data.wind_direction?.value || '0', 10); const windSpeed = parseInt(data.wind_speed?.value || '0', 10); const gustSpeed = parseInt(data.wind_gust?.value || windSpeed.toString(), 10); const windDirectionMag = (windDirectionTrue + magneticVariation + 360) % 360; const effectiveWindSpeed = gustSpeed > windSpeed ? gustSpeed : windSpeed; const { bestRunway, bestHeadwind, bestCrosswind } = calculateBestRunway( airportCode, windDirectionMag, effectiveWindSpeed ); const result = { airport: airportCode, flightRules: data.flight_rules || 'Unknown', flightRulesClass: (data.flight_rules || 'Unknown').toLowerCase(), bestRunway: bestRunway || 'N/A', wind: `${windDirectionMag}° / ${effectiveWindSpeed}`, headwind: formatHeadwind(bestHeadwind), crosswind: bestCrosswind !== null ? Math.abs(bestCrosswind) : 'N/A', altimeter: data.altimeter?.repr || 'N/A', metar: sanitizeMETAR(data.raw), headwindClass: getWindClass(bestHeadwind, 'headwind'), crosswindClass: getWindClass(bestCrosswind, 'crosswind'), runwayClass: getRunwayClass(bestHeadwind, bestCrosswind), }; // Cache the result for 1 minute cache.set(airportCode, result); return NextResponse.json(result); } catch (error) { console.error(`Error fetching data for ${airportCode}:`, error); return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); } } // Helper functions function calculateBestRunway(airportCode: string, windDirection: number, windSpeed: number) { let bestRunway = null; let bestHeadwind = -Infinity; let bestCrosswind = null; for (const [runway, heading] of Object.entries(runways[airportCode] || {})) { const { headwind, crosswind } = calculateWindComponents( windDirection, windSpeed, Number(heading) ); if ( headwind > bestHeadwind || (headwind === bestHeadwind && Math.abs(crosswind) < Math.abs(bestCrosswind)) ) { bestHeadwind = headwind; bestCrosswind = crosswind; bestRunway = runway; } } return { bestRunway, bestHeadwind, bestCrosswind }; } function calculateWindComponents(windDirection: number, windSpeed: number, runwayHeading: number) { const angle = ((windDirection - runwayHeading + 360) % 360) * (Math.PI / 180); return { headwind: Math.round(windSpeed * Math.cos(angle)), crosswind: Math.round(windSpeed * Math.sin(angle)), }; } function sanitizeMETAR(raw: string) { const altimeterPattern = /(A\d{4})/; const match = raw.match(altimeterPattern); return match ? raw.split(match[0])[0] + match[0] : raw; } function formatHeadwind(headwind: number) { if (headwind === -Infinity) return 'N/A'; return headwind > 0 ? `+${headwind}` : headwind.toString(); } function getWindClass(windSpeed: number | null, type: 'headwind' | 'crosswind') { if (windSpeed === null) return ''; if (type === 'headwind') { return windSpeed >= 0 ? 'green' : windSpeed >= -5 ? 'orange' : 'red'; } if (type === 'crosswind') { return Math.abs(windSpeed) > 15 ? 'red' : Math.abs(windSpeed) >= 12 ? 'orange' : 'green'; } return ''; } function getRunwayClass(headwind: number, crosswind: number | null) { if ( getWindClass(headwind, 'headwind') === 'red' || getWindClass(crosswind, 'crosswind') === 'red' ) { return 'runway-red'; } if ( getWindClass(headwind, 'headwind') === 'orange' || getWindClass(crosswind, 'crosswind') === 'orange' ) { return 'runway-orange'; } return 'runway-green'; }