A live 3D globe of the world's air traffic. Around 10,000 aircraft, updated from the OpenSky Network, glide across a dot-matrix Earth with real day and night. Filter to cargo operators, slice by altitude, search a callsign, and click any aircraft for its details and route.
Built with Three.js r186, TypeScript and Vite. No framework and no backend: it runs entirely in the browser, with an optional proxy for production.
- Live traffic: every aircraft OpenSky can see, refreshed about every 75 seconds. Between updates, each one keeps flying along its great circle at its reported speed and heading.
- Cargo view: freight operators (FedEx, UPS, DHL, Cargolux, Atlas and others) are highlighted in amber, with a one-click "Cargo only" filter.
- Filters: an altitude range, aircraft on the ground, and callsign search with suggestions.
- Flight details: altitude, speed, heading, climb rate and registration. A route lookup draws the great circle from origin to destination, solid behind the aircraft and dashed ahead.
- Live summaries: counts, busiest operators and countries of registration, all following the filters.
- Fly-to: regions, plus a Follow this aircraft camera.
- Shareable links:
…/flightline/#UPS231opens straight to that flight. - Phones: the panel becomes a bottom sheet.
GPU dead reckoning. All aircraft are one InstancedBufferGeometry draw call. Each instance stores its last position fix, heading, speed and fix time. The vertex shader moves it to "now" every frame along its great circle, using P(d) = cos d · P₀ + sin d · T₀, and orients it to its new heading. The CPU never updates a position between API calls. Instance times are stored relative to a base epoch, so they fit in 32-bit floats. (src/globe/flights.ts)
Picking without raycasting. Clicks and hovers run the same dead reckoning on the CPU, project the visible aircraft to the screen, and take the nearest within a few pixels, skipping any behind the globe. A unit test checks that the shader's vector maths matches the CPU formula. (tests/geo.test.ts)
A respectful API client. The feed controller (src/data/feed.ts):
- polls every 75 s, slowing down as OpenSky reports fewer credits left;
- honours
429andX-Rate-Limit-Retry-After-Seconds; - backs off exponentially on errors, from 30 s up to 10 min;
- times out hung requests;
- pauses entirely while the tab is hidden.
Never an empty globe. If live data can't be reached, the app falls back, in order, to:
- a recorded snapshot, if you made one locally with
npm run snapshot, replayed as if it were captured just now; - simulated traffic on real routes between hub airports.
The status pill always says which one you're looking at, and why.
The Earth. Land comes from Natural Earth (via world-atlas). It is painted once into an equirectangular mask, with rings that cross the antimeridian unwrapped first, and then sampled on an equal-area dot grid. Borders are a LineSegments mesh. The ocean, dots and atmosphere shaders all share one sun direction, computed from the date. The land dots shrink as you zoom in, and the view is offset so the globe centres in the space the panel leaves free. (src/globe/earth.ts)
| Source | Used for | Notes |
|---|---|---|
| OpenSky Network REST API | Live positions | Anonymous use: 400 credits a day per visitor, and a global query costs 4. |
| adsbdb | Origin and destination for a callsign | A crowd-maintained schedule, so it can be wrong for a given day. Labelled as the scheduled route. |
| Natural Earth via world-atlas | Land and borders | Public domain |
OpenSky's terms. OpenSky licenses its data for non-profit research and education, and asks for written permission before its API is used in a live product or service. Its terms also forbid redistributing the data, which is why recorded snapshots are git-ignored and never committed here. If you publish a fork, ask OpenSky first, or build the demo mode, which uses simulated traffic only: set VITE_LIVE=off, or the LIVE repository variable to off.
Citation: The OpenSky Network, https://opensky-network.org. M. Schäfer, M. Strohmeier, V. Lenders, I. Martinovic and M. Wilhelm, "Bringing up OpenSky: A large-scale ADS-B sensor network for research", IPSN 2014.
The "cargo" label comes from a hand-made list of freight operators by callsign prefix (src/data/airlines.ts). Passenger airlines' belly cargo isn't counted.
Out of the box, each visitor calls OpenSky directly with their own allowance. Serving one shared, cached feed to the public counts as operational use, so get OpenSky's permission first. proxy/ holds a small Cloudflare Worker that adds CORS headers, caches each update for 30 s so every visitor shares one feed, and can use an OpenSky API client for a larger allowance. Set its address as the FLIGHTS_URL repository variable and GitHub Actions builds it in. See proxy/README.md.
npm install
npm run dev # http://localhost:5173
npm test # unit tests (Node's built-in test runner)
npm run build # type-checks, then builds to dist/
npm run snapshot # records live data into public/data/snapshot.json (local only, git-ignored)Deploying: in the repository, go to Settings → Pages and set Source to GitHub Actions. Every push to main then tests, builds and deploys through .github/workflows/deploy.yml.
index.html page and panel markup
src/
main.ts scene, camera, fly-to, panel, filters, search, selection, loop
geo.ts great circles, dead reckoning, bearings, the sun's position
globe/earth.ts ocean, dot-matrix land, borders, atmosphere, day and night
globe/flights.ts instanced aircraft, dead-reckoned in the vertex shader; picking
globe/route.ts the selected flight's route
data/opensky.ts OpenSky client and state-vector parser
data/feed.ts polling, rate limits, backoff, fallbacks
data/simulated.ts fallback traffic between real hubs
data/airlines.ts callsign prefix → operator, cargo flag
data/routes.ts adsbdb route lookup
ui/format.ts feet, knots, headings, "40 s ago"
style.css
tests/ unit tests
scripts/snapshot.mjs records a snapshot
proxy/ optional Cloudflare Worker
MIT. See LICENSE. The flight data belongs to its providers and is used under their terms.