mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
Keep source availability in the catalog and cover absent military acquisition
This commit is contained in:
1 parent
b78ff7a915
commit
161a3a796a
8 files changed
+115
-21
No files matched your search
@@ -1,5 +1,8 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
- Catalog source availability no longer modifies layer objects; unavailable
|
||||||
|
tooltips use layer display names.
|
||||||
|
|
||||||
- Street Level with no providers uses the shared unavailable state and refuses
|
- Street Level with no providers uses the shared unavailable state and refuses
|
||||||
activation before initialization, while keeping its layer identity visible.
|
activation before initialization, while keeping its layer identity visible.
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
Catalog source contracts and their consuming layers are declared in
|
Catalog source contracts and their consuming layers are declared in
|
||||||
`src/app/sourceComposition.js`. Omitted or null acquisition sources preserve layer
|
`src/app/sourceComposition.js`. Omitted or null acquisition sources preserve layer
|
||||||
registration and serialization, report unavailable, and are refused by the existing
|
registration and serialization, report unavailable, and are refused by the existing
|
||||||
visibility lifecycle before initialization. Invalid supplied methods still fail
|
visibility lifecycle before initialization. Availability is held in an explicit
|
||||||
|
catalog lookup supplied to the lifecycle, without modifying layer instances;
|
||||||
|
unavailable reasons use the layer display name. Invalid supplied methods still fail
|
||||||
construction. `createStandaloneApplication` and `createStandaloneCatalog` accept
|
construction. `createStandaloneApplication` and `createStandaloneCatalog` accept
|
||||||
`sources` overrides; unspecified entries retain
|
`sources` overrides; unspecified entries retain
|
||||||
standalone defaults, and null entries explicitly remove a source. Street Level
|
standalone defaults, and null entries explicitly remove a source. Street Level
|
||||||
|
|||||||
@@ -65,13 +65,28 @@ test('data setup seals the caller catalog before controls can restore and drains
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
catalog: createLayerCatalog(layers, metadata(layers)),
|
catalog: {
|
||||||
|
...createLayerCatalog(layers, metadata(layers)),
|
||||||
|
getSourceAvailability: (id) =>
|
||||||
|
id === 'one'
|
||||||
|
? {
|
||||||
|
available: false,
|
||||||
|
reason: 'One: data source not configured',
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
defer: (release) => releases.push(release),
|
defer: (release) => releases.push(release),
|
||||||
onData: (manager) => {
|
onData: (manager) => {
|
||||||
observed = manager;
|
observed = manager;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
assert.equal(observed, dataManager);
|
assert.equal(observed, dataManager);
|
||||||
|
assert.equal(await dataManager.setEnabled('one', true), false);
|
||||||
|
assert.equal(dataManager.layers.get('one').initialized, false);
|
||||||
|
assert.equal(
|
||||||
|
dataManager.getAll().find(({ id }) => id === 'one').stats.error,
|
||||||
|
'One: data source not configured',
|
||||||
|
);
|
||||||
assert.deepEqual(attached, [['one', 'two']]);
|
assert.deepEqual(attached, [['one', 'two']]);
|
||||||
assert.deepEqual([...dataManager.layers.keys()], ['one', 'two']);
|
assert.deepEqual([...dataManager.layers.keys()], ['one', 'two']);
|
||||||
assert.throws(() => dataManager.register({ id: 'late' }), /finalized/);
|
assert.throws(() => dataManager.register({ id: 'late' }), /finalized/);
|
||||||
|
|||||||
@@ -173,9 +173,11 @@ export function createApplicationCatalog({
|
|||||||
],
|
],
|
||||||
metadata,
|
metadata,
|
||||||
);
|
);
|
||||||
sourceComposition.bind(catalog.layers);
|
|
||||||
return Object.freeze({
|
return Object.freeze({
|
||||||
...catalog,
|
...catalog,
|
||||||
|
getSourceAvailability: sourceComposition.createAvailabilityLookup(
|
||||||
|
catalog.layers,
|
||||||
|
),
|
||||||
militaryRegistry,
|
militaryRegistry,
|
||||||
surface,
|
surface,
|
||||||
weatherClock,
|
weatherClock,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export function createApplicationData({
|
|||||||
// Initialize data layer manager
|
// Initialize data layer manager
|
||||||
const dataManager = new LayerLifecycle(viewer, {
|
const dataManager = new LayerLifecycle(viewer, {
|
||||||
allowQaRegistration,
|
allowQaRegistration,
|
||||||
|
getSourceAvailability: catalog?.getSourceAvailability,
|
||||||
});
|
});
|
||||||
defer(async () => {
|
defer(async () => {
|
||||||
await dataManager.destroyAll();
|
await dataManager.destroyAll();
|
||||||
|
|||||||
@@ -80,22 +80,29 @@ export function resolveCatalogSources(supplied = {}) {
|
|||||||
return {
|
return {
|
||||||
sources,
|
sources,
|
||||||
isConfigured: (name) => !missing.has(name),
|
isConfigured: (name) => !missing.has(name),
|
||||||
bind(layers) {
|
createAvailabilityLookup(layers) {
|
||||||
|
const byId = new Map(layers.map((layer) => [layer.id, layer]));
|
||||||
|
const availability = new Map();
|
||||||
for (const [name, { layers: ids }] of Object.entries(
|
for (const [name, { layers: ids }] of Object.entries(
|
||||||
CATALOG_SOURCE_CONTRACTS,
|
CATALOG_SOURCE_CONTRACTS,
|
||||||
)) {
|
)) {
|
||||||
for (const id of ids) {
|
for (const id of ids) {
|
||||||
const layer = layers.find((entry) => entry.id === id);
|
const layer = byId.get(id);
|
||||||
if (!layer) throw new Error(`Missing source consumer: ${id}`);
|
if (!layer) throw new Error(`Missing source consumer: ${id}`);
|
||||||
layer.getSourceAvailability = () => ({
|
availability.set(
|
||||||
available: !missing.has(name),
|
id,
|
||||||
source: name,
|
Object.freeze({
|
||||||
reason: missing.has(name)
|
available: !missing.has(name),
|
||||||
? `Data source not configured: ${name}`
|
source: name,
|
||||||
: null,
|
reason: missing.has(name)
|
||||||
});
|
? `${layer.name || id}: data source not configured`
|
||||||
|
: null,
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return (id) =>
|
||||||
|
availability.get(id) ?? byId.get(id)?.getSourceAvailability?.();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -30,7 +30,7 @@ test('every acquisition source can be omitted without losing catalog membership'
|
|||||||
const actual = catalog(sources, t);
|
const actual = catalog(sources, t);
|
||||||
assert.deepEqual(actual.metadata, baseline.metadata, name);
|
assert.deepEqual(actual.metadata, baseline.metadata, name);
|
||||||
for (const id of CATALOG_SOURCE_CONTRACTS[name].layers) {
|
for (const id of CATALOG_SOURCE_CONTRACTS[name].layers) {
|
||||||
assert.equal(actual.get(id).getSourceAvailability().available, false, id);
|
assert.equal(actual.getSourceAvailability(id).available, false, id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -38,7 +38,10 @@ test('every acquisition source can be omitted without losing catalog membership'
|
|||||||
test('all acquisition sources may be absent, and enable fails before initializing or fetching', async (t) => {
|
test('all acquisition sources may be absent, and enable fails before initializing or fetching', async (t) => {
|
||||||
const actual = catalog({}, t);
|
const actual = catalog({}, t);
|
||||||
assert.equal(actual.layers.length, 30);
|
assert.equal(actual.layers.length, 30);
|
||||||
const manager = new LayerLifecycle({});
|
const manager = new LayerLifecycle(
|
||||||
|
{},
|
||||||
|
{ getSourceAvailability: actual.getSourceAvailability },
|
||||||
|
);
|
||||||
for (const layer of actual.layers) manager.register(layer);
|
for (const layer of actual.layers) manager.register(layer);
|
||||||
for (const { layers: ids } of Object.values(CATALOG_SOURCE_CONTRACTS)) {
|
for (const { layers: ids } of Object.values(CATALOG_SOURCE_CONTRACTS)) {
|
||||||
for (const id of ids) {
|
for (const id of ids) {
|
||||||
@@ -47,7 +50,7 @@ test('all acquisition sources may be absent, and enable fails before initializin
|
|||||||
const row = manager.getAll().find((entry) => entry.id === id);
|
const row = manager.getAll().find((entry) => entry.id === id);
|
||||||
assert.equal(row.enabled, false);
|
assert.equal(row.enabled, false);
|
||||||
assert.equal(row.stats.status, 'unavailable');
|
assert.equal(row.stats.status, 'unavailable');
|
||||||
assert.match(row.stats.error, /not configured/);
|
assert.match(row.stats.error, /data source not configured/);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -85,7 +88,10 @@ test('standalone defaults allow exact replacements and explicit removal, and rej
|
|||||||
|
|
||||||
test('empty Street Level uses the shared unavailable lifecycle without initialization', async (t) => {
|
test('empty Street Level uses the shared unavailable lifecycle without initialization', async (t) => {
|
||||||
const actual = catalog({}, t);
|
const actual = catalog({}, t);
|
||||||
const manager = new LayerLifecycle({});
|
const manager = new LayerLifecycle(
|
||||||
|
{},
|
||||||
|
{ getSourceAvailability: actual.getSourceAvailability },
|
||||||
|
);
|
||||||
manager.register(actual.get('street-level'));
|
manager.register(actual.get('street-level'));
|
||||||
assert.equal(await manager.setEnabled('street-level', true), false);
|
assert.equal(await manager.setEnabled('street-level', true), false);
|
||||||
assert.equal(manager.layers.get('street-level').initialized, false);
|
assert.equal(manager.layers.get('street-level').initialized, false);
|
||||||
@@ -99,3 +105,52 @@ test('empty Street Level uses the shared unavailable lifecycle without initializ
|
|||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('availability lookup accepts frozen layers and uses their display names', () => {
|
||||||
|
const composition = resolveCatalogSources({});
|
||||||
|
const layers = Object.values(CATALOG_SOURCE_CONTRACTS)
|
||||||
|
.flatMap(({ layers }) => layers)
|
||||||
|
.map((id) =>
|
||||||
|
Object.freeze({
|
||||||
|
id,
|
||||||
|
name: id === 'fire-perimeters' ? 'Fire perimeters' : id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const lookup = composition.createAvailabilityLookup(layers);
|
||||||
|
assert.equal(
|
||||||
|
lookup('fire-perimeters').reason,
|
||||||
|
'Fire perimeters: data source not configured',
|
||||||
|
);
|
||||||
|
assert.equal(Object.hasOwn(layers[0], 'getSourceAvailability'), false);
|
||||||
|
const manager = new LayerLifecycle({}, { getSourceAvailability: lookup });
|
||||||
|
manager.register(layers.find(({ id }) => id === 'fire-perimeters'));
|
||||||
|
assert.equal(
|
||||||
|
manager.getAll()[0].stats.error,
|
||||||
|
'Fire perimeters: data source not configured',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('absent military acquisition does not poll, suppress flights, or erase supplied classification', async (t) => {
|
||||||
|
let fetches = 0;
|
||||||
|
t.mock.method(globalThis, 'fetch', async () => {
|
||||||
|
fetches++;
|
||||||
|
throw new Error('Unexpected fallback acquisition');
|
||||||
|
});
|
||||||
|
const actual = catalog(createStandaloneLayerSources({ military: null }), t);
|
||||||
|
const registry = actual.militaryRegistry;
|
||||||
|
assert.equal(actual.getSourceAvailability('flights').available, true);
|
||||||
|
assert.equal(registry.isMilitaryLayerActive(), false);
|
||||||
|
assert.equal(registry.isMilitaryIcao('abc123'), false);
|
||||||
|
registry.registerMilitaryIcaos([' ABC123 ']);
|
||||||
|
await registry.refreshMilitaryRegistryIfStale();
|
||||||
|
assert.equal(registry.isMilitaryIcao('abc123'), true);
|
||||||
|
assert.equal(registry.isMilitaryIcao('def456'), false);
|
||||||
|
const manager = new LayerLifecycle(
|
||||||
|
{},
|
||||||
|
{ getSourceAvailability: actual.getSourceAvailability },
|
||||||
|
);
|
||||||
|
manager.register(actual.get('military'));
|
||||||
|
assert.equal(await manager.setEnabled('military', true), false);
|
||||||
|
assert.equal(registry.isMilitaryLayerActive(), false);
|
||||||
|
assert.equal(fetches, 0);
|
||||||
|
});
|
||||||
+14
-5
@@ -74,7 +74,11 @@ function refreshFailureFromStats(stats, label) {
|
|||||||
* for real-time data overlays on the CesiumJS globe.
|
* for real-time data overlays on the CesiumJS globe.
|
||||||
*/
|
*/
|
||||||
export class LayerLifecycle {
|
export class LayerLifecycle {
|
||||||
constructor(viewer, { allowQaRegistration = false } = {}) {
|
constructor(
|
||||||
|
viewer,
|
||||||
|
{ allowQaRegistration = false, getSourceAvailability } = {},
|
||||||
|
) {
|
||||||
|
this._catalogSourceAvailability = getSourceAvailability;
|
||||||
this.viewer = viewer;
|
this.viewer = viewer;
|
||||||
this._activityListeners = new Set();
|
this._activityListeners = new Set();
|
||||||
this.layers = new Map(); // id → { module, enabled, initialized, intervalId, lifecycleState, lifecycleUncertain }
|
this.layers = new Map(); // id → { module, enabled, initialized, intervalId, lifecycleState, lifecycleUncertain }
|
||||||
@@ -228,7 +232,7 @@ export class LayerLifecycle {
|
|||||||
|
|
||||||
_normalizedStats(entry) {
|
_normalizedStats(entry) {
|
||||||
const moduleStats = this._moduleStats(entry);
|
const moduleStats = this._moduleStats(entry);
|
||||||
const availability = entry.module.getSourceAvailability?.();
|
const availability = this._sourceAvailability(entry.module.id);
|
||||||
const lifecycleLoading =
|
const lifecycleLoading =
|
||||||
entry.lifecycleState === 'enabling' ||
|
entry.lifecycleState === 'enabling' ||
|
||||||
entry.lifecycleState === 'disabling';
|
entry.lifecycleState === 'disabling';
|
||||||
@@ -2290,10 +2294,15 @@ export class LayerLifecycle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_sourceAvailability(layerId) {
|
||||||
|
return (
|
||||||
|
this._catalogSourceAvailability?.(layerId) ??
|
||||||
|
this.layers.get(layerId)?.module.getSourceAvailability?.()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async _visibilityBlockReason(change) {
|
async _visibilityBlockReason(change) {
|
||||||
const availability = this.layers
|
const availability = this._sourceAvailability(change.layerId);
|
||||||
.get(change.layerId)
|
|
||||||
?.module.getSourceAvailability?.();
|
|
||||||
if (change.enabled && availability?.available === false)
|
if (change.enabled && availability?.available === false)
|
||||||
return availability.reason || 'Data source unavailable';
|
return availability.reason || 'Data source unavailable';
|
||||||
for (const callback of this._visibilityGuards) {
|
for (const callback of this._visibilityGuards) {
|
||||||
|
|||||||
Reference in new issue
Block a user