GODSIGH/js/layers/aircraft.js
jing 6c7b945848 phases 2-5: satellite, aircraft, ship, infra & event layers
- satellites: Celestrak TLE -> SGP4 -> time-dynamic entities w/ glowing orbit
  paths; per-source cap + reordering so COSMOS/ISS aren't starved by the 72-sat
  Gaofen fleet (now ~97 sats, diverse); chunked propagation, orbital InfoBox
- aircraft: global OpenSky ADS-B -> BillboardCollection primitive (6444 planes
  verified), altitude-banded, quota/scrub/visibility-gated polling, click overlay
- ships: [DEMO] fleet through Hormuz incl. toll-route carrier, dark-vessel AIS
  gap, Fujairah idle cluster; optional aisstream.io live path
- infra: choke-point rings, Petroline + Habshan-Fujairah pipelines, 9 facilities
- events: [DEMO] time-anchored pulsing markers that appear as the slider crosses
- authored + adversarially hardened via parallel agent workflow

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 12:25:51 +10:00

159 lines
4.3 KiB
JavaScript

// 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();
}
// One immediate fetch on init.
kick();
return { id: 'aircraft', onClockTick, handlePick, clearPick };
}