engineFor(map): build markers, popups and poster maps with the map's OWN engine
Every generated leaf <head> 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.
This commit is contained in:
parent
966c44fcc0
commit
056942b33f
7 changed files with 44 additions and 16 deletions
|
|
@ -33,6 +33,7 @@
|
||||||
|
|
||||||
import { ref, readonly } from 'vue';
|
import { ref, readonly } from 'vue';
|
||||||
import { computeLayout } from './usePosterLayout.js';
|
import { computeLayout } from './usePosterLayout.js';
|
||||||
|
import { engineFor } from '../useMapProvider.js';
|
||||||
|
|
||||||
const MAX_SINGLE_CANVAS = 4096; // WebGL safe ceiling for most hardware
|
const MAX_SINGLE_CANVAS = 4096; // WebGL safe ceiling for most hardware
|
||||||
const TILE_PX = 2048; // tile size for stitch path
|
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
|
// eslint-disable-next-line no-undef
|
||||||
const W = (typeof window !== 'undefined' ? window : {});
|
const W = (typeof window !== 'undefined' ? window : {});
|
||||||
const isMaplibre = !!W.maplibregl && sourceMap instanceof W.maplibregl.Map;
|
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');
|
if (!gl) throw new Error('useMapPoster: ' + (isMaplibre ? 'maplibregl' : 'mapboxgl') + ' global not found');
|
||||||
|
|
||||||
const styleObj = sourceMap.getStyle();
|
const styleObj = sourceMap.getStyle();
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
// Import Vue functions from NPM package
|
// Import Vue functions from NPM package
|
||||||
import { ref, inject } from 'vue';
|
import { ref, inject } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { engineFor } from './useMapProvider.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* useMapEvents composable - manages Mapbox layer event handlers
|
* useMapEvents composable - manages Mapbox layer event handlers
|
||||||
|
|
@ -245,7 +246,7 @@ export function useMapEvents(options = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: show popup (for non-prayer legend types)
|
// Fallback: show popup (for non-prayer legend types)
|
||||||
new mapboxgl.Popup({
|
new (engineFor(map).Popup)({
|
||||||
maxWidth: '320px',
|
maxWidth: '320px',
|
||||||
className: 'people-group-popup'
|
className: 'people-group-popup'
|
||||||
})
|
})
|
||||||
|
|
@ -290,7 +291,7 @@ export function useMapEvents(options = {}) {
|
||||||
if (e.features && e.features.length > 0) {
|
if (e.features && e.features.length > 0) {
|
||||||
const properties = e.features[0].properties;
|
const properties = e.features[0].properties;
|
||||||
|
|
||||||
new mapboxgl.Popup()
|
new (engineFor(map).Popup)()
|
||||||
.setLngLat(e.lngLat)
|
.setLngLat(e.lngLat)
|
||||||
.setHTML(generateRegionPopupContent(properties))
|
.setHTML(generateRegionPopupContent(properties))
|
||||||
.addTo(map);
|
.addTo(map);
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,28 @@ export function isMapboxMap(map) {
|
||||||
return typeof window !== 'undefined' && !!window.mapboxgl && map instanceof window.mapboxgl.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 <head> 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
|
/** Mapbox-Standard-only lighting paint props, per engine. MapLibre rejects layers whose
|
||||||
* paint carries unknown properties (the whole layer is dropped → invisible pins), so
|
* paint carries unknown properties (the whole layer is dropped → invisible pins), so
|
||||||
* spread this instead of writing '<kind>-emissive-strength' literally.
|
* spread this instead of writing '<kind>-emissive-strength' literally.
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
// floats an animated "GO" location pin above it with a pulsing light glow.
|
// floats an animated "GO" location pin above it with a pulsing light glow.
|
||||||
|
|
||||||
import { watch, inject } from 'vue';
|
import { watch, inject } from 'vue';
|
||||||
import { emissivePaint, isMapboxMap } from './useMapProvider.js';
|
import { emissivePaint, isMapboxMap, engineFor } from './useMapProvider.js';
|
||||||
|
|
||||||
// ─────────────────────────────────────────────
|
// ─────────────────────────────────────────────
|
||||||
// Constants
|
// Constants
|
||||||
|
|
@ -151,7 +151,8 @@ export function useSelectedPin(options = {}) {
|
||||||
// anchor:'bottom' pins the marker's bottom edge to the lat/lng.
|
// anchor:'bottom' pins the marker's bottom edge to the lat/lng.
|
||||||
// After the wrapper resize to match the SVG (43px), the bottom edge
|
// After the wrapper resize to match the SVG (43px), the bottom edge
|
||||||
// IS the pin's tip — no offset compensation needed.
|
// 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,
|
element : _markerEl,
|
||||||
anchor : 'bottom',
|
anchor : 'bottom',
|
||||||
offset : [0, 0]
|
offset : [0, 0]
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ import { labelFont } from '@map/maps/label-font.js'
|
||||||
import { computed, inject, ref, shallowRef, markRaw, onMounted, onBeforeUnmount } from 'vue'
|
import { computed, inject, ref, shallowRef, markRaw, onMounted, onBeforeUnmount } from 'vue'
|
||||||
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
|
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
|
||||||
import { addMapAttribution } from '@map/composables/useMapAttribution'
|
import { addMapAttribution } from '@map/composables/useMapAttribution'
|
||||||
|
import { ensureEngineScript, engineGlobal } from '@map/composables/useMapInstance.js'
|
||||||
import { resolveAssetsBase } from '@map/utils/embedBase.js'
|
import { resolveAssetsBase } from '@map/utils/embedBase.js'
|
||||||
|
|
||||||
const profileConfig = inject('profileConfig', ref(null))
|
const profileConfig = inject('profileConfig', ref(null))
|
||||||
|
|
@ -131,7 +132,10 @@ function paint(map) {
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const gl = window.mapboxgl
|
// Load the engine rather than assume the page did: leaf <head>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 }
|
if (!gl || !mapEl.value) { err.value = 'mapbox-gl not loaded'; return }
|
||||||
try {
|
try {
|
||||||
await load()
|
await load()
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
import { ref, watch, onMounted, onBeforeUnmount, computed, inject } from 'vue'
|
import { ref, watch, onMounted, onBeforeUnmount, computed, inject } from 'vue'
|
||||||
import { ensureGeocoder, GEOCODER } from '../utils/pplrData.js'
|
import { ensureGeocoder, GEOCODER } from '../utils/pplrData.js'
|
||||||
import { resolveMapProvider } from '@map/composables/useMapProvider.js'
|
import { resolveMapProvider } from '@map/composables/useMapProvider.js'
|
||||||
|
import { ensureEngineScript, engineGlobal } from '@map/composables/useMapInstance.js'
|
||||||
import { useInstance } from '../composables/usePplrInstance.js'
|
import { useInstance } from '../composables/usePplrInstance.js'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|
@ -155,7 +156,7 @@ function setup() {
|
||||||
render: renderSuggestion,
|
render: renderSuggestion,
|
||||||
})
|
})
|
||||||
} else {
|
} 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 <script> tags')
|
console.warn('[MapboxGeocoder] mapbox-gl-geocoder or mapbox-gl not on window — check CDN <script> tags')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -164,7 +165,7 @@ function setup() {
|
||||||
}
|
}
|
||||||
geocoder = new window.MapboxGeocoder({
|
geocoder = new window.MapboxGeocoder({
|
||||||
accessToken: props.mapboxToken,
|
accessToken: props.mapboxToken,
|
||||||
mapboxgl: window.mapboxgl,
|
mapboxgl: engineGlobal('mapbox'),
|
||||||
placeholder: props.placeholder,
|
placeholder: props.placeholder,
|
||||||
localGeocoder: buildLocalGeocoder(),
|
localGeocoder: buildLocalGeocoder(),
|
||||||
localGeocoderOnly: false,
|
localGeocoderOnly: false,
|
||||||
|
|
@ -252,7 +253,10 @@ function onGlobalKey(e) {
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// setup() swallows its own errors; a failed script load leaves the map working without search.
|
// setup() swallows its own errors; a failed script load leaves the map working without search.
|
||||||
ensureGeocoder(provider).then(setup, (e) => console.warn('[MapboxGeocoder]', e.message))
|
// The ENGINE first, then its geocoder: the Mapbox path hands mapboxgl to the geocoder, and it
|
||||||
|
// used to find it only because every leaf <head> preloaded mapbox-gl.js. ensureEngineScript is
|
||||||
|
// a no-op when the map already loaded it.
|
||||||
|
ensureEngineScript(provider).then(() => ensureGeocoder(provider)).then(setup, (e) => console.warn('[MapboxGeocoder]', e.message))
|
||||||
document.addEventListener('keydown', onGlobalKey)
|
document.addEventListener('keydown', onGlobalKey)
|
||||||
})
|
})
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
|
|
||||||
|
|
@ -60,13 +60,8 @@ export function ensureGeocoder(provider = 'mapbox') {
|
||||||
|
|
||||||
// The engine namespace that built THIS map (Popup, Marker, NavigationControl…). The views were
|
// 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
|
// written against the Mapbox global; under provider=maplibre that global does not exist and a
|
||||||
// `new mapboxgl.Popup` throws on the first pin click.
|
// `new mapboxgl.Popup` throws on the first pin click. Now the library's engineFor (one copy).
|
||||||
export function glFor(map) {
|
export { engineFor as glFor } from '@map/composables/useMapProvider.js'
|
||||||
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
|
// 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.
|
// actually did, so a check can assert "loaded" rather than inferring it from missing requests.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue