GODSIGH/js/ui.js
jing 3dde813d7d wave3: categorized collapsible HUD (Space/Air/Sea/Earth/Context/Regional)
12+ layers in a flat list was unusable. ui.js now groups rows into ordered,
collapsible category sections (scheme owned in ui.js so order is stable
regardless of registration order); Regional — Australia collapsed by default;
empty categories auto-hidden via :has(:empty). URL-state get/set layer APIs
unchanged.

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

137 lines
5.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// HUD controller: categorized collapsible layer list, per-feed status lines,
// LIVE/SCRUBBED chip.
const rows = new Map();
// Category scheme is owned here so HUD order is stable regardless of layer
// registration order. Categories collapsed-by-default are marked below.
const CATEGORY_ORDER = ['Space', 'Air', 'Sea', 'Earth & hazards', 'Context', 'Regional — Australia'];
const COLLAPSED_BY_DEFAULT = new Set(['Regional — Australia']);
const CATEGORY_BY_ID = {
satellites: 'Space', 'sat-paths': 'Space', launches: 'Space',
aircraft: 'Air', military: 'Air', 'aircraft-mil': 'Air',
ships: 'Sea',
quakes: 'Earth & hazards', fires: 'Earth & hazards', gdacs: 'Earth & hazards', nws: 'Earth & hazards',
infra: 'Context', events: 'Context',
rfs: 'Regional — Australia',
};
const sections = new Map(); // category → { body }
// Lazily create a category section, inserted in CATEGORY_ORDER position.
function sectionFor(category) {
if (sections.has(category)) return sections.get(category);
const wrap = document.createElement('div');
wrap.className = 'cat';
if (COLLAPSED_BY_DEFAULT.has(category)) wrap.classList.add('collapsed');
const header = document.createElement('div');
header.className = 'cat-header';
header.innerHTML = `<span class="cat-chevron">▾</span><span class="cat-name"></span>`;
header.querySelector('.cat-name').textContent = category;
header.addEventListener('click', () => wrap.classList.toggle('collapsed'));
const body = document.createElement('div');
body.className = 'cat-body';
wrap.appendChild(header);
wrap.appendChild(body);
// Insert before the existing section that is this one's immediate successor in
// CATEGORY_ORDER (smallest index greater than ours), so DOM order stays stable
// regardless of registration order. Unknown categories sort last.
const container = document.getElementById('layers');
const ord = (c) => { const i = CATEGORY_ORDER.indexOf(c); return i < 0 ? Infinity : i; };
const myOrder = ord(category);
let before = null, bestOrder = Infinity;
for (const [cat, s] of sections) {
const idx = ord(cat);
if (idx > myOrder && idx < bestOrder) { bestOrder = idx; before = s.wrap; }
}
container.insertBefore(wrap, before);
const rec = { wrap, body };
sections.set(category, rec);
return rec;
}
// Register a layer row. Returns a handle so callers can flip the checkbox.
// `category` is optional; a known id maps to a fixed category regardless.
export function addLayer(id, name, defaultOn, onToggle, category) {
const cat = CATEGORY_BY_ID[id] || category || 'Other';
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));
sectionFor(cat).body.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;
}