// Layer registry (Wave 3) — one object literal = one working layer, rendered by // createGeoJsonLayer (js/layers/geojson-layer.js). Adding a public GeoJSON-ish // feed is a data-entry task here, not new code. Bespoke layers with real // per-layer logic (satellites, aircraft, ships, military, infra, events) stay as // their own modules in main.js's LAYER_MODULES — the registry is additive. // // Each entry's style(f, ctx) / description(f, ctx) / status(...) receive the // live ctx, so they reach Cesium, lib (cz/escapeHtml/safeUrl/flameGlyph), CONFIG. // One shared flame canvas across every fire billboard (Cesium atlases it). let _fireGlyph = null; const fireGlyph = (ctx) => (_fireGlyph ||= ctx.lib.flameGlyph(ctx.CONFIG.colors.fire)); const iso = (s) => { const d = new Date(s); return isNaN(d) ? '—' : d.toISOString().replace('T', ' ').replace('.000Z', ' UTC'); }; export const REGISTRY = [ // ─────────────────────────── Earth ─────────────────────────── { id: 'quakes', name: 'Earthquakes (USGS)', category: 'Earth', defaultOn: true, url: 'https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_day.geojson', refreshMs: 300000, cap: 400, adapt: (j) => j.features, usable: (f) => { const m = f && f.properties && f.properties.mag; const c = f && f.geometry && f.geometry.coordinates; return typeof m === 'number' && isFinite(m) && Array.isArray(c) && isFinite(c[0]) && isFinite(c[1]) && f.id != null; }, featureId: (f) => f.id, sig: (f) => { const p = f.properties || {}; const c = (f.geometry || {}).coordinates || []; return `${p.mag}|${p.place}|${c[2]}|${p.time}`; }, timeAt: (f) => f.properties.time, sortKey: (f) => f.properties.mag, style: (f, { Cesium, lib, CONFIG }) => { const mag = f.properties.mag; const t = Cesium.Math.clamp((mag - 2) / (6 - 2), 0, 1); const color = Cesium.Color.lerp(lib.cz(CONFIG.colors.quakeMin), lib.cz(CONFIG.colors.quakeMax), t, new Cesium.Color()); const base = Math.max(3, 4 + mag * 2.2); const timeMs = f.properties.time; const recent = typeof timeMs === 'number' && (Date.now() - timeMs) < 3.6e6; const pixelSize = recent ? new Cesium.CallbackProperty(() => ((Date.now() - timeMs) < 3.6e6 ? base + 3 * Math.sin(Date.now() / 300) : base), false) : base; const s = { point: { pixelSize, color, outlineColor: Cesium.Color.BLACK, outlineWidth: 1, disableDepthTestDistance: Number.POSITIVE_INFINITY } }; if (mag >= 4.5) s.label = { text: `M${mag.toFixed(1)} ${f.properties.place || ''}`, fade: [2.0e6, 1.2e7] }; return s; }, description: (f, { lib }) => { const p = f.properties || {}; const c = (f.geometry || {}).coordinates || []; const url = lib.safeUrl(p.url); return `
| Magnitude | M${(p.mag ?? 0).toFixed(1)} |
|---|---|
| Place | ${lib.escapeHtml(p.place || '—')} |
| Depth | ${c[2] != null ? c[2].toFixed(1) + ' km' : '—'} |
| Time (UTC) | ${p.time != null ? iso(p.time) : '—'} |
| Event | ${lib.escapeHtml(f.title || '—')} |
|---|---|
| Category | ${lib.escapeHtml(cat)} |
Active wildfire event from NASA EONET (real data).
`; }, status: ({ shown }) => `${shown} active fires`, }, // ─────────────────────────── Human ─────────────────────────── { id: 'gdacs', name: 'Disasters (GDACS)', category: 'Human', defaultOn: false, url: 'https://www.gdacs.org/gdacsapi/api/events/geteventlist/MAP', refreshMs: 900000, cap: 300, adapt: (j) => j.features, timeAt: (f) => f.properties && f.properties.fromdate, // clamps into the window style: (f, { Cesium, lib }) => { const p = f.properties || {}; const c = { Green: '#46e08a', Orange: '#ff9f40', Red: '#ff453a' }[p.alertlevel] || '#9fb0bd'; return { point: { pixelSize: p.alertlevel === 'Red' ? 12 : 9, color: lib.cz(c), outlineColor: Cesium.Color.BLACK, outlineWidth: 1, disableDepthTestDistance: Number.POSITIVE_INFINITY }, label: { text: `${TYPE[p.eventtype] || p.eventtype || ''} ${p.name || p.eventname || ''}`.trim(), fillColor: lib.cz(c), fade: [1.5e6, 1.4e7] }, }; }, description: (f, { lib }) => { const p = f.properties || {}; return `| Event | ${lib.escapeHtml(p.name || p.eventname || '—')} |
|---|---|
| Type | ${lib.escapeHtml((TYPE[p.eventtype] || p.eventtype || '—'))} |
| Alert | ${lib.escapeHtml(p.alertlevel || '—')} |
| Since | ${p.fromdate ? iso(p.fromdate) : '—'} |
Global Disaster Alert & Coordination System (real data).
`; }, status: ({ shown }) => `${shown} active disasters`, }, { id: 'nws', name: 'Severe weather (US · NWS)', category: 'Human', defaultOn: false, url: 'https://api.weather.gov/alerts/active?severity=Severe', refreshMs: 300000, cap: 300, adapt: (j) => j.features, // Many alerts have null geometry (zone-only) — skip those (default usable does). timeAt: (f) => f.properties && f.properties.onset, style: (f, { Cesium, lib }) => ({ point: { pixelSize: 8, color: lib.cz('#ffcf50'), outlineColor: Cesium.Color.BLACK, outlineWidth: 1, disableDepthTestDistance: Number.POSITIVE_INFINITY }, label: { text: (f.properties && f.properties.event) || 'Alert', fillColor: lib.cz('#ffcf50'), fade: [4.0e5, 4.0e6] }, }), description: (f, { lib }) => { const p = f.properties || {}; return `| Event | ${lib.escapeHtml(p.event || '—')} |
|---|---|
| Severity | ${lib.escapeHtml(p.severity || '—')} |
| Area | ${lib.escapeHtml((p.areaDesc || '—').slice(0, 120))} |
| Onset | ${p.onset ? iso(p.onset) : '—'} |
${lib.escapeHtml((p.headline || '').slice(0, 200))}
` + `US National Weather Service (real data; US only).
`; }, status: ({ shown }) => `${shown} severe alerts (US)`, }, // ─────────────────────────── Space ─────────────────────────── { id: 'launches', name: 'Rocket launches', category: 'Space', defaultOn: false, url: 'https://ll.thespacedevs.com/2.2.0/launch/upcoming/?limit=30', refreshMs: 1800000, // 30 min — LL2 free tier is rate-limited; launches move slowly adapt: (j) => j.results, // NOT time-anchored: most `net` are beyond the +6h window; render statically // at the pad with a T- countdown in the label instead. locate: (r) => { const pad = r.pad || {}; const lon = parseFloat(pad.longitude), lat = parseFloat(pad.latitude); return isFinite(lon) && isFinite(lat) ? { lon, lat } : null; }, style: (r, { Cesium, lib }) => ({ point: { pixelSize: 9, color: lib.cz('#35e0ff'), outlineColor: Cesium.Color.BLACK, outlineWidth: 1, disableDepthTestDistance: Number.POSITIVE_INFINITY }, label: { text: `🚀 ${countdown(r.net)}`, fillColor: lib.cz('#35e0ff'), fade: [8.0e5, 2.0e7] }, }), description: (r, { lib }) => { const pad = r.pad || {}; const loc = (pad.location || {}).name || ''; return `| Mission | ${lib.escapeHtml(r.name || '—')} |
|---|---|
| When (UTC) | ${r.net ? iso(r.net) : '—'} |
| Pad | ${lib.escapeHtml(pad.name || '—')} |
| Site | ${lib.escapeHtml(loc)} |
| Status | ${lib.escapeHtml((r.status || {}).abbrev || '—')} |
Upcoming launch — The Space Devs / Launch Library 2.
`; }, status: ({ shown }) => `${shown} upcoming launches`, }, // ──────────────────── Regional — Australia ──────────────────── { id: 'rfs', name: 'NSW bushfires (RFS)', category: 'Regional — Australia', defaultOn: false, url: 'https://www.rfs.nsw.gov.au/feeds/majorIncidents.json', refreshMs: 600000, cap: 300, adapt: (j) => j.features, // features carry GeometryCollection — default locate handles it style: (f, { Cesium, lib }) => { const cat = (f.properties && f.properties.category) || ''; const c = { 'Emergency Warning': '#ff453a', 'Watch and Act': '#ff9f40', 'Advice': '#ffcf50' }[cat] || '#9fb0bd'; return { point: { pixelSize: cat === 'Emergency Warning' ? 12 : 9, color: lib.cz(c), outlineColor: Cesium.Color.BLACK, outlineWidth: 1, disableDepthTestDistance: Number.POSITIVE_INFINITY }, label: { text: (f.properties && f.properties.title) || 'Incident', fillColor: lib.cz(c), fade: [2.0e5, 3.0e6] }, }; }, description: (f, { lib }) => { const p = f.properties || {}; // description is HTML from a GeoRSS feed — strip tags, then escape. const txt = String(p.description || '').replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim(); return `| Incident | ${lib.escapeHtml(p.title || '—')} |
|---|---|
| Alert | ${lib.escapeHtml(p.category || '—')} |
${lib.escapeHtml(txt.slice(0, 300))}
` + `NSW Rural Fire Service — current major incidents (real data).
`; }, status: ({ shown }) => `${shown} NSW incidents`, }, ]; // GDACS event-type codes → readable prefix. const TYPE = { EQ: '🌍 Quake', TC: '🌀 Cyclone', FL: '🌊 Flood', DR: '🏜 Drought', VO: '🌋 Volcano', WF: '🔥 Wildfire', TS: '🌊 Tsunami' }; function countdown(net) { const t = new Date(net).getTime(); if (isNaN(t)) return 'launch'; const dm = Math.round((t - Date.now()) / 60000); if (dm < 0) return 'launched'; if (dm < 60) return `T-${dm}m`; if (dm < 1440) return `T-${Math.round(dm / 60)}h`; return `T-${Math.round(dm / 1440)}d`; }