doxa-maps/plugin-designs/pplr-data-maps/src/utils/pplrData.js
doxa-maps-88655c70 17931ed031 PPLR data maps: every view loads under MapLibre, and the legend has accessible names
Under provider=maplibre (the keyless gallery) four of the seven map views never
finished loading. They built `new mapboxgl.NavigationControl` in the load
handler, and that global does not exist under MapLibre, so they sat on
"Loading…". Popups and the pin marker had the same fault. Every such call now
goes through glFor(map), the namespace of the engine that built the map. The
views' styles and the Dark/Satellite/Light switch now use registry names (ink,
hybrid, paper), which are the same Mapbox styles under Mapbox and OpenFreeMap
under MapLibre.

The search box loads its geocoder by engine, as GeocoderComponent does. Under
MapLibre it is MaplibreGeocoder over this map's own local index, so the keyless
gallery makes no request to api.mapbox.com at all.

The legend's row caret, clear chip and collapse/reopen buttons carry accessible
names and aria-expanded from legend.a11y.* and aria.* (optional i18n, English
fallback), the keys the library legend uses. check:shared-reached now counts
pplr-data-maps as carrying legend-a11y-keys.

window.__pplrMap() reports what the active view's map did (engine, load fired,
style layers, this bundle's data sources), so "loaded" is asserted, not inferred.

Measured on a served built tree, host page on another origin: all ten views
loaded under MapLibre and under Mapbox (load fired, style layers and data
sources present, canvas not blank, 0 errors), with 0 api.mapbox.com requests
under MapLibre. The same check on the parent build fails four map views under
MapLibre. A control with no provider makes 13 Mapbox requests.
2026-09-25 09:57:17 +00:00

90 lines
4.7 KiB
JavaScript

/**
* pplrData.js — where this bundle's data files are served from.
*
* The maps were written against a page that served assets/data/ at the site root. Embedded, that
* relative path resolves against the HOST page and 404s, so every fetch goes through dataUrl(),
* which joins the tree's shared assets base (set once by the hub from resolveAssetsBase) with
* pplr/<file>. The files are derived by ../scripts/build-data.mjs into data/pplr/.
*/
let base = './assets'
export function setDataBase(b) { if (b) base = String(b).replace(/\/+$/, '') }
export function dataUrl(file) { return base + '/pplr/' + file }
// d3 v7 is a page global here (as in the original build). Load it once, on first need.
let d3Promise = null
export function ensureD3() {
if (typeof window === 'undefined' || window.d3) return Promise.resolve()
if (!d3Promise) d3Promise = new Promise((res, rej) => {
const s = document.createElement('script')
s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js'
s.onload = () => res(); s.onerror = () => { d3Promise = null; rej(new Error('d3 failed to load')) }
document.head.appendChild(s)
})
return d3Promise
}
// The 17 locale names per PG, split out of pg-points so the first paint stays small. Loaded on
// the first pin that opens a detail card.
let namesPromise = null
export function loadNames() {
if (!namesPromise) namesPromise = fetch(dataUrl('pg-names.json'))
.then((r) => { if (!r.ok) throw new Error('pg-names.json ' + r.status); return r.json() })
.catch((e) => { namesPromise = null; throw e })
return namesPromise
}
// The geocoder is a page global too (the original page carried a <script> tag). Load it once, on
// first need, and pick it BY ENGINE, as library/components/map-controls/GeocoderComponent.vue does:
// the Mapbox plugin under provider=mapbox, MapLibre's under provider=maplibre. A hard-coded Mapbox
// script made this the only request to api.mapbox.com in the keyless (maplibre) gallery.
export const GEOCODER = {
mapbox: { js: 'https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v5.0.0/mapbox-gl-geocoder.min.js',
css: 'https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v5.0.0/mapbox-gl-geocoder.css',
global: 'MapboxGeocoder' },
maplibre: { js: 'https://cdn.jsdelivr.net/npm/@maplibre/maplibre-gl-geocoder@1/dist/maplibre-gl-geocoder.min.js',
css: 'https://cdn.jsdelivr.net/npm/@maplibre/maplibre-gl-geocoder@1/dist/maplibre-gl-geocoder.css',
global: 'MaplibreGeocoder' },
}
const geocoderPromises = {}
export function ensureGeocoder(provider = 'mapbox') {
const g = GEOCODER[provider] || GEOCODER.mapbox
if (typeof window === 'undefined' || window[g.global]) return Promise.resolve()
if (!geocoderPromises[provider]) geocoderPromises[provider] = new Promise((res, rej) => {
const s = document.createElement('script')
s.src = g.js
s.onload = () => res(); s.onerror = () => { delete geocoderPromises[provider]; rej(new Error('geocoder failed to load')) }
document.head.appendChild(s)
})
return geocoderPromises[provider]
}
// The engine namespace that built THIS map (Popup, Marker, NavigationControl…). The views were
// written against the Mapbox global; under provider=maplibre that global does not exist and a
// `new mapboxgl.Popup` throws on the first pin click.
export function glFor(map) {
if (typeof window === 'undefined') return undefined
const ml = window.maplibregl
if (ml && map && ml.Map && map instanceof ml.Map) return ml
return window.mapboxgl || ml
}
// Verification handle (the gospel-resources __grState contract): what the ACTIVE view's map
// actually did, so a check can assert "loaded" rather than inferring it from missing requests.
// window.__pplrMap() -> { engine, loaded, styleLayers, pplrLayers }
export function exposeMap(map) {
if (typeof window === 'undefined' || !map) return
let loaded = false
map.once('load', () => { loaded = true })
window.__pplrMap = () => {
let layers = []
try { layers = map.getStyle()?.layers || [] } catch (_) {}
return {
engine: window.maplibregl && window.maplibregl.Map && map instanceof window.maplibregl.Map ? 'maplibre' : 'mapbox',
// 'load' may already have fired before this listener was attached; a loaded style says so too
loaded: loaded || !!(map.isStyleLoaded && map.isStyleLoaded()),
styleLayers: layers.length,
// sources this bundle added (not the basemap's): proof the view's own data reached the map
dataSources: (() => { try { return Object.keys(map.getStyle()?.sources || {}).filter((k) => !/^(composite|mapbox|openmaptiles|natural_earth|ne2_shaded|maplibre)/i.test(k)).length } catch (_) { return 0 } })(),
}
}
}