// Generic adsb.lol query-layer factory (free OSINT feeds). Powers the emergency // squawk, rare-type hunter, and shadow-aircraft layers — each is a spec that // names one or more adsb.lol v2 endpoints (proxy/adsb/), merged by hex and // drawn as a BillboardCollection primitive with a click overlay. Live-only // (paused when scrubbed off-now or the tab is hidden), like the aircraft layer. // // spec: { id, name, category, defaultOn, pollMs, // endpoints: ['sqk/7700', 'type/U2', ...], // fetched sequentially (anti-burst) // color(a) -> hex, scale(a)|number, label(a) -> string|null, // describe(a) -> [[k,v],...], status(n) -> string, emptyStatus? -> string } export function createAdsbLayer(ctx, spec) { const { viewer, Cesium, lib, ui } = ctx; const bc = viewer.scene.primitives.add(new Cesium.BillboardCollection()); const glyph = lib.aircraftGlyph(); const pollMs = spec.pollMs || 90000; let enabled = spec.defaultOn !== false; let isLive = true; let inFlight = false; let timer = null; let lastOk = true; bc.show = enabled; ui.addLayer(spec.id, spec.name, enabled, (on) => { enabled = on; bc.show = on && isLive; if (on && isLive) kick(); }, spec.category); ui.setStatus(spec.id, 'loading…', 'warn'); 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; } // On a total failure (e.g. adsb.lol rate-limited the whole batch), retry // soon instead of waiting the full interval. if (canPoll()) schedule(lastOk ? pollMs : 9000); } const altFeet = (a) => (typeof a.alt_baro === 'number' ? a.alt_baro : 0); async function poll() { // Fetch endpoints in PARALLEL and merge by hex. The proxy already spaces the // upstream calls (rate-limit politeness), so parallel is safe here — and it // means one slow/hung endpoint (adsb.lol can be flaky) never stalls the rest. const byHex = new Map(); let anyOk = false; const results = await Promise.allSettled( spec.endpoints.map((ep) => fetch(`proxy/adsb/${ep}`).then((r) => { if (!r.ok) throw new Error(String(r.status)); return r.json().then((d) => ({ ep, d })); })) ); for (const r of results) { if (r.status !== 'fulfilled') continue; anyOk = true; const { ep, d } = r.value; for (const a of (Array.isArray(d.ac) ? d.ac : [])) { if (a.lat == null || a.lon == null || byHex.has(a.hex)) continue; a._src = ep; byHex.set(a.hex, a); } } lastOk = anyOk; if (!anyOk) { ui.setStatus(spec.id, 'busy — retrying shortly', 'warn'); return; } // Optional client-side filter (e.g. rare-types filters the shared mil feed // by ICAO type — one cached call, no per-type rate-limit risk). let list = [...byHex.values()]; if (spec.filter) list = list.filter(spec.filter); bc.removeAll(); for (const a of list) { bc.add({ position: Cesium.Cartesian3.fromDegrees(a.lon, a.lat, altFeet(a) * 0.3048), image: glyph, color: lib.cz(spec.color(a)), rotation: lib.headingToRotation(a.track), scale: typeof spec.scale === 'function' ? spec.scale(a) : (spec.scale || 0.8), disableDepthTestDistance: 50000, // occlude when behind the globe (far side) id: { layer: spec.id, hex: a.hex, flight: (a.flight || '').trim(), t: a.t, reg: a.r, altFt: altFeet(a), gs: a.gs, track: a.track, squawk: a.squawk, src: a._src, }, }); } const n = list.length; const txt = n === 0 && spec.emptyStatus ? spec.emptyStatus() : spec.status(n); ui.setStatus(spec.id, txt, n > 0 && spec.alertOnHits ? 'err' : 'ok'); } function onClockTick(currentTime, live) { isLive = live; if (!isLive) { bc.show = false; return; } bc.show = enabled; if (enabled) kick(); } function handlePick(picked) { if (picked?.id?.layer !== spec.id) return false; ui.showPickOverlay(spec.pickTitle ? spec.pickTitle(picked.id) : (picked.id.flight || picked.id.hex || '—'), spec.describe(picked.id)); return true; } function clearPick() { ui.hidePickOverlay(); } document.addEventListener('visibilitychange', () => { if (!document.hidden) kick(); }); kick(); return { id: spec.id, onClockTick, handlePick, clearPick }; }