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:
parent
092fe5254a
commit
3f64f7ed4e
2
.gitignore
vendored
2
.gitignore
vendored
@ -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
71
COMPARISON.md
Normal 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 `1–4`, 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 `1–4`; 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.
|
||||
19
README.md
19
README.md
@ -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 `1–4`): 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 East–West Petroline & Habshan–Fujairah 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
19
assets/README.md
Normal 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.
|
||||
1
assets/cables/cable-geo.json
Normal file
1
assets/cables/cable-geo.json
Normal file
File diff suppressed because one or more lines are too long
1
assets/cables/landing-point-geo.json
Normal file
1
assets/cables/landing-point-geo.json
Normal file
File diff suppressed because one or more lines are too long
1
assets/dams.geojson
Normal file
1
assets/dams.geojson
Normal file
File diff suppressed because one or more lines are too long
1
assets/datacenters.geojson
Normal file
1
assets/datacenters.geojson
Normal file
File diff suppressed because one or more lines are too long
@ -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;
|
||||
|
||||
@ -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 1–4)">
|
||||
<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>
|
||||
|
||||
@ -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
94
js/layers/cables.js
Normal 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' };
|
||||
}
|
||||
@ -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();
|
||||
|
||||
|
||||
15
js/lib.js
15
js/lib.js
@ -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)) };
|
||||
}
|
||||
|
||||
12
js/main.js
12
js/main.js
@ -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',
|
||||
|
||||
@ -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
120
js/styles.js
Normal 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 1–4, 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;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user