diff --git a/scripts/main-chunk-baseline.json b/scripts/main-chunk-baseline.json index deca26aa2..6b8e30420 100644 --- a/scripts/main-chunk-baseline.json +++ b/scripts/main-chunk-baseline.json @@ -1,6 +1,6 @@ { - "gzipBytes": 2021566, - "allowanceBytes": 20216, - "measuredAt": "merge of origin/main ac28a11e, keyless build (GOOGLE_MAPS_API_KEY and CESIUM_ION_TOKEN empty), entry index-ChmLBAMR.js, 6 220 779 bytes, gzip level 9; main itself grew 93 873 bytes gzip-9 from the merge base (1 853 283 to 1 947 156)", + "gzipBytes": 2066560, + "allowanceBytes": 20666, + "measuredAt": "HEAD 47d63128 with the layer displays loaded through import() (src/app/layers/displays.js), keyless build (GOOGLE_MAPS_API_KEY and CESIUM_ION_TOKEN empty), entry index-DEjtUM5w.js, 6 322 899 bytes, gzip level 9; HEAD 47d63128 itself measured 2 113 641 and the merge of main 92c8f8d9 2 090 274", "allowance": "1% of the baseline; growth past it fails npm run check:main-chunk until the baseline is raised on purpose" } diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index 8ed62c234..f25089b4f 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -3447,6 +3447,7 @@ "src/globe/navara/followGestures.js", "src/app/layers/directory.js", "src/app/layers/renderers.js", + "src/app/layers/displays.js", "src/layers/aircraft/render/display.js", "src/layers/flights/render/display.js", "src/layers/military/render/display.js", @@ -3551,6 +3552,7 @@ "src/layers/cctv/render/cesiumMeshes.js", "src/layers/cctv/render/meshes.js", "src/layers/cctv/render/display.js", + "src/layers/cctv/worldClick.js", "src/layers/transit/render/display.js", "src/layers/earthquakes/render/display.js", "src/layers/airQuality/render/display.js", @@ -4229,6 +4231,7 @@ "src/globe/cesium/follow.js", "src/app/layers/directory.js", "src/app/layers/renderers.js", + "src/app/layers/displays.js", "src/layers/aircraft/render/display.js", "src/data/selectionDispatcher.js", "src/layers/shared/color.js", @@ -4293,6 +4296,7 @@ "src/layers/cctv/render/cesiumMeshes.js", "src/layers/cctv/render/meshes.js", "src/layers/cctv/render/display.js", + "src/layers/cctv/worldClick.js", "src/layers/transit/render/display.js", "src/layers/earthquakes/render/display.js", "src/layers/airQuality/render/display.js", diff --git a/src/app/layers/cctv.js b/src/app/layers/cctv.js index 40ca15de4..13fd536a7 100644 --- a/src/app/layers/cctv.js +++ b/src/app/layers/cctv.js @@ -1,4 +1,4 @@ -import { bindCctvWorldClickGesture } from '../../layers/cctv/render/display.js'; +import { bindCctvWorldClickGesture } from '../../layers/cctv/worldClick.js'; import { createCctvLayer } from '../../layers/cctv/index.js'; import { rendererFor } from './renderers.js'; import * as credits from '../../data/dataCredits.js'; diff --git a/src/app/layers/displays.js b/src/app/layers/displays.js new file mode 100644 index 000000000..51ad3a980 --- /dev/null +++ b/src/app/layers/displays.js @@ -0,0 +1,33 @@ +// Every layer's display (render/display.js): one chunk, which the layer +// manifest (./renderers.js) loads through import() on every engine kind, +// before an engine's catalog is built. Every start loads them all, so they +// share one request and one compression context. + +export { createFlightsDisplay } from '../../layers/flights/render/display.js'; +export { createMilitaryDisplay } from '../../layers/military/render/display.js'; +export { createLocalAdsbDisplay } from '../../layers/localAdsb/render/display.js'; +export { createVesselsDisplay } from '../../layers/vessels/render/display.js'; +export { createSatellitesDisplay } from '../../layers/satellites/render/display.js'; +export { createLaunchesDisplay } from '../../layers/launches/render/display.js'; +export { createAlprDisplay } from '../../layers/alpr/render/display.js'; +export { createBikeshareDisplay } from '../../layers/bikeshare/render/display.js'; +export { createOsmPlacesDisplay } from '../../layers/osmPlaces/render/display.js'; +export { createRadioDisplay } from '../../layers/radio/render/display.js'; +export { createTransitDisplay } from '../../layers/transit/render/display.js'; +export { createCctvDisplay } from '../../layers/cctv/render/display.js'; +export { createEarthquakesDisplay } from '../../layers/earthquakes/render/display.js'; +export { createAirQualityDisplay } from '../../layers/airQuality/render/display.js'; +export { createPerimetersDisplay } from '../../layers/perimeters/render/display.js'; +export { createGnssDisplay } from '../../layers/gnss/render/display.js'; +export { createGhostMapsDisplay } from '../../layers/ghostMaps/render/display.js'; +export { createDroneAirspaceDisplay } from '../../layers/droneAirspace/render/display.js'; +export { createCyclonesDisplay } from '../../layers/cyclones/render/display.js'; +export { createCablesDisplay } from '../../layers/submarineCables/render/display.js'; +export { createInstallationsDisplay } from '../../layers/installations/render/display.js'; +export { createAwarenessDisplay } from '../../layers/awareness/render/display.js'; +export { createInfrastructureDisplay } from '../../layers/infrastructure/render/display.js'; +export { createStreetLevelDisplay } from '../../layers/streetLevel/render/display.js'; +export { createTrafficDisplay } from '../../layers/traffic/render/display.js'; +export { createDirectionsDisplay } from '../../layers/directions/render/display.js'; +export { createFirmsDisplay } from '../../layers/firms/render/display.js'; +export { createRecentImageryDisplay } from '../../layers/recentImagery/render/display.js'; diff --git a/src/app/layers/renderers.js b/src/app/layers/renderers.js index 4d744ce93..1d955dfff 100644 --- a/src/app/layers/renderers.js +++ b/src/app/layers/renderers.js @@ -6,14 +6,16 @@ // One entry per layer of the directory (src/app/layers/directory.js), in // its order, of one of three kinds: // -// - `display(factory, needs, backends)`: the layer's render/display.js, the +// - `display(name, needs, backends)`: the layer's render/display.js, the // same factory `(engine, options) => renderer` on every engine, drawn // through the contract primitives in `needs` (docs/GLOBE-PRIMITIVES.md). -// `backends` is the display's engine-specific option, keyed by engine -// kind: an import() loader of `{ option: value }` merged into the -// display's options on that engine (CCTV's meshes, Global Context's -// pulses on Navara). The displays are static (the main chunk, as before): -// the layers build their renderers synchronously. +// The factory is the export `name` of ./displays.js, one chunk loaded +// through import() on every engine kind, outside the Cesium-mode main +// chunk (but for the displays a layer package also imports as its +// default); the entry's `display` is its loader. `backends` is the +// display's engine-specific option, keyed by engine kind: an import() +// loader of `{ option: value }` merged into the display's options on that +// engine (CCTV's meshes, Global Context's pulses on Navara). // - `drawsItself(needs)`: no renderer; the layer draws itself through the // contract primitives in `needs` (Wildlife Sightings). // - `perEngine({ [kind]: loader | DRAWS_ITSELF })`: a layer with no display @@ -24,7 +26,8 @@ // // Every import() here is resolved by `loadLayerRenderers(kind)`, which the // page awaits before it builds an engine's catalog (src/standalone/ -// application.js), so a Cesium page fetches none of the Navara ones. +// application.js), so a Cesium page fetches its displays and none of the +// Navara renderers. // `rendererFor(id, engine, options)` then answers synchronously: the // layer's renderer on that engine, or null when it draws itself or the // engine cannot show it. @@ -47,34 +50,6 @@ import { ENGINE_KINDS } from '../../globe/contract.js'; import { APPLICATION_LAYER_DIRECTORY } from './directory.js'; -import { createFlightsDisplay } from '../../layers/flights/render/display.js'; -import { createMilitaryDisplay } from '../../layers/military/render/display.js'; -import { createLocalAdsbDisplay } from '../../layers/localAdsb/render/display.js'; -import { createSatellitesDisplay } from '../../layers/satellites/render/display.js'; -import { createVesselsDisplay } from '../../layers/vessels/render/display.js'; -import { createLaunchesDisplay } from '../../layers/launches/render/display.js'; -import { createOsmPlacesDisplay } from '../../layers/osmPlaces/render/display.js'; -import { createAlprDisplay } from '../../layers/alpr/render/display.js'; -import { createBikeshareDisplay } from '../../layers/bikeshare/render/display.js'; -import { createRadioDisplay } from '../../layers/radio/render/display.js'; -import { createTransitDisplay } from '../../layers/transit/render/display.js'; -import { createCctvDisplay } from '../../layers/cctv/render/display.js'; -import { createEarthquakesDisplay } from '../../layers/earthquakes/render/display.js'; -import { createAirQualityDisplay } from '../../layers/airQuality/render/display.js'; -import { createGnssDisplay } from '../../layers/gnss/render/display.js'; -import { createPerimetersDisplay } from '../../layers/perimeters/render/display.js'; -import { createDroneAirspaceDisplay } from '../../layers/droneAirspace/render/display.js'; -import { createFirmsDisplay } from '../../layers/firms/render/display.js'; -import { createGhostMapsDisplay } from '../../layers/ghostMaps/render/display.js'; -import { createCyclonesDisplay } from '../../layers/cyclones/render/display.js'; -import { createCablesDisplay } from '../../layers/submarineCables/render/display.js'; -import { createInfrastructureDisplay } from '../../layers/infrastructure/render/display.js'; -import { createInstallationsDisplay } from '../../layers/installations/render/display.js'; -import { createAwarenessDisplay } from '../../layers/awareness/render/display.js'; -import { createStreetLevelDisplay } from '../../layers/streetLevel/render/display.js'; -import { createTrafficDisplay } from '../../layers/traffic/render/display.js'; -import { createDirectionsDisplay } from '../../layers/directions/render/display.js'; -import { createRecentImageryDisplay } from '../../layers/recentImagery/render/display.js'; /** * The contract's drawing primitives (docs/GLOBE-PRIMITIVES.md). They are @@ -119,11 +94,16 @@ const checkLoaders = (loaders, allowDrawsItself) => { }; /** A layer drawn by its display on every engine (see the module comment). */ -function display(factory, needs, backends = {}) { - if (typeof factory !== 'function') - throw new TypeError('A layer display is a factory'); +function display(name, needs, backends = {}) { + if (typeof name !== 'string') + throw new TypeError('A layer display is named by its export'); return Object.freeze({ - display: factory, + display: async () => { + const factory = (await displays())[name]; + if (typeof factory !== 'function') + throw new TypeError(`No layer display ${name} in ./displays.js`); + return factory; + }, needs: checkNeeds(needs), backends: checkLoaders(backends, false), }); @@ -150,6 +130,9 @@ const navaraWeather = async () => { const navaraOnly = (navara, others = DRAWS_ITSELF) => perEngine({ cesium: others, navara }); +// The layers' displays (./displays.js), one chunk. +const displays = () => import('./displays.js'); + /** How every application layer draws, by id, in directory order. */ export const LAYER_MANIFEST = Object.freeze({ // The Bhote Koshi event and locator draw themselves on Cesium @@ -166,44 +149,47 @@ export const LAYER_MANIFEST = Object.freeze({ drawStorySource(engine, navaraNative(engine), data); }), // The aircraft family. - flights: display(createFlightsDisplay, ['movers', 'lines']), - military: display(createMilitaryDisplay, ['movers', 'lines']), - 'local-adsb': display(createLocalAdsbDisplay, ['movers', 'lines']), - earthquakes: display(createEarthquakesDisplay, ['ground']), - 'air-quality': display(createAirQualityDisplay, ['ground']), - 'fire-perimeters': display(createPerimetersDisplay, ['ground']), - 'gnss-integrity': display(createGnssDisplay, ['ground']), - 's2-ghostmaps': display(createGhostMapsDisplay, ['movers', 'ground']), - 'drone-airspace': display(createDroneAirspaceDisplay, ['ground']), + flights: display('createFlightsDisplay', ['movers', 'lines']), + military: display('createMilitaryDisplay', ['movers', 'lines']), + 'local-adsb': display('createLocalAdsbDisplay', ['movers', 'lines']), + earthquakes: display('createEarthquakesDisplay', ['ground']), + 'air-quality': display('createAirQualityDisplay', ['ground']), + 'fire-perimeters': display('createPerimetersDisplay', ['ground']), + 'gnss-integrity': display('createGnssDisplay', ['ground']), + 's2-ghostmaps': display('createGhostMapsDisplay', ['movers', 'ground']), + 'drone-airspace': display('createDroneAirspaceDisplay', ['ground']), // Draws itself through the globe contract (engine.movers, engine.lines). 'wildlife-sightings': drawsItself(['movers', 'lines']), - 'alpr-cameras': display(createAlprDisplay, ['movers']), - 'street-level': display(createStreetLevelDisplay, ['movers', 'ground']), - satellites: display(createSatellitesDisplay, ['movers', 'lines']), - 'rocket-launches': display(createLaunchesDisplay, ['movers', 'lines']), - traffic: display(createTrafficDisplay, ['movers', 'ground']), + 'alpr-cameras': display('createAlprDisplay', ['movers']), + 'street-level': display('createStreetLevelDisplay', ['movers', 'ground']), + satellites: display('createSatellitesDisplay', ['movers', 'lines']), + 'rocket-launches': display('createLaunchesDisplay', ['movers', 'lines']), + traffic: display('createTrafficDisplay', ['movers', 'ground']), // CCTV's display, with the Navara backend of its plane and volume // (render/meshes.js). - cctv: display(createCctvDisplay, ['movers', 'lines'], { + cctv: display('createCctvDisplay', ['movers', 'lines'], { navara: async () => ({ navaraMeshes: (await import('../../layers/cctv/render/navaraMeshes.js')) .createCctvNavaraMeshes, }), }), - radio: display(createRadioDisplay, ['movers']), - transit: display(createTransitDisplay, ['movers', 'lines']), - bikeshare: display(createBikeshareDisplay, ['movers']), - directions: display(createDirectionsDisplay, ['movers', 'ground']), - 'recent-imagery': display(createRecentImageryDisplay, ['ground', 'imagery']), - 'osm-places': display(createOsmPlacesDisplay, ['movers']), - 'ais-live-vessels': display(createVesselsDisplay, ['movers', 'lines']), - 'military-installations': display(createInstallationsDisplay, [ + radio: display('createRadioDisplay', ['movers']), + transit: display('createTransitDisplay', ['movers', 'lines']), + bikeshare: display('createBikeshareDisplay', ['movers']), + directions: display('createDirectionsDisplay', ['movers', 'ground']), + 'recent-imagery': display('createRecentImageryDisplay', [ + 'ground', + 'imagery', + ]), + 'osm-places': display('createOsmPlacesDisplay', ['movers']), + 'ais-live-vessels': display('createVesselsDisplay', ['movers', 'lines']), + 'military-installations': display('createInstallationsDisplay', [ 'movers', 'ground', ]), // Global Context's display, with the Navara backend of its range pulses // (render/pulseDisc.js). - 'military-awareness': display(createAwarenessDisplay, ['lines'], { + 'military-awareness': display('createAwarenessDisplay', ['lines'], { navara: async () => ({ navaraPulses: ( await import('../../layers/awareness/render/navaraPulses.js') @@ -220,27 +206,27 @@ export const LAYER_MANIFEST = Object.freeze({ 'weather-radar': navaraOnly(navaraWeather), 'weather-satellite': navaraOnly(navaraWeather), 'weather-lightning': navaraOnly(navaraWeather), - 'weather-cyclones': display(createCyclonesDisplay, ['movers', 'ground']), + 'weather-cyclones': display('createCyclonesDisplay', ['movers', 'ground']), // The open map overlays drape themselves: Cesium imagery on the viewer, // engine.maps.addOverlay on Navara (src/data/osmOverlays.js). 'sea-marks': perEngine({ cesium: DRAWS_ITSELF, navara: DRAWS_ITSELF }), 'ski-pistes': perEngine({ cesium: DRAWS_ITSELF, navara: DRAWS_ITSELF }), - 'local-datacenters': display(createInfrastructureDisplay, [ + 'local-datacenters': display('createInfrastructureDisplay', [ 'movers', 'lines', 'ground', ]), - 'local-dams': display(createInfrastructureDisplay, [ + 'local-dams': display('createInfrastructureDisplay', [ 'movers', 'lines', 'ground', ]), - 'telegeography-submarine-cables': display(createCablesDisplay, [ + 'telegeography-submarine-cables': display('createCablesDisplay', [ 'movers', 'lines', 'ground', ]), - 'local-firms': display(createFirmsDisplay, ['movers', 'ground', 'imagery']), + 'local-firms': display('createFirmsDisplay', ['movers', 'ground', 'imagery']), }); /** The manifest entry of a layer, or null. */ @@ -257,21 +243,42 @@ const entryAvailableOn = (entry, engineKind) => { return Object.hasOwn(entry.engines, engineKind); }; -/** The import() loader of a layer on an engine kind, or null. */ +/** The engine-specific import() loader of a layer on an engine kind, or null. */ const loaderOf = (entry, engineKind) => { const loader = entry.backends?.[engineKind] ?? entry.engines?.[engineKind]; return typeof loader === 'function' ? loader : null; }; -// What loadLayerRenderers resolved: engine kind → layer id → value (the -// backend's options, or the engine-specific factory). +/** Whether a layer has an import() to load on an engine kind. */ +const loadsOn = (entry, engineKind) => + entryAvailableOn(entry, engineKind) && + Boolean(entry.display || loaderOf(entry, engineKind)); + +/** + * What a layer's import()s resolve to on an engine kind: its display with + * the backend's options (`{ display, options }`), or the engine-specific + * factory. + */ +const loadEntry = async (entry, engineKind) => { + const loader = loaderOf(entry, engineKind); + if (!entry.display) return loader(); + const [factory, options] = await Promise.all([ + entry.display(), + loader ? loader() : {}, + ]); + return { display: factory, options }; +}; + +// What loadLayerRenderers resolved: engine kind → layer id → value +// (loadEntry's). const loaded = new Map(); const loading = new Map(); /** - * Loads every import() the manifest has for `engineKind`, once per page: - * concurrent calls share the load, and after a failure the next call loads - * what is still missing. + * Loads every import() the manifest has for `engineKind` (the displays and + * that engine's renderers and backends), once per page: concurrent calls + * share the load, and after a failure the next call loads what is still + * missing. * @param {string} engineKind One of ENGINE_KINDS. * @returns {Promise} */ @@ -283,8 +290,8 @@ export function loadLayerRenderers(engineKind) { const done = loaded.get(engineKind); const load = Promise.all( Object.entries(LAYER_MANIFEST).map(async ([id, entry]) => { - const loader = loaderOf(entry, engineKind); - if (loader && !done.has(id)) done.set(id, await loader()); + if (loadsOn(entry, engineKind) && !done.has(id)) + done.set(id, await loadEntry(entry, engineKind)); }), ).then( () => { @@ -338,15 +345,15 @@ export function rendererFor(layerId, engine, options = {}) { const standIn = standIns.get(engineKind)?.get(layerId); if (standIn) return standIn(engine, options); const entry = entryOf(LAYER_MANIFEST, layerId); - if (!entryAvailableOn(entry, engineKind)) return null; - const loader = loaderOf(entry, engineKind); - const value = loader ? loaded.get(engineKind)?.get(layerId) : undefined; - if (loader && value === undefined) + if (!loadsOn(entry, engineKind)) return null; + const value = loaded.get(engineKind)?.get(layerId); + if (value === undefined) throw new Error( `Layer ${layerId} on ${engineKind}: its renderer is not loaded (loadLayerRenderers)`, ); - if (entry.display) return entry.display(engine, { ...options, ...value }); - return typeof value === 'function' ? value(engine, options) : null; + if (entry.display) + return value.display(engine, { ...options, ...value.options }); + return value(engine, options); } /** Whether the application can show a layer on an engine kind. */ diff --git a/src/app/layers/renderers.test.mjs b/src/app/layers/renderers.test.mjs index 0221e6aa7..be304cfd3 100644 --- a/src/app/layers/renderers.test.mjs +++ b/src/app/layers/renderers.test.mjs @@ -6,6 +6,7 @@ import { LAYER_MANIFEST, createLayerAvailability, layerAvailableOn, + loadLayerRenderers, registerLayerRenderers, rendererFor, } from './renderers.js'; @@ -118,6 +119,23 @@ test('the layers drawn through one display, and the ones that draw without one', }); }); +test('a display or an engine-specific renderer answers only after its engine loaded them', () => { + const engine = createFakeEngine({ kind: 'navara' }); + // Nothing is loaded yet: the later tests load them. + assert.throws( + () => rendererFor('weather-radar', engine), + /weather-radar on navara: its renderer is not loaded/, + ); + assert.throws(() => rendererFor('cctv', engine), /not loaded/); + const cesium = createFakeEngine(); + assert.throws( + () => rendererFor('flights', cesium), + /flights on cesium: its renderer is not loaded/, + ); + cesium.destroy(); + engine.destroy(); +}); + test('the layers drawn through one display get the same factory on every engine', async () => { const displays = { flights: ['flights', 'createFlightsDisplay'], @@ -164,11 +182,12 @@ test('the layers drawn through one display get the same factory on every engine' const factory = ( await import(`../../layers/${directory}/render/display.js`) )[name]; - assert.equal(LAYER_MANIFEST[id].display, factory, id); + assert.equal(await LAYER_MANIFEST[id].display(), factory, id); // Each display's own tests build it with its services; the aircraft // family needs none beyond an atlas. if (!['flights', 'military', 'local-adsb'].includes(id)) continue; for (const kind of ENGINE_KINDS) { + await loadLayerRenderers(kind); const engine = createFakeEngine({ kind }); const display = rendererFor(id, engine, { drawAtlas }); assert.equal(display.engine, engine, `${id} on ${kind}`); @@ -194,17 +213,6 @@ test('a layer that draws itself has no renderer; an unknown engine none either', engine.destroy(); }); -test('an engine-specific renderer answers only after its engine loaded them', () => { - const engine = createFakeEngine({ kind: 'navara' }); - // This test file never loads Navara's renderers. - assert.throws( - () => rendererFor('weather-radar', engine), - /weather-radar on navara: its renderer is not loaded/, - ); - assert.throws(() => rendererFor('cctv', engine), /not loaded/); - engine.destroy(); -}); - test('a test engine of its own kind brings its renderers', () => { const made = []; registerLayerRenderers('renderers-test', { diff --git a/src/data/alprCameras.test.mjs b/src/data/alprCameras.test.mjs index 0cfb0cce3..894695217 100644 --- a/src/data/alprCameras.test.mjs +++ b/src/data/alprCameras.test.mjs @@ -1291,6 +1291,11 @@ test('unsupported ALPR coverage suppresses the nearby count and names the extrac import { createGroundFloor } from '../services/groundFloor.js'; import { createTerrainHeights } from '../services/terrainHeights.js'; import { createApplicationRequestServices } from '../services/requests.js'; +import { loadLayerRenderers } from '../app/layers/renderers.js'; + +// The layers draw with the manifest's displays, which load through +// import() (src/app/layers/renderers.js). +await loadLayerRenderers('cesium'); test('slow ALPR floors share real terrain requests across preparation, render and pans; disable aborts', async () => { const requests = []; diff --git a/src/data/bikeshare.test.mjs b/src/data/bikeshare.test.mjs index 7d2682d18..7682d6d10 100644 --- a/src/data/bikeshare.test.mjs +++ b/src/data/bikeshare.test.mjs @@ -10,6 +10,11 @@ import { _setBikeshareSelectionStateForTest, createBikeshareSelectedOverlayEntry, } from './bikeshare.js'; +import { loadLayerRenderers } from '../app/layers/renderers.js'; + +// The layers draw with the manifest's displays, which load through +// import() (src/app/layers/renderers.js). +await loadLayerRenderers('cesium'); function makeRecord() { return { diff --git a/src/data/cctv.test.mjs b/src/data/cctv.test.mjs index 8b12612e0..8814171a4 100644 --- a/src/data/cctv.test.mjs +++ b/src/data/cctv.test.mjs @@ -83,6 +83,11 @@ import { IDLE_CAMERA_COLOR, LIVE_CAMERA_COLOR, } from '../layers/cctv/policy.js'; +import { loadLayerRenderers } from '../app/layers/renderers.js'; + +// The layers draw with the manifest's displays, which load through +// import() (src/app/layers/renderers.js). +await loadLayerRenderers('cesium'); const ASPECT = 16 / 9; const toRad = (deg) => (deg * Math.PI) / 180; diff --git a/src/data/cctvViewThinning.test.mjs b/src/data/cctvViewThinning.test.mjs index b31b48f24..b35cdb578 100644 --- a/src/data/cctvViewThinning.test.mjs +++ b/src/data/cctvViewThinning.test.mjs @@ -11,6 +11,11 @@ import cctvLayer, { _viewRecordIdsForTest, refreshHorizonCulling, } from './cctv.js'; +import { loadLayerRenderers } from '../app/layers/renderers.js'; + +// The layers draw with the manifest's displays, which load through +// import() (src/app/layers/renderers.js). +await loadLayerRenderers('cesium'); const WIDTH = 800; const HEIGHT = 600; diff --git a/src/data/earthquakes.test.mjs b/src/data/earthquakes.test.mjs index c92681677..29673d804 100644 --- a/src/data/earthquakes.test.mjs +++ b/src/data/earthquakes.test.mjs @@ -20,6 +20,11 @@ import { installRenderGovernor, _resetRenderGovernorForTest, } from '../renderGovernor.js'; +import { loadLayerRenderers } from '../app/layers/renderers.js'; + +// The layers draw with the manifest's displays, which load through +// import() (src/app/layers/renderers.js). +await loadLayerRenderers('cesium'); const FULL_RAW = { id: 'us7000abcd', diff --git a/src/data/firmsCards.test.mjs b/src/data/firmsCards.test.mjs index 86c7d2ce0..60951a854 100644 --- a/src/data/firmsCards.test.mjs +++ b/src/data/firmsCards.test.mjs @@ -18,6 +18,11 @@ import { getLocale, setLocale } from '../i18n/index.js'; import { reportMeshFloorCell, setMeshFloorPreferred } from './groundFloor.js'; import { createCesiumMoversEngine } from '../testSupport/cesiumMoversEngine.mjs'; import { createFakeEventHandlers } from '../globe/cesium/testing/fakeViewer.mjs'; +import { loadLayerRenderers } from '../app/layers/renderers.js'; + +// The layers draw with the manifest's displays, which load through +// import() (src/app/layers/renderers.js). +await loadLayerRenderers('cesium'); const NOW = Date.UTC(2026, 6, 17, 4, 0); const H = 3600000; diff --git a/src/data/militaryInstallations.test.mjs b/src/data/militaryInstallations.test.mjs index f0301af46..954632474 100644 --- a/src/data/militaryInstallations.test.mjs +++ b/src/data/militaryInstallations.test.mjs @@ -26,6 +26,11 @@ import { import * as Cesium from 'cesium'; import { getContextStore, registerEntityContext, selectEntityContext, getSelectedEntityContext, clearSelectedEntityContextForLayer } from './contextStore.js'; import { createFakeEngine } from '../globe/testing/fakeEngine.js'; +import { loadLayerRenderers } from '../app/layers/renderers.js'; + +// The layers draw with the manifest's displays, which load through +// import() (src/app/layers/renderers.js). +await loadLayerRenderers('cesium'); const LAYER_ID = 'military-installations'; diff --git a/src/data/radio.test.mjs b/src/data/radio.test.mjs index 5552cc8ce..43d4bcafc 100644 --- a/src/data/radio.test.mjs +++ b/src/data/radio.test.mjs @@ -53,6 +53,11 @@ import { } from './radio.js'; import { radioStationRow, withRadioLayer } from '../testSupport/radioLayerHarness.mjs'; import { radioSelectedMarker, radioTestEngine } from '../testSupport/radioTestEngine.mjs'; +import { loadLayerRenderers } from '../app/layers/renderers.js'; + +// The layers draw with the manifest's displays, which load through +// import() (src/app/layers/renderers.js). +await loadLayerRenderers('cesium'); const stations = [ { id: 'news', tags: ['News', 'Weather Radio', 'air traffic'] }, diff --git a/src/data/radioNextState.test.mjs b/src/data/radioNextState.test.mjs index ac1f82734..56b1277b1 100644 --- a/src/data/radioNextState.test.mjs +++ b/src/data/radioNextState.test.mjs @@ -5,6 +5,11 @@ import { radioLayer } from './radio.js'; import { setLocale } from '../i18n/index.js'; import { renderRadioState } from '../ui/radioPresentation.js'; import { radioStationRow, withRadioLayer } from '../testSupport/radioLayerHarness.mjs'; +import { loadLayerRenderers } from '../app/layers/renderers.js'; + +// The layers draw with the manifest's displays, which load through +// import() (src/app/layers/renderers.js). +await loadLayerRenderers('cesium'); const READY = 'Ready — playback starts only from your action'; const ids = [1, 2, 3].map((n) => `00000000-0000-4000-8000-00000000009${n}`); diff --git a/src/data/radioProxy.test.mjs b/src/data/radioProxy.test.mjs index 37796ed48..d377204c3 100644 --- a/src/data/radioProxy.test.mjs +++ b/src/data/radioProxy.test.mjs @@ -20,6 +20,11 @@ import { import { RADIO_REJECTED_STATION_COUNT_MAX as SERVER_REJECTED_MAX } from '../../server/providers/radio/constants.js'; import { RADIO_REJECTED_STATION_COUNT_MAX as CLIENT_REJECTED_MAX } from '../layers/radio/policy.js'; import { radioTestEngine } from '../testSupport/radioTestEngine.mjs'; +import { loadLayerRenderers } from '../app/layers/renderers.js'; + +// The layers draw with the manifest's displays, which load through +// import() (src/app/layers/renderers.js). +await loadLayerRenderers('cesium'); const UUID = '12345678-1234-4234-8234-123456789abc'; const publicLookup = async () => [{ address: '93.184.216.34', family: 4 }]; diff --git a/src/data/rocketLaunches.test.mjs b/src/data/rocketLaunches.test.mjs index b24c1309f..0475feedb 100644 --- a/src/data/rocketLaunches.test.mjs +++ b/src/data/rocketLaunches.test.mjs @@ -66,6 +66,11 @@ import { satelliteCatalogModeChanged, scoreSatelliteNameMatch, } from './satellites.js'; +import { loadLayerRenderers } from '../app/layers/renderers.js'; + +// The layers draw with the manifest's displays, which load through +// import() (src/app/layers/renderers.js). +await loadLayerRenderers('cesium'); const NOW = new Date('2026-07-27T00:00:00Z'); diff --git a/src/data/satellitesTrackedRefresh.test.mjs b/src/data/satellitesTrackedRefresh.test.mjs index 5eb94dd45..71a6bc6e0 100644 --- a/src/data/satellitesTrackedRefresh.test.mjs +++ b/src/data/satellitesTrackedRefresh.test.mjs @@ -19,6 +19,11 @@ import { import satellitesLayer from './satellites.js'; import { createTrackedOverlayEntry } from './trackedReadout.js'; import { createFakeEngine } from '../globe/testing/fakeEngine.js'; +import { loadLayerRenderers } from '../app/layers/renderers.js'; + +// The layers draw with the manifest's displays, which load through +// import() (src/app/layers/renderers.js). +await loadLayerRenderers('cesium'); const L1 = '1 25544U 98067A 08264.51782528 -.00002182 00000-0 -11606-4 0 2927'; diff --git a/src/data/trackedCardLocale.test.mjs b/src/data/trackedCardLocale.test.mjs index 6e3e3ba2f..3bbce86d0 100644 --- a/src/data/trackedCardLocale.test.mjs +++ b/src/data/trackedCardLocale.test.mjs @@ -9,6 +9,11 @@ import { } from './satellites.js'; import { setLocale } from '../i18n/index.js'; import { createFakeEngine } from '../globe/testing/fakeEngine.js'; +import { loadLayerRenderers } from '../app/layers/renderers.js'; + +// The layers draw with the manifest's displays, which load through +// import() (src/app/layers/renderers.js). +await loadLayerRenderers('cesium'); const L1 = '1 25544U 98067A 08264.51782528 -.00002182 00000-0 -11606-4 0 2927'; diff --git a/src/data/trafficTiming.test.mjs b/src/data/trafficTiming.test.mjs index 93fb6b253..dd4a251c1 100644 --- a/src/data/trafficTiming.test.mjs +++ b/src/data/trafficTiming.test.mjs @@ -181,6 +181,10 @@ test('traffic timing pairs real ordering to the scheduling change and guards re- }], }); const traffic = await server.ssrLoadModule('/src/data/traffic.js'); + // The layer draws with the manifest's display, which loads through + // import() (src/app/layers/renderers.js), in this server's module graph. + const renderers = await server.ssrLoadModule('/src/app/layers/renderers.js'); + await renderers.loadLayerRenderers('cesium'); trafficLayer = traffic.default; globalThis.document = { documentElement: { dataset: {} } }; diff --git a/src/layers/cctv/render/display.js b/src/layers/cctv/render/display.js index d31b2afc5..94bff1f05 100644 --- a/src/layers/cctv/render/display.js +++ b/src/layers/cctv/render/display.js @@ -15,12 +15,9 @@ // choices (../rendering.js, ./style.js). import * as geo from '../../../geo/index.js'; -import { - bindTrackingClickGesture, - isTrackingClickGesture, -} from '../../../data/trackingClickGesture.js'; import { GIZMO_ID_PREFIX } from '../../../data/cctvGizmo.js'; import { CAMERA_ICON, IDLE_CAMERA_COLOR } from '../policy.js'; +import { bindCctvWorldClickGesture } from '../worldClick.js'; import { createCctvMeshes } from './meshes.js'; import { COVERAGE_DASH, @@ -87,18 +84,6 @@ export function planeOutline(shape, positions, orientation) { return [tl, corner(1, 1), corner(1, -1), corner(-1, -1), tl]; } -/** The tracking click gesture over engine.input (the CCTV world click). */ -export function bindCctvWorldClickGesture(handler, onClick, options = {}) { - bindTrackingClickGesture( - handler, - (click, gesture) => { - if (!isTrackingClickGesture(gesture)) return; - onClick(click); - }, - options, - ); -} - /** * @param {import('../../../globe/contract.js').GlobeEngine} engine * @param {{ state: object, services: object, navaraMeshes?: Function }} options diff --git a/src/layers/cctv/worldClick.js b/src/layers/cctv/worldClick.js new file mode 100644 index 000000000..092188ff2 --- /dev/null +++ b/src/layers/cctv/worldClick.js @@ -0,0 +1,19 @@ +// The CCTV world click: a tracking click over engine.input, shared by the +// display (render/display.js) and the application layer. + +import { + bindTrackingClickGesture, + isTrackingClickGesture, +} from '../../data/trackingClickGesture.js'; + +/** The tracking click gesture over engine.input (the CCTV world click). */ +export function bindCctvWorldClickGesture(handler, onClick, options = {}) { + bindTrackingClickGesture( + handler, + (click, gesture) => { + if (!isTrackingClickGesture(gesture)) return; + onClick(click); + }, + options, + ); +}