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>
140 lines
4.8 KiB
JavaScript
140 lines
4.8 KiB
JavaScript
// Military aircraft (REAL) — the free, keyless adsb.lol /v2/mil feed via
|
|
// proxy/mil. This is the unfiltered military traffic OpenSky/FR24 hide (C-17
|
|
// Reach flights, tankers, the Area 51 "Janet" shuttle, etc.) — the OSINT prize.
|
|
//
|
|
// BillboardCollection primitive like the civilian aircraft layer, but a distinct
|
|
// red so it never blends in. Live-only (no history feed for this), polled every
|
|
// minute (the feed is free/unmetered; a 60s server cache coalesces callers).
|
|
|
|
export default function create(ctx) {
|
|
const { viewer, CONFIG, lib, ui, Cesium } = ctx;
|
|
const A = CONFIG.adsbx;
|
|
|
|
const bc = viewer.scene.primitives.add(new Cesium.BillboardCollection());
|
|
const glyph = lib.aircraftGlyph(); // shared triangle, tinted per-billboard
|
|
|
|
let enabled = true;
|
|
let isLive = true;
|
|
let inFlight = false;
|
|
let timer = null;
|
|
let delay = A.pollMs;
|
|
|
|
ui.addLayer('military', 'Military (adsb.lol)', true, (on) => {
|
|
enabled = on;
|
|
bc.show = on && isLive;
|
|
if (on && isLive) kick();
|
|
});
|
|
ui.setStatus('military', 'loading…', 'warn');
|
|
|
|
// Transponder emergency codes → the highlight label.
|
|
const EMERG = { '7500': 'HIJACK', '7600': 'RADIO FAIL', '7700': 'EMERGENCY' };
|
|
|
|
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);
|
|
}
|
|
|
|
// ADS-B Exchange altitude is in FEET (and "ground" for on-ground aircraft).
|
|
function altFeet(a) { return typeof a.alt_baro === 'number' ? a.alt_baro : 0; }
|
|
|
|
async function poll() {
|
|
let res;
|
|
try {
|
|
res = await fetch(A.proxyMil);
|
|
} catch (err) {
|
|
ui.setStatus('military', 'network error — retrying', 'warn');
|
|
return;
|
|
}
|
|
if (res.status === 503) {
|
|
// Key not configured (e.g. prod before the key is installed) — degrade quietly.
|
|
bc.removeAll();
|
|
ui.setStatus('military', 'ADS-B Exchange not configured', 'warn');
|
|
return;
|
|
}
|
|
if (!res.ok) {
|
|
ui.setStatus('military', `HTTP ${res.status} — retrying`, 'warn');
|
|
return;
|
|
}
|
|
let data;
|
|
try {
|
|
data = await res.json();
|
|
} catch (err) {
|
|
ui.setStatus('military', 'bad response — retrying', 'warn');
|
|
return;
|
|
}
|
|
|
|
const list = Array.isArray(data && data.ac) ? data.ac : [];
|
|
bc.removeAll();
|
|
let count = 0;
|
|
let emergencies = 0;
|
|
for (const a of list) {
|
|
if (a.lat == null || a.lon == null) continue;
|
|
const sq = a.squawk;
|
|
const isEmerg = !!EMERG[sq] || (a.emergency && a.emergency !== 'none');
|
|
if (isEmerg) emergencies++;
|
|
bc.add({
|
|
position: Cesium.Cartesian3.fromDegrees(a.lon, a.lat, altFeet(a) * 0.3048),
|
|
image: glyph,
|
|
color: lib.cz(isEmerg ? CONFIG.colors.militaryEmerg : CONFIG.colors.militaryReal),
|
|
rotation: lib.headingToRotation(a.track), // lib negates — track may be absent (→0)
|
|
scale: isEmerg ? 1.05 : 0.82, // bolder than civilian; emergencies bigger still
|
|
disableDepthTestDistance: 50000, // occlude when behind the globe (far side)
|
|
id: {
|
|
layer: 'military',
|
|
hex: a.hex,
|
|
callsign: (a.flight || '').trim(),
|
|
type: a.t,
|
|
reg: a.r,
|
|
altFt: altFeet(a),
|
|
gs: a.gs,
|
|
track: a.track,
|
|
squawk: sq,
|
|
emerg: isEmerg ? (EMERG[sq] || String(a.emergency).toUpperCase()) : null,
|
|
},
|
|
});
|
|
count++;
|
|
}
|
|
|
|
delay = A.pollMs;
|
|
const stamp = data.now ? new Date(data.now).toLocaleTimeString() : new Date().toLocaleTimeString();
|
|
const em = emergencies ? ` · ${emergencies}⚠` : '';
|
|
ui.setStatus('military', `${count} military · ${stamp}${em}`, emergencies ? 'err' : 'ok');
|
|
}
|
|
|
|
function onClockTick(currentTime, live) {
|
|
isLive = live;
|
|
if (!isLive) { bc.show = false; return; } // live-only feed; pause when scrubbed
|
|
bc.show = enabled;
|
|
if (enabled) kick();
|
|
}
|
|
|
|
function handlePick(picked) {
|
|
if (picked?.id?.layer !== 'military') return false;
|
|
const id = picked.id;
|
|
const rows = [
|
|
['Type', id.type || '—'],
|
|
['Reg', id.reg || '—'],
|
|
['Altitude', `${Math.round(id.altFt || 0)} ft`],
|
|
['Speed', `${Math.round(id.gs || 0)} kt`],
|
|
['Squawk', id.squawk || '—'],
|
|
];
|
|
if (id.emerg) rows.unshift(['⚠ Status', id.emerg]);
|
|
ui.showPickOverlay('✈ MIL ' + (id.callsign || id.hex || '—'), rows);
|
|
return true;
|
|
}
|
|
|
|
function clearPick() { ui.hidePickOverlay(); }
|
|
|
|
document.addEventListener('visibilitychange', () => { if (!document.hidden) kick(); });
|
|
kick();
|
|
|
|
return { id: 'military', onClockTick, handlePick, clearPick };
|
|
}
|