The footer's attribution is a licence statement, so it has to be true rather than merely present. posterDefaults ships "(c) Mapbox (c) OpenStreetMap" and any caller that did not override it printed exactly that -- including country-posters, whose maps run on the fleet's MapLibre default, where Mapbox serves nothing at all and OpenFreeMap, OpenMapTiles and OpenStreetMap are the three parties whose licences actually apply. research-map had a hand-written pair for the two providers, which was right for research-map and helped nobody else. The obvious approach is to read the style's sources. Checked against the live style rather than assumed, and it does not work: tiles.openfreemap.org/styles/ liberty declares two sources and NEITHER carries an attribution field. The credit lives in the TileJSON the vector source points at, which the engine fetches and merges into the SOURCE OBJECT once it loads. The style is the request and the source is what came back, and only the second one knows. map-attribution.js reads the live source first and falls back to any style-declared value. The TileJSON value is HTML with anchors and a © entity, and a poster footer is text, so it is flattened rather than printed raw. Repeated credits collapse. When nothing can be read -- no map, a style that throws before it has loaded -- it returns empty and the caller keeps its own string, because a blank licence line is the one outcome that is never acceptable. The check uses the real TileJSON string as its fixture. A tidy invented style would have passed while the real map printed the wrong credit, and its first negative test is exactly that: reading the style alone returns nothing for OpenFreeMap. Cut directly on main rather than on the branch this began life in, so it does not wait behind a poster-layout change it has nothing to do with. A wrong tile credit is a licence problem; the layout question it was sitting behind is not.
54 lines
2.8 KiB
JavaScript
54 lines
2.8 KiB
JavaScript
/**
|
|
* map-attribution — the credit the tiles on THIS map actually ask for, as plain text.
|
|
*
|
|
* ★ WHY THIS IS READ AND NOT DECLARED. The poster footer defaulted to '© Mapbox © OpenStreetMap',
|
|
* which is a licence statement, so it must be true. Under the fleet's MapLibre default Mapbox serves
|
|
* nothing and OpenFreeMap serves everything, and any poster carrying that default credited a company
|
|
* with no involvement while omitting the three parties whose licences actually apply.
|
|
*
|
|
* ★ AND IT CANNOT BE READ FROM THE STYLE. Checked against the live style 2026-09-28:
|
|
* tiles.openfreemap.org/styles/liberty declares TWO sources and NEITHER carries an `attribution`
|
|
* field. The credit lives in the TileJSON the vector source points at
|
|
* (tiles.openfreemap.org/planet), which the engine fetches and merges into the SOURCE OBJECT once it
|
|
* loads. So `map.getStyle().sources[id].attribution` is null and `map.getSource(id).attribution` is
|
|
* the real answer — the style is the request, the source is what came back. This reads both and
|
|
* prefers the live source, because a style that DOES declare one should still be honoured.
|
|
*
|
|
* The TileJSON value is HTML with anchors and entities; a poster footer is text, so it is flattened
|
|
* rather than escaped-and-printed. Returns '' when nothing can be read, and the CALLER keeps its own
|
|
* fallback — printing an empty attribution is the one outcome that is never acceptable.
|
|
*/
|
|
|
|
const ENTITIES = { '©': '©', '&': '&', ' ': ' ', '<': '<', '>': '>', '"': '"', ''': "'" };
|
|
|
|
/** Flatten one attribution value (HTML or plain) to trimmed, single-spaced text. */
|
|
export function attributionToText(value) {
|
|
if (!value) return '';
|
|
return String(value)
|
|
.replace(/<[^>]*>/g, ' ') // anchors carry the link, not the credit
|
|
.replace(/&[a-z]+;|&#\d+;/gi, (e) => ENTITIES[e.toLowerCase()] ?? ' ')
|
|
.replace(/\s+/g, ' ')
|
|
.trim();
|
|
}
|
|
|
|
/**
|
|
* Every distinct credit the map's sources ask for, joined into one line.
|
|
*
|
|
* @param {Object} map a mapboxgl/maplibregl Map instance
|
|
* @returns {string} plain text, or '' when the map cannot be read
|
|
*/
|
|
export function attributionTextFromMap(map) {
|
|
if (!map || typeof map.getStyle !== 'function') return '';
|
|
let style;
|
|
try { style = map.getStyle(); } catch (_) { return ''; } // getStyle throws before the style loads
|
|
const sources = (style && style.sources) || {};
|
|
const seen = [];
|
|
for (const id of Object.keys(sources)) {
|
|
// The live source first: it is the one the TileJSON was merged into.
|
|
let live = null;
|
|
try { live = typeof map.getSource === 'function' ? map.getSource(id) : null; } catch (_) { live = null; }
|
|
const text = attributionToText((live && live.attribution) || sources[id].attribution);
|
|
if (text && !seen.includes(text)) seen.push(text);
|
|
}
|
|
return seen.join(' ');
|
|
}
|