// Radius focus (Wave 3.1) — all traffic within 250 nm of where you're looking, // via adsb.lol's free lat/lon/dist query (proxy/adsb/lat/.../lon/.../dist/250). // Camera-driven: refetches when you stop panning/zooming, plus a slow refresh // for freshness. Most useful zoomed into a region. Live-only; off by default. export default function create(ctx) { const { viewer, Cesium, lib, ui } = ctx; const bc = viewer.scene.primitives.add(new Cesium.BillboardCollection()); const glyph = lib.aircraftGlyph(); const DIST_NM = 250; // adsb.lol free radius cap let enabled = false; let isLive = true; let inFlight = false; let debounce = null; let poller = null; let lastKey = null; bc.show = false; ui.addLayer('radius', `Traffic near view (${DIST_NM}nm)`, false, (on) => { enabled = on; bc.show = on && isLive; if (on && isLive) { refresh(); ensurePoller(); } else { clearInterval(poller); poller = null; ui.setStatus('radius', on ? 'locating…' : '', on ? 'warn' : 'ok'); } }); viewer.camera.percentageChanged = 0.25; // fire camera.changed a bit more eagerly viewer.camera.changed.addEventListener(() => { if (!enabled || !isLive) return; if (debounce) clearTimeout(debounce); debounce = setTimeout(refresh, 1800); // refetch after panning stops }); function ensurePoller() { if (poller) return; poller = setInterval(() => { if (enabled && isLive && !document.hidden) refresh(); }, 60000); } // Center = camera nadir point, rounded (also improves proxy cache hits). function center() { const c = viewer.camera.positionCartographic; return [ +Cesium.Math.toDegrees(c.latitude).toFixed(2), +Cesium.Math.toDegrees(c.longitude).toFixed(2), ]; } async function refresh() { if (!enabled || !isLive || document.hidden || inFlight) return; const [lat, lon] = center(); if (!isFinite(lat) || !isFinite(lon)) return; const key = `${lat}/${lon}`; lastKey = key; inFlight = true; try { const res = await fetch(`proxy/adsb/lat/${lat}/lon/${lon}/dist/${DIST_NM}`); if (key !== lastKey) return; // camera moved again mid-fetch — stale if (!res.ok) { ui.setStatus('radius', `HTTP ${res.status}`, 'warn'); return; } const data = await res.json(); const list = Array.isArray(data.ac) ? data.ac : []; bc.removeAll(); let n = 0; for (const a of list) { if (a.lat == null || a.lon == null) continue; const altFt = typeof a.alt_baro === 'number' ? a.alt_baro : 0; bc.add({ position: Cesium.Cartesian3.fromDegrees(a.lon, a.lat, altFt * 0.3048), image: glyph, color: lib.cz('#7bff9d'), rotation: lib.headingToRotation(a.track), scale: 0.7, disableDepthTestDistance: 50000, // occlude when behind the globe (far side) id: { layer: 'radius', hex: a.hex, flight: (a.flight || '').trim(), t: a.t, reg: a.r, altFt, gs: a.gs, track: a.track }, }); n++; } ui.setStatus('radius', `${n} within ${DIST_NM}nm of view`, 'ok'); } catch { ui.setStatus('radius', 'network error', 'warn'); } finally { inFlight = false; } } function onClockTick(currentTime, live) { isLive = live; if (!isLive) { bc.show = false; return; } bc.show = enabled; if (enabled) { refresh(); ensurePoller(); } } function handlePick(picked) { if (picked?.id?.layer !== 'radius') return false; const id = picked.id; ui.showPickOverlay('✈ ' + (id.flight || id.hex || '—'), [ ['Type', id.t || '—'], ['Reg', id.reg || '—'], ['Altitude', `${Math.round(id.altFt || 0)} ft`], ['Speed', `${Math.round(id.gs || 0)} kt`], ]); return true; } function clearPick() { ui.hidePickOverlay(); } document.addEventListener('visibilitychange', () => { if (!document.hidden && enabled && isLive) refresh(); }); return { id: 'radius', onClockTick, handlePick, clearPick }; }