Skip to content

Commit cb4374f

Browse files
committed
v1.0.0: Merge v1.0.0-dev into main
2 parents 75aafc1 + f20755b commit cb4374f

32 files changed

Lines changed: 3208 additions & 4800 deletions

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,9 @@ data/
2727
# Private documentation (kept locally)
2828
docs/
2929

30+
# Archived unused source files (kept locally)
31+
archive/
32+
3033
# Environment variables
3134
.env.local
35+
.vercel

README.md

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Lightpath calculates the flight trajectory between any two airports and visualiz
1111
## What it does
1212

1313
- **Great circle routing** — Plots the shortest path between airports on a 3D globe
14+
- **Real flight trajectories** — Look up any flight by callsign to trace its actual route using historical flight data
1415
- **Solar illumination mapping** — Color-codes flight segments by daylight, twilight (civil/nautical/astronomical), and darkness
1516
- **Time animation** — Watch flights move through changing light as Earth rotates
1617
- **Astronomical accuracy** — Uses NOAA equations for solar declination and Jean Meeus algorithms for solar position
@@ -21,7 +22,7 @@ Lightpath calculates the flight trajectory between any two airports and visualiz
2122

2223
- **React 19** + **Three.js** for 3D rendering
2324
- **Vite** for build tooling
24-
- **solar-calculator** (NOAA) + **suncalc** (Jean Meeus) for astronomical calculations
25+
- **solar-calculator** (NOAA) + **suncalc** for astronomical calculations
2526
- **Custom GLSL shaders** for twilight visualization
2627

2728
---
@@ -30,26 +31,35 @@ Lightpath calculates the flight trajectory between any two airports and visualiz
3031

3132
- Real-time solar position calculation
3233
- Latitude-dependent twilight width modeling
34+
- Two search modes: airport-to-airport routing (Great Circle Routes) and flight number lookup
3335
- Animated flight playback with sun position sync
3436
- Color and black-and-white visualization modes
3537
- Shareable flight URLs
38+
- Optional overlays: airport dots, graticule, timezone boundaries, twilight lines
3639

3740
---
3841

3942
## Project structure
4043
```
4144
src/
42-
├── App.jsx # Main component
45+
├── App.jsx # Main component (3D scene, state, non-panel UI)
4346
├── App.css # All styles
4447
├── components/
4548
│ ├── AirportSearchInput.jsx
4649
│ ├── FlightInputPanel.jsx
47-
│ └── AnimationControls.jsx
50+
│ ├── AnimationControls.jsx
51+
│ └── ShareButton.jsx
52+
├── services/
53+
│ └── fr24.js # FlightRadar24 API client
4854
└── utils/
49-
├── geoUtils.js # Coordinate conversion
55+
├── geoUtils.js # Coordinate conversion + flight scaling
5056
├── solarUtils.js # Solar position calculations
5157
├── sceneUtils.js # Label texture generation
52-
└── animationUtils.js # Fade animations
58+
├── animationUtils.js # Fade animations
59+
├── routeInterpolation.js # Timestamp interpolation along arc-length
60+
├── captureUtils.js # Screenshot capture for share cards
61+
├── cardGenerator.js # Share card image generation
62+
└── shareUtils.js # Web Share API wrapper
5363
5464
scripts/
5565
└── build-airports.js # OurAirports CSV → airports.json
@@ -59,7 +69,7 @@ public/
5969
├── earth-texture.png # Custom Earth texture
6070
├── graticule-10.geojson # Latitude/longitude grid
6171
├── timezones.geojson # Timezone boundaries
62-
└── icons/ # SVG icons
72+
└── fonts/ # ABC Repro, ABC Repro Mono
6373
```
6474

6575
---
@@ -80,9 +90,10 @@ npm run build
8090
## Credits
8191

