wave4: upstream open-source port — submarine cables + datacenters + dams (bundled), NVG/FLIR/CRT sensor styles, dead-reckoned smooth motion, COMPARISON.md (fable)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-08-25 12:46:41 +10:00
parent 092fe5254a
commit 3f64f7ed4e
17 changed files with 445 additions and 6 deletions

2
.gitignore vendored
View File

@ -10,3 +10,5 @@ openskycredentials.json
.env
# Generated by ais_bridge.py — live data, never committed.
ais_snapshot.json
# Vendored upstream open-source release (reference only, 88MB — never commit)
gods-eye-view-main/

71
COMPARISON.md Normal file
View File

@ -0,0 +1,71 @@
# GODSIGH vs. the open-sourced God's Eye View
*2026-08-25 — Bilawal Sidhu open-sourced the real
[God's Eye View](https://github.com/bilawalsidhu/gods-eye-view) (vendored here
at `gods-eye-view-main/`, gitignored). GODSIGH was our guess at it, built from
the YouTube videos alone. This is the honest side-by-side, and the record of
what we ported in Wave 4.*
## Architecture — two different bets
| | GODSIGH (ours) | God's Eye View (theirs) |
|---|---|---|
| Size | ~2,400 lines, 12 modules | ~300 modules + per-module test files |
| Build | none — ES modules + 2 CDN globals | Vite, Node 24/26 enforced |
| Globe | CARTO dark / EOX Sentinel-2 raster (keyless) | **Google Photorealistic 3D Tiles** (metered 🔴 key required) |
| Entry fee | $0, zero keys | Google Maps key mandatory; OpenAI key for voice |
| Server | stdlib `serve.py` proxy + shared OpenSky cache | Vite dev server + key-brokering proxies, rate limits |
| Testing | none | huge `.test.mjs` suite + QA screenshot scripts |
| Focus | **one theater, one timeline** (Persian Gulf OSINT, scrubbable ±6h) | **the whole planet as a place** (photoreal, cinematic, voice-driven) |
The big divergence: we guessed "OSINT analysis console" (time scrubbing,
replay, choke points, dark vessels); the real one is an "experience" (cockpit
rides, voice agent, sensor skins, cinematic scenes). Their README even concedes
our corner: *"the moment you try to go back in time… the data gets expensive"*
— our timeline/replay/record.py stack is the thing they explicitly didn't build.
## Feature matrix
| Feature | Theirs | Ours before | Ours now (Wave 4) |
|---|---|---|---|
| Live aircraft (OpenSky + adsb.lol) | ✅ | ✅ | ✅ |
| Military ADS-B | ✅ | ✅ | ✅ |
| Satellites (SGP4) | ~840 + Starlink chip | ~97 curated recon/EO | unchanged (curated > bulk for our theater) |
| Earthquakes / fires / launches | ✅ | ✅ (+GDACS, NWS, RFS they lack) | ✅ |
| **Timeline scrub + aircraft replay** | ❌ | ✅ | ✅ — still our moat |
| Demo ship analytics (AIS-gap, toll route) | live AIS only | ✅ | ✅ |
| **Smooth motion between polls** | ✅ interpolation | ❌ planes teleported every 90 s | ✅ dead reckoning, aircraft + military |
| **Sensor styles (NVG/FLIR/CRT GLSL)** | ✅ 6 styles + uniforms | ❌ | ✅ 3 compact post-process styles, keys `14`, in share links |
| **Submarine cables** | ✅ bundled TeleGeography | ❌ | ✅ 712 cables + 1,917 landings |
| **Datacenters / dams** | ✅ bundled OSM | ❌ | ✅ 4,351 + 704 as registry layers |
| Voice agent (28 tools) | ✅ OpenAI Realtime 🔴 | ❌ | skipped — needs paid key, against our zero-key rule |
| Cockpit / contacts / 3D hangar | ✅ | ❌ | skipped — built on Google 3D tiles 🔴 |
| CCTV projected into 3D | ✅ | ❌ | skipped — same reason; flat raster has nothing to project onto |
| Traffic (TomTom), radio tuner, bikeshare | ✅ | ❌ | skipped/deferred — off-theater or key-gated |
| Detection overlay / military HUD | ✅ | ❌ | skipped — cosmetic without their photoreal globe |
## What Wave 4 ported (all keyless, no build step)
1. **Submarine cables** — their bundled TeleGeography GeoJSON, rendered by a
new [`js/layers/cables.js`](js/layers/cables.js). © TeleGeography,
CC BY-NC-SA 3.0 — non-commercial only, see [`assets/README.md`](assets/README.md).
2. **Datacenters + dams** — their OSM/Open Infrastructure Map snapshots,
collapsed to centroid points and dropped into the layer registry as two
data entries (ODbL, attributed). Off by default.
3. **Sensor styles** — NVG / FLIR-Ironbow / CRT as Cesium `PostProcessStage`
GLSL in [`js/styles.js`](js/styles.js), compacted from their style system.
Hotkeys `14`; serialized into share links as `s=`.
4. **Smooth motion** — their "render behind, interpolate" idea, done the lazy
way: `lib.deadReckon()` advances every billboard along its reported course
at 10 Hz between polls, live-only, capped at 5 min extrapolation.
## Worth stealing later
- **Trace-history backfill** — click a military contact, see its last ~24 h as
3D loops. Feasible keyless via OpenSky `/tracks` through `serve.py`.
- **First-run missions** — their staged "first five minutes" onboarding card.
- **Their test culture** — per-module `.test.mjs`; ours has none.
- **Radio layer** — Radio Browser API is keyless and geolocated; audio-in-
InfoBox needs a spike.
- Natural Earth named-region polygons (public domain) if we ever want
"outline the Strait of Hormuz"-style annotation.

