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";
|
||||
|
||||
import { useState, useCallback, useSyncExternalStore } from "react";
|
||||
import { useState, useCallback, useEffect, useSyncExternalStore } from "react";
|
||||
import { ErrorBoundary } from "@/components/error-boundary";
|
||||
import { Map } from "@/components/map/map";
|
||||
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 type { FlightState } from "@/lib/opensky";
|
||||
import type { PickingInfo } from "@deck.gl/core";
|
||||
import { Github, Star } from "lucide-react";
|
||||
|
||||
const DEFAULT_CITY_ID = "mia";
|
||||
const STYLE_STORAGE_KEY = "aeris:mapStyle";
|
||||
|
||||
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 = () => () => {};
|
||||
|
||||
@ -121,6 +124,29 @@ function FlightTrackerInner() {
|
||||
const trails = useTrailHistory(flights);
|
||||
const [hoveredFlight, setHoveredFlight] = useState<FlightState | null>(null);
|
||||
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) => {
|
||||
if (info?.object) {
|
||||
@ -143,8 +169,12 @@ function FlightTrackerInner() {
|
||||
}, []);
|
||||
|
||||
const handleResetView = useCallback(() => {
|
||||
window.dispatchEvent(new CustomEvent("aeris:reset-view"));
|
||||
}, []);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("aeris:reset-view", {
|
||||
detail: { center: activeCity.coordinates },
|
||||
}),
|
||||
);
|
||||
}, [activeCity.coordinates]);
|
||||
|
||||
return (
|
||||
<main className="relative h-screen w-screen overflow-hidden bg-black">
|
||||
@ -175,6 +205,41 @@ function FlightTrackerInner() {
|
||||
</div>
|
||||
|
||||
<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
|
||||
activeCity={activeCity}
|
||||
onSelectCity={setActiveCity}
|
||||
@ -226,3 +291,9 @@ function Brand({ isDark }: { isDark: boolean }) {
|
||||
</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({
|
||||
center: city.coordinates,
|
||||
center,
|
||||
zoom: DEFAULT_ZOOM,
|
||||
pitch: DEFAULT_PITCH,
|
||||
bearing: DEFAULT_BEARING,
|
||||
|
||||
@ -14,6 +14,7 @@ const TELEPORT_THRESHOLD = 0.3; // degrees
|
||||
const TRAIL_BELOW_AIRCRAFT_METERS = 20;
|
||||
const STARTUP_TRAIL_POLLS = 3;
|
||||
const STARTUP_TRAIL_STEP_SEC = 12;
|
||||
const TRACK_DAMPING = 0.18;
|
||||
|
||||
function buildStartupFallbackTrail(f: FlightState): [number, number][] {
|
||||
if (f.longitude == null || f.latitude == null) return [];
|
||||
@ -164,11 +165,16 @@ export function FlightLayers({
|
||||
const next = new Map<string, Snapshot>();
|
||||
for (const f of flights) {
|
||||
if (f.longitude != null && f.latitude != null) {
|
||||
const prev = newPrev.get(f.icao24);
|
||||
const rawTrack = f.trueTrack ?? 0;
|
||||
next.set(f.icao24, {
|
||||
lng: f.longitude,
|
||||
lat: f.latitude,
|
||||
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 rawT = elapsed / ANIM_DURATION_MS;
|
||||
const tPos = Math.min(rawT, 1);
|
||||
const tAngle = smoothStep(tPos);
|
||||
const tAngle = smoothStep(smoothStep(smoothStep(tPos)));
|
||||
|
||||
const currentFlights = flightsRef.current;
|
||||
const currentTrails = trailsRef.current;
|
||||
@ -402,11 +408,12 @@ export function FlightLayers({
|
||||
: defaultColor;
|
||||
return Array.from({ length: len }, (_, i) => {
|
||||
const tVal = len > 1 ? i / (len - 1) : 1;
|
||||
const fade = Math.pow(tVal, 2.4);
|
||||
return [
|
||||
base[0],
|
||||
base[1],
|
||||
base[2],
|
||||
Math.round(70 + tVal * 130),
|
||||
Math.min(255, base[0] + 22),
|
||||
Math.min(255, base[1] + 22),
|
||||
Math.min(255, base[2] + 22),
|
||||
Math.round(20 + fade * 200),
|
||||
];
|
||||
}) as [number, number, number, number][];
|
||||
},
|
||||
|
||||
@ -13,7 +13,7 @@ export type TrailEntry = {
|
||||
|
||||
const MAX_POINTS = 40;
|
||||
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_STEP_SEC = 12;
|
||||
const BOOTSTRAP_UPDATES = 3;
|
||||
|
||||
Reference in New Issue
Block a user