GODSIGH/js/layers/adsb-layer.js
type-two 2a51bcad2f fix: markers bled through the globe (far-side dots didn't track rotation)
Every marker used disableDepthTestDistance: Infinity, so points/billboards on
the FAR side of the Earth drew on top of the near hemisphere instead of being
occluded — reading as 'dots stuck, not moving with the globe' when you rotate.
Changed to 50000 (50km) across all layers: the globe now occludes far-side
markers, while you can still zoom right up to one without it clipping into the
surface. Also made the billboard layers (aircraft/military/adsb/radius) explicit
for consistent behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 10:19:21 +10:00

119 lines
4.6 KiB
JavaScript

// 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/<path>), 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 };
}