ADSB enrichment
This commit is contained in:
@@ -1,100 +1,81 @@
|
|||||||
import {cfg} from './config.mjs';
|
import {cfg} from './config.mjs';
|
||||||
import Database from 'better-sqlite3';
|
|
||||||
import {createWriteStream, existsSync, mkdirSync} from 'fs';
|
|
||||||
import {resolve, dirname} from 'path';
|
import {resolve, dirname} from 'path';
|
||||||
import {fileURLToPath} from 'url';
|
import {fileURLToPath} from 'url';
|
||||||
import {pipeline} from 'stream/promises';
|
|
||||||
import AdmZip from 'adm-zip';
|
|
||||||
import * as fs from 'node:fs';
|
import * as fs from 'node:fs';
|
||||||
|
|
||||||
const DIR = dirname(fileURLToPath(import.meta.url));
|
const DIR = dirname(fileURLToPath(import.meta.url));
|
||||||
const DATA = resolve(DIR, '../data');
|
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 SHAPES_PATH = resolve(DATA, 'shapes.json');
|
||||||
|
const CSV_CACHE = resolve(DATA, 'aircraft_db.csv');
|
||||||
const history = new Map();
|
const history = new Map();
|
||||||
const MAX_HISTORY = 500;
|
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) {
|
function parseCsv(text) {
|
||||||
const res = await fetch(url);
|
const map = new Map();
|
||||||
if(!res.ok) throw new Error(`Download failed ${url}: ${res.status}`);
|
const lines = text.split('\n');
|
||||||
await pipeline(res.body, createWriteStream(dest));
|
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() {
|
async function loadAircraftCsv() {
|
||||||
db.exec(`
|
let text = null;
|
||||||
CREATE TABLE IF NOT EXISTS aircraft (
|
let fromCache = false;
|
||||||
icao TEXT PRIMARY KEY,
|
|
||||||
reg TEXT,
|
|
||||||
type_code TEXT,
|
|
||||||
flags TEXT,
|
|
||||||
desc TEXT
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function importAircraftDb() {
|
try {
|
||||||
await downloadFile(DB_URL, DB_ZIP);
|
const res = await fetch(CSV_URL);
|
||||||
const zip = new AdmZip(DB_ZIP);
|
if(!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
const entries = zip.getEntries();
|
text = await res.text();
|
||||||
|
} catch(e) {
|
||||||
let total = 0;
|
console.warn(`✈️ Failed to download CSV (${e.message}), trying cache...`);
|
||||||
const insert = db.prepare(`INSERT OR REPLACE INTO aircraft(icao, reg, type_code, flags, desc) VALUES (?,?,?,?,?)`);
|
if(fs.existsSync(CSV_CACHE)) {
|
||||||
const run = db.transaction((rows) => {
|
text = await fs.promises.readFile(CSV_CACHE, 'utf8');
|
||||||
for(const [icao, a] of Object.entries(rows)) {
|
fromCache = true;
|
||||||
insert.run(icao.toUpperCase().padStart(6, '0'), a.r || null, a.t || null, a.f || null, a.desc || null);
|
} else {
|
||||||
total++;
|
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);
|
aircraftMap = parseCsv(text);
|
||||||
console.log(`✈️ Aircraft imported: ${total}`);
|
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() {
|
export async function initAircraftDb() {
|
||||||
if(!existsSync(DATA)) mkdirSync(DATA, {recursive: true});
|
if(!fs.existsSync(DATA)) fs.mkdirSync(DATA, {recursive: true});
|
||||||
db = new Database(DB_PATH);
|
console.log('✈️ Downloading aircraft CSV...');
|
||||||
initSchema();
|
await loadAircraftCsv();
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function enrichAircraft(a) {
|
export function enrichAircraft(a) {
|
||||||
if(!a.hex) return a;
|
if(!a.hex) return a;
|
||||||
const row = lookupAircraft(a.hex);
|
const key = a.hex.replace(/^~/, '').toUpperCase();
|
||||||
|
const row = aircraftMap.get(key);
|
||||||
return {
|
return {
|
||||||
...a,
|
...a,
|
||||||
registration: row?.reg || a.r || null,
|
registration: row?.reg || a.r || null,
|
||||||
type_code: row?.type_code || a.t || null,
|
type_code: row?.type_code || a.t || null,
|
||||||
type_desc: row?.desc || null,
|
type_desc: row?.desc || null,
|
||||||
...decodeFlags(row?.flags),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user