refactor(maps): describe map sources to the engine

The map source controller always hands engine.maps an engine-neutral
description of the registry source: imagery kind/style/accessToken and
credit, photoreal kind and credentials, terrain kind/accessToken. The
default registry carries those beside its Cesium factories. Only a
Cesium engine also gets the prebuilt provider, tileset and cached terrain
(Cesium-mode behaviour, ordering and same-provider guard unchanged); for
any other engine the controller builds nothing, falls back by description
when a setter rejects, and has no tile-failure watch.
This commit is contained in:
German Khokhlov committed 2026-10-15 19:01:00 +04:00
1 parent ab0acb0e09
commit 6dbd9c983e
4 files changed
+210 -18

No files matched your search

+102 -16
View File
@@ -15,12 +15,18 @@ const AUTOMATIC = Symbol('automatic switch');
* choices.
*
* With `engine` (the page's globe engine) every scene change goes through
* `engine.maps`: the controller keeps resolving, caching and watching
* providers and tilesets, and hands the engine ready sources. Supplied
* tilesets must then be ones the engine knows (its startup tileset) or has
* shown. On destroy the engine keeps what it shows; owned tilesets are
* released and disposed. Without an engine the controller applies sources
* to the viewer itself (standalone use and tests).
* `engine.maps` as an engine-neutral description of the registry source
* (imagery `kind`/`style`/`accessToken`, terrain `kind`/`accessToken`,
* photoreal credentials, credit). The registry's factories build Cesium
* objects, so only a Cesium engine also gets them prebuilt (`provider`,
* `tileset`, terrain `create`): the controller then keeps resolving, caching
* and watching them as before (supplied tilesets must be ones the engine
* knows, i.e. its startup tileset, or has shown). Any other engine builds
* its own sources from the descriptions; the controller constructs nothing
* for it, falls back by description when a setter rejects, and has no tile
* failure watch. On destroy the engine keeps what it shows; owned tilesets
* are released and disposed. Without an engine the controller applies
* sources to the viewer itself (standalone use and tests).
*/
export class MapSourceController {
constructor(
@@ -47,6 +53,8 @@ export class MapSourceController {
this._onError = onError;
this._requestRender = requestRender;
this._engine = engine;
/** Whether the engine takes the registry's prebuilt Cesium objects. */
this._prebuilt = engine?.kind === 'cesium';
this._scene = engine
? null
: createMapScene(viewer, { createImageryLayer });
@@ -246,6 +254,11 @@ export class MapSourceController {
}
async _activateTileset(source, gen) {
if (this._engine && !this._prebuilt) {
this._removeImageryLayer();
await this._engine.maps.setPhotorealTiles(this._describeTiles(source));
return;
}
let tileset = source.tileset;
if (!tileset) {
if (!source.createTileset)
@@ -259,10 +272,9 @@ export class MapSourceController {
// The engine adds an owned tileset to the scene when it shows it.
if (!source.tileset) this._ownedTilesets.add(tileset);
this._removeImageryLayer();
await this._engine.maps.setPhotorealTiles({
tileset,
credit: source.credit ?? null,
});
await this._engine.maps.setPhotorealTiles(
this._describeTiles(source, tileset),
);
return;
}
if (!source.tileset && !this._ownedTilesets.has(tileset)) {
@@ -285,6 +297,8 @@ export class MapSourceController {
}
async _activateGlobeStack(stack, gen) {
if (this._engine && !this._prebuilt)
return this._activateDescribedGlobeStack(stack, gen);
const resolution = await this._getImageryProvider(stack);
if (gen !== this._switchGen) return;
if (this._engine) return this._activateEngineGlobeStack(resolution, gen);
@@ -325,11 +339,9 @@ export class MapSourceController {
const source = this._sources.get(resolution.effectiveStackId);
this._removeImageryErrorListener?.();
this._removeImageryErrorListener = null;
await maps.setBaseImagery({
descriptor: source?.descriptor,
provider: resolution.provider,
credit: source?.credit ?? null,
});
await maps.setBaseImagery(
this._describeImagery(source, resolution.provider),
);
if (gen !== this._switchGen) return;
this._baseProvider = resolution.provider;
this._watchProvider(resolution, gen);
@@ -344,13 +356,87 @@ export class MapSourceController {
() => terrain.create({ signal: this._abort.signal }),
);
if (gen !== this._switchGen) return;
await maps.setTerrain({ id: terrain.id, create: () => result });
await maps.setTerrain(this._describeTerrain(terrain, result));
if (gen !== this._switchGen) return;
this._terrainMode = terrain.id;
}
return resolution;
}
// A non-Cesium engine builds every source from its description. A source it
// cannot build falls back as the construction fallback says.
async _activateDescribedGlobeStack(stack, gen, visited = new Set()) {
const { maps } = this._engine;
const source = this._sources.get(stack.id);
try {
await maps.setBaseImagery(this._describeImagery(source));
} catch (error) {
const fallback = source.constructionFallback;
if (
this._destroyed ||
!fallback ||
visited.has(stack.id) ||
!this.isStackAvailable(fallback.id)
)
throw error;
const resolution = await this._activateDescribedGlobeStack(
this.getStack(fallback.id),
gen,
new Set(visited).add(stack.id),
);
return resolution && { ...resolution, fallbackMessage: fallback.message };
}
if (gen !== this._switchGen) return;
await maps.setPhotorealTiles(null);
if (gen !== this._switchGen) return;
this._activeId = stack.id;
if (source.terrain && source.terrain.id !== this._terrainMode) {
await maps.setTerrain(this._describeTerrain(source.terrain));
if (gen !== this._switchGen) return;
this._terrainMode = source.terrain.id;
}
return { effectiveStackId: stack.id, fallbackMessage: null };
}
/** Engine description of an imagery source (+ a prebuilt Cesium provider). */
_describeImagery(source, provider) {
const { descriptor } = source;
return {
descriptor,
kind: descriptor.kind,
...(descriptor.style !== undefined && { style: descriptor.style }),
...(source.accessToken !== undefined && {
accessToken: source.accessToken,
}),
credit: source.credit ?? null,
...(provider && { provider }),
};
}
/** Engine description of a terrain source (+ the cached Cesium result). */
_describeTerrain(terrain, result) {
return {
id: terrain.id,
kind: terrain.kind ?? terrain.id,
...(terrain.accessToken !== undefined && {
accessToken: terrain.accessToken,
}),
...(result && { create: () => result }),
};
}
/** Engine description of a photoreal source (+ a prebuilt Cesium tileset). */
_describeTiles(source, tileset) {
return {
descriptor: source.descriptor,
kind: source.descriptor.kind,
credit: source.credit ?? null,
...(source.googleApiKey && { googleApiKey: source.googleApiKey }),
...(source.cesiumToken && { cesiumToken: source.cesiumToken }),
...(tileset && { tileset }),
};
}
/** The live base imagery provider, or null. */
_liveBaseProvider() {
return this._engine ? this._baseProvider : this._scene.baseProvider;
+72
View File
@@ -993,3 +993,75 @@ test('controller and Cesium engine: photoreal to a globe stack restores imagery
engine.destroy();
assert.deepEqual(credits(), []);
});
// --- On a non-Cesium engine: descriptions only, nothing Cesium is built ------
function navaraFixture() {
return import('../globe/testing/fakeEngine.js').then(
({ createFakeEngine }) => {
const engine = createFakeEngine({ kind: 'navara' });
const registry = createDefaultMapSources({
googleTileset: { show: true },
cesiumToken: 'ion-test',
googleApiKey: 'google-test',
});
const built = [];
for (const source of registry.sources) {
if (source.imagery)
source.imagery = () => built.push(`imagery:${source.descriptor.id}`);
if (source.terrain)
source.terrain = {
...source.terrain,
create: () => built.push(`terrain:${source.terrain.id}`),
};
}
const controller = new MapSourceController(null, { registry, engine });
return { engine, controller, built };
},
);
}
test('on a non-Cesium engine the controller sends descriptions and builds nothing', async () => {
const { engine, controller, built } = await navaraFixture();
await controller.setStack('esri-imagery');
const [imagery] = engine.fake.callsOf('maps.setBaseImagery')[0].args;
assert.equal(imagery.kind, 'esri-imagery');
assert.equal(imagery.descriptor.id, 'esri-imagery');
assert.match(imagery.credit, /Esri/);
assert.equal('provider' in imagery, false);
assert.equal(typeof imagery.imagery, 'undefined');
const [terrain] = engine.fake.callsOf('maps.setTerrain')[0].args;
assert.deepEqual(terrain, {
id: 'world',
kind: 'world',
accessToken: 'ion-test',
});
await controller.setStack('bing-aerial');
const [ion] = engine.fake.callsOf('maps.setBaseImagery')[1].args;
assert.equal(ion.kind, 'ion');
assert.equal(ion.accessToken, 'ion-test');
assert.ok(ion.style !== undefined);
await controller.setStack('photoreal');
const [photoreal] = engine.fake.callsOf('maps.setPhotorealTiles').at(-1).args;
assert.equal(photoreal.kind, 'photoreal');
assert.equal(photoreal.googleApiKey, 'google-test');
assert.equal(photoreal.cesiumToken, 'ion-test');
assert.equal('tileset' in photoreal, false);
assert.deepEqual(built, [], 'no Cesium provider, terrain or tileset built');
assert.equal(controller.getActiveId(), 'photoreal');
controller.destroy();
});
test('on a non-Cesium engine a source the engine cannot build falls back by description', async () => {
const { engine, controller } = await navaraFixture();
const setBaseImagery = engine.maps.setBaseImagery;
engine.maps.setBaseImagery = async (source) => {
if (source.kind === 'esri-imagery') throw new Error('no Esri here');
return setBaseImagery(source);
};
const state = await controller.setStack('esri-imagery');
assert.equal(state.activeId, 'osm');
assert.equal(state.lastError, 'Esri Satellite is unavailable; using OSM');
assert.equal(engine.fake.maps.imagery.kind, 'osm');
controller.destroy();
});
+13 -2
View File
@@ -12,8 +12,16 @@ export function createDefaultMapSources({
} = {}) {
const ionToken = String(cesiumToken || '').trim();
const hasIon = Boolean(ionToken);
const hasGoogle = Boolean(String(googleApiKey || '').trim());
const terrain = terrainSourceFor(ionToken);
// Engine-neutral descriptions ride beside the Cesium factories: a terrain
// `kind` (+ token), ion imagery's token and the photoreal credentials, so
// an engine that builds its own sources needs none of the factories.
const terrain = {
...terrainSourceFor(ionToken),
kind: hasIon ? 'world' : 'keyless',
...(hasIon && { accessToken: ionToken }),
};
const googleKey = String(googleApiKey || '').trim();
const hasGoogle = Boolean(googleKey);
return {
defaultId: googleTileset ? 'photoreal' : 'esri-imagery',
unknownId: 'photoreal',
@@ -33,11 +41,14 @@ export function createDefaultMapSources({
available: Boolean(googleTileset),
unavailableReason: photorealUnavailableReason(hasIon || hasGoogle),
tileset: googleTileset,
...(googleKey && { googleApiKey: googleKey }),
...(hasIon && { cesiumToken: ionToken }),
};
const imagery = imageryFactoryFor(descriptor, ionToken);
return {
...common,
imagery,
...(descriptor.kind === 'ion' && { accessToken: ionToken }),
terrain,
...(descriptor.id === 'esri-imagery'
? {
+23
View File
@@ -216,3 +216,26 @@ test('applyTerrain installs a terrain object or a provider', () => {
assert.deepEqual(calls, ['terrain']);
assert.equal(viewer.terrainProvider, 'provider');
});
test('default sources carry engine-neutral descriptions beside their Cesium factories', () => {
const sources = createDefaultMapSources({
cesiumToken: 'ion-test',
googleApiKey: 'google-test',
googleTileset: { show: true },
}).sources;
const byId = (id) => sources.find(({ descriptor }) => descriptor.id === id);
assert.equal(byId('bing-aerial').accessToken, 'ion-test');
assert.equal(
byId('osm').accessToken,
undefined,
'only ion imagery needs the token',
);
assert.equal(byId('osm').terrain.kind, 'world');
assert.equal(byId('osm').terrain.accessToken, 'ion-test');
assert.equal(byId('photoreal').googleApiKey, 'google-test');
assert.equal(byId('photoreal').cesiumToken, 'ion-test');
const keyless = createDefaultMapSources().sources;
const osm = keyless.find(({ descriptor }) => descriptor.id === 'osm');
assert.equal(osm.terrain.kind, 'keyless');
assert.equal(osm.terrain.accessToken, undefined);
});