174 lines
6.0 KiB
TypeScript
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';
|
|
}
|