refactor(credits): use the globe engine

Map credits (maps/credits.js) are shown through a credits capability
(engine.credits.addStatic) instead of the scene's credit display; the
shared map scene takes the engine's credits, so the Cesium engine's maps
and its credits part share one owner. Data credits (registerDataCredits,
registerDynamicCredit, show/hideOsmCredit) take the globe engine and keep
accepting a viewer from layers that still hold one; the scene registers
the data credits on its engine. The Cesium credits part reads
Viewer.creditDisplay, else the scene frame state display it aliases.
This commit is contained in:
German Khokhlov committed 2026-10-15 18:37:02 +04:00
1 parent be56144d75
commit 83c13f330d
11 files changed
+169 -59

No files matched your search

+2
View File
@@ -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",
+1 -1
View File
@@ -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
+60 -41
View File
@@ -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(
'<a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener">© OpenStreetMap</a>',
true,
),
remove: null,
html: '<a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener">© OpenStreetMap</a>',
},
tiles: {
visible: false,
credit: new Cesium.Credit(
'<a href="https://openmaptiles.org" target="_blank" rel="noopener">© OpenMapTiles</a>',
true,
),
remove: null,
html: '<a href="https://openmaptiles.org" target="_blank" rel="noopener">© OpenMapTiles</a>',
},
};
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 });
}
}
+39
View File
@@ -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,
);
});
+3 -1
View File
@@ -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),
}),
};
+3 -1
View File
@@ -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);
+15
View File
@@ -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, []);
});
+6 -1
View File
@@ -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;
+11 -11
View File
@@ -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() {
+11 -3
View File
@@ -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 = {
+18
View File
@@ -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');
});