Initial release: collaborative trip planner
Multi-user trip planning web app in a single Docker container. Mullvad-style token accounts, trip sharing via join codes, day-by-day calendar with typed entries (activity, hotel, travel, flight, rental car, immigration, note), multi-leg flight segments with bundled IATA airport dataset, Leaflet/OSM map with per-leg great-circle km (air vs ground), rough km-driven vs rental included-km comparison, cost splitting with settle-up suggestions, flip-clock departure countdown. Node 20 + Express + SQLite (WAL, additive migrations), vanilla JS SPA, 44 API tests.
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
// Regenerate src/server/data/airports.json from the public-domain OurAirports
|
||||
// dataset. Usage: node scripts/generate-airports.mjs
|
||||
//
|
||||
// Keeps airports that have an IATA code AND scheduled service, projecting each
|
||||
// to { code, name, city, country, lat, lng }.
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const SOURCE_URL = 'https://davidmegginson.github.io/ourairports-data/airports.csv';
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const OUT_PATH = path.join(__dirname, '..', 'src', 'server', 'data', 'airports.json');
|
||||
|
||||
// Minimal RFC-4180 CSV parser (handles quotes, escaped quotes, embedded commas/newlines).
|
||||
function parseCsv(str) {
|
||||
const rows = [];
|
||||
let row = [];
|
||||
let field = '';
|
||||
let inQuotes = false;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const c = str[i];
|
||||
if (inQuotes) {
|
||||
if (c === '"') {
|
||||
if (str[i + 1] === '"') { field += '"'; i++; }
|
||||
else inQuotes = false;
|
||||
} else field += c;
|
||||
} else if (c === '"') inQuotes = true;
|
||||
else if (c === ',') { row.push(field); field = ''; }
|
||||
else if (c === '\n') { row.push(field); rows.push(row); row = []; field = ''; }
|
||||
else if (c === '\r') { /* ignore */ }
|
||||
else field += c;
|
||||
}
|
||||
if (field.length || row.length) { row.push(field); rows.push(row); }
|
||||
return rows;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log(`Downloading ${SOURCE_URL} ...`);
|
||||
const res = await fetch(SOURCE_URL, { headers: { 'User-Agent': 'trip-plan-app/0.1 (self-hosted)' } });
|
||||
if (!res.ok) throw new Error(`download failed: HTTP ${res.status}`);
|
||||
const text = await res.text();
|
||||
|
||||
const rows = parseCsv(text);
|
||||
const header = rows[0];
|
||||
const col = (name) => header.indexOf(name);
|
||||
const iIata = col('iata_code');
|
||||
const iSched = col('scheduled_service');
|
||||
const iName = col('name');
|
||||
const iCity = col('municipality');
|
||||
const iCountry = col('iso_country');
|
||||
const iLat = col('latitude_deg');
|
||||
const iLng = col('longitude_deg');
|
||||
|
||||
const airports = [];
|
||||
for (let r = 1; r < rows.length; r++) {
|
||||
const row = rows[r];
|
||||
if (!row || row.length < header.length) continue;
|
||||
const code = (row[iIata] || '').trim();
|
||||
if (!code) continue;
|
||||
if ((row[iSched] || '').trim() !== 'yes') continue;
|
||||
const lat = Number(row[iLat]);
|
||||
const lng = Number(row[iLng]);
|
||||
airports.push({
|
||||
code,
|
||||
name: (row[iName] || '').trim(),
|
||||
city: (row[iCity] || '').trim(),
|
||||
country: (row[iCountry] || '').trim(),
|
||||
lat: Number.isFinite(lat) ? lat : null,
|
||||
lng: Number.isFinite(lng) ? lng : null,
|
||||
});
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.dirname(OUT_PATH), { recursive: true });
|
||||
fs.writeFileSync(OUT_PATH, JSON.stringify(airports));
|
||||
const bytes = fs.statSync(OUT_PATH).size;
|
||||
console.log(`Wrote ${airports.length} airports to ${OUT_PATH} (${bytes} bytes)`);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user