feat: add north-up and reset view functionality to flight tracker and status bar; enhance trail synthesis in useTrailHistory
This commit is contained in:
@ -138,6 +138,14 @@ function FlightTrackerInner() {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleNorthUp = useCallback(() => {
|
||||||
|
window.dispatchEvent(new CustomEvent("aeris:north-up"));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleResetView = useCallback(() => {
|
||||||
|
window.dispatchEvent(new CustomEvent("aeris:reset-view"));
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="relative h-screen w-screen overflow-hidden bg-black">
|
<main className="relative h-screen w-screen overflow-hidden bg-black">
|
||||||
<Map mapStyle={mapStyle.style} isDark={mapStyle.dark}>
|
<Map mapStyle={mapStyle.style} isDark={mapStyle.dark}>
|
||||||
@ -182,6 +190,8 @@ function FlightTrackerInner() {
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
rateLimited={rateLimited}
|
rateLimited={rateLimited}
|
||||||
retryIn={retryIn}
|
retryIn={retryIn}
|
||||||
|
onNorthUp={handleNorthUp}
|
||||||
|
onResetView={handleResetView}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,9 @@ import { useSettings } from "@/hooks/use-settings";
|
|||||||
import type { City } from "@/lib/cities";
|
import type { City } from "@/lib/cities";
|
||||||
|
|
||||||
const IDLE_TIMEOUT_MS = 5_000;
|
const IDLE_TIMEOUT_MS = 5_000;
|
||||||
|
const DEFAULT_ZOOM = 9.2;
|
||||||
|
const DEFAULT_PITCH = 49;
|
||||||
|
const DEFAULT_BEARING = 27.4;
|
||||||
|
|
||||||
export function CameraController({ city }: { city: City }) {
|
export function CameraController({ city }: { city: City }) {
|
||||||
const { map, isLoaded } = useMap();
|
const { map, isLoaded } = useMap();
|
||||||
@ -22,14 +25,45 @@ export function CameraController({ city }: { city: City }) {
|
|||||||
prevCityRef.current = city.id;
|
prevCityRef.current = city.id;
|
||||||
map.flyTo({
|
map.flyTo({
|
||||||
center: city.coordinates,
|
center: city.coordinates,
|
||||||
zoom: 9.2,
|
zoom: DEFAULT_ZOOM,
|
||||||
pitch: 49,
|
pitch: DEFAULT_PITCH,
|
||||||
bearing: 27.4,
|
bearing: DEFAULT_BEARING,
|
||||||
duration: 2800,
|
duration: 2800,
|
||||||
essential: true,
|
essential: true,
|
||||||
});
|
});
|
||||||
}, [map, isLoaded, city]);
|
}, [map, isLoaded, city]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!map || !isLoaded || !city) return;
|
||||||
|
|
||||||
|
const onNorthUp = () => {
|
||||||
|
map.easeTo({
|
||||||
|
bearing: 0,
|
||||||
|
duration: 650,
|
||||||
|
essential: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onResetView = () => {
|
||||||
|
map.flyTo({
|
||||||
|
center: city.coordinates,
|
||||||
|
zoom: DEFAULT_ZOOM,
|
||||||
|
pitch: DEFAULT_PITCH,
|
||||||
|
bearing: DEFAULT_BEARING,
|
||||||
|
duration: 1200,
|
||||||
|
essential: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("aeris:north-up", onNorthUp);
|
||||||
|
window.addEventListener("aeris:reset-view", onResetView);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("aeris:north-up", onNorthUp);
|
||||||
|
window.removeEventListener("aeris:reset-view", onResetView);
|
||||||
|
};
|
||||||
|
}, [map, isLoaded, city]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!map || !isLoaded || !city || !settings.autoOrbit) {
|
if (!map || !isLoaded || !city || !settings.autoOrbit) {
|
||||||
if (orbitFrameRef.current) cancelAnimationFrame(orbitFrameRef.current);
|
if (orbitFrameRef.current) cancelAnimationFrame(orbitFrameRef.current);
|
||||||
|
|||||||
@ -11,6 +11,28 @@ import type { PickingInfo } from "@deck.gl/core";
|
|||||||
|
|
||||||
const ANIM_DURATION_MS = 30_000;
|
const ANIM_DURATION_MS = 30_000;
|
||||||
const TELEPORT_THRESHOLD = 0.3; // degrees
|
const TELEPORT_THRESHOLD = 0.3; // degrees
|
||||||
|
const TRAIL_BELOW_AIRCRAFT_METERS = 20;
|
||||||
|
const STARTUP_TRAIL_POLLS = 3;
|
||||||
|
const STARTUP_TRAIL_STEP_SEC = 12;
|
||||||
|
|
||||||
|
function buildStartupFallbackTrail(f: FlightState): [number, number][] {
|
||||||
|
if (f.longitude == null || f.latitude == null) return [];
|
||||||
|
|
||||||
|
const heading = ((f.trueTrack ?? 0) * Math.PI) / 180;
|
||||||
|
const speed = f.velocity ?? 200;
|
||||||
|
const degPerSecond = speed / 111_320;
|
||||||
|
|
||||||
|
const path: [number, number][] = [];
|
||||||
|
for (let i = STARTUP_TRAIL_POLLS; i >= 1; i--) {
|
||||||
|
const distDeg = Math.min(degPerSecond * STARTUP_TRAIL_STEP_SEC * i, 0.08);
|
||||||
|
path.push([
|
||||||
|
f.longitude - Math.sin(heading) * distDeg,
|
||||||
|
f.latitude - Math.cos(heading) * distDeg,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
path.push([f.longitude, f.latitude]);
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
type Snapshot = { lng: number; lat: number; alt: number; track: number };
|
type Snapshot = { lng: number; lat: number; alt: number; track: number };
|
||||||
|
|
||||||
@ -296,18 +318,49 @@ export function FlightLayers({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (showTrailsRef.current) {
|
if (showTrailsRef.current) {
|
||||||
|
const trailMap = new Map(currentTrails.map((t) => [t.icao24, t]));
|
||||||
|
const handledIds = new Set<string>();
|
||||||
|
const trailData: TrailEntry[] = [];
|
||||||
|
|
||||||
|
for (const f of interpolated) {
|
||||||
|
if (f.longitude == null || f.latitude == null) continue;
|
||||||
|
|
||||||
|
const existing = trailMap.get(f.icao24);
|
||||||
|
handledIds.add(f.icao24);
|
||||||
|
|
||||||
|
if (existing && existing.path.length >= 2) {
|
||||||
|
trailData.push(existing);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startupPath = buildStartupFallbackTrail(f);
|
||||||
|
|
||||||
|
trailData.push({
|
||||||
|
icao24: f.icao24,
|
||||||
|
path: startupPath,
|
||||||
|
baroAltitude: existing?.baroAltitude ?? f.baroAltitude,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const d of currentTrails) {
|
||||||
|
if (!handledIds.has(d.icao24)) {
|
||||||
|
trailData.push(d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
layers.push(
|
layers.push(
|
||||||
new PathLayer<TrailEntry>({
|
new PathLayer<TrailEntry>({
|
||||||
id: "flight-trails",
|
id: "flight-trails",
|
||||||
data: currentTrails,
|
data: trailData,
|
||||||
updateTriggers: { getPath: elapsed },
|
updateTriggers: { getPath: elapsed },
|
||||||
getPath: (d) => {
|
getPath: (d) => {
|
||||||
const animFlight = interpolatedMap.get(d.icao24);
|
const animFlight = interpolatedMap.get(d.icao24);
|
||||||
const alt = altitudeToElevation(
|
const alt = altitudeToElevation(
|
||||||
animFlight?.baroAltitude ?? d.baroAltitude,
|
animFlight?.baroAltitude ?? d.baroAltitude,
|
||||||
);
|
);
|
||||||
|
const trailAlt = Math.max(0, alt - TRAIL_BELOW_AIRCRAFT_METERS);
|
||||||
const basePath = d.path.map(
|
const basePath = d.path.map(
|
||||||
(p) => [p[0], p[1], alt] as [number, number, number],
|
(p) => [p[0], p[1], trailAlt] as [number, number, number],
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
animFlight &&
|
animFlight &&
|
||||||
@ -318,20 +371,27 @@ export function FlightLayers({
|
|||||||
const ax = animFlight.longitude;
|
const ax = animFlight.longitude;
|
||||||
const ay = animFlight.latitude;
|
const ay = animFlight.latitude;
|
||||||
|
|
||||||
const heading = ((animFlight.trueTrack ?? 0) * Math.PI) / 180;
|
if (
|
||||||
const fdx = Math.sin(heading);
|
animFlight.trueTrack != null &&
|
||||||
const fdy = Math.cos(heading);
|
basePath.length > STARTUP_TRAIL_POLLS + 3 &&
|
||||||
|
(animFlight.velocity ?? 0) > 40
|
||||||
|
) {
|
||||||
|
const heading = (animFlight.trueTrack * Math.PI) / 180;
|
||||||
|
const fdx = Math.sin(heading);
|
||||||
|
const fdy = Math.cos(heading);
|
||||||
|
|
||||||
for (let i = basePath.length - 1; i >= 0; i--) {
|
for (let i = basePath.length - 1; i >= 0; i--) {
|
||||||
const vx = basePath[i][0] - ax;
|
const vx = basePath[i][0] - ax;
|
||||||
const vy = basePath[i][1] - ay;
|
const vy = basePath[i][1] - ay;
|
||||||
if (vx * fdx + vy * fdy > 0) {
|
if (vx * fdx + vy * fdy > 0) {
|
||||||
basePath[i] = [ax, ay, alt];
|
basePath[i] = [ax, ay, trailAlt];
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
basePath[basePath.length - 1] = [ax, ay, alt];
|
|
||||||
|
basePath[basePath.length - 1] = [ax, ay, trailAlt];
|
||||||
}
|
}
|
||||||
return basePath;
|
return basePath;
|
||||||
},
|
},
|
||||||
@ -346,14 +406,15 @@ export function FlightLayers({
|
|||||||
base[0],
|
base[0],
|
||||||
base[1],
|
base[1],
|
||||||
base[2],
|
base[2],
|
||||||
Math.round(tVal * tVal * 100),
|
Math.round(70 + tVal * 130),
|
||||||
];
|
];
|
||||||
}) as [number, number, number, number][];
|
}) as [number, number, number, number][];
|
||||||
},
|
},
|
||||||
getWidth: 2,
|
getWidth: 3,
|
||||||
widthUnits: "pixels",
|
widthUnits: "pixels",
|
||||||
widthMinPixels: 1,
|
widthMinPixels: 2,
|
||||||
widthMaxPixels: 4,
|
widthMaxPixels: 6,
|
||||||
|
billboard: true,
|
||||||
capRounded: true,
|
capRounded: true,
|
||||||
jointRounded: true,
|
jointRounded: true,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { motion, AnimatePresence } from "motion/react";
|
import { motion, AnimatePresence } from "motion/react";
|
||||||
import { Plane, Radio, ShieldAlert } from "lucide-react";
|
import { Compass, Plane, Radio, ShieldAlert } from "lucide-react";
|
||||||
|
|
||||||
type StatusBarProps = {
|
type StatusBarProps = {
|
||||||
flightCount: number;
|
flightCount: number;
|
||||||
@ -9,6 +9,8 @@ type StatusBarProps = {
|
|||||||
loading: boolean;
|
loading: boolean;
|
||||||
rateLimited?: boolean;
|
rateLimited?: boolean;
|
||||||
retryIn?: number;
|
retryIn?: number;
|
||||||
|
onNorthUp?: () => void;
|
||||||
|
onResetView?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function StatusBar({
|
export function StatusBar({
|
||||||
@ -17,6 +19,8 @@ export function StatusBar({
|
|||||||
loading,
|
loading,
|
||||||
rateLimited = false,
|
rateLimited = false,
|
||||||
retryIn = 0,
|
retryIn = 0,
|
||||||
|
onNorthUp,
|
||||||
|
onResetView,
|
||||||
}: StatusBarProps) {
|
}: StatusBarProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-start gap-2">
|
<div className="flex flex-col items-start gap-2">
|
||||||
@ -46,66 +50,104 @@ export function StatusBar({
|
|||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
<motion.div
|
<div className="flex items-center gap-2">
|
||||||
initial={{ opacity: 0, y: 12 }}
|
<motion.div
|
||||||
animate={{ opacity: 1, y: 0 }}
|
initial={{ opacity: 0, y: 12 }}
|
||||||
transition={{
|
animate={{ opacity: 1, y: 0 }}
|
||||||
type: "spring",
|
transition={{
|
||||||
stiffness: 300,
|
type: "spring",
|
||||||
damping: 24,
|
stiffness: 300,
|
||||||
delay: 0.4,
|
damping: 24,
|
||||||
}}
|
delay: 0.4,
|
||||||
className="flex items-center gap-3 rounded-xl border px-3.5 py-2 backdrop-blur-2xl"
|
}}
|
||||||
style={{
|
className="flex items-center gap-3 rounded-xl border px-3.5 py-2 backdrop-blur-2xl"
|
||||||
borderColor: "rgb(var(--ui-fg) / 0.06)",
|
style={{
|
||||||
backgroundColor: "rgb(var(--ui-bg) / 0.5)",
|
borderColor: "rgb(var(--ui-fg) / 0.06)",
|
||||||
}}
|
backgroundColor: "rgb(var(--ui-bg) / 0.5)",
|
||||||
aria-live="polite"
|
}}
|
||||||
aria-atomic="true"
|
aria-live="polite"
|
||||||
>
|
aria-atomic="true"
|
||||||
<div className="flex items-center gap-2">
|
>
|
||||||
<div className="relative">
|
<div className="flex items-center gap-2">
|
||||||
<Radio
|
<div className="relative">
|
||||||
className={`h-3 w-3 ${rateLimited ? "text-amber-400/80" : "text-emerald-400/80"}`}
|
<Radio
|
||||||
/>
|
className={`h-3 w-3 ${rateLimited ? "text-amber-400/80" : "text-emerald-400/80"}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className="text-[11px] font-medium tracking-wide"
|
||||||
|
style={{ color: "rgb(var(--ui-fg) / 0.4)" }}
|
||||||
|
>
|
||||||
|
{rateLimited ? "Paused" : loading ? "Scanning..." : "Live"}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="h-3 w-px"
|
||||||
|
style={{ backgroundColor: "rgb(var(--ui-fg) / 0.08)" }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<Plane
|
||||||
|
className="h-3 w-3"
|
||||||
|
style={{ color: "rgb(var(--ui-fg) / 0.3)" }}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className="text-[11px] font-semibold tracking-wide"
|
||||||
|
style={{ color: "rgb(var(--ui-fg) / 0.6)" }}
|
||||||
|
>
|
||||||
|
{flightCount}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="h-3 w-px"
|
||||||
|
style={{ backgroundColor: "rgb(var(--ui-fg) / 0.08)" }}
|
||||||
|
/>
|
||||||
<span
|
<span
|
||||||
className="text-[11px] font-medium tracking-wide"
|
className="text-[11px] font-medium tracking-wide"
|
||||||
style={{ color: "rgb(var(--ui-fg) / 0.4)" }}
|
style={{ color: "rgb(var(--ui-fg) / 0.4)" }}
|
||||||
|
title={cityName}
|
||||||
>
|
>
|
||||||
{rateLimited ? "Paused" : loading ? "Scanning..." : "Live"}
|
{cityName}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</motion.div>
|
||||||
|
|
||||||
<div
|
<motion.div
|
||||||
className="h-3 w-px"
|
initial={{ opacity: 0, y: 12 }}
|
||||||
style={{ backgroundColor: "rgb(var(--ui-fg) / 0.08)" }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
/>
|
transition={{
|
||||||
|
type: "spring",
|
||||||
<div className="flex items-center gap-1.5">
|
stiffness: 300,
|
||||||
<Plane
|
damping: 24,
|
||||||
className="h-3 w-3"
|
delay: 0.48,
|
||||||
style={{ color: "rgb(var(--ui-fg) / 0.3)" }}
|
}}
|
||||||
/>
|
className="flex items-center gap-1 rounded-xl border px-1.5 py-1.5 backdrop-blur-2xl"
|
||||||
<span
|
style={{
|
||||||
className="text-[11px] font-semibold tracking-wide"
|
borderColor: "rgb(var(--ui-fg) / 0.06)",
|
||||||
style={{ color: "rgb(var(--ui-fg) / 0.6)" }}
|
backgroundColor: "rgb(var(--ui-bg) / 0.5)",
|
||||||
>
|
}}
|
||||||
{flightCount}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="h-3 w-px"
|
|
||||||
style={{ backgroundColor: "rgb(var(--ui-fg) / 0.08)" }}
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
className="text-[11px] font-medium tracking-wide"
|
|
||||||
style={{ color: "rgb(var(--ui-fg) / 0.4)" }}
|
|
||||||
>
|
>
|
||||||
{cityName}
|
<button
|
||||||
</span>
|
type="button"
|
||||||
</motion.div>
|
onClick={onNorthUp}
|
||||||
|
aria-label="North up"
|
||||||
|
title="North up"
|
||||||
|
className="rounded-lg px-2.5 py-1 text-[11px] font-medium tracking-wide transition-colors"
|
||||||
|
style={{ color: "rgb(var(--ui-fg) / 0.55)" }}
|
||||||
|
>
|
||||||
|
<Compass className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onResetView}
|
||||||
|
className="rounded-lg px-2.5 py-1 text-[11px] font-medium tracking-wide transition-colors"
|
||||||
|
style={{ color: "rgb(var(--ui-fg) / 0.55)" }}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,9 +12,11 @@ export type TrailEntry = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const MAX_POINTS = 40;
|
const MAX_POINTS = 40;
|
||||||
const SYNTHETIC_COUNT = 12;
|
|
||||||
const JUMP_THRESHOLD_DEG = 0.3;
|
const JUMP_THRESHOLD_DEG = 0.3;
|
||||||
export const SAMPLES_PER_SEGMENT = 8;
|
export const SAMPLES_PER_SEGMENT = 8;
|
||||||
|
const HISTORICAL_BOOTSTRAP_POLLS = 3;
|
||||||
|
const HISTORICAL_BOOTSTRAP_STEP_SEC = 12;
|
||||||
|
const BOOTSTRAP_UPDATES = 3;
|
||||||
|
|
||||||
// Centripetal Catmull-Rom spline (Barry-Goldman algorithm, α = 0.5).
|
// Centripetal Catmull-Rom spline (Barry-Goldman algorithm, α = 0.5).
|
||||||
// Produces smooth C1 curves that pass through every control point.
|
// Produces smooth C1 curves that pass through every control point.
|
||||||
@ -72,30 +74,39 @@ function catmullRomSmooth(
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function synthesizeTail(f: FlightState): Position[] {
|
function synthesizeHistoricalPolls(f: FlightState): Position[] {
|
||||||
const lng = f.longitude!;
|
if (f.longitude == null || f.latitude == null) return [];
|
||||||
const lat = f.latitude!;
|
const lng = f.longitude;
|
||||||
|
const lat = f.latitude;
|
||||||
const heading = ((f.trueTrack ?? 0) * Math.PI) / 180;
|
const heading = ((f.trueTrack ?? 0) * Math.PI) / 180;
|
||||||
const speed = f.velocity ?? 200;
|
const speed = f.velocity ?? 200;
|
||||||
const step = Math.min((speed * 10) / 111_320, 0.02);
|
const degPerSecond = speed / 111_320;
|
||||||
|
|
||||||
const pts: Position[] = [];
|
const polls: Position[] = [];
|
||||||
for (let i = SYNTHETIC_COUNT; i >= 1; i--) {
|
for (let i = HISTORICAL_BOOTSTRAP_POLLS; i >= 1; i--) {
|
||||||
const d = step * i;
|
const tSec = HISTORICAL_BOOTSTRAP_STEP_SEC * i;
|
||||||
pts.push([lng - Math.sin(heading) * d, lat - Math.cos(heading) * d]);
|
const decay = 1 - (HISTORICAL_BOOTSTRAP_POLLS - i) * 0.08;
|
||||||
|
const distanceDeg = Math.min(degPerSecond * tSec * decay, 0.06);
|
||||||
|
polls.push([
|
||||||
|
lng - Math.sin(heading) * distanceDeg,
|
||||||
|
lat - Math.cos(heading) * distanceDeg,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
return pts;
|
return polls;
|
||||||
}
|
}
|
||||||
|
|
||||||
class TrailStore {
|
class TrailStore {
|
||||||
private trails = new Map<string, Position[]>();
|
private trails = new Map<string, Position[]>();
|
||||||
private seen = new Set<string>();
|
private seen = new Set<string>();
|
||||||
|
private bootstrapUpdatesRemaining = BOOTSTRAP_UPDATES;
|
||||||
|
|
||||||
update(flights: FlightState[]): TrailEntry[] {
|
update(flights: FlightState[]): TrailEntry[] {
|
||||||
const current = new Set<string>();
|
const current = new Set<string>();
|
||||||
|
let processedFlightCount = 0;
|
||||||
|
|
||||||
for (const f of flights) {
|
for (const f of flights) {
|
||||||
if (f.longitude === null || f.latitude === null) continue;
|
if (f.longitude == null || f.latitude == null) continue;
|
||||||
|
processedFlightCount += 1;
|
||||||
const id = f.icao24;
|
const id = f.icao24;
|
||||||
current.add(id);
|
current.add(id);
|
||||||
|
|
||||||
@ -103,10 +114,16 @@ class TrailStore {
|
|||||||
let trail = this.trails.get(id);
|
let trail = this.trails.get(id);
|
||||||
|
|
||||||
if (!trail) {
|
if (!trail) {
|
||||||
trail = synthesizeTail(f);
|
trail =
|
||||||
|
this.bootstrapUpdatesRemaining > 0 ? synthesizeHistoricalPolls(f) : [];
|
||||||
this.trails.set(id, trail);
|
this.trails.set(id, trail);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (trail.length === 0) {
|
||||||
|
trail.push(pos);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const last = trail[trail.length - 1];
|
const last = trail[trail.length - 1];
|
||||||
const dx = pos[0] - last[0];
|
const dx = pos[0] - last[0];
|
||||||
const dy = pos[1] - last[1];
|
const dy = pos[1] - last[1];
|
||||||
@ -125,13 +142,17 @@ class TrailStore {
|
|||||||
}
|
}
|
||||||
this.seen = current;
|
this.seen = current;
|
||||||
|
|
||||||
|
if (this.bootstrapUpdatesRemaining > 0 && processedFlightCount > 0) {
|
||||||
|
this.bootstrapUpdatesRemaining -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
const result: TrailEntry[] = [];
|
const result: TrailEntry[] = [];
|
||||||
for (const f of flights) {
|
for (const f of flights) {
|
||||||
const trail = this.trails.get(f.icao24);
|
const trail = this.trails.get(f.icao24);
|
||||||
if (trail && trail.length >= 2) {
|
if (trail && trail.length >= 2) {
|
||||||
result.push({
|
result.push({
|
||||||
icao24: f.icao24,
|
icao24: f.icao24,
|
||||||
path: trail.length >= 3 ? catmullRomSmooth(trail) : [...trail],
|
path: trail.length >= 5 ? catmullRomSmooth(trail) : [...trail],
|
||||||
baroAltitude: f.baroAltitude,
|
baroAltitude: f.baroAltitude,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user