doxa-maps/library/composables/useMapProvider.js
Doxa Maps e19697673d
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Doxa Maps — the map bundler, 7 production plugins and the Experimental shelf
2026-09-25 20:07:59 +00:00

104 lines
5.2 KiB
JavaScript

/**
* 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 <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
* paint carries unknown properties (the whole layer is dropped → invisible pins), so
* spread this instead of writing '<kind>-emissive-strength' literally.
*
* ★ `kind` EXISTS BECAUSE THE CIRCLE CASE WAS NOT THE ONLY ONE. This helper was hardcoded to
* 'circle', while fill, line and clustering layers still wrote the literal — so those layers kept
* being dropped by MapLibre for exactly the reason written above. MEASURED on countries-map by
* listening to the map's own `error` event, which is the ONLY place it surfaces:
* layers.regions-fill.paint.fill-emissive-strength: unknown property
* layers.regions-border.paint.line-emissive-strength: unknown property
* addLayer() does NOT throw for this — it accepts the call, emits an error event, and silently
* omits the layer. getLayer() then returns undefined and the map simply renders without it, which
* is why it read as "the fills do not paint" rather than as an error.
* @param {object} map the live map instance
* @param {'circle'|'fill'|'line'} [kind='circle']
*/
export function emissivePaint(map, kind = 'circle') {
return isMapboxMap(map) ? { [`${kind}-emissive-strength`]: 1 } : {}
}