ADSB updates

This commit is contained in:
2026-06-25 09:13:16 -04:00
parent e91876b7b7
commit 19d2dd2d9d
3 changed files with 53 additions and 59 deletions

View File

@@ -213,7 +213,7 @@ export class AirTrafficLayer {
if (this.visible) return if (this.visible) return
this.visible = true this.visible = true
if (!this.shapes) this.shapes = await fetch(`${API}/air-traffic-shapes`).then(r => r.json()) if (!this.shapes) this.shapes = await fetch(`${API}/vehicles`).then(r => r.json())
this.layer = new VectorLayer({ source: new VectorSource(), zIndex: 100 }) this.layer = new VectorLayer({ source: new VectorSource(), zIndex: 100 })
this.map.addLayer(this.layer) this.map.addLayer(this.layer)
@@ -226,7 +226,7 @@ export class AirTrafficLayer {
await this._fetch() await this._fetch()
this._draw() this._draw()
this._refreshPopups() this._refreshPopups()
}, 15_000) }, 1_000)
} }
hide() { hide() {
@@ -247,7 +247,7 @@ export class AirTrafficLayer {
// ── Private ─────────────────────────────────────────────────────────────── // ── Private ───────────────────────────────────────────────────────────────
private async _fetch() { private async _fetch() {
const j = await fetch(`${API}/air-traffic`).then(r => r.json()) const j = await fetch(`${API}/adsb`).then(r => r.json())
this.data = (j.data || []).map((a: any) => ({ this.data = (j.data || []).map((a: any) => ({
...a, ...a,
icao: a.hex, icao: a.hex,
@@ -262,46 +262,30 @@ export class AirTrafficLayer {
private _draw() { private _draw() {
const source = this.layer.getSource()! const source = this.layer.getSource()!
const existing: Record<string, Feature> = {} source.clear()
source.getFeatures().forEach(f => { existing[f.get('icao')] = f })
const incoming = new Set(this.data.filter(p => p.latitude != null).map(p => p.icao))
// Remove stale
Object.keys(existing).forEach(icao => {
if (!incoming.has(icao)) { source.removeFeature(<any>existing[icao]); this._closePopup(icao) }
})
// Update / add
for (const plane of this.data) { for (const plane of this.data) {
if (plane.latitude == null) continue if (plane.latitude == null) continue
const coord = fromLonLat([plane.longitude, plane.latitude]) const coord = fromLonLat([plane.longitude, plane.latitude])
const style = new Style({ const f = new Feature({ geometry: new Point(coord) })
f.set('icao', plane.icao)
f.set('planeData', plane)
f.setStyle(new Style({
image: new Icon({ image: new Icon({
src: buildPlaneIcon(plane, this.shapes), src: buildPlaneIcon(plane, this.shapes),
scale: 1, scale: 1,
rotation: (plane.heading ?? 0) * (Math.PI / 180), rotation: (plane.heading ?? 0) * (Math.PI / 180),
anchor: [0.5, 0.5], anchor: [0.5, 0.5],
}), }),
}) }))
if (existing[plane.icao]) {
;(<any>((<any>existing[plane.icao]).getGeometry() as Point)).setCoordinates(coord)
(<any>existing[plane.icao]).setStyle(style)
(<any>existing[plane.icao]).set('planeData', plane)
} else {
const f = new Feature({ geometry: new Point(coord) })
f.set('icao', plane.icao)
f.set('planeData', plane)
f.setStyle(style)
source.addFeature(f) source.addFeature(f)
} }
} }
}
private async _fetchTrace(plane: any) { private async _fetchTrace(plane: any) {
const icao = plane.icao const icao = plane.icao
if (!this.historyCache[icao]) { if (!this.historyCache[icao]) {
const j = await fetch(`${API}/air-traffic/${icao}`).then(r => r.json()) const j = await fetch(`${API}/adsb/${icao}`).then(r => r.json())
this.historyCache[icao] = j.history || [] this.historyCache[icao] = j.history || []
} }

View File

@@ -1,37 +1,46 @@
import { cfg } from './config.mjs' import {cfg} from './config.mjs';
import { readFile } from 'fs/promises' import {readFile} from 'fs/promises';
import { resolve, dirname } from 'path' import {resolve, dirname} from 'path';
import { fileURLToPath } from 'url' import {fileURLToPath} from 'url';
const DIR = dirname(fileURLToPath(import.meta.url)) const DIR = dirname(fileURLToPath(import.meta.url));
const SHAPES_PATH = resolve(DIR, '../public/aircraft.json') const SHAPES_PATH = resolve(DIR, '../public/aircraft.json');
const history = new Map() // icao -> [{lat, lon, alt, ts}] const history = new Map(); // icao -> [{lat, lon, alt, ts}]
const MAX_HISTORY = 500 const MAX_HISTORY = 500;
export async function getShapes() { export async function getShapes() {
return JSON.parse(await readFile(SHAPES_PATH, 'utf8')) return JSON.parse(await readFile(SHAPES_PATH, 'utf8'));
} }
export async function getAirTraffic() { export async function getADSB() {
const { ADSB_URL } = cfg() const {ADSB_URL} = cfg();
if (!ADSB_URL) return { data: [] } if(!ADSB_URL) return {data: []};
const r = await fetch(`${ADSB_URL}/data/aircraft.json`) const r = await fetch(`${ADSB_URL}/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.unshift({ 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 { data: aircraft } 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) { export async function getAirTrafficHistory(icao) {
return { history: history.get(icao?.toLowerCase()) || [] } return {history: history.get(icao?.toLowerCase()) || []};
} }

View File

@@ -1,18 +1,18 @@
import express from 'express'; import express from 'express';
import {resolve, dirname} from 'path'; import {resolve, dirname} from 'path';
import {fileURLToPath} from 'url'; import {fileURLToPath} from 'url';
import {cfg, localDateStr, tzOffsetMinutes} from './config.mjs'; import {cfg, localDateStr} from './config.mjs';
import {queryCurrent, queryHourly, queryDaily, getCoords} from './influx.mjs'; import {queryCurrent, queryHourly, queryDaily, getCoords} from './influx.mjs';
import {getCelestialCurrent, getCelestialForecast} from './celestial.mjs'; import {getCelestialCurrent, getCelestialForecast} from './celestial.mjs';
import {getSpaceWeather} from './space.mjs'; 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 {getAirTraffic, getAirTrafficHistory, getShapes} from './airtraffic.mjs'; import {getADSB, getAirTrafficHistory, getAIS, getShapes} from './airtraffic.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 {Aurora} from './aurora.mjs'; // import {Aurora} from './aurora.mjs';
// ── Uncaught error handlers ─────────────────────────────────────────────────── // ── Uncaught error handlers ───────────────────────────────────────────────────
@@ -146,7 +146,7 @@ app.get('/api/position', async (req, res) => {
// ── Space ───────────────────────────────────────────────────────────────────── // ── Space ─────────────────────────────────────────────────────────────────────
app.get('/api/aurora', async (req, res) => res.json(await Aurora.get())); // app.get('/api/aurora', async (req, res) => res.json(await Aurora.get()));
app.get('/api/space', async (req, res) => { app.get('/api/space', async (req, res) => {
const {fields} = req.query; const {fields} = req.query;
res.json(filterFields(await getSpaceWeather(), fields)); res.json(filterFields(await getSpaceWeather(), fields));
@@ -154,9 +154,10 @@ app.get('/api/space', async (req, res) => {
// ── ADSB Proxy ──────────────────────────────────────────────────────────────── // ── ADSB Proxy ────────────────────────────────────────────────────────────────
app.get('/api/air-traffic', async (req, res) => res.json(await getAirTraffic())); app.get('/api/adsb', async (req, res) => res.json(await getADSB()));
app.get('/api/air-traffic/:icao', async (req, res) => res.json(await getAirTrafficHistory(req.params.icao))); app.get('/api/adsb/:icao', async (req, res) => res.json(await getAirTrafficHistory(req.params.icao)));
app.get('/api/air-traffic-shapes', async (req, res) => res.json(await getShapes())); app.get('/api/ais', async (req, res) => res.json(await getAIS()));
app.get('/api/vehicles', async (req, res) => res.json(await getShapes()));
// ── DOCS ────────────────────────────────────────────────────────────────────── // ── DOCS ──────────────────────────────────────────────────────────────────────