// NASA FIRMS layer (Wave 6.1) — raw VIIRS S-NPP satellite fire detections, // worldwide, last 24 h. This is the sensor-level truth EONET curates from: // every thermal anomaly the satellite saw, colored/sized by fire radiative // power. CSV comes via proxy/firms (key injected server-side, 1 h cache). export default function create(ctx) { const { viewer, Cesium, ui } = ctx; const pts = viewer.scene.primitives.add(new Cesium.PointPrimitiveCollection()); pts.show = false; let enabled = false; let loaded = false; let timer = null; const REFRESH_MS = 1800000; // 30 min; server caches 1 h anyway const DIM = Cesium.Color.fromCssColorString('#b3572c').withAlpha(0.8); const MID = Cesium.Color.fromCssColorString('#ff7a2f').withAlpha(0.9); const HOT = Cesium.Color.fromCssColorString('#ffd24a'); const OUTLINE = Cesium.Color.BLACK; ui.addLayer('firms', 'Fire hotspots (NASA FIRMS)', false, (on) => { enabled = on; pts.show = on; if (on && !loaded) refresh(); if (on && !timer) timer = setInterval(refresh, REFRESH_MS); if (!on && timer) { clearInterval(timer); timer = null; } if (!on) { ui.hidePickOverlay(); ui.setStatus('firms', '', 'ok'); } else if (loaded) ui.setStatus('firms', statusText(), 'ok'); }, 'Earth & hazards'); function statusText() { return `${pts.length} detections · 24h · VIIRS`; } async function refresh() { if (!loaded) ui.setStatus('firms', 'fetching detections…', 'warn'); let res; try { res = await fetch('proxy/firms'); } catch { ui.setStatus('firms', 'network error', 'err'); return; } if (res.status === 503) { ui.setStatus('firms', 'no key — add firmscredentials.json', 'err'); return; } if (!res.ok) { ui.setStatus('firms', `HTTP ${res.status}`, 'warn'); return; } const text = await res.text(); const lines = text.trim().split('\n'); const head = (lines.shift() || '').split(','); const col = (n) => head.indexOf(n); const iLat = col('latitude'), iLon = col('longitude'), iFrp = col('frp'); const iBright = col('bright_ti4'), iDate = col('acq_date'), iTime = col('acq_time'); const iSat = col('satellite'), iDn = col('daynight'), iConf = col('confidence'); if (iLat < 0 || iLon < 0) { ui.setStatus('firms', 'unexpected CSV shape', 'err'); return; } pts.removeAll(); for (const line of lines) { const c = line.split(','); const lat = +c[iLat], lon = +c[iLon], frp = +c[iFrp] || 0; if (!isFinite(lat) || !isFinite(lon)) continue; pts.add({ position: Cesium.Cartesian3.fromDegrees(lon, lat), pixelSize: frp > 50 ? 6 : frp > 5 ? 4 : 3, color: frp > 50 ? HOT : frp > 5 ? MID : DIM, outlineColor: OUTLINE, outlineWidth: 1, disableDepthTestDistance: 50000, id: { layer: 'firms', frp, bright: +c[iBright] || null, date: c[iDate], time: c[iTime], sat: c[iSat], dn: c[iDn], conf: c[iConf], }, }); } loaded = true; if (enabled) ui.setStatus('firms', statusText(), 'ok'); } function handlePick(picked) { if (!enabled || picked?.id?.layer !== 'firms') return false; const d = picked.id; const hhmm = String(d.time || '').padStart(4, '0'); ui.showPickOverlay('🛰 VIIRS fire detection', [ ['FRP', d.frp ? `${d.frp.toFixed(1)} MW` : '—'], ['Brightness', d.bright ? `${d.bright.toFixed(0)} K` : '—'], ['Seen', `${d.date || '—'} ${hhmm.slice(0, 2)}:${hhmm.slice(2)} UTC`], ['Satellite', `${d.sat === 'N' ? 'Suomi NPP' : d.sat || '—'} · ${d.dn === 'D' ? 'day' : 'night'}`], ['Confidence', { l: 'low', n: 'nominal', h: 'high' }[d.conf] || d.conf || '—'], ]); return true; } function clearPick() { ui.hidePickOverlay(); } return { id: 'firms', handlePick, clearPick }; }