Merge the pilot (c7b77b0a1) and rebuild the 3 region-data bundles
Merge studio's poster type registry + pins-in-state + preview-flag pilot into the region data fix. Built-output conflicts resolved by taking the pilot's public/ then rebuilding the three bundles that embed country-posters.json (country-posters, poster-design, regions-dashboard) on the merged source, so they carry both the registry changes and the corrected regions. Tree + SRI regenerated. check-poster-section's dropped 'other' scope and the 18 region-only records are preserved through the merge.
This commit is contained in:
commit
fcd6d51910
16 changed files with 714 additions and 141 deletions
81
internal/check-poster-type-registry.mjs
Normal file
81
internal/check-poster-type-registry.mjs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-poster-type-registry.mjs — the poster type registry is ONE per page, whichever bundle's copy asks.
|
||||
*
|
||||
* Every bundle inlines its own copy of library/components/poster/posterTypes.js, and <dxm-poster-section> is
|
||||
* defined by whichever bundle loads first. A module-level registry made that element see only its own bundle's
|
||||
* types: a type another bundle registered rendered "none". So the registry is page-global (globalThis).
|
||||
*
|
||||
* ASSERTS, with TWO independent copies of the module loaded side by side (as two bundles are on one page):
|
||||
* A. a type registered through copy B is found through copy A (the copy that defined the element);
|
||||
* B. the built-in 'country' is listed by both copies, once;
|
||||
* C. a name registered twice keeps the FIRST registration (the second returns false);
|
||||
* D. every registration is announced on window ('dxm-poster-type-registered', { name }), so a section that asked
|
||||
* before its type arrived can render when it does;
|
||||
* E. PosterSectionView.vue re-resolves its type on that event.
|
||||
* `--self-test`: the per-bundle (module-level Map) registry must FAIL arm A.
|
||||
*/
|
||||
import { readFileSync, writeFileSync, mkdtempSync, rmSync, mkdirSync } from 'node:fs'
|
||||
import { join, dirname } from 'node:path'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
|
||||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const SRC = join(ROOT, 'library/components/poster/posterTypes.js')
|
||||
const VIEW = join(ROOT, 'library/components/poster/PosterSectionView.vue')
|
||||
|
||||
// A stand-in window that records the announcements.
|
||||
const events = []
|
||||
globalThis.window = { dispatchEvent: (e) => { events.push(e); return true } }
|
||||
globalThis.CustomEvent = class { constructor(type, init) { this.type = type; this.detail = init && init.detail } }
|
||||
|
||||
async function copies(source) {
|
||||
// Two copies = two module instances (as two IIFE bundles each inline one). Written to a scratch dir with a stub
|
||||
// for the lazily imported country adapter, so nothing else loads.
|
||||
const dir = mkdtempSync(join(tmpdir(), 'dxm-pt-'))
|
||||
mkdirSync(join(dir, 'types'))
|
||||
writeFileSync(join(dir, 'types', 'countryPosterType.js'), 'export default { items: () => [], title: () => "", actions: () => ({}) }\n')
|
||||
const code = source.replace(/import\.meta\.env\s*&&\s*import\.meta\.env\.DEV/g, 'false')
|
||||
writeFileSync(join(dir, 'a.mjs'), code); writeFileSync(join(dir, 'b.mjs'), code)
|
||||
const a = await import(pathToFileURL(join(dir, 'a.mjs')).href)
|
||||
const b = await import(pathToFileURL(join(dir, 'b.mjs')).href)
|
||||
return { a, b, done: () => rmSync(dir, { recursive: true, force: true }) }
|
||||
}
|
||||
|
||||
async function audit(source, viewSrc) {
|
||||
const fails = []
|
||||
for (const k of Object.getOwnPropertyNames(globalThis)) if (k === '__DXM_POSTER_TYPES__') delete globalThis[k]
|
||||
events.length = 0
|
||||
const { a, b, done } = await copies(source)
|
||||
try {
|
||||
const fixture = { items: () => [{ id: 'x' }], title: () => 'X', actions: () => ({}) }
|
||||
b.registerPosterType('gate-b-type', fixture)
|
||||
if (!(await a.posterType('gate-b-type'))) fails.push('A: a type registered through one bundle\'s copy is NOT found through another\'s (a per-bundle registry)')
|
||||
const na = a.posterTypeNames(), nb = b.posterTypeNames()
|
||||
if (na.filter((n) => n === 'country').length !== 1 || nb.filter((n) => n === 'country').length !== 1) fails.push(`B: 'country' is not listed exactly once by each copy (${na.join(',')} | ${nb.join(',')})`)
|
||||
const second = a.registerPosterType('gate-b-type', { items: () => [], title: () => 'Y', actions: () => ({}) })
|
||||
const kept = await b.posterType('gate-b-type')
|
||||
if (second !== false || !kept || kept.title() !== 'X') fails.push('C: a name registered twice did not keep the FIRST registration')
|
||||
if (!events.some((e) => e.type === 'dxm-poster-type-registered' && e.detail && e.detail.name === 'gate-b-type')) fails.push('D: a registration is not announced on window (dxm-poster-type-registered)')
|
||||
} finally { done() }
|
||||
if (!/dxm-poster-type-registered|TYPE_REGISTERED_EVENT/.test(viewSrc)) fails.push('E: PosterSectionView.vue does not re-resolve its type when one is registered later')
|
||||
return fails
|
||||
}
|
||||
|
||||
if (process.argv.includes('--self-test')) {
|
||||
// The per-bundle registry this gate exists to stop: a module-level Map.
|
||||
const perBundle = `const LOADERS = new Map([['country', () => import('./types/countryPosterType.js')]])
|
||||
const CACHE = new Map()
|
||||
export function registerPosterType(name, a) { LOADERS.set(name, typeof a === 'function' ? a : () => Promise.resolve({ default: a })); CACHE.delete(name); return true }
|
||||
export function posterType(name) { if (!LOADERS.has(name)) return Promise.resolve(null); if (!CACHE.has(name)) CACHE.set(name, LOADERS.get(name)().then((m) => m.default || m)); return CACHE.get(name) }
|
||||
export const posterTypeNames = () => [...LOADERS.keys()]
|
||||
`
|
||||
const f = await audit(perBundle, readFileSync(VIEW, 'utf8'))
|
||||
const ok = f.some((x) => x.startsWith('A:'))
|
||||
console.log(ok ? `✓ caught: a per-bundle registry -> ${f.find((x) => x.startsWith('A:'))}` : '✗ MISSED: the per-bundle registry passed arm A')
|
||||
process.exitCode = ok ? 0 : 1
|
||||
} else {
|
||||
const f = await audit(readFileSync(SRC, 'utf8'), readFileSync(VIEW, 'utf8'))
|
||||
if (f.length) { for (const x of f) console.log('✗ ' + x); console.log(`FAIL check:poster-type-registry (${f.length})`); process.exitCode = 1 }
|
||||
else console.log('PASS check:poster-type-registry — one registry per page: a type registered through one bundle\'s copy is found through another\'s; the first registration wins; registrations are announced and the section re-resolves')
|
||||
}
|
||||
|
|
@ -8,18 +8,19 @@
|
|||
* landing and to DX Life's /dx-maps, so one assignment there would print India publicly.
|
||||
*
|
||||
* ASSERTS:
|
||||
* A. no file under public/ ASSIGNS the flag (any spelling a minifier writes: = true, =!0, =1, …); reading it
|
||||
* (=== true) is fine;
|
||||
* B. every SOURCE line that assigns it (library/, plugin-designs/, outside scripts/ and tests) is behind
|
||||
* import.meta.env.DEV on the same line.
|
||||
* `--self-test` plants each defect in memory and exits 0 only if both are caught.
|
||||
* A. NO file under public/ MENTIONS the flag at all. An assignment there would turn the preview on, and a READ
|
||||
* there means the withheld path itself shipped, behind a global any embedding page or console can set (that
|
||||
* IS shipping it). Every read is compiled away by its DEV guard, so the built tree never names the flag;
|
||||
* B. every SOURCE line that mentions it, read OR write (library/, plugin-designs/, outside scripts/ and tests;
|
||||
* comments aside), carries import.meta.env.DEV on the same line.
|
||||
* `--self-test` plants each defect in memory and exits 0 only if all are caught.
|
||||
*/
|
||||
import { readFileSync, readdirSync, statSync } from 'node:fs'
|
||||
import { join, dirname, relative } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const ASSIGN = /DXM_PREVIEW_UNRELEASED\s*=(?!=)/
|
||||
const MENTION = /DXM_PREVIEW_UNRELEASED/
|
||||
const TEXT = /\.(m?js|html|json|css|txt)$/
|
||||
|
||||
function* walk(dir) {
|
||||
|
|
@ -37,9 +38,9 @@ function* walk(dir) {
|
|||
function auditBuilt(files) {
|
||||
const fails = []
|
||||
for (const [rel, text] of files) {
|
||||
if (!ASSIGN.test(text)) continue
|
||||
const line = text.split('\n').findIndex((l) => ASSIGN.test(l)) + 1
|
||||
fails.push(`A ${rel}:${line} ASSIGNS DXM_PREVIEW_UNRELEASED in the built tree (it would show withheld content publicly)`)
|
||||
if (!MENTION.test(text)) continue
|
||||
const line = text.split('\n').findIndex((l) => MENTION.test(l)) + 1
|
||||
fails.push(`A ${rel}:${line} names DXM_PREVIEW_UNRELEASED in the built tree (a setter would show withheld content; a read means the withheld path shipped)`)
|
||||
}
|
||||
return fails
|
||||
}
|
||||
|
|
@ -47,7 +48,7 @@ function auditSource(files) {
|
|||
const fails = []
|
||||
for (const [rel, text] of files) {
|
||||
text.split('\n').forEach((l, i) => {
|
||||
if (ASSIGN.test(l) && !/import\.meta\.env\.DEV/.test(l) && !/^\s*(\/\/|\*)/.test(l)) fails.push(`B ${rel}:${i + 1} assigns DXM_PREVIEW_UNRELEASED outside an import.meta.env.DEV guard`)
|
||||
if (MENTION.test(l) && !/import\.meta\.env\.DEV/.test(l) && !/^\s*(\/\/|\*|\/\*)/.test(l)) fails.push(`B ${rel}:${i + 1} reads or writes DXM_PREVIEW_UNRELEASED outside an import.meta.env.DEV guard`)
|
||||
})
|
||||
}
|
||||
return fails
|
||||
|
|
@ -59,14 +60,16 @@ const source = () => [...walk(join(ROOT, 'library')), ...walk(join(ROOT, 'plugin
|
|||
.map((p) => [relative(ROOT, p), readFileSync(p, 'utf8')])
|
||||
|
||||
if (process.argv.includes('--self-test')) {
|
||||
const a = auditBuilt([['public/dx-maps/x/x.js', 'var a=1;globalThis.DXM_PREVIEW_UNRELEASED=!0;'], ['public/dx-maps/x/ok.js', 'if(globalThis.DXM_PREVIEW_UNRELEASED===true)f()']])
|
||||
const b = auditSource([['library/x.js', 'globalThis.DXM_PREVIEW_UNRELEASED = true'], ['library/ok.js', 'if (import.meta.env.DEV) globalThis.DXM_PREVIEW_UNRELEASED = true']])
|
||||
const okA = a.length === 1 && /x\/x\.js/.test(a[0]), okB = b.length === 1 && /library\/x\.js/.test(b[0])
|
||||
console.log(okA ? '✓ caught: a built bundle that assigns the flag (minified =!0); a read (===) is not flagged' : '✗ MISSED: the built-tree assignment')
|
||||
console.log(okB ? '✓ caught: a source assignment outside import.meta.env.DEV; the guarded one passes' : '✗ MISSED: the unguarded source assignment')
|
||||
const a = auditBuilt([['public/dx-maps/x/set.js', 'var a=1;globalThis.DXM_PREVIEW_UNRELEASED=!0;'], ['public/dx-maps/x/read.js', 'if(globalThis.DXM_PREVIEW_UNRELEASED===!0)showIndia()'], ['public/dx-maps/x/ok.js', 'showOthers()']])
|
||||
const b = auditSource([['library/set.js', 'globalThis.DXM_PREVIEW_UNRELEASED = true'], ['library/read.js', "export const previewUnreleased = () => typeof globalThis !== 'undefined' && globalThis.DXM_PREVIEW_UNRELEASED === true"],
|
||||
['library/ok.js', 'if (import.meta.env.DEV) globalThis.DXM_PREVIEW_UNRELEASED = true'], ['library/okread.js', 'export const p = () => import.meta.env.DEV && globalThis.DXM_PREVIEW_UNRELEASED === true'], ['library/c.js', '// DXM_PREVIEW_UNRELEASED is named in a comment']])
|
||||
const okA = a.length === 2 && a.some((x) => /set\.js/.test(x)) && a.some((x) => /read\.js/.test(x))
|
||||
const okB = b.length === 2 && b.some((x) => /library\/set\.js/.test(x)) && b.some((x) => /library\/read\.js/.test(x))
|
||||
console.log(okA ? '✓ caught: a built bundle that SETS the flag (=!0) and one that READS it (the withheld path shipped); a clean one passes' : '✗ MISSED: a built-tree setter or reader')
|
||||
console.log(okB ? '✓ caught: an unguarded source SETTER and an unguarded source READ (the plain previewUnreleased()); the DEV-guarded ones and comments pass' : '✗ MISSED: an unguarded source setter or read')
|
||||
process.exitCode = okA && okB ? 0 : 1
|
||||
} else {
|
||||
const fails = [...auditBuilt(built()), ...auditSource(source())]
|
||||
if (fails.length) { for (const f of fails) console.log('✗ ' + f); console.log(`FAIL check:preview-flag-dev-only (${fails.length})`); process.exitCode = 1 }
|
||||
else console.log('PASS check:preview-flag-dev-only — no built file assigns DXM_PREVIEW_UNRELEASED; every source assignment is behind import.meta.env.DEV')
|
||||
else console.log('PASS check:preview-flag-dev-only — no built file names DXM_PREVIEW_UNRELEASED; every source read and write is behind import.meta.env.DEV')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
<script setup>
|
||||
/**
|
||||
* PosterSectionView.vue — ONE poster section on its own: the profile behind <dxm-poster-section>.
|
||||
* It reads its settings from profile-config (type, scope, collapsible, collapsed, locale, assetsBase), finds the
|
||||
* It reads its settings from profile-config (type, scope, collapsible, collapsed, locale, assetsBase, columns), finds the
|
||||
* poster TYPE in posterTypes.js, and draws a PosterWall in section-only mode: the same section a wall draws (the
|
||||
* same PosterSection, the same print actions and overlay), with no wall head around it.
|
||||
*/
|
||||
import { computed, inject, ref, watch } from 'vue'
|
||||
import { computed, inject, ref, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||
import PosterWall from './PosterWall.vue'
|
||||
import { posterType } from './posterTypes.js'
|
||||
import { posterType, TYPE_REGISTERED_EVENT } from './posterTypes.js'
|
||||
import { useOptionalT } from '@map/composables/useOptionalT.js'
|
||||
import { getRuntimeLocale, detectLocale } from '@map/i18n/index.js'
|
||||
import { getApiBaseUrl } from '@map/api/apiBaseUrl.js'
|
||||
|
|
@ -18,10 +18,18 @@ const t = useOptionalT('')
|
|||
const typeName = computed(() => cfg.value.type || 'country')
|
||||
const adapter = ref(null)
|
||||
const unknownType = ref(false)
|
||||
watch(typeName, async (name) => {
|
||||
async function resolveType(name) {
|
||||
const a = await posterType(name)
|
||||
if (name !== typeName.value) return // the type changed while this one loaded
|
||||
adapter.value = a; unknownType.value = !a
|
||||
}, { immediate: true })
|
||||
}
|
||||
watch(typeName, resolveType, { immediate: true })
|
||||
// ★ A TYPE MAY ARRIVE AFTER THIS SECTION ASKED: the registry is page-global (posterTypes.js), and another bundle
|
||||
// that registers the type (upg100's lists, say) can load after the bundle that defined this element. Look again
|
||||
// when a registration of OUR type is announced.
|
||||
const onTypeRegistered = (e) => { if (e && e.detail && e.detail.name === typeName.value && !adapter.value) resolveType(typeName.value) }
|
||||
onMounted(() => { try { window.addEventListener(TYPE_REGISTERED_EVENT, onTypeRegistered) } catch (_) { /* no window */ } })
|
||||
onBeforeUnmount(() => { try { window.removeEventListener(TYPE_REGISTERED_EVENT, onTypeRegistered) } catch (_) { /* no window */ } })
|
||||
|
||||
const lang = () => cfg.value.locale || getRuntimeLocale() || detectLocale() || 'en'
|
||||
const assetsBase = computed(() => cfg.value.assetsBase || './assets')
|
||||
|
|
@ -52,6 +60,7 @@ const noneLine = computed(() => t('poster.sectionNone', { scope: String(cfg.valu
|
|||
:collapsed-by="collapsedBy"
|
||||
:instance-id="instanceId"
|
||||
:is-dark="cfg.isDark === true"
|
||||
:columns="cfg.columns || 0"
|
||||
:assets-base="assetsBase"
|
||||
:event-name="actions.eventName"
|
||||
:unit="actions.unit"
|
||||
|
|
|
|||
|
|
@ -12,21 +12,55 @@
|
|||
* actions({ assetsBase, apiBase, lang, profileConfig, t }) → the PosterWall props that print a card:
|
||||
* { records, sheet, unsupported, unit, itemCountKey, eventName, downloadPrefix, cardStocks }
|
||||
* }
|
||||
*
|
||||
* ★ ONE REGISTRY PER PAGE, NOT PER BUNDLE. Every bundle inlines its own copy of this file, and <dxm-poster-section>
|
||||
* is defined by whichever bundle loads first. With a module-level Map, that element read only ITS bundle's types,
|
||||
* so type="upg100-list" rendered "none" whenever the country-posters bundle had defined the element. So the
|
||||
* registry lives on the page (globalThis[REGISTRY_KEY], created once): every copy registers into it and reads from
|
||||
* it. A name registered twice keeps the FIRST (a dev warning says so; the built-in 'country' is registered by every
|
||||
* copy and is silent). Every registration is announced (TYPE_REGISTERED_EVENT on window), so a section that asked
|
||||
* before its type arrived renders when it does.
|
||||
*/
|
||||
const LOADERS = new Map([
|
||||
['country', () => import('./types/countryPosterType.js')],
|
||||
])
|
||||
const CACHE = new Map()
|
||||
export const REGISTRY_KEY = '__DXM_POSTER_TYPES__'
|
||||
export const TYPE_REGISTERED_EVENT = 'dxm-poster-type-registered'
|
||||
const VERSION = 1
|
||||
|
||||
/** Register (or replace) a type: an adapter object, or a function that loads one (a dynamic import). */
|
||||
function registry() {
|
||||
const g = typeof globalThis !== 'undefined' ? globalThis : {}
|
||||
let r = g[REGISTRY_KEY]
|
||||
if (!r || !(r.loaders instanceof Map) || !(r.cache instanceof Map)) {
|
||||
r = { version: VERSION, loaders: new Map(), cache: new Map() }
|
||||
try { Object.defineProperty(g, REGISTRY_KEY, { value: r, configurable: true, writable: false }) } catch (_) { g[REGISTRY_KEY] = r }
|
||||
}
|
||||
return r
|
||||
}
|
||||
const toLoader = (a) => (typeof a === 'function' ? a : () => Promise.resolve({ default: a }))
|
||||
function announce(name) {
|
||||
try { if (typeof window !== 'undefined' && typeof CustomEvent === 'function') window.dispatchEvent(new CustomEvent(TYPE_REGISTERED_EVENT, { detail: { name } })) } catch (_) { /* no window */ }
|
||||
}
|
||||
|
||||
/** Register a type: an adapter object, or a function that loads one (a dynamic import). The first registration wins. */
|
||||
export function registerPosterType(name, adapterOrLoader) {
|
||||
LOADERS.set(name, typeof adapterOrLoader === 'function' ? adapterOrLoader : () => Promise.resolve({ default: adapterOrLoader }))
|
||||
CACHE.delete(name)
|
||||
const r = registry()
|
||||
if (r.loaders.has(name)) {
|
||||
if (import.meta.env && import.meta.env.DEV && typeof console !== 'undefined') console.warn(`[dx-maps] poster type "${name}" is already registered on this page; the first registration is kept.`)
|
||||
return false
|
||||
}
|
||||
r.loaders.set(name, toLoader(adapterOrLoader))
|
||||
announce(name)
|
||||
return true
|
||||
}
|
||||
/** The adapter for a type, or null for a type nobody registered. */
|
||||
/** The adapter for a type, or null for a type nobody on this page registered (yet). */
|
||||
export function posterType(name) {
|
||||
if (!LOADERS.has(name)) return Promise.resolve(null)
|
||||
if (!CACHE.has(name)) CACHE.set(name, LOADERS.get(name)().then((m) => m.default || m))
|
||||
return CACHE.get(name)
|
||||
const r = registry()
|
||||
if (!r.loaders.has(name)) return Promise.resolve(null)
|
||||
if (!r.cache.has(name)) r.cache.set(name, r.loaders.get(name)().then((m) => m.default || m))
|
||||
return r.cache.get(name)
|
||||
}
|
||||
export const posterTypeNames = () => [...registry().loaders.keys()]
|
||||
|
||||
// The built-in type, from every copy: silent, first one kept.
|
||||
if (!registry().loaders.has('country')) {
|
||||
registry().loaders.set('country', () => import('./types/countryPosterType.js'))
|
||||
announce('country')
|
||||
}
|
||||
export const posterTypeNames = () => [...LOADERS.keys()]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// <dxm-poster-section>'s attributes → its profile-config. Pure (no Vue), so it is testable on its own.
|
||||
export const ATTRS = ['type', 'scope', 'collapsible', 'collapsed', 'locale', 'assets-base', 'api-base-url', 'is-dark']
|
||||
export const ATTRS = ['type', 'scope', 'collapsible', 'collapsed', 'locale', 'assets-base', 'api-base-url', 'is-dark', 'columns']
|
||||
export const TAG = 'dxm-poster-section'
|
||||
|
||||
export function posterSectionConfig(el) {
|
||||
|
|
@ -9,6 +9,9 @@ export function posterSectionConfig(el) {
|
|||
if (a('locale')) cfg.locale = a('locale')
|
||||
if (a('assets-base')) cfg.assetsBase = a('assets-base')
|
||||
if (a('api-base-url')) cfg.apiBaseUrl = a('api-base-url')
|
||||
// posters per row (1-5); absent = the wall's own default. A reader's saved choice still wins (PosterWall).
|
||||
const cols = Number(a('columns'))
|
||||
if (el.hasAttribute('columns') && Number.isInteger(cols) && cols >= 1 && cols <= 5) cfg.columns = cols
|
||||
return cfg
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@
|
|||
* <dxm-poster-section type="country" scope="africa" locale="ar"></dxm-poster-section>
|
||||
*
|
||||
* Attributes: type (default "country"), scope (a region / continent / list key), collapsible (default on;
|
||||
* collapsible="false" turns it off), collapsed (start shut), locale, assets-base, api-base-url, is-dark.
|
||||
* collapsible="false" turns it off), collapsed (start shut), locale, assets-base, api-base-url, is-dark, columns
|
||||
* (posters per row, 1-5).
|
||||
* They are mapped into the standard element's profile-config, so the element keeps defineMapElement's per-instance
|
||||
* i18n and store, and the profile (PosterSectionView.vue) reads them like every other profile reads its config.
|
||||
* Any bundle can call registerPosterSectionElement(); a second call is a no-op.
|
||||
|
|
|
|||
237
library/packet/pins-in-state.js
Normal file
237
library/packet/pins-in-state.js
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
/**
|
||||
* pins-in-state.js — place people-group pins on a multi-state poster so that
|
||||
* (a) no two pins overlap (every pair ≥ 2r+gap apart, where feasible), AND
|
||||
* (b) every pin stays inside its OWN state's land.
|
||||
*
|
||||
* It is a self-contained copy of the poster engine's pin de-overlap relaxation
|
||||
* (poster-render.js, the pass that settles the `_fixed` pins): a repulsion loop
|
||||
* in pixel space that pushes any pair closer than MIN_SEP apart along the line
|
||||
* between them, capped so a pin never drifts far from its true position. The one
|
||||
* addition is the state constraint: where the engine re-anchors a drifted pin to
|
||||
* the map rect, this re-anchors it to its state polygon — a pin pushed outside is
|
||||
* pulled back to the nearest point just inside (by `inset`). The engine is read,
|
||||
* not called, so the whole-India classic4 mode can own the constraint without
|
||||
* reaching into the renderer.
|
||||
*
|
||||
* PX ONLY. Pins and polygons arrive already projected to poster pixels through
|
||||
* the renderer's own proj(lng,lat); this module never projects.
|
||||
*
|
||||
* DETERMINISTIC. Fixed iteration order, golden-angle split for exact coincidence,
|
||||
* no Math.random — the same input yields the same output bytes, so the sheet is
|
||||
* stable across runs.
|
||||
*
|
||||
* @param {object} opts
|
||||
* @param {Array} opts.pins [{ id, x, y, stateId }] in poster px. Not mutated.
|
||||
* @param {object} opts.states { [stateId]: polygonsPx }, where polygonsPx is a
|
||||
* GeoJSON-Polygon-like list of PARTS; each part is
|
||||
* [outerRing, ...holeRings]; each ring is [[x,y],…]
|
||||
* in px. "Inside the state" = inside ANY part's
|
||||
* outer ring and outside that part's holes.
|
||||
* @param {number} opts.r pin radius in px.
|
||||
* @param {number} [opts.gap=0] extra clearance between pin edges; MIN_SEP = 2r+gap.
|
||||
* @param {number} [opts.iters=260] relaxation passes (matches the engine).
|
||||
* @param {number} [opts.maxDrift=58] hard cap on how far a pin may move from truth.
|
||||
* @param {number} [opts.inset=0.5] how far INSIDE the boundary a pulled-back pin lands.
|
||||
* @returns {{
|
||||
* pins: Array<{id,x,y,stateId,moved:boolean,dx:number,dy:number}>,
|
||||
* moved: number, maxDisp: number, meanDisp: number,
|
||||
* overlapsRemaining: number,
|
||||
* outsideState: number,
|
||||
* infeasible: Array<{ stateId: string, pins: Array }>,
|
||||
* }}
|
||||
*/
|
||||
export function placePinsInState({ pins, states, r, gap = 0, iters = 260, maxDrift = 58, inset = 0.5 }) {
|
||||
if (!Array.isArray(pins)) throw new TypeError('pins must be an array');
|
||||
if (!states || typeof states !== 'object') throw new TypeError('states must be an object');
|
||||
if (!(r > 0)) throw new TypeError('r must be a positive number');
|
||||
|
||||
const MIN_SEP = r * 2 + gap;
|
||||
|
||||
// Working copy — input is never mutated. _ox/_oy hold the true (projected) origin.
|
||||
const work = pins.map((p) => ({
|
||||
id: p.id, stateId: p.stateId,
|
||||
x: p.x, y: p.y, _ox: p.x, _oy: p.y,
|
||||
}));
|
||||
|
||||
// Resolve each pin's state parts once. A pin whose stateId has no polygon is
|
||||
// left unconstrained by the land test (it still de-overlaps) and is reported.
|
||||
const partsOf = (stateId) => {
|
||||
const poly = states[stateId];
|
||||
return Array.isArray(poly) ? poly : null;
|
||||
};
|
||||
|
||||
for (let pass = 0; pass < iters; pass++) {
|
||||
let moved = false;
|
||||
|
||||
// Pairwise repulsion — same shape as the engine's pass, same fixed order.
|
||||
for (let i = 0; i < work.length; i++) {
|
||||
for (let j = i + 1; j < work.length; j++) {
|
||||
const a = work[i], b = work[j];
|
||||
let dx = b.x - a.x, dy = b.y - a.y;
|
||||
let dist = Math.hypot(dx, dy);
|
||||
if (dist >= MIN_SEP) continue;
|
||||
if (dist < 0.0001) { // exactly coincident — split on a
|
||||
const ang = i * 2.399963; // golden angle, deterministic
|
||||
dx = Math.cos(ang); dy = Math.sin(ang); dist = 1;
|
||||
}
|
||||
const push = (MIN_SEP - dist) / 2 + 0.15;
|
||||
const ux = dx / dist, uy = dy / dist;
|
||||
a.x -= ux * push; a.y -= uy * push;
|
||||
b.x += ux * push; b.y += uy * push;
|
||||
moved = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Re-anchor: cap drift from truth, then keep each pin inside its own state.
|
||||
for (const it of work) {
|
||||
const ddx = it.x - it._ox, ddy = it.y - it._oy;
|
||||
const d = Math.hypot(ddx, ddy);
|
||||
if (d > maxDrift) { it.x = it._ox + (ddx / d) * maxDrift; it.y = it._oy + (ddy / d) * maxDrift; }
|
||||
const parts = partsOf(it.stateId);
|
||||
if (parts && !pointInState(it.x, it.y, parts)) {
|
||||
const near = nearestInside(it.x, it.y, parts, inset);
|
||||
it.x = near.x; it.y = near.y;
|
||||
}
|
||||
}
|
||||
|
||||
if (!moved) break;
|
||||
}
|
||||
|
||||
// ── Report ────────────────────────────────────────────────────────────────
|
||||
let maxDisp = 0, sumDisp = 0, movedCount = 0, outsideState = 0;
|
||||
const outPins = work.map((it) => {
|
||||
const dx = it.x - it._ox, dy = it.y - it._oy;
|
||||
const disp = Math.hypot(dx, dy);
|
||||
const didMove = disp > 1e-6;
|
||||
if (didMove) { movedCount++; sumDisp += disp; if (disp > maxDisp) maxDisp = disp; }
|
||||
const parts = partsOf(it.stateId);
|
||||
if (parts && !pointInState(it.x, it.y, parts)) outsideState++;
|
||||
return { id: it.id, x: it.x, y: it.y, stateId: it.stateId, moved: didMove, dx, dy };
|
||||
});
|
||||
|
||||
// Overlaps remaining: count unordered pairs still closer than MIN_SEP.
|
||||
let overlapsRemaining = 0;
|
||||
const infeasibleByState = new Map();
|
||||
for (let i = 0; i < outPins.length; i++) {
|
||||
for (let j = i + 1; j < outPins.length; j++) {
|
||||
const a = outPins[i], b = outPins[j];
|
||||
if (Math.hypot(b.x - a.x, b.y - a.y) < MIN_SEP - 1e-6) {
|
||||
overlapsRemaining++;
|
||||
// An overlap that survives is a feasibility failure of the state(s)
|
||||
// holding these pins — record both so nothing is silently stacked.
|
||||
for (const p of [a, b]) {
|
||||
if (!infeasibleByState.has(p.stateId)) infeasibleByState.set(p.stateId, new Set());
|
||||
infeasibleByState.get(p.stateId).add(p.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const infeasible = [...infeasibleByState.entries()]
|
||||
.map(([stateId, ids]) => ({ stateId, pins: [...ids] }))
|
||||
.sort((p, q) => String(p.stateId).localeCompare(String(q.stateId)));
|
||||
|
||||
return {
|
||||
pins: outPins,
|
||||
moved: movedCount,
|
||||
maxDisp,
|
||||
meanDisp: movedCount ? sumDisp / movedCount : 0,
|
||||
overlapsRemaining,
|
||||
outsideState,
|
||||
infeasible,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Geometry (self-contained; px space) ───────────────────────────────────────
|
||||
|
||||
// Ray-casting point-in-ring. A point ON an edge counts as inside (poster pins on
|
||||
// a coast must not be rejected for landing exactly on the line).
|
||||
function pointInRing(x, y, ring) {
|
||||
let inside = false;
|
||||
for (let i = 0, k = ring.length - 1; i < ring.length; k = i++) {
|
||||
const xi = ring[i][0], yi = ring[i][1];
|
||||
const xk = ring[k][0], yk = ring[k][1];
|
||||
const intersect = ((yi > y) !== (yk > y)) &&
|
||||
(x < ((xk - xi) * (y - yi)) / ((yk - yi) || 1e-12) + xi);
|
||||
if (intersect) inside = !inside;
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
|
||||
// Inside the state = inside ANY part's outer ring AND outside that part's holes.
|
||||
function pointInState(x, y, parts) {
|
||||
for (const part of parts) {
|
||||
if (!part || !part.length) continue;
|
||||
const [outer, ...holes] = part;
|
||||
if (!outer || outer.length < 3) continue;
|
||||
if (!pointInRing(x, y, outer)) continue;
|
||||
let inHole = false;
|
||||
for (const h of holes) { if (h && h.length >= 3 && pointInRing(x, y, h)) { inHole = true; break; } }
|
||||
if (!inHole) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Nearest point ON a ring's edges to (x,y), as {x,y,d2}.
|
||||
function nearestOnRing(x, y, ring) {
|
||||
let best = null;
|
||||
for (let i = 0, k = ring.length - 1; i < ring.length; k = i++) {
|
||||
const p = nearestOnSegment(x, y, ring[k][0], ring[k][1], ring[i][0], ring[i][1]);
|
||||
if (!best || p.d2 < best.d2) best = p;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
function nearestOnSegment(px, py, ax, ay, bx, by) {
|
||||
const abx = bx - ax, aby = by - ay;
|
||||
const len2 = abx * abx + aby * aby;
|
||||
let t = len2 ? ((px - ax) * abx + (py - ay) * aby) / len2 : 0;
|
||||
t = t < 0 ? 0 : t > 1 ? 1 : t;
|
||||
const x = ax + t * abx, y = ay + t * aby;
|
||||
const dx = px - x, dy = py - y;
|
||||
return { x, y, d2: dx * dx + dy * dy };
|
||||
}
|
||||
|
||||
// Pull an outside point to the nearest point just INSIDE the state, by `inset`.
|
||||
// Nearest boundary over ALL parts' rings (outer + holes), then step inward along
|
||||
// the direction from the boundary toward the pin, re-tested; if that still reads
|
||||
// outside (a thin neck), fall to the nearest part's centroid, which is inside a
|
||||
// simple part by construction and a safe honest fallback for a degenerate one.
|
||||
function nearestInside(x, y, parts, inset) {
|
||||
let best = null, bestPart = null;
|
||||
for (const part of parts) {
|
||||
for (const ring of part) {
|
||||
if (!ring || ring.length < 2) continue;
|
||||
const cand = nearestOnRing(x, y, ring);
|
||||
if (cand && (!best || cand.d2 < best.d2)) { best = cand; bestPart = part; }
|
||||
}
|
||||
}
|
||||
if (!best) return { x, y };
|
||||
// Step from the boundary toward the original pin (inward for an outer edge);
|
||||
// if the pin sat in a hole this points out of the hole, also inward to land.
|
||||
const vx = x - best.x, vy = y - best.y;
|
||||
const vlen = Math.hypot(vx, vy) || 1;
|
||||
let nx = best.x + (vx / vlen) * inset;
|
||||
let ny = best.y + (vy / vlen) * inset;
|
||||
if (pointInState(nx, ny, parts)) return { x: nx, y: ny };
|
||||
// Thin-neck fallback: step the other way, then the part centroid.
|
||||
nx = best.x - (vx / vlen) * inset; ny = best.y - (vy / vlen) * inset;
|
||||
if (pointInState(nx, ny, parts)) return { x: nx, y: ny };
|
||||
const c = ringCentroid(bestPart[0]);
|
||||
return pointInState(c.x, c.y, parts) ? c : { x: best.x, y: best.y };
|
||||
}
|
||||
|
||||
function ringCentroid(ring) {
|
||||
let a = 0, cx = 0, cy = 0;
|
||||
for (let i = 0, k = ring.length - 1; i < ring.length; k = i++) {
|
||||
const cross = ring[k][0] * ring[i][1] - ring[i][0] * ring[k][1];
|
||||
a += cross; cx += (ring[k][0] + ring[i][0]) * cross; cy += (ring[k][1] + ring[i][1]) * cross;
|
||||
}
|
||||
if (Math.abs(a) < 1e-9) { // degenerate: mean of vertices
|
||||
let mx = 0, my = 0; for (const p of ring) { mx += p[0]; my += p[1]; }
|
||||
return { x: mx / ring.length, y: my / ring.length };
|
||||
}
|
||||
return { x: cx / (3 * a), y: cy / (3 * a) };
|
||||
}
|
||||
|
||||
export default placePinsInState;
|
||||
200
library/packet/scripts/check-pins-in-state.mjs
Normal file
200
library/packet/scripts/check-pins-in-state.mjs
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-pins-in-state.mjs — unit check for library/packet/pins-in-state.js.
|
||||
*
|
||||
* Verifies, at the whole-India 60x96in poster's px scale (sheet 2500x4000,
|
||||
* PIN_R_BASE 8.0, MIN_SEP = 2r*1.16 ≈ the engine's), that placePinsInState():
|
||||
* 1. leaves 0 pins outside their own state polygon,
|
||||
* 2. leaves 0 overlaps that are NOT reported in `infeasible`,
|
||||
* 3. is DETERMINISTIC (two runs, identical output), and
|
||||
* 4. is poison-tested: a state too small for its pins is NAMED in `infeasible`,
|
||||
* never silently overlapped.
|
||||
*
|
||||
* Runs on the REAL India data when it is present (the state-assignment JSON that
|
||||
* reg-post provides); otherwise on a self-contained synthetic 60x96 fixture, so
|
||||
* the gate is runnable now and tightens automatically once the data lands. A
|
||||
* vacuous pass is refused: the run asserts a floor on how many pins it exercised.
|
||||
*
|
||||
* No browser, no network. Exit 0 PASS, 1 FAIL (reason flushed first), 75 NOT
|
||||
* MEASURED only if the module cannot be imported.
|
||||
*/
|
||||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const REPO = resolve(HERE, '..', '..', '..');
|
||||
|
||||
let placePinsInState;
|
||||
try {
|
||||
({ placePinsInState } = await import('../pins-in-state.js'));
|
||||
} catch (e) {
|
||||
console.error(`NOT MEASURED: cannot import pins-in-state.js — ${e.message}`);
|
||||
process.exitCode = 75;
|
||||
}
|
||||
|
||||
if (placePinsInState) {
|
||||
// ── Scale constants, taken from poster-render.js for the 60x96 sheet ─────────
|
||||
const SHEET_W = 2500, SHEET_H = 4000; // '60x96': { w:2500, h:4000 }
|
||||
const GUTTER = 572;
|
||||
const MAP_X = GUTTER, MAP_W = SHEET_W - 2 * GUTTER; // ≈1356
|
||||
const MAP_Y = 360, MAP_H = SHEET_H - 760; // generous India map band
|
||||
const R = 8.0; // PIN_R_BASE
|
||||
const GAP = R * 2 * 0.16; // MIN_SEP = 2r+GAP ≈ 2r*1.16
|
||||
const MIN_SEP = R * 2 + GAP;
|
||||
|
||||
let pins, states, source;
|
||||
const real = findRealData();
|
||||
if (real) { ({ pins, states } = real); source = 'REAL India data'; }
|
||||
else { ({ pins, states } = synthFixture()); source = 'synthetic 60x96 fixture'; }
|
||||
|
||||
const FLOOR = 200; // refuse a vacuous pass
|
||||
const fails = [];
|
||||
const note = [];
|
||||
|
||||
if (pins.length < FLOOR) {
|
||||
fails.push(`exercised only ${pins.length} pins (floor ${FLOOR}) — vacuous`);
|
||||
}
|
||||
|
||||
// ── 1+2: the real placement ─────────────────────────────────────────────────
|
||||
const res = placePinsInState({ pins, states, r: R, gap: GAP, maxDrift: 58 });
|
||||
note.push(`${source}: ${pins.length} pins across ${Object.keys(states).length} states`);
|
||||
note.push(`moved ${res.moved}, maxDisp ${res.maxDisp.toFixed(1)}px, meanDisp ${res.meanDisp.toFixed(1)}px, ` +
|
||||
`overlapsRemaining ${res.overlapsRemaining}, outsideState ${res.outsideState}, ` +
|
||||
`infeasible states ${res.infeasible.length}`);
|
||||
|
||||
if (res.outsideState !== 0) fails.push(`${res.outsideState} pins ended OUTSIDE their own state`);
|
||||
|
||||
// Input must be unmutated.
|
||||
if (pins.some((p, i) => p.x !== (real ? real.origX[i] : p.x))) { /* only meaningful for real */ }
|
||||
|
||||
// Every surviving overlap must be accounted for in `infeasible`.
|
||||
const infeasibleIds = new Set(res.infeasible.flatMap((f) => f.pins));
|
||||
let unaccountedOverlaps = 0;
|
||||
for (let i = 0; i < res.pins.length; i++) {
|
||||
for (let j = i + 1; j < res.pins.length; j++) {
|
||||
const a = res.pins[i], b = res.pins[j];
|
||||
if (Math.hypot(b.x - a.x, b.y - a.y) < MIN_SEP - 1e-6) {
|
||||
if (!infeasibleIds.has(a.id) || !infeasibleIds.has(b.id)) unaccountedOverlaps++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (unaccountedOverlaps !== 0) fails.push(`${unaccountedOverlaps} overlaps NOT reported in infeasible (silent stacking)`);
|
||||
|
||||
// ── 3: determinism ───────────────────────────────────────────────────────────
|
||||
const res2 = placePinsInState({ pins, states, r: R, gap: GAP, maxDrift: 58 });
|
||||
const same = JSON.stringify(res.pins) === JSON.stringify(res2.pins);
|
||||
if (!same) fails.push('NON-DETERMINISTIC: two runs produced different output');
|
||||
|
||||
// ── 4: poison — a UT too small to hold its pins must be NAMED infeasible ──────
|
||||
const poison = poisonFixture(R, GAP);
|
||||
const pres = placePinsInState({ pins: poison.pins, states: poison.states, r: R, gap: GAP });
|
||||
const poisonNamed = pres.infeasible.some((f) => f.stateId === 'tiny-UT' && f.pins.length >= 2);
|
||||
// Count overlaps inside the tiny UT; they MUST all be reported (not silent).
|
||||
let poisonSilent = 0;
|
||||
for (let i = 0; i < pres.pins.length; i++)
|
||||
for (let j = i + 1; j < pres.pins.length; j++) {
|
||||
const a = pres.pins[i], b = pres.pins[j];
|
||||
if (Math.hypot(b.x - a.x, b.y - a.y) < MIN_SEP - 1e-6) {
|
||||
const ids = new Set(pres.infeasible.flatMap((f) => f.pins));
|
||||
if (!ids.has(a.id) || !ids.has(b.id)) poisonSilent++;
|
||||
}
|
||||
}
|
||||
if (!poisonNamed) fails.push('POISON: a too-small state was NOT named in infeasible');
|
||||
if (poisonSilent !== 0) fails.push(`POISON: ${poisonSilent} overlaps in the tiny state went unreported`);
|
||||
// Poison pins must still be INSIDE the tiny state (constraint beats separation).
|
||||
if (pres.outsideState !== 0) fails.push(`POISON: ${pres.outsideState} pins escaped the tiny state`);
|
||||
note.push(`poison: tiny-UT named=${poisonNamed}, silent overlaps=${poisonSilent}, outside=${pres.outsideState}`);
|
||||
|
||||
// ── 5: multipolygon + hole + island-fallback paths (the island/coast cases) ──
|
||||
{
|
||||
const outer = [[100, 100], [400, 100], [400, 400], [100, 400], [100, 100]];
|
||||
const hole = [[200, 200], [300, 200], [300, 300], [200, 300], [200, 200]];
|
||||
const island = [[600, 100], [680, 100], [680, 180], [600, 180], [600, 100]];
|
||||
const mstates = { A: [[outer, hole], [island]] };
|
||||
const mpins = [
|
||||
{ id: 'inHole', x: 250, y: 250, stateId: 'A' }, // in the mainland hole → must leave it
|
||||
{ id: 'onIsland', x: 640, y: 140, stateId: 'A' }, // on the island part → inside
|
||||
{ id: 'mainland', x: 150, y: 150, stateId: 'A' }, // inside mainland
|
||||
];
|
||||
const mres = placePinsInState({ pins: mpins, states: mstates, r: R, gap: GAP });
|
||||
if (mres.outsideState !== 0) fails.push(`multipoly: ${mres.outsideState} pins outside a 2-part state with a hole`);
|
||||
const moved = mres.pins.find((p) => p.id === 'inHole');
|
||||
if (Math.abs(moved.dx) + Math.abs(moved.dy) < 1e-6) fails.push('multipoly: the in-hole pin was not moved onto land');
|
||||
|
||||
// Island fallback: a pin geocoded OFF its state is pulled onto the state's land.
|
||||
const istates = { Lakshadweep: [[[[50, 50], [90, 50], [90, 90], [50, 90], [50, 50]]]] };
|
||||
const ires = placePinsInState({ pins: [{ id: 'offshore', x: 300, y: 300, stateId: 'Lakshadweep' }], states: istates, r: R, gap: GAP });
|
||||
if (ires.outsideState !== 0) fails.push('island: an offshore pin was not pulled onto its state');
|
||||
note.push(`multipoly+hole outside=${mres.outsideState}, in-hole moved=${(Math.abs(moved.dx) + Math.abs(moved.dy) > 1e-6)}; island pulled ${Math.hypot(ires.pins[0].dx, ires.pins[0].dy).toFixed(1)}px onto land`);
|
||||
}
|
||||
|
||||
for (const n of note) console.log(` · ${n}`);
|
||||
if (fails.length) {
|
||||
console.error(`FAIL check:pins-in-state (${source})`);
|
||||
for (const f of fails) console.error(` ✗ ${f}`);
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
console.log(`PASS check:pins-in-state — ${pins.length} pins, 0 outside-state, 0 silent overlaps, deterministic, poison caught (${source})`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Real-data loader (best-effort; the shapes reg-post controls) ───────────────
|
||||
function findRealData() {
|
||||
// Candidate committed locations for the India state-assignment + boundary px.
|
||||
const candidates = [
|
||||
'plugin-designs/country-posters/data/india-pins-in-state.json',
|
||||
'plugin-designs/country-posters/data/india-state-pins-px.json',
|
||||
];
|
||||
for (const rel of candidates) {
|
||||
const p = resolve(REPO, rel);
|
||||
if (!existsSync(p)) continue;
|
||||
try {
|
||||
const j = JSON.parse(readFileSync(p, 'utf8'));
|
||||
if (Array.isArray(j.pins) && j.states && typeof j.states === 'object') {
|
||||
const origX = j.pins.map((q) => q.x);
|
||||
return { pins: j.pins, states: j.states, origX };
|
||||
}
|
||||
} catch { /* fall through to synthetic */ }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Synthetic 60x96 fixture: a grid of states, pins clustered so de-overlap bites,
|
||||
// every pin tagged to the state it sits in. Deterministic, ≥ FLOOR pins. ──────
|
||||
function synthFixture() {
|
||||
const states = {};
|
||||
const pins = [];
|
||||
const COLS = 6, ROWS = 7; // 42 states
|
||||
const CW = 1356 / COLS, CH = 2600 / ROWS; // within the India map band
|
||||
const X0 = 572, Y0 = 400, PAD = 6;
|
||||
let pid = 0;
|
||||
for (let c = 0; c < COLS; c++) {
|
||||
for (let r = 0; r < ROWS; r++) {
|
||||
const id = `S${c}-${r}`;
|
||||
const x0 = X0 + c * CW + PAD, y0 = Y0 + r * CH + PAD;
|
||||
const x1 = X0 + (c + 1) * CW - PAD, y1 = Y0 + (r + 1) * CH - PAD;
|
||||
// Single-part rectangle (outer ring only).
|
||||
states[id] = [[[[x0, y0], [x1, y0], [x1, y1], [x0, y1], [x0, y0]]]];
|
||||
// 5 pins per state, deliberately stacked near the centre so they must spread.
|
||||
const cx = (x0 + x1) / 2, cy = (y0 + y1) / 2;
|
||||
for (let k = 0; k < 5; k++) {
|
||||
pins.push({ id: `p${pid++}`, x: cx + (k % 2), y: cy + (k * 0.7), stateId: id });
|
||||
}
|
||||
}
|
||||
}
|
||||
return { pins, states }; // 210 pins ≥ FLOOR
|
||||
}
|
||||
|
||||
// ── Poison: one tiny state with far more pins than fit at MIN_SEP ──────────────
|
||||
function poisonFixture(R, GAP) {
|
||||
const MIN_SEP = R * 2 + GAP;
|
||||
// A box barely larger than one pin — cannot separate 4 pins to MIN_SEP.
|
||||
const s = MIN_SEP * 0.8;
|
||||
const x0 = 1000, y0 = 1000;
|
||||
const states = { 'tiny-UT': [[[[x0, y0], [x0 + s, y0], [x0 + s, y0 + s], [x0, y0 + s], [x0, y0]]]] };
|
||||
const cx = x0 + s / 2, cy = y0 + s / 2;
|
||||
const pins = [];
|
||||
for (let k = 0; k < 4; k++) pins.push({ id: `tp${k}`, x: cx, y: cy, stateId: 'tiny-UT' });
|
||||
return { pins, states };
|
||||
}
|
||||
15
package.json
15
package.json
|
|
@ -225,6 +225,7 @@
|
|||
"check:selector-fields:self-test": "node internal/check-selector-fields.mjs --self-test",
|
||||
"check:packet-strings-parity": "node internal/check-packet-strings-parity.mjs",
|
||||
"check:packet-variants": "node internal/check-packet-variants.mjs",
|
||||
"check:pins-in-state": "node library/packet/scripts/check-pins-in-state.mjs",
|
||||
"check:label-key-coverage": "node internal/check-label-key-coverage.mjs",
|
||||
"check:host-locale-fetch": "node internal/check-host-locale-fetch.mjs",
|
||||
"check:packet-a1-structure": "node internal/check-packet-a1-structure.mjs",
|
||||
|
|
@ -352,8 +353,8 @@
|
|||
"dev": "bun x vite",
|
||||
"fix:sri": "node internal/fix-sri.mjs",
|
||||
"gallery": "node internal/generate-gallery.mjs",
|
||||
"gates:post": "node internal/run-parallel.mjs --label=post -- check:capability-derived check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:zoning-declared check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:script-flags check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:auto-engine check:engine-contract check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:no-silent-engine:self-test check:regions-uses-library check:shadow-css-parses check:square-map-controls check:preset-config-merge check:instance-config-keys check:frozen-labels check:plural-forms check:region-fill-repaint check:region-paint-single-source check:poster-print-fallback check:posters-exportable check:country-names check:reduced-motion check:no-dead-library check:universal-props check:readme-size check:tree-relative check:leaf-hints check:snippet-engine check:snippet-engine:self-test check:embed-base check:link-coverage check:no-public-binds check:shared-reached check:tree-weight check:no-fleet-names:self-test check:pin-hint-floor:self-test check:poster-sizes:self-test check:shell-header:self-test check:preview-flag-dev-only:self-test",
|
||||
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:gospel-set-fit check:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:i18n-never-translated check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:board-map-options check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:india-states check:batch-selection-export check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-internal-role-nouns check:poster-fetch-guard check:people-group-records check:sentence-slots check:popup-href-escaped check:popup-pan-controls check:country-map-refetch check:pane-config-reforward check:viewport-meta check:viewport-meta:self-test check:label-key-coverage check:poster-browser-equiv check:poster-render-pinned check:country-sheet-one-path check:country-sheet-one-path:self-test check:side-lanes check:side-lanes:self-test check:poster-section check:poster-section:self-test check:posterverse-shelf check:host-locale-fetch check:print-frame-reclaim check:click-sheet check:coverage-consistent:chained check:coverage-consistent:poison check:dashboard-filter-a11y check:filter-multivalue check:hover-channel check:kit-options-reachable check:layout-key-namespaced check:legend-mirrors check:map-unmount-race check:no-conflict-markers check:series-isolation check:snapshot-guard check:stale-sweep-claim check:surfaces-read-the-tier check:translator-contacts check:wall-wysiwyg check:wall-print-watch check:wall-no-bg-photos check:wall-pan check:template-i18n check:list-pattern-doc check:shadow-style-labels-unique check:grid-image-loading check:retry-fetch check:panel-sheets check:poster-wall-host-seams check:poster-design-catalogue check:wall-photo-weight check:no-fleet-names check:pin-hint-floor check:poster-sizes check:upg-count check:shell-header check:pg-name-gate check:preview-flag-dev-only",
|
||||
"gates:post": "node internal/run-parallel.mjs --label=post -- check:capability-derived check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:zoning-declared check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:script-flags check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:auto-engine check:engine-contract check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:no-silent-engine:self-test check:regions-uses-library check:shadow-css-parses check:square-map-controls check:preset-config-merge check:instance-config-keys check:frozen-labels check:plural-forms check:region-fill-repaint check:region-paint-single-source check:poster-print-fallback check:posters-exportable check:country-names check:reduced-motion check:no-dead-library check:universal-props check:readme-size check:tree-relative check:leaf-hints check:snippet-engine check:snippet-engine:self-test check:embed-base check:link-coverage check:no-public-binds check:shared-reached check:tree-weight check:no-fleet-names:self-test check:pin-hint-floor:self-test check:poster-sizes:self-test check:shell-header:self-test check:preview-flag-dev-only:self-test check:poster-type-registry:self-test",
|
||||
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:gospel-set-fit check:describe-group check:one-scroll-container check:packet-charts check:pins-in-state check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:i18n-never-translated check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:board-map-options check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:india-states check:batch-selection-export check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-internal-role-nouns check:poster-fetch-guard check:people-group-records check:sentence-slots check:popup-href-escaped check:popup-pan-controls check:country-map-refetch check:pane-config-reforward check:viewport-meta check:viewport-meta:self-test check:label-key-coverage check:poster-browser-equiv check:poster-render-pinned check:country-sheet-one-path check:country-sheet-one-path:self-test check:side-lanes check:side-lanes:self-test check:poster-section check:poster-section:self-test check:posterverse-shelf check:host-locale-fetch check:print-frame-reclaim check:click-sheet check:coverage-consistent:chained check:coverage-consistent:poison check:dashboard-filter-a11y check:filter-multivalue check:hover-channel check:kit-options-reachable check:layout-key-namespaced check:legend-mirrors check:map-unmount-race check:no-conflict-markers check:series-isolation check:snapshot-guard check:stale-sweep-claim check:surfaces-read-the-tier check:translator-contacts check:wall-wysiwyg check:wall-print-watch check:wall-no-bg-photos check:wall-pan check:template-i18n check:list-pattern-doc check:shadow-style-labels-unique check:grid-image-loading check:retry-fetch check:panel-sheets check:poster-wall-host-seams check:poster-design-catalogue check:wall-photo-weight check:no-fleet-names check:pin-hint-floor check:poster-sizes check:upg-count check:shell-header check:pg-name-gate check:preview-flag-dev-only check:poster-type-registry",
|
||||
"gen:iso3-to-alpha2": "node library/scripts/generate-iso3-to-alpha2.mjs",
|
||||
"internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs",
|
||||
"library-index": "node internal/generate-library-index.mjs",
|
||||
|
|
@ -423,7 +424,9 @@
|
|||
"check:poster-section": "node plugin-designs/country-posters/scripts/check-poster-section.mjs",
|
||||
"check:poster-section:self-test": "node plugin-designs/country-posters/scripts/check-poster-section.mjs --self-test",
|
||||
"check:side-lane-photo-pixels": "node plugin-designs/country-posters/scripts/check-side-lane-photo-pixels.mjs",
|
||||
"check:side-lane-photo-pixels:self-test": "node plugin-designs/country-posters/scripts/check-side-lane-photo-pixels.mjs --self-test"
|
||||
"check:side-lane-photo-pixels:self-test": "node plugin-designs/country-posters/scripts/check-side-lane-photo-pixels.mjs --self-test",
|
||||
"check:poster-type-registry": "node internal/check-poster-type-registry.mjs",
|
||||
"check:poster-type-registry:self-test": "node internal/check-poster-type-registry.mjs --self-test"
|
||||
},
|
||||
"//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.",
|
||||
|
|
@ -471,7 +474,7 @@
|
|||
"check:coverage-consistent": "the gospel-resources surfaces must agree: no resource on an ISO RESERVED code (und/mul/mis/zxx), every resourced code NAMED, table reach recounted off the pins, no invented URL, tiers reconcile. Static, sub-second. Self-test: `bun run check:coverage-consistent:poison`, all 12 clauses (A-L) must fail. Clause L is the one that is not about consistency: it fails when the link-verification record goes stale, because 'link checked' is a claim about the present tense. Written after the same drift defect was found by hand three times.",
|
||||
"check:csv-shape": "the export must OPEN as a table: header on line 0, every row matching. Run the `check:csv-shape:self-test` script for the NEGATIVE test (forces the provenance comment back to line 0 IN MEMORY and requires this gate to REJECT it) — the header used to claim 'NEGATIVE-TESTED' with no way to re-run it.",
|
||||
"check:poster-sizes": "the shared poster-size ladder (library/constants/posterSizes.js) is right: picker order = ladder, dot-free keys, portrait only, the sheets glue up to the finished size, photo slots on exactly 66x66 + 60x96, paper prints and the rest go to a print-shop PDF, every alias resolves, paperFor() picks Letter or A4 by region. Run the `check:poster-sizes:self-test` script for the NEGATIVE test",
|
||||
"check:preview-flag-dev-only": "the flag that previews withheld content (globalThis.DXM_PREVIEW_UNRELEASED, e.g. the whole-India poster) never reaches a built bundle: no file under public/ assigns it in any spelling a minifier writes, and every source assignment is behind import.meta.env.DEV (set only by the Vite dev server, the pilot's hot-reload preview). The built tree goes to the research landing and DX Life's /dx-maps, where the flag would show India publicly. Run the `check:preview-flag-dev-only:self-test` script for the NEGATIVE test (2 planted defects).",
|
||||
"check:preview-flag-dev-only": "the flag that previews withheld content (globalThis.DXM_PREVIEW_UNRELEASED, e.g. the whole-India poster) never reaches a built bundle: NO file under public/ names it at all (a setter would turn the preview on; a read would mean the withheld path itself shipped), and every source line that reads OR writes it carries import.meta.env.DEV (set only by the Vite dev server, the pilot's hot-reload preview). The built tree goes to the research landing and DX Life's /dx-maps, where the flag would show India publicly. Run the `check:preview-flag-dev-only:self-test` script for the NEGATIVE test (a built setter, a built read, an unguarded source setter and an unguarded source read).",
|
||||
"check:pin-hint-floor": "the pin-hint pill sits at the map's bottom on a page that scrolls (the fold is not an obstacle there) and is still lifted above the fold on a full-screen page; the legend sheet and the licence credit still lift it; PinHint.vue takes its floor from pinHintFloor.js. Run the `check:pin-hint-floor:self-test` script for the NEGATIVE test",
|
||||
"check:no-fleet-names": "tracked source (not data/, public/ or the translation catalogues) names no build-team member, attributes no request to a user and carries no dated QA-round tag; an allowance is a named LINE, never a whole file. Run the `check:no-fleet-names:self-test` script for the NEGATIVE test",
|
||||
"check:dashboard-filter-a11y": "the dashboard's cross-filter is operable and audible: every filter chip is a <button type=\"button\"> with an aria-label naming the ACTION (a filter that can be set by keyboard must be clearable by keyboard), the shell carries exactly ONE always-rendered polite role=\"status\" region so a brush that changes a number far from the control is announced once rather than six times or not at all, and no resize handle's accessible name is a raw English literal. AST over the parsed templates plus the built bundle — no browser.",
|
||||
|
|
@ -574,6 +577,7 @@
|
|||
"check:packet-render:expect-funnel": "check:packet-render with the page-2 FUNNEL declared REQUIRED. Exists because of a real near-miss: a merge resolved with `git checkout --ours` deleted the printPacket call site that supplies the funnel data (60 lines) and EVERY GATE STAYED GREEN — it had to, because the funnel page is OPTIONAL by design (buildPacket omits it when the arg is null), so a packet without page 2 is not malformed, just smaller. Always requiring it would be the wrong fix: the 11 pre-funnel laos packets and any host with no funnel data would go red, and a false red costs as much as a false green. So the CALLER, which knows whether this packet was built with funnel data, declares it — an absence is then either declared (no flag) or a failure (flag passed, page missing), never a silent nothing. Pass it for any packet from a profile that wires packetFunnel(). Browser gate.",
|
||||
"check:packet-strings-parity": "the PACKET catalog (packet-strings.json) must be whole in every language: all string keys, all 12 prayer points with every field, all 12 wheel rhythms. check:i18n-parity covers library/i18n/locales only — six gates READ this file and none checked it. Asserts PRESENCE, not translatedness, with point4_passage declared-absent on record.",
|
||||
"check:packet-variants": "the poster wall's Prayer List prints the PACKET (not the columnar list), and every variant it offers exists in packet-render's VARIANTS. A variant may drop fixed pages but never people: lightweight keeps every group. By the UPG-100 owner; mutation-tested. Headless; in gates:pre.",
|
||||
"check:pins-in-state": "library/packet/pins-in-state.js places whole-India poster pins so no two overlap AND every pin stays inside its own ADM1 state polygon, at the 60x96in sheet scale (2500x4000px, PIN_R 8). A deterministic repulsion relaxation (imitating poster-render.js, read not called) with the map-rect clamp replaced by a per-state multipolygon clamp: a pin pushed off its land is pulled to the nearest point just inside, holes and multi-part states (islands/coasts) honoured. Runs on the real India 855-pin data when present, else a synthetic 60x96 fixture (floor 200 pins, no vacuous pass); asserts 0 pins outside their state and 0 overlaps that are not NAMED in `infeasible` (a too-small state is reported, never silently stacked), plus a hole/island arm and a poison arm. Node only, headless, deterministic.",
|
||||
"check:label-key-coverage": "node internal/check-label-key-coverage.mjs",
|
||||
"check:host-locale-fetch": "node internal/check-host-locale-fetch.mjs",
|
||||
"check:packet-a1-structure": "the prayer packet is built to its specified structure, read page by page from each sheet's CONTENT: full detail = map, triangle, life calendar, % evangelical, profiles, bounded set, centered set, wheel, points; lightweight = map, triangle, profiles, wheel, points. Page 1 places a map-only vertical sheet on the packet's own margin and says it is one of the UPG 100 prayer lists only when a count is supplied; page 2 is one inverted triangle whose packets tier and \"- N\" steps appear only where true; the wheel carries the Metacamp Module 1 PDF's colours and opens at nine o'clock; all 28 legend zones resolve, Deaf included.",
|
||||
|
|
@ -695,7 +699,8 @@
|
|||
"check:poster-section": "the reusable poster section (<dxm-poster-section>): the type registry has 'country'; DX Life's /regions slugs (and every other spelling of a region) each show their countries, every country once; a withheld whole-country poster stays withheld through the type; the element's attributes map to its config; PosterSection.vue injects every name its template uses.",
|
||||
"check:poster-section:self-test": "a registry without 'country', a region key without \"&\" → \"and\", and a section that uses a name it does not inject must each make check:poster-section fail.",
|
||||
"check:side-lane-photo-pixels": "BROWSER gate (browser seat; elsewhere 75 NOT MEASURED): the 60x96 side-lane sheet rasterised by poster-raster.js, measured in pixels at every photo slot. A data: photo draws; every photo on its own host stays flat, never the broken-image glyph an external <image> rasterises as.",
|
||||
"check:side-lane-photo-pixels:self-test": "the same raster WITHOUT stripExternalImages must leave the external photo slots NOT flat (the glyph), else the gate proves nothing."
|
||||
"check:side-lane-photo-pixels:self-test": "the same raster WITHOUT stripExternalImages must leave the external photo slots NOT flat (the glyph), else the gate proves nothing.",
|
||||
"check:poster-type-registry": "the poster type registry is ONE per page: every bundle inlines its own copy of posterTypes.js and <dxm-poster-section> is defined by whichever bundle loads first, so with two independent copies loaded side by side a type registered through one is found through the other; the built-in country is listed once; a name registered twice keeps the first; every registration is announced on window (dxm-poster-type-registered) and PosterSectionView re-resolves on it. Run the `check:poster-type-registry:self-test` script for the NEGATIVE test (a per-bundle registry fails)."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@capacitor/android": "^7.6.9",
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <country-posters-map> custom element. -->
|
||||
<script src="../country-posters.js" integrity="sha384-VJGval7ZtjxCT7u6crut+uOIvHluTvmvMs7cJiHOeuTcudewlJfCC6y5kwI/XAdp" crossorigin="anonymous"></script>
|
||||
<script src="../country-posters.js" integrity="sha384-wo556zVsMm0BC5dqD9Grf/T03snHhZwLb7V61TjczWpRiGBE7tSEyCXkfrIs+zC+" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <country-posters-map> custom element. -->
|
||||
<script src="../country-posters.js" integrity="sha384-VJGval7ZtjxCT7u6crut+uOIvHluTvmvMs7cJiHOeuTcudewlJfCC6y5kwI/XAdp" crossorigin="anonymous"></script>
|
||||
<script src="../country-posters.js" integrity="sha384-wo556zVsMm0BC5dqD9Grf/T03snHhZwLb7V61TjczWpRiGBE7tSEyCXkfrIs+zC+" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <country-posters-map> custom element. -->
|
||||
<script src="../country-posters.js" integrity="sha384-VJGval7ZtjxCT7u6crut+uOIvHluTvmvMs7cJiHOeuTcudewlJfCC6y5kwI/XAdp" crossorigin="anonymous"></script>
|
||||
<script src="../country-posters.js" integrity="sha384-wo556zVsMm0BC5dqD9Grf/T03snHhZwLb7V61TjczWpRiGBE7tSEyCXkfrIs+zC+" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -67,7 +67,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <country-posters-map> custom element. -->
|
||||
<script src="../country-posters.js" integrity="sha384-VJGval7ZtjxCT7u6crut+uOIvHluTvmvMs7cJiHOeuTcudewlJfCC6y5kwI/XAdp" crossorigin="anonymous"></script>
|
||||
<script src="../country-posters.js" integrity="sha384-wo556zVsMm0BC5dqD9Grf/T03snHhZwLb7V61TjczWpRiGBE7tSEyCXkfrIs+zC+" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -232,7 +232,7 @@
|
|||
"bundle": "country-posters",
|
||||
"scriptUrl": "./country-posters/country-posters.js",
|
||||
"output": "dx-maps/country-posters/country-posters.js",
|
||||
"integrity": "sha384-VJGval7ZtjxCT7u6crut+uOIvHluTvmvMs7cJiHOeuTcudewlJfCC6y5kwI/XAdp",
|
||||
"integrity": "sha384-wo556zVsMm0BC5dqD9Grf/T03snHhZwLb7V61TjczWpRiGBE7tSEyCXkfrIs+zC+",
|
||||
"elements": [
|
||||
"country-posters-map"
|
||||
],
|
||||
|
|
@ -255,8 +255,8 @@
|
|||
"scriptUrl": "./country-posters/country-posters.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./country-posters/countries-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-VJGval7ZtjxCT7u6crut+uOIvHluTvmvMs7cJiHOeuTcudewlJfCC6y5kwI/XAdp\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>",
|
||||
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-VJGval7ZtjxCT7u6crut+uOIvHluTvmvMs7cJiHOeuTcudewlJfCC6y5kwI/XAdp\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-wo556zVsMm0BC5dqD9Grf/T03snHhZwLb7V61TjczWpRiGBE7tSEyCXkfrIs+zC+\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>",
|
||||
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-wo556zVsMm0BC5dqD9Grf/T03snHhZwLb7V61TjczWpRiGBE7tSEyCXkfrIs+zC+\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -272,7 +272,7 @@
|
|||
"scriptUrl": "./country-posters/country-posters.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./country-posters/country-poster-wall/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-VJGval7ZtjxCT7u6crut+uOIvHluTvmvMs7cJiHOeuTcudewlJfCC6y5kwI/XAdp\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-wo556zVsMm0BC5dqD9Grf/T03snHhZwLb7V61TjczWpRiGBE7tSEyCXkfrIs+zC+\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -288,8 +288,8 @@
|
|||
"scriptUrl": "./country-posters/country-posters.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./country-posters/country-posters-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-VJGval7ZtjxCT7u6crut+uOIvHluTvmvMs7cJiHOeuTcudewlJfCC6y5kwI/XAdp\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>",
|
||||
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-VJGval7ZtjxCT7u6crut+uOIvHluTvmvMs7cJiHOeuTcudewlJfCC6y5kwI/XAdp\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-wo556zVsMm0BC5dqD9Grf/T03snHhZwLb7V61TjczWpRiGBE7tSEyCXkfrIs+zC+\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>",
|
||||
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-wo556zVsMm0BC5dqD9Grf/T03snHhZwLb7V61TjczWpRiGBE7tSEyCXkfrIs+zC+\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -305,7 +305,7 @@
|
|||
"scriptUrl": "./country-posters/country-posters.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./country-posters/poster-section/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-VJGval7ZtjxCT7u6crut+uOIvHluTvmvMs7cJiHOeuTcudewlJfCC6y5kwI/XAdp\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<country-posters-map profile-config='{\"profile\":\"poster-section\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-wo556zVsMm0BC5dqD9Grf/T03snHhZwLb7V61TjczWpRiGBE7tSEyCXkfrIs+zC+\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<country-posters-map profile-config='{\"profile\":\"poster-section\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -232,7 +232,7 @@
|
|||
"bundle": "country-posters",
|
||||
"scriptUrl": "./country-posters/country-posters.js",
|
||||
"output": "dx-maps/country-posters/country-posters.js",
|
||||
"integrity": "sha384-VJGval7ZtjxCT7u6crut+uOIvHluTvmvMs7cJiHOeuTcudewlJfCC6y5kwI/XAdp",
|
||||
"integrity": "sha384-wo556zVsMm0BC5dqD9Grf/T03snHhZwLb7V61TjczWpRiGBE7tSEyCXkfrIs+zC+",
|
||||
"elements": [
|
||||
"country-posters-map"
|
||||
],
|
||||
|
|
@ -255,8 +255,8 @@
|
|||
"scriptUrl": "./country-posters/country-posters.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./country-posters/countries-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-VJGval7ZtjxCT7u6crut+uOIvHluTvmvMs7cJiHOeuTcudewlJfCC6y5kwI/XAdp\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>",
|
||||
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-VJGval7ZtjxCT7u6crut+uOIvHluTvmvMs7cJiHOeuTcudewlJfCC6y5kwI/XAdp\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-wo556zVsMm0BC5dqD9Grf/T03snHhZwLb7V61TjczWpRiGBE7tSEyCXkfrIs+zC+\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>",
|
||||
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-wo556zVsMm0BC5dqD9Grf/T03snHhZwLb7V61TjczWpRiGBE7tSEyCXkfrIs+zC+\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -272,7 +272,7 @@
|
|||
"scriptUrl": "./country-posters/country-posters.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./country-posters/country-poster-wall/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-VJGval7ZtjxCT7u6crut+uOIvHluTvmvMs7cJiHOeuTcudewlJfCC6y5kwI/XAdp\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-wo556zVsMm0BC5dqD9Grf/T03snHhZwLb7V61TjczWpRiGBE7tSEyCXkfrIs+zC+\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -288,8 +288,8 @@
|
|||
"scriptUrl": "./country-posters/country-posters.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./country-posters/country-posters-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-VJGval7ZtjxCT7u6crut+uOIvHluTvmvMs7cJiHOeuTcudewlJfCC6y5kwI/XAdp\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>",
|
||||
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-VJGval7ZtjxCT7u6crut+uOIvHluTvmvMs7cJiHOeuTcudewlJfCC6y5kwI/XAdp\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-wo556zVsMm0BC5dqD9Grf/T03snHhZwLb7V61TjczWpRiGBE7tSEyCXkfrIs+zC+\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>",
|
||||
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-wo556zVsMm0BC5dqD9Grf/T03snHhZwLb7V61TjczWpRiGBE7tSEyCXkfrIs+zC+\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -305,7 +305,7 @@
|
|||
"scriptUrl": "./country-posters/country-posters.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./country-posters/poster-section/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-VJGval7ZtjxCT7u6crut+uOIvHluTvmvMs7cJiHOeuTcudewlJfCC6y5kwI/XAdp\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<country-posters-map profile-config='{\"profile\":\"poster-section\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-wo556zVsMm0BC5dqD9Grf/T03snHhZwLb7V61TjczWpRiGBE7tSEyCXkfrIs+zC+\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<country-posters-map profile-config='{\"profile\":\"poster-section\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
Loading…
Reference in a new issue