Merge remote-tracking branch 'origin/main' into rega/research-gallery-embed

This commit is contained in:
doxa-maps-88655c70 2026-09-25 12:15:20 +00:00
commit 178d7ac3e8
21 changed files with 397 additions and 68 deletions

View file

@ -0,0 +1,185 @@
#!/usr/bin/env node
/**
* check-experimental-shelf — "all experimental plugins working", as a measurement.
*
* THE SHELF is derived, never listed: every bundle in the built manifest that the Research Gallery
* would discover into its Experimental folder, i.e. not a curated tab and not in NOT_EXPERIMENTAL.
* Both sets are read from plugin-designs/doxa-maps-spa/doxa-maps-spa.vue, so a bundle that joins or
* leaves the shelf changes what this gate measures without an edit here.
*
* PER BUNDLE, PER ENGINE (mapbox, maplibre), on TWO surfaces: its own leaf page (A) and its tab
* inside the Research Gallery (D). A surface passes when:
* - the element mounted with content (its shadow tree has real nodes),
* - 0 page errors and 0 failed requests (4xx/5xx; Mapbox telemetry and favicons excluded),
* - no loading text is left on screen (Loading / Adding / Computing),
* - and, for every MAP on the page: its 'load' fired, and it DRAWS its own data, meaning at
* least one rendered feature on a layer fed by a GeoJSON source. That is the lesson from the
* PPLR check: 'load' alone is not loaded, and an empty GeoJSON source still counts as a
* source.
* Maps are found without touching any bundle: an init script wraps the engine's Map class as the
* engine script defines it, so every map the page constructs is recorded.
*
* IDENTITY: the gate serves THIS checkout's public/ itself, on a port the OS hands out, so it
* cannot grade another worktree's tree (the fixed-port trap internal/_serves-this-tree.mjs names).
*
* SELF-TEST (--self-test, and always first unless --no-self-test): the first shelf bundle's script
* is replaced in flight by one that throws. That bundle MUST fail on both surfaces; if it does not,
* the gate is blind and exits 3 before grading anything.
*
* Usage: node internal/check-experimental-shelf.mjs [--engines mapbox,maplibre] [--only a,b]
* [--no-self-test] [--self-test]
* Exit: 0 all pass · 1 a bundle failed · 3 self-test could not see a broken bundle · 2 setup.
* Run browser gates through internal/browser-slot.sh.
*/
import { readFileSync, existsSync } from 'node:fs'
import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { spawn } from 'node:child_process'
import { createServer } from 'node:net'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const argv = process.argv.slice(2)
const arg = (n, d) => { const i = argv.indexOf(n); return i >= 0 && argv[i + 1] ? argv[i + 1] : d }
const ENGINES = arg('--engines', 'mapbox,maplibre').split(',')
const ONLY = arg('--only', '') ? arg('--only', '').split(',') : null
const SELF_TEST = !argv.includes('--no-self-test')
const WAIT_MS = Number(process.env.SHELF_WAIT_MS || 75000)
const { chromium } = await import(resolve(ROOT, 'node_modules/playwright/index.mjs'))
const manifest = JSON.parse(readFileSync(resolve(ROOT, 'public/doxa-maps/manifest.json'), 'utf8'))
const spa = readFileSync(resolve(ROOT, 'plugin-designs/doxa-maps-spa/doxa-maps-spa.vue'), 'utf8')
const cfg = JSON.parse(readFileSync(resolve(ROOT, 'vite.doxa-maps-build-config.json'), 'utf8'))
const TK = process.env.MAPBOX_TOKEN || cfg.token || ''
// ── the shelf, derived from the Gallery's own rules ───────────────────────────
const notExp = new Set((spa.match(/const NOT_EXPERIMENTAL = new Set\(\[([\s\S]*?)\]\)/) || [, ''])[1].match(/'([a-z0-9-]+)'/g)?.map(s => s.slice(1, -1)) || [])
const curated = new Set([...spa.matchAll(/bundle:\s*'([a-z0-9-]+)'/g)].map(m => m[1]))
if (!notExp.size || !curated.size) { console.error('check-experimental-shelf: could not read NOT_EXPERIMENTAL / curated tabs from doxa-maps-spa.vue'); process.exit(2) }
const shelf = manifest.bundles
.filter(b => !notExp.has(b.bundle) && !curated.has(b.bundle) && (b.profiles || []).length)
.filter(b => !ONLY || ONLY.includes(b.bundle))
.map(b => {
// the entry profile the Gallery opens: a map profile, else the one named like the bundle, else the first
const p = b.profiles.find(x => x.isMap) || b.profiles.find(x => x.profile === b.bundle) || b.profiles[0]
return { bundle: b.bundle, profile: p.profile, isMap: !!p.isMap, tag: p.element || (b.elements || [])[0] || b.bundle }
})
if (!shelf.length) { console.error('check-experimental-shelf: the shelf is empty'); process.exit(2) }
// ── SEEDS: bundles whose data is the USER'S, so a fresh browser correctly shows none ─────────
// Seeded before load, then required to be DRAWN, which is stronger than exempting them. Printed on
// every run with the reason, so a seed can never quietly hide a regression.
const SEED = {
'prayer-walk-planner': {
why: 'walk zones are user-drawn and kept in localStorage; a fresh browser has none, so one known zone is seeded and must be drawn',
keyPrefix: 'prayer-walk-planner:v1:',
value: JSON.stringify([{ id: 'shelf-seed', title: 'Shelf check zone', coords: [[0, 10], [20, 10], [20, 25], [0, 25]] }]),
},
}
// ── our own server, on a port nobody else can be holding ──────────────────────
const port = await new Promise(ok => { const s = createServer().listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => ok(p)) }) })
const srv = spawn(process.execPath, [resolve(ROOT, 'internal/serve-strict.mjs'), '--port', String(port), '--directory', 'public'], { cwd: ROOT, stdio: 'ignore' })
const reap = () => { try { srv.kill() } catch (_) {} }
process.on('exit', reap); for (const s of ['SIGINT', 'SIGTERM']) process.on(s, () => { reap(); process.exit(130) })
const BASE = `http://127.0.0.1:${port}/doxa-maps`
for (let i = 0; i < 40; i++) { try { if ((await fetch(BASE + '/manifest.json')).ok) break } catch (_) {} await new Promise(r => setTimeout(r, 250)) }
// ── every Map the page builds, recorded without touching the bundle ───────────
const RECORD_MAPS = () => {
window.__shelfMaps = []
for (const name of ['mapboxgl', 'maplibregl']) {
let v
Object.defineProperty(window, name, {
configurable: true,
get() { return v },
set(val) {
if (val && val.Map && !val.Map.__shelfWrapped) {
const Orig = val.Map
class Recorded extends Orig { constructor(...a) { super(...a); window.__shelfMaps.push(this); this.once('load', () => { this.__shelfLoaded = true }) } }
Recorded.__shelfWrapped = true
try { val.Map = Recorded } catch (_) { try { Object.defineProperty(val, 'Map', { value: Recorded }) } catch (_) {} }
}
v = val
},
})
}
}
const PROBE = (tag) => `(() => {
const acc = []; const w = (r) => { for (const el of r.querySelectorAll('*')) { acc.push(el); if (el.shadowRoot) w(el.shadowRoot) } }; w(document)
// CONTENT is the plugin's own subtree, not the page: a small card on a bare leaf and a card inside the
// Gallery's chrome must be judged the same way.
const host = acc.find(e => e.tagName === ${JSON.stringify('__TAG__')}.toUpperCase())
let own = 0, sized = false
if (host) { const inner = []; const w2 = (r) => { for (const el of r.querySelectorAll('*')) { inner.push(el); if (el.shadowRoot) w2(el.shadowRoot) } }; w2(host.shadowRoot || host); own = inner.length
const hr = host.getBoundingClientRect(); sized = hr.width > 100 && hr.height > 100 }
const busy = acc.filter(e => e.children.length === 0 && /Loading|Adding|Computing/i.test(e.textContent || '') && e.getBoundingClientRect().height > 0).length
const maps = (window.__shelfMaps || []).filter(m => { try { return m.getContainer() && m.getContainer().isConnected } catch (_) { return false } }).map(m => {
let drawn = 0, geo = 0
try {
const st = m.getStyle(); const srcs = st.sources || {}
const geoIds = Object.keys(srcs).filter(k => srcs[k].type === 'geojson'); geo = geoIds.length
const layers = (st.layers || []).filter(l => geoIds.includes(l.source)).map(l => l.id)
if (layers.length) drawn = m.queryRenderedFeatures({ layers }).length
} catch (_) {}
return { loaded: !!(m.__shelfLoaded || (m.isStyleLoaded && m.isStyleLoaded())), geo, drawn }
})
return { own, sized, host: !!host, busy, maps }
})()`.replace('__TAG__', tag)
const b = await chromium.launch({ headless: true, args: ['--use-gl=swiftshader', '--enable-unsafe-swiftshader', '--ignore-gpu-blocklist', '--enable-webgl', '--no-sandbox'] })
async function measure(url, s, poisonBundle) {
const p = await b.newPage({ viewport: { width: 1280, height: 800 } })
await p.addInitScript(RECORD_MAPS)
const seed = SEED[s.bundle]
if (seed) await p.addInitScript(({ keyPrefix, value }) => {
const get = Storage.prototype.getItem
Storage.prototype.getItem = function (k) { const v = get.call(this, k); return (v == null && String(k).startsWith(keyPrefix)) ? value : v }
}, seed)
if (poisonBundle) await p.route(new RegExp(`/${poisonBundle}/${poisonBundle}\\.js(\\?|$)`), r => r.fulfill({ status: 200, contentType: 'text/javascript', body: 'throw new Error("shelf self-test: deliberately broken bundle")' }))
const errs = [], fails = []
p.on('pageerror', e => errs.push(e.message.slice(0, 140)))
p.on('response', r => { if (r.status() >= 400 && !/events\.mapbox\.com|favicon/.test(r.url())) fails.push(r.status() + ' ' + r.url().replace(/\?.*/, '').slice(-80)) })
await p.goto(url)
let st = null
const t0 = Date.now()
while (Date.now() - t0 < WAIT_MS) {
await p.waitForTimeout(2500)
st = await p.evaluate(PROBE(s.tag))
const mapsReady = st.maps.every(m => m.loaded && m.drawn > 0)
if (st.own > 8 && st.sized && st.busy === 0 && mapsReady && Date.now() - t0 > 6000) break
}
await p.close()
const why = []
if (!st || !st.host) why.push(`<${s.tag}> never appeared`)
else if (st.own <= 8 || !st.sized) why.push(`<${s.tag}> mounted empty (${st.own} node(s)${st.sized ? '' : ', not sized'})`)
if (st && st.busy) why.push(`${st.busy} loading label(s) left`)
if (st) st.maps.forEach((m, i) => { if (!m.loaded) why.push(`map ${i + 1}: 'load' never fired`); else if (!m.drawn) why.push(`map ${i + 1}: no own data drawn (${m.geo} geojson source(s))`) })
if (errs.length) why.push(`page error: ${errs[0]}`)
if (fails.length) why.push(`failed request: ${fails[0]}`)
return { pass: !why.length, why, maps: st ? st.maps.length : 0 }
}
const leaf = (s, eng) => `${BASE}/${s.bundle}/${s.profile}/index.html?provider=${eng}` + (eng === 'mapbox' ? `&tk=${TK}` : '')
const gallery = (s, eng) => `${BASE}/doxa-maps-spa/doxa-maps-spa/index.html?tab=${s.bundle}&provider=${eng}` + (eng === 'mapbox' ? `&tk=${TK}` : '')
let exit = 0
try {
if (SELF_TEST) {
const s = shelf[0]
const a = await measure(leaf(s, 'maplibre'), s, s.bundle), d = await measure(gallery(s, 'maplibre'), s, s.bundle)
if (a.pass || d.pass) { console.error(`SELF-TEST FAILED: a deliberately broken ${s.bundle} still passed (${a.pass ? 'leaf' : ''} ${d.pass ? 'gallery' : ''}). This gate cannot see a broken bundle.`); exit = 3 }
else console.log(`self-test: a deliberately broken ${s.bundle} fails as it must (leaf: ${a.why[0]}; gallery: ${d.why[0]})`)
}
if (exit !== 3) {
for (const [k, v] of Object.entries(SEED)) if (shelf.some(s => s.bundle === k)) console.log(`SEEDED ${k}: ${v.why}`)
console.log(`EXPERIMENTAL SHELF — ${shelf.length} bundle(s): ${shelf.map(s => s.bundle).join(', ')} · engines: ${ENGINES.join(', ')}`)
for (const s of shelf) for (const eng of ENGINES) for (const [surf, url] of [['A leaf', leaf(s, eng)], ['D gallery', gallery(s, eng)]]) {
const r = await measure(url, s)
if (!r.pass) exit = 1
console.log(`${r.pass ? 'PASS' : 'FAIL'} ${s.bundle.padEnd(24)} ${eng.padEnd(9)} ${surf.padEnd(10)} ${r.maps ? r.maps + ' map(s)' : 'no map'}${r.pass ? '' : ' — ' + r.why.join('; ')}`)
}
console.log(exit ? 'EXPERIMENTAL-SHELF: FAIL' : `EXPERIMENTAL-SHELF: PASS — every experimental plugin loads on its own page and in the Gallery, under ${ENGINES.join(' and ')}`)
}
} finally { await b.close(); reap() }
process.exit(exit)