8292
Airport data: [OurAirports](https://ourairports.com/data/)
83-
Astronomical calculations: [solar-calculator](https://www.npmjs.com/package/solar-calculator) (NOAA) and [suncalc](https://github.com/mourner/suncalc) (Jean Meeus)
93+
Astronomical calculations: [solar-calculator](https://www.npmjs.com/package/solar-calculator) (NOAA) and [suncalc](https://github.com/mourner/suncalc) (Jean Meeus)
94+
Topography: [NASA](https://science.nasa.gov/earth/earth-observatory/blue-marble-next-generation/topography-bathymetry-maps/)
8495

85-
Designed and developed by [Studio Folder](https:www.studiofolder.it)
96+
Designed and developed by [Studio Folder](https://www.studiofolder.it)
8697

8798

8899
---

api/flight-lookup.js

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import { Redis } from '@upstash/redis';
2+
3+
const FR24_BASE = 'https://fr24api.flightradar24.com/api';
4+
5+
const redis = process.env.KV_REST_API_URL
6+
? new Redis({
7+
url: process.env.KV_REST_API_URL,
8+
token: process.env.KV_REST_API_TOKEN,
9+
})
10+
: null;
11+
12+
const FR24_HEADERS = {
13+
Authorization: `Bearer ${process.env.FR24_API_TOKEN}`,
14+
'Accept-Version': 'v1',
15+
Accept: 'application/json',
16+
};
17+
18+
function isoUtc(date) {
19+
return date.toISOString().slice(0, 19);
20+
}
21+
22+
function toMs(ts) {
23+
if (typeof ts === 'number') return ts < 1e12 ? ts * 1000 : ts;
24+
return new Date(ts).getTime();
25+
}
26+
27+
export default async function handler(req, res) {
28+
res.setHeader('Access-Control-Allow-Origin', '*');
29+
30+
if (req.method === 'OPTIONS') {
31+
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
32+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
33+
return res.status(204).end();
34+
}
35+
36+
const { flight } = req.query;
37+
if (!flight) {
38+
return res.status(400).json({ error: 'Missing required query parameter: flight' });
39+
}
40+
41+
const key = `flight:${flight.trim().toUpperCase()}`;
42+
43+
if (redis) {
44+
try {
45+
const cached = await redis.get(key);
46+
if (cached) {
47+
return res.status(200).json({ data: cached, cached: true });
48+
}
49+
} catch (_) {
50+
// Cache read failed — fall through to FR24
51+
}
52+
}
53+
54+
// Step 1: Flight Summary Light
55+
const now = new Date();
56+
const from = new Date(now);
57+
from.setDate(from.getDate() - 14);
58+
59+
const summaryParams = new URLSearchParams({
60+
flights: flight,
61+
flight_datetime_from: isoUtc(from),
62+
flight_datetime_to: isoUtc(now),
63+
sort: 'desc',
64+
limit: '2',
65+
});
66+
67+
let summaryRes;
68+
try {
69+
summaryRes = await fetch(`${FR24_BASE}/flight-summary/light?${summaryParams}`, {
70+
headers: FR24_HEADERS,
71+
});
72+
} catch (err) {
73+
return res.status(502).json({ error: 'Failed to reach FR24 API', detail: err.message });
74+
}
75+
76+
if (summaryRes.status !== 200) {
77+
const body = await summaryRes.text();
78+
res.setHeader('Content-Type', 'application/json');
79+
return res.status(summaryRes.status).end(body);
80+
}
81+
82+
const summaryJson = await summaryRes.json();
83+
const completed = (summaryJson.data ?? []).find(f => f.flight_ended === true);
84+
85+
if (!completed) {
86+
return res.status(200).json({ data: null });
87+
}
88+
89+
// Step 2: Historic Flight Events Light
90+
const eventsParams = new URLSearchParams({
91+
flight_ids: completed.fr24_id,
92+
event_types: 'all',
93+
});
94+
95+
let eventsRes;
96+
try {
97+
eventsRes = await fetch(`${FR24_BASE}/historic/flight-events/light?${eventsParams}`, {
98+
headers: FR24_HEADERS,
99+
});
100+
} catch (err) {
101+
return res.status(502).json({ error: 'Failed to reach FR24 API', detail: err.message });
102+
}
103+
104+
if (eventsRes.status !== 200) {
105+
const body = await eventsRes.text();
106+
res.setHeader('Content-Type', 'application/json');
107+
return res.status(eventsRes.status).end(body);
108+
}
109+
110+
const eventsJson = await eventsRes.json();
111+
const record = eventsJson.data?.[0];
112+
const rawEvents = record?.events ?? [];
113+
114+
// Compute offsetMs relative to first event's timestamp
115+
const firstTs = rawEvents.length > 0 ? toMs(rawEvents[0].timestamp) : 0;
116+
const events = rawEvents.map(ev => ({
117+
type: ev.type,
118+
lat: ev.lat ?? null,
119+
lon: ev.lon ?? null,
120+
offsetMs: toMs(ev.timestamp) - firstTs,
121+
details: ev.details ?? {},
122+
}));
123+
124+
const totalDurationMs = events.length > 0 ? events[events.length - 1].offsetMs : 0;
125+
126+
// Extract typical departure time as "HH:MM" from datetime_takeoff (UTC)
127+
let typicalDepartureTimeUtc = null;
128+
if (completed.datetime_takeoff) {
129+
const takeoff = new Date(toMs(completed.datetime_takeoff));
130+
const hh = String(takeoff.getUTCHours()).padStart(2, '0');
131+
const mm = String(takeoff.getUTCMinutes()).padStart(2, '0');
132+
typicalDepartureTimeUtc = `${hh}:${mm}`;
133+
}
134+
135+
const payload = {
136+
summary: {
137+
flight: completed.flight,
138+
orig_icao: completed.orig_icao,
139+
dest_icao: completed.dest_icao,
140+
dest_icao_actual: completed.dest_icao_actual,
141+
type: completed.type,
142+
reg: completed.reg,
143+
callsign: completed.callsign,
144+
},
145+
events,
146+
totalDurationMs,
147+
typicalDepartureTimeUtc,
148+
};
149+
150+
if (redis) {
151+
try {
152+
await redis.set(key, payload, { ex: 30 * 24 * 60 * 60 }); // 30 days TTL
153+
} catch (_) {
154+
// Cache write failed — non-critical, continue
155+
}
156+
}
157+
158+
return res.status(200).json({ data: payload, cached: false });
159+
}

0 commit comments

Comments
 (0)