mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 15:52:21 +00:00
Add data credits through the renderer's credits port
Data attribution no longer builds Cesium credits itself: each credit is added through the renderer's credits port, in the attribution list as before. The port's credit carries one link, so a credit whose markup holds several links becomes one credit per link, shown together in order, its wording kept. dataCredits.js leaves the Cesium allow-list.
This commit is contained in:
1 parent
8f612ba858
commit
9049357916
3 files changed
+167
-25
No files matched your search
+65
-24
@@ -1,4 +1,4 @@
|
|||||||
import * as Cesium from 'cesium';
|
import { creditFromMarkup } from '../render/creditMarkup.js';
|
||||||
import { findCredits } from '../render/rendererOf.js';
|
import { findCredits } from '../render/rendererOf.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -617,43 +617,84 @@ export const BHOTE_KOSHI_LOCATOR_CREDIT = {
|
|||||||
'(CC BY-NC 4.0)',
|
'(CC BY-NC 4.0)',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** A link, its whole element. */
|
||||||
|
const LINK = /<a\b(?:[^>"']|"[^"]*"|'[^']*')*>[\s\S]*?<\/a\s*>/gi;
|
||||||
|
/**
|
||||||
|
* The separators text between two links can break at; it breaks at its
|
||||||
|
* last. A semicolon ending a character reference is not one. A full stop
|
||||||
|
* after an abbreviation reads as one, so keep "Dr." and "e.g." out of the
|
||||||
|
* text between links.
|
||||||
|
*/
|
||||||
|
const SEPARATOR = /\s*(?:[,·—–]|(?<!&#?\w+);)\s*|\.\s+|\s+(?=\()/g;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A credit's markup as the renderer's credits, one link each: the
|
||||||
|
* contract's credit carries one link, so markup with several becomes one
|
||||||
|
* credit per link, shown together in order. The text between two links
|
||||||
|
* breaks at its last separator (a comma, semicolon, dot or dash, a full
|
||||||
|
* stop, or before an opening bracket); closing brackets right after a link
|
||||||
|
* stay with it, and a break's comma, semicolon, dot or dash is dropped.
|
||||||
|
* @param {string} html
|
||||||
|
* @returns {import('../render/contract/credits').Credit[]}
|
||||||
|
*/
|
||||||
|
export function creditsFromMarkup(html) {
|
||||||
|
const markup = String(html);
|
||||||
|
const pieces = [''];
|
||||||
|
let at = 0;
|
||||||
|
for (const link of markup.matchAll(LINK)) {
|
||||||
|
let gap = markup.slice(at, link.index);
|
||||||
|
if (at > 0) {
|
||||||
|
const close = /^[)\]]*/.exec(gap)[0];
|
||||||
|
gap = gap.slice(close.length);
|
||||||
|
const cut = [...gap.matchAll(SEPARATOR)].at(-1);
|
||||||
|
const before = cut ? gap.slice(0, cut.index) : '';
|
||||||
|
const stop = cut && cut[0].trimStart().startsWith('.') ? '.' : '';
|
||||||
|
pieces[pieces.length - 1] += close + before + stop;
|
||||||
|
pieces.push(cut ? gap.slice(cut.index + cut[0].length) : gap);
|
||||||
|
} else pieces[0] += gap;
|
||||||
|
pieces[pieces.length - 1] += link[0];
|
||||||
|
at = link.index + link[0].length;
|
||||||
|
}
|
||||||
|
pieces[pieces.length - 1] += markup.slice(at);
|
||||||
|
return pieces
|
||||||
|
.map((piece) => ({ ...creditFromMarkup(piece), inline: false }))
|
||||||
|
.filter((credit) => credit.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Add a data credit to the attribution list through the renderer. */
|
||||||
|
function addDataCredit(viewer, credit) {
|
||||||
|
const credits = findCredits(viewer);
|
||||||
|
if (!credits) return false;
|
||||||
|
for (const one of creditsFromMarkup(credit.html)) credits.addStatic(one);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/** @type {Set<string>} Keys of dynamic credits already registered this session. */
|
/** @type {Set<string>} Keys of dynamic credits already registered this session. */
|
||||||
const _dynamicCreditKeys = new Set();
|
const _dynamicCreditKeys = new Set();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a conditional credit at the moment its data source activates.
|
* Register a conditional credit at the moment its data source activates.
|
||||||
* Idempotent per `credit.key`; lands in the same "Data attribution" popover
|
* Idempotent per `credit.key`; lands in the same "Data attribution" list
|
||||||
* as the static credits (showOnScreen=false).
|
* as the static credits, through the renderer's credits port.
|
||||||
* @param {Cesium.Viewer} viewer — the initialized Cesium viewer
|
* @param {object} viewer — the scene object the renderer is bound to
|
||||||
* @param {{ key: string, html: string }} credit — e.g. `TOMTOM_CREDIT`
|
* @param {{ key: string, html: string }} credit — e.g. `TOMTOM_CREDIT`
|
||||||
* @returns {boolean} True when the credit is (now) registered.
|
* @returns {boolean} True when the credit is (now) registered.
|
||||||
*/
|
*/
|
||||||
export function registerDynamicCredit(viewer, credit) {
|
export function registerDynamicCredit(viewer, credit) {
|
||||||
const creditDisplay = viewer?.creditDisplay;
|
if (!viewer || !credit?.key || !credit?.html) return false;
|
||||||
if (!creditDisplay || typeof creditDisplay.addStaticCredit !== 'function') {
|
if (_dynamicCreditKeys.has(credit.key)) return Boolean(findCredits(viewer));
|
||||||
return false;
|
if (!addDataCredit(viewer, credit)) return false;
|
||||||
}
|
|
||||||
if (!credit?.key || !credit?.html) return false;
|
|
||||||
if (_dynamicCreditKeys.has(credit.key)) return true;
|
|
||||||
creditDisplay.addStaticCredit(new Cesium.Credit(credit.html, false));
|
|
||||||
_dynamicCreditKeys.add(credit.key);
|
_dynamicCreditKeys.add(credit.key);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register every per-layer data credit into the viewer's credit display.
|
* Register every per-layer data credit through the renderer's credits
|
||||||
* Idempotent: safe to call once at init. Credits are static and always
|
* port. Safe to call once at init. Credits are static and always present
|
||||||
* present in the "Data attribution" popover.
|
* in the "Data attribution" list, not on the map's credit line.
|
||||||
* @param {Cesium.Viewer} viewer — the initialized Cesium viewer
|
* @param {object} viewer — the scene object the renderer is bound to
|
||||||
*/
|
*/
|
||||||
export function registerDataCredits(viewer, credits = DATA_CREDITS) {
|
export function registerDataCredits(viewer, credits = DATA_CREDITS) {
|
||||||
const creditDisplay = viewer?.creditDisplay;
|
if (!viewer) return;
|
||||||
if (!creditDisplay || typeof creditDisplay.addStaticCredit !== 'function') {
|
for (const credit of credits) addDataCredit(viewer, credit);
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const { html } of credits) {
|
|
||||||
// showOnScreen=false → lives in the expandable "Data attribution" popover,
|
|
||||||
// not the on-globe credit line.
|
|
||||||
creditDisplay.addStaticCredit(new Cesium.Credit(html, false));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -142,3 +142,105 @@ test('every Street Level imagery source is in the attribution lightbox', () => {
|
|||||||
`${key} has no credit`,
|
`${key} has no credit`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a credit with several links becomes one credit per link, in order', async () => {
|
||||||
|
const {
|
||||||
|
creditsFromMarkup,
|
||||||
|
OSM_CREDIT,
|
||||||
|
OPENMAPTILES_CREDIT,
|
||||||
|
WOF_CREDIT,
|
||||||
|
BHOTE_KOSHI_CREDIT,
|
||||||
|
} = await import('./dataCredits.js');
|
||||||
|
const html = (key) => DATA_CREDITS.find((entry) => entry.key === key).html;
|
||||||
|
const read = (markup) =>
|
||||||
|
creditsFromMarkup(markup).map(({ text, href }) => [text, href]);
|
||||||
|
assert.deepEqual(read(OSM_CREDIT.html), [
|
||||||
|
[
|
||||||
|
'Map and place data © OpenStreetMap contributors',
|
||||||
|
'https://www.openstreetmap.org/copyright',
|
||||||
|
],
|
||||||
|
['(ODbL)', 'https://opendatacommons.org/licenses/odbl/1-0/'],
|
||||||
|
]);
|
||||||
|
assert.deepEqual(read(OPENMAPTILES_CREDIT.html), [
|
||||||
|
['Vector tiles: OpenFreeMap', 'https://openfreemap.org'],
|
||||||
|
['© OpenMapTiles', 'https://openmaptiles.org'],
|
||||||
|
]);
|
||||||
|
assert.deepEqual(read(html('nasa-gibs'))[1], [
|
||||||
|
'HLS product page',
|
||||||
|
'https://lpdaac.usgs.gov/products/hlss30v002/',
|
||||||
|
]);
|
||||||
|
assert.deepEqual(read(html('wfigs')), [
|
||||||
|
[
|
||||||
|
'Wildfire perimeters: National Interagency Fire Center (WFIGS)',
|
||||||
|
'https://data-nifc.opendata.arcgis.com/',
|
||||||
|
],
|
||||||
|
['Incident information: InciWeb', 'https://inciweb.wildfire.gov/'],
|
||||||
|
]);
|
||||||
|
assert.deepEqual(read(html('eccc-aqhi')), [
|
||||||
|
[
|
||||||
|
'Air quality (AQHI): Environment and Climate Change Canada — MSC GeoMet (eccc-msc.github.io/open-data)',
|
||||||
|
'https://eccc-msc.github.io/open-data/',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'ECCC Data Servers End-use Licence',
|
||||||
|
'https://eccc-msc.github.io/open-data/licence/readme_en/',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
assert.deepEqual(
|
||||||
|
read(BHOTE_KOSHI_CREDIT.html)[1][0].slice(0, 15),
|
||||||
|
'and GeoPera (CC',
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
read(WOF_CREDIT.html).map(([text]) => text),
|
||||||
|
[
|
||||||
|
"Neighborhood boundaries: Who's On First and contributors, simplified",
|
||||||
|
'(sources and licences)',
|
||||||
|
],
|
||||||
|
);
|
||||||
|
const link = (text) => `<a href="https://example.org">${text}</a>`;
|
||||||
|
assert.deepEqual(
|
||||||
|
read(`${link('A')} by Hoffleit & Warren ${link('B')}`).map(
|
||||||
|
([text]) => text,
|
||||||
|
),
|
||||||
|
['A', 'by Hoffleit & Warren B'],
|
||||||
|
'a character reference is not a separator',
|
||||||
|
);
|
||||||
|
assert.deepEqual(read(html('usgs')), [
|
||||||
|
['Earthquakes: Data courtesy of the U.S. Geological Survey', undefined],
|
||||||
|
]);
|
||||||
|
for (const entry of DATA_CREDITS) {
|
||||||
|
const credits = creditsFromMarkup(entry.html);
|
||||||
|
const links = entry.html.match(/<a\b/g)?.length ?? 0;
|
||||||
|
assert.equal(credits.length, Math.max(links, 1), entry.key);
|
||||||
|
assert.ok(
|
||||||
|
credits.every((credit) => credit.inline === false),
|
||||||
|
`${entry.key} belongs in the attribution list`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('data credits go through the renderer to the attribution list', async () => {
|
||||||
|
const { registerDataCredits, registerDynamicCredit, TOMTOM_CREDIT } =
|
||||||
|
await import('./dataCredits.js');
|
||||||
|
const shown = [];
|
||||||
|
const viewer = bindFakeCesiumCredits({
|
||||||
|
creditDisplay: {
|
||||||
|
addStaticCredit: (c) => shown.push(c),
|
||||||
|
removeStaticCredit() {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const credits = DATA_CREDITS.filter((entry) =>
|
||||||
|
['openstreetmap', 'usgs'].includes(entry.key),
|
||||||
|
);
|
||||||
|
registerDataCredits(viewer, credits);
|
||||||
|
assert.equal(shown.length, 3);
|
||||||
|
assert.ok(shown.every((credit) => !credit.showOnScreen));
|
||||||
|
const odbl = shown.find((credit) => credit.html.includes('opendatacommons'));
|
||||||
|
assert.match(odbl.html, /href="https:\/\/opendatacommons.org/);
|
||||||
|
assert.match(odbl.html, />\(ODbL\)</);
|
||||||
|
assert.equal(registerDynamicCredit(viewer, TOMTOM_CREDIT), true);
|
||||||
|
assert.equal(registerDynamicCredit(viewer, TOMTOM_CREDIT), true);
|
||||||
|
assert.equal(shown.length, 4, 'a dynamic credit is added once');
|
||||||
|
assert.match(shown[3].html, /Traffic flow data © TomTom/);
|
||||||
|
assert.equal(registerDynamicCredit({}, { key: 'none', html: 'x' }), false);
|
||||||
|
});
|
||||||
@@ -24,7 +24,6 @@
|
|||||||
"src/data/cctvGizmo.js",
|
"src/data/cctvGizmo.js",
|
||||||
"src/data/cctvViewshed.js",
|
"src/data/cctvViewshed.js",
|
||||||
"src/data/contactTrailRenderer.js",
|
"src/data/contactTrailRenderer.js",
|
||||||
"src/data/dataCredits.js",
|
|
||||||
"src/data/detection.js",
|
"src/data/detection.js",
|
||||||
"src/data/focusDeemphasis.js",
|
"src/data/focusDeemphasis.js",
|
||||||
"src/data/geoFileImport.js",
|
"src/data/geoFileImport.js",
|
||||||
|
|||||||
Reference in new issue
Block a user