View file

@ -0,0 +1,114 @@
/**
* check-gospel-english-leaks.mjs — a translated key must not render in English.
*
* THE METHOD, from rega by way of dx-shepherd. A string is a LEAK when the catalogue has a
* DIFFERENT value for the locale and the page still shows the English one. That is a render
* or wiring fault, and it is a different thing from an untranslated string, which is a
* catalogue gap. Only the first is this check's business.
*
* WHAT IT DOES NOT COUNT, so the number means something:
* - third-party strings, e.g. the Mapbox and geoBoundaries attributions, which are licence
* obligations and must never be translated;
* - DATA, the people-group and place names that come from the publishers;
* - PLACEHOLDER-ONLY strings;
* - the explanatory PROSE this bundle deliberately keeps in English, which is declared by
* being absent from the catalogue entirely — if there is no translated value, there is
* nothing to leak.
*
* SCOPE: THE LEAF, AND THAT BOUNDARY IS THE POINT. Six keys were reported leaking in the
* gallery. Measured here, the standalone leaf is clean in ar, zh, ru and hi — and the
* gallery's iframe src is built without a lang parameter, so the embedded leaf never learns
* the locale and correctly falls back to English. That is an embedder defect, not a bundle
* one, and a check that conflated them would have sent someone to translate strings that are
* already translated.
*/
import { chromium } from 'playwright'
import { readFileSync } from 'node:fs'
import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { serveReaped } from './_serve-reaped.mjs'
const HERE = dirname(fileURLToPath(import.meta.url))
const REPO = resolve(HERE, '..')
const PORT = Number(process.env.PORT || 5399)
const BASE = process.env.BASE_URL || ('http://127.0.0.1:' + PORT)
const LOCALES = (process.env.LOCALES || 'ar,zh,ru,hi').split(',')
const LEAVES = ['resources-map', 'resources-list', 'data-tables', 'charts']
let srv = null
if (!process.env.BASE_URL) srv = serveReaped(resolve(REPO, 'public'), PORT)
const cat = (lc) => JSON.parse(readFileSync(resolve(REPO, 'library/i18n/locales/' + lc + '/gospel.json'), 'utf8'))
const flat = (o, p = []) => Object.entries(o).flatMap(([k, v]) =>
k.startsWith('_') ? [] : (v && typeof v === 'object' ? flat(v, [...p, k]) : [[[...p, k].join('.'), String(v)]]))
let browser
try {
browser = await chromium.launch()
const en = Object.fromEntries(flat(cat('en')))
const leaks = []
let compared = 0
for (const leaf of LEAVES) {
for (const lc of LOCALES) {
const loc = Object.fromEntries(flat(cat(lc)))
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } })
await page.goto(BASE + '/doxa-maps/gospel-resources/' + leaf + '/index.html?lang=' + lc,
{ waitUntil: 'networkidle', timeout: 90000 })
await page.waitForTimeout(8000)
/* ═══ READ ONLY THE CHROME, NOT THE DECLARED-ENGLISH PROSE ═══════════════════════
* The first version read the whole shadow root and reported 113 leaks, nearly all
* false: "JESUS Film" and "Scripture" appear inside the explanatory paragraphs this
* bundle deliberately keeps in English, and three-letter catalogue entries like "for"
* match inside ordinary English sentences. Both are noise, and a gate that cries 113
* when 0 are real is worse than none — someone would have gone and "fixed" strings
* that were already correct.
* Every English prose block in this bundle declares itself with dir="ltr". So the
* chrome is everything OUTSIDE those subtrees, which is exactly the population a
* translated key is supposed to reach. */
const text = await page.evaluate(() => {
const sr = document.querySelector('gospel-resources-map')?.shadowRoot
if (!sr) return ''
/* A ShadowRoot is not clonable, so walk it and skip declared-English subtrees. */
const parts = []
const walk = (node) => {
for (const child of node.childNodes) {
if (child.nodeType === 3) { parts.push(child.textContent); continue }
if (child.nodeType !== 1) continue
if (child.getAttribute && child.getAttribute('dir') === 'ltr') continue
if (child.tagName === 'STYLE' || child.tagName === 'SCRIPT') continue
walk(child)
}
}
walk(sr)
return parts.join(' ').replace(/\s+/g, ' ')
})
await page.close()
if (!text) { leaks.push(leaf + ' ' + lc + ': rendered nothing at all'); continue }
for (const [key, e] of Object.entries(en)) {
const t = loc[key]
/* Short entries match inside unrelated English words; six characters is the floor
at which a match is about the key rather than about the alphabet. */
if (!t || t === e || e.length < 6) continue
compared++
if (text.includes(e) && !text.includes(t)) leaks.push(leaf + ' ' + lc + ' ' + key + ': shows English "' + e.slice(0, 40) + '"')
}
}
}
console.log(' compared ' + compared + ' key/locale pair(s) across ' + LEAVES.length + ' leaves and ' + LOCALES.length + ' locales')
if (leaks.length) {
/* exitCode, NOT process.exit(). process.exit() terminates before pending stdout/stderr
writes flush, and this gate proved it on itself: its first real run returned 1 with
every explanatory line missing, so it failed without saying why. A gate that cannot
explain its own failure is barely better than no gate. */
console.error('GOSPEL-ENGLISH-LEAKS: FAIL — ' + leaks.length + ' translated key(s) rendering in English:')
for (const l of leaks.slice(0, 12)) console.error(' ' + l)
process.exitCode = 1
} else {
console.log('GOSPEL-ENGLISH-LEAKS: PASS — every key with a translation renders in it.')
}
} finally {
await browser?.close()
srv?.stop()
}

