ADS-B map: altitude-colored aircraft icons + click-to-show flight trail
Icons were 16px accent-colored glyphs, indistinguishable from OSM's own
airport symbols. Now a 30px outlined airliner silhouette filled on
tar1090/ADS-B Exchange's altitude hue ramp, with a callsign/altitude
hover tooltip; the selected aircraft grows and gets a white outline.
Clicking an aircraft draws the path heard so far, segment-colored by
altitude. c2-core writes one point per position change to
aircraft/{icao}/positions (deduped in-process, writes now concurrent);
points carry expire_at and a TTL fieldOverride deletes them after ~24h.
Trail reads are gated on the parent aircraft doc's org via get(), so the
query needs no org filter or composite index. The latest stretch without
a 20-min gap counts as the current flight.
Verified: c2-core pytest 479 passed; frontend tsc --noEmit clean (node:20
container on radio-box).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
9b83f0ec6d
commit
bff69a1d04
@@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { collection, onSnapshot, orderBy, query, where, FirestoreError } from "firebase/firestore";
|
||||
import { db } from "@/lib/firebase";
|
||||
import type { AircraftTrailPoint } from "@/lib/types";
|
||||
|
||||
// Trail points live at aircraft/{icao}/positions (written by c2-core
|
||||
// telemetry.py on every position change, TTL-deleted after ~24h). The same
|
||||
// icao can fly several legs a day, so only the latest continuous stretch is
|
||||
// "this flight": a gap longer than FLIGHT_GAP_MS starts a new one.
|
||||
const LOOKBACK_MS = 6 * 60 * 60 * 1000;
|
||||
const FLIGHT_GAP_MS = 20 * 60 * 1000;
|
||||
|
||||
function currentFlight(points: AircraftTrailPoint[]): AircraftTrailPoint[] {
|
||||
let start = 0;
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
if (new Date(points[i].t).getTime() - new Date(points[i - 1].t).getTime() > FLIGHT_GAP_MS) start = i;
|
||||
}
|
||||
return points.slice(start);
|
||||
}
|
||||
|
||||
/** Live flight path for one aircraft; pass null to subscribe to nothing. */
|
||||
export function useAircraftTrail(icao: string | null) {
|
||||
const [trail, setTrail] = useState<AircraftTrailPoint[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setTrail([]);
|
||||
if (!icao) return;
|
||||
// `t` is Python's isoformat() in UTC ("...T17:06:48.755123+00:00"), so it
|
||||
// sorts and range-filters correctly as a string against toISOString()'s
|
||||
// "...T17:06:48.755Z" down to the second — no composite index needed.
|
||||
const since = new Date(Date.now() - LOOKBACK_MS).toISOString();
|
||||
const q = query(collection(db, "aircraft", icao, "positions"), where("t", ">=", since), orderBy("t"));
|
||||
return onSnapshot(
|
||||
q,
|
||||
(snap) => setTrail(currentFlight(snap.docs.map((d) => d.data() as AircraftTrailPoint))),
|
||||
(err: FirestoreError) => console.error("useAircraftTrail:", err),
|
||||
);
|
||||
}, [icao]);
|
||||
|
||||
return trail;
|
||||
}
|
||||
Reference in New Issue
Block a user