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 {
|
function buildPlaneIcon(plane: any, shapes: any): string {
|
||||||
const color = COLORS[plane.class] || COLORS['Unknown']
|
const color = COLORS[plane.class] || COLORS['Unknown']
|
||||||
const shape = shapes[plane.shape] || shapes['unknown'] || shapes[<any>Object.keys(shapes)[0]]
|
const shape = shapes[plane.typecode]
|
||||||
if (!shape) {
|
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>`)}`
|
||||||
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 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>`)}`
|
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 { cfg } from './config.mjs';
|
||||||
import {resolve, dirname} from 'path';
|
import { resolve, dirname } from 'path';
|
||||||
import {fileURLToPath} from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import * as fs from 'node:fs';
|
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 DIR = dirname(fileURLToPath(import.meta.url));
|
||||||
const DATA = resolve(DIR, '../data');
|
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 CSV_CACHE = resolve(DATA, 'aircraft_db.csv');
|
||||||
|
const SHAPES_PATH = resolve(DATA, 'shapes.json');
|
||||||
const history = new Map();
|
const history = new Map();
|
||||||
const MAX_HISTORY = 500;
|
const MAX_HISTORY = 500;
|
||||||
|
|
||||||
const CSV_URL = 'https://s3.opensky-network.org/data-samples/metadata/aircraft-database-complete-2024-06.csv';
|
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() {
|
export async function initAircraftDb() {
|
||||||
if(!fs.existsSync(DATA)) fs.mkdirSync(DATA, {recursive: true});
|
if (!fs.existsSync(DATA)) fs.mkdirSync(DATA, { recursive: true });
|
||||||
const missing = !fs.existsSync(CSV_CACHE);
|
|
||||||
console.log(`✈️ Aircraft cache ${missing ? 'downloading' : 'loading'}`);
|
const missing = !fs.existsSync(DB_PATH);
|
||||||
if(missing) {
|
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);
|
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();
|
const text = await res.text();
|
||||||
fs.promises.writeFile(CSV_CACHE, text);
|
fs.writeFileSync(CSV_CACHE, text);
|
||||||
aircraftMap = fromCsv(text, true);
|
|
||||||
console.log(`✈️ Aircraft loaded: ${aircraftMap.length} (fresh)`);
|
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 {
|
} else {
|
||||||
const text = fs.readFileSync(CSV_CACHE, 'utf8');
|
const count = db.prepare('SELECT COUNT(*) as c FROM aircraft').get();
|
||||||
aircraftMap = fromCsv(text, true);
|
console.log(`✈️ Aircraft loaded: ${count.c}`);
|
||||||
console.log(`✈️ Aircraft loaded: ${aircraftMap.length} (cache)`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
export function enrichAircraft(a) {
|
||||||
if(!a.hex) return a;
|
if (!a.hex) return a;
|
||||||
const key = a.hex.replace(/^~/, '').toUpperCase();
|
const row = db.prepare('SELECT * FROM aircraft WHERE icao24 = ?').get(a.hex.toUpperCase());
|
||||||
const row = aircraftMap.get(key);
|
if (!row) return a;
|
||||||
return {
|
return { ...a, ...row, type: classifyAircraft(row) };
|
||||||
...a,
|
|
||||||
registration: row?.reg || a.r || null,
|
|
||||||
type_code: row?.type_code || a.t || null,
|
|
||||||
type_desc: row?.desc || null,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getShapes() {
|
export async function getShapes() {
|
||||||
@@ -50,24 +130,24 @@ export async function getShapes() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getADSB() {
|
export async function getADSB() {
|
||||||
const {ADSB_URL} = cfg();
|
const { ADSB_URL } = cfg();
|
||||||
if(!ADSB_URL) return [];
|
if (!ADSB_URL) return [];
|
||||||
const r = await fetch(`${ADSB_URL}:8080/data/aircraft.json`);
|
const r = await fetch(`${ADSB_URL}:8080/data/aircraft.json`);
|
||||||
const j = await r.json();
|
const j = await r.json();
|
||||||
const aircraft = j.aircraft || [];
|
const aircraft = j.aircraft || [];
|
||||||
|
|
||||||
for(const a of aircraft) {
|
for (const a of aircraft) {
|
||||||
if(!a.hex || !a.lat || !a.lon) continue;
|
if (!a.hex || !a.lat || !a.lon) continue;
|
||||||
const key = a.hex.toLowerCase();
|
const key = a.hex.toLowerCase();
|
||||||
if(!history.has(key)) history.set(key, []);
|
if (!history.has(key)) history.set(key, []);
|
||||||
const trail = history.get(key);
|
const trail = history.get(key);
|
||||||
trail.push({latitude: a.lat, longitude: a.lon, altitude: a.alt_baro || 0, ts: Date.now()});
|
trail.push({ latitude: a.lat, longitude: a.lon, altitude: a.alt_baro || 0, ts: Date.now() });
|
||||||
if(trail.length > MAX_HISTORY) trail.shift();
|
if (trail.length > MAX_HISTORY) trail.shift();
|
||||||
}
|
}
|
||||||
|
|
||||||
return aircraft.map(enrichAircraft);
|
return aircraft.map(enrichAircraft);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAirTrafficHistory(icao) {
|
export async function getAirTrafficHistory(icao) {
|
||||||
return {history: history.get(icao?.toLowerCase()) || []};
|
return { history: history.get(icao?.toLowerCase()) || [] };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user