feat: add flight tracking components and hooks
- Introduced FlightCard component for displaying flight information with animations. - Added ScrollArea component for custom scroll behavior. - Implemented StatusBar component to show flight count and loading status. - Created useFlights hook for fetching and managing flight data based on city selection. - Developed useSettings hook for managing user settings with local storage persistence. - Added useTrailHistory hook for managing flight trail data. - Defined City type and CITIES constant for city data management. - Implemented flight utility functions for altitude and speed conversions. - Created map styles for different visual representations. - Added OpenSky API integration for fetching flight data. - Implemented utility functions for class name merging. - Configured TypeScript settings for the project.
This commit is contained in:
148
src/hooks/use-trail-history.ts
Normal file
148
src/hooks/use-trail-history.ts
Normal file
@ -0,0 +1,148 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo } from "react";
|
||||
import type { FlightState } from "@/lib/opensky";
|
||||
|
||||
type Position = [lng: number, lat: number];
|
||||
|
||||
export type TrailEntry = {
|
||||
icao24: string;
|
||||
path: Position[];
|
||||
baroAltitude: number | null;
|
||||
};
|
||||
|
||||
const MAX_POINTS = 40;
|
||||
const SYNTHETIC_COUNT = 12;
|
||||
const JUMP_THRESHOLD_DEG = 0.3;
|
||||
export const SAMPLES_PER_SEGMENT = 8;
|
||||
|
||||
// Centripetal Catmull-Rom spline (Barry-Goldman algorithm, α = 0.5).
|
||||
// Produces smooth C1 curves that pass through every control point.
|
||||
function catmullRomSmooth(
|
||||
points: Position[],
|
||||
samplesPerSegment: number = SAMPLES_PER_SEGMENT,
|
||||
): Position[] {
|
||||
if (points.length < 3) return [...points];
|
||||
|
||||
const result: Position[] = [points[0]];
|
||||
|
||||
for (let i = 0; i < points.length - 1; i++) {
|
||||
const p0 = points[Math.max(0, i - 1)];
|
||||
const p1 = points[i];
|
||||
const p2 = points[i + 1];
|
||||
const p3 = points[Math.min(points.length - 1, i + 2)];
|
||||
|
||||
// Knot intervals
|
||||
const d01 = Math.pow(Math.hypot(p1[0] - p0[0], p1[1] - p0[1]), 0.5) || 1e-6;
|
||||
const d12 = Math.pow(Math.hypot(p2[0] - p1[0], p2[1] - p1[1]), 0.5) || 1e-6;
|
||||
const d23 = Math.pow(Math.hypot(p3[0] - p2[0], p3[1] - p2[1]), 0.5) || 1e-6;
|
||||
|
||||
const t0 = 0;
|
||||
const t1 = d01;
|
||||
const t2 = t1 + d12;
|
||||
const t3 = t2 + d23;
|
||||
|
||||
for (let s = 1; s <= samplesPerSegment; s++) {
|
||||
const t = t1 + (t2 - t1) * (s / samplesPerSegment);
|
||||
|
||||
// Barry-Goldman interpolation
|
||||
const a1x =
|
||||
((t1 - t) / (t1 - t0)) * p0[0] + ((t - t0) / (t1 - t0)) * p1[0];
|
||||
const a1y =
|
||||
((t1 - t) / (t1 - t0)) * p0[1] + ((t - t0) / (t1 - t0)) * p1[1];
|
||||
const a2x =
|
||||
((t2 - t) / (t2 - t1)) * p1[0] + ((t - t1) / (t2 - t1)) * p2[0];
|
||||
const a2y =
|
||||
((t2 - t) / (t2 - t1)) * p1[1] + ((t - t1) / (t2 - t1)) * p2[1];
|
||||
const a3x =
|
||||
((t3 - t) / (t3 - t2)) * p2[0] + ((t - t2) / (t3 - t2)) * p3[0];
|
||||
const a3y =
|
||||
((t3 - t) / (t3 - t2)) * p2[1] + ((t - t2) / (t3 - t2)) * p3[1];
|
||||
|
||||
const b1x = ((t2 - t) / (t2 - t0)) * a1x + ((t - t0) / (t2 - t0)) * a2x;
|
||||
const b1y = ((t2 - t) / (t2 - t0)) * a1y + ((t - t0) / (t2 - t0)) * a2y;
|
||||
const b2x = ((t3 - t) / (t3 - t1)) * a2x + ((t - t1) / (t3 - t1)) * a3x;
|
||||
const b2y = ((t3 - t) / (t3 - t1)) * a2y + ((t - t1) / (t3 - t1)) * a3y;
|
||||
|
||||
const cx = ((t2 - t) / (t2 - t1)) * b1x + ((t - t1) / (t2 - t1)) * b2x;
|
||||
const cy = ((t2 - t) / (t2 - t1)) * b1y + ((t - t1) / (t2 - t1)) * b2y;
|
||||
|
||||
result.push([cx, cy]);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function synthesizeTail(f: FlightState): Position[] {
|
||||
const lng = f.longitude!;
|
||||
const lat = f.latitude!;
|
||||
const heading = ((f.trueTrack ?? 0) * Math.PI) / 180;
|
||||
const speed = f.velocity ?? 200;
|
||||
const step = Math.min((speed * 10) / 111_320, 0.02);
|
||||
|
||||
const pts: Position[] = [];
|
||||
for (let i = SYNTHETIC_COUNT; i >= 1; i--) {
|
||||
const d = step * i;
|
||||
pts.push([lng - Math.sin(heading) * d, lat - Math.cos(heading) * d]);
|
||||
}
|
||||
return pts;
|
||||
}
|
||||
|
||||
class TrailStore {
|
||||
private trails = new Map<string, Position[]>();
|
||||
private seen = new Set<string>();
|
||||
|
||||
update(flights: FlightState[]): TrailEntry[] {
|
||||
const current = new Set<string>();
|
||||
|
||||
for (const f of flights) {
|
||||
if (f.longitude === null || f.latitude === null) continue;
|
||||
const id = f.icao24;
|
||||
current.add(id);
|
||||
|
||||
const pos: Position = [f.longitude, f.latitude];
|
||||
let trail = this.trails.get(id);
|
||||
|
||||
if (!trail) {
|
||||
trail = synthesizeTail(f);
|
||||
this.trails.set(id, trail);
|
||||
}
|
||||
|
||||
const last = trail[trail.length - 1];
|
||||
const dx = pos[0] - last[0];
|
||||
const dy = pos[1] - last[1];
|
||||
if (dx * dx + dy * dy > JUMP_THRESHOLD_DEG * JUMP_THRESHOLD_DEG) {
|
||||
trail.length = 0;
|
||||
}
|
||||
|
||||
trail.push(pos);
|
||||
if (trail.length > MAX_POINTS) {
|
||||
trail.splice(0, trail.length - MAX_POINTS);
|
||||
}
|
||||
}
|
||||
|
||||
for (const id of this.seen) {
|
||||
if (!current.has(id)) this.trails.delete(id);
|
||||
}
|
||||
this.seen = current;
|
||||
|
||||
const result: TrailEntry[] = [];
|
||||
for (const f of flights) {
|
||||
const trail = this.trails.get(f.icao24);
|
||||
if (trail && trail.length >= 2) {
|
||||
result.push({
|
||||
icao24: f.icao24,
|
||||
path: trail.length >= 3 ? catmullRomSmooth(trail) : [...trail],
|
||||
baroAltitude: f.baroAltitude,
|
||||
});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export function useTrailHistory(flights: FlightState[]): TrailEntry[] {
|
||||
const [store] = useState(() => new TrailStore());
|
||||
return useMemo(() => store.update(flights), [store, flights]);
|
||||
}
|
||||
Reference in New Issue
Block a user