// 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'; import { REGISTRY } from './registry.js'; import { createGeoJsonLayer } from './layers/geojson-layer.js'; import { ADSB_LAYERS } from './adsb-registry.js'; import { createAdsbLayer } from './layers/adsb-layer.js'; import initSolarSystem from './solarsystem.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; let currentMode = 'data'; function setMode(mode) { const photo = mode === 'photo'; currentMode = photo ? 'photo' : 'data'; 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')); // ---- portal to SOLARGOD (sibling orrery) ---- // Deep-links to the solar-system app at GODSIGH's current sim moment via its // shared `#t=@` convention; the href is rebuilt fresh at click time. ui.wireSolargodLink(() => Cesium.JulianDate.toDate(viewer.clock.currentTime).getTime()); // ---- 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); // ---- shareable URL state (#c=lon,lat,height,heading,pitch&m=d|p&L=on,ids&t=offsetSec) ---- function clampToWindow(jd) { if (Cesium.JulianDate.lessThan(jd, start)) return start.clone(); if (Cesium.JulianDate.greaterThan(jd, stop)) return stop.clone(); return jd; } let pendingLayerList = null; // ON-layer ids from the hash, applied after layers load function applyHashState() { const state = { cameraApplied: false, modeApplied: false }; const raw = location.hash.replace(/^#/, ''); if (!raw) return state; const params = new URLSearchParams(raw); const c = params.get('c'); if (c) { const parts = c.split(','); const p = parts.map(Number); // Reject blank tokens too — Number('') is 0, which would silently apply a // bogus camera from a truncated hash instead of falling back to the default. if (parts.length === 5 && parts.every((s) => s.trim() !== '') && p.every(Number.isFinite)) { viewer.camera.setView({ destination: Cesium.Cartesian3.fromDegrees(p[0], p[1], p[2]), orientation: { heading: Cesium.Math.toRadians(p[3]), pitch: Cesium.Math.toRadians(p[4]), roll: 0 }, }); state.cameraApplied = true; } } const m = params.get('m'); if (m === 'p') { setMode('photo'); state.modeApplied = true; } else if (m === 'd') { setMode('data'); state.modeApplied = true; } const t = params.get('t'); if (t !== null && t !== '' && Number.isFinite(Number(t))) { viewer.clock.currentTime = clampToWindow( Cesium.JulianDate.addSeconds(now, Number(t), new Cesium.JulianDate())); } if (params.has('L')) pendingLayerList = params.get('L'); return state; } // Parse once at boot; malformed hashes are ignored silently. let hashState = { cameraApplied: false, modeApplied: false }; try { hashState = applyHashState(); } catch (err) { /* ignore malformed hash */ } // Default camera opens on the Gulf — only when the hash didn't provide one. if (!hashState.cameraApplied) { viewer.camera.setView({ destination: Cesium.Cartesian3.fromDegrees(CONFIG.camera.lon, CONFIG.camera.lat, CONFIG.camera.height), }); } function serializeState() { const cam = viewer.camera; const carto = cam.positionCartographic; const lon = Cesium.Math.toDegrees(carto.longitude).toFixed(3); const lat = Cesium.Math.toDegrees(carto.latitude).toFixed(3); const height = Math.round(carto.height); const heading = (((Cesium.Math.toDegrees(cam.heading) % 360) + 360) % 360).toFixed(3); const pitch = Cesium.Math.toDegrees(cam.pitch).toFixed(3); const mode = currentMode === 'photo' ? 'p' : 'd'; const onLayers = ui.getLayerIds().filter((id) => ui.getLayerChecked(id)); const curMs = Cesium.JulianDate.toDate(viewer.clock.currentTime).getTime(); const offset = Math.round((curMs - Date.now()) / 1000); const t = Math.abs(offset) <= CONFIG.liveThresholdSec ? 0 : offset; // 0 = live return `#c=${lon},${lat},${height},${heading},${pitch}&m=${mode}&L=${onLayers.join(',')}&t=${t}`; } function startHashSync() { // Poll once a second; rewrite only when the serialized state actually changed // (replaceState keeps it out of history). Covers camera, mode, layers, scrub. setInterval(() => { const hash = serializeState(); if (hash !== location.hash) history.replaceState(null, '', hash); }, 1000); } // ---- 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/ships.js', './layers/aircraft.js', './layers/military.js', './layers/radius.js', './layers/celestial.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); } }); // Registry-driven layers (Wave 3): data-defined GeoJSON feeds. for (const spec of REGISTRY) { try { const layer = createGeoJsonLayer(ctx, spec); if (layer) activeLayers.push(layer); } catch (err) { console.error(`[godsigh] registry layer ${spec.id} threw during init:`, err); } } // ADS-B OSINT layers (Wave 3.1): emergency squawks, rare-type hunter, shadow. for (const spec of ADSB_LAYERS) { try { const layer = createAdsbLayer(ctx, spec); if (layer) activeLayers.push(layer); } catch (err) { console.error(`[godsigh] adsb layer ${spec.id} 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); if (!hashState.modeApplied) setMode('data'); ui.setLiveChip(true); loadLayers().then(() => { // Restore layer on/off from the hash once every layer has registered its row. if (pendingLayerList !== null) { const wanted = new Set(pendingLayerList.split(',').filter(Boolean)); for (const id of ui.getLayerIds()) ui.setLayerChecked(id, wanted.has(id)); } startHashSync(); }); // Solar System mode ("Travel to…") — a separate mode, not a layer. const solarSystem = initSolarSystem(ctx); window.__godsigh = { viewer, ctx, activeLayers, solarSystem }; // debug handle