Gospel x Internet uses the library's whiteboard map instead of its own copy

boardEngine.js was createWhiteboardMap under another name: the bodies are
identical line for line, and every import it made already came from library.
It is deleted and the four components import @map/maps/whiteboard-map.js.

The copies were NOT interchangeable, which is the part worth recording. Their
DEFAULTS are opposites, because the library file was extracted for the
whiteboard case and boardEngine served both map kinds:

  boardEngine  whiteboard:false boundaries:true  centre [60,20] zoom 1.4
  library      whiteboard:true  boundaries:false centre [0,0]   zoom 1

Three of the four callers pass all four options explicitly, so they were never
at risk. GiMap — the geographic map — passed only a token and took every
default. Swapping its import alone would have set whiteboard:true, hiding every
basemap layer, dropping boundary attribution and moving the camera: a blank map
in production, from a change that reads like pure de-duplication.

So GiMap now states its four options. Every effective option is unchanged from
today's values, verified field by field, and the call reads the same against
either module.
This commit is contained in:
Doxa Maps 2026-09-29 11:07:05 +00:00
parent 5095d798b8
commit c00b0b4b5d
5 changed files with 21 additions and 79 deletions

View file

@ -1,67 +0,0 @@
/**
* boardEngine.js — build one map on whichever engine the embed asked for, the way this board builds all
* of its maps: engine from resolveMapProvider, script + CSS ensured, RTL shaping installed before `new
* Map`, the style from the registry ('paper' = Mapbox light / OpenFreeMap positron), labels in the
* reader's language, attribution added. Used by the geographic map AND the language-tree whiteboard, so
* the two can never drift apart on an engine detail.
*
* whiteboard:true hides every basemap layer except the background once the style loads. The style is
* still loaded because it is what declares the GLYPH server: a hand-typed glyph URL is how labels
* silently vanished on one engine before (library/maps/label-font.js).
*/
import { resolveMapProvider } from '@map/composables/useMapProvider.js'
import { ensureEngineScript, engineGlobal, ensureEngineCSS, engineStylesheetUrl, engineUiLocale } from '@map/composables/useMapInstance.js'
import { resolveNamedStyle } from '@map/styles/registry.js'
import { installRtlTextPlugin } from '@map/maps/rtl-text.js'
import { localizeBasemapLabels } from '@map/maps/localize-labels.js'
import { addMapAttribution } from '@map/composables/useMapAttribution'
export function readerLang(profileConfig) {
const pc = profileConfig || {}
if (pc.locale || pc.lang) return String(pc.locale || pc.lang)
try { return new URLSearchParams(location.search).get('lang') || document.documentElement.lang || 'en' } catch { return 'en' }
}
/**
* @returns {Promise<{ map, provider } | { error: string, provider }>} resolves once the style has loaded
*/
export async function createBoardMap(el, profileConfig, { center = [60, 20], zoom = 1.4, whiteboard = false, boundaries = true, background = '#f3f3f1', token = '' } = {}) {
const provider = resolveMapProvider(profileConfig)
try { await ensureEngineScript(provider) } catch (_) { /* reported below */ }
const gl = engineGlobal(provider)
if (!gl) return { provider, error: provider === 'maplibre' ? 'MapLibre' : 'Mapbox' }
installRtlTextPlugin(gl)
const css = engineStylesheetUrl(provider)
if (css) await ensureEngineCSS(el, css)
const lang = readerLang(profileConfig)
// The host's token by the same ladder the production maps use: profile-config tk, then the token
// ProfileLoader provides (a tokenUrl token-service answer, or the page's shared token), then the page global.
const tk = profileConfig?.tk || token || (typeof window !== 'undefined' && window.MAP_TOKEN) || ''
const map = new gl.Map({
container: el,
style: resolveNamedStyle('paper', provider).style || 'mapbox://styles/mapbox/light-v11',
accessToken: tk,
locale: engineUiLocale(lang, provider),
center, zoom, projection: 'mercator', attributionControl: false,
// a whiteboard is not a world: no wrap-around copies of the tapestry at low zoom
renderWorldCopies: !whiteboard,
})
addMapAttribution(map, { boundaries, basemap: !whiteboard })
await new Promise((res) => map.once('load', res))
if (whiteboard) {
for (const l of map.getStyle().layers || []) {
try {
if (l.type === 'background') map.setPaintProperty(l.id, 'background-color', background)
else map.setLayoutProperty(l.id, 'visibility', 'none')
} catch (_) { /* a layer the engine will not touch is left as it is */ }
}
} else {
try { localizeBasemapLabels(map, lang) } catch (_) { /* a nicety, never a broken map */ }
}
return { map, provider }
}
/** Reduced motion, read LIVE (never cached): under it every camera change is a jump. */
export function reducedMotion() {
try { return window.matchMedia('(prefers-reduced-motion: reduce)').matches } catch (_) { return false }
}

