refactor: export infrastructure layers with explicit application callbacks

This commit is contained in:
Sameh Khamis committed 2026-09-11 13:21:22 -07:00
1 parent e465197cee
commit 142bf583f8
10 files changed
+1338 -1115

No files matched your search

+4
View File
@@ -13,6 +13,10 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md
## [Unreleased]
- Datacenter and dam factories are available through scoped package exports with
explicit context, overlay and render callbacks. The standalone app uses the
same implementation and bundled datasets.
- Local GeoJSON layers share concurrent loads, cancel pending fetches on destruction,
discard late results, and remove their entity-context records on teardown.
+6
View File
@@ -2,6 +2,12 @@
## Infrastructure marker visibility
Datacenter/dam registration uses fresh reusable factories with the application's
existing context, overlay and render functions. The scoped package exports do
not import standalone application globals; see
[the infrastructure interface](INFRASTRUCTURE-LAYERS.md) for lifecycle and asset
requirements.
Local GeoJSON layers coalesce concurrent enables into one load. Disabling while
loading keeps the result hidden; destruction aborts the fetch and rejects late
parse/add results. Destruction and failed setup remove owned context records,
+62
View File
@@ -0,0 +1,62 @@
# Reusing infrastructure layers
`gods-eye-view/infrastructure` exports `createInfrastructureLayers(services)`.
It returns fresh datacenter and dam layers in that order, with the existing IDs,
appearance, label budgets and bundled public datasets. Importing the module or
calling the factory does not load data, create a viewer, or start the application.
The asset URLs resolve relative to the module; Vite rewrites them for production
base paths. This browser source export expects a Vite-compatible asset build.
Pass the application's existing functions:
```js
import { createInfrastructureLayers } from 'gods-eye-view/infrastructure';
const layers = createInfrastructureLayers({
overlayHost: { setEntries, setVisible, clearSource },
registerEntityContext,
selectEntityContext,
clearSelectedEntityContextForLayer,
removeEntityContextsForLayer,
governorRequestRender,
});
// Register these layers with the existing data-layer manager and viewer.
```
| Function | Arguments / responsibility |
| --- | --- |
| `overlayHost.setEntries` | `(sourceId, entries, options)`; publish the existing infrastructure overlay records and paint budget |
| `overlayHost.setVisible` | `(sourceId, visible)`; toggle a source |
| `overlayHost.clearSource` | `(sourceId)`; remove that source's overlay entries |
| `registerEntityContext` | `(entity, metadata)`; register selectable feature metadata, including the owning layer and data source |
| `selectEntityContext` | `(entity)`; select that registered feature |
| `clearSelectedEntityContextForLayer` | `(layerId)`; clear selection only if owned by this layer |
| `removeEntityContextsForLayer` | `(layerId)`; remove records owned by the layer on destruction or failed setup |
| `governorRequestRender` | `(reason)`; request a frame through the existing render scheduler |
Each returned layer supports `init(viewer)`, `enable(viewer)`, `disable(viewer)`,
`update(viewer)`, `destroy(viewer)`, `getStats()` and `getLodDiagnostics()`.
Use the same viewer for the instance's entire lifetime. Each viewer/context/overlay
host permits one live instance per layer ID; destroy an old instance before
replacing it. Separate applications may use identical IDs with separate hosts.
Callbacks must be ready before enable and remain usable through destruction.
Disable retains the loaded dataset for reuse and clears selection and listeners.
Concurrent enables share one load. Destroy is permanent, aborts pending fetches,
discards late results, and clears the owned data source, context records, overlays,
input handler and listeners. The layer never creates an application context store,
overlay host, viewer, or render scheduler. The consumer must resolve one compatible
Cesium installation shared with its viewer; it must not bundle separate copies.
`gods-eye-view/infrastructure/geojson` exports the lower-level
`createLocalGeoJsonLayer(options, services)` and existing infrastructure overlay
helpers. `gods-eye-view/infrastructure/lod` exports the existing LOD policy helpers.
The standalone app's `src/data/localGeojson.js` keeps its original single-argument
factory and supplies its existing functions. Other consumers should use the package
exports, which do not import standalone application globals.
Dataset files and source/license notices remain under `src/data/local_data/`.
Their input counts are 4,351 datacenter features and 704 dam features. Cesium may
expand multipart geometries into multiple entities; entity and stem counts are
different measurements. This change does not refresh or relicense the datasets.
+6 -1
View File
@@ -2,7 +2,7 @@
"name": "gods-eye-view",
"private": true,
"version": "0.1.1",
"description": "A real-time intelligence console for planet Earth — photorealistic 3D globe, live aircraft/ships/satellites/earthquakes/CCTV, and hands-free voice control. Runs in a browser.",
"description": "A real-time intelligence console for planet Earth \u2014 photorealistic 3D globe, live aircraft/ships/satellites/earthquakes/CCTV, and hands-free voice control. Runs in a browser.",
"type": "module",
"license": "MIT",
"author": "Bilawal Sidhu",
@@ -56,5 +56,10 @@
"vite": "^6.0.0",
"vite-plugin-cesium": "^1.2.23",
"ws": "^8.21.0"
},
"exports": {
"./infrastructure": "./src/data/infrastructure.js",
"./infrastructure/geojson": "./src/data/localGeojsonCore.js",
"./infrastructure/lod": "./src/data/localGeojsonLod.js"
}
}
+38
View File
@@ -0,0 +1,38 @@
import { createLocalGeoJsonLayer } from './localGeojsonCore.js';
// Resolved by Vite in builds and relative to this module in other consumers.
const datacentersUrl = new URL('./local_data/datacenters/datacenters.geojsonl', import.meta.url).href;
const damsUrl = new URL('./local_data/dams/dams.geojsonl', import.meta.url).href;
/**
* Create fresh datacenter and dam layers without starting or loading them.
* @param {object} services Caller-owned context, overlay and render operations.
* @returns {object[]} Datacenters then dams, with stable standalone identities.
*/
export function createInfrastructureLayers(services) {
const datacenters = createLocalGeoJsonLayer({
id: 'local-datacenters',
url: datacentersUrl,
name: 'Datacenters',
color: '#00ffff', // Cyan
icon: '▣',
source: 'Local',
labels: true,
labelMax: 700,
labelGridPx: 138,
}, services);
const dams = createLocalGeoJsonLayer({
id: 'local-dams',
url: damsUrl,
name: 'Dams',
color: '#0088ff', // Blue
icon: '▰',
source: 'USACE',
labels: true,
labelMax: 900,
labelGridPx: 132,
}, services);
return [datacenters, dams];
}
+99
View File
@@ -0,0 +1,99 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
import * as Cesium from 'cesium';
import { createInfrastructureLayers } from 'gods-eye-view/infrastructure';
import { createLocalGeoJsonLayer } from 'gods-eye-view/infrastructure/geojson';
function services() {
const records = new Map();
let selection;
return {
records,
overlayHost: { setEntries() {}, setVisible() {}, clearSource() {} },
registerEntityContext(entity, metadata) { records.set(metadata.id, { entity, ...metadata }); },
selectEntityContext(entity) { selection = entity; },
clearSelectedEntityContextForLayer() { selection = undefined; },
removeEntityContextsForLayer(id) {
for (const [key, record] of records) if (record.layerId === id) records.delete(key);
},
governorRequestRender() {},
selection: () => selection,
};
}
test('package exports import without an application, DOM, fetch, or timers', () => {
const result = spawnSync(process.execPath, ['--input-type=module', '-e', `
globalThis.fetch = () => { throw new Error('unexpected fetch'); };
globalThis.setTimeout = () => { throw new Error('unexpected timer'); };
for (const key of ['window', 'document']) {
delete globalThis[key];
}
await import('gods-eye-view/infrastructure');
await import('gods-eye-view/infrastructure/geojson');
await import('gods-eye-view/infrastructure/lod');
`], { cwd: new URL('../..', import.meta.url), encoding: 'utf8' });
assert.equal(result.status, 0, result.stderr);
});
test('infrastructure factory preserves identity and creates independent state without loading', t => {
t.mock.method(globalThis, 'fetch', () => { throw new Error('factory must not fetch'); });
const first = createInfrastructureLayers(services());
const second = createInfrastructureLayers(services());
assert.deepEqual(first.map(({ id, name, source }) => ({ id, name, source })), [
{ id: 'local-datacenters', name: 'Datacenters', source: 'Local' },
{ id: 'local-dams', name: 'Dams', source: 'USACE' },
]);
first.forEach((layer, index) => {
assert.notEqual(layer, second[index]);
layer.destroy();
assert.deepEqual(second[index].getStats(), { count: 0, lastUpdate: null, error: null });
second[index].destroy();
});
});
test('dataset URLs still name the complete bundled sources', () => {
for (const [file, count] of [['datacenters', 4351], ['dams', 704]]) {
const lines = readFileSync(new URL(`./local_data/${file}/${file}.geojsonl`, import.meta.url), 'utf8')
.split('\n').filter(line => line.trim());
assert.equal(lines.length, count);
}
});
test('two viewers use their supplied contexts and dispose independently', async t => {
t.mock.method(globalThis, 'fetch', async () => ({ ok: true, text: async () => JSON.stringify({
type: 'Feature', id: 'same-id', properties: { name: 'Dam' },
geometry: { type: 'Polygon', coordinates: [[[0, 0], [0.01, 0], [0, 0.01], [0, 0]]] },
}) }));
const hosts = [services(), services()];
const instances = hosts.map(host => {
const sources = new Cesium.DataSourceCollection();
let click;
const layer = createLocalGeoJsonLayer({
id: 'local-dams', name: 'Dams', color: '#0088ff', url: '/dams.geojsonl',
screenSpaceEventHandlerFactory: () => ({ setInputAction(fn) { click = fn; }, destroy() {} }),
}, host);
const viewer = {
dataSources: sources,
scene: {
canvas: {}, preRender: new Cesium.Event(), requestRender() {},
screenSpaceCameraController: { enableInputs: true },
pick: () => ({ id: sources.get(0).entities.values[0] }),
},
camera: { moveEnd: new Cesium.Event(), flyTo() {} },
};
return { layer, viewer, click: () => click({ position: {} }) };
});
t.after(() => instances.forEach(({ layer, viewer }) => layer.destroy(viewer)));
await Promise.all(instances.map(({ layer, viewer }) => layer.enable(viewer)));
assert.equal(hosts[0].records.size, 1);
assert.equal(hosts[1].records.size, 1);
instances[0].click();
assert.ok(hosts[0].selection());
assert.equal(hosts[1].selection(), undefined);
instances[0].layer.destroy(instances[0].viewer);
assert.equal(hosts[0].records.size, 0);
assert.equal(hosts[1].records.size, 1);
assert.equal(instances[1].viewer.dataSources.length, 1);
});
+24 -1081
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -489,7 +489,7 @@ for (const [heightM, minStemM, maxStemM] of [[500, 45, 90], [10000, 1100, 1400]]
}
test('local infrastructure creates no native labels or per-frame geometry callbacks', () => {
const source = readFileSync(new URL('./localGeojson.js', import.meta.url), 'utf8');
const source = readFileSync(new URL('./localGeojsonCore.js', import.meta.url), 'utf8');
assert.doesNotMatch(source, /new Cesium\.LabelGraphics/);
assert.doesNotMatch(source, /new Cesium\.CallbackProperty/);
assert.match(source, /feature\.position = tip/);
File diff suppressed because it is too large. Load diff
+3 -32
View File
@@ -1,38 +1,9 @@
import { createLocalGeoJsonLayer } from './localGeojson.js';
import { localGeoJsonServices } from './localGeojson.js';
import { createInfrastructureLayers } from './infrastructure.js';
import { createFirmsHeatmapLayer } from './firmsHeatmap.js';
import submarineCablesLayer from './telegeographySubmarineCables.js';
// Use Vite's ?url import to properly resolve these assets in dev and build
import datacentersUrl from './local_data/datacenters/datacenters.geojsonl?url';
import damsUrl from './local_data/dams/dams.geojsonl?url';
/**
* Registry of local GeoJSON datasets.
* These are lazily loaded natively into Cesium when enabled.
*/
const datacenters = createLocalGeoJsonLayer({
id: 'local-datacenters',
url: datacentersUrl,
name: 'Datacenters',
color: '#00ffff', // Cyan
icon: '▣',
source: 'Local',
labels: true,
labelMax: 700,
labelGridPx: 138,
});
const dams = createLocalGeoJsonLayer({
id: 'local-dams',
url: damsUrl,
name: 'Dams',
color: '#0088ff', // Blue
icon: '▰',
source: 'USACE',
labels: true,
labelMax: 900,
labelGridPx: 132,
});
const [datacenters, dams] = createInfrastructureLayers(localGeoJsonServices);
// Live NASA FIRMS fires (VIIRS ×3 NRT via the /api/firms proxy). The id keeps
// the historical `local-` prefix for persistence + voice-tool-enum compat,