My UPG Neighbors: the flat projection is applied on MapLibre too

Four lenses (send-map, lang-cluster, the cockpit's MapCell and FlowerClusters)
called setProjectionOn(m, 'mercator') straight after initializeMap(). MapLibre
throws "Style is not done loading" from setProjection before the style loads;
setProjectionOn swallows it and returns false, so on MapLibre the projection was
never set (rega, C2 at 0f136d25). Mapbox accepts it early, which is why only
one engine showed it.

A small plugin helper, src/whenStyleReady.js, runs the call at once if the style
has loaded, else on 'style.load'. Headless, against a MapLibre-shaped map that
throws before load, with the real setProjectionOn: before, it returns false and
the projection stays null; after, it is mercator, both when the style loads
later and when it is already loaded.
This commit is contained in:
Doxa Maps 2026-09-26 12:14:41 +00:00
parent afb34488cc
commit 64568ba685
5 changed files with 25 additions and 4 deletions

View file

@ -33,6 +33,7 @@ import langFamilyByLanguage from '@map/data/langFamilyByLanguage.json'
import bakedReached from '../data/reached-neighbors.json'
import { useNeighborT } from '../composables/useNeighborT.js'
import { setProjectionOn } from '@map/composables/useMapProvider.js'
import { whenStyleReady } from '../whenStyleReady.js'
const nt = useNeighborT()
const C_RED = '#e11d48', C_RED_DK = '#9f1239', C_NAVY = '#002e6e', C_GOLD = '#f59e0b'
@ -279,7 +280,8 @@ async function fetchDoxa() {
onMounted(async () => {
await initializeMap()
const m = map.value
setProjectionOn(m, 'mercator') // flat mercator, never globe (Driver); per-engine shape via C2 helper
// flat mercator, never globe (Driver); applied once the style is ready (MapLibre throws before)
whenStyleReady(m, () => setProjectionOn(m, 'mercator'))
try { doxaFeatures.value = await fetchDoxa() }
catch (e) { statusMsg.value = 'Live doxa API unreachable — ' + e.message; appReady.value = true; return }
appReady.value = true

View file

@ -23,6 +23,7 @@ import { useMapInstance } from '@map/composables/useMapInstance.js'
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
import { useCockpitStore, COLORS, redColor, reachedColor, logRadius, fmtPop } from './useCockpit.js'
import { setProjectionOn } from '@map/composables/useMapProvider.js'
import { whenStyleReady } from '../whenStyleReady.js'
const store = useCockpitStore()
@ -365,7 +366,8 @@ onMounted(async () => {
await initializeMap()
const m = map.value
if (!m) { return } // no/invalid token — overlay shows the hint, store still drives the rest
setProjectionOn(m, 'mercator') // flat mercator, never globe (Driver); per-engine shape via C2 helper
// flat mercator, never globe (Driver); applied once the style is ready (MapLibre throws before)
whenStyleReady(m, () => setProjectionOn(m, 'mercator'))
// Gated test hook (inert in production): lets a harness use the live map.project().
if (cfg.value.debugExpose && typeof window !== 'undefined') {

View file

@ -389,6 +389,7 @@ function onMove(ev) { if (tip.value.show) { tip.value.x = ev.clientX; tip.value.
import bakedReached from './data/reached-neighbors.json'
import bakedAdmin1 from './data/admin1-membership.json'
import { setProjectionOn } from '@map/composables/useMapProvider.js'
import { whenStyleReady } from './whenStyleReady.js'
async function loadJSON(relCandidates) {
for (const url of relCandidates) {
try { const r = await fetch(url); if (r.ok) return await r.json() } catch (_) {}
@ -411,7 +412,8 @@ async function fetchDoxa() {
onMounted(async () => {
await initializeMap()
const m = map.value
setProjectionOn(m, 'mercator') // flat mercator, never globe (Driver); per-engine shape via C2 helper
// flat mercator, never globe (Driver); applied once the style is ready (MapLibre throws before)
whenStyleReady(m, () => setProjectionOn(m, 'mercator'))
// baked data (own bundle dir). `base` lets the host say where data/ is served from
// (platform deploy serves data/ alongside the bundle, like send-map/flower-mesh).

View file

@ -402,6 +402,7 @@ function flyToSender(n) {
// profile-config `dataUrl` still overrides live.
import bakedReached from './data/reached-neighbors.json'
import { setProjectionOn } from '@map/composables/useMapProvider.js'
import { whenStyleReady } from './whenStyleReady.js'
async function loadDataset() {
const cfgUrl = profileConfig?.value?.dataUrl
if (cfgUrl) {
@ -413,7 +414,8 @@ async function loadDataset() {
onMounted(async () => {
await initializeMap()
const m = map.value
setProjectionOn(m, 'mercator') // flat mercator, never globe (Driver); per-engine shape via C2 helper
// flat mercator, never globe (Driver); applied once the style is ready (MapLibre throws before)
whenStyleReady(m, () => setProjectionOn(m, 'mercator'))
// Test-only hook (inert in production — platform never sets debugExpose). Lets an
// automated harness use the live map.project() to click exact circle positions.

View file

@ -0,0 +1,13 @@
/**
* whenStyleReady.js — run once the map's style has loaded (immediately if it already has).
*
* MapLibre throws "Style is not done loading" from setProjection before that, and setProjectionOn
* swallows the throw, so the lenses that set 'mercator' right after initializeMap() never got it on
* MapLibre (rega, C2: lang-cluster, send-map, the bloom cockpit's MapCell). Mapbox accepts it early,
* which is why this only showed on one engine.
*/
export function whenStyleReady(map, fn) {
if (!map) return
if (typeof map.isStyleLoaded === 'function' && map.isStyleLoaded()) fn()
else map.once('style.load', fn)
}