Every marker used disableDepthTestDistance: Infinity, so points/billboards on the FAR side of the Earth drew on top of the near hemisphere instead of being occluded — reading as 'dots stuck, not moving with the globe' when you rotate. Changed to 50000 (50km) across all layers: the globe now occludes far-side markers, while you can still zoom right up to one without it clipping into the surface. Also made the billboard layers (aircraft/military/adsb/radius) explicit for consistent behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
223 lines
13 KiB
JavaScript
223 lines
13 KiB
JavaScript
// 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: 50000 } };
|
|
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 `<table class="cesium-infoBox-defaultTable"><tbody>` +
|
|
`<tr><th>Magnitude</th><td>M${(p.mag ?? 0).toFixed(1)}</td></tr>` +
|
|
`<tr><th>Place</th><td>${lib.escapeHtml(p.place || '—')}</td></tr>` +
|
|
`<tr><th>Depth</th><td>${c[2] != null ? c[2].toFixed(1) + ' km' : '—'}</td></tr>` +
|
|
`<tr><th>Time (UTC)</th><td>${p.time != null ? iso(p.time) : '—'}</td></tr></tbody></table>` +
|
|
(url ? `<p><a href="${lib.escapeHtml(url)}" target="_blank" rel="noopener">USGS event page ↗</a></p>` : '');
|
|
},
|
|
status: ({ shown, features }) => {
|
|
const max = features.reduce((m, f) => Math.max(m, f.properties.mag), -Infinity);
|
|
return `${shown} quakes${features.length ? ` · max M${max.toFixed(1)}` : ''} · 24h`;
|
|
},
|
|
},
|
|
|
|
{
|
|
id: 'fires', name: 'Wildfires (NASA EONET)', category: 'Earth', defaultOn: true,
|
|
url: 'https://eonet.gsfc.nasa.gov/api/v3/events?category=wildfires&status=open&limit=500',
|
|
refreshMs: 900000, cap: 500,
|
|
adapt: (j) => j.events, // NOTE: EONET mislabels Content-Type rss+xml; res.json() still parses
|
|
// EONET events carry a dated `geometry` ARRAY (not a GeoJSON feature.geometry).
|
|
locate: (ev) => {
|
|
const geoms = Array.isArray(ev.geometry) ? ev.geometry : [];
|
|
let latest = geoms[0];
|
|
for (const g of geoms) if (g.date && latest.date && g.date > latest.date) latest = g;
|
|
if (!latest) return null;
|
|
const c = latest.coordinates;
|
|
if (latest.type === 'Point' && Array.isArray(c) && isFinite(c[0]) && isFinite(c[1])) return { lon: c[0], lat: c[1] };
|
|
if (latest.type === 'Polygon' && Array.isArray(c) && Array.isArray(c[0])) {
|
|
let sx = 0, sy = 0, n = 0;
|
|
for (const pt of c[0]) if (Array.isArray(pt) && isFinite(pt[0]) && isFinite(pt[1])) { sx += pt[0]; sy += pt[1]; n++; }
|
|
return n ? { lon: sx / n, lat: sy / n } : null;
|
|
}
|
|
return null;
|
|
},
|
|
style: (f, ctx) => ({
|
|
billboard: { image: fireGlyph(ctx), width: 14, height: 14, verticalOrigin: ctx.Cesium.VerticalOrigin.BOTTOM, disableDepthTestDistance: 50000 },
|
|
label: { text: f.title || 'Wildfire', fillColor: ctx.lib.cz(ctx.CONFIG.colors.fire), fade: [3.0e5, 1.5e6] },
|
|
}),
|
|
description: (f, { lib }) => {
|
|
const cat = (f.categories && f.categories[0] && f.categories[0].title) || 'Wildfires';
|
|
const link = lib.safeUrl(f.link || (f.id ? `https://eonet.gsfc.nasa.gov/api/v3/events/${encodeURIComponent(f.id)}` : ''));
|
|
return `<table class="cesium-infoBox-defaultTable"><tbody>` +
|
|
`<tr><th>Event</th><td>${lib.escapeHtml(f.title || '—')}</td></tr>` +
|
|
`<tr><th>Category</th><td>${lib.escapeHtml(cat)}</td></tr></tbody></table>` +
|
|
(link ? `<p><a href="${lib.escapeHtml(link)}" target="_blank" rel="noopener">Open event on EONET ↗</a></p>` : '') +
|
|
`<p style="opacity:.7">Active wildfire event from NASA EONET (real data).</p>`;
|
|
},
|
|
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: 50000 },
|
|
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 `<table class="cesium-infoBox-defaultTable"><tbody>` +
|
|
`<tr><th>Event</th><td>${lib.escapeHtml(p.name || p.eventname || '—')}</td></tr>` +
|
|
`<tr><th>Type</th><td>${lib.escapeHtml((TYPE[p.eventtype] || p.eventtype || '—'))}</td></tr>` +
|
|
`<tr><th>Alert</th><td>${lib.escapeHtml(p.alertlevel || '—')}</td></tr>` +
|
|
`<tr><th>Since</th><td>${p.fromdate ? iso(p.fromdate) : '—'}</td></tr></tbody></table>` +
|
|
`<p style="opacity:.7">Global Disaster Alert & Coordination System (real data).</p>`;
|
|
},
|
|
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: 50000 },
|
|
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 `<table class="cesium-infoBox-defaultTable"><tbody>` +
|
|
`<tr><th>Event</th><td>${lib.escapeHtml(p.event || '—')}</td></tr>` +
|
|
`<tr><th>Severity</th><td>${lib.escapeHtml(p.severity || '—')}</td></tr>` +
|
|
`<tr><th>Area</th><td>${lib.escapeHtml((p.areaDesc || '—').slice(0, 120))}</td></tr>` +
|
|
`<tr><th>Onset</th><td>${p.onset ? iso(p.onset) : '—'}</td></tr></tbody></table>` +
|
|
`<p>${lib.escapeHtml((p.headline || '').slice(0, 200))}</p>` +
|
|
`<p style="opacity:.7">US National Weather Service (real data; US only).</p>`;
|
|
},
|
|
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: 50000 },
|
|
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 `<table class="cesium-infoBox-defaultTable"><tbody>` +
|
|
`<tr><th>Mission</th><td>${lib.escapeHtml(r.name || '—')}</td></tr>` +
|
|
`<tr><th>When (UTC)</th><td>${r.net ? iso(r.net) : '—'}</td></tr>` +
|
|
`<tr><th>Pad</th><td>${lib.escapeHtml(pad.name || '—')}</td></tr>` +
|
|
`<tr><th>Site</th><td>${lib.escapeHtml(loc)}</td></tr>` +
|
|
`<tr><th>Status</th><td>${lib.escapeHtml((r.status || {}).abbrev || '—')}</td></tr></tbody></table>` +
|
|
`<p style="opacity:.7">Upcoming launch — The Space Devs / Launch Library 2.</p>`;
|
|
},
|
|
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: 50000 },
|
|
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 `<table class="cesium-infoBox-defaultTable"><tbody>` +
|
|
`<tr><th>Incident</th><td>${lib.escapeHtml(p.title || '—')}</td></tr>` +
|
|
`<tr><th>Alert</th><td>${lib.escapeHtml(p.category || '—')}</td></tr></tbody></table>` +
|
|
`<p>${lib.escapeHtml(txt.slice(0, 300))}</p>` +
|
|
`<p style="opacity:.7">NSW Rural Fire Service — current major incidents (real data).</p>`;
|
|
},
|
|
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`;
|
|
}
|