// 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/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