aircraft updates
This commit is contained in:
@@ -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>`)}`
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
@@ -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()) || [] };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user