diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json
index 76540e1a2..973e02e2d 100644
--- a/scripts/package-boundaries.json
+++ b/scripts/package-boundaries.json
@@ -1607,6 +1607,7 @@
"./maps/defaults"
],
"modules": [
+ "src/globe/cesium/credits.js",
"src/maps/availability.js",
"src/maps/catalog.js",
"src/maps/controller.js",
@@ -3086,6 +3087,7 @@
"application-operations": {
"exports": ["./application/operations"],
"modules": [
+ "src/globe/cesium/credits.js",
"src/services/groundSnap.js",
"src/globe/contract.js",
"src/annotations/annotationResolver.js",
diff --git a/src/app/scene.js b/src/app/scene.js
index 993efee3c..524e1804e 100644
--- a/src/app/scene.js
+++ b/src/app/scene.js
@@ -62,7 +62,7 @@ export async function createApplicationScene({
const tilesetRef = { current: null };
const engine = createSceneEngine(viewer, { tilesetRef, defer });
defer(installTrackpadPinchZoom(viewer));
- registerDataCredits(viewer, credits);
+ registerDataCredits(engine, credits);
configureCreditKeyboardAccess(document);
loaderStatus.textContent =
googleApiKey || googleTokens || cesiumToken
diff --git a/src/data/dataCredits.js b/src/data/dataCredits.js
index 2967f0a2b..9ddb70258 100644
--- a/src/data/dataCredits.js
+++ b/src/data/dataCredits.js
@@ -1,4 +1,5 @@
-import * as Cesium from 'cesium';
+import { isGlobeEngine } from '../globe/contract.js';
+import { createCesiumCredits } from '../globe/cesium/credits.js';
/**
* Per-layer data attribution registered into Cesium's credit display.
@@ -35,8 +36,32 @@ export const OPENMAPTILES_CREDIT = {
};
const osmDisplays = new WeakMap();
+const viewerCredits = new WeakMap();
-function updateOsmDisplay(viewer, state) {
+/**
+ * Credits of a globe engine, or of a Cesium viewer's credit display for
+ * callers that still hold a viewer (layers until phase 1c); null without one.
+ * @returns {{ addStatic: Function, request: () => void } | null}
+ */
+function creditsOf(target) {
+ if (isGlobeEngine(target))
+ return {
+ addStatic: (html, options) => target.credits.addStatic(html, options),
+ request: () => target.render.request('credits'),
+ };
+ if (typeof target?.creditDisplay?.addStaticCredit !== 'function') return null;
+ let credits = viewerCredits.get(target);
+ if (!credits) {
+ credits = {
+ addStatic: createCesiumCredits(target).credits.addStatic,
+ request: () => target.scene?.requestRender?.(),
+ };
+ viewerCredits.set(target, credits);
+ }
+ return credits;
+}
+
+function updateOsmDisplay(credits, state) {
const osm = state.owners.size > 0;
let tiles = false;
for (const usesTiles of state.owners.values()) tiles ||= usesTiles;
@@ -44,50 +69,48 @@ function updateOsmDisplay(viewer, state) {
['osm', osm],
['tiles', tiles],
]) {
- if (state[key].visible === visible) continue;
- state[key].visible = visible;
- viewer.creditDisplay[visible ? 'addStaticCredit' : 'removeStaticCredit'](
- state[key].credit,
- );
- viewer.scene?.requestRender?.();
+ const entry = state[key];
+ if ((entry.remove !== null) === visible) continue;
+ if (visible)
+ entry.remove = credits.addStatic(entry.html, { onScreen: true });
+ else {
+ entry.remove();
+ entry.remove = null;
+ }
+ credits.request();
}
}
/** Keep one short inline credit while an OSM display owner has visible data. */
-export function showOsmCredit(viewer, owner, { openMapTiles = false } = {}) {
- if (!viewer?.creditDisplay?.addStaticCredit || !owner) return false;
- let state = osmDisplays.get(viewer);
+export function showOsmCredit(target, owner, { openMapTiles = false } = {}) {
+ const credits = creditsOf(target);
+ if (!credits || !owner) return false;
+ let state = osmDisplays.get(target);
if (!state) {
state = {
owners: new Map(),
osm: {
- visible: false,
- credit: new Cesium.Credit(
- '© OpenStreetMap',
- true,
- ),
+ remove: null,
+ html: '© OpenStreetMap',
},
tiles: {
- visible: false,
- credit: new Cesium.Credit(
- '© OpenMapTiles',
- true,
- ),
+ remove: null,
+ html: '© OpenMapTiles',
},
};
- osmDisplays.set(viewer, state);
+ osmDisplays.set(target, state);
}
if (state.owners.get(owner) === openMapTiles) return false;
state.owners.set(owner, openMapTiles);
- updateOsmDisplay(viewer, state);
+ updateOsmDisplay(credits, state);
return true;
}
/** Release only this owner's credit; other displayed OSM sources retain theirs. */
-export function hideOsmCredit(viewer, owner) {
- const state = osmDisplays.get(viewer);
+export function hideOsmCredit(target, owner) {
+ const state = osmDisplays.get(target);
if (!state?.owners.delete(owner)) return false;
- updateOsmDisplay(viewer, state);
+ updateOsmDisplay(creditsOf(target), state);
return true;
}
@@ -440,18 +463,16 @@ const _dynamicCreditKeys = new Set();
* Register a conditional credit at the moment its data source activates.
* Idempotent per `credit.key`; lands in the same "Data attribution" popover
* as the static credits (showOnScreen=false).
- * @param {Cesium.Viewer} viewer — the initialized Cesium viewer
+ * @param {object} target — the globe engine, or the initialized Cesium viewer
* @param {{ key: string, html: string }} credit — e.g. `TOMTOM_CREDIT`
* @returns {boolean} True when the credit is (now) registered.
*/
-export function registerDynamicCredit(viewer, credit) {
- const creditDisplay = viewer?.creditDisplay;
- if (!creditDisplay || typeof creditDisplay.addStaticCredit !== 'function') {
- return false;
- }
+export function registerDynamicCredit(target, credit) {
+ const credits = creditsOf(target);
+ if (!credits) return false;
if (!credit?.key || !credit?.html) return false;
if (_dynamicCreditKeys.has(credit.key)) return true;
- creditDisplay.addStaticCredit(new Cesium.Credit(credit.html, false));
+ credits.addStatic(credit.html, { onScreen: false });
_dynamicCreditKeys.add(credit.key);
return true;
}
@@ -460,16 +481,14 @@ export function registerDynamicCredit(viewer, credit) {
* Register every per-layer data credit into the viewer's credit display.
* Idempotent: safe to call once at init. Credits are static and always
* present in the "Data attribution" popover.
- * @param {Cesium.Viewer} viewer — the initialized Cesium viewer
+ * @param {object} target — the globe engine, or the initialized Cesium viewer
*/
-export function registerDataCredits(viewer, credits = DATA_CREDITS) {
- const creditDisplay = viewer?.creditDisplay;
- if (!creditDisplay || typeof creditDisplay.addStaticCredit !== 'function') {
- return;
- }
+export function registerDataCredits(target, credits = DATA_CREDITS) {
+ const display = creditsOf(target);
+ if (!display) return;
for (const { html } of credits) {
- // showOnScreen=false → lives in the expandable "Data attribution" popover,
+ // onScreen false → lives in the expandable "Data attribution" popover,
// not the on-globe credit line.
- creditDisplay.addStaticCredit(new Cesium.Credit(html, false));
+ display.addStatic(html, { onScreen: false });
}
}
diff --git a/src/data/dataCredits.test.mjs b/src/data/dataCredits.test.mjs
index 46de48972..fb76a351b 100644
--- a/src/data/dataCredits.test.mjs
+++ b/src/data/dataCredits.test.mjs
@@ -120,3 +120,42 @@ test('tile attribution follows source changes and viewers have independent owner
assert.equal(a.credits.size, 0);
assert.equal(b.credits.size, 1);
});
+
+test('data credits go through the globe engine credits', async () => {
+ const {
+ registerDataCredits,
+ registerDynamicCredit,
+ showOsmCredit,
+ hideOsmCredit,
+ } = await import('./dataCredits.js');
+ const { createFakeEngine } = await import('../globe/testing/fakeEngine.js');
+ const engine = createFakeEngine();
+ registerDataCredits(engine, [{ html: 'A' }, { html: 'B' }]);
+ assert.deepEqual(engine.fake.credits, [
+ { html: 'A', onScreen: false },
+ { html: 'B', onScreen: false },
+ ]);
+ assert.equal(
+ registerDynamicCredit(engine, { key: 'engine-dyn', html: 'D' }),
+ true,
+ );
+ assert.equal(
+ registerDynamicCredit(engine, { key: 'engine-dyn', html: 'D' }),
+ true,
+ );
+ assert.equal(
+ engine.fake.credits.filter(({ html }) => html === 'D').length,
+ 1,
+ );
+ engine.fake.resetCalls();
+ assert.equal(showOsmCredit(engine, 'alpr', { openMapTiles: true }), true);
+ const inline = engine.fake.credits.filter(({ onScreen }) => onScreen);
+ assert.equal(inline.length, 2);
+ assert.match(inline[0].html, /© OpenStreetMap/);
+ assert.ok(engine.fake.callsOf('render.request').length > 0);
+ assert.equal(hideOsmCredit(engine, 'alpr'), true);
+ assert.equal(
+ engine.fake.credits.filter(({ onScreen }) => onScreen).length,
+ 0,
+ );
+});
diff --git a/src/globe/cesium/createCesiumEngine.js b/src/globe/cesium/createCesiumEngine.js
index 0139af1ee..03c348a28 100644
--- a/src/globe/cesium/createCesiumEngine.js
+++ b/src/globe/cesium/createCesiumEngine.js
@@ -34,6 +34,7 @@ export function createCesiumEngine(viewer, options = {}) {
// Viewer.canvas is scene.canvas; the engine reads the public alias.
const canvas = viewer.canvas ?? viewer.scene.canvas;
const render = createCesiumRender(viewer, options);
+ const credits = createCesiumCredits(viewer);
const parts = {
camera: createCesiumCamera(viewer),
picker: createCesiumPicker(viewer),
@@ -42,9 +43,10 @@ export function createCesiumEngine(viewer, options = {}) {
render,
surface: createCesiumSurface(viewer),
input: createCesiumInput(viewer, options),
- credits: createCesiumCredits(viewer),
+ credits,
maps: createCesiumMaps(viewer, {
...options,
+ credits: credits.credits,
requestRender: (reason) => render.render.request(reason),
}),
};
diff --git a/src/globe/cesium/credits.js b/src/globe/cesium/credits.js
index 364247b64..651568676 100644
--- a/src/globe/cesium/credits.js
+++ b/src/globe/cesium/credits.js
@@ -8,7 +8,9 @@ import * as Cesium from 'cesium';
*/
export function createCesiumCredits(viewer) {
const added = new Set();
- const display = () => viewer.creditDisplay;
+ // Viewer.creditDisplay is the scene frame state's display.
+ const display = () =>
+ viewer.creditDisplay ?? viewer.scene?.frameState?.creditDisplay;
const remove = (credit) => {
if (!added.delete(credit)) return;
display()?.removeStaticCredit(credit);
diff --git a/src/globe/cesium/credits.test.mjs b/src/globe/cesium/credits.test.mjs
index ffb48fdf0..f67aaa5d4 100644
--- a/src/globe/cesium/credits.test.mjs
+++ b/src/globe/cesium/credits.test.mjs
@@ -31,3 +31,18 @@ test('destroy removes the credits the engine added', () => {
assert.equal(viewer.staticCredits.length, 0);
assert.equal(viewer.callsOf('creditDisplay.removeStaticCredit').length, 2);
});
+
+test('the display is Viewer.creditDisplay, else the scene frame state one it aliases', () => {
+ const added = [];
+ const display = {
+ addStaticCredit: (credit) => added.push(credit),
+ removeStaticCredit: (credit) => added.splice(added.indexOf(credit), 1),
+ };
+ const { credits } = createCesiumCredits({
+ scene: { frameState: { creditDisplay: display } },
+ });
+ const remove = credits.addStatic('frame state');
+ assert.equal(added[0].html, 'frame state');
+ remove();
+ assert.deepEqual(added, []);
+});
diff --git a/src/globe/cesium/maps.js b/src/globe/cesium/maps.js
index 8cf29b0ba..291d139ab 100644
--- a/src/globe/cesium/maps.js
+++ b/src/globe/cesium/maps.js
@@ -63,6 +63,7 @@ const dispose = (value) => {
* tilesetRef?: { current: object | null } | (() => object | null),
* requestRender?: (reason: string) => void,
* createImageryLayer?: (provider: object) => object,
+ * credits?: object,
* loadPhotorealTileset?: (credentials: object) => Promise<{ tileset: object | null, errors: Error[] }>,
* }} [options] `tilesetRef` names the startup Google tileset the app added.
* @returns {{ maps: object, destroy: () => void }}
@@ -73,12 +74,16 @@ export function createCesiumMaps(
tilesetRef = null,
requestRender = () => viewer.scene.requestRender?.(),
createImageryLayer = (provider) => new Cesium.ImageryLayer(provider),
+ credits,
loadPhotorealTileset = (credentials) =>
loadPhotorealisticTileset(Cesium, credentials),
} = {},
) {
const abort = new AbortController();
- const mapScene = createMapScene(viewer, { createImageryLayer });
+ const mapScene = createMapScene(viewer, {
+ createImageryLayer,
+ ...(credits && { credits }),
+ });
const providers = new Map();
let ownedTileset = null;
let shownTileset = null;
diff --git a/src/maps/credits.js b/src/maps/credits.js
index 65a008e0c..002f933ea 100644
--- a/src/maps/credits.js
+++ b/src/maps/credits.js
@@ -1,21 +1,21 @@
-import * as Cesium from 'cesium';
-
-/** Static credits follow the source actually on screen, including fallback. */
-export function createMapCredits(viewer) {
- let active = null;
+/**
+ * Static credits follow the source actually on screen, including fallback.
+ * @param {{ addStatic: (html: string, options?: { onScreen?: boolean }) => () => void }} credits
+ * A globe engine's credits capability.
+ */
+export function createMapCredits(credits) {
+ let remove = null;
let markup = null;
return {
show(html) {
if (html === markup) return;
- const display = viewer?.scene?.frameState?.creditDisplay;
- if (!display) return;
try {
- if (active) display.removeStaticCredit(active);
- active = html ? new Cesium.Credit(html, true) : null;
+ remove?.();
+ remove = null;
+ if (html) remove = credits.addStatic(html, { onScreen: true });
markup = html;
- if (active) display.addStaticCredit(active);
} catch {
- /* Switching remains usable on older credit displays. */
+ /* Switching remains usable without a credit display. */
}
},
destroy() {
diff --git a/src/maps/sceneApply.js b/src/maps/sceneApply.js
index 7f82a77af..21a773d2d 100644
--- a/src/maps/sceneApply.js
+++ b/src/maps/sceneApply.js
@@ -1,4 +1,5 @@
import * as Cesium from 'cesium';
+import { createCesiumCredits } from '../globe/cesium/credits.js';
import { createMapCredits } from './credits.js';
/**
@@ -7,13 +8,20 @@ import { createMapCredits } from './credits.js';
* Shared by MapSourceController and the Cesium globe engine's `maps`, so
* both apply sources the same way. Creating it touches nothing.
* @param {Cesium.Viewer} viewer
- * @param {{ createImageryLayer?: (provider: object) => object }} [options]
+ * @param {{
+ * createImageryLayer?: (provider: object) => object,
+ * credits?: import('../globe/contract.js').GlobeEngine['credits'],
+ * }} [options] `credits`: the globe engine's credits (default: the viewer's
+ * credit display through the Cesium engine's credits).
*/
export function createMapScene(
viewer,
- { createImageryLayer = (provider) => new Cesium.ImageryLayer(provider) } = {},
+ {
+ createImageryLayer = (provider) => new Cesium.ImageryLayer(provider),
+ credits: engineCredits = createCesiumCredits(viewer).credits,
+ } = {},
) {
- const credits = createMapCredits(viewer);
+ const credits = createMapCredits(engineCredits);
let layer = null;
let provider = null;
const scene = {
diff --git a/src/maps/sceneApply.test.mjs b/src/maps/sceneApply.test.mjs
index 76aec8a5e..61d2df678 100644
--- a/src/maps/sceneApply.test.mjs
+++ b/src/maps/sceneApply.test.mjs
@@ -89,3 +89,21 @@ test('removeBaseImagery, tilesets, globe and credits', () => {
assert.deepEqual(layers, []);
assert.deepEqual(credits, []);
});
+
+test('the map scene shows its credit through a given credits capability', async () => {
+ const { createFakeEngine } = await import('../globe/testing/fakeEngine.js');
+ const engine = createFakeEngine();
+ const { viewer, credits } = fixture();
+ const scene = createMapScene(viewer, {
+ createImageryLayer: (provider) => ({ provider }),
+ credits: engine.credits,
+ });
+ scene.showCredit('Esri');
+ scene.showCredit('Esri');
+ assert.deepEqual(engine.fake.credits, [{ html: 'Esri', onScreen: true }]);
+ scene.showCredit('OSM');
+ assert.deepEqual(engine.fake.credits, [{ html: 'OSM', onScreen: true }]);
+ scene.destroy();
+ assert.deepEqual(engine.fake.credits, []);
+ assert.deepEqual(credits, [], 'nothing on the viewer display');
+});