New js/layers/fires.js — fetches EONET open wildfire events directly (ACAO:*), refreshes every 15 min, rebuilt wholesale (no time dynamics — EONET events are slow-moving). Uses the latest dated geometry per event; Point coords direct, Polygon reduced to first-ring centroid. Flame-teardrop billboard (new lib.flameGlyph, drawn in #ff7a1a with dark outline, shared canvas), aggressive label translucency so dense California/Australia clusters stay legible, cap 500. HUD row "Wildfires (NASA EONET)". PITFALL handled: EONET mislabels Content-Type as application/rss+xml but the body is JSON — we call res.json() and never gate on the header. Verified in browser: 500 fires, all finite positions, glyphs render over North America, zero console errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
153 lines
5.4 KiB
JavaScript
153 lines
5.4 KiB
JavaScript
// GODSIGH bootstrap: viewer, basemaps, clock, live/scrub gating, picking, and
|
|
// dynamic layer loading.
|
|
//
|
|
// LAYER CONTRACT — each module in js/layers/*.js default-exports a factory:
|
|
//
|
|
// export default function create(ctx) {
|
|
// const { viewer, CONFIG, lib, ui, Cesium, start, stop, now } = ctx;
|
|
// // register HUD row(s) with ui.addLayer(id, name, defaultOn, onToggle)
|
|
// // report progress/failure with ui.setStatus(id, text, 'ok'|'warn'|'err')
|
|
// return {
|
|
// id: 'x', // string
|
|
// onClockTick(currentTime, isLive) {}, // optional; called ~1/s
|
|
// handlePick(picked) { return false; }, // optional; true if it consumed the click
|
|
// clearPick() {}, // optional; hide any custom overlay
|
|
// };
|
|
// }
|
|
|
|
import { CONFIG } from './config.js';
|
|
import * as lib from './lib.js';
|
|
import * as ui from './ui.js';
|
|
|
|
const Cesium = window.Cesium;
|
|
|
|
// ---- clock window: now-6h .. now+6h ----
|
|
const now = Cesium.JulianDate.now();
|
|
const start = Cesium.JulianDate.addHours(now, -CONFIG.windowHours, new Cesium.JulianDate());
|
|
const stop = Cesium.JulianDate.addHours(now, CONFIG.windowHours, new Cesium.JulianDate());
|
|
|
|
const viewer = new Cesium.Viewer('cesiumContainer', {
|
|
baseLayer: false, // we manage imagery ourselves — no Cesium ion token
|
|
baseLayerPicker: false,
|
|
geocoder: false,
|
|
sceneModePicker: false,
|
|
navigationHelpButton: false,
|
|
homeButton: false,
|
|
fullscreenButton: false,
|
|
timeline: true,
|
|
animation: true,
|
|
infoBox: true,
|
|
selectionIndicator: true,
|
|
shouldAnimate: true,
|
|
});
|
|
|
|
// ---- basemaps ----
|
|
const imagery = viewer.imageryLayers;
|
|
const darkLayer = imagery.addImageryProvider(new Cesium.UrlTemplateImageryProvider({
|
|
url: CONFIG.basemaps.dark.url,
|
|
subdomains: CONFIG.basemaps.dark.subdomains,
|
|
credit: CONFIG.basemaps.dark.credit,
|
|
maximumLevel: CONFIG.basemaps.dark.maximumLevel,
|
|
}));
|
|
const photoLayer = imagery.addImageryProvider(new Cesium.UrlTemplateImageryProvider({
|
|
url: CONFIG.basemaps.photo.url,
|
|
credit: CONFIG.basemaps.photo.credit,
|
|
maximumLevel: CONFIG.basemaps.photo.maximumLevel,
|
|
}));
|
|
photoLayer.show = false;
|
|
|
|
function setMode(mode) {
|
|
const photo = mode === 'photo';
|
|
photoLayer.show = photo;
|
|
darkLayer.show = !photo;
|
|
// Data Mode = flat high-contrast asset map (always readable).
|
|
// Photo Mode = realistic imagery with a live day/night terminator.
|
|
viewer.scene.globe.enableLighting = photo;
|
|
viewer.scene.globe.showGroundAtmosphere = photo;
|
|
document.getElementById('mode-data').classList.toggle('active', !photo);
|
|
document.getElementById('mode-photo').classList.toggle('active', photo);
|
|
}
|
|
document.getElementById('mode-data').addEventListener('click', () => setMode('data'));
|
|
document.getElementById('mode-photo').addEventListener('click', () => setMode('photo'));
|
|
|
|
// ---- clock configuration ----
|
|
viewer.clock.startTime = start.clone();
|
|
viewer.clock.stopTime = stop.clone();
|
|
viewer.clock.currentTime = now.clone();
|
|
viewer.clock.clockRange = Cesium.ClockRange.CLAMPED;
|
|
viewer.clock.multiplier = 1;
|
|
viewer.timeline.zoomTo(start, stop);
|
|
|
|
// ---- camera opens on the Gulf ----
|
|
viewer.camera.setView({
|
|
destination: Cesium.Cartesian3.fromDegrees(CONFIG.camera.lon, CONFIG.camera.lat, CONFIG.camera.height),
|
|
});
|
|
|
|
// ---- context handed to every layer factory ----
|
|
const ctx = { viewer, CONFIG, lib, ui, Cesium, start, stop, now };
|
|
|
|
// ---- dynamic layer loading (order = draw/registration order in the HUD) ----
|
|
const LAYER_MODULES = [
|
|
'./layers/satellites.js',
|
|
'./layers/infra.js',
|
|
'./layers/events.js',
|
|
'./layers/quakes.js',
|
|
'./layers/fires.js',
|
|
'./layers/ships.js',
|
|
'./layers/aircraft.js',
|
|
];
|
|
const activeLayers = [];
|
|
|
|
async function loadLayers() {
|
|
const results = await Promise.allSettled(LAYER_MODULES.map((p) => import(p)));
|
|
results.forEach((r, i) => {
|
|
if (r.status === 'rejected') {
|
|
console.error(`[godsigh] layer ${LAYER_MODULES[i]} failed to load:`, r.reason);
|
|
return;
|
|
}
|
|
const factory = r.value.default;
|
|
if (typeof factory !== 'function') {
|
|
console.warn(`[godsigh] ${LAYER_MODULES[i]} has no default export factory yet`);
|
|
return;
|
|
}
|
|
try {
|
|
const layer = factory(ctx);
|
|
if (layer) activeLayers.push(layer);
|
|
} catch (err) {
|
|
console.error(`[godsigh] layer ${LAYER_MODULES[i]} threw during init:`, err);
|
|
}
|
|
});
|
|
}
|
|
|
|
// ---- live/scrub gating (throttled ~1/s) ----
|
|
let lastTickMs = 0;
|
|
viewer.clock.onTick.addEventListener((clock) => {
|
|
const curMs = Cesium.JulianDate.toDate(clock.currentTime).getTime();
|
|
if (lastTickMs && Math.abs(curMs - lastTickMs) < 1000) return;
|
|
lastTickMs = curMs;
|
|
const isLive = Math.abs(curMs - Date.now()) <= CONFIG.liveThresholdSec * 1000;
|
|
ui.setLiveChip(isLive);
|
|
for (const l of activeLayers) l.onClockTick?.(clock.currentTime, isLive);
|
|
});
|
|
|
|
// ---- picking: aircraft (primitives) get a custom overlay; entities use InfoBox ----
|
|
const clickHandler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
|
|
clickHandler.setInputAction((movement) => {
|
|
const picked = viewer.scene.pick(movement.position);
|
|
let consumed = false;
|
|
for (const l of activeLayers) {
|
|
if (l.handlePick?.(picked)) { consumed = true; break; }
|
|
}
|
|
if (consumed) {
|
|
viewer.selectedEntity = undefined; // suppress InfoBox when a layer handled it
|
|
} else {
|
|
for (const l of activeLayers) l.clearPick?.();
|
|
}
|
|
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
|
|
|
|
setMode('data');
|
|
ui.setLiveChip(true);
|
|
loadLayers();
|
|
|
|
window.__godsigh = { viewer, ctx, activeLayers }; // debug handle
|