1. Military callsigns (aircraft.js): callsigns matching CONFIG.aircraft. militaryPrefixes tint red regardless of altitude band and bump larger. New "Military filter" HUD row (default off) shows only military aircraft when on; status "N military of M". Pick overlay shows Military/Civil class. 2. Satellite ground track (satellites.js): selecting a satellite draws its sub-satellite path (height 0, ±half orbit around now) as a dashed polyline in the sat's colour; deselect hides it; one track at a time. Recomputed on demand from the satrec (no upfront precompute of 97 tracks). 3. Shareable URL state (main.js + ui.js): camera / mode / layer toggles / clock offset serialize to location.hash (replaceState, 1 s cadence, only on change) and are restored on boot; malformed hashes ignored silently. 4. Screenshot endpoint (serve.py): dev-only POST snap?name= writes a base64 PNG body to docs/<name>.png (name sanitized to [a-z0-9-], can't escape docs/). Captured docs/screenshot-data.png + screenshot-photo.png (render() called synchronously before toDataURL — the preserveDrawingBuffer pitfall). README rewritten for Wave 2: quakes/fires layer rows (marked REAL), replay, shared cache, shareable links, ground tracks, military, screenshots, updated architecture + roadmap. Verified in browser: 11 military of 6062 + filter; ground track 180 pts on select / hidden on deselect; URL round-trips camera+mode+layers+clock; both screenshots written as valid 1280x720 PNGs; subpath audit clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
86 lines
2.9 KiB
JavaScript
86 lines
2.9 KiB
JavaScript
// HUD controller: layer registry, per-feed status lines, LIVE/SCRUBBED chip.
|
||
|
||
const rows = new Map();
|
||
|
||
// Register a layer row. Returns a handle so callers can flip the checkbox.
|
||
export function addLayer(id, name, defaultOn, onToggle) {
|
||
const row = document.createElement('div');
|
||
row.className = 'layer-row';
|
||
row.innerHTML = `
|
||
<label>
|
||
<input type="checkbox" ${defaultOn ? 'checked' : ''} />
|
||
<span class="dot" data-state="${defaultOn ? 'ok' : 'off'}"></span>
|
||
<span class="lname"></span>
|
||
</label>
|
||
<div class="lstatus"></div>`;
|
||
row.querySelector('.lname').textContent = name;
|
||
const cb = row.querySelector('input');
|
||
cb.addEventListener('change', () => onToggle(cb.checked));
|
||
document.getElementById('layers').appendChild(row);
|
||
rows.set(id, row);
|
||
return { setChecked: (v) => { cb.checked = v; } };
|
||
}
|
||
|
||
// Registered layer ids, in registration order (for URL-state serialization).
|
||
export function getLayerIds() {
|
||
return [...rows.keys()];
|
||
}
|
||
|
||
// Is a layer's checkbox currently checked?
|
||
export function getLayerChecked(id) {
|
||
const row = rows.get(id);
|
||
return row ? row.querySelector('input').checked : false;
|
||
}
|
||
|
||
// Set a layer's checkbox AND fire its onToggle (replays a real user toggle),
|
||
// so restoring URL state actually shows/hides the layer. No-op if unchanged.
|
||
export function setLayerChecked(id, on) {
|
||
const row = rows.get(id);
|
||
if (!row) return;
|
||
const cb = row.querySelector('input');
|
||
if (cb.checked !== on) {
|
||
cb.checked = on;
|
||
cb.dispatchEvent(new Event('change'));
|
||
}
|
||
}
|
||
|
||
// state ∈ 'ok' | 'warn' | 'err' | 'off'
|
||
export function setStatus(id, text, state = 'ok') {
|
||
const row = rows.get(id);
|
||
if (!row) return;
|
||
row.querySelector('.lstatus').textContent = text;
|
||
row.querySelector('.dot').dataset.state = state;
|
||
}
|
||
|
||
export function setLiveChip(isLive) {
|
||
const chip = document.getElementById('live-chip');
|
||
if (!chip) return;
|
||
chip.textContent = isLive ? 'LIVE' : 'SCRUBBED';
|
||
chip.title = isLive ? 'Clock is at wall-clock now' : 'Live layers paused — scrubbed off now';
|
||
chip.dataset.state = isLive ? 'live' : 'scrub';
|
||
}
|
||
|
||
// Small fixed overlay for primitive picks (aircraft), which have no Cesium InfoBox.
|
||
export function showPickOverlay(title, rowPairs) {
|
||
const el = document.getElementById('pick-overlay');
|
||
if (!el) return;
|
||
el.innerHTML =
|
||
`<div class="po-close" role="button" aria-label="close">×</div>` +
|
||
`<div class="po-title"></div>` +
|
||
rowPairs.map(() => `<div class="po-row"><span></span><span></span></div>`).join('');
|
||
el.querySelector('.po-title').textContent = title;
|
||
const rowEls = el.querySelectorAll('.po-row');
|
||
rowPairs.forEach(([k, v], i) => {
|
||
const spans = rowEls[i].querySelectorAll('span');
|
||
spans[0].textContent = k;
|
||
spans[1].textContent = v;
|
||
});
|
||
el.querySelector('.po-close').addEventListener('click', hidePickOverlay);
|
||
el.hidden = false;
|
||
}
|
||
|
||
export function hidePickOverlay() {
|
||
const el = document.getElementById('pick-overlay');
|
||
if (el) el.hidden = true;
|
||
}
|