// Wildfires layer (Wave 2) — REAL data from NASA's EONET events API. // No time dynamics: EONET events are slow-moving, so availability games would // mislead more than inform. Refreshed every 15 min, rebuilt wholesale. // // PITFALL baked in: EONET mislabels its Content-Type as `application/rss+xml`, // but the body is JSON. fetch's res.json() ignores the header, so it works — // we simply never gate on the content type. export default function create(ctx) { const { viewer, CONFIG, lib, ui, Cesium } = ctx; const F = CONFIG.fires; const ds = new Cesium.CustomDataSource('fires'); viewer.dataSources.add(ds); const glyph = lib.flameGlyph(CONFIG.colors.fire); // one shared canvas const fireColor = lib.cz(CONFIG.colors.fire); let enabled = true; let timer = null; let inFlight = false; ui.addLayer('fires', 'Wildfires (NASA EONET)', true, (on) => { enabled = on; ds.show = on; }); ui.setStatus('fires', 'loading EONET feed…', 'warn'); // Pick the most recent dated geometry and reduce it to a {lon, lat, date}. function locate(event) { const geoms = Array.isArray(event.geometry) ? event.geometry : []; if (!geoms.length) return null; // Latest by date (EONET lists chronologically; be robust to ordering). let latest = geoms[0]; for (const gm of geoms) { if (gm.date && latest.date && gm.date > latest.date) latest = gm; } const c = latest.coordinates; if (latest.type === 'Point' && Array.isArray(c) && isFinite(c[0]) && isFinite(c[1])) { return { lon: c[0], lat: c[1], date: latest.date }; } if (latest.type === 'Polygon' && Array.isArray(c) && Array.isArray(c[0])) { const ring = c[0]; let sx = 0, sy = 0, n = 0; for (const pt of ring) { if (Array.isArray(pt) && isFinite(pt[0]) && isFinite(pt[1])) { sx += pt[0]; sy += pt[1]; n++; } } if (n) return { lon: sx / n, lat: sy / n, date: latest.date }; } return null; } function build(events) { ds.entities.removeAll(); let count = 0; for (const ev of events) { if (count >= F.cap) break; const loc = locate(ev); if (!loc) continue; const title = ev.title || 'Wildfire'; const category = (ev.categories && ev.categories[0] && ev.categories[0].title) || 'Wildfires'; const link = lib.safeUrl(ev.link || (ev.id ? `https://eonet.gsfc.nasa.gov/api/v3/events/${encodeURIComponent(ev.id)}` : '')); const dateTxt = loc.date ? new Date(loc.date).toISOString().replace('T', ' ').replace('.000Z', ' UTC') : '—'; ds.entities.add({ name: title, position: Cesium.Cartesian3.fromDegrees(loc.lon, loc.lat), billboard: { image: glyph, width: 14, height: 14, verticalOrigin: Cesium.VerticalOrigin.BOTTOM, disableDepthTestDistance: Number.POSITIVE_INFINITY, }, label: { text: title, font: '11px "Segoe UI", system-ui, sans-serif', fillColor: fireColor, outlineColor: Cesium.Color.fromCssColorString('#0a0f14'), outlineWidth: 3, style: Cesium.LabelStyle.FILL_AND_OUTLINE, verticalOrigin: Cesium.VerticalOrigin.BOTTOM, pixelOffset: new Cesium.Cartesian2(0, -12), // Aggressive fade: fires cluster hard (California, Australia), so labels // only resolve once you've zoomed well into a region. translucencyByDistance: new Cesium.NearFarScalar(3.0e5, 1.0, 1.5e6, 0.0), disableDepthTestDistance: Number.POSITIVE_INFINITY, }, description: `` + `` + `` + `` + `
Event${lib.escapeHtml(title)}
Category${lib.escapeHtml(category)}
Last report${dateTxt}
` + (link ? `

Open event on EONET ↗

` : '') + `

Active wildfire event from NASA EONET (real data).

`, }); count++; } ui.setStatus('fires', `${count} active fires`, 'ok'); } async function poll() { let res; try { res = await fetch(F.url); } catch { ui.setStatus('fires', 'network error — retrying', 'warn'); return; } if (!res.ok) { ui.setStatus('fires', `HTTP ${res.status} — retrying`, 'warn'); return; } let data; try { // Body is JSON despite the rss+xml Content-Type — do NOT check the header. data = await res.json(); } catch { ui.setStatus('fires', 'bad response — retrying', 'err'); return; } const events = Array.isArray(data && data.events) ? data.events : []; build(events); } async function tick() { timer = null; if (inFlight) return; inFlight = true; try { await poll(); } finally { inFlight = false; } timer = setTimeout(tick, F.refreshMs); } tick(); return { id: 'fires' }; }