feat(app): layer catalog per engine

- src/app/layers/renderers.js: the layer renderer registry
  (registerLayerRenderers(engineKind, { [layerId]: factory }),
  rendererFor(layerId, engine), layerAvailableOn(layerId, engineKind)).
  Every application layer is registered for Cesium as CESIUM_BUILTIN (it
  still draws itself until its render/cesium.js); Navara factories come with
  the layer tasks. On it, the stable availability API phase 4 injects into
  its layer list: createLayerAvailability(engineKind) →
  { engine, isAvailable(id), available(), unavailable() } with { id, name }
  rows (src/app/layers/directory.js holds the ids and names in catalog
  order; a test keeps it equal to the constructed catalog).
- createApplicationCatalog({ engineKind, availability }) constructs only the
  layers the engine can show, in the same order (shared instances made
  once, on demand); the catalog carries `availability` and the others'
  metadata (`unavailableMetadata`). Cesium builds the same 30 layers in the
  same order as before; Navara builds none yet (no Navara renderers) - the
  standalone app now asks the catalog for it instead of making an empty one.
- catalogControlServices: a role whose layer the engine leaves out gets an
  empty stand-in (absentLayerControlServices folded in); any other missing
  layer is still a wiring error.
- LayerLifecycle(viewer, { engine }): every module call (init, enable,
  update, disable, destroy) gets { signal, engine } as its second argument;
  viewer is null on Navara.
- Links: a link that turns on layers this engine leaves out keeps them in
  the durable state (re-saved links carry them; back on Cesium they turn
  on) and activates nothing; a notice names them ("NOT ON THIS ENGINE · Kept
  in the link: …"). The Navara notice lists the engine's data layers from
  the registry.
- The dev launcher's CCTV wiring check matches the new catalog form.
This commit is contained in:
German Khokhlov committed 2026-10-16 22:07:55 +04:00
1 parent e1be2eac19
commit b0cbed081b
23 files changed
+826 -146

No files matched your search

+125
View File
@@ -0,0 +1,125 @@
// Which engine draws which layer: the layer renderer registry (plan, "layer
// renderer interface") and the layer availability API built on it.
//
// A renderer factory `(engine, options) => renderer` draws one layer on one
// engine kind (`render/cesium.js`, `render/navara.js` of a layer). Cesium
// layers still draw themselves until their render/cesium.js is extracted
// (the *a tasks): they are registered as CESIUM_BUILTIN. Cesium entries are
// registered here, statically (main chunk); Navara factories are registered
// by the Navara mode after its import() (src/app/navaraMode.js).
//
// ## Layer availability (stable API, used by phase 4's layer list)
//
// import { createLayerAvailability } from './app/layers/renderers.js';
// const availability = createLayerAvailability(engine.kind);
// availability.engine // 'cesium' | 'navara'
// availability.isAvailable(id) // can this engine show layer `id`?
// availability.available() // [{ id, name }] in catalog order
// availability.unavailable() // [{ id, name }] the catalog leaves out
//
// The object is frozen; available() and unavailable() return fresh arrays.
// It reads the registry when called, so a Navara factory registered later
// (after the Navara mode loads) shows up. `name` is the layer's display name
// (src/app/layers/directory.js). Phase 4 injects it as its `availability`
// provider (`{ engine, unavailable() }` is the part it reads).
import { APPLICATION_LAYER_DIRECTORY } from './directory.js';
const ENGINE_KINDS = new Set(['cesium', 'navara']);
/** The layer draws itself on Cesium (no separate renderer yet). */
export const CESIUM_BUILTIN = Object.freeze({ builtin: 'cesium' });
const isFactory = (value) =>
typeof value === 'function' || value === CESIUM_BUILTIN;
/** A registry of layer renderers per engine kind. */
export function createRendererRegistry() {
const byEngine = new Map();
const entries = (engineKind) => byEngine.get(engineKind) ?? new Map();
return {
/** Adds `{ [layerId]: factory }` for an engine kind; one per layer. */
register(engineKind, factories) {
if (typeof engineKind !== 'string' || !engineKind)
throw new TypeError('registerLayerRenderers needs an engine kind');
let map = byEngine.get(engineKind);
if (!map) byEngine.set(engineKind, (map = new Map()));
for (const [layerId, factory] of Object.entries(factories ?? {})) {
if (!isFactory(factory))
throw new TypeError(
`Layer renderer for ${layerId} on ${engineKind} is not a factory`,
);
if (map.has(layerId))
throw new Error(
`Layer ${layerId} already has a renderer on ${engineKind}`,
);
map.set(layerId, factory);
}
},
/**
* The renderer of a layer on `engine` (factory(engine, options)), or null
* when the engine has none or the layer still draws itself (Cesium).
*/
rendererFor(layerId, engine, options = {}) {
const factory = entries(engine?.kind).get(layerId);
return typeof factory === 'function' ? factory(engine, options) : null;
},
layerAvailableOn: (layerId, engineKind) => entries(engineKind).has(layerId),
availableIds: (engineKind) => [...entries(engineKind).keys()],
};
}
const defaultRegistry = createRendererRegistry();
/** Registers layer renderers on the application's registry. */
export const registerLayerRenderers = (engineKind, factories) =>
defaultRegistry.register(engineKind, factories);
/** The application registry's renderer of a layer on an engine, or null. */
export const rendererFor = (layerId, engine, options) =>
defaultRegistry.rendererFor(layerId, engine, options);
/** Whether the application can show a layer on an engine kind. */
export const layerAvailableOn = (layerId, engineKind) =>
defaultRegistry.layerAvailableOn(layerId, engineKind);
// Every application layer draws itself on Cesium today.
registerLayerRenderers(
'cesium',
Object.fromEntries(
APPLICATION_LAYER_DIRECTORY.map(({ id }) => [id, CESIUM_BUILTIN]),
),
);
/**
* @typedef {object} LayerAvailability
* @property {'cesium' | 'navara'} engine
* @property {(layerId: string) => boolean} isAvailable
* @property {() => { id: string, name: string }[]} available
* @property {() => { id: string, name: string }[]} unavailable
*/
/**
* The layers an engine kind can show (stable API; see the module comment).
* @param {'cesium' | 'navara'} engineKind
* @param {{ registry?: object, directory?: { id: string, name: string }[] }} [options]
* Test seams: the registry and the layer directory.
* @returns {LayerAvailability}
*/
export function createLayerAvailability(
engineKind,
{ registry = defaultRegistry, directory = APPLICATION_LAYER_DIRECTORY } = {},
) {
if (!ENGINE_KINDS.has(engineKind))
throw new TypeError(`Unknown engine kind for layers: ${engineKind}`);
const isAvailable = (layerId) =>
registry.layerAvailableOn(layerId, engineKind);
const rows = (wanted) =>
directory
.filter(({ id }) => isAvailable(id) === wanted)
.map(({ id, name }) => ({ id, name }));
return Object.freeze({
engine: engineKind,
isAvailable,
available: () => rows(true),
unavailable: () => rows(false),
});
}