View file

@ -204,7 +204,10 @@
"check:gospel-all": "node internal/check-gospel-all.mjs",
"check:no-conflict-markers": "node internal/check-no-conflict-markers.mjs",
"check:no-conflict-markers:poison": "node internal/check-no-conflict-markers.mjs --poison",
"check:poster-wall-localised": "node internal/check-poster-wall-localised.mjs"
"check:english-leaks": "node internal/check-gospel-english-leaks.mjs",
"check:poster-wall-localised": "node internal/check-poster-wall-localised.mjs",
"check:poster-wall-localised": "node internal/check-poster-wall-localised.mjs",
"check:experimental-shelf": "node internal/check-experimental-shelf.mjs"
},
"//scripts": {
"_": "What each script does. These notes used to sit inside the command strings as trailing \"# …\" comments, which made the shell discard any arguments passed after \"--\" (so a gate given --url silently measured its hardcoded default port — possibly another agent's server). Keep notes here, never in a command.",
@ -309,7 +312,10 @@
"check:leaves-mount": "DID THE ELEMENT ACTUALLY MOUNT, on all five gospel leaves. Exists because a ReferenceError thrown during setup dies INSIDE the custom-element boundary: the page loads, every asset returns 200, pageerror fires zero times, and the element sits there with an empty shadow root. A healthy-looking dead page. This asks the cheap unambiguous question the rich gates are too slow to ask on every leaf — is anything there — in five steps that each name their own failure: tag defined, host present, shadowRoot attached, shadow root non-empty, visible text rendered. It does NOT trust the absence of console errors; zero errors is not the same as working. --poison for the negative test.",
"check:gospel-all": "EVERYTHING gospel-resources owns, in one command. Its ten gates were all hand-run, which is how a dead bundle shipped: a duplicate module killed all five leaves and the one gate run that day was pointed at the map. check:no-orphan-gates was right that they were reachable — reachable is not run. Deliberately NOT in gates:pre/post: three need a browser, the shared wave holds browser gates under load, and a bundle owner should not be able to slow everyone else's build by adding to a list they do not own. Static gates run first and stop the run on failure, so a scarce browser slot is never spent confirming a table is inconsistent. Exit 75 = NOT MEASURED, never folded into a pass.",
"check:no-conflict-markers": "Nothing tracked may carry an unresolved merge. Exists because a merge from this worktree committed a BUILT bundle with 21 conflict markers - both sides concatenated, 1.13MB - and every gospel leaf rendered dead while the SRI hash matched and every request returned 200. The cause was a landing script that resolved one expected file then ran \"git add -A public\", staging every other file including the ones still holding markers. For a built file: take either side, REBUILD, commit the rebuild, then run this before pushing. --poison for the negative test.",
"check:basemap-labels": "the BASEMAP's place names follow the reader's language on BOTH engines (library/maps/localize-labels.js): reads the RENDERED country labels at ?lang=en and ?lang=ru — Mapbox via the Standard basemap's place-labels featureset, MapLibre via each symbol's evaluated text — and requires them to flip. Serves this checkout on an OS-assigned port; --dir <tree> for a negative control. Negative-tested: a bundle built before 319064a1 FAILS on both engines."
"check:english-leaks": "A translated key must not render in English. rega's method: a string is a LEAK when the catalogue has a DIFFERENT value for the locale and the page still shows the English one - a render/wiring fault, not a missing translation. Third-party attributions, data names, placeholder-only strings and the prose this bundle deliberately keeps in English are all excluded, the last by simply not being in the catalogue. Scoped to the LEAF on purpose: six keys were reported leaking in the gallery and the leaf is clean in ar/zh/ru/hi - the gallery builds its iframe src with no lang parameter, so the embed never learns the locale. Embedder defect, not a bundle one.",
"check:basemap-labels": "the BASEMAP's place names follow the reader's language on BOTH engines (library/maps/localize-labels.js): reads the RENDERED country labels at ?lang=en and ?lang=ru — Mapbox via the Standard basemap's place-labels featureset, MapLibre via each symbol's evaluated text — and requires them to flip. Serves this checkout on an OS-assigned port; --dir <tree> for a negative control. Negative-tested: a bundle built before 319064a1 FAILS on both engines.",
"check:basemap-labels": "the BASEMAP's place names follow the reader's language on BOTH engines (library/maps/localize-labels.js): reads the RENDERED country labels at ?lang=en and ?lang=ru — Mapbox via the Standard basemap's place-labels featureset, MapLibre via each symbol's evaluated text — and requires them to flip. Serves this checkout on an OS-assigned port; --dir <tree> for a negative control. Negative-tested: a bundle built before 319064a1 FAILS on both engines.",
"check:experimental-shelf": "Every Experimental-folder bundle, leaf + Gallery tab, mapbox + maplibre: mounts with content, 0 errors / failed requests, maps load and DRAW their own data. Self-tests on a deliberately broken bundle first. Run through internal/browser-slot.sh."
},
"devDependencies": {
"@capacitor/android": "^7.6.9",

View file

@ -239,6 +239,9 @@ function setupMapLayers() {
map.value.addLayer({
id: 'pwp-polygons-label', type: 'symbol', source: POLY_SRC,
layout: {
// A font OpenFreeMap's glyph server has. With no text-font, MapLibre asks for its default
// (Open Sans / Arial Unicode MS), which OpenFreeMap 404s, and no zone title ever drew.
'text-font': ['Noto Sans Regular'],
'text-field': ['get', 'title'],
'text-size': 12,
'text-anchor': 'center'

View file

@ -23,6 +23,7 @@
* token-less via the gated host hook). Lives inside the doxa-upg-neighbors merged profile
* as the "Flowers" view; reuses the granular per-PG coords from UPGN-MERGE.
*/
import { labelFont } from '../labelFont.js'
import { inject, ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
import { useMapInstance } from '@map/composables/useMapInstance.js'
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
@ -222,7 +223,7 @@ function paint() {
})
if (!m.getLayer(L_CORE_LBL)) m.addLayer({
id: L_CORE_LBL, type: 'symbol', source: SRC_CORE, minzoom: 2.2,
layout: { 'text-field': ['get', 'label'], 'text-size': 11, 'text-offset': [0, 1.4], 'text-anchor': 'top',
layout: { ...labelFont(m), 'text-field': ['get', 'label'], 'text-size': 11, 'text-offset': [0, 1.4], 'text-anchor': 'top',
'text-allow-overlap': false, 'text-optional': true },
paint: { 'text-color': C_RED_DK, 'text-halo-color': '#fff', 'text-halo-width': 1.4 }
})

View file

@ -17,6 +17,7 @@
* (this child's own <style> would be dropped by defineCustomElement).
*/
import { labelFont } from '../labelFont.js'
import { inject, ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { useMapInstance } from '@map/composables/useMapInstance.js'
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
@ -197,7 +198,7 @@ function ensureLayers(m) {
})
if (!m.getLayer(L_GREEN_LBL)) m.addLayer({
id: L_GREEN_LBL, type: 'symbol', source: SRC_GREEN, minzoom: 3.4,
layout: { 'text-field': ['get', 'label'], 'text-size': 10, 'text-offset': [0, 1.1],
layout: { ...labelFont(m), 'text-field': ['get', 'label'], 'text-size': 10, 'text-offset': [0, 1.1],
'text-anchor': 'top', 'text-allow-overlap': false, 'text-optional': true },
paint: { 'text-color': '#0b5e33', 'text-halo-color': '#ffffff', 'text-halo-width': 1.3 }
})
@ -212,7 +213,7 @@ function ensureLayers(m) {
})
if (!m.getLayer(L_RED_LBL)) m.addLayer({
id: L_RED_LBL, type: 'symbol', source: SRC_RED, minzoom: 2.4,
layout: { 'text-field': ['get', 'label'], 'text-size': 11, 'text-offset': [0, 1.2],
layout: { ...labelFont(m), 'text-field': ['get', 'label'], 'text-size': 11, 'text-offset': [0, 1.2],
'text-anchor': 'top', 'text-allow-overlap': false, 'text-optional': true },
paint: { 'text-color': COLORS.RED_DK, 'text-halo-color': '#ffffff', 'text-halo-width': 1.4 }
})

View file

@ -0,0 +1,17 @@
/**
* labelFont.js — a text-font the map's own glyph server actually has.
*
* A symbol layer with no text-font asks for the engine default ("Open Sans Regular, Arial Unicode MS
* Regular"), and the bold labels here named Mapbox's "DIN Offc Pro Bold". Mapbox serves both.
* OpenFreeMap, the basemap under provider=maplibre, serves neither: measured, the glyph request is
* a 404 and the labels never draw. So under MapLibre the labels use Noto Sans, which it does serve;
* under Mapbox nothing changes.
*
* Spread into a layer's layout: layout: { 'text-field': …, ...labelFont(m) }
*/
export function labelFont(map, bold = false) {
const ml = typeof window !== 'undefined' && window.maplibregl
const isMaplibre = !!(ml && ml.Map && map instanceof ml.Map)
if (isMaplibre) return { 'text-font': [bold ? 'Noto Sans Bold' : 'Noto Sans Regular'] }
return bold ? { 'text-font': ['DIN Offc Pro Bold', 'Arial Unicode MS Bold'] } : {}
}

View file

@ -34,6 +34,7 @@
* "dataUrl":"<reached-neighbors.json>", "instanceId":"..." }
*/
import { labelFont } from './labelFont.js'
import { inject, ref, computed, onMounted, onBeforeUnmount } from 'vue'
import { useMapInstance } from '@map/composables/useMapInstance.js'
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
@ -318,7 +319,7 @@ function paintAll() {
})
if (!m.getLayer(L_CL_LABEL)) m.addLayer({
id: L_CL_LABEL, type: 'symbol', source: SRC_CL, minzoom: 2.6,
layout: { 'text-field': ['get', 'label'], 'text-size': 11, 'text-offset': [0, 1.3],
layout: { ...labelFont(m), 'text-field': ['get', 'label'], 'text-size': 11, 'text-offset': [0, 1.3],
'text-anchor': 'top', 'text-allow-overlap': false, 'text-optional': true },
paint: { 'text-color': C_RED_DK, 'text-halo-color': '#ffffff', 'text-halo-width': 1.4 }
})

View file

@ -43,6 +43,7 @@
* }
*/
import { labelFont } from './labelFont.js'
import { inject, ref, computed, onMounted, onBeforeUnmount } from 'vue'
import { useMapInstance } from '@map/composables/useMapInstance.js'
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
@ -308,7 +309,7 @@ function syncLayers() {
id: L_SENDER_LABEL, type: 'symbol', source: SRC_SENDERS,
minzoom: 3.4,
layout: {
'text-field': ['get', 'name'], 'text-size': 10,
...labelFont(m), 'text-field': ['get', 'name'], 'text-size': 10,
'text-offset': [0, 1.1], 'text-anchor': 'top', 'text-allow-overlap': false
},
paint: { 'text-color': '#0b5e33', 'text-halo-color': '#ffffff', 'text-halo-width': 1.3 }
@ -329,7 +330,7 @@ function syncLayers() {
m.addLayer({
id: L_TARGET_LABEL, type: 'symbol', source: SRC_TARGET,
layout: {
'text-field': ['get', 'name'], 'text-size': 13, 'text-font': ['DIN Offc Pro Bold', 'Arial Unicode MS Bold'],
'text-field': ['get', 'name'], 'text-size': 13, ...labelFont(m, true),
'text-offset': [0, -1.4], 'text-anchor': 'bottom', 'text-allow-overlap': true
},
paint: { 'text-color': '#8a3800', 'text-halo-color': '#ffffff', 'text-halo-width': 1.6 }

View file

@ -863,7 +863,7 @@
"bundle": "prayer-walk-planner",
"scriptUrl": "./prayer-walk-planner/prayer-walk-planner.js",
"output": "doxa-maps/prayer-walk-planner/prayer-walk-planner.js",
"integrity": "sha384-rI097ecm/idym9j/Od/aOtgqhk0kB/1CwMpal9jTLm/ZyNKdlo6eYRMwV+J0rJyV",
"integrity": "sha384-v0hrmAp17LGeHHfWbgdEkrTILIqrZRQh2QDFT1tgxN359vVW/Xt7t+GAQS16uA4U",
"elements": [
"prayer-walk-planner"
],
@ -879,7 +879,7 @@
"scriptUrl": "./prayer-walk-planner/prayer-walk-planner.js",
"embed": {
"iframe": "<iframe src=\"./prayer-walk-planner/prayer-walk-planner/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early; remove this line if you set \"provider\":\"maplibre\" -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./prayer-walk-planner/prayer-walk-planner.js\" integrity=\"sha384-rI097ecm/idym9j/Od/aOtgqhk0kB/1CwMpal9jTLm/ZyNKdlo6eYRMwV+J0rJyV\" crossorigin=\"anonymous\"></script>\n<prayer-walk-planner profile-config='{\"profile\":\"prayer-walk-planner\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></prayer-walk-planner>"
"script": "<!-- Mapbox engine, fetched early; remove this line if you set \"provider\":\"maplibre\" -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./prayer-walk-planner/prayer-walk-planner.js\" integrity=\"sha384-v0hrmAp17LGeHHfWbgdEkrTILIqrZRQh2QDFT1tgxN359vVW/Xt7t+GAQS16uA4U\" crossorigin=\"anonymous\"></script>\n<prayer-walk-planner profile-config='{\"profile\":\"prayer-walk-planner\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></prayer-walk-planner>"
}
}
]
@ -1059,7 +1059,7 @@
"bundle": "upg-neighbor",
"scriptUrl": "./upg-neighbor/upg-neighbor.js",
"output": "doxa-maps/upg-neighbor/upg-neighbor.js",
"integrity": "sha384-YViVG3snYU3j6AZ8ZxBIJlBBMUL5Pj2guGdWiiKvexR9VNtQFCHrX+PQ6lLa2DrN",
"integrity": "sha384-L9U5BmHdIQSgYXleGnKoycRFoPQq/zCBFGCK+76Xt++6f/Z0byB9sSlVef7ofjXi",
"elements": [
"upg-neighbor"
],
@ -1075,7 +1075,7 @@
"scriptUrl": "./upg-neighbor/upg-neighbor.js",
"embed": {
"iframe": "<iframe src=\"./upg-neighbor/upg-neighbor/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early; remove this line if you set \"provider\":\"maplibre\" -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./upg-neighbor/upg-neighbor.js\" integrity=\"sha384-YViVG3snYU3j6AZ8ZxBIJlBBMUL5Pj2guGdWiiKvexR9VNtQFCHrX+PQ6lLa2DrN\" crossorigin=\"anonymous\"></script>\n<upg-neighbor profile-config='{\"profile\":\"upg-neighbor\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></upg-neighbor>"
"script": "<!-- Mapbox engine, fetched early; remove this line if you set \"provider\":\"maplibre\" -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./upg-neighbor/upg-neighbor.js\" integrity=\"sha384-L9U5BmHdIQSgYXleGnKoycRFoPQq/zCBFGCK+76Xt++6f/Z0byB9sSlVef7ofjXi\" crossorigin=\"anonymous\"></script>\n<upg-neighbor profile-config='{\"profile\":\"upg-neighbor\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></upg-neighbor>"
},
"instances": [
{

File diff suppressed because one or more lines are too long

View file

@ -863,7 +863,7 @@
"bundle": "prayer-walk-planner",
"scriptUrl": "./prayer-walk-planner/prayer-walk-planner.js",
"output": "doxa-maps/prayer-walk-planner/prayer-walk-planner.js",
"integrity": "sha384-rI097ecm/idym9j/Od/aOtgqhk0kB/1CwMpal9jTLm/ZyNKdlo6eYRMwV+J0rJyV",
"integrity": "sha384-v0hrmAp17LGeHHfWbgdEkrTILIqrZRQh2QDFT1tgxN359vVW/Xt7t+GAQS16uA4U",
"elements": [
"prayer-walk-planner"
],
@ -879,7 +879,7 @@
"scriptUrl": "./prayer-walk-planner/prayer-walk-planner.js",
"embed": {
"iframe": "<iframe src=\"./prayer-walk-planner/prayer-walk-planner/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early; remove this line if you set \"provider\":\"maplibre\" -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./prayer-walk-planner/prayer-walk-planner.js\" integrity=\"sha384-rI097ecm/idym9j/Od/aOtgqhk0kB/1CwMpal9jTLm/ZyNKdlo6eYRMwV+J0rJyV\" crossorigin=\"anonymous\"></script>\n<prayer-walk-planner profile-config='{\"profile\":\"prayer-walk-planner\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></prayer-walk-planner>"
"script": "<!-- Mapbox engine, fetched early; remove this line if you set \"provider\":\"maplibre\" -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./prayer-walk-planner/prayer-walk-planner.js\" integrity=\"sha384-v0hrmAp17LGeHHfWbgdEkrTILIqrZRQh2QDFT1tgxN359vVW/Xt7t+GAQS16uA4U\" crossorigin=\"anonymous\"></script>\n<prayer-walk-planner profile-config='{\"profile\":\"prayer-walk-planner\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></prayer-walk-planner>"
}
}
]
@ -1059,7 +1059,7 @@
"bundle": "upg-neighbor",
"scriptUrl": "./upg-neighbor/upg-neighbor.js",
"output": "doxa-maps/upg-neighbor/upg-neighbor.js",
"integrity": "sha384-YViVG3snYU3j6AZ8ZxBIJlBBMUL5Pj2guGdWiiKvexR9VNtQFCHrX+PQ6lLa2DrN",
"integrity": "sha384-L9U5BmHdIQSgYXleGnKoycRFoPQq/zCBFGCK+76Xt++6f/Z0byB9sSlVef7ofjXi",
"elements": [
"upg-neighbor"
],
@ -1075,7 +1075,7 @@
"scriptUrl": "./upg-neighbor/upg-neighbor.js",
"embed": {
"iframe": "<iframe src=\"./upg-neighbor/upg-neighbor/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early; remove this line if you set \"provider\":\"maplibre\" -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./upg-neighbor/upg-neighbor.js\" integrity=\"sha384-YViVG3snYU3j6AZ8ZxBIJlBBMUL5Pj2guGdWiiKvexR9VNtQFCHrX+PQ6lLa2DrN\" crossorigin=\"anonymous\"></script>\n<upg-neighbor profile-config='{\"profile\":\"upg-neighbor\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></upg-neighbor>"
"script": "<!-- Mapbox engine, fetched early; remove this line if you set \"provider\":\"maplibre\" -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./upg-neighbor/upg-neighbor.js\" integrity=\"sha384-L9U5BmHdIQSgYXleGnKoycRFoPQq/zCBFGCK+76Xt++6f/Z0byB9sSlVef7ofjXi\" crossorigin=\"anonymous\"></script>\n<upg-neighbor profile-config='{\"profile\":\"upg-neighbor\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></upg-neighbor>"
},
"instances": [
{

File diff suppressed because one or more lines are too long

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <prayer-walk-planner> custom element. -->
<script src="../prayer-walk-planner.js" integrity="sha384-rI097ecm/idym9j/Od/aOtgqhk0kB/1CwMpal9jTLm/ZyNKdlo6eYRMwV+J0rJyV" crossorigin="anonymous"></script>
<script src="../prayer-walk-planner.js" integrity="sha384-v0hrmAp17LGeHHfWbgdEkrTILIqrZRQh2QDFT1tgxN359vVW/Xt7t+GAQS16uA4U" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <upg-neighbor> custom element. -->
<script src="../../upg-neighbor.js" integrity="sha384-YViVG3snYU3j6AZ8ZxBIJlBBMUL5Pj2guGdWiiKvexR9VNtQFCHrX+PQ6lLa2DrN" crossorigin="anonymous"></script>
<script src="../../upg-neighbor.js" integrity="sha384-L9U5BmHdIQSgYXleGnKoycRFoPQq/zCBFGCK+76Xt++6f/Z0byB9sSlVef7ofjXi" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <upg-neighbor> custom element. -->
<script src="../../upg-neighbor.js" integrity="sha384-YViVG3snYU3j6AZ8ZxBIJlBBMUL5Pj2guGdWiiKvexR9VNtQFCHrX+PQ6lLa2DrN" crossorigin="anonymous"></script>
<script src="../../upg-neighbor.js" integrity="sha384-L9U5BmHdIQSgYXleGnKoycRFoPQq/zCBFGCK+76Xt++6f/Z0byB9sSlVef7ofjXi" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <upg-neighbor> custom element. -->
<script src="../upg-neighbor.js" integrity="sha384-YViVG3snYU3j6AZ8ZxBIJlBBMUL5Pj2guGdWiiKvexR9VNtQFCHrX+PQ6lLa2DrN" crossorigin="anonymous"></script>
<script src="../upg-neighbor.js" integrity="sha384-L9U5BmHdIQSgYXleGnKoycRFoPQq/zCBFGCK+76Xt++6f/Z0byB9sSlVef7ofjXi" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <upg-neighbor> custom element. -->
<script src="../../upg-neighbor.js" integrity="sha384-YViVG3snYU3j6AZ8ZxBIJlBBMUL5Pj2guGdWiiKvexR9VNtQFCHrX+PQ6lLa2DrN" crossorigin="anonymous"></script>
<script src="../../upg-neighbor.js" integrity="sha384-L9U5BmHdIQSgYXleGnKoycRFoPQq/zCBFGCK+76Xt++6f/Z0byB9sSlVef7ofjXi" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <upg-neighbor> custom element. -->
<script src="../../upg-neighbor.js" integrity="sha384-YViVG3snYU3j6AZ8ZxBIJlBBMUL5Pj2guGdWiiKvexR9VNtQFCHrX+PQ6lLa2DrN" crossorigin="anonymous"></script>
<script src="../../upg-neighbor.js" integrity="sha384-L9U5BmHdIQSgYXleGnKoycRFoPQq/zCBFGCK+76Xt++6f/Z0byB9sSlVef7ofjXi" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <upg-neighbor> custom element. -->
<script src="../../upg-neighbor.js" integrity="sha384-YViVG3snYU3j6AZ8ZxBIJlBBMUL5Pj2guGdWiiKvexR9VNtQFCHrX+PQ6lLa2DrN" crossorigin="anonymous"></script>
<script src="../../upg-neighbor.js" integrity="sha384-L9U5BmHdIQSgYXleGnKoycRFoPQq/zCBFGCK+76Xt++6f/Z0byB9sSlVef7ofjXi" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order: