feat: enhance flight tracker with GitHub stars display and improve reset view functionality; update trail history sampling rate
This commit is contained in:
@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useCallback, useSyncExternalStore } from "react";
|
import { useState, useCallback, useEffect, useSyncExternalStore } from "react";
|
||||||
import { ErrorBoundary } from "@/components/error-boundary";
|
import { ErrorBoundary } from "@/components/error-boundary";
|
||||||
import { Map } from "@/components/map/map";
|
import { Map } from "@/components/map/map";
|
||||||
import { CameraController } from "@/components/map/camera-controller";
|
import { CameraController } from "@/components/map/camera-controller";
|
||||||
@ -18,11 +18,14 @@ import { CITIES, type City } from "@/lib/cities";
|
|||||||
import { findByIata, airportToCity } from "@/lib/airports";
|
import { findByIata, airportToCity } from "@/lib/airports";
|
||||||
import type { FlightState } from "@/lib/opensky";
|
import type { FlightState } from "@/lib/opensky";
|
||||||
import type { PickingInfo } from "@deck.gl/core";
|
import type { PickingInfo } from "@deck.gl/core";
|
||||||
|
import { Github, Star } from "lucide-react";
|
||||||
|
|
||||||
const DEFAULT_CITY_ID = "mia";
|
const DEFAULT_CITY_ID = "mia";
|
||||||
const STYLE_STORAGE_KEY = "aeris:mapStyle";
|
const STYLE_STORAGE_KEY = "aeris:mapStyle";
|
||||||
|
|
||||||
const DEFAULT_CITY = CITIES.find((c) => c.id === DEFAULT_CITY_ID) ?? CITIES[0];
|
const DEFAULT_CITY = CITIES.find((c) => c.id === DEFAULT_CITY_ID) ?? CITIES[0];
|
||||||
|
const GITHUB_REPO_URL = "https://github.com/kewonit/aeris";
|
||||||
|
const GITHUB_REPO_API = "https://api.github.com/repos/kewonit/aeris";
|
||||||
|
|
||||||
const subscribeNoop = () => () => {};
|
const subscribeNoop = () => () => {};
|
||||||
|
|
||||||
@ -121,6 +124,29 @@ function FlightTrackerInner() {
|
|||||||
const trails = useTrailHistory(flights);
|
const trails = useTrailHistory(flights);
|
||||||
const [hoveredFlight, setHoveredFlight] = useState<FlightState | null>(null);
|
const [hoveredFlight, setHoveredFlight] = useState<FlightState | null>(null);
|
||||||
const [cursorPos, setCursorPos] = useState({ x: 0, y: 0 });
|
const [cursorPos, setCursorPos] = useState({ x: 0, y: 0 });
|
||||||
|
const [repoStars, setRepoStars] = useState<number | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let mounted = true;
|
||||||
|
|
||||||
|
async function loadRepoStars() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(GITHUB_REPO_API, { cache: "no-store" });
|
||||||
|
if (!res.ok) return;
|
||||||
|
const data = (await res.json()) as { stargazers_count?: number };
|
||||||
|
if (mounted && typeof data.stargazers_count === "number") {
|
||||||
|
setRepoStars(data.stargazers_count);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* silent fallback */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadRepoStars();
|
||||||
|
return () => {
|
||||||
|
mounted = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleHover = useCallback((info: PickingInfo<FlightState> | null) => {
|
const handleHover = useCallback((info: PickingInfo<FlightState> | null) => {
|
||||||
if (info?.object) {
|
if (info?.object) {
|
||||||
@ -143,8 +169,12 @@ function FlightTrackerInner() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleResetView = useCallback(() => {
|
const handleResetView = useCallback(() => {
|
||||||
window.dispatchEvent(new CustomEvent("aeris:reset-view"));
|
window.dispatchEvent(
|
||||||
}, []);
|
new CustomEvent("aeris:reset-view", {
|
||||||
|
detail: { center: activeCity.coordinates },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}, [activeCity.coordinates]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="relative h-screen w-screen overflow-hidden bg-black">
|
<main className="relative h-screen w-screen overflow-hidden bg-black">
|
||||||
@ -175,6 +205,41 @@ function FlightTrackerInner() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pointer-events-auto absolute right-4 top-4 flex items-center gap-2">
|
<div className="pointer-events-auto absolute right-4 top-4 flex items-center gap-2">
|
||||||
|
<a
|
||||||
|
href={GITHUB_REPO_URL}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
aria-label="Open GitHub repository"
|
||||||
|
className="relative inline-flex h-9 w-9 items-center justify-center rounded-xl backdrop-blur-2xl transition-colors"
|
||||||
|
style={{
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "rgb(var(--ui-fg) / 0.06)",
|
||||||
|
backgroundColor: "rgb(var(--ui-fg) / 0.03)",
|
||||||
|
color: "rgb(var(--ui-fg) / 0.5)",
|
||||||
|
}}
|
||||||
|
title={
|
||||||
|
repoStars != null
|
||||||
|
? `GitHub · ${formatStarCount(repoStars)} stars`
|
||||||
|
: "Open GitHub repository"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Github className="h-4 w-4" />
|
||||||
|
{repoStars != null && (
|
||||||
|
<span
|
||||||
|
className="pointer-events-none absolute -bottom-1 -right-1 rounded-full px-1.5 py-0.5 text-[9px] font-semibold tabular-nums"
|
||||||
|
style={{
|
||||||
|
backgroundColor: "rgb(var(--ui-bg) / 0.95)",
|
||||||
|
border: "1px solid rgb(var(--ui-fg) / 0.1)",
|
||||||
|
color: "rgb(var(--ui-fg) / 0.55)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-0.5">
|
||||||
|
<Star className="h-2 w-2" />
|
||||||
|
{formatStarCount(repoStars)}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</a>
|
||||||
<ControlPanel
|
<ControlPanel
|
||||||
activeCity={activeCity}
|
activeCity={activeCity}
|
||||||
onSelectCity={setActiveCity}
|
onSelectCity={setActiveCity}
|
||||||
@ -226,3 +291,9 @@ function Brand({ isDark }: { isDark: boolean }) {
|
|||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatStarCount(value: number): string {
|
||||||
|
if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}m`;
|
||||||
|
if (value >= 1_000) return `${(value / 1_000).toFixed(1)}k`;
|
||||||
|
return `${value}`;
|
||||||
|
}
|
||||||
|
|||||||
@ -44,9 +44,11 @@ export function CameraController({ city }: { city: City }) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onResetView = () => {
|
const onResetView = (event: Event) => {
|
||||||
|
const customEvent = event as CustomEvent<{ center?: [number, number] }>;
|
||||||
|
const center = customEvent.detail?.center ?? city.coordinates;
|
||||||
map.flyTo({
|
map.flyTo({
|
||||||
center: city.coordinates,
|
center,
|
||||||
zoom: DEFAULT_ZOOM,
|
zoom: DEFAULT_ZOOM,
|
||||||
pitch: DEFAULT_PITCH,
|
pitch: DEFAULT_PITCH,
|
||||||
bearing: DEFAULT_BEARING,
|
bearing: DEFAULT_BEARING,
|
||||||
|
|||||||
@ -14,6 +14,7 @@ const TELEPORT_THRESHOLD = 0.3; // degrees
|
|||||||
const TRAIL_BELOW_AIRCRAFT_METERS = 20;
|
const TRAIL_BELOW_AIRCRAFT_METERS = 20;
|
||||||
const STARTUP_TRAIL_POLLS = 3;
|
const STARTUP_TRAIL_POLLS = 3;
|
||||||
const STARTUP_TRAIL_STEP_SEC = 12;
|
const STARTUP_TRAIL_STEP_SEC = 12;
|
||||||
|
const TRACK_DAMPING = 0.18;
|
||||||
|
|
||||||
function buildStartupFallbackTrail(f: FlightState): [number, number][] {
|
function buildStartupFallbackTrail(f: FlightState): [number, number][] {
|
||||||
if (f.longitude == null || f.latitude == null) return [];
|
if (f.longitude == null || f.latitude == null) return [];
|
||||||
@ -164,11 +165,16 @@ export function FlightLayers({
|
|||||||
const next = new Map<string, Snapshot>();
|
const next = new Map<string, Snapshot>();
|
||||||
for (const f of flights) {
|
for (const f of flights) {
|
||||||
if (f.longitude != null && f.latitude != null) {
|
if (f.longitude != null && f.latitude != null) {
|
||||||
|
const prev = newPrev.get(f.icao24);
|
||||||
|
const rawTrack = f.trueTrack ?? 0;
|
||||||
next.set(f.icao24, {
|
next.set(f.icao24, {
|
||||||
lng: f.longitude,
|
lng: f.longitude,
|
||||||
lat: f.latitude,
|
lat: f.latitude,
|
||||||
alt: f.baroAltitude ?? 0,
|
alt: f.baroAltitude ?? 0,
|
||||||
track: f.trueTrack ?? 0,
|
track:
|
||||||
|
prev != null
|
||||||
|
? lerpAngle(prev.track, rawTrack, TRACK_DAMPING)
|
||||||
|
: rawTrack,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -228,7 +234,7 @@ export function FlightLayers({
|
|||||||
const elapsed = performance.now() - dataTimestampRef.current;
|
const elapsed = performance.now() - dataTimestampRef.current;
|
||||||
const rawT = elapsed / ANIM_DURATION_MS;
|
const rawT = elapsed / ANIM_DURATION_MS;
|
||||||
const tPos = Math.min(rawT, 1);
|
const tPos = Math.min(rawT, 1);
|
||||||
const tAngle = smoothStep(tPos);
|
const tAngle = smoothStep(smoothStep(smoothStep(tPos)));
|
||||||
|
|
||||||
const currentFlights = flightsRef.current;
|
const currentFlights = flightsRef.current;
|
||||||
const currentTrails = trailsRef.current;
|
const currentTrails = trailsRef.current;
|
||||||
@ -402,11 +408,12 @@ export function FlightLayers({
|
|||||||
: defaultColor;
|
: defaultColor;
|
||||||
return Array.from({ length: len }, (_, i) => {
|
return Array.from({ length: len }, (_, i) => {
|
||||||
const tVal = len > 1 ? i / (len - 1) : 1;
|
const tVal = len > 1 ? i / (len - 1) : 1;
|
||||||
|
const fade = Math.pow(tVal, 2.4);
|
||||||
return [
|
return [
|
||||||
base[0],
|
Math.min(255, base[0] + 22),
|
||||||
base[1],
|
Math.min(255, base[1] + 22),
|
||||||
base[2],
|
Math.min(255, base[2] + 22),
|
||||||
Math.round(70 + tVal * 130),
|
Math.round(20 + fade * 200),
|
||||||
];
|
];
|
||||||
}) as [number, number, number, number][];
|
}) as [number, number, number, number][];
|
||||||
},
|
},
|
||||||
|
|||||||
@ -13,7 +13,7 @@ export type TrailEntry = {
|
|||||||
|
|
||||||
const MAX_POINTS = 40;
|
const MAX_POINTS = 40;
|
||||||
const JUMP_THRESHOLD_DEG = 0.3;
|
const JUMP_THRESHOLD_DEG = 0.3;
|
||||||
export const SAMPLES_PER_SEGMENT = 8;
|
export const SAMPLES_PER_SEGMENT = 16;
|
||||||
const HISTORICAL_BOOTSTRAP_POLLS = 3;
|
const HISTORICAL_BOOTSTRAP_POLLS = 3;
|
||||||
const HISTORICAL_BOOTSTRAP_STEP_SEC = 12;
|
const HISTORICAL_BOOTSTRAP_STEP_SEC = 12;
|
||||||
const BOOTSTRAP_UPDATES = 3;
|
const BOOTSTRAP_UPDATES = 3;
|
||||||
|
|||||||
Reference in New Issue
Block a user