doxa-maps/library/composables/useMapAttribution.js
Doxa Maps 93ca8dab91
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Update from source d3d3c3f8
2026-09-26 11:59:17 +00:00

107 lines
5.4 KiB
JavaScript

// useMapAttribution.js — the data-credit line every map MUST carry.
//
// import { addMapAttribution } from '@map/composables/useMapAttribution'
// addMapAttribution(map, { boundaries: true })
//
// ═══ WHY THIS EXISTS — IT IS A LICENCE OBLIGATION, NOT A NICETY ═════════════
// Every map in this repo constructs Mapbox with `attributionControl: false`
// (useMapInstance.js and all three research profiles), and NOTHING replaced it. So
// the live maps were rendering NO attribution at all — not for geoBoundaries, and
// not for Mapbox/OpenStreetMap either, whose terms also require it.
//
// geoBoundaries' own request, verbatim: "we ask that you put the name
// 'geoBoundaries' with a link back to this website, www.geoboundaries.org,
// somewhere prominent on the page that uses the boundaries."
//
// gbOpen is ALSO a MIXED-LICENCE collection — it passes through each country's
// upstream licence, verified live against their API:
// India ADM1 CC BY 2.5 IN (source: github.com/datameet/maps)
// Sudan ADM0 CC BY 3.0 IGO (source: HDX)
// Brazil ADM0 CC BY 3.0 IGO (source: HDX)
// USA ADM0 Public Domain (US Census)
// China ADM0 Public Domain (Wikimedia Commons)
// Several therefore carry a genuine per-country attribution requirement. A repo
// comment previously called this "Apache-2.0-ish", which is wrong and would be the
// wrong basis for a publication decision.
//
// SELF-HOSTED, NO NETWORK. This is plain DOM + inline style: it adds no request, so
// it cannot leak a viewer's IP the way a third-party badge would. That matters here
// — see the open OPEN_DEFECT_mapbox_cdn finding.
const LINKS = {
boundaries: {
label: 'geoBoundaries',
href: 'https://www.geoboundaries.org',
title: 'Country and administrative boundaries — geoBoundaries gbOpen (mixed upstream licences; see docs)',
},
mapbox: { label: 'Mapbox', href: 'https://www.mapbox.com/about/maps/' },
osm: { label: '© OpenStreetMap', href: 'https://www.openstreetmap.org/copyright' },
// Under MapLibre the basemap is OpenFreeMap's, built from OpenMapTiles on OpenStreetMap data — the
// credit its own TileJSON asks for. Mapbox serves nothing on those maps and must not be credited.
openfreemap: { label: 'OpenFreeMap', href: 'https://openfreemap.org' },
openmaptiles: { label: '© OpenMapTiles', href: 'https://www.openmaptiles.org/' },
osmContributors: { label: '© OpenStreetMap contributors', href: 'https://www.openstreetmap.org/copyright' },
};
/** The basemap credit for the engine that actually drew THIS map (read from the instance, not config). */
function basemapCredit(map) {
const ml = typeof window !== 'undefined' ? window.maplibregl : null;
const isMaplibre = !!(ml && ml.Map && map instanceof ml.Map);
return isMaplibre ? [LINKS.openfreemap, LINKS.openmaptiles, LINKS.osmContributors] : [LINKS.mapbox, LINKS.osm];
}
/**
* Add the bottom-right attribution bar to a Mapbox map.
*
* @param {Object} map a mapboxgl.Map
* @param {Object} [opts]
* @param {boolean} [opts.boundaries=true] credit geoBoundaries (set false only if
* the map draws NO gb polygons at all)
* @param {boolean} [opts.basemap=true] credit the basemap of the engine that drew this map:
* Mapbox + OpenStreetMap, or under MapLibre OpenFreeMap +
* OpenMapTiles + OpenStreetMap contributors
*/
export function addMapAttribution(map, opts = {}) {
if (!map || typeof map.addControl !== 'function') return null;
const { boundaries = true, basemap = true } = opts;
const el = document.createElement('div');
el.className = 'doxa-attribution mapboxgl-ctrl';
// Inline style: these maps live in SHADOW ROOTS, so a stylesheet in the host page
// would not reach here. Kept muted and small so it reads as a credit line, not UI.
el.style.cssText = [
'font:11px/1.45 system-ui,-apple-system,Segoe UI,Roboto,sans-serif',
'background:rgba(255,255,255,.82)', 'color:#3a4048',
'padding:2px 6px', 'border-radius:3px 0 0 0',
'pointer-events:auto', 'max-width:min(92vw,420px)',
].join(';');
const parts = [];
if (boundaries) parts.push(LINKS.boundaries);
if (basemap) parts.push(...basemapCredit(map));
el.innerHTML = parts.map((p) =>
`<a href="${p.href}" target="_blank" rel="noopener noreferrer"`
+ (p.title ? ` title="${p.title}"` : '')
+ ` style="color:#2a5d8f;text-decoration:none">${p.label}</a>`).join(' · ');
const control = {
onAdd: () => el,
onRemove: () => { el.parentNode && el.parentNode.removeChild(el); },
getDefaultPosition: () => 'bottom-right',
};
map.addControl(control, 'bottom-right');
return control;
}
/**
* The same credit as plain text — for posters, PDFs and print, which have no DOM — for the engine that
* drew the map ('maplibre' → OpenFreeMap / OpenMapTiles / OpenStreetMap contributors).
*/
export function attributionText(provider = 'mapbox') {
return provider === 'maplibre'
? 'Boundaries: geoBoundaries (geoboundaries.org) · OpenFreeMap © OpenMapTiles © OpenStreetMap contributors'
: 'Boundaries: geoBoundaries (geoboundaries.org) · © Mapbox © OpenStreetMap';
}
/** @deprecated the Mapbox credit only; use attributionText(provider). Kept for existing importers. */
export const ATTRIBUTION_TEXT = attributionText('mapbox');