GODSIGH/js/layers/windycams.js
type-two f55d3202e0 feat: Wave 6.1 keyed layers — NASA FIRMS hotspots + Windy webcams
- firms (bespoke): raw VIIRS S-NPP fire detections, world, 24h. CSV via
  new serve.py proxy/firms (key from gitignored firmscredentials.json,
  injected server-side, 1h cache + stale-on-error). Points colored/sized
  by fire radiative power; click -> FRP/brightness/time/satellite overlay.
  Verified live: 1412 detections, hottest 88 MW seen 00:35 UTC today.
- windycams (bespoke): viewport-driven Windy Webcams v3 — nearest ~50 cams
  wherever the camera stops, entities with live preview img in InfoBox.
  proxy/windy validates nearby=lat,lon,km, injects x-windy-api-key header,
  10min coarsened cache. Degrades cleanly: 'key rejected' status until a
  real WEBCAMS-product key lands in windycredentials.json (re-read per
  request, no restart needed).
- key loader tolerant of JSON, brace-less fragments, or raw pasted tokens.

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

88 lines
3.6 KiB
JavaScript

// Windy webcams layer (Wave 6.1) — live webcams near wherever you're looking,
// worldwide. Viewport-driven like the radius layer: each time the camera stops
// moving we ask proxy/windy for the ~50 nearest cams (key injected server-side,
// 10 min cache, coarsened cache key so nearby pans reuse entries). Cams render
// as entities so the InfoBox shows the live preview image + a player link.
export default function create(ctx) {
const { viewer, Cesium, lib, ui } = ctx;
const ds = new Cesium.CustomDataSource('windycams');
ds.show = false;
viewer.dataSources.add(ds);
let enabled = false;
let inFlight = false;
let debounce = null;
let moveUnsub = null;
ui.addLayer('windycams', 'Webcams (Windy)', false, (on) => {
enabled = on;
ds.show = on;
if (on) {
moveUnsub = viewer.camera.moveEnd.addEventListener(scheduleRefresh);
refresh();
} else {
if (moveUnsub) { moveUnsub(); moveUnsub = null; }
if (debounce) { clearTimeout(debounce); debounce = null; }
ui.setStatus('windycams', '', 'ok');
}
}, 'Context');
function scheduleRefresh() {
if (!enabled) return;
if (debounce) clearTimeout(debounce);
debounce = setTimeout(refresh, 400);
}
function viewCenter() {
const c = Cesium.Cartographic.fromCartesian(viewer.camera.positionWC);
const km = Math.min(250, Math.max(15, Math.round(c.height / 2000)));
return { lat: Cesium.Math.toDegrees(c.latitude), lon: Cesium.Math.toDegrees(c.longitude), km };
}
async function refresh() {
if (!enabled || inFlight) return;
inFlight = true;
try {
const { lat, lon, km } = viewCenter();
let res;
try {
res = await fetch(`proxy/windy?nearby=${lat.toFixed(2)},${lon.toFixed(2)},${km}`);
} catch {
ui.setStatus('windycams', 'network error', 'warn'); return;
}
if (res.status === 401) { ui.setStatus('windycams', 'key rejected — needs a Webcams API key', 'err'); return; }
if (res.status === 503) { ui.setStatus('windycams', 'no key — add windycredentials.json', 'err'); return; }
if (!res.ok) { ui.setStatus('windycams', `HTTP ${res.status}`, 'warn'); return; }
let j;
try { j = await res.json(); } catch { ui.setStatus('windycams', 'bad response', 'err'); return; }
const cams = j.webcams || [];
ds.entities.removeAll();
for (const w of cams) {
const loc = w.location || {};
if (!isFinite(loc.latitude) || !isFinite(loc.longitude)) continue;
const img = lib.safeUrl(((w.images || {}).current || {}).preview);
const detail = lib.safeUrl((w.urls || {}).detail || ((w.player || {}).day));
ds.entities.add({
position: Cesium.Cartesian3.fromDegrees(loc.longitude, loc.latitude),
point: {
pixelSize: 6, color: lib.cz('#ffd166'),
outlineColor: Cesium.Color.BLACK, outlineWidth: 1,
disableDepthTestDistance: 50000,
},
description:
`<p><b>${lib.escapeHtml(w.title || 'Webcam')}</b></p>` +
(img ? `<p><img src="${lib.escapeHtml(img)}" style="width:100%;border-radius:6px" alt="webcam preview"/></p>` : '') +
`<p style="opacity:.7">${lib.escapeHtml([loc.city, loc.country].filter(Boolean).join(' · '))}</p>` +
(detail ? `<p><a href="${lib.escapeHtml(detail)}" target="_blank" rel="noopener">Open live player ↗</a></p>` : ''),
});
}
ui.setStatus('windycams', `${ds.entities.values.length} cams near view${j.total ? ` · ${j.total} in area` : ''}`, 'ok');
} finally {
inFlight = false;
}
}
return { id: 'windycams' };
}