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>
44 lines
1.8 KiB
TypeScript
44 lines
1.8 KiB
TypeScript
"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;
|
|
}
|