From afb530a999f02867c5f11053268662a28f1fafeb Mon Sep 17 00:00:00 2001 From: ztimson Date: Thu, 25 Jun 2026 10:15:23 -0400 Subject: [PATCH] ADSB updates --- client/src/services/airtraffic.ts | 2 +- .../aircraft.json => data/shapes.json} | 0 server/src/adsb.mjs | 160 ++++++++++++++++++ server/src/airtraffic.mjs | 46 ----- server/src/ais.mjs | 15 ++ server/src/server.mjs | 19 ++- 6 files changed, 188 insertions(+), 54 deletions(-) rename server/{public/aircraft.json => data/shapes.json} (100%) create mode 100644 server/src/adsb.mjs delete mode 100644 server/src/airtraffic.mjs create mode 100644 server/src/ais.mjs diff --git a/client/src/services/airtraffic.ts b/client/src/services/airtraffic.ts index f986636..ad7ad70 100644 --- a/client/src/services/airtraffic.ts +++ b/client/src/services/airtraffic.ts @@ -248,7 +248,7 @@ export class AirTrafficLayer { private async _fetch() { const j = await fetch(`${API}/adsb`).then(r => r.json()) - this.data = (j.data || []).map((a: any) => ({ + this.data = (j || []).map((a: any) => ({ ...a, icao: a.hex, latitude: a.lat, diff --git a/server/public/aircraft.json b/server/data/shapes.json similarity index 100% rename from server/public/aircraft.json rename to server/data/shapes.json diff --git a/server/src/adsb.mjs b/server/src/adsb.mjs new file mode 100644 index 0000000..13dd24e --- /dev/null +++ b/server/src/adsb.mjs @@ -0,0 +1,160 @@ +import {cfg} from './config.mjs'; +import Database from 'better-sqlite3'; +import {createWriteStream, existsSync, mkdirSync} from 'fs'; +import {readFile, rm} from 'fs/promises'; +import {resolve, dirname} from 'path'; +import {fileURLToPath} from 'url'; +import {pipeline} from 'stream/promises'; +import {createGunzip} from 'zlib'; +import AdmZip from 'adm-zip'; + +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 TY_GZ = resolve(DATA, 'aircraft_types.gz'); +const SHAPES_PATH = resolve(DATA, 'shapes.json'); +const history = new Map(); // icao -> [{lat, lon, alt, ts}] +const MAX_HISTORY = 500; + +const DB_URL = 'https://www.mictronics.de/aircraft-database/aircraft_db.php'; +const TY_URL = 'https://www.mictronics.de/aircraft-database/aircraft_types.php'; + +let db; + +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)); +} + +async function downloadTypes() { + await downloadFile(TY_URL, TY_GZ); + const gunzip = createGunzip(); + const chunks = []; + await new Promise((res, rej) => { + const gz = require('fs').createReadStream(TY_GZ); + gz.pipe(gunzip); + gunzip.on('data', c => chunks.push(c)); + gunzip.on('end', res); + gunzip.on('error', rej); + }); + return JSON.parse(Buffer.concat(chunks).toString('utf8')); +} + +async function importTypes(types) { + const insert = db.prepare(`INSERT OR IGNORE INTO aircraft_types(type_code, desc, wtc, icon) VALUES (?,?,?,?)`); + const run = db.transaction(() => { + for(const [code, t] of Object.entries(types)) { + insert.run(code, t.desc || null, t.wtc || null, t.icon || null); + } + }); + run(); + console.log(`✈️ Types imported: ${Object.keys(types).length}`); +} + +async function importAircraftDb() { + await downloadFile(DB_URL, DB_ZIP); + const zip = new AdmZip(DB_ZIP); + const entries = zip.getEntries(); + + const insert = db.prepare(`INSERT OR IGNORE INTO aircraft(icao, reg, type_code) VALUES (?,?,?)`); + + let total = 0; + const run = db.transaction((rows) => { + for(const [icao, a] of Object.entries(rows)) { + insert.run(icao.toUpperCase(), a.r || null, a.t || null); + total++; + } + }); + + for(const entry of entries) { + if(!entry.entryName.endsWith('.json')) continue; + const rows = JSON.parse(entry.getData().toString('utf8')); + run(rows); + } + + console.log(`✈️ Aircraft imported: ${total}`); +} + +function initSchema() { + db.exec(` + CREATE TABLE IF NOT EXISTS aircraft ( + icao TEXT PRIMARY KEY, + reg TEXT, + type_code TEXT + ); + CREATE TABLE IF NOT EXISTS aircraft_types ( + type_code TEXT PRIMARY KEY, + desc TEXT, + wtc TEXT, + icon TEXT + ); + `); +} + +export async function initAircraftDb() { + if(!existsSync(DATA)) mkdirSync(DATA, {recursive: true}); + db = new Database(DB_PATH); + initSchema(); + + const count = db.prepare('SELECT COUNT(*) as n FROM aircraft').get(); + if(count.n > 0) { + console.log(`✈️ Aircraft DB already loaded (${count.n} rows) — skipping download`); + return; + } + + console.log('✈️ Downloading aircraft DB...'); + const [, types] = await Promise.all([ + importAircraftDb(), + downloadTypes().then(t => importTypes(t)) + ]); +} + +const lookupAircraft = (icao) => + db?.prepare(` + SELECT a.icao, a.reg, a.type_code, t.desc, t.wtc, t.icon + FROM aircraft a + LEFT JOIN aircraft_types t ON a.type_code = t.type_code + WHERE a.icao = ? + `).get(icao.toUpperCase()); + +export function enrichAircraft(a) { + if(!a.hex) return a; + const row = lookupAircraft(a.hex); + return { + ...a, + registration: row?.reg || a.r || null, + type_code: row?.type_code || a.t || null, + type_desc: row?.desc || null, + wtc: row?.wtc || null, + icon: row?.icon || null, + }; +} + +export async function getShapes() { + return JSON.parse(await readFile(SHAPES_PATH, 'utf8')); +} + +export async function getADSB() { + const {ADSB_URL} = cfg(); + if(!ADSB_URL) return {data: []}; + const r = await fetch(`${ADSB_URL}:8080/data/aircraft.json`); + const j = await r.json(); + const aircraft = j.aircraft || []; + + for(const a of aircraft) { + if(!a.hex || !a.lat || !a.lon) continue; + const key = a.hex.toLowerCase(); + if(!history.has(key)) history.set(key, []); + const trail = history.get(key); + trail.push({latitude: a.lat, longitude: a.lon, altitude: a.alt_baro || 0, ts: Date.now()}); + if(trail.length > MAX_HISTORY) trail.shift(); + } + + return aircraft.map(enrichAircraft); +} + +export async function getAirTrafficHistory(icao) { + return {history: history.get(icao?.toLowerCase()) || []}; +} diff --git a/server/src/airtraffic.mjs b/server/src/airtraffic.mjs deleted file mode 100644 index 76ddb22..0000000 --- a/server/src/airtraffic.mjs +++ /dev/null @@ -1,46 +0,0 @@ -import {cfg} from './config.mjs'; -import {readFile} from 'fs/promises'; -import {resolve, dirname} from 'path'; -import {fileURLToPath} from 'url'; - -const DIR = dirname(fileURLToPath(import.meta.url)); -const SHAPES_PATH = resolve(DIR, '../public/aircraft.json'); - -const history = new Map(); // icao -> [{lat, lon, alt, ts}] -const MAX_HISTORY = 500; - -export async function getShapes() { - return JSON.parse(await readFile(SHAPES_PATH, 'utf8')); -} - -export async function getADSB() { - const {ADSB_URL} = cfg(); - if(!ADSB_URL) return {data: []}; - const r = await fetch(`${ADSB_URL}/data/aircraft.json`); - const j = await r.json(); - const aircraft = j.aircraft || []; - - for(const a of aircraft) { - if(!a.hex || !a.lat || !a.lon) continue; - const key = a.hex.toLowerCase(); - if(!history.has(key)) history.set(key, []); - const trail = history.get(key); - trail.push({latitude: a.lat, longitude: a.lon, altitude: a.alt_baro || 0, ts: Date.now()}); - if(trail.length > MAX_HISTORY) trail.shift(); - } - - return {data: aircraft}; -} - -export async function getAIS() { - const {ADSB_URL} = cfg(); - if(!ADSB_URL) return {data: []}; - const r = await fetch(`${ADSB_URL}:9990/api/ships_array.json`); - const j = await r.json(); - console.log('todo'); - // TODO -} - -export async function getAirTrafficHistory(icao) { - return {history: history.get(icao?.toLowerCase()) || []}; -} diff --git a/server/src/ais.mjs b/server/src/ais.mjs new file mode 100644 index 0000000..d997125 --- /dev/null +++ b/server/src/ais.mjs @@ -0,0 +1,15 @@ +export async function getAIS() { + const {ADSB_URL} = cfg(); + if(!ADSB_URL) return {data: []}; + const r = await fetch(`${ADSB_URL}:9990/api/ships_array.json`); + const j = await r.json(); + + const {keys, values} = j; + const boats = values.map(row => { + const obj = {type: 'vessel'}; + keys.forEach((key, i) => obj[key] = row[i]); + return obj; + }); + + return boats; +} diff --git a/server/src/server.mjs b/server/src/server.mjs index e9a0960..7362966 100644 --- a/server/src/server.mjs +++ b/server/src/server.mjs @@ -8,10 +8,12 @@ import {getSpaceWeather} from './space.mjs'; import {apiReference} from '@scalar/express-api-reference'; import {spec} from './spec.mjs'; import {existsSync} from 'fs'; -import {getADSB, getAirTrafficHistory, getAIS, getShapes} from './airtraffic.mjs'; +import {getAIS} from './ais.mjs'; +import {getADSB, getAirTrafficHistory, getShapes, initAircraftDb} from './adsb.mjs'; import {getWeatherCondition} from './openweather.mjs'; import {lastForecast, getForecast, forecastTTL} from './forecast.mjs'; import {dailyWeather, hourlyWeather} from './openmeteo.mjs'; +import {init} from 'express/lib/application.js'; // import {Aurora} from './aurora.mjs'; // ── Uncaught error handlers ─────────────────────────────────────────────────── @@ -190,11 +192,14 @@ app.use((err, req, res, next) => { // ── Start ───────────────────────────────────────────────────────────────────── -const c = cfg(); +(async () => { + const c = cfg(); -setTimeout(getForecast, 1) -setInterval(getForecast, forecastTTL) + setTimeout(getForecast, 1) + setInterval(getForecast, forecastTTL) + await initAircraftDb(); -app.listen(c.PORT, () => { - console.log(`🌦 Weather API — http://localhost:${c.PORT}`) -}); + app.listen(c.PORT, () => { + console.log(`🌦 Weather API — http://localhost:${c.PORT}`) + }); +})()