diff --git a/client/src/services/airtraffic.ts b/client/src/services/airtraffic.ts index 7a91641..dea1f58 100644 --- a/client/src/services/airtraffic.ts +++ b/client/src/services/airtraffic.ts @@ -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[Object.keys(shapes)[0]] - if (!shape) { - return `data:image/svg+xml,${encodeURIComponent(``)}` - } - const paths = (Array.isArray(shape.path) ? shape.path : [shape.path]) - .map((p: string) => ``) - .join('') + const shape = shapes[plane.typecode] + if(!shape) return `data:image/svg+xml,${encodeURIComponent(``)}` + const paths = (Array.isArray(shape.path) ? shape.path : [shape.path]).map((p: string) => ``).join('') return `data:image/svg+xml,${encodeURIComponent(`${paths}`)}` } diff --git a/server/data/aircraft_db.csv b/server/data/aircraft.db similarity index 62% rename from server/data/aircraft_db.csv rename to server/data/aircraft.db index 8ae87df..c55f8fb 100644 Binary files a/server/data/aircraft_db.csv and b/server/data/aircraft.db differ diff --git a/server/src/adsb.mjs b/server/src/adsb.mjs index 60cc1b8..3ba167c 100644 --- a/server/src/adsb.mjs +++ b/server/src/adsb.mjs @@ -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(`(? + 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()) || [] }; }