From 5c51dd52dea89aa1a9a4ef8509d91709a1ddabc5 Mon Sep 17 00:00:00 2001 From: ztimson Date: Thu, 25 Jun 2026 11:37:34 -0400 Subject: [PATCH] ADSB enrichment --- server/src/adsb.mjs | 113 ++++++++++++++++++-------------------------- 1 file changed, 47 insertions(+), 66 deletions(-) diff --git a/server/src/adsb.mjs b/server/src/adsb.mjs index 270a99c..95b8037 100644 --- a/server/src/adsb.mjs +++ b/server/src/adsb.mjs @@ -1,100 +1,81 @@ import {cfg} from './config.mjs'; -import Database from 'better-sqlite3'; -import {createWriteStream, existsSync, mkdirSync} from 'fs'; import {resolve, dirname} from 'path'; import {fileURLToPath} from 'url'; -import {pipeline} from 'stream/promises'; -import AdmZip from 'adm-zip'; import * as fs from 'node:fs'; const DIR = dirname(fileURLToPath(import.meta.url)); const DATA = resolve(DIR, '../data'); -const DB_PATH = resolve(DATA, 'aircraft.db'); -const DB_ZIP = resolve(DATA, 'aircraft_db.zip'); const SHAPES_PATH = resolve(DATA, 'shapes.json'); +const CSV_CACHE = resolve(DATA, 'aircraft_db.csv'); const history = new Map(); const MAX_HISTORY = 500; -const DB_URL = 'https://www.mictronics.de/aircraft-database/aircraft_db.php'; +const CSV_URL = 'https://s3.opensky-network.org/data-samples/metadata/aircraft-database-complete-2024-06.csv'; -let db; +let aircraftMap = new Map(); -async function downloadFile(url, dest) { - const res = await fetch(url); - if(!res.ok) throw new Error(`Download failed ${url}: ${res.status}`); - await pipeline(res.body, createWriteStream(dest)); +function parseCsv(text) { + const map = new Map(); + const lines = text.split('\n'); + for(let i = 1; i < lines.length; i++) { + const line = lines[i].trim(); + if(!line) continue; + const cols = line.split(','); + const icao24 = cols[0]?.replace(/'/g, '').toUpperCase(); + if(!icao24) continue; + map.set(icao24, { + reg: cols[25]?.replace(/'/g, '') || null, + type_code: cols[29]?.replace(/'/g, '') || null, + desc: cols[14]?.replace(/'/g, '') || null, + }); + } + return map; } -function initSchema() { - db.exec(` - CREATE TABLE IF NOT EXISTS aircraft ( - icao TEXT PRIMARY KEY, - reg TEXT, - type_code TEXT, - flags TEXT, - desc TEXT - ); - `); -} +async function loadAircraftCsv() { + let text = null; + let fromCache = false; -async function importAircraftDb() { - await downloadFile(DB_URL, DB_ZIP); - const zip = new AdmZip(DB_ZIP); - const entries = zip.getEntries(); - - let total = 0; - const insert = db.prepare(`INSERT OR REPLACE INTO aircraft(icao, reg, type_code, flags, desc) VALUES (?,?,?,?,?)`); - const run = db.transaction((rows) => { - for(const [icao, a] of Object.entries(rows)) { - insert.run(icao.toUpperCase().padStart(6, '0'), a.r || null, a.t || null, a.f || null, a.desc || null); - total++; + try { + const res = await fetch(CSV_URL); + if(!res.ok) throw new Error(`HTTP ${res.status}`); + text = await res.text(); + } catch(e) { + console.warn(`✈️ Failed to download CSV (${e.message}), trying cache...`); + if(fs.existsSync(CSV_CACHE)) { + text = await fs.promises.readFile(CSV_CACHE, 'utf8'); + fromCache = true; + } else { + throw new Error('No aircraft CSV available and no cache found'); } - }); - - for(const entry of entries) { - if(!entry.entryName.endsWith('.json')) continue; - const rows = JSON.parse(entry.getData().toString('utf8')); - run(rows); } - await fs.promises.rm(DB_ZIP); - console.log(`✈️ Aircraft imported: ${total}`); + aircraftMap = parseCsv(text); + console.log(`✈️ Aircraft loaded: ${aircraftMap.size} (${fromCache ? 'cache' : 'fresh'})`); + + // Save to cache in background if fresh + if(!fromCache) { + fs.promises.writeFile(CSV_CACHE, text).catch(e => + console.warn('✈️ Failed to cache aircraft CSV:', e.message) + ); + } } export async function initAircraftDb() { - if(!existsSync(DATA)) mkdirSync(DATA, {recursive: true}); - db = new Database(DB_PATH); - initSchema(); - - console.log('✈️ Downloading aircraft DB...'); - await importAircraftDb(); -} - -export function decodeFlags(f) { - if(!f) return {}; - const n = parseInt(f, 16); - return { - military: !!(n & 0x01), - interesting: !!(n & 0x02), - pia: !!(n & 0x04), - ladd: !!(n & 0x08), - }; -} - -const lookupAircraft = (icao) => { - const clean = icao.replace(/^~/, '').toUpperCase().padStart(6, '0'); - return db?.prepare(`SELECT icao, reg, type_code, flags, desc FROM aircraft WHERE icao = ?`).get(clean); + if(!fs.existsSync(DATA)) fs.mkdirSync(DATA, {recursive: true}); + console.log('✈️ Downloading aircraft CSV...'); + await loadAircraftCsv(); } export function enrichAircraft(a) { if(!a.hex) return a; - const row = lookupAircraft(a.hex); + const key = a.hex.replace(/^~/, '').toUpperCase(); + const row = aircraftMap.get(key); return { ...a, registration: row?.reg || a.r || null, type_code: row?.type_code || a.t || null, type_desc: row?.desc || null, - ...decodeFlags(row?.flags), }; }