// Live aircraft (ADS-B via OpenSky) as a BillboardCollection primitive. // Thousands of billboards rebuilt wholesale each poll; entities would crawl. // SPEC §6.3. export default function create(ctx) { const { viewer, CONFIG, lib, ui, Cesium } = ctx; const bc = viewer.scene.primitives.add(new Cesium.BillboardCollection()); const glyph = lib.aircraftGlyph(); // shared canvas, tinted per-billboard let enabled = true; // HUD checkbox let isLive = true; // clock at wall-clock now let inFlight = false; // a fetch is currently running let timer = null; // pending setTimeout handle let delay = CONFIG.aircraft.pollMs; // current cadence (grows on 429) ui.addLayer('aircraft', 'Aircraft (live ADS-B)', true, (on) => { enabled = on; bc.show = on && isLive; if (on) kick(); }); // Poll only while enabled, live, and the tab is visible. function canPoll() { return enabled && isLive && !document.hidden; } function schedule(ms) { if (timer) clearTimeout(timer); timer = setTimeout(tick, ms); } function kick() { if (!canPoll() || timer || inFlight) return; schedule(0); } async function tick() { timer = null; if (!canPoll() || inFlight) return; inFlight = true; try { await poll(); } finally { inFlight = false; } if (canPoll()) schedule(delay); } function buildUrl() { let url = CONFIG.proxy.opensky; // RELATIVE — works at "/" and "/godsigh/" const bb = CONFIG.aircraft.bbox; if (bb) { const q = new URLSearchParams({ lamin: bb.lamin, lomin: bb.lomin, lamax: bb.lamax, lomax: bb.lomax, }); url = `${url}?${q.toString()}`; } return url; } async function poll() { let res; try { res = await fetch(buildUrl()); } catch (err) { ui.setStatus('aircraft', 'network error — retrying', 'warn'); return; } if (res.status === 429) { delay = delay * 4; // back off ui.setStatus('aircraft', 'rate-limited — anonymous OpenSky quota', 'warn'); return; } if (!res.ok) { ui.setStatus('aircraft', `HTTP ${res.status} — retrying`, 'warn'); return; } const remaining = res.headers.get('X-Rate-Limit-Remaining'); let data; try { data = await res.json(); } catch (err) { ui.setStatus('aircraft', 'bad response — retrying', 'warn'); return; } const states = Array.isArray(data && data.states) ? data.states : []; bc.removeAll(); let count = 0; for (const s of states) { if (!s) continue; const lon = s[5]; const lat = s[6]; const onGround = s[8]; if (lon == null || lat == null || onGround) continue; const alt = s[7]; const track = s[10]; bc.add({ position: Cesium.Cartesian3.fromDegrees(lon, lat, alt || 0), image: glyph, color: lib.cz(lib.altitudeColor(CONFIG, alt)), rotation: lib.headingToRotation(track), // lib negates — do NOT negate again scale: 0.55, id: { layer: 'aircraft', icao24: s[0], callsign: (s[1] || '').trim(), country: s[2], alt, vel: s[9], track, }, }); count++; } delay = CONFIG.aircraft.pollMs; // healthy response — reset cadence const stamp = data.time ? new Date(data.time * 1000).toLocaleTimeString() : '—'; const quota = remaining != null ? ` · quota ${remaining}` : ''; ui.setStatus('aircraft', `${count} aircraft · ${stamp}${quota}`, 'ok'); } function onClockTick(currentTime, live) { isLive = live; if (!isLive) { bc.show = false; // pause: hide + skip fetches return; } bc.show = enabled; if (enabled) kick(); } function handlePick(picked) { if (picked?.id?.layer !== 'aircraft') return false; const id = picked.id; ui.showPickOverlay('✈ ' + (id.callsign || id.icao24), [ ['Country', id.country || '—'], ['Altitude', `${Math.round(id.alt || 0)} m / ${Math.round((id.alt || 0) * 3.28084)} ft`], ['Speed', `${Math.round((id.vel || 0) * 1.94384)} kt`], ['Heading', `${Math.round(id.track || 0)}°`], ]); return true; } function clearPick() { ui.hidePickOverlay(); } // Self-heal when the tab regains focus. A backgrounded tab kills the poll // loop (canPoll() is false, so tick() returns without rescheduling); if the // clock is also paused, onClockTick never fires again to restart it (its // currentTime is frozen), so the feed would stall permanently. kick() is // idempotent (guarded by timer/inFlight/canPoll), so this is safe. document.addEventListener('visibilitychange', () => { if (!document.hidden) kick(); }); // One immediate fetch on init. kick(); return { id: 'aircraft', onClockTick, handlePick, clearPick }; }