feat: add caching headers for models, update viewport meta tag, and enhance error handling; improve flight tracker layout and status bar styling
This commit is contained in:
@ -34,6 +34,15 @@ const nextConfig: NextConfig = {
|
|||||||
source: "/api/:path*",
|
source: "/api/:path*",
|
||||||
headers: [{ key: "Cache-Control", value: "no-store, max-age=0" }],
|
headers: [{ key: "Cache-Control", value: "no-store, max-age=0" }],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
source: "/models/:path*",
|
||||||
|
headers: [
|
||||||
|
{
|
||||||
|
key: "Cache-Control",
|
||||||
|
value: "public, max-age=31536000, immutable",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
BIN
public/models/airplane.glb
Normal file
BIN
public/models/airplane.glb
Normal file
Binary file not shown.
@ -36,6 +36,7 @@
|
|||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
height: 100dvh;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@ -61,6 +61,10 @@ export default function RootLayout({
|
|||||||
return (
|
return (
|
||||||
<html lang="en" className="dark">
|
<html lang="en" className="dark">
|
||||||
<head>
|
<head>
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1, viewport-fit=cover"
|
||||||
|
/>
|
||||||
{GA_ID && /^G-[A-Z0-9]+$/.test(GA_ID) && (
|
{GA_ID && /^G-[A-Z0-9]+$/.test(GA_ID) && (
|
||||||
<>
|
<>
|
||||||
<Script
|
<Script
|
||||||
|
|||||||
@ -13,8 +13,10 @@ export class ErrorBoundary extends Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidCatch(error: Error, info: React.ErrorInfo) {
|
componentDidCatch(error: Error, info: React.ErrorInfo) {
|
||||||
|
if (process.env.NODE_ENV === "development") {
|
||||||
console.error("[aeris] Uncaught error:", error, info.componentStack);
|
console.error("[aeris] Uncaught error:", error, info.componentStack);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (this.state.error) {
|
if (this.state.error) {
|
||||||
|
|||||||
@ -177,7 +177,7 @@ function FlightTrackerInner() {
|
|||||||
}, [activeCity.coordinates]);
|
}, [activeCity.coordinates]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="relative h-screen w-screen overflow-hidden bg-black">
|
<main className="relative h-dvh w-screen overflow-hidden bg-black">
|
||||||
<Map mapStyle={mapStyle.style} isDark={mapStyle.dark}>
|
<Map mapStyle={mapStyle.style} isDark={mapStyle.dark}>
|
||||||
<CameraController city={activeCity} />
|
<CameraController city={activeCity} />
|
||||||
<AirportLayer
|
<AirportLayer
|
||||||
@ -202,11 +202,11 @@ function FlightTrackerInner() {
|
|||||||
data-map-theme={mapStyle.dark ? "dark" : "light"}
|
data-map-theme={mapStyle.dark ? "dark" : "light"}
|
||||||
className="pointer-events-none absolute inset-0 z-10"
|
className="pointer-events-none absolute inset-0 z-10"
|
||||||
>
|
>
|
||||||
<div className="pointer-events-auto absolute left-4 top-4 flex items-center gap-3">
|
<div className="pointer-events-auto absolute left-3 top-3 flex items-center gap-3 sm:left-4 sm:top-4">
|
||||||
<Brand isDark={mapStyle.dark} />
|
<Brand isDark={mapStyle.dark} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pointer-events-auto absolute right-4 top-4 flex items-center gap-2">
|
<div className="pointer-events-auto absolute right-3 top-3 flex items-center gap-1.5 sm:right-4 sm:top-4 sm:gap-2">
|
||||||
<a
|
<a
|
||||||
href={GITHUB_REPO_URL}
|
href={GITHUB_REPO_URL}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@ -250,7 +250,7 @@ function FlightTrackerInner() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pointer-events-auto absolute bottom-4 left-4">
|
<div className="pointer-events-auto absolute bottom-[env(safe-area-inset-bottom,0px)] left-3 mb-3 sm:bottom-4 sm:left-4 sm:mb-0">
|
||||||
<StatusBar
|
<StatusBar
|
||||||
flightCount={flights.length}
|
flightCount={flights.length}
|
||||||
cityName={activeCity.name}
|
cityName={activeCity.name}
|
||||||
@ -262,7 +262,7 @@ function FlightTrackerInner() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pointer-events-auto absolute bottom-4 right-4">
|
<div className="pointer-events-auto absolute bottom-[env(safe-area-inset-bottom,0px)] right-3 mb-3 sm:bottom-4 sm:right-4 sm:mb-0">
|
||||||
<AltitudeLegend />
|
<AltitudeLegend />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
import { useEffect, useRef, useCallback } from "react";
|
import { useEffect, useRef, useCallback } from "react";
|
||||||
import { MapboxOverlay } from "@deck.gl/mapbox";
|
import { MapboxOverlay } from "@deck.gl/mapbox";
|
||||||
import { IconLayer, PathLayer } from "@deck.gl/layers";
|
import { IconLayer, PathLayer } from "@deck.gl/layers";
|
||||||
|
import { ScenegraphLayer } from "@deck.gl/mesh-layers";
|
||||||
import { useMap } from "./map";
|
import { useMap } from "./map";
|
||||||
import { altitudeToColor, altitudeToElevation } from "@/lib/flight-utils";
|
import { altitudeToColor, altitudeToElevation } from "@/lib/flight-utils";
|
||||||
import type { FlightState } from "@/lib/opensky";
|
import type { FlightState } from "@/lib/opensky";
|
||||||
@ -13,11 +14,13 @@ const DEFAULT_ANIM_DURATION_MS = 30_000;
|
|||||||
const MIN_ANIM_DURATION_MS = 8_000;
|
const MIN_ANIM_DURATION_MS = 8_000;
|
||||||
const MAX_ANIM_DURATION_MS = 45_000;
|
const MAX_ANIM_DURATION_MS = 45_000;
|
||||||
const TELEPORT_THRESHOLD = 0.3;
|
const TELEPORT_THRESHOLD = 0.3;
|
||||||
const TRAIL_BELOW_AIRCRAFT_METERS = 20;
|
const TRAIL_BELOW_AIRCRAFT_METERS = 40;
|
||||||
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;
|
const TRACK_DAMPING = 0.18;
|
||||||
const TRAIL_SMOOTHING_ITERATIONS = 3;
|
const TRAIL_SMOOTHING_ITERATIONS = 3;
|
||||||
|
const AIRCRAFT_SCENEGRAPH_URL = "/models/airplane.glb";
|
||||||
|
const AIRCRAFT_PX_PER_UNIT = 0.3;
|
||||||
|
|
||||||
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,32 +167,60 @@ function createAircraftAtlas(): HTMLCanvasElement {
|
|||||||
canvas.height = size;
|
canvas.height = size;
|
||||||
const ctx = canvas.getContext("2d")!;
|
const ctx = canvas.getContext("2d")!;
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, size, size);
|
||||||
ctx.fillStyle = "#ffffff";
|
ctx.fillStyle = "#ffffff";
|
||||||
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(64, 12);
|
ctx.moveTo(64, 6);
|
||||||
ctx.lineTo(72, 48);
|
ctx.lineTo(71, 19);
|
||||||
ctx.lineTo(108, 72);
|
ctx.lineTo(71, 33);
|
||||||
ctx.lineTo(104, 78);
|
ctx.lineTo(100, 44);
|
||||||
ctx.lineTo(72, 66);
|
ctx.lineTo(106, 52);
|
||||||
ctx.lineTo(70, 96);
|
ctx.lineTo(80, 53);
|
||||||
|
ctx.lineTo(72, 56);
|
||||||
|
ctx.lineTo(72, 88);
|
||||||
|
ctx.lineTo(90, 101);
|
||||||
ctx.lineTo(88, 108);
|
ctx.lineTo(88, 108);
|
||||||
ctx.lineTo(86, 114);
|
ctx.lineTo(69, 99);
|
||||||
ctx.lineTo(64, 104);
|
ctx.lineTo(69, 121);
|
||||||
ctx.lineTo(42, 114);
|
ctx.lineTo(64, 126);
|
||||||
|
ctx.lineTo(59, 121);
|
||||||
|
ctx.lineTo(59, 99);
|
||||||
ctx.lineTo(40, 108);
|
ctx.lineTo(40, 108);
|
||||||
ctx.lineTo(58, 96);
|
ctx.lineTo(38, 101);
|
||||||
ctx.lineTo(56, 66);
|
ctx.lineTo(56, 88);
|
||||||
ctx.lineTo(24, 78);
|
ctx.lineTo(56, 56);
|
||||||
ctx.lineTo(20, 72);
|
ctx.lineTo(48, 53);
|
||||||
ctx.lineTo(56, 48);
|
ctx.lineTo(22, 52);
|
||||||
|
ctx.lineTo(28, 44);
|
||||||
|
ctx.lineTo(57, 33);
|
||||||
|
ctx.lineTo(57, 19);
|
||||||
ctx.closePath();
|
ctx.closePath();
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
|
|
||||||
|
ctx.globalCompositeOperation = "destination-out";
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(64, 13);
|
||||||
|
ctx.lineTo(67, 19);
|
||||||
|
ctx.lineTo(64, 24);
|
||||||
|
ctx.lineTo(61, 19);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
ctx.globalCompositeOperation = "source-over";
|
||||||
|
|
||||||
return canvas;
|
return canvas;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AIRCRAFT_ICON_MAPPING = {
|
const AIRCRAFT_ICON_MAPPING = {
|
||||||
aircraft: { x: 0, y: 0, width: 128, height: 128, mask: true },
|
aircraft: {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 128,
|
||||||
|
height: 128,
|
||||||
|
anchorX: 64,
|
||||||
|
anchorY: 64,
|
||||||
|
mask: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let _atlasCache: string | undefined;
|
let _atlasCache: string | undefined;
|
||||||
@ -438,7 +469,7 @@ export function FlightLayers({
|
|||||||
data: interpolated,
|
data: interpolated,
|
||||||
getPosition: (d) => [d.longitude!, d.latitude!, 0],
|
getPosition: (d) => [d.longitude!, d.latitude!, 0],
|
||||||
getIcon: () => "aircraft",
|
getIcon: () => "aircraft",
|
||||||
getSize: 18,
|
getSize: 20,
|
||||||
getColor: [0, 0, 0, 60],
|
getColor: [0, 0, 0, 60],
|
||||||
getAngle: (d) => 360 - (d.trueTrack ?? 0),
|
getAngle: (d) => 360 - (d.trueTrack ?? 0),
|
||||||
iconAtlas: atlasUrl,
|
iconAtlas: atlasUrl,
|
||||||
@ -580,7 +611,7 @@ export function FlightLayers({
|
|||||||
}
|
}
|
||||||
|
|
||||||
layers.push(
|
layers.push(
|
||||||
new IconLayer<FlightState>({
|
new ScenegraphLayer<FlightState>({
|
||||||
id: "flight-aircraft",
|
id: "flight-aircraft",
|
||||||
data: interpolated,
|
data: interpolated,
|
||||||
getPosition: (d) => [
|
getPosition: (d) => [
|
||||||
@ -588,16 +619,20 @@ export function FlightLayers({
|
|||||||
d.latitude!,
|
d.latitude!,
|
||||||
altitudeToElevation(d.baroAltitude),
|
altitudeToElevation(d.baroAltitude),
|
||||||
],
|
],
|
||||||
getIcon: () => "aircraft",
|
getOrientation: (d) => {
|
||||||
getSize: 22,
|
const vr = d.verticalRate ?? 0;
|
||||||
|
const v = d.velocity ?? 0;
|
||||||
|
const pitch = v > 0 ? (-Math.atan2(vr, v) * 180) / Math.PI : 0;
|
||||||
|
const yaw = -(d.trueTrack ?? 0);
|
||||||
|
return [pitch, yaw, 90];
|
||||||
|
},
|
||||||
getColor: (d) =>
|
getColor: (d) =>
|
||||||
altColors ? altitudeToColor(d.baroAltitude) : defaultColor,
|
altColors ? altitudeToColor(d.baroAltitude) : defaultColor,
|
||||||
getAngle: (d) => 360 - (d.trueTrack ?? 0),
|
scenegraph: AIRCRAFT_SCENEGRAPH_URL,
|
||||||
iconAtlas: atlasUrl,
|
sizeScale: 25,
|
||||||
iconMapping: AIRCRAFT_ICON_MAPPING,
|
sizeMinPixels: AIRCRAFT_PX_PER_UNIT,
|
||||||
billboard: false,
|
sizeMaxPixels: AIRCRAFT_PX_PER_UNIT,
|
||||||
sizeUnits: "pixels",
|
_lighting: "pbr",
|
||||||
sizeScale: 1,
|
|
||||||
pickable: true,
|
pickable: true,
|
||||||
onHover: handleHover,
|
onHover: handleHover,
|
||||||
onClick: handleClick,
|
onClick: handleClick,
|
||||||
@ -608,13 +643,15 @@ export function FlightLayers({
|
|||||||
|
|
||||||
overlay.setProps({ layers });
|
overlay.setProps({ layers });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (process.env.NODE_ENV === "development") {
|
||||||
console.error("[aeris] FlightLayers render error:", err);
|
console.error("[aeris] FlightLayers render error:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buildAndPushLayers();
|
buildAndPushLayers();
|
||||||
return () => cancelAnimationFrame(animFrameRef.current);
|
return () => cancelAnimationFrame(animFrameRef.current);
|
||||||
}, [atlasUrl, handleHover, handleClick]);
|
}, [atlasUrl, handleHover, handleClick, map]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,7 +50,7 @@ export function StatusBar({
|
|||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 12 }}
|
initial={{ opacity: 0, y: 12 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
@ -122,7 +122,7 @@ export function StatusBar({
|
|||||||
damping: 24,
|
damping: 24,
|
||||||
delay: 0.48,
|
delay: 0.48,
|
||||||
}}
|
}}
|
||||||
className="flex items-center gap-1 rounded-xl border px-1.5 py-1.5 backdrop-blur-2xl"
|
className="flex items-center gap-1.5 rounded-xl border px-2.5 py-2 backdrop-blur-2xl"
|
||||||
style={{
|
style={{
|
||||||
borderColor: "rgb(var(--ui-fg) / 0.06)",
|
borderColor: "rgb(var(--ui-fg) / 0.06)",
|
||||||
backgroundColor: "rgb(var(--ui-bg) / 0.5)",
|
backgroundColor: "rgb(var(--ui-bg) / 0.5)",
|
||||||
@ -133,15 +133,19 @@ export function StatusBar({
|
|||||||
onClick={onNorthUp}
|
onClick={onNorthUp}
|
||||||
aria-label="North up"
|
aria-label="North up"
|
||||||
title="North up"
|
title="North up"
|
||||||
className="rounded-lg px-2.5 py-1 text-[11px] font-medium tracking-wide transition-colors"
|
className="text-[11px] font-medium tracking-wide transition-colors"
|
||||||
style={{ color: "rgb(var(--ui-fg) / 0.55)" }}
|
style={{ color: "rgb(var(--ui-fg) / 0.55)" }}
|
||||||
>
|
>
|
||||||
<Compass className="h-3.5 w-3.5" />
|
<Compass className="h-3 w-3" />
|
||||||
</button>
|
</button>
|
||||||
|
<div
|
||||||
|
className="h-3 w-px"
|
||||||
|
style={{ backgroundColor: "rgb(var(--ui-fg) / 0.08)" }}
|
||||||
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onResetView}
|
onClick={onResetView}
|
||||||
className="rounded-lg px-2.5 py-1 text-[11px] font-medium tracking-wide transition-colors"
|
className="text-[11px] font-medium tracking-wide transition-colors"
|
||||||
style={{ color: "rgb(var(--ui-fg) / 0.55)" }}
|
style={{ color: "rgb(var(--ui-fg) / 0.55)" }}
|
||||||
>
|
>
|
||||||
Reset
|
Reset
|
||||||
|
|||||||
Reference in New Issue
Block a user