From 2e5227f679d2a81ca97441e8a254b04dc6afdfab Mon Sep 17 00:00:00 2001 From: ztimson Date: Mon, 22 Jun 2026 00:02:16 -0400 Subject: [PATCH] ADSB --- client/index.html | 22 +- client/src/components/MapView.vue | 191 +++++++++----- client/src/services/airtraffic.ts | 426 ++++++++++++++++++++++++++++++ client/src/services/api.ts | 2 +- client/src/views/Dashboard.vue | 58 ++-- server/airtraffic.mjs | 49 ++++ server/config.mjs | 1 + server/public/favicon.png | Bin 0 -> 39924 bytes server/server.mjs | 6 + 9 files changed, 635 insertions(+), 120 deletions(-) create mode 100644 client/src/services/airtraffic.ts create mode 100644 server/airtraffic.mjs create mode 100644 server/public/favicon.png diff --git a/client/index.html b/client/index.html index 9e5fc8f..8f71eb0 100644 --- a/client/index.html +++ b/client/index.html @@ -1,13 +1,15 @@ - - - - - Vite App - - -
- - + + Weather Station + + + + + + + +
+ + diff --git a/client/src/components/MapView.vue b/client/src/components/MapView.vue index c61d0ce..fba76b7 100644 --- a/client/src/components/MapView.vue +++ b/client/src/components/MapView.vue @@ -1,4 +1,5 @@ diff --git a/client/src/services/airtraffic.ts b/client/src/services/airtraffic.ts new file mode 100644 index 0000000..4a83325 --- /dev/null +++ b/client/src/services/airtraffic.ts @@ -0,0 +1,426 @@ +import Map from 'ol/Map' +import { Feature } from 'ol' +import Point from 'ol/geom/Point' +import LineString from 'ol/geom/LineString' +import { fromLonLat } from 'ol/proj' +import { Style, Icon, Stroke } from 'ol/style' +import { Vector as VectorLayer } from 'ol/layer' +import { Vector as VectorSource } from 'ol/source' + +const API = '/api' + +const COLORS: Record = { + 'Cargo': '#00ff00', + 'Military': '#ff0000', + 'Private': '#8450ea', + 'Passenger': '#009dff', + 'Unknown': '#fad106', +} + +const SPEED_UNITS = { + knots: { label: 'KTS', convert: (v: number) => Math.round(v) }, + kph: { label: 'KPH', convert: (v: number) => Math.round(v * 1.852) }, + mph: { label: 'MPH', convert: (v: number) => Math.round(v * 1.15078) }, +} + +const ALTITUDE_UNITS = { + meters: { label: 'M', convert: (v: number) => Math.round(v * 0.3048) }, + feet: { label: 'FT', convert: (v: number) => v }, +} + +const VERTICAL_UNITS = { + mps: { label: 'M/S', convert: (v: number) => Math.round(v * 0.3048) }, + fps: { label: 'FT/S', convert: (v: number) => v }, +} + +const UNIT_KEY = 'at_units' +function getUnits() { + const s = localStorage.getItem(UNIT_KEY) + return s ? JSON.parse(s) : { speed: 'knots', altitude: 'meters', vertical: 'mps' } +} +function saveUnits(p: any) { localStorage.setItem(UNIT_KEY, JSON.stringify(p)) } + +function getAltColor(alt: number): [number, number, number] { + const a = Math.max(0, alt) + if (a < 10) return [0, 0, 0] + if (a < 10000) return [135, 206, 250] + if (a < 25000) { + const r = (a - 10000) / 15000 + return [Math.round(135 * (1-r)), Math.round(206 * (1-r)), Math.round(250 * (1-r) + 255 * r)] + } + if (a < 40000) { + const r = (a - 25000) / 15000 + return [0, 0, Math.round(255 * (1-r) + 139 * r)] + } + const r = Math.min((a - 40000) / 10000, 1) + return [Math.round(128 * r), 0, 139] +} + +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('') + return `data:image/svg+xml,${encodeURIComponent(`${paths}`)}` +} + +function buildNavballSvg(data: any): string { + const heading = data.heading ?? 0 + const airspeedKnots = data.speed || 0 + const verticalRateFps = data.climb || 0 + const groundSpeedFps = airspeedKnots * 1.68781 + const pitchRad = groundSpeedFps > 0 ? Math.atan(verticalRateFps / groundSpeedFps) : 0 + const pitch = Math.max(-25, Math.min(25, pitchRad * (180 / Math.PI))) + + const pitchLines: string[] = [] + for (let p = -30; p <= 30; p += 5) { + if (p === 0) continue + const isMajor = p % 10 === 0 + const [start, end] = [85, 115] + if (isMajor) { + pitchLines.push(` + ${Math.abs(p)} + + ${Math.abs(p)} + `) + } else { + pitchLines.push(``) + } + } + + const rollTicks: string[] = [] + for (const angle of [-90, -60, -45, -30, -20, -10, 0, 10, 20, 30, 45, 60, 90]) { + const rad = angle * Math.PI / 180 + const a = Math.abs(angle) + if (a === 45) { + rollTicks.push(``) + } else { + const inner = (a === 0 || a % 30 === 0) ? 72 : 77 + const sw = (a === 0 || a % 30 === 0) ? 2.5 : 1.5 + rollTicks.push(``) + } + } + + const compassLabels = [-60, -45, -30, -15, 0, 15, 30, 45, 60].map(offset => { + const rad = offset * Math.PI / 180 + const x = 100 + 90 * Math.sin(rad) + const y = 100 - 90 * Math.cos(rad) + const h = Math.round((offset + 360) % 360) + const lbl = ({ 0: 'N', 90: 'E', 180: 'S', 270: 'W' } as any)[h] || '' + return lbl ? `${lbl}` : '' + }).join('') + + return ` + + + + + + + + + + + + + + ${heading.toFixed(0).padStart(3, '0')}° + + + + + ${pitchLines.join('')} + + ${rollTicks.join('')} + + + + + ${compassLabels} + + + ` +} + +function buildGaugeHtml(label: string, value: any, unit: string, type: string): string { + return ` +
+
${label}
+
+ ${value} + ${unit} +
+
+ ` +} + +function buildPopupHtml(data: any): string { + const prefs = getUnits() + const speed = SPEED_UNITS[prefs.speed as keyof typeof SPEED_UNITS] + const alt = ALTITUDE_UNITS[prefs.altitude as keyof typeof ALTITUDE_UNITS] + const vert = VERTICAL_UNITS[prefs.vertical as keyof typeof VERTICAL_UNITS] + const callsign = data.name?.trim() + const altitude = data.alt_baro ?? data.alt_geom ?? 0 + + return ` +
+
+

+ ✈️ ${callsign + ? `${callsign.toUpperCase()}` + : 'N/A'} +

+
+ ${data.country || ''} • ${data.class || 'Unknown'} • ${data.type || ''} + ICAO: ${(data.icao || '').toUpperCase()} +
+
+
+
+ ${buildGaugeHtml('Air Speed', speed.convert(data.speed || 0), speed.label, 'speed')} + ${buildGaugeHtml('Altitude', data.landed ? 'LANDED' : alt.convert(altitude), data.landed ? '' : alt.label, 'altitude')} + ${buildGaugeHtml('Climb', vert.convert(data.climb || 0) >= 0 ? `+${vert.convert(data.climb || 0)}` : vert.convert(data.climb || 0), vert.label, 'vertical')} +
+
${buildNavballSvg(data)}
+
+
+ ` +} + +export class AirTrafficLayer { + private map: Map + private layer!: VectorLayer + private traceLayers: Record> = {} + private traceSegs: Record = {} + private popups: Record void }> = {} + private historyCache: Record = {} + private shapes: any = null + private data: any[] = [] + private clickHandler: ((e: any) => void) | null = null + private refreshTimer: ReturnType | null = null + visible = false + + constructor(map: Map) { this.map = map } + + // ── Public ──────────────────────────────────────────────────────────────── + + async show() { + if (this.visible) return + this.visible = true + + if (!this.shapes) this.shapes = await fetch(`${API}/air-traffic-shapes`).then(r => r.json()) + + this.layer = new VectorLayer({ source: new VectorSource(), zIndex: 100 }) + this.map.addLayer(this.layer) + + await this._fetch() + this._draw() + this._attachClick() + + this.refreshTimer = setInterval(async () => { + await this._fetch() + this._draw() + this._refreshPopups() + }, 15_000) + } + + hide() { + if (!this.visible) return + this.visible = false + + if (this.refreshTimer) { clearInterval(this.refreshTimer); this.refreshTimer = null } + if (this.clickHandler) { this.map.un('singleclick', this.clickHandler); this.clickHandler = null } + + for (const icao of Object.keys(this.popups)) this._closePopup(icao) + + this.map.removeLayer(this.layer) + Object.values(this.traceLayers).forEach(l => this.map.removeLayer(l)) + this.traceLayers = {} + this.traceSegs = {} + } + + // ── Private ─────────────────────────────────────────────────────────────── + + private async _fetch() { + const j = await fetch(`${API}/air-traffic`).then(r => r.json()) + this.data = j.data || [] + } + + private _draw() { + const source = this.layer.getSource()! + const existing: Record = {} + 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(existing[icao]); this._closePopup(icao) } + }) + + // Update / add + for (const plane of this.data) { + if (plane.latitude == null) continue + const coord = fromLonLat([plane.longitude, plane.latitude]) + const style = new Style({ + image: new Icon({ + src: buildPlaneIcon(plane, this.shapes), + scale: 1, + rotation: (plane.heading ?? 0) * (Math.PI / 180), + anchor: [0.5, 0.5], + }), + }) + + if (existing[plane.icao]) { + ;(((existing[plane.icao]).getGeometry() as Point)).setCoordinates(coord) + (existing[plane.icao]).setStyle(style) + (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) + } + } + } + + private async _fetchTrace(plane: any) { + const icao = plane.icao + if (!this.historyCache[icao]) { + const j = await fetch(`${API}/air-traffic/${icao}`).then(r => r.json()) + this.historyCache[icao] = j.history || [] + } + + const history = [...(this.historyCache[icao])] + const cur = { latitude: plane.latitude, longitude: plane.longitude, altitude: plane.alt_baro ?? plane.alt_geom ?? 0 } + const last = history[history.length - 1] + if (!last || last.latitude !== cur.latitude || last.longitude !== cur.longitude) history.push(cur) + if (history.length < 2) return + + let traceLayer = this.traceLayers[icao] + let vs: VectorSource + + if (!traceLayer) { + vs = new VectorSource() + traceLayer = new VectorLayer({ source: vs, zIndex: 99 }) + this.map.addLayer(traceLayer) + this.traceLayers[icao] = traceLayer + this.traceSegs[icao] = [] + } else { + vs = traceLayer.getSource()! + ;(this.traceSegs[icao] || []).forEach(f => vs.removeFeature(f)) + } + + const segs: Feature[] = [] + for (let i = 0; i < history.length - 1; i++) { + const s = history[i], e = history[i + 1] + const sc = getAltColor(s.altitude || 0) + const ec = getAltColor(e.altitude || 0) + const avg = sc.map((v, idx) => Math.round((v + ec[idx]) / 2)) + const f = new Feature(new LineString([fromLonLat([s.longitude, s.latitude]), fromLonLat([e.longitude, e.latitude])])) + f.setStyle(new Style({ stroke: new Stroke({ color: `rgba(${avg.join(',')},0.8)`, width: 3 }) })) + vs.addFeature(f) + segs.push(f) + } + this.traceSegs[icao] = segs + } + + private _openPopup(plane: any) { + const icao = plane.icao + if (this.popups[icao]) return + + const el = document.createElement('div') + el.style.cssText = ` + position:absolute; z-index:9999; pointer-events:auto; + background:rgba(0,0,0,0.88); border:1px solid rgba(200,200,220,0.3); + border-radius:8px; box-shadow:0 4px 24px rgba(0,0,0,0.5); overflow:hidden; + ` + + const close = document.createElement('button') + close.innerHTML = '✕' + close.style.cssText = 'position:absolute;top:8px;right:8px;background:none;border:none;color:#888;font-size:14px;cursor:pointer;z-index:1' + close.onclick = () => this._closePopup(icao) + + el.innerHTML = buildPopupHtml(plane) + el.appendChild(close) + document.body.appendChild(el) + this._positionPopup(el, plane) + + el.querySelectorAll('.at-gauge').forEach(g => { + g.addEventListener('click', (e) => { + const prefs = getUnits() + const type = (e.currentTarget as HTMLElement).dataset.gauge! + if (type === 'speed') { + const keys = Object.keys(SPEED_UNITS) + prefs.speed = keys[(keys.indexOf(prefs.speed) + 1) % keys.length] + } else if (type === 'altitude') { + const keys = Object.keys(ALTITUDE_UNITS) + prefs.altitude = keys[(keys.indexOf(prefs.altitude) + 1) % keys.length] + prefs.vertical = prefs.altitude === 'meters' ? 'mps' : 'fps' + } else if (type === 'vertical') { + const keys = Object.keys(VERTICAL_UNITS) + prefs.vertical = keys[(keys.indexOf(prefs.vertical) + 1) % keys.length] + prefs.altitude = prefs.vertical === 'mps' ? 'meters' : 'feet' + } + saveUnits(prefs) + el.innerHTML = buildPopupHtml(plane) + el.appendChild(close) + }) + g.addEventListener('mouseenter', e => (e.currentTarget as HTMLElement).style.borderColor = '#0ff') + g.addEventListener('mouseleave', e => (e.currentTarget as HTMLElement).style.borderColor = '#0f0') + }) + + this.popups[icao] = { el, destroy: () => el.remove() } + this._fetchTrace(plane) + } + + private _positionPopup(el: HTMLDivElement, plane: any) { + const pixel = this.map.getPixelFromCoordinate(fromLonLat([plane.longitude, plane.latitude])) + if (!pixel) return + const rect = (this.map.getTargetElement() as HTMLElement).getBoundingClientRect() + el.style.left = `${rect.left + pixel[0] + 16}px` + el.style.top = `${rect.top + pixel[1] - 16}px` + } + + private _closePopup(icao: string) { + const popup = this.popups[icao] + if (popup) { popup.destroy(); delete this.popups[icao] } + + const segs = this.traceSegs[icao] + const layer = this.traceLayers[icao] + if (segs && layer) segs.forEach(f => layer.getSource()!.removeFeature(f)) + if (layer) { this.map.removeLayer(layer); delete this.traceLayers[icao] } + delete this.traceSegs[icao] + } + + private _refreshPopups() { + for (const icao of Object.keys(this.popups)) { + const plane = this.data.find(p => p.icao === icao) + if (!plane) { this._closePopup(icao); continue } + const { el } = this.popups[icao] + el.innerHTML = buildPopupHtml(plane) + const close = document.createElement('button') + close.innerHTML = '✕' + close.style.cssText = 'position:absolute;top:8px;right:8px;background:none;border:none;color:#888;font-size:14px;cursor:pointer;z-index:1' + close.onclick = () => this._closePopup(icao) + el.appendChild(close) + this._positionPopup(el, plane) + this._fetchTrace(plane) + } + } + + private _attachClick() { + this.clickHandler = (evt) => { + const f = this.map.forEachFeatureAtPixel(evt.pixel, f => f.get('planeData') ? f : null) + if (!f) return + const plane = f.get('planeData') + if (this.popups[plane.icao]) { this._closePopup(plane.icao); return } + this._openPopup(plane) + } + this.map.on('singleclick', this.clickHandler) + } +} diff --git a/client/src/services/api.ts b/client/src/services/api.ts index 4abbcf8..2f1d0ea 100644 --- a/client/src/services/api.ts +++ b/client/src/services/api.ts @@ -1,4 +1,4 @@ -const BASE = import.meta.env.DEV ? 'http://localhost:3000' : '' +const BASE = import.meta.env.DEV ? 'http://10.69.5.23:3000' : '' export type DataRow = Record diff --git a/client/src/views/Dashboard.vue b/client/src/views/Dashboard.vue index c029116..18e1adb 100644 --- a/client/src/views/Dashboard.vue +++ b/client/src/views/Dashboard.vue @@ -14,20 +14,19 @@ const selectedMetric = ref(null) let interval: ReturnType onMounted(async () => { - await fetchAll() - interval = setInterval(fetchAll, 60 * 1000) + await fetchAll() + interval = setInterval(fetchAll, 60 * 1000) }) onUnmounted(() => clearInterval(interval)) -// Group all available metric keys present in current data const groupedMetrics = computed(() => { - return GROUPS.map(group => ({ - group, - keys: Object.entries(METRICS) - .filter(([key, meta]) => meta.group === group && current.value[key] != null) - .map(([key]) => key) - })).filter(g => g.keys.length > 0) + return GROUPS.map(group => ({ + group, + keys: Object.entries(METRICS) + .filter(([key, meta]) => meta.group === group && current.value[key] != null) + .map(([key]) => key) + })).filter(g => g.keys.length > 0) }) @@ -96,50 +95,27 @@ const groupedMetrics = computed(() => { @media (max-width: 768px) { .dashboard { flex-direction: column; - position: relative; - overflow: hidden; + overflow-y: auto; + height: 100dvh; } .map-col { - position: absolute; - inset: 0; - height: 100vh; + width: 100%; + height: 100vw; + min-height: 100vw; padding: 8px; - z-index: 0; + flex-shrink: 0; } .panel-col { - position: absolute; - bottom: 0; - left: 0; - right: 0; - z-index: 10; width: 100%; + flex-shrink: 0; border-left: none; border-top: 1px solid var(--border); - border-radius: 16px 16px 0 0; - - /* Show a peek of the panel, scrolls up over the map */ - max-height: calc(100vh - 120px); - min-height: 40vh; - overflow-y: auto; - - background: var(--bg); - padding: 12px 16px 24px; - - /* Smooth momentum scroll on iOS */ + overflow-y: visible; -webkit-overflow-scrolling: touch; - &::before { - content: ''; - display: block; - width: 36px; - height: 4px; - background: var(--border); - border-radius: 2px; - margin: 0 auto 12px; - } - + &::before { display: none; } &::-webkit-scrollbar { display: none; } } } diff --git a/server/airtraffic.mjs b/server/airtraffic.mjs new file mode 100644 index 0000000..38e62ff --- /dev/null +++ b/server/airtraffic.mjs @@ -0,0 +1,49 @@ +import { cfg } from './config.mjs' +import { writeFile, readFile, existsSync } from 'fs' +import { resolve, dirname } from 'path' +import { fileURLToPath } from 'url' + +const DIR = dirname(fileURLToPath(import.meta.url)) +const SHAPES_PATH = resolve(DIR, 'shapes.json') + +export async function getShapes() { + if (existsSync(SHAPES_PATH)) { + return JSON.parse(await readFile(SHAPES_PATH, 'utf8')) + } + + const r = await fetch('https://raw.githubusercontent.com/wiedehopf/tar1090/master/html/markers.min.js') + const text = await r.text() + + // tar1090 exposes a var with all the shape definitions + let shapes = {} + try { + const match = text.match(/var\s+_aircraft_markers\s*=\s*(\{[\s\S]*?\});/) + if (match) shapes = JSON.parse(match[1]) + } catch (e) { + console.error('Failed to parse tar1090 shapes:', e) + } + + writeFile(SHAPES_PATH, JSON.stringify(shapes), () => {}) + return shapes +} + +export async function getAirTraffic() { + const { ADSB_URL } = cfg() + if (!ADSB_URL) return { data: [] } + const r = await fetch(`${ADSB_URL}/re-api/?all`) + const j = await r.json() + return { data: j.aircraft || [] } +} + +export async function getAirTrafficHistory(icao) { + const { ADSB_URL } = cfg() + if (!ADSB_URL) return { history: [] } + const r = await fetch(`${ADSB_URL}/re-api/?trace&icao=${icao}`) + const j = await r.json() + const history = (j.trace || []).map(t => ({ + latitude: t[1], + longitude: t[2], + altitude: t[3] || 0, + })) + return { history } +} diff --git a/server/config.mjs b/server/config.mjs index f1fcca8..422b656 100644 --- a/server/config.mjs +++ b/server/config.mjs @@ -9,6 +9,7 @@ export function cfg() { config({ path: resolve(ROOT, '.env.local'), override: true }) return { PORT: process.env.PORT || 3000, + ADSB_URL: process.env.ADSB_URL || '', INFLUX_URL: process.env.INFLUX_URL || 'http://localhost:8086', INFLUX_TOKEN: process.env.INFLUX_TOKEN || '', INFLUX_ORG: process.env.INFLUX_ORG || 'weather', diff --git a/server/public/favicon.png b/server/public/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..27b92f0aef729a588c8cb56cf575181f841ade06 GIT binary patch literal 39924 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 OcImt{c+seCAN_yq#F4`Q literal 0 HcmV?d00001 diff --git a/server/server.mjs b/server/server.mjs index f99a790..b380977 100644 --- a/server/server.mjs +++ b/server/server.mjs @@ -9,6 +9,7 @@ import { getOpenMeteo } from './openmeteo.mjs' import { apiReference } from '@scalar/express-api-reference' import { spec } from './spec.mjs' import { existsSync } from 'fs' +import {getAirTraffic, getAirTrafficHistory, getAirTrafficShapes} from './airtraffic.mjs'; const app = express() const DIR = dirname(fileURLToPath(import.meta.url)) @@ -119,6 +120,11 @@ app.get('/api/daily', async (req, res) => { res.json(filterArr(result, fields)) }) +// -- ADSB ---------------------------------------------------------------------- +app.get('/api/air-traffic', async (req, res) => res.json(await getAirTraffic())) +app.get('/api/air-traffic/:icao', async (req, res) => res.json(await getAirTrafficHistory(req.params.icao))) +app.get('/api/air-traffic-shapes', async (req, res) => res.json(await getShapes())) + // ── DOCS ---------------------------------------------------------------------- app.get('/openapi.json', (req, res) => res.json(spec))