Files
TREK/server/scripts/build-airports.mjs
T
2026-07-11 20:59:11 +02:00

140 lines
3.7 KiB
JavaScript

#!/usr/bin/env node
// Build server/assets/airports.json from OurAirports (davidmegginson.github.io/ourairports-data).
// License: Public Domain. Keeps large/medium airports with an IATA code; timezone derived from coords via tz-lookup.
import fs from 'node:fs';
import https from 'node:https';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import tzLookup from 'tz-lookup';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const OUT = path.join(__dirname, '..', 'assets', 'airports.json');
const SRC = 'https://davidmegginson.github.io/ourairports-data/airports.csv';
function fetchText(url) {
return new Promise((resolve, reject) => {
https
.get(url, (res) => {
if (res.statusCode !== 200) return reject(new Error(`HTTP ${res.statusCode}`));
let data = '';
res.setEncoding('utf8');
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => resolve(data));
})
.on('error', reject);
});
}
function parseCsv(text) {
const rows = [];
let row = [];
let cur = '';
let inQuotes = false;
for (let i = 0; i < text.length; i++) {
const ch = text[i];
if (inQuotes) {
if (ch === '"') {
if (text[i + 1] === '"') {
cur += '"';
i++;
} else {
inQuotes = false;
}
} else {
cur += ch;
}
} else {
if (ch === '"') inQuotes = true;
else if (ch === ',') {
row.push(cur);
cur = '';
} else if (ch === '\n') {
row.push(cur);
rows.push(row);
row = [];
cur = '';
} else if (ch === '\r') {
/* skip */
} else cur += ch;
}
}
if (cur.length > 0 || row.length > 0) {
row.push(cur);
rows.push(row);
}
return rows;
}
const raw = await fetchText(SRC);
const rows = parseCsv(raw);
const header = rows[0];
const idx = (name) => header.indexOf(name);
const TYPE = idx('type');
const NAME = idx('name');
const LAT = idx('latitude_deg');
const LNG = idx('longitude_deg');
const COUNTRY = idx('iso_country');
const MUNICIPALITY = idx('municipality');
const SERVICE = idx('scheduled_service');
const ICAO = idx('icao_code');
const IATA = idx('iata_code');
const KEEP = new Set(['large_airport', 'medium_airport', 'small_airport']);
const airports = [];
let skippedNoTz = 0;
for (let i = 1; i < rows.length; i++) {
const r = rows[i];
if (!r || r.length < header.length) continue;
if (!KEEP.has(r[TYPE])) continue;
const iata = r[IATA]?.trim().toUpperCase();
if (!iata || iata.length !== 3) continue;
if (r[SERVICE] !== 'yes') continue;
const lat = Number(r[LAT]);
const lng = Number(r[LNG]);
if (!Number.isFinite(lat) || !Number.isFinite(lng)) continue;
let tz = null;
try {
tz = tzLookup(lat, lng);
} catch {
skippedNoTz++;
continue;
}
if (!tz) {
skippedNoTz++;
continue;
}
airports.push({
iata,
icao: r[ICAO]?.trim().toUpperCase() || null,
name: r[NAME],
city: r[MUNICIPALITY] || '',
country: r[COUNTRY] || '',
lat: Math.round(lat * 1e6) / 1e6,
lng: Math.round(lng * 1e6) / 1e6,
tz,
});
}
const seen = new Map();
for (const a of airports) {
const existing = seen.get(a.iata);
if (!existing) {
seen.set(a.iata, a);
continue;
}
if (existing.icao && !a.icao) continue;
if (!existing.icao && a.icao) seen.set(a.iata, a);
}
const unique = Array.from(seen.values()).sort((a, b) => a.iata.localeCompare(b.iata));
fs.writeFileSync(OUT, JSON.stringify(unique));
const size = fs.statSync(OUT).size;
console.log(
`Wrote ${unique.length} airports to ${OUT} (${(size / 1024).toFixed(1)} KB); skipped ${skippedNoTz} without timezone`,
);