/** * useMapProvider.js — resolve the active map engine ("provider"). * * Resolution order (first defined wins) — matches resolveMapProvider below EXACTLY: * 1. profile-config `provider` — per-map, set in the profile-config JSON * 2. ?provider= in the page URL — read here directly, so any generated leaf page can be * flipped from the URL without the host doing anything * 3. window.MAP_PROVIDER — host global, for a host that sets it programmatically * 4. 'mapbox' — default; existing behaviour, never regresses * * Valid values: 'mapbox' | 'maplibre'. Anything else falls back to the default. * * ★ This header previously listed only THREE rungs, omitting ?provider= and numbering * window.MAP_PROVIDER as step 2 — it described a ladder the function does not implement. * documentation/how-to.md ("Swap the map engine") carries the correct four-rung order; keep the two * in sync, and treat THIS function as the authority when they disagree. */ import { inject } from 'vue' export const MAP_PROVIDERS = ['mapbox', 'maplibre'] export const DEFAULT_PROVIDER = 'mapbox' /** Unwrap a ref/computed to its config object; pass plain objects through. */ function unwrapConfig(profileConfig) { if (profileConfig && typeof profileConfig === 'object' && 'value' in profileConfig && !('provider' in profileConfig) && !('profile' in profileConfig)) { return profileConfig.value } return profileConfig } /** * Resolve the provider from a profile-config (object or ref) + window fallback. * Pure — safe to call outside a Vue setup(). */ export function resolveMapProvider(profileConfig) { const cfg = unwrapConfig(profileConfig) const fromConfig = cfg?.provider const fromUrl = typeof location !== 'undefined' ? new URLSearchParams(location.search).get('provider') : undefined const fromWindow = typeof window !== 'undefined' ? window.MAP_PROVIDER : undefined const raw = String(fromConfig || fromUrl || fromWindow || DEFAULT_PROVIDER).toLowerCase().trim() return MAP_PROVIDERS.includes(raw) ? raw : DEFAULT_PROVIDER } /** Human-readable engine label for a badge / UI. */ export function providerLabel(provider) { return provider === 'maplibre' ? 'MapLibre' : 'Mapbox' } /** Convenience for use inside a Vue setup() — injects the provided profileConfig. */ export function useMapProvider() { const profileConfig = inject('profileConfig', null) return resolveMapProvider(profileConfig) } /** True when this map instance is a Mapbox GL map (vs MapLibre). Per-instance runtime * sniff — works even on dual-engine pages where both globals are loaded. */ 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 '