mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-07 20:04:00 +00:00
Drape the open-data map overlays through the renderer's globe port
The sea-mark and ski-piste overlays add an XYZ imagery layer through the globe port instead of building Cesium layers into a host collection. The port drapes it on the globe on globe stacks and on the Google 3D tiles, which the map stack now marks as its base tilesets. The row's hidden reason reads the map stack's new getDrapeSurface, or the viewer's globe when there is no map stack, and an overlay enabled before its viewer has a globe port adds its layer once there is one. The overlays no longer reach Cesium, so they leave the allow-list.
This commit is contained in:
1 parent
fbcac37f13
commit
70123fe4a9
6 files changed
+152
-126
No files matched your search
@@ -2566,7 +2566,9 @@
|
||||
"src/geo/index.js",
|
||||
"src/maps/catalog.js",
|
||||
"src/maps/controller.js",
|
||||
"src/render/cesium/globe.js",
|
||||
"src/render/cesium/credits.js",
|
||||
"src/render/cesium/tileRequests.js",
|
||||
"src/render/cesium/cameraInput.js",
|
||||
"src/render/cesium/camera.js",
|
||||
"src/render/cesium/cameraControls.js",
|
||||
@@ -2584,6 +2586,7 @@
|
||||
"src/render/cesium/common.js",
|
||||
"src/render/creditMarkup.js",
|
||||
"src/maps/defaultSources.js",
|
||||
"src/render/cesium/imagery.js",
|
||||
"src/maps/google3d.js",
|
||||
"src/maps/imagery.js",
|
||||
"src/maps/registry.js",
|
||||
|
||||
+49
-69
@@ -1,5 +1,7 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { NO_IMAGERY_HOST, resolveImageryHost } from '../maps/imageryHost.js';
|
||||
import { NO_IMAGERY_HOST } from '../maps/imageryHost.js';
|
||||
// The application's renderer gives the globe port; a viewer with no
|
||||
// renderer bound gets one from the factory its Cesium side loads.
|
||||
import { findGlobe } from '../render/rendererOf.js';
|
||||
|
||||
/**
|
||||
* @file Open-data raster overlays: OpenSeaMap sea marks and OpenSnowMap ski
|
||||
@@ -16,10 +18,11 @@ import { NO_IMAGERY_HOST, resolveImageryHost } from '../maps/imageryHost.js';
|
||||
* - OpenInfraMap serves MapLibre vector tiles, not raster XYZ tiles. That
|
||||
* needs a vector-tile rendering pipeline, not an imagery-layer drop-in.
|
||||
*
|
||||
* The overlays drape on whatever surface the active map stack offers (see
|
||||
* `maps/imageryHost.js`): the globe on the OSM and Bing stacks, the Google
|
||||
* Photorealistic tileset's own `imageryLayers` on Google 3D. Only a stack with
|
||||
* neither leaves them hidden, and the row says so.
|
||||
* Each overlay is an imagery layer of the renderer's globe port, draped on
|
||||
* whatever is drawn: the globe on the OSM and Bing stacks, the Google
|
||||
* Photorealistic tiles on Google 3D. The map stack's own record says which
|
||||
* (`getDrapeSurface`); only a stack with neither leaves them hidden, and the
|
||||
* row says so.
|
||||
* @module data/osmOverlays
|
||||
*/
|
||||
|
||||
@@ -54,76 +57,53 @@ export function osmOverlayHiddenReason(enabled, hostKind) {
|
||||
return enabled && hostKind === 'none' ? NO_IMAGERY_HOST : null;
|
||||
}
|
||||
|
||||
/** The imagery spec of one overlay. */
|
||||
export function osmOverlaySpec(overlay) {
|
||||
return {
|
||||
kind: 'xyz',
|
||||
url: overlay.urlTemplate,
|
||||
maxLevel: overlay.maximumLevel,
|
||||
credit: { text: overlay.credit },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the layer for one overlay descriptor. Each application catalog gets
|
||||
* its own instances; the Cesium factories are injectable so tests run without
|
||||
* WebGL.
|
||||
* its own instances; `globeOf` (the viewer's globe port) is injectable so
|
||||
* tests run without WebGL.
|
||||
*/
|
||||
export function createOsmOverlayLayer(
|
||||
overlay,
|
||||
{
|
||||
host = resolveImageryHost,
|
||||
createProvider = (descriptor) =>
|
||||
new Cesium.UrlTemplateImageryProvider({
|
||||
url: descriptor.urlTemplate,
|
||||
credit: descriptor.credit,
|
||||
maximumLevel: descriptor.maximumLevel,
|
||||
}),
|
||||
createImageryLayer = (provider) => new Cesium.ImageryLayer(provider),
|
||||
} = {},
|
||||
) {
|
||||
export function createOsmOverlayLayer(overlay, { globeOf = findGlobe } = {}) {
|
||||
if (!OSM_OVERLAYS.includes(overlay))
|
||||
throw new TypeError(`Unknown open map overlay: ${overlay?.id}`);
|
||||
let viewer = null;
|
||||
let mapStack = null;
|
||||
let unsubscribeMap = null;
|
||||
let enabled = false;
|
||||
let hostInfo = { collection: null, kind: 'none' };
|
||||
/** The imagery layer this instance owns while enabled, and where it sits. */
|
||||
let surface = 'none';
|
||||
/** The imagery layer this instance owns while enabled. */
|
||||
let owned = null;
|
||||
|
||||
const usable = (collection) => collection && !collection.isDestroyed?.();
|
||||
|
||||
function detach(destroy) {
|
||||
if (!owned) return;
|
||||
const { collection, layer } = owned;
|
||||
if (usable(collection) && collection.contains(layer))
|
||||
collection.remove(layer, destroy);
|
||||
else if (destroy && !layer.isDestroyed?.()) layer.destroy?.();
|
||||
owned.collection = null;
|
||||
if (destroy) owned = null;
|
||||
/**
|
||||
* What the overlay drapes on: the map stack's record, or with no map
|
||||
* stack, whether the viewer's globe is shown.
|
||||
*/
|
||||
function drapeSurface() {
|
||||
const recorded = mapStack?.getDrapeSurface?.();
|
||||
if (recorded) return recorded;
|
||||
return viewer?.scene?.globe?.show === true ? 'globe' : 'none';
|
||||
}
|
||||
|
||||
function resolveHost() {
|
||||
return (
|
||||
host({ viewer, tileset: mapStack?.getImageryHostTileset?.() }) || {
|
||||
collection: null,
|
||||
kind: 'none',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/** Create, move or drop the owned imagery layer to match the state. */
|
||||
/** Add or remove the owned imagery layer to match the state. */
|
||||
function sync() {
|
||||
if (!viewer) return;
|
||||
hostInfo = enabled ? resolveHost() : { collection: null, kind: 'none' };
|
||||
surface = enabled ? drapeSurface() : 'none';
|
||||
if (!enabled) {
|
||||
detach(true);
|
||||
} else {
|
||||
owned ||= {
|
||||
layer: createImageryLayer(createProvider(overlay)),
|
||||
collection: null,
|
||||
};
|
||||
if (owned.collection !== hostInfo.collection) {
|
||||
detach(false);
|
||||
if (usable(hostInfo.collection)) {
|
||||
// Appended, so it draws above the map stack's base imagery.
|
||||
hostInfo.collection.add(owned.layer);
|
||||
owned.collection = hostInfo.collection;
|
||||
}
|
||||
}
|
||||
owned?.remove();
|
||||
owned = null;
|
||||
} else if (!owned) {
|
||||
// Added last, so it draws above the map stack's base imagery.
|
||||
owned = globeOf(viewer)?.imagery.add(osmOverlaySpec(overlay)) ?? null;
|
||||
}
|
||||
viewer.scene?.requestRender?.();
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -135,7 +115,7 @@ export function createOsmOverlayLayer(
|
||||
init(nextViewer) {
|
||||
viewer = nextViewer;
|
||||
},
|
||||
/** Follow map-stack switches so the drape moves with the surface. */
|
||||
/** Follow map-stack switches, for the row's hidden reason. */
|
||||
attachMapStackController(controller) {
|
||||
unsubscribeMap?.();
|
||||
unsubscribeMap = null;
|
||||
@@ -159,27 +139,27 @@ export function createOsmOverlayLayer(
|
||||
getStats() {
|
||||
// Without a controller subscription the stats poll doubles as the
|
||||
// map-stack watch.
|
||||
if (enabled && !unsubscribeMap) {
|
||||
const next = resolveHost();
|
||||
if (next.collection !== hostInfo.collection) sync();
|
||||
}
|
||||
if (enabled && !unsubscribeMap) surface = drapeSurface();
|
||||
// No globe port yet (a viewer whose renderer came later): try again.
|
||||
if (enabled && !owned) sync();
|
||||
return {
|
||||
count: enabled ? 1 : 0,
|
||||
error: osmOverlayHiddenReason(enabled, hostInfo.kind),
|
||||
error: osmOverlayHiddenReason(enabled, surface),
|
||||
};
|
||||
},
|
||||
/** Owned layer count and host kind, for tests and live gates. */
|
||||
/** Owned layer count and drape surface, for tests and live gates. */
|
||||
diagnostics() {
|
||||
return { host: hostInfo.kind, ownedCount: owned ? 1 : 0 };
|
||||
return { host: surface, ownedCount: owned ? 1 : 0 };
|
||||
},
|
||||
destroy() {
|
||||
enabled = false;
|
||||
detach(true);
|
||||
owned?.remove();
|
||||
owned = null;
|
||||
unsubscribeMap?.();
|
||||
unsubscribeMap = null;
|
||||
mapStack = null;
|
||||
viewer = null;
|
||||
hostInfo = { collection: null, kind: 'none' };
|
||||
surface = 'none';
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,28 +5,29 @@ import {
|
||||
createOsmOverlayLayer,
|
||||
createOsmOverlayLayers,
|
||||
osmOverlayHiddenReason,
|
||||
osmOverlaySpec,
|
||||
} from './osmOverlays.js';
|
||||
import { NO_IMAGERY_HOST } from '../maps/imageryHost.js';
|
||||
|
||||
function fakeCollection() {
|
||||
/** A globe port recording the imagery layers added and removed through it. */
|
||||
function fakeGlobe() {
|
||||
const layers = [];
|
||||
const removed = [];
|
||||
return {
|
||||
layers,
|
||||
removed,
|
||||
add(layer) {
|
||||
layers.push(layer);
|
||||
},
|
||||
contains(layer) {
|
||||
return layers.includes(layer);
|
||||
},
|
||||
remove(layer, destroy) {
|
||||
const index = layers.indexOf(layer);
|
||||
if (index < 0) return false;
|
||||
layers.splice(index, 1);
|
||||
removed.push({ layer, destroy });
|
||||
if (destroy) layer.destroyed = true;
|
||||
return true;
|
||||
imagery: {
|
||||
add(spec) {
|
||||
const layer = {
|
||||
spec,
|
||||
removed: false,
|
||||
remove() {
|
||||
layer.removed = true;
|
||||
layers.splice(layers.indexOf(layer), 1);
|
||||
},
|
||||
};
|
||||
layers.push(layer);
|
||||
return layer;
|
||||
},
|
||||
list: () => layers.slice(),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -34,9 +35,9 @@ function fakeCollection() {
|
||||
function fakeMapStack() {
|
||||
const subscribers = new Set();
|
||||
return {
|
||||
tileset: null,
|
||||
getImageryHostTileset() {
|
||||
return this.tileset;
|
||||
surface: 'globe',
|
||||
getDrapeSurface() {
|
||||
return this.surface;
|
||||
},
|
||||
subscribe(fn) {
|
||||
subscribers.add(fn);
|
||||
@@ -50,24 +51,15 @@ function fakeMapStack() {
|
||||
}
|
||||
|
||||
function setup(overlay = OSM_OVERLAYS[0]) {
|
||||
const globe = fakeCollection();
|
||||
const viewer = {
|
||||
imageryLayers: globe,
|
||||
scene: { globe: { show: true }, requestRender() {} },
|
||||
};
|
||||
const created = [];
|
||||
const globe = fakeGlobe();
|
||||
const viewer = { scene: { globe: { show: true } } };
|
||||
const layer = createOsmOverlayLayer(overlay, {
|
||||
createProvider: (descriptor) => ({ descriptor }),
|
||||
createImageryLayer: (provider) => {
|
||||
const imagery = { provider, destroyed: false };
|
||||
created.push(imagery);
|
||||
return imagery;
|
||||
},
|
||||
globeOf: (given) => (given === viewer ? globe : null),
|
||||
});
|
||||
const mapStack = fakeMapStack();
|
||||
layer.init(viewer);
|
||||
layer.attachMapStackController(mapStack);
|
||||
return { layer, viewer, globe, created, mapStack };
|
||||
return { layer, viewer, globe, mapStack };
|
||||
}
|
||||
|
||||
test('OSM_OVERLAYS: the two shipped layers, each with a tile template and credit', () => {
|
||||
@@ -103,65 +95,94 @@ test('osmOverlayHiddenReason: only when enabled with no imagery host', () => {
|
||||
assert.equal(osmOverlayHiddenReason(true, 'none'), NO_IMAGERY_HOST);
|
||||
});
|
||||
|
||||
test('a layer owns no imagery until enabled and destroys it on disable', () => {
|
||||
const { layer, globe, created } = setup(OSM_OVERLAYS[1]);
|
||||
assert.equal(created.length, 0);
|
||||
test('a layer owns no imagery until enabled and removes it on disable', () => {
|
||||
const { layer, globe } = setup(OSM_OVERLAYS[1]);
|
||||
assert.equal(globe.layers.length, 0);
|
||||
|
||||
layer.enable();
|
||||
assert.equal(globe.layers.length, 1);
|
||||
assert.equal(globe.layers[0].provider.descriptor.id, 'ski-pistes');
|
||||
assert.deepEqual(globe.layers[0].spec, osmOverlaySpec(OSM_OVERLAYS[1]));
|
||||
assert.deepEqual(globe.layers[0].spec, {
|
||||
kind: 'xyz',
|
||||
url: 'https://www.opensnowmap.org/pistes/{z}/{x}/{y}.png',
|
||||
maxLevel: 18,
|
||||
credit: { text: OSM_OVERLAYS[1].credit },
|
||||
});
|
||||
assert.deepEqual(layer.getStats(), { count: 1, error: null });
|
||||
|
||||
const [first] = globe.layers;
|
||||
layer.disable();
|
||||
assert.equal(globe.layers.length, 0);
|
||||
assert.equal(created[0].destroyed, true);
|
||||
assert.equal(first.removed, true);
|
||||
assert.deepEqual(layer.diagnostics(), { host: 'none', ownedCount: 0 });
|
||||
assert.deepEqual(layer.getStats(), { count: 0, error: null });
|
||||
|
||||
layer.enable();
|
||||
assert.equal(created.length, 2, 're-enabling creates a fresh imagery layer');
|
||||
assert.notEqual(globe.layers[0], first, 're-enabling adds a fresh layer');
|
||||
});
|
||||
|
||||
test('the overlay follows a map-stack switch onto the Google 3D tileset and back', () => {
|
||||
const { layer, viewer, globe, mapStack } = setup();
|
||||
test('a map-stack switch onto Google 3D keeps the one layer, which the renderer drapes there', () => {
|
||||
const { layer, globe, mapStack } = setup();
|
||||
layer.enable();
|
||||
const [imagery] = globe.layers;
|
||||
|
||||
const tileset = { imageryLayers: fakeCollection() };
|
||||
viewer.scene.globe.show = false;
|
||||
mapStack.tileset = tileset;
|
||||
mapStack.surface = 'tileset';
|
||||
mapStack.emit();
|
||||
assert.equal(globe.layers.length, 0);
|
||||
assert.deepEqual(tileset.imageryLayers.layers, [imagery]);
|
||||
assert.equal(globe.removed[0].destroy, false);
|
||||
assert.deepEqual(globe.layers, [imagery]);
|
||||
assert.equal(layer.diagnostics().host, 'tileset');
|
||||
assert.equal(layer.getStats().error, null);
|
||||
|
||||
viewer.scene.globe.show = true;
|
||||
mapStack.tileset = null;
|
||||
mapStack.surface = 'globe';
|
||||
mapStack.emit();
|
||||
assert.deepEqual(globe.layers, [imagery]);
|
||||
assert.equal(tileset.imageryLayers.layers.length, 0);
|
||||
assert.equal(layer.diagnostics().host, 'globe');
|
||||
});
|
||||
|
||||
test('with no surface to drape on the row explains itself, and recovers when one appears', () => {
|
||||
const { layer, viewer, globe, mapStack } = setup();
|
||||
viewer.scene.globe.show = false;
|
||||
const { layer, globe, mapStack } = setup();
|
||||
mapStack.surface = 'none';
|
||||
layer.enable();
|
||||
assert.equal(globe.layers.length, 0);
|
||||
assert.equal(globe.layers.length, 1, 'kept, drawn when a surface is');
|
||||
assert.equal(layer.getStats().error, NO_IMAGERY_HOST);
|
||||
|
||||
viewer.scene.globe.show = true;
|
||||
mapStack.surface = 'globe';
|
||||
mapStack.emit();
|
||||
assert.equal(globe.layers.length, 1);
|
||||
assert.equal(layer.getStats().error, null);
|
||||
});
|
||||
|
||||
test('without a map stack, the viewer’s globe decides, polled by the stats', () => {
|
||||
const globe = fakeGlobe();
|
||||
const viewer = { scene: { globe: { show: false } } };
|
||||
const layer = createOsmOverlayLayer(OSM_OVERLAYS[0], {
|
||||
globeOf: () => globe,
|
||||
});
|
||||
layer.init(viewer);
|
||||
layer.enable();
|
||||
assert.equal(layer.getStats().error, NO_IMAGERY_HOST);
|
||||
viewer.scene.globe.show = true;
|
||||
assert.equal(layer.getStats().error, null);
|
||||
});
|
||||
|
||||
test('destroy removes owned imagery and unsubscribes from the map stack', () => {
|
||||
const { layer, globe, created, mapStack } = setup();
|
||||
const { layer, globe, mapStack } = setup();
|
||||
layer.enable();
|
||||
const [imagery] = globe.layers;
|
||||
layer.destroy();
|
||||
assert.equal(globe.layers.length, 0);
|
||||
assert.equal(created[0].destroyed, true);
|
||||
assert.equal(imagery.removed, true);
|
||||
assert.equal(mapStack.subscribers.size, 0);
|
||||
});
|
||||
|
||||
test('an overlay enabled before its viewer has a globe port adds it once one does', () => {
|
||||
const globe = fakeGlobe();
|
||||
let port = null;
|
||||
const layer = createOsmOverlayLayer(OSM_OVERLAYS[0], { globeOf: () => port });
|
||||
layer.init({ scene: { globe: { show: true } } });
|
||||
layer.enable();
|
||||
assert.equal(layer.diagnostics().ownedCount, 0);
|
||||
port = globe;
|
||||
layer.getStats();
|
||||
assert.equal(globe.layers.length, 1);
|
||||
layer.getStats();
|
||||
assert.equal(globe.layers.length, 1, 'added once');
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import { createMapCredits } from './credits.js';
|
||||
// A viewer of a consumer's own gets its credits port (and camera) from the
|
||||
// factories this loads.
|
||||
import '../render/cesium/camera.js';
|
||||
import { markBaseTileset } from '../render/cesium/globe.js';
|
||||
import { acquireImageryComparison } from './imageryComparison.js';
|
||||
|
||||
/**
|
||||
@@ -75,6 +76,9 @@ export class MapSourceController {
|
||||
this._replacedTerrain = null;
|
||||
this._subscribers = new Set();
|
||||
this._destroyed = false;
|
||||
// The renderer's imagery drapes on these while they are shown.
|
||||
for (const source of this._sources.values())
|
||||
if (source.tileset) markBaseTileset(source.tileset);
|
||||
}
|
||||
|
||||
getStack(id) {
|
||||
@@ -112,6 +116,18 @@ export class MapSourceController {
|
||||
if (tileset.show === true) return tileset;
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* What draped imagery lands on now: `'globe'` while the globe is shown
|
||||
* (a globe stack), `'tileset'` for a shown 3D stack, `'none'` when
|
||||
* neither is drawn; null once destroyed, when it knows nothing.
|
||||
* @returns {'globe' | 'tileset' | 'none' | null}
|
||||
*/
|
||||
getDrapeSurface() {
|
||||
if (this._destroyed) return null;
|
||||
if (this.viewer?.scene?.globe?.show === true) return 'globe';
|
||||
const tileset = this.getImageryHostTileset();
|
||||
return tileset && !tileset.isDestroyed?.() ? 'tileset' : 'none';
|
||||
}
|
||||
getActiveId() {
|
||||
return this._activeId;
|
||||
}
|
||||
@@ -270,6 +286,7 @@ export class MapSourceController {
|
||||
if (gen !== this._switchGen) return;
|
||||
if (!source.tileset && !this._ownedTilesets.has(tileset)) {
|
||||
tileset.show = false;
|
||||
markBaseTileset(tileset);
|
||||
this.viewer.scene.primitives.add(tileset);
|
||||
this._ownedTilesets.add(tileset);
|
||||
}
|
||||
|
||||
@@ -453,12 +453,18 @@ test('imagery host tileset follows supplied, owned and globe map sources', async
|
||||
assert.equal(controller.getImageryHostTileset(), supplied);
|
||||
await controller.setStack('owned');
|
||||
assert.equal(controller.getImageryHostTileset(), owned);
|
||||
assert.equal(controller.getDrapeSurface(), 'tileset');
|
||||
await controller.setStack('globe');
|
||||
assert.equal(controller.getImageryHostTileset(), null);
|
||||
assert.equal(controller.getDrapeSurface(), 'globe');
|
||||
await controller.setStack('supplied');
|
||||
assert.equal(controller.getImageryHostTileset(), supplied);
|
||||
assert.equal(controller.getDrapeSurface(), 'tileset');
|
||||
supplied.show = false;
|
||||
assert.equal(controller.getDrapeSurface(), 'none');
|
||||
controller.destroy();
|
||||
assert.equal(controller.getImageryHostTileset(), null);
|
||||
assert.equal(controller.getDrapeSurface(), null);
|
||||
});
|
||||
|
||||
function leaseFixture() {
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
"src/data/modelRecord.js",
|
||||
"src/data/modelVisualAnchor.js",
|
||||
"src/data/motionModel.js",
|
||||
"src/data/osmOverlays.js",
|
||||
"src/data/trailRenderer.js",
|
||||
"src/input/dispatcher.js",
|
||||
"src/input/sensorPick.js",
|
||||
|
||||
Reference in new issue
Block a user