Merge studio/mirror and rebuild: library/ changed, so the bundle had to follow
studio/mirror (c85eeee7) rebuilt every bundle and touched library/ —
PinHint, LegendDesktop, LegendTools, MapControlMenu, ShareButton, PosterDialog
and others — so unlike the previous merge this one genuinely required rebuilding
my bundle rather than just re-hashing it. Checked rather than assumed: the
decision of whether to rebuild turns on `git diff --name-only <base> <main> |
grep '^library/'`, and last time that was empty and this time it is not.
ALL SIX CONFLICTS WERE DERIVED ARTIFACTS — my bundle, my three leaf HTMLs and the
two manifests — and ZERO source files conflicted. Resolved to main's freshly built
versions and then regenerated from the merged source, because picking a side of a
generated file is meaningless: neither side is authoritative, the source is.
VERIFIED BY HASHING, which is the step whose absence shipped a blank page three
commits ago. fix:sri rewrote files this time (it reported nothing to do last time
only because `tree` had already fixed them), so the summary alone would have been
even less informative. Recomputed sha384 over the actual bundle: upg-100-list,
upg-100-mfe and poster-wall all MATCH, and a whole-tree sweep is 57 integrity
attributes with 0 mismatches.
Used fix:sri and NOT tree, per the gate's own advice — tree re-reads .vue source
and on a shared branch can bake in another agent's half-finished work. studio had
already regenerated the pages, so there was nothing to regenerate.
BLAST RADIUS: the only bundle differing from main is mine. Pins-first source
confirmed intact after the merge (boundaryPromises, paintPinsEarly, the deferred
boundary await).
This commit is contained in:
commit
a6273dc905
150 changed files with 1625 additions and 1174 deletions
|
|
@ -3,10 +3,12 @@
|
|||
"title": "Gospel access by province — India and China",
|
||||
"why": "These two countries hold 47.9% of Doxa's roster (855 + 154 of 2,107). No other country is close; the third is Sudan at 105. A national fill cannot answer \"where would a team go\" for half the map.",
|
||||
"generated": "2026-09-26",
|
||||
"provinces_with_groups": 44,
|
||||
"provinces_available": 46,
|
||||
"groups_assigned": 998,
|
||||
"groups_unassigned": 11,
|
||||
"provinces_with_groups": 46,
|
||||
"provinces_available": 48,
|
||||
"groups_assigned": 1005,
|
||||
"groups_unassigned": 4,
|
||||
"india_note": "India is read from the COMPLETE ADM1 (36 states, geoBoundaries via DataMeet India, CC BY 2.5 IN). The first build of this table read gb-IND-ADM1.geojson, which is the UPG-100 list's curated 34-unit partition, and eleven groups came out with no province because they stood in states that partition does not draw. Switching the file moved 998 assigned to 1,005 and 44 province rows to 46, and turned the province names into India's own states.",
|
||||
"unassigned_note": "Four remain, each one legible rather than lost. Great Andamanese and the Sentinelese sit on small islands whose simplified coastline the point falls just outside — Andaman and Nicobar IS in the file, so this is geometry precision, not a missing province. Jing and Pana are on China's Vietnam and Laos borders, outside the twelve-province subset. They are listed in `unassigned` with their coordinates.",
|
||||
"china_note": "China's ADM1 file carries 12 provinces, not 31+. Measured rather than assumed: 152 of 154 Chinese groups fall inside them and all twelve are used, so the file is cut to the provinces where the unreached peoples are.",
|
||||
"ways_in_are_country_level": "The ways in are chosen for the COUNTRY, and going below the border is what exposes the cost of that. Arunachal Pradesh, in the far north-east, is offered TELUGU — a South Indian language — because the curated bridge table names it for those groups and a curated bridge outranks the computed one. India's computed default is Hindi (41%, official), which is defensible nationally and still wrong for much of the north-east. This table does not fix that: it makes it visible. Until a bridge can be chosen per province, read the ways-in column as \"a language with Scripture and film somewhere in this country\", not as a recommendation for this place.",
|
||||
"approximate": "A province is assigned by point-in-polygon on the group's coordinate, and that coordinate marks where a PEOPLE is found, not a settlement — 108 groups across the roster even share a coordinate with another group. Near a border the point can fall on the wrong side. Every row is flagged approximate and the unassigned are counted, never dropped. No surface may present a province as a fact about a group."
|
||||
|
|
@ -279,8 +281,8 @@
|
|||
"country": "India",
|
||||
"iso3": "IND",
|
||||
"province": "Mahārāshtra",
|
||||
"groups": 53,
|
||||
"people": 2858050,
|
||||
"groups": 54,
|
||||
"people": 2860150,
|
||||
"zero_access_groups": 0,
|
||||
"zero_access_people": 0,
|
||||
"zero_access_examples": "",
|
||||
|
|
@ -467,6 +469,18 @@
|
|||
"ways_in": "",
|
||||
"approximate": true
|
||||
},
|
||||
{
|
||||
"country": "India",
|
||||
"iso3": "IND",
|
||||
"province": "Puducherry",
|
||||
"groups": 5,
|
||||
"people": 41625,
|
||||
"zero_access_groups": 0,
|
||||
"zero_access_people": 0,
|
||||
"zero_access_examples": "",
|
||||
"ways_in": "",
|
||||
"approximate": true
|
||||
},
|
||||
{
|
||||
"country": "India",
|
||||
"iso3": "IND",
|
||||
|
|
@ -566,9 +580,21 @@
|
|||
{
|
||||
"country": "India",
|
||||
"iso3": "IND",
|
||||
"province": "Puducherry",
|
||||
"province": "Lakshadweep",
|
||||
"groups": 1,
|
||||
"people": 20500,
|
||||
"people": 74500,
|
||||
"zero_access_groups": 0,
|
||||
"zero_access_people": 0,
|
||||
"zero_access_examples": "",
|
||||
"ways_in": "",
|
||||
"approximate": true
|
||||
},
|
||||
{
|
||||
"country": "India",
|
||||
"iso3": "IND",
|
||||
"province": "Dādra and Nagar Haveli and Damān and Diu",
|
||||
"groups": 1,
|
||||
"people": 125,
|
||||
"zero_access_groups": 0,
|
||||
"zero_access_people": 0,
|
||||
"zero_access_examples": "",
|
||||
|
|
@ -589,13 +615,6 @@
|
|||
}
|
||||
],
|
||||
"unassigned": [
|
||||
{
|
||||
"country": "India",
|
||||
"iso3": "IND",
|
||||
"people_group": "Bharwad",
|
||||
"lat": 20.322175,
|
||||
"lng": 72.968458
|
||||
},
|
||||
{
|
||||
"country": "India",
|
||||
"iso3": "IND",
|
||||
|
|
@ -603,34 +622,6 @@
|
|||
"lat": 12.211728,
|
||||
"lng": 92.833491
|
||||
},
|
||||
{
|
||||
"country": "India",
|
||||
"iso3": "IND",
|
||||
"people_group": "Indo-French",
|
||||
"lat": 11.930182,
|
||||
"lng": 79.797144
|
||||
},
|
||||
{
|
||||
"country": "India",
|
||||
"iso3": "IND",
|
||||
"people_group": "Indo-Vietnamese",
|
||||
"lat": 11.912456,
|
||||
"lng": 79.815389
|
||||
},
|
||||
{
|
||||
"country": "India",
|
||||
"iso3": "IND",
|
||||
"people_group": "Laccadive",
|
||||
"lat": 10.0675,
|
||||
"lng": 73.6422
|
||||
},
|
||||
{
|
||||
"country": "India",
|
||||
"iso3": "IND",
|
||||
"people_group": "Sagharias",
|
||||
"lat": 20.71815,
|
||||
"lng": 70.988939
|
||||
},
|
||||
{
|
||||
"country": "India",
|
||||
"iso3": "IND",
|
||||
|
|
@ -638,20 +629,6 @@
|
|||
"lat": 11.557662,
|
||||
"lng": 92.267154
|
||||
},
|
||||
{
|
||||
"country": "India",
|
||||
"iso3": "IND",
|
||||
"people_group": "Vettalaikarans",
|
||||
"lat": 11.916308,
|
||||
"lng": 79.807019
|
||||
},
|
||||
{
|
||||
"country": "India",
|
||||
"iso3": "IND",
|
||||
"people_group": "Vettiyan",
|
||||
"lat": 11.91386,
|
||||
"lng": 79.814472
|
||||
},
|
||||
{
|
||||
"country": "China",
|
||||
"iso3": "CHN",
|
||||
|
|
|
|||
|
|
@ -234,6 +234,46 @@ snippets (leave it empty for relative URLs that work wherever the folder is drop
|
|||
|
||||
---
|
||||
|
||||
## Hosting the dx-maps folder: caching, compression, CORS
|
||||
|
||||
**Why it matters.** On a cold load a map's layers draw about 10–17 s after the page opens (measured
|
||||
on the UPG-100 list page). The bundle and the map engine take under 2 s of that; the rest is basemap
|
||||
tiles and data files such as the country-boundary file (1,866 KB raw, 597 KB gzipped). Compression
|
||||
cuts every visit; caching means a returning reader doesn't download the same files again.
|
||||
|
||||
**Every file in `dx-maps/` has a fixed name today.** Nothing is content-hashed, so a new release
|
||||
overwrites files in place: bundles (`<bundle>/<bundle>.js`), their pages (`index.html`) and
|
||||
`manifest.json` change whenever the build changes them. Images, fonts and data files change rarely,
|
||||
but under the same names.
|
||||
|
||||
| Files | `Cache-Control` | Why |
|
||||
|---|---|---|
|
||||
| everything in `dx-maps/`, as it ships today | `public, max-age=86400, stale-while-revalidate=604800`, with an `ETag` | a revisit revalidates cheaply and never re-downloads unchanged bytes, and a release shows up within a day |
|
||||
| content-hashed names, if you add them (e.g. `world-adm0.3f9a1c2e.geojson`) | `public, max-age=31536000, immutable` | a changed file gets a new name, so a year-long cache is always safe |
|
||||
|
||||
**Never send `immutable` for a fixed name**: an update would stay invisible for a year.
|
||||
|
||||
**Compress:** serve `br` or `gzip`, and give data files a JSON content type (`application/json`, or
|
||||
`application/geo+json` for `.geojson`) so CDNs compress them. Many servers send `.geojson` as
|
||||
`application/octet-stream`, which CDNs don't compress.
|
||||
|
||||
**CORS for script embeds:** every `embed.script` snippet loads the bundle with an `integrity` hash
|
||||
and `crossorigin="anonymous"`. For that to work on another site, the host must send
|
||||
`Access-Control-Allow-Origin: *`; without it the browser can't check the hash across origins and refuses
|
||||
the script. iframe embeds need no CORS.
|
||||
|
||||
On Cloudflare Pages or Netlify, a `_headers` file at the folder's root sets the CORS and cache headers:
|
||||
|
||||
```
|
||||
/*
|
||||
Access-Control-Allow-Origin: *
|
||||
Cache-Control: public, max-age=86400, stale-while-revalidate=604800
|
||||
```
|
||||
|
||||
Check the content type your host gives `.geojson` files (`curl -sI <url>`) before relying on compression.
|
||||
|
||||
---
|
||||
|
||||
## Compose maps into a SPA (the app shell)
|
||||
|
||||
Several maps + one layout = a single-page app, served from the same `public/` tree.
|
||||
|
|
|
|||
|
|
@ -81,6 +81,24 @@ for (const rel of wanted) {
|
|||
console.log(` ok ${rel.padEnd(42)} ${String(bytes).padStart(9)} bytes`)
|
||||
}
|
||||
|
||||
/* ★ THE MAP AND THE TABLE MUST READ THE SAME GEOMETRY (2026-09-26). The province layer is
|
||||
coloured in the leaf and counted by scripts/build-gospel-by-province.mjs, from two independent
|
||||
mentions of the same filenames — so a change to one silently draws provinces that disagree with
|
||||
the rows describing them. It already happened in the other direction: both read
|
||||
gb-IND-ADM1.geojson, the UPG-100 list's curated 34-unit partition, when what this table means
|
||||
by "province" is a state. Staging alone cannot catch it; both files are staged. */
|
||||
const boundaryMentions = (src, re) => [...src.matchAll(re)].map((m) => m[m.length - 1]).sort()
|
||||
try {
|
||||
const leafSrc = readFileSync(LEAF, 'utf8')
|
||||
const buildSrc = readFileSync(resolve(ROOT, 'plugin-designs/gospel-resources/scripts/build-gospel-by-province.mjs'), 'utf8')
|
||||
const inLeaf = boundaryMentions(leafSrc, /(?:IND|CHN): '(gb-[A-Z]{3}-ADM1[^']*\.geojson)'/g)
|
||||
const inBuild = boundaryMentions(buildSrc, /file: '(gb-[A-Z]{3}-ADM1[^']*\.geojson)'/g)
|
||||
if (!inLeaf.length || !inBuild.length) missing.push('province boundary filenames not found in the leaf or the builder — this arm would pass vacuously')
|
||||
else if (JSON.stringify(inLeaf) !== JSON.stringify(inBuild)) {
|
||||
missing.push(`the map draws provinces from [${inLeaf.join(', ')}] while the table counts them from [${inBuild.join(', ')}]`)
|
||||
} else console.log(` ok province geometry agrees ${inLeaf.join(', ')}`)
|
||||
} catch (e) { missing.push(`could not compare province geometry: ${e.message}`) }
|
||||
|
||||
if (missing.length) {
|
||||
console.error(`GOSPEL-TABLES-STAGED: FAIL — ${missing.length} of ${wanted.length} asset(s) the map fetches are not in the tree:`)
|
||||
for (const m of missing) console.error(` ${m}`)
|
||||
|
|
|
|||
|
|
@ -58,6 +58,13 @@ const ALLOWED = new Map([
|
|||
'proper noun — Open Bible Stories is the publication\'s name (unfoldingWord), not a description'],
|
||||
['gospel.json:res.scriptureEarth',
|
||||
'proper noun — Scripture Earth is the organisation\'s name; the destination site is English'],
|
||||
// ── UNITS ───────────────────────────────────────────────────────────────────
|
||||
['tables.json:csv',
|
||||
'file-format name — "CSV" names the format the button downloads; the file extension is the same in every locale'],
|
||||
['tables.json:json',
|
||||
'file-format name — "JSON" names the format the button downloads; the file extension is the same in every locale'],
|
||||
['poster.json:dpi',
|
||||
'unit symbol — "DPI" is how print resolution is labelled in every one of these locales\' print dialogs; kept as-is by project decision (the poster-strings landing)'],
|
||||
// ── DRIVER-GATED, NOT RESOLVED ──────────────────────────────────────────────
|
||||
// ⚠ THESE ARE ALLOWED SO THE GATE CAN GO GREEN ON THE THINGS IT HAS ACTUALLY SETTLED, NOT
|
||||
// because the defect is fixed. "UUPGs" really is an English acronym wedged into an otherwise
|
||||
|
|
|
|||
67
internal/check-studio-rail.mjs
Normal file
67
internal/check-studio-rail.mjs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-studio-rail — the Plugin Studio's rail is laid out like plugin-designs/. Headless.
|
||||
*
|
||||
* The Driver's ask (2026-09-26): one FOLDER per BUNDLE with its ELEMENTS inside, bundles sorted by
|
||||
* capability, highest first. Runs the studio's REAL treeSections / compositionTrees / rowsFromManifest
|
||||
* (loaded through Vite, no browser) against the BUILT manifest and asserts:
|
||||
* - sections run CAP 5 → 1, and each bundle sits in the section of its HIGHEST capability, once;
|
||||
* - bundles within a section are in name order;
|
||||
* - a bundle with several elements is ONE folder holding all of them; a single-element bundle is its element;
|
||||
* - every studio row has exactly ONE canonical node (one identity per plugin; parts only target it).
|
||||
* EXIT 0 pass · 1 fail · 0 with SKIP when there is no built manifest
|
||||
*/
|
||||
import { readFileSync, existsSync } from 'node:fs'
|
||||
import { join, resolve, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const MANIFEST = join(ROOT, 'public', TREE_NAME, 'manifest.json')
|
||||
if (!existsSync(MANIFEST)) { console.log('STUDIO-RAIL: SKIP — no built manifest'); process.exit(0) }
|
||||
const { createServer } = await import('vite')
|
||||
const { default: vue } = await import('@vitejs/plugin-vue')
|
||||
const server = await createServer({ root: ROOT, configFile: false, logLevel: 'error', appType: 'custom',
|
||||
server: { middlewareMode: true, hmr: false, watch: null }, plugins: [vue({ customElement: true })],
|
||||
resolve: { alias: { '@map': join(ROOT, 'library') } }, define: { __DOXA_TREE_NAME__: JSON.stringify(TREE_NAME) } })
|
||||
globalThis.window = globalThis
|
||||
globalThis.localStorage = { getItem: () => null, setItem() {} }
|
||||
let bad = 0
|
||||
const fail = (m) => { bad++; console.log(' FAIL ' + m) }
|
||||
try {
|
||||
const comp = (await server.ssrLoadModule('/plugin-designs/plugin-studio/plugin-studio.vue')).default
|
||||
const manifest = JSON.parse(readFileSync(MANIFEST, 'utf8'))
|
||||
const ctx = { manifestBundles: manifest.bundles, tr: (k) => k, cfg: {}, base: '' }
|
||||
ctx.rows = comp.methods.rowsFromManifest.call(ctx, manifest)
|
||||
ctx.resolveRef = (ref, fb) => comp.methods.resolveRef.call(ctx, ref, fb)
|
||||
ctx.compositionTrees = comp.computed.compositionTrees.call(ctx)
|
||||
const secs = comp.computed.treeSections.call(ctx)
|
||||
const caps = secs.map((s) => s.cap)
|
||||
if (caps.join() !== [...caps].sort((a, b) => b - a).join()) fail(`sections not CAP 5 → 1: ${caps.join(',')}`)
|
||||
const byBundle = new Map()
|
||||
for (const r of ctx.rows) { if (!byBundle.has(r.bundle)) byBundle.set(r.bundle, []); byBundle.get(r.bundle).push(r) }
|
||||
const seen = new Map()
|
||||
for (const s of secs) {
|
||||
const tops = s.nodes.filter((n) => n.depth === 0)
|
||||
const names = tops.map((n) => (n.folder ? n.label : ctx.rows.find((r) => r.key === n.target)?.bundle))
|
||||
if (names.join() !== [...names].sort((a, b) => a.localeCompare(b)).join()) fail(`CAP ${s.cap}: bundles not in name order (${names.join(', ')})`)
|
||||
for (const [i, n] of tops.entries()) {
|
||||
const b = names[i]; const rows = byBundle.get(b) || []
|
||||
if (seen.has(b)) fail(`bundle ${b} appears twice (CAP ${seen.get(b)} and CAP ${s.cap})`)
|
||||
seen.set(b, s.cap)
|
||||
const top = Math.max(...rows.map((r) => r.cap))
|
||||
if (top !== s.cap) fail(`bundle ${b} (highest CAP ${top}) filed under CAP ${s.cap}`)
|
||||
if (rows.length > 1) {
|
||||
if (!n.folder) fail(`bundle ${b} has ${rows.length} elements but is not a folder`)
|
||||
const kids = s.nodes.filter((k) => k.parent === n.id && k.canonical).map((k) => k.target).sort()
|
||||
if (kids.join() !== rows.map((r) => r.key).sort().join()) fail(`folder ${b} holds ${kids.length} of its ${rows.length} elements`)
|
||||
} else if (n.folder) fail(`single-element bundle ${b} is shown as a folder`)
|
||||
}
|
||||
}
|
||||
for (const b of byBundle.keys()) if (!seen.has(b)) fail(`bundle ${b} is missing from the rail`)
|
||||
const canon = secs.flatMap((s) => s.nodes.filter((n) => n.canonical))
|
||||
for (const r of ctx.rows) { const c = canon.filter((n) => n.target === r.key).length; if (c !== 1) fail(`${r.key} has ${c} canonical nodes (want 1)`) }
|
||||
console.log(`STUDIO-RAIL: ${byBundle.size} bundle(s), ${ctx.rows.length} element(s), ${secs.length} capability section(s); folders: ${secs.flatMap((s) => s.nodes.filter((n) => n.folder).map((n) => n.label + '(' + n.kids + ')')).join(', ')}`)
|
||||
} finally { await server.close() }
|
||||
if (bad) { console.log(`STUDIO-RAIL: FAIL — ${bad} problem(s).`); process.exit(1) }
|
||||
console.log('STUDIO-RAIL: PASS — one folder per bundle with its elements inside, bundles grouped by their highest capability (5 → 1) and in name order, one identity per plugin.')
|
||||
|
|
@ -38,6 +38,12 @@
|
|||
* dropped from the custom element's shadow DOM (see WARNING.md).
|
||||
*/
|
||||
import { ref, inject, computed, onMounted, onBeforeUnmount, getCurrentInstance, nextTick } from 'vue'
|
||||
import { tStatic } from '@map/i18n/index.js'
|
||||
|
||||
// This component's chrome, read out of the catalogue. tStatic and not useI18n: these mount inside
|
||||
// custom elements that install no i18n plugin, and an unguarded useI18n() throws on the way in and
|
||||
// takes the subtree with it. The English stays as the fallback.
|
||||
const T = (key, en) => tStatic(key) || en
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useShadowStyles } from '../../composables/useShadowStyles.js'
|
||||
|
||||
|
|
@ -374,7 +380,7 @@ useShadowStyles(`
|
|||
<span class="pin-hint-pill">
|
||||
<span class="pin-hint-dot" aria-hidden="true"></span>
|
||||
{{ label }}
|
||||
<button class="pin-hint-x" type="button" aria-label="Dismiss hint" @click="dismiss">×</button>
|
||||
<button class="pin-hint-x" type="button" :aria-label="T('aria.dismissHint', 'Dismiss hint')" @click="dismiss">×</button>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
<script setup>
|
||||
import { ref, computed, onMounted, onBeforeUnmount, watch, inject, toRef } from 'vue'
|
||||
import { tStatic } from '@map/i18n/index.js'
|
||||
|
||||
// This component's chrome, read out of the catalogue. tStatic and not useI18n: these mount inside
|
||||
// custom elements that install no i18n plugin, and an unguarded useI18n() throws on the way in and
|
||||
// takes the subtree with it. The English stays as the fallback.
|
||||
const T = (key, en) => tStatic(key) || en
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useShadowStyles } from '../../composables/useShadowStyles.js'
|
||||
import { useLegendData } from '../../composables/legends/useLegendData.js'
|
||||
|
|
@ -357,7 +363,7 @@ onBeforeUnmount(() => {
|
|||
<!-- Collapsed pill — identical to STL's .stl-reopen -->
|
||||
<button v-else-if="isCollapsed"
|
||||
:class="['legend-reopen', { 'legend-reopen-light': !isDark }]"
|
||||
title="Expand legend"
|
||||
:title="T('aria.expandLegend', 'Expand legend')"
|
||||
@click="isCollapsed = false">
|
||||
<svg class="legend-reopen-icon" width="11" height="11" viewBox="0 0 12 12" fill="none">
|
||||
<path d="M2 4h8M2 6h8M2 8h5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>
|
||||
|
|
|
|||
|
|
@ -38,6 +38,12 @@ export function showsFlyTools(legendType) {
|
|||
* parent profile listens through mapStore).
|
||||
*/
|
||||
import { ref, computed, inject, onBeforeUnmount, toRef } from 'vue'
|
||||
import { tStatic } from '@map/i18n/index.js'
|
||||
|
||||
// This component's chrome, read out of the catalogue. tStatic and not useI18n: these mount inside
|
||||
// custom elements that install no i18n plugin, and an unguarded useI18n() throws on the way in and
|
||||
// takes the subtree with it. The English stays as the fallback.
|
||||
const T = (key, en) => tStatic(key) || en
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useShadowStyles } from '../../composables/useShadowStyles.js'
|
||||
import { useLegendData } from '../../composables/legends/useLegendData.js'
|
||||
|
|
@ -249,7 +255,7 @@ onBeforeUnmount(() => {
|
|||
<path d="M2 6l3 3 5-6" stroke="#3498db" stroke-width="2" fill="none"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="lt-btn-text">Clusters</span>
|
||||
<span class="lt-btn-text">{{ T('controls.clusters', 'Clusters') }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="lt-side-btn"
|
||||
|
|
@ -261,7 +267,7 @@ onBeforeUnmount(() => {
|
|||
<div v-if="showClusterSettings" class="lt-popup" @click.stop>
|
||||
<div class="lt-popup-row active" @click="setClusterMode('mst')">
|
||||
<span class="lt-popup-icon">⌬</span>
|
||||
<span>Minimum Spanning Tree<br><small style="opacity:0.7;font-weight:400">click a legend row to cluster only that group</small></span>
|
||||
<span>{{ T('controls.mst_full_name', 'Minimum Spanning Tree') }}<br><small style="opacity:0.7;font-weight:400">{{ T('controls.mst_hint', 'click a legend row to cluster only that group') }}</small></span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="showClusterSettings" ref="scrimRef" :style="scrimStyle" class="lt-overlay" @click.stop="showClusterSettings = false"></div>
|
||||
|
|
|
|||
|
|
@ -27,6 +27,12 @@
|
|||
-->
|
||||
<script setup>
|
||||
import { ref, inject, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { tStatic } from '@map/i18n/index.js'
|
||||
|
||||
// This component's chrome, read out of the catalogue. tStatic and not useI18n: these mount inside
|
||||
// custom elements that install no i18n plugin, and an unguarded useI18n() throws on the way in and
|
||||
// takes the subtree with it. The English stays as the fallback.
|
||||
const T = (key, en) => tStatic(key) || en
|
||||
import { useShadowStyles } from '../../composables/useShadowStyles.js'
|
||||
|
||||
useShadowStyles(`
|
||||
|
|
@ -140,7 +146,7 @@ onBeforeUnmount(() => {
|
|||
<div v-if="open" ref="el" class="mcm" :class="{ light: !isDark }" :style="style" role="dialog" :aria-label="title">
|
||||
<div class="mcm-header">
|
||||
<span class="mcm-title">{{ title }}</span>
|
||||
<button class="mcm-close" type="button" aria-label="Close" @click="emit('close')">
|
||||
<button class="mcm-close" type="button" :aria-label="T('buttons.close', 'Close')" @click="emit('close')">
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||||
<line x1="2" y1="2" x2="12" y2="12"/><line x1="12" y1="2" x2="2" y2="12"/>
|
||||
</svg>
|
||||
|
|
|
|||
|
|
@ -22,6 +22,12 @@
|
|||
-->
|
||||
<script setup>
|
||||
import { ref, computed, inject, unref, provide } from 'vue'
|
||||
import { tStatic } from '@map/i18n/index.js'
|
||||
|
||||
// This component's chrome, read out of the catalogue. tStatic and not useI18n: these mount inside
|
||||
// custom elements that install no i18n plugin, and an unguarded useI18n() throws on the way in and
|
||||
// takes the subtree with it. The English stays as the fallback.
|
||||
const T = (key, en) => tStatic(key) || en
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useShadowStyles } from '../../composables/useShadowStyles.js'
|
||||
import { resolveShareUrl } from '../../utils/embedBase.js'
|
||||
|
|
@ -538,12 +544,12 @@ function bindAnchor(el) {
|
|||
class="share-tab"
|
||||
:class="{ active: activeTab === 'link' }"
|
||||
@click="activeTab = 'link'"
|
||||
>Embed link</button>
|
||||
>{{ T('share.embed_link', 'Embed link') }}</button>
|
||||
<button
|
||||
class="share-tab"
|
||||
:class="{ active: activeTab === 'embed' }"
|
||||
@click="activeTab = 'embed'"
|
||||
>Add to Your Website</button>
|
||||
>{{ T('share.add_to_website', 'Add to Your Website') }}</button>
|
||||
</div>
|
||||
|
||||
<!-- ===== Tab 1: Share Link ===== -->
|
||||
|
|
@ -579,7 +585,7 @@ function bindAnchor(el) {
|
|||
<div v-if="activeTab === 'embed'" class="share-body">
|
||||
<!-- Friendly description -->
|
||||
<div class="share-embed-desc">
|
||||
Paste this code into your website builder (WordPress, Squarespace, Wix, or any site that accepts HTML widgets).
|
||||
{{ T('share.embed_paste_hint', 'Paste this code into your website builder (WordPress, Squarespace, Wix, or any site that accepts HTML widgets).') }}
|
||||
</div>
|
||||
|
||||
<!-- Size presets -->
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@
|
|||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button class="btn btn--ghost" type="button" @click="onClose">Close</button>
|
||||
<button class="btn btn--ghost" type="button" @click="onClose">{{ T('buttons.close', 'Close') }}</button>
|
||||
<button class="btn btn--primary" type="button" :disabled="poster.isRendering.value" @click="onRender">
|
||||
{{ poster.isRendering.value ? 'Rendering…' : `Render ${format.toUpperCase()}` }}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -33,6 +33,12 @@
|
|||
* ADD to declared widths and columns drift.
|
||||
*/
|
||||
import { ref, shallowRef, onMounted, onBeforeUnmount, watch, computed } from 'vue'
|
||||
import { tStatic } from '@map/i18n/index.js'
|
||||
|
||||
// This component's chrome, read out of the catalogue. tStatic and not useI18n: these mount inside
|
||||
// custom elements that install no i18n plugin, and an unguarded useI18n() throws on the way in and
|
||||
// takes the subtree with it. The English stays as the fallback.
|
||||
const T = (key, en) => tStatic(key) || en
|
||||
import { useShadowStyles } from '../../composables/useShadowStyles.js'
|
||||
// A fixed;inset:0 scrim covers only the CONTENT PANE inside the cap-5 shell (contain:layout on
|
||||
// .vtl-content captures the fixed containing block). Measured here: 1024x793 @ (256,107) at a
|
||||
|
|
@ -297,22 +303,22 @@ useShadowStyles(
|
|||
<span class="dt-count">{{ rowCount.toLocaleString() }} rows</span>
|
||||
<span v-if="meta.grain" class="dt-grain">· {{ meta.grain }}</span>
|
||||
<span class="dt-actions">
|
||||
<button type="button" class="dt-btn" @click="download('csv')">CSV</button>
|
||||
<button type="button" class="dt-btn" @click="download('json')">JSON</button>
|
||||
<button type="button" class="dt-btn" @click="download('csv')">{{ T('tables.csv', 'CSV') }}</button>
|
||||
<button type="button" class="dt-btn" @click="download('json')">{{ T('tables.json', 'JSON') }}</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="err" class="dt-err">{{ err }}</div>
|
||||
<div class="dt-wrap"><div ref="host"></div></div>
|
||||
<p class="dt-hint">Tap any row for its full detail{{ isMobile ? '' : ' — every field, including the lists the table flattens' }}.</p>
|
||||
<p class="dt-hint">{{ T('tables.tap_row_hint', 'Tap any row for its full detail') }}{{ isMobile ? '' : ' — every field, including the lists the table flattens' }}.</p>
|
||||
|
||||
<!-- ROW DETAIL — a bottom sheet on mobile, a panel on desktop. Carries EVERY
|
||||
field, including the ones the list view has to hide or flatten. -->
|
||||
<div v-if="detail" ref="scrimRef" :style="scrimStyle" class="dt-scrim" @click.self="detail = null">
|
||||
<div class="dt-sheet" role="dialog" aria-label="Row detail">
|
||||
<div class="dt-sheet" role="dialog" :aria-label="T('aria.rowDetail', 'Row detail')">
|
||||
<div class="dt-sheet-head">
|
||||
<strong>{{ detail[(table?.columns || [])[0]?.field] || 'Detail' }}</strong>
|
||||
<button type="button" class="dt-close" @click="detail = null">Close</button>
|
||||
<button type="button" class="dt-close" @click="detail = null">{{ T('buttons.close', 'Close') }}</button>
|
||||
</div>
|
||||
<dl class="dt-dl">
|
||||
<template v-for="[k, v] in detailRows" :key="k">
|
||||
|
|
|
|||
|
|
@ -7,5 +7,7 @@
|
|||
"searchPeopleGroups": "ابحث عن الشعوب",
|
||||
"resetAllFilters": "إعادة تعيين كل عوامل التصفية والبحث",
|
||||
"selectAllRows": "تحديد كل الصفوف",
|
||||
"deselectAllRows": "إلغاء تحديد كل الصفوف"
|
||||
"deselectAllRows": "إلغاء تحديد كل الصفوف",
|
||||
"rowDetail": "تفاصيل الصف",
|
||||
"dismissHint": "إخفاء التلميح"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,5 +34,8 @@
|
|||
"prayer_status": "حالة الصلاة",
|
||||
"close": "إغلاق",
|
||||
"clear": "مسح",
|
||||
"map_controls": "أدوات التحكم بالخريطة"
|
||||
"map_controls": "أدوات التحكم بالخريطة",
|
||||
"clusters": "التجمّعات",
|
||||
"mst_full_name": "الشجرة الممتدة الصغرى",
|
||||
"mst_hint": "اضغط على صف في مفتاح الخريطة لتجميع تلك المجموعة وحدها"
|
||||
}
|
||||
|
|
|
|||
5
library/i18n/locales/ar/share.json
Normal file
5
library/i18n/locales/ar/share.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"embed_link": "رابط التضمين",
|
||||
"add_to_website": "أضِفها إلى موقعك",
|
||||
"embed_paste_hint": "الصق هذا الرمز في أداة إنشاء موقعك (WordPress أو Squarespace أو Wix أو أي موقع يقبل أدوات HTML)."
|
||||
}
|
||||
|
|
@ -48,5 +48,7 @@
|
|||
"engineMaplibreNote": "لا يحتاج إلى رمز",
|
||||
"tokenNotNeeded": "تم اختيار MapLibre: خرائطه لا تحتاج إلى رمز Mapbox",
|
||||
"hideHeader": "إخفاء الشريط العلوي",
|
||||
"showHeader": "إظهار الشريط العلوي"
|
||||
"showHeader": "إظهار الشريط العلوي",
|
||||
"elementsCount": "{n} عناصر",
|
||||
"elements": "عناصر"
|
||||
}
|
||||
|
|
|
|||
5
library/i18n/locales/ar/tables.json
Normal file
5
library/i18n/locales/ar/tables.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"csv": "CSV",
|
||||
"json": "JSON",
|
||||
"tap_row_hint": "اضغط على أي صف لعرض تفاصيله الكاملة"
|
||||
}
|
||||
|
|
@ -7,5 +7,7 @@
|
|||
"searchPeopleGroups": "Volksgruppen suchen",
|
||||
"resetAllFilters": "Alle Filter und die Suche zurücksetzen",
|
||||
"selectAllRows": "Alle Zeilen auswählen",
|
||||
"deselectAllRows": "Auswahl aller Zeilen aufheben"
|
||||
"deselectAllRows": "Auswahl aller Zeilen aufheben",
|
||||
"rowDetail": "Zeilendetails",
|
||||
"dismissHint": "Hinweis schließen"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,5 +34,8 @@
|
|||
"prayer_status": "Gebetsstatus",
|
||||
"close": "Schließen",
|
||||
"clear": "Zurücksetzen",
|
||||
"map_controls": "Kartensteuerung"
|
||||
"map_controls": "Kartensteuerung",
|
||||
"clusters": "Cluster",
|
||||
"mst_full_name": "Minimaler Spannbaum",
|
||||
"mst_hint": "Klicken Sie auf eine Legendenzeile, um nur diese Gruppe zu clustern"
|
||||
}
|
||||
|
|
|
|||
5
library/i18n/locales/de/share.json
Normal file
5
library/i18n/locales/de/share.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"embed_link": "Einbettungslink",
|
||||
"add_to_website": "Zu Ihrer Website hinzufügen",
|
||||
"embed_paste_hint": "Fügen Sie diesen Code in Ihren Website-Baukasten ein (WordPress, Squarespace, Wix oder jede Website, die HTML-Widgets akzeptiert)."
|
||||
}
|
||||
|
|
@ -48,5 +48,7 @@
|
|||
"engineMaplibreNote": "kein Token nötig",
|
||||
"tokenNotNeeded": "MapLibre ist gewählt: Seine Karten brauchen keinen Mapbox-Token",
|
||||
"hideHeader": "Kopfzeile ausblenden",
|
||||
"showHeader": "Kopfzeile einblenden"
|
||||
"showHeader": "Kopfzeile einblenden",
|
||||
"elementsCount": "{n} Elemente",
|
||||
"elements": "Elemente"
|
||||
}
|
||||
|
|
|
|||
5
library/i18n/locales/de/tables.json
Normal file
5
library/i18n/locales/de/tables.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"csv": "CSV",
|
||||
"json": "JSON",
|
||||
"tap_row_hint": "Tippen Sie auf eine Zeile für alle Details"
|
||||
}
|
||||
|
|
@ -7,5 +7,7 @@
|
|||
"searchPeopleGroups": "Search people groups",
|
||||
"resetAllFilters": "Reset all filters and search",
|
||||
"selectAllRows": "Select all rows",
|
||||
"deselectAllRows": "Deselect all rows"
|
||||
"deselectAllRows": "Deselect all rows",
|
||||
"rowDetail": "Row detail",
|
||||
"dismissHint": "Dismiss hint"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,5 +34,8 @@
|
|||
"prayer_status": "Prayer Status",
|
||||
"close": "Close",
|
||||
"clear": "Clear",
|
||||
"map_controls": "Research map controls"
|
||||
"map_controls": "Research map controls",
|
||||
"clusters": "Clusters",
|
||||
"mst_full_name": "Minimum Spanning Tree",
|
||||
"mst_hint": "click a legend row to cluster only that group"
|
||||
}
|
||||
|
|
|
|||
5
library/i18n/locales/en/share.json
Normal file
5
library/i18n/locales/en/share.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"embed_link": "Embed link",
|
||||
"add_to_website": "Add to Your Website",
|
||||
"embed_paste_hint": "Paste this code into your website builder (WordPress, Squarespace, Wix, or any site that accepts HTML widgets)."
|
||||
}
|
||||
|
|
@ -48,5 +48,7 @@
|
|||
"engineMaplibreNote": "no token needed",
|
||||
"tokenNotNeeded": "MapLibre is selected: its maps need no Mapbox token",
|
||||
"hideHeader": "Hide header",
|
||||
"showHeader": "Show header"
|
||||
"showHeader": "Show header",
|
||||
"elementsCount": "{n} elements",
|
||||
"elements": "elements"
|
||||
}
|
||||
|
|
|
|||
5
library/i18n/locales/en/tables.json
Normal file
5
library/i18n/locales/en/tables.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"csv": "CSV",
|
||||
"json": "JSON",
|
||||
"tap_row_hint": "Tap any row for its full detail"
|
||||
}
|
||||
|
|
@ -7,5 +7,7 @@
|
|||
"searchPeopleGroups": "Buscar pueblos",
|
||||
"resetAllFilters": "Restablecer todos los filtros y la búsqueda",
|
||||
"selectAllRows": "Seleccionar todas las filas",
|
||||
"deselectAllRows": "Deseleccionar todas las filas"
|
||||
"deselectAllRows": "Deseleccionar todas las filas",
|
||||
"rowDetail": "Detalle de la fila",
|
||||
"dismissHint": "Descartar la sugerencia"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,5 +34,8 @@
|
|||
"prayer_status": "Estado de oración",
|
||||
"close": "Cerrar",
|
||||
"clear": "Borrar",
|
||||
"map_controls": "Controles del mapa"
|
||||
"map_controls": "Controles del mapa",
|
||||
"clusters": "Clústeres",
|
||||
"mst_full_name": "Árbol de expansión mínima",
|
||||
"mst_hint": "haz clic en una fila de la leyenda para agrupar solo ese grupo"
|
||||
}
|
||||
|
|
|
|||
5
library/i18n/locales/es/share.json
Normal file
5
library/i18n/locales/es/share.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"embed_link": "Enlace para insertar",
|
||||
"add_to_website": "Añádelo a tu sitio web",
|
||||
"embed_paste_hint": "Pega este código en tu creador de sitios web (WordPress, Squarespace, Wix o cualquier sitio que acepte widgets HTML)."
|
||||
}
|
||||
|
|
@ -48,5 +48,7 @@
|
|||
"engineMaplibreNote": "no necesita token",
|
||||
"tokenNotNeeded": "MapLibre está seleccionado: sus mapas no necesitan token de Mapbox",
|
||||
"hideHeader": "Ocultar encabezado",
|
||||
"showHeader": "Mostrar encabezado"
|
||||
"showHeader": "Mostrar encabezado",
|
||||
"elementsCount": "{n} elementos",
|
||||
"elements": "elementos"
|
||||
}
|
||||
|
|
|
|||
5
library/i18n/locales/es/tables.json
Normal file
5
library/i18n/locales/es/tables.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"csv": "CSV",
|
||||
"json": "JSON",
|
||||
"tap_row_hint": "Toca cualquier fila para ver su detalle completo"
|
||||
}
|
||||
|
|
@ -7,5 +7,7 @@
|
|||
"searchPeopleGroups": "Rechercher des peuples",
|
||||
"resetAllFilters": "Réinitialiser tous les filtres et la recherche",
|
||||
"selectAllRows": "Sélectionner toutes les lignes",
|
||||
"deselectAllRows": "Désélectionner toutes les lignes"
|
||||
"deselectAllRows": "Désélectionner toutes les lignes",
|
||||
"rowDetail": "Détail de la ligne",
|
||||
"dismissHint": "Masquer l'astuce"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,5 +34,8 @@
|
|||
"prayer_status": "État de prière",
|
||||
"close": "Fermer",
|
||||
"clear": "Effacer",
|
||||
"map_controls": "Commandes de la carte"
|
||||
"map_controls": "Commandes de la carte",
|
||||
"clusters": "Groupes",
|
||||
"mst_full_name": "Arbre couvrant minimal",
|
||||
"mst_hint": "cliquez sur une ligne de la légende pour ne regrouper que ce groupe"
|
||||
}
|
||||
|
|
|
|||
5
library/i18n/locales/fr/share.json
Normal file
5
library/i18n/locales/fr/share.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"embed_link": "Lien d'intégration",
|
||||
"add_to_website": "Ajouter à votre site web",
|
||||
"embed_paste_hint": "Collez ce code dans votre créateur de site web (WordPress, Squarespace, Wix ou tout site acceptant les widgets HTML)."
|
||||
}
|
||||
|
|
@ -48,5 +48,7 @@
|
|||
"engineMaplibreNote": "aucun jeton requis",
|
||||
"tokenNotNeeded": "MapLibre est sélectionné : ses cartes n’ont pas besoin de jeton Mapbox",
|
||||
"hideHeader": "Masquer l'en-tête",
|
||||
"showHeader": "Afficher l'en-tête"
|
||||
"showHeader": "Afficher l'en-tête",
|
||||
"elementsCount": "{n} éléments",
|
||||
"elements": "éléments"
|
||||
}
|
||||
|
|
|
|||
5
library/i18n/locales/fr/tables.json
Normal file
5
library/i18n/locales/fr/tables.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"csv": "CSV",
|
||||
"json": "JSON",
|
||||
"tap_row_hint": "Touchez une ligne pour voir tout son détail"
|
||||
}
|
||||
|
|
@ -7,5 +7,7 @@
|
|||
"searchPeopleGroups": "जनसमूह खोजें",
|
||||
"resetAllFilters": "सभी फ़िल्टर और खोज रीसेट करें",
|
||||
"selectAllRows": "सभी पंक्तियाँ चुनें",
|
||||
"deselectAllRows": "सभी पंक्तियाँ अचयनित करें"
|
||||
"deselectAllRows": "सभी पंक्तियाँ अचयनित करें",
|
||||
"rowDetail": "पंक्ति विवरण",
|
||||
"dismissHint": "संकेत हटाएँ"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,5 +34,8 @@
|
|||
"prayer_status": "प्रार्थना स्थिति",
|
||||
"close": "बंद करें",
|
||||
"clear": "साफ़ करें",
|
||||
"map_controls": "मानचित्र नियंत्रण"
|
||||
"map_controls": "मानचित्र नियंत्रण",
|
||||
"clusters": "क्लस्टर",
|
||||
"mst_full_name": "न्यूनतम विस्तारित वृक्ष",
|
||||
"mst_hint": "केवल उस समूह को क्लस्टर करने के लिए लेजेंड की पंक्ति पर क्लिक करें"
|
||||
}
|
||||
|
|
|
|||
5
library/i18n/locales/hi/share.json
Normal file
5
library/i18n/locales/hi/share.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"embed_link": "एम्बेड लिंक",
|
||||
"add_to_website": "अपनी वेबसाइट में जोड़ें",
|
||||
"embed_paste_hint": "इस कोड को अपने वेबसाइट बिल्डर में पेस्ट करें (WordPress, Squarespace, Wix, या कोई भी साइट जो HTML विजेट स्वीकार करती है)।"
|
||||
}
|
||||
|
|
@ -48,5 +48,7 @@
|
|||
"engineMaplibreNote": "टोकन की ज़रूरत नहीं",
|
||||
"tokenNotNeeded": "MapLibre चुना गया है: इसके मानचित्रों को Mapbox टोकन नहीं चाहिए",
|
||||
"hideHeader": "हेडर छिपाएँ",
|
||||
"showHeader": "हेडर दिखाएँ"
|
||||
"showHeader": "हेडर दिखाएँ",
|
||||
"elementsCount": "{n} तत्व",
|
||||
"elements": "तत्व"
|
||||
}
|
||||
|
|
|
|||
5
library/i18n/locales/hi/tables.json
Normal file
5
library/i18n/locales/hi/tables.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"csv": "CSV",
|
||||
"json": "JSON",
|
||||
"tap_row_hint": "पूरा विवरण देखने के लिए किसी भी पंक्ति पर टैप करें"
|
||||
}
|
||||
|
|
@ -7,5 +7,7 @@
|
|||
"searchPeopleGroups": "Cerca popoli",
|
||||
"resetAllFilters": "Reimposta tutti i filtri e la ricerca",
|
||||
"selectAllRows": "Seleziona tutte le righe",
|
||||
"deselectAllRows": "Deseleziona tutte le righe"
|
||||
"deselectAllRows": "Deseleziona tutte le righe",
|
||||
"rowDetail": "Dettaglio riga",
|
||||
"dismissHint": "Ignora il suggerimento"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,5 +34,8 @@
|
|||
"prayer_status": "Stato di preghiera",
|
||||
"close": "Chiudi",
|
||||
"clear": "Cancella",
|
||||
"map_controls": "Controlli della mappa"
|
||||
"map_controls": "Controlli della mappa",
|
||||
"clusters": "Cluster",
|
||||
"mst_full_name": "Albero ricoprente minimo",
|
||||
"mst_hint": "fai clic su una riga della legenda per raggruppare solo quel gruppo"
|
||||
}
|
||||
|
|
|
|||
5
library/i18n/locales/it/share.json
Normal file
5
library/i18n/locales/it/share.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"embed_link": "Link di incorporamento",
|
||||
"add_to_website": "Aggiungi al tuo sito web",
|
||||
"embed_paste_hint": "Incolla questo codice nel tuo costruttore di siti web (WordPress, Squarespace, Wix o qualsiasi sito che accetti widget HTML)."
|
||||
}
|
||||
|
|
@ -48,5 +48,7 @@
|
|||
"engineMaplibreNote": "nessun token necessario",
|
||||
"tokenNotNeeded": "MapLibre è selezionato: le sue mappe non richiedono un token Mapbox",
|
||||
"hideHeader": "Nascondi intestazione",
|
||||
"showHeader": "Mostra intestazione"
|
||||
"showHeader": "Mostra intestazione",
|
||||
"elementsCount": "{n} elementi",
|
||||
"elements": "elementi"
|
||||
}
|
||||
|
|
|
|||
5
library/i18n/locales/it/tables.json
Normal file
5
library/i18n/locales/it/tables.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"csv": "CSV",
|
||||
"json": "JSON",
|
||||
"tap_row_hint": "Tocca una riga per vederne il dettaglio completo"
|
||||
}
|
||||
|
|
@ -7,5 +7,7 @@
|
|||
"searchPeopleGroups": "Pesquisar povos",
|
||||
"resetAllFilters": "Redefinir todos os filtros e a pesquisa",
|
||||
"selectAllRows": "Selecionar todas as linhas",
|
||||
"deselectAllRows": "Desmarcar todas as linhas"
|
||||
"deselectAllRows": "Desmarcar todas as linhas",
|
||||
"rowDetail": "Detalhe da linha",
|
||||
"dismissHint": "Dispensar a dica"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,5 +34,8 @@
|
|||
"prayer_status": "Estado de oração",
|
||||
"close": "Fechar",
|
||||
"clear": "Limpar",
|
||||
"map_controls": "Controlos do mapa"
|
||||
"map_controls": "Controlos do mapa",
|
||||
"clusters": "Agrupamentos",
|
||||
"mst_full_name": "Árvore geradora mínima",
|
||||
"mst_hint": "clique numa linha da legenda para agrupar apenas esse grupo"
|
||||
}
|
||||
|
|
|
|||
5
library/i18n/locales/pt/share.json
Normal file
5
library/i18n/locales/pt/share.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"embed_link": "Link de incorporação",
|
||||
"add_to_website": "Adicione ao seu site",
|
||||
"embed_paste_hint": "Cole este código no seu construtor de sites (WordPress, Squarespace, Wix ou qualquer site que aceite widgets HTML)."
|
||||
}
|
||||
|
|
@ -48,5 +48,7 @@
|
|||
"engineMaplibreNote": "não precisa de token",
|
||||
"tokenNotNeeded": "MapLibre está selecionado: os seus mapas não precisam de token do Mapbox",
|
||||
"hideHeader": "Ocultar cabeçalho",
|
||||
"showHeader": "Mostrar cabeçalho"
|
||||
"showHeader": "Mostrar cabeçalho",
|
||||
"elementsCount": "{n} elementos",
|
||||
"elements": "elementos"
|
||||
}
|
||||
|
|
|
|||
5
library/i18n/locales/pt/tables.json
Normal file
5
library/i18n/locales/pt/tables.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"csv": "CSV",
|
||||
"json": "JSON",
|
||||
"tap_row_hint": "Toque em qualquer linha para ver o detalhe completo"
|
||||
}
|
||||
|
|
@ -7,5 +7,7 @@
|
|||
"searchPeopleGroups": "Caută popoare",
|
||||
"resetAllFilters": "Resetează toate filtrele și căutarea",
|
||||
"selectAllRows": "Selectează toate rândurile",
|
||||
"deselectAllRows": "Deselectează toate rândurile"
|
||||
"deselectAllRows": "Deselectează toate rândurile",
|
||||
"rowDetail": "Detaliu rând",
|
||||
"dismissHint": "Închide sugestia"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,5 +34,8 @@
|
|||
"prayer_status": "Stare de rugăciune",
|
||||
"close": "Închide",
|
||||
"clear": "Șterge",
|
||||
"map_controls": "Comenzile hărții"
|
||||
"map_controls": "Comenzile hărții",
|
||||
"clusters": "Grupuri",
|
||||
"mst_full_name": "Arbore de acoperire minim",
|
||||
"mst_hint": "dă clic pe un rând din legendă pentru a grupa doar acel grup"
|
||||
}
|
||||
|
|
|
|||
5
library/i18n/locales/ro/share.json
Normal file
5
library/i18n/locales/ro/share.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"embed_link": "Link de încorporare",
|
||||
"add_to_website": "Adaugă pe site-ul tău",
|
||||
"embed_paste_hint": "Lipește acest cod în constructorul site-ului tău (WordPress, Squarespace, Wix sau orice site care acceptă widget-uri HTML)."
|
||||
}
|
||||
|
|
@ -48,5 +48,7 @@
|
|||
"engineMaplibreNote": "nu necesită token",
|
||||
"tokenNotNeeded": "MapLibre este selectat: hărțile sale nu necesită token Mapbox",
|
||||
"hideHeader": "Ascunde antetul",
|
||||
"showHeader": "Afișează antetul"
|
||||
"showHeader": "Afișează antetul",
|
||||
"elementsCount": "{n} elemente",
|
||||
"elements": "elemente"
|
||||
}
|
||||
|
|
|
|||
5
library/i18n/locales/ro/tables.json
Normal file
5
library/i18n/locales/ro/tables.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"csv": "CSV",
|
||||
"json": "JSON",
|
||||
"tap_row_hint": "Atinge orice rând pentru detaliile complete"
|
||||
}
|
||||
|
|
@ -7,5 +7,7 @@
|
|||
"searchPeopleGroups": "Поиск народов",
|
||||
"resetAllFilters": "Сбросить все фильтры и поиск",
|
||||
"selectAllRows": "Выбрать все строки",
|
||||
"deselectAllRows": "Снять выделение со всех строк"
|
||||
"deselectAllRows": "Снять выделение со всех строк",
|
||||
"rowDetail": "Подробности строки",
|
||||
"dismissHint": "Скрыть подсказку"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,5 +34,8 @@
|
|||
"prayer_status": "Статус молитвы",
|
||||
"close": "Закрыть",
|
||||
"clear": "Очистить",
|
||||
"map_controls": "Элементы управления картой"
|
||||
"map_controls": "Элементы управления картой",
|
||||
"clusters": "Кластеры",
|
||||
"mst_full_name": "Минимальное остовное дерево",
|
||||
"mst_hint": "нажмите строку легенды, чтобы сгруппировать только эту группу"
|
||||
}
|
||||
|
|
|
|||
5
library/i18n/locales/ru/share.json
Normal file
5
library/i18n/locales/ru/share.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"embed_link": "Ссылка для встраивания",
|
||||
"add_to_website": "Добавить на ваш сайт",
|
||||
"embed_paste_hint": "Вставьте этот код в конструктор вашего сайта (WordPress, Squarespace, Wix или любой сайт, принимающий HTML-виджеты)."
|
||||
}
|
||||
|
|
@ -48,5 +48,7 @@
|
|||
"engineMaplibreNote": "токен не нужен",
|
||||
"tokenNotNeeded": "Выбран MapLibre: его картам не нужен токен Mapbox",
|
||||
"hideHeader": "Скрыть заголовок",
|
||||
"showHeader": "Показать заголовок"
|
||||
"showHeader": "Показать заголовок",
|
||||
"elementsCount": "{n} элементов",
|
||||
"elements": "элементов"
|
||||
}
|
||||
|
|
|
|||
5
library/i18n/locales/ru/tables.json
Normal file
5
library/i18n/locales/ru/tables.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"csv": "CSV",
|
||||
"json": "JSON",
|
||||
"tap_row_hint": "Нажмите на любую строку, чтобы увидеть все подробности"
|
||||
}
|
||||
|
|
@ -7,5 +7,7 @@
|
|||
"searchPeopleGroups": "搜索民族",
|
||||
"resetAllFilters": "重置所有筛选和搜索",
|
||||
"selectAllRows": "选择所有行",
|
||||
"deselectAllRows": "取消选择所有行"
|
||||
"deselectAllRows": "取消选择所有行",
|
||||
"rowDetail": "行详情",
|
||||
"dismissHint": "关闭提示"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,5 +34,8 @@
|
|||
"prayer_status": "代祷状态",
|
||||
"close": "关闭",
|
||||
"clear": "清除",
|
||||
"map_controls": "地图控件"
|
||||
"map_controls": "地图控件",
|
||||
"clusters": "聚类",
|
||||
"mst_full_name": "最小生成树",
|
||||
"mst_hint": "点击图例中的一行,仅对该组进行聚类"
|
||||
}
|
||||
|
|
|
|||
5
library/i18n/locales/zh/share.json
Normal file
5
library/i18n/locales/zh/share.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"embed_link": "嵌入链接",
|
||||
"add_to_website": "添加到您的网站",
|
||||
"embed_paste_hint": "将此代码粘贴到您的网站构建器中(WordPress、Squarespace、Wix,或任何接受 HTML 小部件的网站)。"
|
||||
}
|
||||
|
|
@ -48,5 +48,7 @@
|
|||
"engineMaplibreNote": "无需令牌",
|
||||
"tokenNotNeeded": "已选择 MapLibre:其地图无需 Mapbox 令牌",
|
||||
"hideHeader": "隐藏标题栏",
|
||||
"showHeader": "显示标题栏"
|
||||
"showHeader": "显示标题栏",
|
||||
"elementsCount": "{n} 个元素",
|
||||
"elements": "元素"
|
||||
}
|
||||
|
|
|
|||
5
library/i18n/locales/zh/tables.json
Normal file
5
library/i18n/locales/zh/tables.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"csv": "CSV",
|
||||
"json": "JSON",
|
||||
"tap_row_hint": "点按任一行查看完整详情"
|
||||
}
|
||||
|
|
@ -25,6 +25,7 @@ const P = {
|
|||
pin: '<path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0z"/><circle cx="12" cy="10" r="3"/>',
|
||||
monitor: '<rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/>',
|
||||
chart: '<line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/>',
|
||||
folder: '<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>',
|
||||
layers: '<polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/>',
|
||||
flask: '<path d="M9 3h6v5l5 11a2 2 0 0 1-2 3H6a2 2 0 0 1-2-3l5-11z"/><line x1="9" y1="3" x2="15" y2="3"/><line x1="6" y1="15" x2="18" y2="15"/>',
|
||||
share: '<circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.6" y1="13.5" x2="15.4" y2="17.5"/><line x1="15.4" y1="6.5" x2="8.6" y2="10.5"/>',
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@
|
|||
"check:gospel-all": "node internal/check-gospel-all.mjs",
|
||||
"check:gospel-tables-staged": "node internal/check-gospel-tables-staged.mjs",
|
||||
"check:map-attribution": "node internal/check-map-attribution.mjs",
|
||||
"check:studio-rail": "node internal/check-studio-rail.mjs",
|
||||
"check:translator-contacts": "node internal/check-translator-contacts.mjs",
|
||||
"check:translator-contacts:poison": "node internal/check-translator-contacts.mjs --poison",
|
||||
"check:heartbeat": "node internal/check-heartbeat-threshold.mjs",
|
||||
|
|
@ -206,7 +207,7 @@
|
|||
"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: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:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status 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:backlog-refs 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: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",
|
||||
"gates:post": "node internal/run-parallel.mjs --label=post -- 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:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status 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:backlog-refs 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: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",
|
||||
"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",
|
||||
"internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs",
|
||||
"library-index": "node internal/generate-library-index.mjs",
|
||||
|
|
@ -358,6 +359,7 @@
|
|||
"check:shot-fidelity": "warns when THIS machine cannot RENDER what the shots must SHOW. `bun run shots` printed 13x OK while every emoji rendered as a tofu box — a glyph fallback loads perfectly (0 console errors, 0 HTTP>=400), so the generator cannot see it. Reports by default; the `check:shot-fidelity:strict` script to block once an emoji font is installed.",
|
||||
"check:shot-fonts": "can THIS machine render the emoji the UI uses? Run BEFORE `bun run shots` — no font = tofu boxes in every committed PNG, which every other shot gate passes. Deliberately NOT in build:unlocked: it reports an ENVIRONMENT condition (this box has no emoji font), and a gate that always fails in the build makes the build useless.",
|
||||
"check:shots": "a README screenshot that predates the bundle it shows is a picture telling a lie — and it looks IDENTICAL to a current one. Compares each shot's PNG against the BUILT BUNDLE it renders (not raw sources: an unbuilt edit is not yet in any leaf). Fix: node internal/readme-shots.mjs",
|
||||
"check:studio-rail": "the Plugin Studio rail is laid out like plugin-designs/: one folder per bundle with its elements inside, bundles in the section of their highest capability (5 \u2192 1) and in name order, one canonical node per plugin. Runs the studio's real rail code on the built manifest, headless.",
|
||||
"check:translator-contacts": "the credit line LINKS the translating organisation now, and a contact URL is a link this map stands behind. Reads ALLOWED_CONTACT out of internal/verify-gospel-map.mjs rather than restating it (two copies of an allowlist drift, and the drifting copy is the one nobody reads), then asserts every cr.u is on that list, was recorded `verified` by a real fetch, is never also serving a RESOURCE (wycliffe.org is somewhere to write to, not a publisher this map reads text from), and is actually RENDERED as an anchor \u2014 the data can carry 838 URLs while the template shows none. Static, sub-second. Self-test: `bun run check:translator-contacts:poison`.",
|
||||
"check:snapshot-guard": "every gospel-resources catalogue fetcher must write through scripts/_snapshot-guard.mjs, which refuses a write that loses more than a tenth of the previous snapshot. Written after a rate-limited FCBH run silently replaced 1,666 video scopes with 1,103. Static, sub-second. Self-test: `bun run check:snapshot-guard:poison` injects a raw write and the check must fail on it.",
|
||||
"check:spa-tab-order": "the cap-5 SPA must open on Big Picture and keep the Driver's order (Big Picture, UPG-100, Prayer Progress, Gospel Resources) — asserted as PRESENCE + tab-1 + RELATIVE order on the RENDERED rail, because the rail gained three sections and now renders 12 rows with others between them. ⚠ NOT WAVE-WIRED: it FAILS rather than skips when the SPA is unreachable, deliberately (\"a gate that cannot reach its subject has not passed, it has not run\") — correct standalone, but gates:post runs on checkouts with no server. Needs `bun run serve` on 4173.",
|
||||
|
|
|
|||
|
|
@ -920,6 +920,45 @@ const countryStats = computed(() => {
|
|||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* ═══ PRAYER IS A MOVE, NOT A STATISTIC ════════════════════════════════════════════════════
|
||||
*
|
||||
* A country click ended in numbers. Every one of them describes work somebody else has to fund,
|
||||
* staff or translate — and the one thing the reader of a country can do THIS MINUTE was reachable
|
||||
* only by guessing which pin to tap. So the country sheet now ends in a move: the largest group
|
||||
* here that has nothing AND nobody praying, with its prayer page one tap away.
|
||||
*
|
||||
* ⚠ THE DENOMINATOR IS STATED BECAUSE IT IS NOT THE COUNTRY. doxa.life publishes a page for
|
||||
* 1,723 of the 12,379 groups on this map, so "nobody praying" can only ever be said of the
|
||||
* groups that HAVE a page — never of the country. Rendering a bare count would read as "14 groups
|
||||
* in this country have nobody praying", which this data cannot support. It is shown as N of M.
|
||||
*
|
||||
* ⚠ ZERO IS ZERO ON doxa.life's COUNTER, which is what the gap tab already says in full. It is
|
||||
* not a claim that nobody in the world prays for these people — an unknowable thing.
|
||||
*/
|
||||
const countryPrayer = computed(() => {
|
||||
const g = groupsForCountry.value
|
||||
let pages = 0, noPray = 0, noPrayPeople = 0
|
||||
let firstMove = null, firstMovePop = -1
|
||||
for (const r of g) {
|
||||
const e = prayIndex.value[prayKey(r.people_group, r._iso3 || selected.value?.iso3 || '')]
|
||||
if (!e) continue
|
||||
pages++
|
||||
if (e.praying > 0) continue
|
||||
noPray++
|
||||
noPrayPeople += r.population || 0
|
||||
/* NEED FIRST, THEN SIZE, and India shows what that costs: of its 601 groups with nobody
|
||||
praying only FIVE have nothing at all, so this offers Malai Pandaram at 8,700 people ahead
|
||||
of Badhai at 851,000 — who already have Scripture and film. That is the map's own ladder
|
||||
applied to prayer rather than a second, quieter ranking, and the sub-label says "nothing
|
||||
found" so the reader can see which rung they were handed. Where every unprayed-for group
|
||||
here has something, the largest of those is offered instead. */
|
||||
const rank = (hasAny(r) ? 0 : 1e12) + (r.population || 0)
|
||||
if (rank > firstMovePop) { firstMovePop = rank; firstMove = { ...e, group: r } }
|
||||
}
|
||||
return { pages, noPray, noPrayPeople, firstMove }
|
||||
})
|
||||
|
||||
/* ITEM 2 — resources in a group's OWN language, joined on the EXACT ISO 639-3 code.
|
||||
* Never a name match: language names collide and drift ("Arabic" is dozens of
|
||||
* distinct codes), and a fuzzy hit here would put the wrong scripture in someone's
|
||||
|
|
@ -1141,7 +1180,7 @@ async function ensureProvinceTable() {
|
|||
return provinceRows.value
|
||||
}
|
||||
|
||||
const PROVINCE_FILE = { IND: 'gb-IND-ADM1.geojson', CHN: 'gb-CHN-ADM1.geojson' }
|
||||
const PROVINCE_FILE = { IND: 'gb-IND-ADM1-full.geojson', CHN: 'gb-CHN-ADM1.geojson' }
|
||||
|
||||
async function ensureProvinces(iso3) {
|
||||
const map = mapRef.value
|
||||
|
|
@ -2630,6 +2669,12 @@ useShadowStyles(
|
|||
rows underneath already showed the third. -->
|
||||
<div class="gm-stat"><span>{{ t('gospel.stat.haveAudio') }}</span><b>{{ fmt(countryStats.audio) }}</b></div>
|
||||
<div class="gm-stat" v-if="countryStats.sign"><span>{{ t('gospel.stat.signLanguages') }}</span><b>{{ fmt(countryStats.sign) }}</b></div>
|
||||
<!-- N of M, never a bare N: the denominator is the groups doxa.life publishes a prayer
|
||||
page for, not the country. See countryPrayer. -->
|
||||
<div class="gm-stat" v-if="countryPrayer.pages">
|
||||
<span>{{ t('gospel.tabs.noprayer') }}</span>
|
||||
<b>{{ fmt(countryPrayer.noPray) }} / {{ fmt(countryPrayer.pages) }}</b>
|
||||
</div>
|
||||
<!-- These two are NOT derivable from the pins: they come from the curated country
|
||||
analysis, over its own narrower set of groups. Labelled so the different basis is
|
||||
visible rather than implied.
|
||||
|
|
@ -2648,6 +2693,18 @@ useShadowStyles(
|
|||
</p>
|
||||
</template>
|
||||
|
||||
<!-- ═══ THE MOVE ═══════════════════════════════════════════════════════════════════
|
||||
Everything above this line is work for somebody else to fund, staff or translate.
|
||||
This is the one a reader can do before closing the tab, and it names the group
|
||||
rather than making them choose: the largest here with nothing and nobody praying. -->
|
||||
<a v-if="countryPrayer.firstMove" class="gm-pray" :href="countryPrayer.firstMove.u"
|
||||
target="_blank" rel="noopener noreferrer"
|
||||
:title="'Open the prayer page for ' + countryPrayer.firstMove.n + ' on doxa.life.'">
|
||||
{{ t('gospel.sheet.pray') }} {{ countryPrayer.firstMove.n }}
|
||||
<span class="gm-pray-n" dir="ltr">{{ fmt(countryPrayer.firstMove.group.population || 0) }}
|
||||
people<template v-if="!countryPrayer.firstMove.group._tier">, nothing found</template></span>
|
||||
</a>
|
||||
|
||||
<!-- Same split as the language link above: a filter when the tables are already on
|
||||
screen beneath this map, a link when they are not. This is the brief's SECOND
|
||||
requirement — click a country, see its resources — and in the composite it was
|
||||
|
|
|
|||
|
|
@ -82,8 +82,14 @@ const wayIn = (code, iso3, name) => {
|
|||
return b ? b.l : ''
|
||||
}
|
||||
|
||||
/* ⚠ INDIA READS THE -full FILE, NOT gb-IND-ADM1.geojson. The unsuffixed file is the UPG-100
|
||||
list's own curated partition — 34 listId-tagged units — and this table was built on it for one
|
||||
build, which is why eleven groups had no province: they were standing in states the partition
|
||||
does not draw. gb-IND-ADM1-full.geojson is the complete ADM1 (36 states, geoBoundaries via
|
||||
DataMeet India, CC BY 2.5 IN). A partition of somebody else's list is not a country's states,
|
||||
even when the filename looks like it is. */
|
||||
const COUNTRIES = [
|
||||
{ iso3: 'IND', file: 'gb-IND-ADM1.geojson', country: 'India' },
|
||||
{ iso3: 'IND', file: 'gb-IND-ADM1-full.geojson', country: 'India' },
|
||||
{ iso3: 'CHN', file: 'gb-CHN-ADM1.geojson', country: 'China' },
|
||||
]
|
||||
|
||||
|
|
@ -146,6 +152,17 @@ if (!assigned || assigned < 900) {
|
|||
provinces_available: provincesAvailable,
|
||||
groups_assigned: assigned,
|
||||
groups_unassigned: unassigned.length,
|
||||
india_note: 'India is read from the COMPLETE ADM1 (36 states, geoBoundaries via DataMeet ' +
|
||||
'India, CC BY 2.5 IN). The first build of this table read gb-IND-ADM1.geojson, which is ' +
|
||||
'the UPG-100 list\'s curated 34-unit partition, and eleven groups came out with no ' +
|
||||
'province because they stood in states that partition does not draw. Switching the file ' +
|
||||
'moved 998 assigned to 1,005 and 44 province rows to 46, and turned the province names ' +
|
||||
'into India\'s own states.',
|
||||
unassigned_note: 'Four remain, each one legible rather than lost. Great Andamanese and ' +
|
||||
'the Sentinelese sit on small islands whose simplified coastline the point falls just ' +
|
||||
'outside — Andaman and Nicobar IS in the file, so this is geometry precision, not a ' +
|
||||
'missing province. Jing and Pana are on China\'s Vietnam and Laos borders, outside the ' +
|
||||
'twelve-province subset. They are listed in `unassigned` with their coordinates.',
|
||||
china_note: 'China\'s ADM1 file carries 12 provinces, not 31+. Measured rather than ' +
|
||||
'assumed: 152 of 154 Chinese groups fall inside them and all twelve are used, so the ' +
|
||||
'file is cut to the provinces where the unreached peoples are.',
|
||||
|
|
|
|||
|
|
@ -567,21 +567,30 @@ export default defineComponent({
|
|||
// plugin left no single name to report it by; a node that only TARGETS the canonical row has no
|
||||
// identity of its own, so nesting and the one-name rule coexist.
|
||||
treeSections() {
|
||||
// ★ LAID OUT LIKE plugin-designs/ (Driver, 2026-09-26): one FOLDER per BUNDLE, its ELEMENTS inside,
|
||||
// bundles grouped by their HIGHEST capability (5 → 1) and sorted by name within a group. A bundle
|
||||
// with one element is shown as that element (a folder of one is noise). A composite's parts still
|
||||
// hang under its element, folded, each TARGETING its canonical row (one identity per plugin).
|
||||
const treeOf = new Map(this.compositionTrees.map(t => [t.nodes[0].selects, t]))
|
||||
return this.groups.map(g => {
|
||||
const byBundle = new Map()
|
||||
for (const r of this.rows) { if (!byBundle.has(r.bundle)) byBundle.set(r.bundle, []); byBundle.get(r.bundle).push(r) }
|
||||
const bundles = [...byBundle.entries()].map(([bundle, rows]) => ({ bundle, rows, cap: Math.max(...rows.map(r => r.cap)) }))
|
||||
return [5, 4, 3, 2, 1].map(cap => {
|
||||
const group = bundles.filter(b => b.cap === cap).sort((x, y) => x.bundle.localeCompare(y.bundle))
|
||||
if (!group.length) return null
|
||||
const nodes = []
|
||||
for (const r of g.rows) {
|
||||
const pushElement = (r, depth, parent) => {
|
||||
const t = treeOf.get(r.key)
|
||||
nodes.push({
|
||||
id: r.key, parent: '', depth: 0, label: r.label, desc: r.tag, icon: r.icon, target: r.key,
|
||||
id: r.key, parent, depth, label: r.label, desc: r.tag, icon: r.icon, target: r.key, canonical: true,
|
||||
kids: t ? t.nodes[0].kids : 0, title: r.label + ' · <' + r.tag + '> · CAP ' + r.cap + ' ' + this.tr('cap' + r.cap)
|
||||
})
|
||||
if (!t) continue
|
||||
if (!t) return
|
||||
const nid = (id) => (id === 0 ? r.key : r.key + '>' + id)
|
||||
for (const n of t.nodes.slice(1)) {
|
||||
nodes.push({
|
||||
id: nid(n.id), parent: nid(n.ancestors[n.ancestors.length - 1]), depth: n.depth,
|
||||
label: n.label, part: !!n.part, target: n.selects || '',
|
||||
id: nid(n.id), parent: nid(n.ancestors[n.ancestors.length - 1]), depth: depth + n.depth,
|
||||
label: n.label, part: !!n.part, target: n.selects || '', canonical: false,
|
||||
icon: n.part ? 'dots' : (CAP_ICON[n.cap] || 'embed'),
|
||||
desc: n.part ? this.tr('noStandalone') : 'cap ' + n.cap + ' · ' + n.tag,
|
||||
kids: n.kids || 0,
|
||||
|
|
@ -590,10 +599,19 @@ export default defineComponent({
|
|||
})
|
||||
}
|
||||
}
|
||||
// Short section names: the header shares a 256px rail with its CAP badge and count, and the
|
||||
// full "App shell · nested-mfe-spa" truncated to "APP SHELL · NEST…". The long name is the tooltip.
|
||||
return { id: 'cap-' + g.cap, cap: g.cap, label: this.tr('cap' + g.cap), title: this.tr('cap' + g.cap) + ' · ' + (MACHINE_TYPE[g.cap] || ''), icon: CAP_ICON[g.cap] || 'embed', nodes }
|
||||
})
|
||||
for (const b of group) {
|
||||
const rows = [...b.rows].sort((x, y) => y.cap - x.cap || x.label.localeCompare(y.label))
|
||||
if (rows.length === 1) { pushElement(rows[0], 0, ''); continue }
|
||||
const fid = 'bundle:' + b.bundle
|
||||
nodes.push({
|
||||
id: fid, parent: '', depth: 0, label: b.bundle, desc: this.tr('elementsCount', { n: rows.length }),
|
||||
icon: 'folder', target: rows[0].key, canonical: false, folder: true, kids: rows.length,
|
||||
title: b.bundle + ' · plugin-designs/' + b.bundle + '/ · ' + rows.length + ' ' + this.tr('elements')
|
||||
})
|
||||
for (const r of rows) pushElement(r, 1, fid)
|
||||
}
|
||||
return { id: 'cap-' + cap, cap, label: this.tr('cap' + cap), title: this.tr('cap' + cap) + ' · ' + (MACHINE_TYPE[cap] || ''), icon: CAP_ICON[cap] || 'embed', nodes }
|
||||
}).filter(Boolean)
|
||||
},
|
||||
// ★ ONE TREE PER COMPOSITE, built ENTIRELY from the manifest's `composes` edges — no bundle names
|
||||
// or child lists live in this file, so a new composite appears here with zero edits.
|
||||
|
|
|
|||
|
|
@ -146,6 +146,11 @@ const EN = {
|
|||
}
|
||||
const CHEV = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 6 15 12 9 18"/></svg>'
|
||||
|
||||
// A node is the CANONICAL row of a plugin when the rail says so (canonical: true); older models without
|
||||
// the flag used depth 0 for that. A bundle FOLDER is not a plugin, and neither is a repeated part.
|
||||
const isCanonical = (n) => (n.canonical !== undefined ? !!n.canonical : n.depth === 0)
|
||||
const isAlias = (n) => (n.canonical !== undefined ? !n.canonical && !n.folder : n.depth > 0)
|
||||
|
||||
export default defineComponent({
|
||||
name: 'StudioTree',
|
||||
props: {
|
||||
|
|
@ -209,13 +214,13 @@ export default defineComponent({
|
|||
if (!this.kept && this.secClosed[s.id]) continue
|
||||
for (const n of nodes) {
|
||||
if (!this.kept && !this.ancestorsOpen(n)) continue
|
||||
out.push({ ...n, sectionId: s.id, level: n.depth + 2, expandable: !!n.kids, alias: n.depth > 0 })
|
||||
out.push({ ...n, sectionId: s.id, level: n.depth + 2, expandable: !!n.kids, alias: isAlias(n) })
|
||||
}
|
||||
}
|
||||
return out
|
||||
},
|
||||
activeSectionId() {
|
||||
for (const s of this.sections) if (s.nodes.some(n => n.depth === 0 && n.target === this.active)) return s.id
|
||||
for (const s of this.sections) if (s.nodes.some(n => isCanonical(n) && n.target === this.active)) return s.id
|
||||
return ''
|
||||
}
|
||||
},
|
||||
|
|
@ -255,7 +260,7 @@ export default defineComponent({
|
|||
collapseAll() {
|
||||
this.nodeOpen = {}
|
||||
this.saveState()
|
||||
const a = this.visible.find(i => !i.section && i.target === this.active && i.depth === 0)
|
||||
const a = this.visible.find(i => !i.section && i.target === this.active && isCanonical(i))
|
||||
if (a) this.focusId = a.id
|
||||
},
|
||||
rowClass(item) {
|
||||
|
|
@ -325,9 +330,15 @@ export default defineComponent({
|
|||
if (!this.active || !this.sections.length) return
|
||||
let row = null
|
||||
for (const s of this.sections) {
|
||||
row = s.nodes.find(n => n.depth === 0 && n.target === this.active)
|
||||
row = s.nodes.find(n => isCanonical(n) && n.target === this.active)
|
||||
if (row) { if (this.secClosed[s.id]) { this.secClosed = { ...this.secClosed, [s.id]: false }; this.saveState() } break }
|
||||
}
|
||||
// A plugin inside a BUNDLE FOLDER: open the folder(s) above it, or it cannot be seen.
|
||||
if (row) {
|
||||
let p = row.parent, opened = false
|
||||
while (p) { if (!this.nodeOpen[p]) { this.nodeOpen = { ...this.nodeOpen, [p]: true }; opened = true } p = this.byId.get(p) && this.byId.get(p).parent }
|
||||
if (opened) this.saveState()
|
||||
}
|
||||
if (!row) return
|
||||
if (!this.focusId || !this.byId.has(this.focusId) || this.byId.get(this.focusId).target !== this.active) this.focusId = row.id
|
||||
this.$nextTick(() => { const el = this._rows && this._rows[row.id]; if (el && el.scrollIntoView) el.scrollIntoView({ block: 'nearest' }) })
|
||||
|
|
|
|||
|
|
@ -434,6 +434,11 @@ onMounted(async () => {
|
|||
map.value.addControl(new FullscreenToggleControl(profileRoot.value), 'bottom-right')
|
||||
}
|
||||
map.value.on('load', async () => {
|
||||
// The constructor's projection:'mercator' is only a request: on Mapbox the style's own projection
|
||||
// (globe) wins, so the map booted as a globe while this view's state and button said Mercator, and
|
||||
// the first "Switch to Globe" click did nothing (rega, C2). The style is ready here, on both
|
||||
// engines, so apply the state to the map.
|
||||
setProjectionFor(map.value, projection.value)
|
||||
try {
|
||||
// Shift logical center right of the 380px legend so the globe / pin
|
||||
// density isn't hidden behind the column.
|
||||
|
|
|
|||
|
|
@ -252,6 +252,11 @@ onMounted(async () => {
|
|||
map.value.addControl(new FullscreenToggleControl(profileRoot.value), 'bottom-right')
|
||||
}
|
||||
map.value.on('load', async () => {
|
||||
// The constructor's projection:'mercator' is only a request: on Mapbox the style's own projection
|
||||
// (globe) wins, so the map booted as a globe while this view's state and button said Mercator, and
|
||||
// the first "Switch to Globe" click did nothing (rega, C2). The style is ready here, on both
|
||||
// engines, so apply the state to the map.
|
||||
setProjectionFor(map.value, projection.value)
|
||||
try {
|
||||
// Legend now sits top-left under the search bar (~320px wide). Shift the
|
||||
// logical center right of that column so the world / globe isn't drawn
|
||||
|
|
|
|||
|
|
@ -410,6 +410,11 @@ onMounted(async () => {
|
|||
treeData.value = await treeResp.json()
|
||||
|
||||
map.value.on('load', async () => {
|
||||
// The constructor's projection:'mercator' is only a request: on Mapbox the style's own projection
|
||||
// (globe) wins, so the map booted as a globe while this view's state and button said Mercator, and
|
||||
// the first "Switch to Globe" click did nothing (rega, C2). The style is ready here, on both
|
||||
// engines, so apply the state to the map.
|
||||
setProjectionFor(map.value, projection.value)
|
||||
try {
|
||||
// Globe centering: shift the map's logical center right of the legend
|
||||
// so the globe doesn't render beneath the 380px legend column.
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import langFamilyByLanguage from '@map/data/langFamilyByLanguage.json'
|
|||
import bakedReached from '../data/reached-neighbors.json'
|
||||
import { useNeighborT } from '../composables/useNeighborT.js'
|
||||
import { setProjectionOn } from '@map/composables/useMapProvider.js'
|
||||
import { whenStyleReady } from '../whenStyleReady.js'
|
||||
const nt = useNeighborT()
|
||||
|
||||
const C_RED = '#e11d48', C_RED_DK = '#9f1239', C_NAVY = '#002e6e', C_GOLD = '#f59e0b'
|
||||
|
|
@ -279,7 +280,8 @@ async function fetchDoxa() {
|
|||
onMounted(async () => {
|
||||
await initializeMap()
|
||||
const m = map.value
|
||||
setProjectionOn(m, 'mercator') // flat mercator, never globe (Driver); per-engine shape via C2 helper
|
||||
// flat mercator, never globe (Driver); applied once the style is ready (MapLibre throws before)
|
||||
whenStyleReady(m, () => setProjectionOn(m, 'mercator'))
|
||||
try { doxaFeatures.value = await fetchDoxa() }
|
||||
catch (e) { statusMsg.value = 'Live doxa API unreachable — ' + e.message; appReady.value = true; return }
|
||||
appReady.value = true
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import { useMapInstance } from '@map/composables/useMapInstance.js'
|
|||
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
|
||||
import { useCockpitStore, COLORS, redColor, reachedColor, logRadius, fmtPop } from './useCockpit.js'
|
||||
import { setProjectionOn } from '@map/composables/useMapProvider.js'
|
||||
import { whenStyleReady } from '../whenStyleReady.js'
|
||||
|
||||
const store = useCockpitStore()
|
||||
|
||||
|
|
@ -365,7 +366,8 @@ onMounted(async () => {
|
|||
await initializeMap()
|
||||
const m = map.value
|
||||
if (!m) { return } // no/invalid token — overlay shows the hint, store still drives the rest
|
||||
setProjectionOn(m, 'mercator') // flat mercator, never globe (Driver); per-engine shape via C2 helper
|
||||
// flat mercator, never globe (Driver); applied once the style is ready (MapLibre throws before)
|
||||
whenStyleReady(m, () => setProjectionOn(m, 'mercator'))
|
||||
|
||||
// Gated test hook (inert in production): lets a harness use the live map.project().
|
||||
if (cfg.value.debugExpose && typeof window !== 'undefined') {
|
||||
|
|
|
|||
|
|
@ -389,6 +389,7 @@ function onMove(ev) { if (tip.value.show) { tip.value.x = ev.clientX; tip.value.
|
|||
import bakedReached from './data/reached-neighbors.json'
|
||||
import bakedAdmin1 from './data/admin1-membership.json'
|
||||
import { setProjectionOn } from '@map/composables/useMapProvider.js'
|
||||
import { whenStyleReady } from './whenStyleReady.js'
|
||||
async function loadJSON(relCandidates) {
|
||||
for (const url of relCandidates) {
|
||||
try { const r = await fetch(url); if (r.ok) return await r.json() } catch (_) {}
|
||||
|
|
@ -411,7 +412,8 @@ async function fetchDoxa() {
|
|||
onMounted(async () => {
|
||||
await initializeMap()
|
||||
const m = map.value
|
||||
setProjectionOn(m, 'mercator') // flat mercator, never globe (Driver); per-engine shape via C2 helper
|
||||
// flat mercator, never globe (Driver); applied once the style is ready (MapLibre throws before)
|
||||
whenStyleReady(m, () => setProjectionOn(m, 'mercator'))
|
||||
|
||||
// baked data (own bundle dir). `base` lets the host say where data/ is served from
|
||||
// (platform deploy serves data/ alongside the bundle, like send-map/flower-mesh).
|
||||
|
|
|
|||
|
|
@ -402,6 +402,7 @@ function flyToSender(n) {
|
|||
// profile-config `dataUrl` still overrides live.
|
||||
import bakedReached from './data/reached-neighbors.json'
|
||||
import { setProjectionOn } from '@map/composables/useMapProvider.js'
|
||||
import { whenStyleReady } from './whenStyleReady.js'
|
||||
async function loadDataset() {
|
||||
const cfgUrl = profileConfig?.value?.dataUrl
|
||||
if (cfgUrl) {
|
||||
|
|
@ -413,7 +414,8 @@ async function loadDataset() {
|
|||
onMounted(async () => {
|
||||
await initializeMap()
|
||||
const m = map.value
|
||||
setProjectionOn(m, 'mercator') // flat mercator, never globe (Driver); per-engine shape via C2 helper
|
||||
// flat mercator, never globe (Driver); applied once the style is ready (MapLibre throws before)
|
||||
whenStyleReady(m, () => setProjectionOn(m, 'mercator'))
|
||||
|
||||
// Test-only hook (inert in production — platform never sets debugExpose). Lets an
|
||||
// automated harness use the live map.project() to click exact circle positions.
|
||||
|
|
|
|||
13
plugin-designs/upg-neighbor/src/whenStyleReady.js
Normal file
13
plugin-designs/upg-neighbor/src/whenStyleReady.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
/**
|
||||
* whenStyleReady.js — run once the map's style has loaded (immediately if it already has).
|
||||
*
|
||||
* MapLibre throws "Style is not done loading" from setProjection before that, and setProjectionOn
|
||||
* swallows the throw, so the lenses that set 'mercator' right after initializeMap() never got it on
|
||||
* MapLibre (rega, C2: lang-cluster, send-map, the bloom cockpit's MapCell). Mapbox accepts it early,
|
||||
* which is why this only showed on one engine.
|
||||
*/
|
||||
export function whenStyleReady(map, fn) {
|
||||
if (!map) return
|
||||
if (typeof map.isStyleLoaded === 'function' && map.isStyleLoaded()) fn()
|
||||
else map.once('style.load', fn)
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <all-people-groups> custom element. -->
|
||||
<script src="../all-people-groups.js" integrity="sha384-yda1IvWyb6O5+st/W+s6qZMh+g/CUhXzARi4VZc/KjfXu7p/yzVjbpSsY1gCbMRW" crossorigin="anonymous"></script>
|
||||
<script src="../all-people-groups.js" integrity="sha384-8rqwLEQ0MCPH6srSqGcrf0r9Ca2UxpnHyrLiF0lKZJlXPHdnhMc0Dl6nY6cCeBmN" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@
|
|||
"title": "Gospel access by province — India and China",
|
||||
"why": "These two countries hold 47.9% of Doxa's roster (855 + 154 of 2,107). No other country is close; the third is Sudan at 105. A national fill cannot answer \"where would a team go\" for half the map.",
|
||||
"generated": "2026-09-26",
|
||||
"provinces_with_groups": 44,
|
||||
"provinces_available": 46,
|
||||
"groups_assigned": 998,
|
||||
"groups_unassigned": 11,
|
||||
"provinces_with_groups": 46,
|
||||
"provinces_available": 48,
|
||||
"groups_assigned": 1005,
|
||||
"groups_unassigned": 4,
|
||||
"india_note": "India is read from the COMPLETE ADM1 (36 states, geoBoundaries via DataMeet India, CC BY 2.5 IN). The first build of this table read gb-IND-ADM1.geojson, which is the UPG-100 list's curated 34-unit partition, and eleven groups came out with no province because they stood in states that partition does not draw. Switching the file moved 998 assigned to 1,005 and 44 province rows to 46, and turned the province names into India's own states.",
|
||||
"unassigned_note": "Four remain, each one legible rather than lost. Great Andamanese and the Sentinelese sit on small islands whose simplified coastline the point falls just outside — Andaman and Nicobar IS in the file, so this is geometry precision, not a missing province. Jing and Pana are on China's Vietnam and Laos borders, outside the twelve-province subset. They are listed in `unassigned` with their coordinates.",
|
||||
"china_note": "China's ADM1 file carries 12 provinces, not 31+. Measured rather than assumed: 152 of 154 Chinese groups fall inside them and all twelve are used, so the file is cut to the provinces where the unreached peoples are.",
|
||||
"ways_in_are_country_level": "The ways in are chosen for the COUNTRY, and going below the border is what exposes the cost of that. Arunachal Pradesh, in the far north-east, is offered TELUGU — a South Indian language — because the curated bridge table names it for those groups and a curated bridge outranks the computed one. India's computed default is Hindi (41%, official), which is defensible nationally and still wrong for much of the north-east. This table does not fix that: it makes it visible. Until a bridge can be chosen per province, read the ways-in column as \"a language with Scripture and film somewhere in this country\", not as a recommendation for this place.",
|
||||
"approximate": "A province is assigned by point-in-polygon on the group's coordinate, and that coordinate marks where a PEOPLE is found, not a settlement — 108 groups across the roster even share a coordinate with another group. Near a border the point can fall on the wrong side. Every row is flagged approximate and the unassigned are counted, never dropped. No surface may present a province as a fact about a group."
|
||||
|
|
@ -279,8 +281,8 @@
|
|||
"country": "India",
|
||||
"iso3": "IND",
|
||||
"province": "Mahārāshtra",
|
||||
"groups": 53,
|
||||
"people": 2858050,
|
||||
"groups": 54,
|
||||
"people": 2860150,
|
||||
"zero_access_groups": 0,
|
||||
"zero_access_people": 0,
|
||||
"zero_access_examples": "",
|
||||
|
|
@ -467,6 +469,18 @@
|
|||
"ways_in": "",
|
||||
"approximate": true
|
||||
},
|
||||
{
|
||||
"country": "India",
|
||||
"iso3": "IND",
|
||||
"province": "Puducherry",
|
||||
"groups": 5,
|
||||
"people": 41625,
|
||||
"zero_access_groups": 0,
|
||||
"zero_access_people": 0,
|
||||
"zero_access_examples": "",
|
||||
"ways_in": "",
|
||||
"approximate": true
|
||||
},
|
||||
{
|
||||
"country": "India",
|
||||
"iso3": "IND",
|
||||
|
|
@ -566,9 +580,21 @@
|
|||
{
|
||||
"country": "India",
|
||||
"iso3": "IND",
|
||||
"province": "Puducherry",
|
||||
"province": "Lakshadweep",
|
||||
"groups": 1,
|
||||
"people": 20500,
|
||||
"people": 74500,
|
||||
"zero_access_groups": 0,
|
||||
"zero_access_people": 0,
|
||||
"zero_access_examples": "",
|
||||
"ways_in": "",
|
||||
"approximate": true
|
||||
},
|
||||
{
|
||||
"country": "India",
|
||||
"iso3": "IND",
|
||||
"province": "Dādra and Nagar Haveli and Damān and Diu",
|
||||
"groups": 1,
|
||||
"people": 125,
|
||||
"zero_access_groups": 0,
|
||||
"zero_access_people": 0,
|
||||
"zero_access_examples": "",
|
||||
|
|
@ -589,13 +615,6 @@
|
|||
}
|
||||
],
|
||||
"unassigned": [
|
||||
{
|
||||
"country": "India",
|
||||
"iso3": "IND",
|
||||
"people_group": "Bharwad",
|
||||
"lat": 20.322175,
|
||||
"lng": 72.968458
|
||||
},
|
||||
{
|
||||
"country": "India",
|
||||
"iso3": "IND",
|
||||
|
|
@ -603,34 +622,6 @@
|
|||
"lat": 12.211728,
|
||||
"lng": 92.833491
|
||||
},
|
||||
{
|
||||
"country": "India",
|
||||
"iso3": "IND",
|
||||
"people_group": "Indo-French",
|
||||
"lat": 11.930182,
|
||||
"lng": 79.797144
|
||||
},
|
||||
{
|
||||
"country": "India",
|
||||
"iso3": "IND",
|
||||
"people_group": "Indo-Vietnamese",
|
||||
"lat": 11.912456,
|
||||
"lng": 79.815389
|
||||
},
|
||||
{
|
||||
"country": "India",
|
||||
"iso3": "IND",
|
||||
"people_group": "Laccadive",
|
||||
"lat": 10.0675,
|
||||
"lng": 73.6422
|
||||
},
|
||||
{
|
||||
"country": "India",
|
||||
"iso3": "IND",
|
||||
"people_group": "Sagharias",
|
||||
"lat": 20.71815,
|
||||
"lng": 70.988939
|
||||
},
|
||||
{
|
||||
"country": "India",
|
||||
"iso3": "IND",
|
||||
|
|
@ -638,20 +629,6 @@
|
|||
"lat": 11.557662,
|
||||
"lng": 92.267154
|
||||
},
|
||||
{
|
||||
"country": "India",
|
||||
"iso3": "IND",
|
||||
"people_group": "Vettalaikarans",
|
||||
"lat": 11.916308,
|
||||
"lng": 79.807019
|
||||
},
|
||||
{
|
||||
"country": "India",
|
||||
"iso3": "IND",
|
||||
"people_group": "Vettiyan",
|
||||
"lat": 11.91386,
|
||||
"lng": 79.814472
|
||||
},
|
||||
{
|
||||
"country": "China",
|
||||
"iso3": "CHN",
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -43,7 +43,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <bp-big-picture> custom element. -->
|
||||
<script src="../../big-picture.js" integrity="sha384-ol5VjnttxiwjEl9dlcOzHFYhGqUUFNfDm/FKV3zeR7YQCFOczRHasOGlJtdsRuuy" crossorigin="anonymous"></script>
|
||||
<script src="../../big-picture.js" integrity="sha384-Ii8bDqkNqNyPoL4R6n7nz0C7hBM4DggWRXcaFRSHrcKTjSgFYebrpeBfmbAKLCzx" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <bp-big-picture> custom element. -->
|
||||
<script src="../../big-picture.js" integrity="sha384-ol5VjnttxiwjEl9dlcOzHFYhGqUUFNfDm/FKV3zeR7YQCFOczRHasOGlJtdsRuuy" crossorigin="anonymous"></script>
|
||||
<script src="../../big-picture.js" integrity="sha384-Ii8bDqkNqNyPoL4R6n7nz0C7hBM4DggWRXcaFRSHrcKTjSgFYebrpeBfmbAKLCzx" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <bp-big-picture> custom element. -->
|
||||
<script src="../big-picture.js" integrity="sha384-ol5VjnttxiwjEl9dlcOzHFYhGqUUFNfDm/FKV3zeR7YQCFOczRHasOGlJtdsRuuy" crossorigin="anonymous"></script>
|
||||
<script src="../big-picture.js" integrity="sha384-Ii8bDqkNqNyPoL4R6n7nz0C7hBM4DggWRXcaFRSHrcKTjSgFYebrpeBfmbAKLCzx" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -32,7 +32,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <cities-internet-map> custom element. -->
|
||||
<script src="../cities-internet.js" integrity="sha384-pE5no47osDxGiWc1ES6HhtEhTjKnTs8BqTdZ3OfNoIBDCoZm+6ZeOKWxrHvaCSZT" crossorigin="anonymous"></script>
|
||||
<script src="../cities-internet.js" integrity="sha384-fn/XqzAvikU3wkpcVg0TllWNs4sP9V5ahWKYodEhmXO+eFTWfUkR+Qx+5Sz2moN9" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <cities-internet-map> custom element. -->
|
||||
<script src="../cities-internet.js" integrity="sha384-pE5no47osDxGiWc1ES6HhtEhTjKnTs8BqTdZ3OfNoIBDCoZm+6ZeOKWxrHvaCSZT" crossorigin="anonymous"></script>
|
||||
<script src="../cities-internet.js" integrity="sha384-fn/XqzAvikU3wkpcVg0TllWNs4sP9V5ahWKYodEhmXO+eFTWfUkR+Qx+5Sz2moN9" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <cities-internet-map> custom element. -->
|
||||
<script src="../cities-internet.js" integrity="sha384-pE5no47osDxGiWc1ES6HhtEhTjKnTs8BqTdZ3OfNoIBDCoZm+6ZeOKWxrHvaCSZT" crossorigin="anonymous"></script>
|
||||
<script src="../cities-internet.js" integrity="sha384-fn/XqzAvikU3wkpcVg0TllWNs4sP9V5ahWKYodEhmXO+eFTWfUkR+Qx+5Sz2moN9" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -36,7 +36,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-P37hAb5gyrk9w9aA4l9HFjM7FwjKlsHjHSeZVDDUZ4KddKM+wlbabTrZCgcVklWS" crossorigin="anonymous"></script>
|
||||
<script src="../country-posters.js" integrity="sha384-pSEjrRVgGp3ArV0+sMjER5pDVl/MZIQntQAc/DfrK7ENXykRDug54abbm3wcjOoI" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -32,7 +32,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-P37hAb5gyrk9w9aA4l9HFjM7FwjKlsHjHSeZVDDUZ4KddKM+wlbabTrZCgcVklWS" crossorigin="anonymous"></script>
|
||||
<script src="../country-posters.js" integrity="sha384-pSEjrRVgGp3ArV0+sMjER5pDVl/MZIQntQAc/DfrK7ENXykRDug54abbm3wcjOoI" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -47,7 +47,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-P37hAb5gyrk9w9aA4l9HFjM7FwjKlsHjHSeZVDDUZ4KddKM+wlbabTrZCgcVklWS" crossorigin="anonymous"></script>
|
||||
<script src="../country-posters.js" integrity="sha384-pSEjrRVgGp3ArV0+sMjER5pDVl/MZIQntQAc/DfrK7ENXykRDug54abbm3wcjOoI" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -47,7 +47,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <dashboard-plus-grid> custom element. -->
|
||||
<script src="../../dashboard-plus-grid.js" integrity="sha384-HlacN5eFJy1tUz5BXycPsWdFNXVKpOHxk+s4HCXZs9AndFI4jrZb3H//UwKO51Vh" crossorigin="anonymous"></script>
|
||||
<script src="../../dashboard-plus-grid.js" integrity="sha384-5Yvna6wDcKVmPHD5Gc+ed3n+AAxx4a2hM4IwHIb08bus3WNjC5Eq9mC4lxH+T2VM" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <dashboard-plus-grid> custom element. -->
|
||||
<script src="../dashboard-plus-grid.js" integrity="sha384-HlacN5eFJy1tUz5BXycPsWdFNXVKpOHxk+s4HCXZs9AndFI4jrZb3H//UwKO51Vh" crossorigin="anonymous"></script>
|
||||
<script src="../dashboard-plus-grid.js" integrity="sha384-5Yvna6wDcKVmPHD5Gc+ed3n+AAxx4a2hM4IwHIb08bus3WNjC5Eq9mC4lxH+T2VM" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue