aircraft updates

This commit is contained in:
2026-06-25 13:36:39 -04:00
parent 1f5ae434b3
commit 40b6a5488f
3 changed files with 117 additions and 41 deletions

View File

@@ -60,13 +60,9 @@ function getAltColor(alt: number): [number, number, number] {
function buildPlaneIcon(plane: any, shapes: any): string {
const color = COLORS[plane.class] || COLORS['Unknown']
const shape = shapes[plane.shape] || shapes['unknown'] || shapes[<any>Object.keys(shapes)[0]]
if (!shape) {
return `data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><polygon points="12,2 22,22 12,17 2,22" fill="${color}" stroke="#000" stroke-width="1"/></svg>`)}`
}
const paths = (Array.isArray(shape.path) ? shape.path : [shape.path])
.map((p: string) => `<path d="${p}" fill="${color}" stroke="#000" stroke-width="${shape.stroke || 1}"/>`)
.join('')
const shape = shapes[plane.typecode]
if(!shape) return `data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><polygon points="12,2 22,22 12,17 2,22" fill="${color}" stroke="#000" stroke-width="1"/></svg>`)}`
const paths = (Array.isArray(shape.path) ? shape.path : [shape.path]).map((p: string) => `<path d="${p}" fill="${color}" stroke="#000" stroke-width="${shape.stroke || 1}"/>`).join('')
return `data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="${shape.w * 2}" height="${shape.h * 2}" viewBox="${shape.viewBox}">${paths}</svg>`)}`
}

View File

@@ -1,48 +1,128 @@
import {cfg} from './config.mjs';
import {resolve, dirname} from 'path';
import {fileURLToPath} from 'url';
import { cfg } from './config.mjs';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
import * as fs from 'node:fs';
import {fromCsv} from '@ztimson/utils';
import Database from 'better-sqlite3';
import { fromCsv } from '@ztimson/utils';
const DIR = dirname(fileURLToPath(import.meta.url));
const DATA = resolve(DIR, '../data');
const SHAPES_PATH = resolve(DATA, 'shapes.json');
const DB_PATH = resolve(DATA, 'aircraft.db');
const CSV_CACHE = resolve(DATA, 'aircraft_db.csv');
const SHAPES_PATH = resolve(DATA, 'shapes.json');
const history = new Map();
const MAX_HISTORY = 500;
const CSV_URL = 'https://s3.opensky-network.org/data-samples/metadata/aircraft-database-complete-2024-06.csv';
let aircraftMap = new Map();
const MILITARY_OPERATORS = ['air force', 'army', 'navy', 'marine', 'coast guard', 'military', 'defence', 'defense', 'luftwaffe', 'RAF', 'USAF', 'USN', 'USMC'];
const CARGO_OPERATORS = ['fedex', 'ups', 'dhl', 'cargo', 'freight', 'logistic', 'atlas air', 'kalitta', 'air freight'];
const PASSENGER_OPERATORS = ['airlines', 'airways', 'air ', 'jet', 'easyjet', 'ryanair', 'delta', 'united', 'american', 'southwest', 'lufthansa', 'emirates', 'british'];
const MILITARY_CLASSES = ['H1M', 'H2M', 'L1M', 'L2M', 'L4M', 'A0'];
let db;
export async function initAircraftDb() {
if(!fs.existsSync(DATA)) fs.mkdirSync(DATA, {recursive: true});
const missing = !fs.existsSync(CSV_CACHE);
console.log(`✈️ Aircraft cache ${missing ? 'downloading' : 'loading'}`);
if(missing) {
if (!fs.existsSync(DATA)) fs.mkdirSync(DATA, { recursive: true });
const missing = !fs.existsSync(DB_PATH);
if(missing) console.log(`✈️ Building database`);
db = new Database(DB_PATH);
db.exec(`
CREATE TABLE IF NOT EXISTS aircraft (
icao24 TEXT PRIMARY KEY,
built TEXT,
categoryDescription TEXT,
country TEXT,
engines TEXT,
class TEXT,
manufacturer TEXT,
model TEXT,
modes TEXT,
operator TEXT,
operatorCallsign TEXT,
owner TEXT,
registration TEXT,
serialNumber TEXT,
aircraft TEXT
)
`);
if (missing) {
const res = await fetch(CSV_URL);
if(!res.ok) throw new Error(`HTTP ${res.status}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const text = await res.text();
fs.promises.writeFile(CSV_CACHE, text);
aircraftMap = fromCsv(text, true);
console.log(`✈️ Aircraft loaded: ${aircraftMap.length} (fresh)`);
fs.writeFileSync(CSV_CACHE, text);
const csv = fromCsv(text, true);
const insert = db.prepare(`
INSERT OR REPLACE INTO aircraft VALUES (
@icao24, @built, @categoryDescription, @country, @engines,
@class, @manufacturer, @model, @modes, @operator,
@operatorCallsign, @owner, @registration, @serialNumber, @aircraft
)
`);
const insertMany = db.transaction(rows => {
for (const row of rows) insert.run(row);
});
insertMany(csv.map(row => ({
icao24: (row.icao24 || '').toUpperCase(),
built: row.built || null,
categoryDescription: row.categoryDescription || null,
country: row.country || null,
engines: row.engines || null,
class: row.icaoAircraftClass || null,
manufacturer: row.manufacturerName || row.manufacturerIcao || null,
model: row.model || null,
modes: row.modes || null,
operator: row.operator || null,
operatorCallsign: row.operatorCallsign || null,
owner: row.owner || null,
registration: row.registration || null,
serialNumber: row.serialNumber || null,
aircraft: row.typecode || null,
})));
fs.unlinkSync(CSV_CACHE);
console.log(`✈️ Aircraft imported: ${csv.length}`);
} else {
const text = fs.readFileSync(CSV_CACHE, 'utf8');
aircraftMap = fromCsv(text, true);
console.log(`✈️ Aircraft loaded: ${aircraftMap.length} (cache)`);
const count = db.prepare('SELECT COUNT(*) as c FROM aircraft').get();
console.log(`✈️ Aircraft loaded: ${count.c}`);
}
}
function wordMatch(text, keyword) {
if (!text) return false;
const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
return new RegExp(`(?<![\\w])${escaped}(?![\\w])`, 'i').test(text);
}
export function classifyAircraft(row) {
if (!row) return 'unknown';
const matchesAny = (fields, list) =>
fields.some(f => list.some(k => wordMatch(f, k)));
const operatorFields = [row.operator, row.operatorCallsign];
const ownerFields = [row.owner];
const allFields = [...operatorFields, ...ownerFields, row.categoryDescription];
if (MILITARY_CLASSES.includes(row.class) || matchesAny(allFields, MILITARY_OPERATORS)) return 'military';
if (row.categoryDescription?.toLowerCase().includes('cargo') || matchesAny(operatorFields, CARGO_OPERATORS)) return 'cargo';
if (matchesAny(operatorFields, PASSENGER_OPERATORS)) return 'passenger';
if (row.owner && !row.operator) return 'private';
return 'unknown';
}
export function enrichAircraft(a) {
if(!a.hex) return a;
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,
};
if (!a.hex) return a;
const row = db.prepare('SELECT * FROM aircraft WHERE icao24 = ?').get(a.hex.toUpperCase());
if (!row) return a;
return { ...a, ...row, type: classifyAircraft(row) };
}
export async function getShapes() {
@@ -50,24 +130,24 @@ export async function getShapes() {
}
export async function getADSB() {
const {ADSB_URL} = cfg();
if(!ADSB_URL) return [];
const { ADSB_URL } = cfg();
if (!ADSB_URL) return [];
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;
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, []);
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();
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()) || []};
return { history: history.get(icao?.toLowerCase()) || [] };
}