2024-11-14 16:22:14 -05:00

174 lines
6.0 KiB
TypeScript

// 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 for ' + airportCode);
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';
}