// 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: `

${lib.escapeHtml(w.title || 'Webcam')}

` + (img ? `

webcam preview

` : '') + `

${lib.escapeHtml([loc.city, loc.country].filter(Boolean).join(' · '))}

` + (detail ? `

Open live player ↗

` : ''), }); } ui.setStatus('windycams', `${ds.entities.values.length} cams near view${j.total ? ` · ${j.total} in area` : ''}`, 'ok'); } finally { inFlight = false; } } return { id: 'windycams' }; }