mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 19:34:01 +00:00
refactor(maps): use the globe engine
MapSourceController (and MapStackController) take the page's globe engine: a globe stack becomes engine.maps.setBaseImagery with the controller's ready provider and credit, then setPhotorealTiles(null), then setTerrain with the cached terrain; a 3D stack becomes setPhotorealTiles with its tileset and credit; owned tilesets are released through maps.releasePhotorealTiles before disposal. The controller keeps its provider/terrain/tileset caches, construction and tile-failure fallbacks, switch generations and error watch. The scene builds its map stack controller with its engine; without an engine the controller applies sources to the viewer as before. The photoreal-to- globe sequence (imagery, credit, globe, tileset) is tested end to end on the Cesium engine.
This commit is contained in:
1 parent
83c13f330d
commit
3e3aa0bd9f
4 files changed
+258
-7
No files matched your search
@@ -105,6 +105,8 @@ export async function createApplicationScene({
|
||||
|
||||
const mapStackController = new MapController(viewer, {
|
||||
requestRender: governorRequestRender,
|
||||
// Map sources reach the scene through the globe engine's maps.
|
||||
engine,
|
||||
...mapOptions,
|
||||
googleTileset: tileset,
|
||||
cesiumToken,
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { MapStackController } from './mapStackController.js';
|
||||
import { createFakeEngine } from './globe/testing/fakeEngine.js';
|
||||
|
||||
test('the map stack controller applies its stacks through a given globe engine', async () => {
|
||||
const engine = createFakeEngine();
|
||||
const tileset = { show: true };
|
||||
const controller = new MapStackController(null, {
|
||||
engine,
|
||||
googleTileset: tileset,
|
||||
});
|
||||
await controller.setStack('photoreal');
|
||||
assert.equal(
|
||||
engine.fake.callsOf('maps.setPhotorealTiles')[0].args[0].tileset,
|
||||
tileset,
|
||||
);
|
||||
await controller.setStack('osm');
|
||||
const [imagery] = engine.fake.callsOf('maps.setBaseImagery')[0].args;
|
||||
assert.equal(imagery.descriptor.id, 'osm');
|
||||
assert.ok(imagery.provider, 'a ready provider the controller built');
|
||||
assert.equal(engine.fake.callsOf('maps.setPhotorealTiles')[1].args[0], null);
|
||||
controller.destroy();
|
||||
});
|
||||
+78
-7
@@ -10,7 +10,18 @@ import { acquireImageryComparison } from './imageryComparison.js';
|
||||
*/
|
||||
const AUTOMATIC = Symbol('automatic switch');
|
||||
|
||||
/** Coordinate source lifetimes and scene changes; the registry owns provider choices. */
|
||||
/**
|
||||
* Coordinate source lifetimes and scene changes; the registry owns provider
|
||||
* 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).
|
||||
*/
|
||||
export class MapSourceController {
|
||||
constructor(
|
||||
viewer,
|
||||
@@ -19,7 +30,10 @@ export class MapSourceController {
|
||||
initialStack,
|
||||
onChange = null,
|
||||
onError = null,
|
||||
requestRender = () => viewer?.scene?.requestRender?.(),
|
||||
engine = null,
|
||||
requestRender = engine
|
||||
? () => engine.render.request('map-stack')
|
||||
: () => viewer?.scene?.requestRender?.(),
|
||||
createImageryLayer = (provider) => new Cesium.ImageryLayer(provider),
|
||||
},
|
||||
) {
|
||||
@@ -32,7 +46,12 @@ export class MapSourceController {
|
||||
this._onChange = onChange;
|
||||
this._onError = onError;
|
||||
this._requestRender = requestRender;
|
||||
this._scene = createMapScene(viewer, { createImageryLayer });
|
||||
this._engine = engine;
|
||||
this._scene = engine
|
||||
? null
|
||||
: createMapScene(viewer, { createImageryLayer });
|
||||
/** Engine mode: the provider last handed to engine.maps as base imagery. */
|
||||
this._baseProvider = null;
|
||||
this._abort = new AbortController();
|
||||
this._imageryProviders = new Map();
|
||||
this._terrainProviders = new Map();
|
||||
@@ -236,6 +255,16 @@ export class MapSourceController {
|
||||
);
|
||||
}
|
||||
if (gen !== this._switchGen) return;
|
||||
if (this._engine) {
|
||||
// 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,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!source.tileset && !this._ownedTilesets.has(tileset)) {
|
||||
tileset.show = false;
|
||||
this.viewer.scene.primitives.add(tileset);
|
||||
@@ -258,6 +287,7 @@ export class MapSourceController {
|
||||
async _activateGlobeStack(stack, gen) {
|
||||
const resolution = await this._getImageryProvider(stack);
|
||||
if (gen !== this._switchGen) return;
|
||||
if (this._engine) return this._activateEngineGlobeStack(resolution, gen);
|
||||
// Scene shots reapply their map stack at every handoff; the map scene
|
||||
// keeps the live layer when the resolved provider is unchanged.
|
||||
if (this._scene.baseProvider !== resolution.provider) {
|
||||
@@ -288,6 +318,44 @@ export class MapSourceController {
|
||||
return resolution;
|
||||
}
|
||||
|
||||
// engine.maps in the order the viewer path applies them: base imagery and
|
||||
// its credit, no photoreal tiles (the globe shows), then terrain.
|
||||
async _activateEngineGlobeStack(resolution, gen) {
|
||||
const { maps } = this._engine;
|
||||
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,
|
||||
});
|
||||
if (gen !== this._switchGen) return;
|
||||
this._baseProvider = resolution.provider;
|
||||
this._watchProvider(resolution, gen);
|
||||
await maps.setPhotorealTiles(null);
|
||||
if (gen !== this._switchGen) return;
|
||||
this._activeId = resolution.effectiveStackId;
|
||||
if (source?.terrain && source.terrain.id !== this._terrainMode) {
|
||||
const terrain = source.terrain;
|
||||
const result = await this._cached(
|
||||
this._terrainProviders,
|
||||
terrain.id,
|
||||
() => terrain.create({ signal: this._abort.signal }),
|
||||
);
|
||||
if (gen !== this._switchGen) return;
|
||||
await maps.setTerrain({ id: terrain.id, create: () => result });
|
||||
if (gen !== this._switchGen) return;
|
||||
this._terrainMode = terrain.id;
|
||||
}
|
||||
return resolution;
|
||||
}
|
||||
|
||||
/** The live base imagery provider, or null. */
|
||||
_liveBaseProvider() {
|
||||
return this._engine ? this._baseProvider : this._scene.baseProvider;
|
||||
}
|
||||
|
||||
_cached(cache, id, create) {
|
||||
if (cache.has(id)) return cache.get(id);
|
||||
const promise = Promise.resolve()
|
||||
@@ -337,7 +405,7 @@ export class MapSourceController {
|
||||
this._removeImageryErrorListener = errorEvent.addEventListener((error) => {
|
||||
if (
|
||||
gen !== this._switchGen ||
|
||||
this._scene.baseProvider !== resolution.provider
|
||||
this._liveBaseProvider() !== resolution.provider
|
||||
)
|
||||
return;
|
||||
const retryCount = Number(error?.timesRetried);
|
||||
@@ -372,7 +440,9 @@ export class MapSourceController {
|
||||
_removeImageryLayer() {
|
||||
this._removeImageryErrorListener?.();
|
||||
this._removeImageryErrorListener = null;
|
||||
this._scene.removeBaseImagery();
|
||||
// Engine mode: engine.maps drops the base layer when tiles replace it.
|
||||
if (this._engine) this._baseProvider = null;
|
||||
else this._scene.removeBaseImagery();
|
||||
}
|
||||
_dispose(value) {
|
||||
if (!value || this._disposed.has(value)) return;
|
||||
@@ -388,7 +458,7 @@ export class MapSourceController {
|
||||
this._abort.abort();
|
||||
this._isSwitching = false;
|
||||
this._removeImageryLayer();
|
||||
this._scene.destroy();
|
||||
this._scene?.destroy();
|
||||
for (const promise of this._imageryProviders.values())
|
||||
void Promise.resolve(promise).then(
|
||||
(value) => this._dispose(value.provider),
|
||||
@@ -400,7 +470,8 @@ export class MapSourceController {
|
||||
() => {},
|
||||
);
|
||||
for (const tileset of this._ownedTilesets) {
|
||||
this.viewer.scene.primitives.remove(tileset);
|
||||
if (this._engine) this._engine.maps.releasePhotorealTiles({ tileset });
|
||||
else this.viewer.scene.primitives.remove(tileset);
|
||||
this._dispose(tileset);
|
||||
}
|
||||
for (const promise of this._tilesets.values())
|
||||
|
||||
@@ -839,3 +839,157 @@ test('each switch generation reports its origin: an outside setStack is manual,
|
||||
]);
|
||||
recovering.controller.destroy();
|
||||
});
|
||||
|
||||
// --- On a globe engine: scene changes go through engine.maps ----------------
|
||||
|
||||
async function engineFixture({ googleTileset = { show: true }, engine } = {}) {
|
||||
const { createFakeEngine } = await import('../globe/testing/fakeEngine.js');
|
||||
engine ??= createFakeEngine();
|
||||
const registry = createDefaultMapSources({ googleTileset });
|
||||
const providers = new Map();
|
||||
for (const source of registry.sources) {
|
||||
if (!source.imagery) continue;
|
||||
const provider = { id: source.descriptor.id, errorEvent: event() };
|
||||
providers.set(source.descriptor.id, provider);
|
||||
source.imagery = async () => provider;
|
||||
source.terrain = {
|
||||
id: 'keyless',
|
||||
create: async () => ({ provider: { id: 'terrain' } }),
|
||||
};
|
||||
}
|
||||
const changes = [];
|
||||
const controller = new MapSourceController(null, {
|
||||
registry,
|
||||
engine,
|
||||
onChange: (state) => changes.push(state),
|
||||
});
|
||||
return { engine, registry, providers, controller, changes, googleTileset };
|
||||
}
|
||||
|
||||
test('on an engine a globe stack is base imagery, then no photoreal tiles, then terrain', async () => {
|
||||
const env = await engineFixture();
|
||||
await env.controller.setStack('esri-imagery');
|
||||
const calls = env.engine.fake.calls.filter(({ method }) =>
|
||||
method.startsWith('maps.'),
|
||||
);
|
||||
assert.deepEqual(
|
||||
calls.map(({ method }) => method),
|
||||
['maps.setBaseImagery', 'maps.setPhotorealTiles', 'maps.setTerrain'],
|
||||
);
|
||||
const [imagery] = calls[0].args;
|
||||
assert.equal(imagery.provider, env.providers.get('esri-imagery'));
|
||||
assert.equal(imagery.descriptor.id, 'esri-imagery');
|
||||
assert.match(imagery.credit, /Esri/);
|
||||
assert.equal(calls[1].args[0], null);
|
||||
assert.deepEqual(await calls[2].args[0].create(), {
|
||||
provider: { id: 'terrain' },
|
||||
});
|
||||
assert.equal(env.controller.getActiveId(), 'esri-imagery');
|
||||
env.engine.fake.resetCalls();
|
||||
await env.controller.setStack('esri-imagery');
|
||||
assert.equal(
|
||||
env.engine.fake.callsOf('maps.setTerrain').length,
|
||||
0,
|
||||
'terrain stays applied',
|
||||
);
|
||||
env.controller.destroy();
|
||||
});
|
||||
|
||||
test('on an engine a photoreal stack hands the engine its tileset and credit', async () => {
|
||||
const env = await engineFixture();
|
||||
await env.controller.setStack('osm');
|
||||
env.engine.fake.resetCalls();
|
||||
await env.controller.setStack('photoreal');
|
||||
const [call] = env.engine.fake.callsOf('maps.setPhotorealTiles');
|
||||
assert.equal(call.args[0].tileset, env.googleTileset);
|
||||
assert.equal(env.engine.fake.callsOf('maps.setBaseImagery').length, 0);
|
||||
assert.equal(env.controller.getActiveId(), 'photoreal');
|
||||
env.controller.destroy();
|
||||
});
|
||||
|
||||
test('on an engine tile failures still fall back through the error watch', async () => {
|
||||
const env = await engineFixture();
|
||||
await env.controller.setStack('esri-imagery');
|
||||
const errors = env.providers.get('esri-imagery').errorEvent;
|
||||
assert.equal(errors.size, 1);
|
||||
errors.raise();
|
||||
errors.raise();
|
||||
await settle();
|
||||
assert.equal(env.controller.getActiveId(), 'osm');
|
||||
assert.equal(errors.size, 0);
|
||||
assert.equal(
|
||||
env.engine.fake.callsOf('maps.setBaseImagery').at(-1).args[0].provider,
|
||||
env.providers.get('osm'),
|
||||
);
|
||||
env.controller.destroy();
|
||||
});
|
||||
|
||||
test('on an engine owned tilesets are released through engine.maps and disposed', async () => {
|
||||
const { createFakeEngine } = await import('../globe/testing/fakeEngine.js');
|
||||
const engine = createFakeEngine();
|
||||
let destroyed = 0;
|
||||
const owned = { show: false, destroy: () => destroyed++ };
|
||||
const controller = new MapSourceController(null, {
|
||||
engine,
|
||||
registry: {
|
||||
defaultId: 'owned',
|
||||
sources: [
|
||||
{ descriptor: descriptor('owned'), createTileset: async () => owned },
|
||||
],
|
||||
},
|
||||
});
|
||||
await controller.setStack('owned');
|
||||
assert.equal(
|
||||
engine.fake.callsOf('maps.setPhotorealTiles')[0].args[0].tileset,
|
||||
owned,
|
||||
);
|
||||
controller.destroy();
|
||||
assert.deepEqual(engine.fake.callsOf('maps.releasePhotorealTiles')[0].args, [
|
||||
{ tileset: owned },
|
||||
]);
|
||||
assert.equal(destroyed, 1);
|
||||
});
|
||||
|
||||
test('controller and Cesium engine: photoreal to a globe stack restores imagery and credit and back', async () => {
|
||||
const { createCesiumEngine } =
|
||||
await import('../globe/cesium/createCesiumEngine.js');
|
||||
const { createFakeViewer } =
|
||||
await import('../globe/cesium/testing/fakeViewer.mjs');
|
||||
const viewer = createFakeViewer();
|
||||
const google = { show: true };
|
||||
viewer.primitiveList.push(google);
|
||||
viewer.scene.globe.show = false;
|
||||
const engine = createCesiumEngine(viewer, {
|
||||
tilesetRef: () => google,
|
||||
requestRender: () => {},
|
||||
createImageryLayer: (provider) => ({ provider }),
|
||||
});
|
||||
const env = await engineFixture({ googleTileset: google, engine });
|
||||
const credits = () => viewer.staticCredits.map((credit) => credit.html);
|
||||
await env.controller.setStack('photoreal');
|
||||
assert.equal(google.show, true);
|
||||
assert.equal(viewer.scene.globe.show, false);
|
||||
const photorealCredits = credits();
|
||||
await env.controller.setStack('esri-imagery');
|
||||
assert.equal(google.show, false);
|
||||
assert.equal(viewer.scene.globe.show, true);
|
||||
assert.deepEqual(viewer.imageryList, [
|
||||
{ provider: env.providers.get('esri-imagery') },
|
||||
]);
|
||||
assert.equal(credits().length, 1);
|
||||
assert.match(credits()[0], /Esri/);
|
||||
assert.equal(viewer.terrainProvider?.id, 'terrain', 'terrain applied');
|
||||
await env.controller.setStack('osm');
|
||||
assert.deepEqual(viewer.imageryList, [
|
||||
{ provider: env.providers.get('osm') },
|
||||
]);
|
||||
assert.deepEqual(credits(), [], 'OSM carries no static credit');
|
||||
await env.controller.setStack('photoreal');
|
||||
assert.deepEqual(viewer.imageryList, []);
|
||||
assert.equal(google.show, true);
|
||||
assert.equal(viewer.scene.globe.show, false);
|
||||
assert.deepEqual(credits(), photorealCredits);
|
||||
env.controller.destroy();
|
||||
engine.destroy();
|
||||
assert.deepEqual(credits(), []);
|
||||
});
|
||||
Reference in new issue
Block a user