From 056942b33fd24ff6d65fc2ac3bfc6439baffaffc Mon Sep 17 00:00:00 2001 From: doxa-maps-88655c70 <88627970+doxa-maps-88655c70@users.noreply.github.com> Date: Fri, 25 Sep 2026 18:01:49 +0000 Subject: [PATCH] engineFor(map): build markers, popups and poster maps with the map's OWN engine Every generated leaf preloaded mapbox-gl.js whatever the provider, so code that used the bare `mapboxgl` global always found one, even on a MapLibre map. Removing that peer (it sends every visitor's IP to Mapbox under provider=maplibre) turns each such site into an error. engineFor(map), next to isMapboxMap in useMapProvider.js, asks the map which engine built it: - useSelectedPin: the selected-pin Marker - useMapEvents: the pin-fallback and region Popups - useMapPoster: main already builds the hidden map with the source map's engine (1cbfb3aa); its choice now goes through this one helper instead of a second inline sniff - pplr-data-maps: glFor is now this helper; the geocoder awaits ensureEngineScript first - key-cities: ensureEngineScript('mapbox') before reading the global Measured with every api.mapbox.com tag stripped from the served leaf (no peer at all for key-cities), before -> after: pin select under MapLibre, on cities-internet, country-posters, doxa-research-map and doxa-simple-map: "reading 'Marker'" -> marker drawn; country-posters region popup: none -> popup; PPLR search: no input -> suggestions (both engines); key-cities: "mapbox-gl not loaded" -> map loaded. Mapbox rows unchanged (they passed before and after). The poster UI could not be driven: PosterDialog never opens in country-posters or doxa-research-map (they pass props it does not declare and no modelValue/getMap), reported separately. useMapPoster itself was rendered from a real map with only its engine on the page: MapLibre "mapboxgl global not found" on the pre-1cbfb3aa library -> a 400x300 PNG with 88 colours. --- library/composables/poster/useMapPoster.js | 3 ++- library/composables/useMapEvents.js | 5 +++-- library/composables/useMapProvider.js | 22 +++++++++++++++++++ library/composables/useSelectedPin.js | 5 +++-- plugin-designs/key-cities/cities-map.vue | 6 ++++- .../src/components/MapboxGeocoder.vue | 10 ++++++--- .../pplr-data-maps/src/utils/pplrData.js | 9 ++------ 7 files changed, 44 insertions(+), 16 deletions(-) diff --git a/library/composables/poster/useMapPoster.js b/library/composables/poster/useMapPoster.js index b69f773b6..1ca5e9b75 100644 --- a/library/composables/poster/useMapPoster.js +++ b/library/composables/poster/useMapPoster.js @@ -33,6 +33,7 @@ import { ref, readonly } from 'vue'; import { computeLayout } from './usePosterLayout.js'; +import { engineFor } from '../useMapProvider.js'; const MAX_SINGLE_CANVAS = 4096; // WebGL safe ceiling for most hardware const TILE_PX = 2048; // tile size for stitch path @@ -370,7 +371,7 @@ async function _spawnHiddenMap(sourceMap, w, h, opts = {}) { // eslint-disable-next-line no-undef const W = (typeof window !== 'undefined' ? window : {}); const isMaplibre = !!W.maplibregl && sourceMap instanceof W.maplibregl.Map; - const gl = isMaplibre ? W.maplibregl : W.mapboxgl; + const gl = engineFor(sourceMap); // the one per-instance helper (useMapProvider.js) if (!gl) throw new Error('useMapPoster: ' + (isMaplibre ? 'maplibregl' : 'mapboxgl') + ' global not found'); const styleObj = sourceMap.getStyle(); diff --git a/library/composables/useMapEvents.js b/library/composables/useMapEvents.js index a5cc61f60..6871af20b 100644 --- a/library/composables/useMapEvents.js +++ b/library/composables/useMapEvents.js @@ -5,6 +5,7 @@ // Import Vue functions from NPM package import { ref, inject } from 'vue'; import { useI18n } from 'vue-i18n'; +import { engineFor } from './useMapProvider.js'; /** * useMapEvents composable - manages Mapbox layer event handlers @@ -245,7 +246,7 @@ export function useMapEvents(options = {}) { } // Fallback: show popup (for non-prayer legend types) - new mapboxgl.Popup({ + new (engineFor(map).Popup)({ maxWidth: '320px', className: 'people-group-popup' }) @@ -290,7 +291,7 @@ export function useMapEvents(options = {}) { if (e.features && e.features.length > 0) { const properties = e.features[0].properties; - new mapboxgl.Popup() + new (engineFor(map).Popup)() .setLngLat(e.lngLat) .setHTML(generateRegionPopupContent(properties)) .addTo(map); diff --git a/library/composables/useMapProvider.js b/library/composables/useMapProvider.js index 00cbf3557..f764575cb 100644 --- a/library/composables/useMapProvider.js +++ b/library/composables/useMapProvider.js @@ -61,6 +61,28 @@ export function isMapboxMap(map) { return typeof window !== 'undefined' && !!window.mapboxgl && map instanceof window.mapboxgl.Map } +/** + * The engine namespace that built THIS map: window.maplibregl or window.mapboxgl. Use it for + * everything constructed alongside a map (Marker, Popup, a second Map, controls) instead of + * the bare `mapboxgl` global. + * + * ★ WHY. Every generated leaf used to load mapbox-gl.js statically, whatever the provider, + * so a bare `new mapboxgl.Popup()` always found a global, even on a MapLibre map (where it + * built a MAPBOX popup and attached it to a MapLibre map). Removing that static peer (a visitor-IP + * leak to api.mapbox.com under MapLibre) turns each of those into a ReferenceError on the first + * pin select or click, and the poster's hidden map into 'mapboxgl global not found'. + * Asking the MAP which engine it is removes the dependency on what else the page loaded. + * @param {object} map the live map instance + * @returns {object|undefined} the engine namespace, or undefined when neither is loaded + */ +export function engineFor(map) { + if (typeof window === 'undefined') return undefined + const ml = window.maplibregl, mb = window.mapboxgl + if (ml && ml.Map && map instanceof ml.Map) return ml + if (mb && mb.Map && map instanceof mb.Map) return mb + return mb || ml +} + /** Mapbox-Standard-only lighting paint props, per engine. MapLibre rejects layers whose * paint carries unknown properties (the whole layer is dropped → invisible pins), so * spread this instead of writing '-emissive-strength' literally. diff --git a/library/composables/useSelectedPin.js b/library/composables/useSelectedPin.js index ca94a2ad6..64488cc42 100644 --- a/library/composables/useSelectedPin.js +++ b/library/composables/useSelectedPin.js @@ -3,7 +3,7 @@ // floats an animated "GO" location pin above it with a pulsing light glow. import { watch, inject } from 'vue'; -import { emissivePaint, isMapboxMap } from './useMapProvider.js'; +import { emissivePaint, isMapboxMap, engineFor } from './useMapProvider.js'; // ───────────────────────────────────────────── // Constants @@ -151,7 +151,8 @@ export function useSelectedPin(options = {}) { // anchor:'bottom' pins the marker's bottom edge to the lat/lng. // After the wrapper resize to match the SVG (43px), the bottom edge // IS the pin's tip — no offset compensation needed. - _marker = new mapboxgl.Marker({ + // engineFor, not the bare `mapboxgl` global: under MapLibre there may be no Mapbox global + _marker = new (engineFor(map).Marker)({ element : _markerEl, anchor : 'bottom', offset : [0, 0] diff --git a/plugin-designs/key-cities/cities-map.vue b/plugin-designs/key-cities/cities-map.vue index 69e1a6740..a6e2b88cc 100644 --- a/plugin-designs/key-cities/cities-map.vue +++ b/plugin-designs/key-cities/cities-map.vue @@ -36,6 +36,7 @@ import { labelFont } from '@map/maps/label-font.js' import { computed, inject, ref, shallowRef, markRaw, onMounted, onBeforeUnmount } from 'vue' import { useShadowStyles } from '@map/composables/useShadowStyles.js' import { addMapAttribution } from '@map/composables/useMapAttribution' +import { ensureEngineScript, engineGlobal } from '@map/composables/useMapInstance.js' import { resolveAssetsBase } from '@map/utils/embedBase.js' const profileConfig = inject('profileConfig', ref(null)) @@ -131,7 +132,10 @@ function paint(map) { } onMounted(async () => { - const gl = window.mapboxgl + // Load the engine rather than assume the page did: leaf s no longer preload mapbox-gl.js + // (it leaked every visitor's IP to Mapbox under provider=maplibre). This map is Mapbox-only. + try { await ensureEngineScript('mapbox') } catch (_) {} + const gl = engineGlobal('mapbox') if (!gl || !mapEl.value) { err.value = 'mapbox-gl not loaded'; return } try { await load() diff --git a/plugin-designs/pplr-data-maps/src/components/MapboxGeocoder.vue b/plugin-designs/pplr-data-maps/src/components/MapboxGeocoder.vue index 9d1d73ed0..96d3148b2 100644 --- a/plugin-designs/pplr-data-maps/src/components/MapboxGeocoder.vue +++ b/plugin-designs/pplr-data-maps/src/components/MapboxGeocoder.vue @@ -21,6 +21,7 @@ import { ref, watch, onMounted, onBeforeUnmount, computed, inject } from 'vue' import { ensureGeocoder, GEOCODER } from '../utils/pplrData.js' import { resolveMapProvider } from '@map/composables/useMapProvider.js' +import { ensureEngineScript, engineGlobal } from '@map/composables/useMapInstance.js' import { useInstance } from '../composables/usePplrInstance.js' const props = defineProps({ @@ -155,7 +156,7 @@ function setup() { render: renderSuggestion, }) } else { - if (!window.MapboxGeocoder || !window.mapboxgl) { + if (!window.MapboxGeocoder || !engineGlobal('mapbox')) { console.warn('[MapboxGeocoder] mapbox-gl-geocoder or mapbox-gl not on window — check CDN