ADSB updates
This commit is contained in:
@@ -248,7 +248,7 @@ export class AirTrafficLayer {
|
|||||||
|
|
||||||
private async _fetch() {
|
private async _fetch() {
|
||||||
const j = await fetch(`${API}/adsb`).then(r => r.json())
|
const j = await fetch(`${API}/adsb`).then(r => r.json())
|
||||||
this.data = (j.data || []).map((a: any) => ({
|
this.data = (j || []).map((a: any) => ({
|
||||||
...a,
|
...a,
|
||||||
icao: a.hex,
|
icao: a.hex,
|
||||||
latitude: a.lat,
|
latitude: a.lat,
|
||||||
|
|||||||
160
server/src/adsb.mjs
Normal file
160
server/src/adsb.mjs
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
import {cfg} from './config.mjs';
|
||||||
|
import Database from 'better-sqlite3';
|
||||||
|
import {createWriteStream, existsSync, mkdirSync} from 'fs';
|
||||||
|
import {readFile, rm} from 'fs/promises';
|
||||||
|
import {resolve, dirname} from 'path';
|
||||||
|
import {fileURLToPath} from 'url';
|
||||||
|
import {pipeline} from 'stream/promises';
|
||||||
|
import {createGunzip} from 'zlib';
|
||||||
|
import AdmZip from 'adm-zip';
|
||||||
|
|
||||||
|
const DIR = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const DATA = resolve(DIR, '../data');
|
||||||
|
const DB_PATH = resolve(DATA, 'aircraft.db');
|
||||||
|
const DB_ZIP = resolve(DATA, 'aircraft_db.zip');
|
||||||
|
const TY_GZ = resolve(DATA, 'aircraft_types.gz');
|
||||||
|
const SHAPES_PATH = resolve(DATA, 'shapes.json');
|
||||||
|
const history = new Map(); // icao -> [{lat, lon, alt, ts}]
|
||||||
|
const MAX_HISTORY = 500;
|
||||||
|
|
||||||
|
const DB_URL = 'https://www.mictronics.de/aircraft-database/aircraft_db.php';
|
||||||
|
const TY_URL = 'https://www.mictronics.de/aircraft-database/aircraft_types.php';
|
||||||
|
|
||||||
|
let db;
|
||||||
|
|
||||||
|
async function downloadFile(url, dest) {
|
||||||
|
const res = await fetch(url);
|
||||||
|
if(!res.ok) throw new Error(`Download failed ${url}: ${res.status}`);
|
||||||
|
await pipeline(res.body, createWriteStream(dest));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadTypes() {
|
||||||
|
await downloadFile(TY_URL, TY_GZ);
|
||||||
|
const gunzip = createGunzip();
|
||||||
|
const chunks = [];
|
||||||
|
await new Promise((res, rej) => {
|
||||||
|
const gz = require('fs').createReadStream(TY_GZ);
|
||||||
|
gz.pipe(gunzip);
|
||||||
|
gunzip.on('data', c => chunks.push(c));
|
||||||
|
gunzip.on('end', res);
|
||||||
|
gunzip.on('error', rej);
|
||||||
|
});
|
||||||
|
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function importTypes(types) {
|
||||||
|
const insert = db.prepare(`INSERT OR IGNORE INTO aircraft_types(type_code, desc, wtc, icon) VALUES (?,?,?,?)`);
|
||||||
|
const run = db.transaction(() => {
|
||||||
|
for(const [code, t] of Object.entries(types)) {
|
||||||
|
insert.run(code, t.desc || null, t.wtc || null, t.icon || null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
run();
|
||||||
|
console.log(`✈️ Types imported: ${Object.keys(types).length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function importAircraftDb() {
|
||||||
|
await downloadFile(DB_URL, DB_ZIP);
|
||||||
|
const zip = new AdmZip(DB_ZIP);
|
||||||
|
const entries = zip.getEntries();
|
||||||
|
|
||||||
|
const insert = db.prepare(`INSERT OR IGNORE INTO aircraft(icao, reg, type_code) VALUES (?,?,?)`);
|
||||||
|
|
||||||
|
let total = 0;
|
||||||
|
const run = db.transaction((rows) => {
|
||||||
|
for(const [icao, a] of Object.entries(rows)) {
|
||||||
|
insert.run(icao.toUpperCase(), a.r || null, a.t || null);
|
||||||
|
total++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for(const entry of entries) {
|
||||||
|
if(!entry.entryName.endsWith('.json')) continue;
|
||||||
|
const rows = JSON.parse(entry.getData().toString('utf8'));
|
||||||
|
run(rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✈️ Aircraft imported: ${total}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initSchema() {
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS aircraft (
|
||||||
|
icao TEXT PRIMARY KEY,
|
||||||
|
reg TEXT,
|
||||||
|
type_code TEXT
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS aircraft_types (
|
||||||
|
type_code TEXT PRIMARY KEY,
|
||||||
|
desc TEXT,
|
||||||
|
wtc TEXT,
|
||||||
|
icon TEXT
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initAircraftDb() {
|
||||||
|
if(!existsSync(DATA)) mkdirSync(DATA, {recursive: true});
|
||||||
|
db = new Database(DB_PATH);
|
||||||
|
initSchema();
|
||||||
|
|
||||||
|
const count = db.prepare('SELECT COUNT(*) as n FROM aircraft').get();
|
||||||
|
if(count.n > 0) {
|
||||||
|
console.log(`✈️ Aircraft DB already loaded (${count.n} rows) — skipping download`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✈️ Downloading aircraft DB...');
|
||||||
|
const [, types] = await Promise.all([
|
||||||
|
importAircraftDb(),
|
||||||
|
downloadTypes().then(t => importTypes(t))
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const lookupAircraft = (icao) =>
|
||||||
|
db?.prepare(`
|
||||||
|
SELECT a.icao, a.reg, a.type_code, t.desc, t.wtc, t.icon
|
||||||
|
FROM aircraft a
|
||||||
|
LEFT JOIN aircraft_types t ON a.type_code = t.type_code
|
||||||
|
WHERE a.icao = ?
|
||||||
|
`).get(icao.toUpperCase());
|
||||||
|
|
||||||
|
export function enrichAircraft(a) {
|
||||||
|
if(!a.hex) return a;
|
||||||
|
const row = lookupAircraft(a.hex);
|
||||||
|
return {
|
||||||
|
...a,
|
||||||
|
registration: row?.reg || a.r || null,
|
||||||
|
type_code: row?.type_code || a.t || null,
|
||||||
|
type_desc: row?.desc || null,
|
||||||
|
wtc: row?.wtc || null,
|
||||||
|
icon: row?.icon || null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getShapes() {
|
||||||
|
return JSON.parse(await readFile(SHAPES_PATH, 'utf8'));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getADSB() {
|
||||||
|
const {ADSB_URL} = cfg();
|
||||||
|
if(!ADSB_URL) return {data: []};
|
||||||
|
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;
|
||||||
|
const key = a.hex.toLowerCase();
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
return aircraft.map(enrichAircraft);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAirTrafficHistory(icao) {
|
||||||
|
return {history: history.get(icao?.toLowerCase()) || []};
|
||||||
|
}
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import {cfg} from './config.mjs';
|
|
||||||
import {readFile} from 'fs/promises';
|
|
||||||
import {resolve, dirname} from 'path';
|
|
||||||
import {fileURLToPath} from 'url';
|
|
||||||
|
|
||||||
const DIR = dirname(fileURLToPath(import.meta.url));
|
|
||||||
const SHAPES_PATH = resolve(DIR, '../public/aircraft.json');
|
|
||||||
|
|
||||||
const history = new Map(); // icao -> [{lat, lon, alt, ts}]
|
|
||||||
const MAX_HISTORY = 500;
|
|
||||||
|
|
||||||
export async function getShapes() {
|
|
||||||
return JSON.parse(await readFile(SHAPES_PATH, 'utf8'));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getADSB() {
|
|
||||||
const {ADSB_URL} = cfg();
|
|
||||||
if(!ADSB_URL) return {data: []};
|
|
||||||
const r = await fetch(`${ADSB_URL}/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;
|
|
||||||
const key = a.hex.toLowerCase();
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {data: aircraft};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getAIS() {
|
|
||||||
const {ADSB_URL} = cfg();
|
|
||||||
if(!ADSB_URL) return {data: []};
|
|
||||||
const r = await fetch(`${ADSB_URL}:9990/api/ships_array.json`);
|
|
||||||
const j = await r.json();
|
|
||||||
console.log('todo');
|
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getAirTrafficHistory(icao) {
|
|
||||||
return {history: history.get(icao?.toLowerCase()) || []};
|
|
||||||
}
|
|
||||||
15
server/src/ais.mjs
Normal file
15
server/src/ais.mjs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export async function getAIS() {
|
||||||
|
const {ADSB_URL} = cfg();
|
||||||
|
if(!ADSB_URL) return {data: []};
|
||||||
|
const r = await fetch(`${ADSB_URL}:9990/api/ships_array.json`);
|
||||||
|
const j = await r.json();
|
||||||
|
|
||||||
|
const {keys, values} = j;
|
||||||
|
const boats = values.map(row => {
|
||||||
|
const obj = {type: 'vessel'};
|
||||||
|
keys.forEach((key, i) => obj[key] = row[i]);
|
||||||
|
return obj;
|
||||||
|
});
|
||||||
|
|
||||||
|
return boats;
|
||||||
|
}
|
||||||
@@ -8,10 +8,12 @@ import {getSpaceWeather} from './space.mjs';
|
|||||||
import {apiReference} from '@scalar/express-api-reference';
|
import {apiReference} from '@scalar/express-api-reference';
|
||||||
import {spec} from './spec.mjs';
|
import {spec} from './spec.mjs';
|
||||||
import {existsSync} from 'fs';
|
import {existsSync} from 'fs';
|
||||||
import {getADSB, getAirTrafficHistory, getAIS, getShapes} from './airtraffic.mjs';
|
import {getAIS} from './ais.mjs';
|
||||||
|
import {getADSB, getAirTrafficHistory, getShapes, initAircraftDb} from './adsb.mjs';
|
||||||
import {getWeatherCondition} from './openweather.mjs';
|
import {getWeatherCondition} from './openweather.mjs';
|
||||||
import {lastForecast, getForecast, forecastTTL} from './forecast.mjs';
|
import {lastForecast, getForecast, forecastTTL} from './forecast.mjs';
|
||||||
import {dailyWeather, hourlyWeather} from './openmeteo.mjs';
|
import {dailyWeather, hourlyWeather} from './openmeteo.mjs';
|
||||||
|
import {init} from 'express/lib/application.js';
|
||||||
// import {Aurora} from './aurora.mjs';
|
// import {Aurora} from './aurora.mjs';
|
||||||
|
|
||||||
// ── Uncaught error handlers ───────────────────────────────────────────────────
|
// ── Uncaught error handlers ───────────────────────────────────────────────────
|
||||||
@@ -190,11 +192,14 @@ app.use((err, req, res, next) => {
|
|||||||
|
|
||||||
// ── Start ─────────────────────────────────────────────────────────────────────
|
// ── Start ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
(async () => {
|
||||||
const c = cfg();
|
const c = cfg();
|
||||||
|
|
||||||
setTimeout(getForecast, 1)
|
setTimeout(getForecast, 1)
|
||||||
setInterval(getForecast, forecastTTL)
|
setInterval(getForecast, forecastTTL)
|
||||||
|
await initAircraftDb();
|
||||||
|
|
||||||
app.listen(c.PORT, () => {
|
app.listen(c.PORT, () => {
|
||||||
console.log(`🌦 Weather API — http://localhost:${c.PORT}`)
|
console.log(`🌦 Weather API — http://localhost:${c.PORT}`)
|
||||||
});
|
});
|
||||||
|
})()
|
||||||
|
|||||||
Reference in New Issue
Block a user