View file

@ -22,7 +22,7 @@ import { useShadowStyles } from '@map/composables/useShadowStyles.js'
import { labelFont } from '@map/maps/label-font.js'
import { whiteboardProjection } from '@map/maps/whiteboard-projection.js'
import { addTieredPointLayers } from '@map/charts/whiteboard/tiered-points.js'
import { createBoardMap, reducedMotion } from '../boardEngine.js'
import { createWhiteboardMap, reducedMotion } from '@map/maps/whiteboard-map.js'
import { useBoardState } from '../useBoard.js'
import { useGiT } from '../useGiT.js'
import { layoutBoundedSet } from '@map/components/charts/boundedSet.js'
@ -83,11 +83,11 @@ function openAndFrame(slug, feature) {
onMounted(async () => {
try {
const made = await createBoardMap(el.value, board.profileConfig?.value || {},
const made = await createWhiteboardMap(el.value, board.profileConfig?.value || {},
{ whiteboard: true, boundaries: false, center: [0, 0], zoom: 1 })
const map = markRaw(made.map || made)
mapRef.value = map
// ★ createBoardMap ALREADY AWAITS LOAD. boardEngine resolves only after
// ★ createWhiteboardMap ALREADY AWAITS LOAD. The library helper resolves only after
// `await new Promise(res => map.once('load', res))`, so by the time this runs the event has
// fired and will not fire again — a second `map.on('load', …)` never runs and the layers are
// never added. The chart then draws a basemap-less whiteboard with no source on it: empty,
@ -141,7 +141,7 @@ onMounted(async () => {
} catch (_) { /* no ResizeObserver: the initial fit stands */ }
}
// ★ CALLED UNCONDITIONALLY, AND THE GUARD THAT USED TO BE HERE IS WHY THIS WAS BROKEN TWICE.
// createBoardMap only resolves after `await map.once('load')`, so by here the event HAS fired.
// createWhiteboardMap only resolves after `await map.once('load')`, so by here the event HAS fired.
// The previous line read `map.loaded() ? setup() : map.once('load', setup)` — but loaded()
// does not mean "the load event fired", it means "the style AND every tile are loaded", and
// the basemap tiles were still arriving. So it returned false, took the else branch, and

View file

@ -23,7 +23,7 @@ import { useShadowStyles } from '@map/composables/useShadowStyles.js'
import { labelFont } from '@map/maps/label-font.js'
import { whiteboardProjection } from '@map/maps/whiteboard-projection.js'
import { addTieredPointLayers } from '@map/charts/whiteboard/tiered-points.js'
import { createBoardMap, reducedMotion } from '../boardEngine.js'
import { createWhiteboardMap, reducedMotion } from '@map/maps/whiteboard-map.js'
import { useBoardState } from '../useBoard.js'
import { useGiT } from '../useGiT.js'
import { ringInfo, CENTRE_COLOR } from '../ringInfo.js'
@ -147,11 +147,11 @@ function openAndFrame(slug, feature) {
onMounted(async () => {
try {
const made = await createBoardMap(el.value, board.profileConfig?.value || {},
const made = await createWhiteboardMap(el.value, board.profileConfig?.value || {},
{ whiteboard: true, boundaries: false, center: [0, 0], zoom: 1 })
const map = markRaw(made.map || made)
mapRef.value = map
// ★ createBoardMap ALREADY AWAITS LOAD. boardEngine resolves only after
// ★ createWhiteboardMap ALREADY AWAITS LOAD. The library helper resolves only after
// `await new Promise(res => map.once('load', res))`, so by the time this runs the event has
// fired and will not fire again — a second `map.on('load', …)` never runs and the layers are
// never added. The chart then draws a basemap-less whiteboard with no source on it: empty,
@ -209,7 +209,7 @@ onMounted(async () => {
ready.value = true
}
// ★ CALLED UNCONDITIONALLY, AND THE GUARD THAT USED TO BE HERE IS WHY THIS WAS BROKEN TWICE.
// createBoardMap only resolves after `await map.once('load')`, so by here the event HAS fired.
// createWhiteboardMap only resolves after `await map.once('load')`, so by here the event HAS fired.
// The previous line read `map.loaded() ? setup() : map.once('load', setup)` — but loaded()
// does not mean "the load event fired", it means "the style AND every tile are loaded", and
// the basemap tiles were still arriving. So it returned false, took the else branch, and

View file

@ -37,7 +37,7 @@ import ShareButton from '@map/components/map-controls/ShareButton.vue'
import HelpButton from '@map/components/map-controls/HelpButton.vue'
import { useBoardState, EMBED_PATH } from '../useBoard.js'
import { useGiT } from '../useGiT.js'
import { createBoardMap, reducedMotion } from '../boardEngine.js'
import { createWhiteboardMap, reducedMotion } from '@map/maps/whiteboard-map.js'
import { ACCESS_DETAILS, BANDS, DETAIL_FILL, DETAIL_RIM, BAND_COLOR, label } from '../model.js'
const T = useGiT()
@ -285,7 +285,16 @@ onMounted(async () => {
}).catch(() => { adm1Table = null })
try {
if (!mapEl.value) return
const made = await createBoardMap(mapEl.value, board.profileConfig.value, { token: injectedToken?.value || '' })
// ★ THE FOUR OPTIONS ARE STATED, NEVER DEFAULTED. This is the ONE geographic caller — the three
// whiteboard callers all pass whiteboard:true — and the library's defaults are the inverse of what
// a real map needs (whiteboard:true, boundaries:false, centre [0,0], zoom 1), because it was
// extracted for the whiteboard case. Leaning on defaults here meant a blank basemap and no
// boundary attribution the moment the import moved. Stating them makes this call read the same
// against either module.
const made = await createWhiteboardMap(mapEl.value, board.profileConfig.value, {
whiteboard: false, boundaries: true, center: [60, 20], zoom: 1.4,
token: injectedToken?.value || '',
})
if (made.error) { err.value = T('messages.engineMissing', `The ${made.error} map engine did not load.`, { engine: made.error }); return }
const map = made.map
if (unmounted) { try { map.remove() } catch (_) {} return }

View file

@ -35,7 +35,7 @@ import ZoomOutButton from '@map/components/map-controls/ZoomOutButton.vue'
import HelpButton from '@map/components/map-controls/HelpButton.vue'
import { useBoardState } from '../useBoard.js'
import { useGiT } from '../useGiT.js'
import { createBoardMap, reducedMotion } from '../boardEngine.js'
import { createWhiteboardMap, reducedMotion } from '@map/maps/whiteboard-map.js'
import { treeScope, TREE_COLOR } from '../model.js'
import { buildTreePoster, posterPrintHtml } from '../treePoster.js'
import { openPrintFrame } from '@map/composables/printInPlace.js'
@ -225,7 +225,7 @@ function frame(coords) {
async function build() {
const t = await board.loadTree()
if (unmounted || !el.value || mapRef.value) return
const made = await createBoardMap(el.value, board.profileConfig.value, { whiteboard: true, boundaries: false, center: [0, 0], zoom: 1, token: injectedToken?.value || '' })
const made = await createWhiteboardMap(el.value, board.profileConfig.value, { whiteboard: true, boundaries: false, center: [0, 0], zoom: 1, token: injectedToken?.value || '' })
if (made.error) { err.value = T('messages.engineMissing', `The ${made.error} map engine did not load.`, { engine: made.error }); return }
if (unmounted) { try { made.map.remove() } catch (_) {} return }
const map = made.map