View File

@ -1,6 +1,6 @@
# GODSIGH ▸ World View
A browser-based **3D/4D OSINT world view** — a CesiumJS digital-twin globe that fuses independent, public sensor feeds onto one scrubbable timeline. Satellite orbits, live aircraft, ship traffic, and energy infrastructure, all synced to a single space-time slider. Inspired by Bilawal Sidhu's *God's Eye View*.
A browser-based **3D/4D OSINT world view** — a CesiumJS digital-twin globe that fuses independent, public sensor feeds onto one scrubbable timeline. Satellite orbits, live aircraft, ship traffic, and energy infrastructure, all synced to a single space-time slider. Inspired by Bilawal Sidhu's *God's Eye View* — which has since been [open-sourced](https://github.com/bilawalsidhu/gods-eye-view); see [COMPARISON.md](COMPARISON.md) for our guess vs. the real thing, and what Wave 4 ported from it.
Everything runs client-side from free, public, **unclassified** data. No API keys are required. No Cesium ion token. No build step.
@ -24,6 +24,8 @@ A dark digital-twin globe with the built-in Cesium **timeline + animation widget
- **Data Mode** — a flat, high-contrast dark basemap for asset tracking (the default).
- **Photo Mode** — Sentinel-2 satellite imagery with a live day/night terminator.
Below those, **sensor styles** (keys `14`): full-screen GLSL post-process skins ported from the open-sourced God's Eye View — **NVG** night vision, **FLIR** Ironbow thermal, and **CRT** phosphor console. They compose with either render mode and travel in share links.
The HUD lists every layer with a live status line and an on/off toggle, plus a `LIVE / SCRUBBED` chip that shows when you've scrubbed off wall-clock-now (at which point the live-only layers pause to save quota).
## Layers & data sources
@ -31,7 +33,7 @@ The HUD lists every layer with a live status line and an on/off toggle, plus a `
| Layer | Source | Live? | Notes |
|---|---|---|---|
| **Satellites** (~97) | [Celestrak](https://celestrak.org) GP/TLE | ✅ deterministic | SGP4-propagated orbits with glowing paths; EO/recon birds incl. WorldView, Pléiades, Capella, SkySat, plus Chinese **Gaofen** and Russian **Cosmos** (Persona) military satellites. Click one for orbital elements. |
| **Aircraft** | [OpenSky Network](https://opensky-network.org) ADS-B | ✅ live | Thousands of live flights, colored by altitude band; **military callsigns highlighted** and filterable; click one for callsign/country/altitude/speed/heading. Scrub off-live and it **replays recorded traffic** (see below). |
| **Aircraft** | [OpenSky Network](https://opensky-network.org) ADS-B | ✅ live | Thousands of live flights, colored by altitude band; **military callsigns highlighted** and filterable; click one for callsign/country/altitude/speed/heading. Between polls every plane **dead-reckons smoothly along its course** instead of teleporting. Scrub off-live and it **replays recorded traffic** (see below). |
| **Earthquakes** | [USGS](https://earthquake.usgs.gov) GeoJSON | ✅ real | Last 24 h of quakes, time-anchored so each **appears as the slider crosses its origin time**; magnitude ramps amber→red, recent quakes pulse, M≥4.5 labeled. Click one for magnitude/depth/time and the USGS event link. |
| **Wildfires** | [NASA EONET](https://eonet.gsfc.nasa.gov) | ✅ real | Active wildfire events worldwide as flame glyphs; click one for the event details and EONET link. |
| **Military** | [adsb.lol](https://adsb.lol) `/v2/mil` | ✅ real | Unfiltered military ADS-B the civil feeds hide (Reach airlift, tankers, the Area 51 "Janet" shuttle). Free & keyless; emergency squawks highlighted. |
@ -41,6 +43,9 @@ The HUD lists every layer with a live status line and an on/off toggle, plus a `
| **NSW bushfires** | [NSW RFS](https://www.rfs.nsw.gov.au) | ✅ real | Current major bushfire incidents (Advice/Watch/Emergency). *(Regional — Australia, off by default)* |
| **Ships** `[DEMO]` | synthetic | — | Illustrative tanker traffic through the Strait of Hormuz, incl. a "toll-route" carrier, a **dark-vessel AIS gap**, and the Fujairah idle anchorage. Optional live AIS — see below. |
| **Infrastructure** | static vectors | — | Choke points (Hormuz, Bab el-Mandeb, Suez, Malacca), the EastWest Petroline & HabshanFujairah pipelines, and key oil/desal/base facilities. Coordinates approximate. |
| **Submarine cables** | [TeleGeography](https://www.submarinecablemap.com) (bundled) | — | 712 international cable systems in their map colors + 1,917 landing points. **CC BY-NC-SA — non-commercial only** (see `assets/README.md`). |
| **Datacenters** | OpenStreetMap (bundled) | — | 4,351 datacenter locations with operator, via Open Infrastructure Map (ODbL). *(off by default)* |
| **Dams** | OpenStreetMap (bundled) | — | 704 dams with hydro output where tagged (ODbL). *(off by default)* |
| **Events** `[DEMO]` | synthetic | — | Time-anchored markers that appear as the slider crosses their moment — illustrative placeholders for a future real event feed. |
Select a satellite and its **sub-satellite ground track** draws under it. In **Data Mode**, the whole picture reads as orbits + markers on a dark globe:
@ -99,10 +104,12 @@ No framework, no bundler — vanilla ES modules plus two CDN globals (CesiumJS,
index.html CDN script tags, HUD shell, #cesiumContainer
css/style.css dark HUD styling
js/config.js all URLs, intervals, camera, colors, keys — one file
js/lib.js shared helpers (glyphs, bearing, color bands)
js/lib.js shared helpers (glyphs, bearing, color bands, dead reckoning)
js/ui.js HUD: layer rows, status lines, LIVE/SCRUBBED chip
js/styles.js sensor styles: NVG/FLIR/CRT GLSL post-process skins
js/main.js viewer bootstrap, basemaps, clock, gating, picking, loader, URL-state sync
js/layers/*.js one module per layer (satellites, aircraft, ships, infra, events, quakes, fires)
js/layers/*.js one module per layer (satellites, aircraft, ships, infra, cables, events, quakes, fires)
assets/ bundled datasets: submarine cables, datacenters, dams (see assets/README.md)
serve.py dev server + same-origin proxy, shared OpenSky cache, history + snap endpoints
record.py optional dev daemon: snapshots live aircraft into data/history.db for replay
```
@ -125,7 +132,7 @@ record.py optional dev daemon: snapshots live aircraft into data/his
## Deployment
Target: `https://partly.party/godsigh/` (games VPS, behind the `forum-nginx` container and Cloudflare). Only `index.html`, `css/`, and `js/` ship; `serve.py` is replaced by nginx `location` blocks that proxy OpenSky/Celestrak. See [SPEC.md](SPEC.md) §9 for the exact deploy steps.
Target: `https://partly.party/godsigh/` (games VPS, behind the `forum-nginx` container and Cloudflare). Only `index.html`, `css/`, `js/`, and `assets/` ship; `serve.py` is replaced by nginx `location` blocks that proxy OpenSky/Celestrak. See [SPEC.md](SPEC.md) §9 for the exact deploy steps.
## Attribution & terms
@ -133,6 +140,8 @@ Target: `https://partly.party/godsigh/` (games VPS, behind the `forum-nginx` con
- **Celestrak** — GP/TLE orbital data. https://celestrak.org
- **CARTO / OpenStreetMap** — basemap tiles. © OpenStreetMap contributors © CARTO.
- **EOX** — Sentinel-2 cloudless; modified Copernicus Sentinel data 2023. https://s2maps.eu
- **TeleGeography** — submarine cable & landing-point data, CC BY-NC-SA 3.0 (**non-commercial**). https://www.submarinecablemap.com
- **OpenStreetMap contributors** — datacenter & dam data via Open Infrastructure Map, ODbL 1.0.
- **CesiumJS**, **satellite.js** — Apache-2.0 / MIT.
The Cesium credit display is kept visible in-app as a condition of these licenses.

19
assets/README.md Normal file
View File

@ -0,0 +1,19 @@
# Bundled datasets — provenance & licenses
Static datasets vendored from the open-sourced
[God's Eye View](https://github.com/bilawalsidhu/gods-eye-view) repo
(`src/data/local_data/`), reduced to what GODSIGH renders.
| File | Contents | Source | License |
|---|---|---|---|
| `datacenters.geojson` | 4,351 datacenter points (name, operator). Polygons collapsed to centroids. | OpenStreetMap contributors, via Open Infrastructure Map | **ODbL 1.0** — keep OSM attribution |
| `dams.geojson` | 704 dam points (name, power output/source). Polygons collapsed to centroids. | OpenStreetMap contributors, via Open Infrastructure Map | **ODbL 1.0** — keep OSM attribution |
| `cables/cable-geo.json` | 712 submarine cable routes (MultiLineString, per-cable color) | TeleGeography — submarinecablemap.com | **CC BY-NC-SA 3.0** — see below |
| `cables/landing-point-geo.json` | 1,917 cable landing points | TeleGeography — submarinecablemap.com | **CC BY-NC-SA 3.0** — see below |
## ⚠️ TeleGeography cable data — NonCommercial
The cable files are © TeleGeography, CC BY-NC-SA 3.0. Attribution
("© TeleGeography — submarinecablemap.com") is shown in the layer's info
panels. **Non-commercial use only** — if GODSIGH is ever deployed
commercially, remove `assets/cables/` or license the data from TeleGeography.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
assets/dams.geojson Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -132,6 +132,8 @@ body.hud-hidden #hud-toggle {
cursor: pointer;
transition: all 0.15s;
}
.modes.styles { margin-top: 6px; }
.modes.styles button { padding: 4px 6px; font-size: 10px; }
.modes button:hover { color: var(--text); }
.modes button.active {
color: #04121a;

View File

@ -23,6 +23,12 @@
<button id="mode-data" class="active" type="button">Data</button>
<button id="mode-photo" type="button">Photo</button>
</div>
<div class="modes styles" title="Sensor styles (keys 14)">
<button id="style-none" class="active" type="button">Normal</button>
<button id="style-nvg" type="button">NVG</button>
<button id="style-flir" type="button">FLIR</button>
<button id="style-crt" type="button">CRT</button>
</div>
<div id="layers"></div>
<a id="solargod-link" class="portal-link" href="http://127.0.0.1:8147/"
target="_blank" rel="noopener" title="Open SOLARGOD at this moment">🪐 SOLARGOD</a>

View File

@ -70,6 +70,7 @@ export default function create(ctx) {
// the aircraft count stays stable whether or not the filter is engaged.
function renderPlanes(planes) {
lastPlanes = planes; // remembered so toggling the filter re-renders instantly
fixMs = Date.now(); // dead-reckoning baseline: billboards are at their fix now
bc.removeAll();
let total = 0;
let mil = 0;
@ -92,6 +93,8 @@ export default function create(ctx) {
icao24: p.icao24,
callsign: p.callsign,
country: p.country,
lon: p.lon,
lat: p.lat,
alt: p.alt,
vel: p.vel,
track: p.track,
@ -289,6 +292,28 @@ export default function create(ctx) {
ui.hidePickOverlay();
}
// ---- smooth motion (Wave 4) -----------------------------------------------
// ADS-B fixes arrive every ~90 s; without this, planes teleport per poll.
// Dead-reckon every billboard forward along its course at ~10 Hz while live
// (replay frames stay static). Stops extrapolating after 5 min of stale data.
let fixMs = 0;
let lastDrMs = 0;
viewer.scene.preRender.addEventListener(() => {
if (!isLive || !enabled || !bc.show || !fixMs) return;
const nowMs = Date.now();
if (nowMs - lastDrMs < 100) return;
lastDrMs = nowMs;
const dt = (nowMs - fixMs) / 1000;
if (dt <= 0 || dt > 300) return;
for (let i = 0; i < bc.length; i++) {
const b = bc.get(i);
const d = b.id;
if (!d || d.lon == null || !d.vel) continue;
const p = lib.deadReckon(d.lon, d.lat, d.vel, d.track, dt);
b.position = Cesium.Cartesian3.fromDegrees(p.lon, p.lat, d.alt || 0);
}
});
// Self-heal when the tab regains focus. A backgrounded tab kills the poll
// loop (canPoll() is false, so tick() returns without rescheduling); if the
// clock is also paused, onClockTick never fires again to restart it (its

94
js/layers/cables.js Normal file
View File

@ -0,0 +1,94 @@
// Submarine cables — TeleGeography routes + landing points, bundled in
// assets/cables/ (CC BY-NC-SA 3.0, © TeleGeography; see assets/README.md).
// Static context like infra.js: no time dynamics, loaded once at boot.
export default function create(ctx) {
const { viewer, lib, ui, Cesium } = ctx;
const ds = new Cesium.CustomDataSource('cables');
viewer.dataSources.add(ds);
let enabled = true;
ui.addLayer('cables', 'Submarine cables', true, (on) => { enabled = on; ds.show = on; }, 'Context');
ui.setStatus('cables', 'loading…', 'warn');
const CREDIT = '<p style="opacity:.7">© TeleGeography — submarinecablemap.com (CC BY-NC-SA 3.0). Routes are schematic.</p>';
// Landing-point labels only appear zoomed well in; the dots a bit earlier.
const labelFade = new Cesium.NearFarScalar(2.0e5, 1.0, 1.2e6, 0.0);
const dotFade = new Cesium.NearFarScalar(1.0e6, 1.0, 8.0e6, 0.0);
async function load() {
let cables, landings;
try {
[cables, landings] = await Promise.all([
fetch('assets/cables/cable-geo.json').then((r) => r.json()),
fetch('assets/cables/landing-point-geo.json').then((r) => r.json()),
]);
} catch (err) {
ui.setStatus('cables', 'failed to load bundled data', 'err');
return;
}
let nCables = 0;
for (const f of cables.features || []) {
const g = f.geometry;
if (!g || g.type !== 'MultiLineString') continue;
const p = f.properties || {};
const color = lib.cz(p.color || '#5aa7d0', 0.85);
const name = p.name || 'Submarine cable';
for (const line of g.coordinates) {
if (!Array.isArray(line) || line.length < 2) continue;
ds.entities.add({
name: `Cable — ${name}`,
polyline: {
positions: Cesium.Cartesian3.fromDegreesArray(line.flat()),
width: 1.4,
material: color,
},
description: `<p><b>${lib.escapeHtml(name)}</b> — international submarine cable system.</p>` + CREDIT,
});
}
nCables++;
}
let nLandings = 0;
for (const f of landings.features || []) {
const g = f.geometry;
if (!g || g.type !== 'Point') continue;
const [lon, lat] = g.coordinates;
if (!isFinite(lon) || !isFinite(lat)) continue;
const name = (f.properties || {}).name || 'Landing point';
ds.entities.add({
name: `Landing — ${name}`,
position: Cesium.Cartesian3.fromDegrees(lon, lat),
point: {
pixelSize: 4, color: lib.cz('#8fd8ff'),
outlineColor: Cesium.Color.BLACK, outlineWidth: 1,
translucencyByDistance: dotFade,
disableDepthTestDistance: Number.POSITIVE_INFINITY,
},
label: {
text: name,
font: '10px "Segoe UI", system-ui, sans-serif',
fillColor: lib.cz('#8fd8ff'),
outlineColor: Cesium.Color.fromCssColorString('#0a0f14'),
outlineWidth: 3,
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
pixelOffset: new Cesium.Cartesian2(0, -6),
translucencyByDistance: labelFade,
disableDepthTestDistance: Number.POSITIVE_INFINITY,
},
description: `<p><b>${lib.escapeHtml(name)}</b> — cable landing point.</p>` + CREDIT,
});
nLandings++;
}
ds.show = enabled;
ui.setStatus('cables', `${nCables} cables · ${nLandings} landings`, 'ok');
}
load();
return { id: 'cables' };
}

View File

@ -71,6 +71,7 @@ export default function create(ctx) {
}
const list = Array.isArray(data && data.ac) ? data.ac : [];
fixMs = Date.now(); // dead-reckoning baseline
bc.removeAll();
let count = 0;
let emergencies = 0;
@ -89,6 +90,8 @@ export default function create(ctx) {
id: {
layer: 'military',
hex: a.hex,
lon: a.lon,
lat: a.lat,
callsign: (a.flight || '').trim(),
type: a.t,
reg: a.r,
@ -132,6 +135,25 @@ export default function create(ctx) {
function clearPick() { ui.hidePickOverlay(); }
// Smooth motion between the 60 s polls (see aircraft.js) — gs is KNOTS here.
let fixMs = 0;
let lastDrMs = 0;
viewer.scene.preRender.addEventListener(() => {
if (!isLive || !enabled || !bc.show || !fixMs) return;
const nowMs = Date.now();
if (nowMs - lastDrMs < 100) return;
lastDrMs = nowMs;
const dt = (nowMs - fixMs) / 1000;
if (dt <= 0 || dt > 300) return;
for (let i = 0; i < bc.length; i++) {
const b = bc.get(i);
const d = b.id;
if (!d || d.lon == null || !d.gs) continue;
const p = lib.deadReckon(d.lon, d.lat, d.gs * 0.51444, d.track, dt);
b.position = Cesium.Cartesian3.fromDegrees(p.lon, p.lat, (d.altFt || 0) * 0.3048);
}
});
document.addEventListener('visibilitychange', () => { if (!document.hidden) kick(); });
kick();

View File

@ -105,3 +105,18 @@ export function cz(hex, alpha) {
export function headingToRotation(headingDeg) {
return -Cesium.Math.toRadians(headingDeg || 0);
}
// Dead-reckon a fix forward along its course: equirectangular advance, good to
// well under a pixel for the few minutes between ADS-B polls (mirrors the
// open-sourced God's Eye View "smooth motion from choppy data" idea).
// ponytail: flat-earth step, swap for geodesic if we ever extrapolate >10 min.
const EARTH_R = 6371000;
export function deadReckon(lon, lat, velMps, trackDeg, dtSec) {
const d = (velMps || 0) * dtSec;
if (!d || !isFinite(d)) return { lon, lat };
const th = (trackDeg || 0) * Math.PI / 180;
const latR = lat * Math.PI / 180;
const newLat = lat + (d * Math.cos(th) / EARTH_R) * 180 / Math.PI;
const newLon = lon + (d * Math.sin(th) / (EARTH_R * Math.max(0.05, Math.cos(latR)))) * 180 / Math.PI;
return { lon: ((newLon + 540) % 360) - 180, lat: Math.max(-89.9, Math.min(89.9, newLat)) };
}

View File

@ -23,6 +23,7 @@ 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';
import * as styles from './styles.js';
const Cesium = window.Cesium;
@ -90,6 +91,9 @@ document.getElementById('live-chip').addEventListener('click', () => {
viewer.clock.currentTime = clampToWindow(Cesium.JulianDate.now());
});
// ---- sensor styles (NVG/FLIR/CRT post-process; must init before hash parse) ----
styles.initStyles(viewer);
// ---- portal to SOLARGOD (sibling orrery) ----
// Deep-links to the solar-system app at GODSIGH's current sim moment via its
// shared `#t=@<unixMs>` convention; the href is rebuilt fresh at click time.
@ -137,6 +141,9 @@ function applyHashState() {
if (m === 'p') { setMode('photo'); state.modeApplied = true; }
else if (m === 'd') { setMode('data'); state.modeApplied = true; }
const s = params.get('s');
if (s) styles.setStyle(s); // unknown ids are ignored by setStyle
const t = params.get('t');
if (t !== null && t !== '' && Number.isFinite(Number(t))) {
viewer.clock.currentTime = clampToWindow(
@ -171,7 +178,9 @@ function serializeState() {
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}`;
const style = styles.getStyle();
const s = style !== 'none' ? `&s=${style}` : '';
return `#c=${lon},${lat},${height},${heading},${pitch}&m=${mode}${s}&L=${onLayers.join(',')}&t=${t}`;
}
function startHashSync() {
@ -190,6 +199,7 @@ const ctx = { viewer, CONFIG, lib, ui, Cesium, start, stop, now };
const LAYER_MODULES = [
'./layers/satellites.js',
'./layers/infra.js',
'./layers/cables.js',
'./layers/events.js',
'./layers/ships.js',
'./layers/ais.js',

View File

@ -180,6 +180,46 @@ export const REGISTRY = [
status: ({ shown }) => `${shown} upcoming launches`,
},
// ─────────────────────────── Context ───────────────────────────
// Bundled static datasets (assets/*.geojson) — OSM-derived, ODbL 1.0.
// Served locally, so refreshMs is just a token daily re-read.
{
id: 'datacenters', name: 'Datacenters (OSM)', category: 'Context', defaultOn: false,
url: 'assets/datacenters.geojson',
refreshMs: 86_400_000,
style: (f, { Cesium, lib }) => ({
point: { pixelSize: 4, color: lib.cz('#35e0ff'), outlineColor: Cesium.Color.BLACK, outlineWidth: 1, disableDepthTestDistance: Number.POSITIVE_INFINITY },
label: { text: f.properties.name, font: '10px "Segoe UI", system-ui, sans-serif', fillColor: lib.cz('#35e0ff'), fade: [1.5e5, 9.0e5] },
}),
description: (f, { lib }) => {
const p = f.properties || {};
return `<table class="cesium-infoBox-defaultTable"><tbody>` +
`<tr><th>Name</th><td>${lib.escapeHtml(p.name || '—')}</td></tr>` +
`<tr><th>Operator</th><td>${lib.escapeHtml(p.operator || '—')}</td></tr></tbody></table>` +
`<p style="opacity:.7">© OpenStreetMap contributors (ODbL) via Open Infrastructure Map. Bundled snapshot.</p>`;
},
status: ({ shown }) => `${shown} datacenters (bundled)`,
},
{
id: 'dams', name: 'Dams (OSM)', category: 'Context', defaultOn: false,
url: 'assets/dams.geojson',
refreshMs: 86_400_000,
style: (f, { Cesium, lib }) => ({
point: { pixelSize: 4, color: lib.cz('#7bff9d'), outlineColor: Cesium.Color.BLACK, outlineWidth: 1, disableDepthTestDistance: Number.POSITIVE_INFINITY },
label: { text: f.properties.name, font: '10px "Segoe UI", system-ui, sans-serif', fillColor: lib.cz('#7bff9d'), fade: [2.0e5, 1.2e6] },
}),
description: (f, { lib }) => {
const p = f.properties || {};
return `<table class="cesium-infoBox-defaultTable"><tbody>` +
`<tr><th>Name</th><td>${lib.escapeHtml(p.name || '—')}</td></tr>` +
`<tr><th>Output</th><td>${lib.escapeHtml(p.output || '—')}</td></tr>` +
`<tr><th>Source</th><td>${lib.escapeHtml(p.source || '—')}</td></tr></tbody></table>` +
`<p style="opacity:.7">© OpenStreetMap contributors (ODbL) via Open Infrastructure Map. Bundled snapshot.</p>`;
},
status: ({ shown }) => `${shown} dams (bundled)`,
},
// ──────────────────── Regional — Australia ────────────────────
{
id: 'rfs', name: 'NSW bushfires (RFS)', category: 'Regional — Australia', defaultOn: false,

120
js/styles.js Normal file
View File

@ -0,0 +1,120 @@
// Sensor styles — full-screen GLSL post-process skins over the whole globe,
// ported (compacted) from the open-sourced God's Eye View style system.
// NVG = night vision · FLIR = Ironbow thermal ramp · CRT = phosphor console.
// Keyless and client-side: Cesium PostProcessStage, no imagery change.
const STYLES = {
none: null,
nvg: `
uniform sampler2D colorTexture;
uniform float time;
in vec2 v_textureCoordinates;
float hash(vec2 p){ vec3 p3 = fract(vec3(p.xyx) * 0.1031); p3 += dot(p3, p3.yzx + 33.33); return fract((p3.x + p3.y) * p3.z); }
void main() {
vec2 uv = v_textureCoordinates;
vec3 c = texture(colorTexture, uv).rgb;
float lum = dot(c, vec3(0.299, 0.587, 0.114));
lum = pow(lum, 0.62); // intensifier gain lifts shadows
lum += (hash(uv * 900.0 + time * 60.0) - 0.5) * 0.13; // photon noise
lum *= 0.94 + 0.06 * sin(uv.y * 800.0); // faint scanlines
float d = distance(uv, vec2(0.5));
lum *= smoothstep(0.78, 0.42, d); // tube vignette
out_FragColor = vec4(vec3(0.06, 1.0, 0.28) * lum, 1.0);
}`,
flir: `
uniform sampler2D colorTexture;
uniform float time;
in vec2 v_textureCoordinates;
// Ironbow "Predator" ramp: black→purple→magenta→red→orange→yellow→white.
vec3 ironbow(float t) {
t = clamp(t, 0.0, 1.0);
float s = t * 6.0;
if (s < 1.0) return mix(vec3(0.0), vec3(0.13, 0.0, 0.30), s);
if (s < 2.0) return mix(vec3(0.13, 0.0, 0.30), vec3(0.49, 0.0, 0.45), s - 1.0);
if (s < 3.0) return mix(vec3(0.49, 0.0, 0.45), vec3(0.86, 0.10, 0.18), s - 2.0);
if (s < 4.0) return mix(vec3(0.86, 0.10, 0.18), vec3(1.0, 0.55, 0.0), s - 3.0);
if (s < 5.0) return mix(vec3(1.0, 0.55, 0.0), vec3(1.0, 0.91, 0.32), s - 4.0);
return mix(vec3(1.0, 0.91, 0.32), vec3(1.0), s - 5.0);
}
float hash(vec2 p){ vec3 p3 = fract(vec3(p.xyx) * 0.1031); p3 += dot(p3, p3.yzx + 33.33); return fract((p3.x + p3.y) * p3.z); }
void main() {
vec2 uv = v_textureCoordinates;
// Sensor pixelation grid.
vec2 grid = floor(uv * vec2(640.0, 360.0)) / vec2(640.0, 360.0);
vec3 c = texture(colorTexture, grid).rgb;
float t = dot(c, vec3(0.299, 0.587, 0.114));
t = clamp((t - 0.08) * 1.5, 0.0, 1.0); // stretch the "temperature" range
t += (hash(grid * 700.0 + time * 30.0) - 0.5) * 0.05; // thermal noise
out_FragColor = vec4(ironbow(t), 1.0);
}`,
crt: `
uniform sampler2D colorTexture;
uniform float time;
in vec2 v_textureCoordinates;
void main() {
vec2 uv = v_textureCoordinates - 0.5;
uv *= 1.0 + 0.12 * dot(uv, uv); // barrel distortion
uv += 0.5;
if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) { out_FragColor = vec4(0.0, 0.0, 0.0, 1.0); return; }
// Chromatic aberration at the edges.
float ab = 0.0015 * distance(uv, vec2(0.5)) * 4.0;
vec3 c = vec3(
texture(colorTexture, uv + vec2(ab, 0.0)).r,
texture(colorTexture, uv).g,
texture(colorTexture, uv - vec2(ab, 0.0)).b);
c *= 0.82 + 0.18 * sin(uv.y * 1200.0); // scanlines
c *= 0.97 + 0.03 * sin(time * 90.0); // flicker
c = mix(c, vec3(0.1, 0.85, 0.45) * dot(c, vec3(0.333)), 0.35); // phosphor cast
float d = distance(uv, vec2(0.5));
c *= smoothstep(0.85, 0.45, d); // vignette
out_FragColor = vec4(c, 1.0);
}`,
};
let viewer = null;
let activeId = 'none';
let activeStage = null;
export function initStyles(v) {
viewer = v;
viewer.scene.preUpdate.addEventListener(() => {
if (activeStage) activeStage.uniforms.time = performance.now() / 1000;
});
for (const id of Object.keys(STYLES)) {
const btn = document.getElementById(`style-${id}`);
if (btn) btn.addEventListener('click', () => setStyle(id));
}
// Keyboard 14, matching the upstream project's style hotkeys.
const keys = Object.keys(STYLES);
document.addEventListener('keydown', (e) => {
if (e.target && /INPUT|TEXTAREA/.test(e.target.tagName)) return;
const i = Number(e.key) - 1;
if (i >= 0 && i < keys.length) setStyle(keys[i]);
});
}
export function setStyle(id) {
if (!viewer || !(id in STYLES) || id === activeId) return;
if (activeStage) {
viewer.scene.postProcessStages.remove(activeStage); // remove() also destroys
activeStage = null;
}
if (STYLES[id]) {
activeStage = viewer.scene.postProcessStages.add(new window.Cesium.PostProcessStage({
fragmentShader: STYLES[id],
uniforms: { time: 0.0 },
}));
}
activeId = id;
for (const k of Object.keys(STYLES)) {
document.getElementById(`style-${k}`)?.classList.toggle('active', k === id);
}
}
export function getStyle() {
return activeId;
}