Releases: hook-365/adsb-3d
Release list
v0.6.1: placed maps stay placed
Two fixes from the demo-recording session on issue #6:
- AR free-fly is locked to scope-style movement when hit-test placement is available, so a scope parked on real furniture stays there. Headsets without hit-test keep free-fly for manual positioning.
- Headset performance in busy airspace: trails render at half density (capped to the most recent 300 points) while presenting, cutting the dominant geometry load. An
[xr] perfconsole line every 5 seconds reports frame time and draw calls for further tuning.
Full details in CHANGELOG.md.
v0.6.0: the sky lands on your desk
The VR/AR release. Every feature here was shaped by real Quest 3 hardware testing from @tyzbit, who also recorded the attached demo video: orbiting live traffic, free-flying through it, and dropping the scope onto actual furniture with AR place mode.
Highlights
- AR place mode: look at a surface, pull the trigger, the airspace parks there. WebXR hit-test snaps it to tables, desks, and beds.
- Free-fly, tuned by feel: first-person yaw, deliberate height control, zoom that anchors on what you're looking at. "Years of video games finally paying off."
- Wrist menu with full settings parity: three pages generated from a declarative spec, guarded by a test so no future setting can silently miss the in-headset UI.
- Fat lines: altitude lines and trails at real pixel widths scaled to your render resolution. No more 1px shimmer at 5040x2640 per eye.
- Honest quality readout: the settings panel now reports the actual per-eye resolution your headset granted.
- Stereo mode for WayVR / crossed-eye viewers: billboard and exit button rendered in both eyes.
- Mobile bottom sheet: the aircraft detail card no longer eats the map on phones. Fixed-height sheet, internal scroll, photo docked beside the airframe data at its natural aspect.
Full details in CHANGELOG.md. The attached mp4 is the full-length demo; a shorter cut is embedded in the README.
v0.5.3: the ground is where you are
Changed
- Flat mode grounds at the home field, not sea level. Without 3D terrain enabled, aircraft altitudes render relative to your station's field elevation (clamped at the map plane) — so a jet rolling out at a 4,200 ft-elevation airport sits on the map instead of floating field-elevation-high above it. With 3D terrain on, geometry stays true MSL, as before.
- Documentation now states explicitly that
ALTITUDEand everyFEEDN_ALTare feet MSL.
Pairs with v0.5.0's 3D terrain and v0.5.2's shadow-draped icons: high-elevation stations now read correctly with terrain on or off.
Full details in CHANGELOG.md.
v0.5.2: terrain polish — icons that fall like shadows
A polish pass on 3D terrain, driven by real-world testing over the Wasatch on a Salt Lake City feed.
Fixed
- Ground icons now drape over terrain like shadows. The flat ground-projection icons were being sliced by slopes (or, in an intermediate attempt, hovering above them). Each icon is now a segmented mesh whose vertices conform to the terrain under its heading-rotated footprint — it falls across ridgelines the way an actual shadow would, and hides honestly behind mountains. Which is, after all, what a ground projection is.
- The emergency-squawk ring rides the terrain instead of pulsing at sea level (previously up to thousands of feet underground on high-elevation feeds).
- No more sea-level flash on load. Aircraft ground anchors rendered at sea level until elevation tiles finished decoding, then popped up one by one as data ticks arrived. An arriving elevation tile now re-anchors every aircraft on the next frame.
Full details in CHANGELOG.md.
v0.5.1: integration tests, retention fix, polite polling
A patch release, headlined by the project's first merged outside code contribution — thanks @ValkyrieUK!
Added
- Full-stack integration test suite + CI (#9): a deterministic Docker Compose stack (readsb + ACARS fixtures, TimescaleDB, both backends, the production nginx image in live-only and multi-feed modes) now verifies every push end to end, including a backend restart to catch non-idempotent schema startup.
Fixed
- Fresh installs silently ended up with no retention policy (#9): the retention call failed on clean database init and the service restarted healthy without it, so data grew without bound. Existing databases were unaffected. Upgrading to this release repairs affected databases automatically — a startup migration detects the missing policy and adds it with your configured
RETENTION_DAYS. If you can't upgrade yet, the manual fix is:SELECT add_retention_policy('aircraft_positions', INTERVAL '90 days');
- The suite's timescaledb healthcheck raced the postgres init phase on fast machines; it now probes TCP.
Changed
- track-service polls remote feeders every 5 s instead of every 1 s (heuristic on the feeder address;
FEEDER_POLL_SECONDSoverrides in either direction). Local feeders keep the 1 s cadence. Polling a stranger's home connection every second around the clock was impolite.
Full details in CHANGELOG.md.
v0.5.0: the community-issues release
Everything in this release traces to a GitHub issue (#6, #7, #8, #10).
Added
- Localization — English, plus machine-drafted German and Spanish pending native review (#10). Language follows the browser locale by default; a drift-guard test keeps all locales in lockstep.
- Altitude scale slider (#8) — continuous vertical bias from low-altitude detail (pattern traffic spreads apart) through linear to high-altitude detail (flight levels spread apart). Applies consistently to aircraft, trails, playback, heatmap, terrain, and VR.
- 3D terrain (#7, opt-in) — the basemap rises to real ground elevation (AWS Open Data, no API key). Range rings and markers drape over the hills, ground icons anchor to terrain, the camera stays above the surface, and the detail card shows height above ground. ENABLE_TERRAIN=false disables deploy-wide. Try it with the OpenTopoMap basemap.
- VR comfort options (#6) — movement model (scope vs free-fly) and turn style (snap vs smooth), in settings and on the wrist menu. B/Y cycles through aircraft nearest-first.
Fixed
- The AR rendering freeze (#6) — a theme recolor of the removed passthrough sky threw inside the XR frame loop; subscriber fan-outs now isolate failures.
- A/X recenter no longer teleports the world away; the wrist-menu Labels toggle now governs the VR billboard, which keeps a minimum readable size; AR keeps the basemap as a floating diorama; VR starts at table height.
Full details in CHANGELOG.md.
v0.4.0: performance and UX push
A frontend + backend performance and UX push. The headline goal was
to make the Europe / Hetzner feed (~1500 contacts) feel as snappy
as the local feed in both initial load and steady state, and to
let survey aircraft and loitering tankers keep their full in-scope
trails instead of being cut off at 10 minutes.
Added
- Click to extend trail. Selecting any aircraft now lifts its
per-hex trail cap and triggers a 24 h history backfill, so a
single survey orbiting an airfield gets its full pattern even on
a busy feed. The per-hex cap survives the aircraft dropping out
and reappearing within the session. - URL deep-link isolation. Loading a shared
#hexlink now
also applies that hex as the search query, so the recipient
lands on just that aircraft and its trail. Clearing the search
box brings the rest of the fleet back. - Per-feed trail policy. The local feed defaults to unlimited
trail length with a 4 h history backfill window, so survey
aircraft and loitering tankers keep their full in-scope history.
Higher-density feeds keep the conservative 600-point cap and
30 min backfill. - Stationary sample dedup. Aircraft that haven't moved past the
jitter threshold only sample once per minute instead of once per
second, so a parked aircraft over 12 hours accumulates ~720
trail points instead of ~43k. - Altitude inheritance. The Aircraft record carries an
altFtKnownflag and the store keeps a per-hex last-known
altitude cache; transient frames that lackalt_baroand
alt_geomsubstitute the prior good altitude instead of
snapping the cone to ground. Historical backfill does the same
forward-inheritance walk duringparsePoints.
Changed
- Aircraft list is now virtualized. Only rows inside the
scroll viewport (plus a small overscan) get real DOM. Initial
load on Europe is dramatically faster and scroll feels native
at any fleet size. Filter / sort / search changes re-target
scroll to keep the selected row in view; snapshot-only updates
leave scrollTop alone. - Search filters the map. Typing in the panel search box
now hides non-matching aircraft from the scene as well as the
list, matching the behavior of the MIL / GROUND / AIR / EMERG
filter buttons. Selected aircraft are exempt. - Lazy-loaded feature modules. The voice panel, historical
playback, heatmap layer, and ACARS browser modal are dynamic
imports now. They no longer ship in the cold-load bundle for
deployments (or sessions) that don't use them. - Browser tab title reads
ADS-B 3D · {feed location}and
updates on feed switch. - Backend
track-service./tracks/{hex}and
/tracks/bulk/timelapseauto-downsample whenresolution=full
is requested over a window wider than 4 hours; targets ~7200
points across the window, so a 24 h selection-extension call
returns ~700 kB instead of ~8.6 MB. asyncpg poolmax_size
raised from 20 to 40 so multi-tab and bursty backfill traffic
no longer pushes the acquire timeout.
Fixed
- Invisible-aircraft clicks. Three.js's raycaster doesn't
honorGroup.visible = false, so invisible pick proxies inside
filtered-out aircraft were still registering hits. Clicking on
empty sky in MIL-filter mode no longer surfaces civilian
aircraft hiding underneath. - Trails dipping to ground. Transient upstream frames with
no altitude data used to snap the cone and trail to zero
altitude. The new altitude inheritance keeps the cone at its
last known altitude through the gap, both live and on backfill.
Performance
The reconciler is the biggest contributor; together these changes
take the steady-state per-frame cost on Europe from "noticeably
laggy" to "indistinguishable from local".
- Reconciler
syncFramegates per-aircraft work on per-hexrev
counters from the store. Most frames find every aircraft at the
same rev as last frame and skip the full refresh block. altitudeColorCached/altitudeColorStyleCachedreturn shared
Colorinstances and interned CSS strings bucketed by 250 ft
altitude steps.refreshTrail,refreshColor,refreshLabel
switched over, saving ~450 kColorallocations per second on
a busy feed.- Yaw quaternion math is cached against
lastTrackDegand shared
between the cone and the ground icon. updateLabelLODskips its per-entry pass when the camera hasn't
moved past a small epsilon since the last call.- Settings subscriber only walks the entry map when one of the
three visibility-relevant keys actually flipped. - Label frustum culling. Labels whose anchor is outside the
camera view are flipped tovisible = falseso
CSS2DRendererskips them; a panned-in view on Europe does
~150 DOM transform writes per render instead of ~1500. - Growable + incremental trail buffers. Trail buffer
allocations grow by doubling as needed. A fast path appends
only the new tail segments to existing buffer slots when the
trail's first sample is unchanged. A selected aircraft with a
multi-hour trail goes from rewriting the entire buffer every
refresh to writing one new segment.
Tests
127 → 145 unit tests across reconciler / store / filter / altitude
color / parsePoints altitude inheritance.
v0.3.0: WebXR support (Phases 1-5)
v0.3.0 brings the full WebXR pipeline online. Any connected headset can now open an immersive session directly from the page.
Enter VR in Settings opens an immersive-vr session with head tracking, controllers as laser pointers, and a world-space billboard above the selected aircraft. Trigger pulls raycast aircraft picks through the same selection path as a mouse click, so the detail panel and follow-cam stay in sync.
In-VR wrist menu on the left controller, tilted toward the eye. The right laser hovers rows and the trigger activates them: cycle Theme, cycle Basemap, toggle Range rings, Labels, or Alt lines. No need to lift the headset to change anything.
Locomotion via the thumbsticks and buttons. Left stick scales the world from tabletop to room-scale (persisted across sessions). Right stick snap-turns 30° per push. Right A/X button recenters the scope in front of you.
Enter AR on devices that support immersive-ar (Quest 3, Vision Pro, etc.). Sky and basemap drop away and aircraft float in your living room via passthrough.
See CHANGELOG.md for the full entry.
v0.2.0 — Color themes + FAA chart basemaps
v0.2.0 — color themes + FAA chart basemaps
Five built-in palettes (Midnight Glass, Daylight, Sectional Chart,
Phosphor CRT, High Contrast) with prefers-color-scheme auto mode and
live in-place switching across CSS and Three.js materials.
Five FAA aeronautical chart basemaps (US only): Sectional, Sectional
- Roads, Helicopter, IFR Low, IFR High — served via VFRMap with
auto-discovery of the 56-day chart cycle at container start.
See CHANGELOG.md for the full entry.
ADS-B 3D v0.1.0
First public release of ADS-B 3D — a real-time 3D visualization of ADS-B aircraft traffic. A ground-up TypeScript / Three.js rewrite of the original vanilla-JS app.
Highlights
- Live 3D view — per-second updates with a tar1090 altitude color palette; click-through aircraft detail cards with photos, filed routes, and autopilot data.
- Historical playback — scrub the last 1h / 24h / 7d at 1×–60× speed, with a 3D airway-density heatmap.
- ACARS — optional datalink message decoding with OOOI flight-phase tracking.
- Multi-feed — switch between multiple receivers in-place, no page reload.
- VHF voice scanner — optional call-based airband audio panel.
Ships as a single nginx Docker image plus optional track-service and acars-service.
Getting started
See the README for the quick start, and CHANGELOG.md for full detail.
Multi-arch images (linux/amd64, arm64, arm/v7) build from this tag and publish to GHCR:
ghcr.io/hook-365/adsb-3d:0.1.0ghcr.io/hook-365/adsb-track-service:0.1.0ghcr.io/hook-365/adsb-acars-service:0.1.0