Compare commits
3 commits
main
...
country-po
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fe29c8ad0 | ||
|
|
3be2ea856b | ||
|
|
940c65e85c |
19 changed files with 294 additions and 23 deletions
|
|
@ -63,8 +63,19 @@ const HAND_VOCAB = {
|
|||
'Project 700',
|
||||
'How this list was found',
|
||||
],
|
||||
// The phrases that NAME a series. Derived vocabulary drops any sentence two series share, so a verbatim
|
||||
// copy of one series' framing into another would go unseen without these (the copy becomes "shared").
|
||||
country: [
|
||||
'PRAY FOR THIS COUNTRY',
|
||||
'Country Prayer Map',
|
||||
'one sheet for each country',
|
||||
'which country God has put',
|
||||
],
|
||||
indiaState: [
|
||||
'PRAY FOR THIS STATE',
|
||||
'State Prayer Map',
|
||||
'one sheet for each state',
|
||||
'which state God has put',
|
||||
],
|
||||
}
|
||||
|
||||
|
|
@ -100,13 +111,13 @@ function deriveVocab(series) {
|
|||
...strings(e.poster || {}),
|
||||
].map((s) => s.trim()).filter((s) => s.length >= 12))
|
||||
}
|
||||
// Drop anything two series share — it cannot identify either of them.
|
||||
for (const a of ids) {
|
||||
for (const b of ids) {
|
||||
if (a === b) continue
|
||||
for (const s of own[b]) own[a].delete(s)
|
||||
}
|
||||
}
|
||||
// Drop anything two series share — it cannot identify either of them. The shared set is found on the
|
||||
// ORIGINAL sets first: deleting while still comparing pairs removed a shared string from the earlier
|
||||
// series only, so the later one kept it as a "fingerprint" and the earlier one failed for carrying it.
|
||||
// Invisible with two series; a third (indiaState, sharing country's networks) exposed it.
|
||||
const shared = new Set()
|
||||
for (const a of ids) for (const b of ids) if (a !== b) for (const s of own[a]) if (own[b].has(s)) shared.add(s)
|
||||
for (const id of ids) for (const s of shared) own[id].delete(s)
|
||||
const out = {}
|
||||
for (const id of ids) out[id] = [...new Set([...(HAND_VOCAB[id] || []), ...own[id]])]
|
||||
return out
|
||||
|
|
@ -120,8 +131,10 @@ function* strings(v) {
|
|||
}
|
||||
|
||||
/** The text a series OWNS: its overrides and its poster furniture. Not its prose documentation. */
|
||||
// The poster header block too: that is where the country poster once printed the UPG-100 side box, and a scan
|
||||
// of strings + networks alone could never see a series' framing copied into another's poster text.
|
||||
function seriesText(entry) {
|
||||
return [...strings(entry.strings || {}), ...strings(entry.networks || [])]
|
||||
return [...strings(entry.strings || {}), ...strings(entry.networks || []), ...strings(entry.poster || {})]
|
||||
}
|
||||
|
||||
function checkDeclarations(series) {
|
||||
|
|
@ -208,6 +221,9 @@ function selfTest() {
|
|||
['upg100 loses its funnel', (s) => { s.upg100.funnel = false }],
|
||||
['a series loses a networks block', (s) => { s.country.networks.pop() }],
|
||||
['dataDir points nowhere', (s) => { s.country.dataDir = 'no/such/folder' }],
|
||||
['indiaState keeps the country side box', (s) => { s.indiaState.poster.sideBoxHeading = 'What is a Country Prayer Map?' }],
|
||||
['country keeps the state network line', (s) => { s.country.networks[0][1] = s.indiaState.networks[0][1] }],
|
||||
['indiaState keeps PRAY FOR THIS COUNTRY', (s) => { s.indiaState.strings.map_page_kicker = 'PRAY FOR THIS COUNTRY' }],
|
||||
]
|
||||
let pass = 0
|
||||
for (const [name, mutate] of cases) {
|
||||
|
|
@ -224,6 +240,19 @@ function selfTest() {
|
|||
fails.length = 0
|
||||
if (pass !== cases.length) fail(`self-test: caught ${pass}/${cases.length} known-bad inputs`)
|
||||
else ok(`self-test: caught all ${cases.length} known-bad inputs`)
|
||||
// Shared wording is nobody's fingerprint, in EITHER order: three series where the first and last share a
|
||||
// sentence must neither flag it nor lose their own phrases.
|
||||
const SHARED = 'A sentence two series both print, word for word.'
|
||||
const tri = {
|
||||
a: { strings: { k: 'Only series a says this phrase.' }, networks: [['h', SHARED]] },
|
||||
b: { strings: { k: 'Only series b says this phrase.' }, networks: [] },
|
||||
c: { strings: { k: 'Only series c says this phrase.' }, networks: [['h', SHARED]] },
|
||||
}
|
||||
const v = deriveVocab(tri)
|
||||
const leaked = Object.entries(v).filter(([, p]) => p.includes(SHARED)).map(([id]) => id)
|
||||
const lost = ['a', 'b', 'c'].filter((id) => !v[id].includes(`Only series ${id} says this phrase.`))
|
||||
if (leaked.length || lost.length) fail(`self-test: shared wording kept as vocabulary of [${leaked}] / own phrases lost for [${lost}]`)
|
||||
else ok('self-test: shared wording is dropped from every series, whatever their order')
|
||||
}
|
||||
|
||||
// ── run ──────────────────────────────────────────────────────────────────────
|
||||
|
|
|
|||
4
library/i18n/locales/ar/countryPosters.json
Normal file
4
library/i18n/locales/ar/countryPosters.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"indiaStatesHeading": "الهند — الولايات والأقاليم الاتحادية",
|
||||
"indiaStateNoGroups": "لا توجد هنا شعوب غير مُبشَّرة مُدرَجة."
|
||||
}
|
||||
4
library/i18n/locales/de/countryPosters.json
Normal file
4
library/i18n/locales/de/countryPosters.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"indiaStatesHeading": "Indien – Bundesstaaten und Unionsterritorien",
|
||||
"indiaStateNoGroups": "Hier sind keine unerreichten Volksgruppen verzeichnet."
|
||||
}
|
||||
4
library/i18n/locales/en/countryPosters.json
Normal file
4
library/i18n/locales/en/countryPosters.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"indiaStatesHeading": "India — states and union territories",
|
||||
"indiaStateNoGroups": "No unreached people groups are listed here."
|
||||
}
|
||||
4
library/i18n/locales/es/countryPosters.json
Normal file
4
library/i18n/locales/es/countryPosters.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"indiaStatesHeading": "India: estados y territorios de la unión",
|
||||
"indiaStateNoGroups": "Aquí no figura ningún pueblo no alcanzado."
|
||||
}
|
||||
4
library/i18n/locales/fr/countryPosters.json
Normal file
4
library/i18n/locales/fr/countryPosters.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"indiaStatesHeading": "Inde — États et territoires de l’Union",
|
||||
"indiaStateNoGroups": "Aucun peuple non atteint n’est répertorié ici."
|
||||
}
|
||||
4
library/i18n/locales/hi/countryPosters.json
Normal file
4
library/i18n/locales/hi/countryPosters.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"indiaStatesHeading": "भारत — राज्य और केंद्र शासित प्रदेश",
|
||||
"indiaStateNoGroups": "यहाँ कोई अपहुँचा जनसमूह सूचीबद्ध नहीं है।"
|
||||
}
|
||||
4
library/i18n/locales/it/countryPosters.json
Normal file
4
library/i18n/locales/it/countryPosters.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"indiaStatesHeading": "India — stati e territori dell’Unione",
|
||||
"indiaStateNoGroups": "Qui non è elencato alcun popolo non raggiunto."
|
||||
}
|
||||
4
library/i18n/locales/pt/countryPosters.json
Normal file
4
library/i18n/locales/pt/countryPosters.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"indiaStatesHeading": "Índia — estados e territórios da união",
|
||||
"indiaStateNoGroups": "Nenhum povo não alcançado está listado aqui."
|
||||
}
|
||||
4
library/i18n/locales/ro/countryPosters.json
Normal file
4
library/i18n/locales/ro/countryPosters.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"indiaStatesHeading": "India — state și teritorii ale Uniunii",
|
||||
"indiaStateNoGroups": "Aici nu este listat niciun popor neatins."
|
||||
}
|
||||
4
library/i18n/locales/ru/countryPosters.json
Normal file
4
library/i18n/locales/ru/countryPosters.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"indiaStatesHeading": "Индия — штаты и союзные территории",
|
||||
"indiaStateNoGroups": "Здесь не указано ни одного недостигнутого народа."
|
||||
}
|
||||
4
library/i18n/locales/zh/countryPosters.json
Normal file
4
library/i18n/locales/zh/countryPosters.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"indiaStatesHeading": "印度——邦与中央直辖区",
|
||||
"indiaStateNoGroups": "此处未列出任何未得之民族群。"
|
||||
}
|
||||
|
|
@ -58,8 +58,9 @@
|
|||
* keeps the renderer's own default rather than a fabricated position.
|
||||
*
|
||||
* A null sheet is a tier too: India (sheet null, too many groups for any paper size) is its own
|
||||
* 1-of-1 series. Skipping it printed "map 1 of 0" on India's packet page 1 from the Regions tab
|
||||
* while the wall's own copy printed "map 1 of 1" -- the two paths disagreeing again.
|
||||
* 1-of-1 series. Skipping it gave India's sheet "map 1 of 0" on the Regions path and "map 1 of 1" on the
|
||||
* wall's old copy. Neither was visible then (the map-only packet page prints no series line, and India's
|
||||
* poster is gated), but any India sheet with a header would have shown it.
|
||||
* Every null-sheet country shares that ONE oversized series: a second would make India "map 1 of 2".
|
||||
* check:country-sheet-one-path fails when that happens, so it is decided, not drifted into.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -143,5 +143,49 @@
|
|||
"emit-countries.mjs), so the SHAPE was replaced: not 'split the set' but 'ask which one,",
|
||||
"then give them that poster'. The numbers are gone from the headings for the same reason."
|
||||
]
|
||||
},
|
||||
"indiaState": {
|
||||
"label": "India states and union territories — one poster for each of the 34 with people groups listed",
|
||||
"dataDir": "plugin-designs/my-upg-100-list/data",
|
||||
"countryMode": true,
|
||||
"funnel": false,
|
||||
"poster": {
|
||||
"kicker": "Unreached People Groups",
|
||||
"titleQualifier": "Pray for",
|
||||
"seriesName": "India State Maps",
|
||||
"sideBoxHeading": "What is a State Prayer Map?",
|
||||
"sideBoxBody": "This map is one of a {approxLists}-map set called the {seriesName} — one sheet for each state and union territory of India, naming the unreached people groups who live there so a reader can pray for them by name."
|
||||
},
|
||||
"_poster_why": [
|
||||
"A state poster is a country poster one level down: the Country Posters wall prints one for each of India's",
|
||||
"states and union territories that has people groups listed (34 of 36; data/people-groups/india-states.json).",
|
||||
"The country series' text cannot be reused: it would call Gujarāt's poster one of the ~170 'Country Maps',",
|
||||
"one sheet for each country. The term is 'state and union territory', never 'province'. Approved as the",
|
||||
"default wording 2026-09-30 and shown as editable; a change is a strings-only follow-up."
|
||||
],
|
||||
"strings": {
|
||||
"map_page_kicker": "PRAY FOR THIS STATE",
|
||||
"map_page_sub": "A prayer list of {count} unreached people groups who live here",
|
||||
"points_intro": "One hour with God, five minutes each. Pray these twelve for the peoples of this state.",
|
||||
"christian_lede": "This is the same grid — one human life in weeks. Now each box is a share of the people in this state. The shaded boxes are the evangelical share of their population; the rest are waiting."
|
||||
},
|
||||
"_strings_why": [
|
||||
"The country series' four keys with 'country' read as 'state'. Used by the Node generator; the browser",
|
||||
"packet reads the localized packet-strings.json, which is series-invariant."
|
||||
],
|
||||
"networks": [
|
||||
[
|
||||
"1 · Disciples.",
|
||||
"Ask a disciple which state God has put on their heart, then give them that state's poster to print and prepare."
|
||||
],
|
||||
[
|
||||
"2 · Churches.",
|
||||
"List the churches you know and ask each the same question, so every church has a poster to pray over together."
|
||||
],
|
||||
[
|
||||
"3 · Indigenous leaders.",
|
||||
"Ask which leaders you know who live in one of these states, and give them their own state's map to pray for it specifically."
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -449,7 +449,7 @@
|
|||
"check:composite-token-url": "a composite embedded BARE (no outer ProfileLoader) must honour a tokenUrl-only config: it reads tokenUrl, forwards it, and asks the token service only when no token is given. The expectation is derived from ProfileLoader, not restated. Headless; in gates:pre.",
|
||||
"check:composites-mount": "every capability-4/5 composite's OWN setup() and mounted hooks run without throwing, in en and ar, headless (no browser): vite loads each composite as its bundle entry defines it (configureApp, and ProfileLoader's provides where the entry mounts ProfileLoader), and @vue/runtime-core mounts it on a no-op renderer. Exists because 08f89f55 shipped two composites calling getRuntimeLocale/detectLocale/RTL_LOCALES with no import: the build was green, setup threw a ReferenceError in every locale, and Big Picture and Simple Research rendered nothing. Proves setup runs; cannot prove the children render.",
|
||||
"check:country-names": "every ISO 3166-1 alpha-3 our data uses must resolve to a localized country name. Guards the JOIN, not Intl: library/data/iso3-to-alpha2.json drifting from library/data/countries.json makes a country render as a bare code, which still looks like a value. Checks en/ar/zh (ar is our only RTL locale; zh makes an untranslated Latin code unmistakable) and asserts BOTH directions of drift. Carries two negative arms — an unassigned code and a malformed one — and FAILS if either resolves, because a coverage count cannot otherwise tell a hit from a lookup that returns its input.",
|
||||
"check:country-sheet-one-path": "a country sheet is drawn through ONE path, @map/packet/countrySheet.js, so one country cannot print two posters. Parses (comments cannot count) every plugin source naming generateCountrySheet/makeCountrySheet: a direct generateCountrySheet in code FAILS, every makeCountrySheet call must pass `countries` (the series list), and both known callers (the Country Posters wall, regions-list) must be found. Also runs seriesFor over the real country-posters.json with named answers: India 1 of 1, every 3x3 country N of the 3x3 tier with no repeat, a code absent from the data no series.",
|
||||
"check:country-sheet-one-path": "a country sheet, and an Indian state or union territory sheet, each draw through ONE path (@map/packet/countrySheet.js; country-posters/src/indiaStateSheet.js), so one place cannot print two posters. Parses (comments cannot count) plugin sources: a direct generateCountrySheet FAILS anywhere, a direct generateSheet FAILS inside country-posters outside the state helper, every makeCountrySheet call must pass `countries`, and the known callers must be found. Named data answers: seriesFor over country-posters.json (India 1 of 1, each 3x3 country N of 173, an absent code no series, exactly one no-sheet country); over the served india-states.json + gb-IND-ADM1.geojson: 36 divisions, the 34 with groups numbered 1..34 each with an outline, the rest named, the shared ADM1 never mutated.",
|
||||
"check:country-sheet-one-path:self-test": "plants each defect check:country-sheet-one-path guards (a direct generateCountrySheet call, makeCountrySheet without countries, the old guard that skipped India, a position given to an absent code) and requires each to FAIL; a comment naming generateCountrySheet must NOT.",
|
||||
"check:coverage-consistent": "the gospel-resources surfaces must agree: no resource on an ISO RESERVED code (und/mul/mis/zxx), every resourced code NAMED, table reach recounted off the pins, no invented URL, tiers reconcile. Static, sub-second. Self-test: `bun run check:coverage-consistent:poison`, all 12 clauses (A-L) must fail. Clause L is the one that is not about consistency: it fails when the link-verification record goes stale, because 'link checked' is a claim about the present tense. Written after the same drift defect was found by hand three times.",
|
||||
"check:csv-shape": "the export must OPEN as a table: header on line 0, every row matching. Run the `check:csv-shape:self-test` script for the NEGATIVE test (forces the provenance comment back to line 0 IN MEMORY and requires this gate to REJECT it) — the header used to claim 'NEGATIVE-TESTED' with no way to re-run it.",
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@
|
|||
* The shared wall always sorts sections AND cards by descending count, so the
|
||||
* dense countries lead and the long tail sinks. There is no floor.
|
||||
*/
|
||||
import { computed, inject, ref } from 'vue'
|
||||
import { computed, inject, ref, watch } from 'vue'
|
||||
import PosterWall from '@map/components/poster/PosterWall.vue'
|
||||
import { useOptionalT, useOptionalI18n } from '@map/composables/useOptionalT.js'
|
||||
// A country's name in the reader's language (Intl, joined from our ISO-3 codes).
|
||||
|
|
@ -49,6 +49,9 @@ import { getRuntimeLocale, detectLocale } from '@map/i18n/index.js'
|
|||
import data from '@map/data/country-posters.json'
|
||||
import { resolveAssetsBase } from '@map/utils/embedBase.js'
|
||||
import { makeCountrySheet } from '@map/packet/countrySheet.js'
|
||||
// India's states and union territories: the shared table (fetched when the wall shows them) and their sheet.
|
||||
import { loadIndiaStates } from '@map/data/indiaStates.js'
|
||||
import { makeIndiaStateSheet, statesWithPosters } from './src/indiaStateSheet.js'
|
||||
// The regions catalogue holds the not-yet sentence a host's country gets when no poster exists (all eleven locales);
|
||||
// its keys are English sentences with dots, so it is read by literal key, the way regions-list.vue reads it.
|
||||
import { useRegionsI18n } from '@map/i18n/useRegionsI18n.js'
|
||||
|
|
@ -105,15 +108,36 @@ const hostColumns = computed(() => Number(profileConfig.value?.columns) || 2)
|
|||
let tRegions = (key) => key
|
||||
try { tRegions = useRegionsI18n().t } catch (_) { /* a host with no i18n reads the English sentence, like every string here */ }
|
||||
|
||||
// ★ INDIA'S STATES AND UNION TERRITORIES, one poster each, like a country one level down. The table is
|
||||
// fetched (and sha-checked) only when the unfiltered wall is shown; a host's filtered wall never asks for it.
|
||||
// A failure hides the section rather than showing cards that cannot print. States with no people groups
|
||||
// listed (Nagaland, Mizoram) are NAMED with a card that says so, never silently left out.
|
||||
const indiaStates = ref(null)
|
||||
watch(filtered, (f) => {
|
||||
if (f || indiaStates.value) return
|
||||
loadIndiaStates(assetsBase.value).then((table) => { indiaStates.value = table.states }).catch(() => { indiaStates.value = null })
|
||||
}, { immediate: true })
|
||||
const INDIA_REGION = (byIso3.get('IND') || {}).region || 'Asia'
|
||||
const stateItems = computed(() => {
|
||||
const all = indiaStates.value
|
||||
if (filtered.value || !all) return []
|
||||
const withGroups = statesWithPosters(all).map((st) => ({
|
||||
id: st.id, label: st.name, count: st.count, sheet: '3x3', region: INDIA_REGION, indiaState: true, slugs: st.slugs,
|
||||
}))
|
||||
const none = all.filter((st) => !st.count).map((st) => ({ id: st.id, label: st.name, count: 0, indiaState: true, noGroups: true, slugs: [] }))
|
||||
return [...withGroups, ...none]
|
||||
})
|
||||
|
||||
const items = computed(() => {
|
||||
const l = wallLocale && (wallLocale.value ?? wallLocale)
|
||||
const locale = (typeof l === 'string' && l) || getRuntimeLocale() || detectLocale() || 'en'
|
||||
const countries = selected.value
|
||||
if (/^en\b/i.test(locale)) return countries
|
||||
return countries.map((c) => {
|
||||
// State names stay as the table has them (place names from the data); only country names are localized.
|
||||
if (/^en\b/i.test(locale)) return [...countries, ...stateItems.value]
|
||||
return [...countries.map((c) => {
|
||||
const name = countryName(c.iso3, locale)
|
||||
return name && name !== c.iso3 ? { ...c, label: name } : c
|
||||
})
|
||||
}), ...stateItems.value]
|
||||
})
|
||||
|
||||
// ★ EVERY COUNTRY PRINTS ITS PRAYER RESOURCES — the list, the card deck and the packet — through the
|
||||
|
|
@ -125,7 +149,9 @@ const recordsFor = createPeopleGroupRecords({
|
|||
lang: () => getRuntimeLocale() || detectLocale(),
|
||||
profileConfig,
|
||||
})
|
||||
const countryRecords = (c) => recordsFor({ type: 'country', values: [c.iso3] })
|
||||
const countryRecords = (c) => (c.indiaState
|
||||
? (c.slugs.length ? recordsFor({ type: 'pg_slugs', values: c.slugs }) : Promise.resolve([]))
|
||||
: recordsFor({ type: 'country', values: [c.iso3] }))
|
||||
// ★ GENERATED AT THE CLICK, through the ONE shared path (@map/packet/countrySheet.js, which the Regions
|
||||
// tab uses too): the country's sheet is drawn in the browser from its people groups and the world
|
||||
// boundaries, for the poster print (letter, vector) and the packet's page 1. No stored sheet exists for a
|
||||
|
|
@ -139,7 +165,12 @@ const sheetForRecord = makeCountrySheet({
|
|||
assetsBase: () => assetsBase.value,
|
||||
countries: data.countries || [],
|
||||
})
|
||||
const countrySheet = (card, format) => sheetForRecord(byId.get(card.id) || card, format)
|
||||
const countryOnlySheet = (card, format) => sheetForRecord(byId.get(card.id) || card, format)
|
||||
// A state's sheet through its own ONE path (src/indiaStateSheet.js), in India's hue and the indiaState series.
|
||||
const stateSheet = makeIndiaStateSheet({
|
||||
records: recordsFor, assetsBase: () => assetsBase.value, states: () => indiaStates.value, region: INDIA_REGION,
|
||||
})
|
||||
const countrySheet = (card, format) => (card.indiaState ? stateSheet(card, format) : countryOnlySheet(card, format))
|
||||
// The name table the generated sheet was labelled from, so a packet in another language relabels page 1's
|
||||
// country names (the same seam the UPG-100 wall passes).
|
||||
const iso3Names = () => import('@map/data/iso3-name.js').then((m) => m.ISO3_NAME)
|
||||
|
|
@ -161,6 +192,7 @@ async function countryMap(c) {
|
|||
const t = useOptionalT('')
|
||||
const regionSlug = (label) => String(label).toLowerCase().replace(/\s+/g, '_')
|
||||
const groupBy = (c) => {
|
||||
if (c.indiaState) return t('countryPosters.indiaStatesHeading')
|
||||
if (!c.region || c.region === 'Other') return t('dashboard.other')
|
||||
const key = `dashboard.wagfRegion.${regionSlug(c.region)}`
|
||||
const v = t(key)
|
||||
|
|
@ -178,7 +210,7 @@ const collapsedBy = (sec) => sec.total < 60
|
|||
// unsupportedReason is an English note (and a flag), not display text. The card already states the
|
||||
// count above it, so the sentence needs no number and no plural agreement.
|
||||
// A requested country with no poster at all says so in regions-list.vue's words (the regions catalogue).
|
||||
const unsupported = (c) => (c.noPoster ? tRegions('No poster has been generated for this country yet.') : c.unsupportedReason ? t('messages.countryPosterTooMany') : null)
|
||||
const unsupported = (c) => (c.noGroups ? t('countryPosters.indiaStateNoGroups') : c.noPoster ? tRegions('No poster has been generated for this country yet.') : c.unsupportedReason ? t('messages.countryPosterTooMany') : null)
|
||||
|
||||
// ★ THROUGH THE CATALOGUE, like the UPG-100 wall: the heading, the tag, the intro and the counts read
|
||||
// in the page's language. The intro keeps its one emphasis as three plain keys (A, the emphasis, B)
|
||||
|
|
|
|||
|
|
@ -183,7 +183,10 @@ function onShowOnMap(e) {
|
|||
// The attribute carries the country ID, not its geometry: countries-map resolves the id against
|
||||
// the same library/data/country-posters.json the wall renders from, so the bbox has exactly one
|
||||
// source of truth rather than being copied through an attribute.
|
||||
const countryId = d.listId ?? null
|
||||
// An Indian state's card (the state table's ids are 'india-<code>') shows India: this map draws countries,
|
||||
// not states, so framing the country is the honest answer rather than a world view with nothing selected.
|
||||
const listId = d.listId ?? null
|
||||
const countryId = typeof listId === 'string' && listId.startsWith('india-') ? 'country-ind' : listId
|
||||
const el = paneEls.map
|
||||
if (!el) return
|
||||
if (countryId) el.setAttribute('select-country', String(countryId))
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@ import { parse as parseSfc, babelParse } from '@vue/compiler-sfc'
|
|||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..')
|
||||
const PD = join(ROOT, 'plugin-designs')
|
||||
const KNOWN_CALLERS = ['plugin-designs/country-posters/country-poster-wall.vue', 'plugin-designs/regions-dashboard/regions-list.vue']
|
||||
// India's states and union territories draw through their own ONE path; inside this plugin only that helper may
|
||||
// call the generator directly.
|
||||
const STATE_HELPER = 'plugin-designs/country-posters/src/indiaStateSheet.js'
|
||||
const STATE_CALLER = 'plugin-designs/country-posters/country-poster-wall.vue'
|
||||
|
||||
function scriptsOf(file, src) {
|
||||
if (!file.endsWith('.vue')) return [src]
|
||||
|
|
@ -78,9 +82,11 @@ function listSources(dir, out = []) {
|
|||
export function armA(files, callers = []) {
|
||||
const fails = []
|
||||
for (const [rel, src] of Object.entries(files)) {
|
||||
if (!/generateCountrySheet|makeCountrySheet/.test(src)) continue // a text prefilter; the parse decides
|
||||
if (!/generateCountrySheet|makeCountrySheet|generateSheet|makeIndiaStateSheet/.test(src)) continue // a text prefilter; the parse decides
|
||||
const { names, makeCalls } = codeFacts(rel, src)
|
||||
if (names.has('generateCountrySheet')) fails.push(`${rel}: draws a country sheet directly (generateCountrySheet in code); use makeCountrySheet from @map/packet/countrySheet.js`)
|
||||
if (rel.startsWith('plugin-designs/country-posters/') && rel !== STATE_HELPER && names.has('generateSheet')) fails.push(`${rel}: calls generateSheet directly; a state sheet goes through makeIndiaStateSheet (${STATE_HELPER})`)
|
||||
if (rel === STATE_CALLER && names.has('makeIndiaStateSheet')) callers.push(`${rel}#state`)
|
||||
if (makeCalls.length) callers.push(rel)
|
||||
makeCalls.forEach((c, i) => { if (!c.hasCountries) fails.push(`${rel}: makeCountrySheet call #${i + 1} passes no \`countries\`, so its "map N of M" is not the wall's`) })
|
||||
}
|
||||
|
|
@ -111,6 +117,23 @@ export function armB(seriesFor, countries) {
|
|||
return fails
|
||||
}
|
||||
|
||||
// ARM C: the state posters over the SERVED table and the renderer's own ADM1 file (named expectations).
|
||||
export function armC(H, states, adm1Ind) {
|
||||
const fails = []
|
||||
const before = JSON.stringify((adm1Ind.features || []).map((f) => f.properties && f.properties.listId)) // BEFORE any call
|
||||
const withGroups = H.statesWithPosters(states)
|
||||
const zero = (states || []).filter((st) => !st.count).map((st) => st.name)
|
||||
if ((states || []).length !== 36) fails.push(`the state table has ${(states || []).length} divisions, expected 36 (28 states + 8 union territories)`)
|
||||
if (withGroups.length + zero.length !== (states || []).length) fails.push('every division is either postered or named as having none')
|
||||
const idx = withGroups.map((st) => (H.stateSeries(states, st) || {}).index)
|
||||
if (new Set(idx).size !== withGroups.length || Math.min(...idx) !== 1 || Math.max(...idx) !== withGroups.length) fails.push(`state series indexes are not 1..${withGroups.length}, unique: ${idx.join(',')}`)
|
||||
const noShape = withGroups.filter((st) => !H.stateGeometry(H.stateZone(st, '#000'), adm1Ind, st.shapeName)).map((st) => st.name)
|
||||
if (noShape.length) fails.push(`states with people groups but no outline in gb-IND-ADM1.geojson (their poster would not draw): ${noShape.join(', ')}`)
|
||||
withGroups.forEach((st) => H.stateGeometry(H.stateZone(st, '#000'), adm1Ind, st.shapeName))
|
||||
if (JSON.stringify((adm1Ind.features || []).map((f) => f.properties && f.properties.listId)) !== before) fails.push('stateGeometry mutated the shared ADM1 collection')
|
||||
return fails
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const selfTest = process.argv.includes('--self-test')
|
||||
const { seriesFor } = await import(join(ROOT, 'library/packet/countrySheet.js'))
|
||||
|
|
@ -118,6 +141,11 @@ async function main() {
|
|||
const countries = data.countries || []
|
||||
if (countries.length < 170) { console.log(`NOT MEASURED: country-posters.json parsed ${countries.length} countries (floor 170)`); process.exitCode = 75; return }
|
||||
|
||||
const H = await import(join(ROOT, STATE_HELPER))
|
||||
const states = JSON.parse(readFileSync(join(ROOT, 'public/dx-maps/assets/people-groups/india-states.json'), 'utf8')).states || []
|
||||
const adm1Ind = JSON.parse(readFileSync(join(ROOT, 'public/dx-maps/assets/boundaries/gb-IND-ADM1.geojson'), 'utf8'))
|
||||
if (states.length < 30 || (adm1Ind.features || []).length < 30) { console.log(`NOT MEASURED: state table ${states.length} / ADM1 ${(adm1Ind.features || []).length} features (floor 30)`); process.exitCode = 75; return }
|
||||
|
||||
const files = {}
|
||||
for (const p of listSources(PD)) files[relative(ROOT, p)] = readFileSync(p, 'utf8')
|
||||
|
||||
|
|
@ -129,6 +157,10 @@ async function main() {
|
|||
['India skipped (the old guard)', () => armB((l, it) => (!it || !it.sheet ? undefined : seriesFor(l, it)), countries)],
|
||||
['absent code given a position', () => armB((l, it) => seriesFor(l, it) || { index: 1, totalLists: 1, totalGroups: 0 }, countries)],
|
||||
['a second no-sheet country joins India', () => armB(seriesFor, countries.map((c) => (c.iso3 === 'CHN' ? { ...c, sheet: null } : c)))],
|
||||
['the wall calls generateSheet directly', () => armA({ ...files, [wall]: files[wall].replace('<script setup>', "<script setup>\nimport { generateSheet } from '@map/packet/posterSheets.js'\ngenerateSheet({})") })],
|
||||
['a state with groups loses its outline', () => armC(H, states.map((st) => (st.id === 'india-gj' ? { ...st, shapeName: 'Nowhere' } : st)), adm1Ind)],
|
||||
['a division silently dropped', () => armC(H, states.filter((st) => st.id !== 'india-mz'), adm1Ind)],
|
||||
['a helper that mutates the shared ADM1', () => armC({ ...H, stateGeometry: (z, a, n) => { (a.features || []).forEach((f) => { if (f.properties.shapeName === n) f.properties.listId = z.id }); return H.stateGeometry(z, a, n) } }, states, JSON.parse(JSON.stringify(adm1Ind)))],
|
||||
]
|
||||
let bad = 0
|
||||
for (const [name, run] of cases) {
|
||||
|
|
@ -145,12 +177,12 @@ async function main() {
|
|||
}
|
||||
|
||||
const callers = []
|
||||
const fails = [...armA(files, callers), ...armB(seriesFor, countries)]
|
||||
const missing = KNOWN_CALLERS.filter((k) => !callers.includes(k))
|
||||
const fails = [...armA(files, callers), ...armB(seriesFor, countries), ...armC(H, states, adm1Ind)]
|
||||
const missing = [...KNOWN_CALLERS, `${STATE_CALLER}#state`].filter((k) => !callers.includes(k))
|
||||
if (missing.length) fails.push(`known callers not found calling makeCountrySheet: ${missing.join(', ')}`)
|
||||
if (fails.length) { fails.forEach((f) => console.log(`FAIL ${f}`)); process.exitCode = 1; return }
|
||||
const ind = seriesFor(countries, countries.find((c) => c.iso3 === 'IND'))
|
||||
console.log(`PASS check:country-sheet-one-path — ${Object.keys(files).length} plugin sources parsed-or-skipped, makeCountrySheet callers: ${callers.join(', ')}; seriesFor over ${countries.length} countries (India ${ind.index} of ${ind.totalLists})`)
|
||||
console.log(`PASS check:country-sheet-one-path — ${Object.keys(files).length} plugin sources parsed-or-skipped, makeCountrySheet callers: ${callers.join(', ')}; seriesFor over ${countries.length} countries (India ${ind.index} of ${ind.totalLists}); ${H.statesWithPosters(states).length} state posters, series 1..${H.statesWithPosters(states).length}, every outline found`)
|
||||
}
|
||||
|
||||
main().catch((e) => { console.error(`FAIL ${e.stack || e}`); process.exitCode = 1 })
|
||||
|
|
|
|||
86
plugin-designs/country-posters/src/indiaStateSheet.js
Normal file
86
plugin-designs/country-posters/src/indiaStateSheet.js
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
/**
|
||||
* indiaStateSheet.js — one Indian state's or union territory's map sheet, GENERATED in the browser at the click,
|
||||
* the same print standard as a country's (@map/packet/countrySheet.js): no sheet is stored, only previews.
|
||||
*
|
||||
* A state poster is a country poster one level down. Its people groups come from the shared state table
|
||||
* (library/data/indiaStates.js: each state's slugs), its outline from the ADM1 boundaries the renderer
|
||||
* already loads (boundaries/gb-IND-ADM1.geojson, matched on shapeName), and its text from the `indiaState`
|
||||
* series (series.json), so it never calls itself one of the Country Maps.
|
||||
*
|
||||
* const sheet = makeIndiaStateSheet({ records, assetsBase: () => base, states })
|
||||
* <PosterWall :sheet="sheet" /> // (item, format) -> html, '' when it cannot be drawn
|
||||
*
|
||||
* ONE PATH: the wall's state cards draw through this and nothing else (check:country-sheet-one-path).
|
||||
*/
|
||||
|
||||
// The states that get a poster: those with people groups listed, in the wall's own order (most groups first,
|
||||
// then by name), so a sheet's "map N of M" matches the card's place on the wall.
|
||||
export function statesWithPosters(states) {
|
||||
return (states || []).filter((s) => s && s.count > 0)
|
||||
.slice().sort((a, b) => b.count - a.count || String(a.name).localeCompare(String(b.name), 'en'))
|
||||
}
|
||||
|
||||
export function stateSeries(states, state) {
|
||||
const tier = statesWithPosters(states)
|
||||
const index = tier.findIndex((s) => s.id === state?.id) + 1
|
||||
if (!index) return undefined
|
||||
return { totalGroups: tier.reduce((a, s) => a + s.count, 0), totalLists: tier.length, index }
|
||||
}
|
||||
|
||||
// The zone the renderer draws. `provinceLabel` makes the title the state's own name, and the header's
|
||||
// "Administrative Areas" row lists it (the renderer never says "province" for India).
|
||||
export function stateZone(state, color) {
|
||||
return {
|
||||
id: state.id, zoneKey: state.id, label: state.name, provinceLabel: state.name, provinces: [state.name],
|
||||
count: state.count, color,
|
||||
}
|
||||
}
|
||||
|
||||
// The renderer knows a zone's OWN ADM1 shapes by `listId === zone.id`; the served file carries UPG-100's zone
|
||||
// ids there. So it gets a COPY with this state's feature re-tagged (the cached collection is shared, never
|
||||
// mutated), plus the stamped zone geometry. null when the state's outline is not in the file.
|
||||
export function stateGeometry(zone, adm1Ind, shapeName) {
|
||||
const feats = (adm1Ind && adm1Ind.features) || []
|
||||
const own = feats.filter((f) => f.properties && f.properties.shapeName === shapeName)
|
||||
if (!own.length) return null
|
||||
const adm1 = { ...adm1Ind, features: feats.map((f) => (own.includes(f) ? { ...f, properties: { ...f.properties, listId: zone.id } } : f)) }
|
||||
const zoneFC = {
|
||||
type: 'FeatureCollection',
|
||||
features: own.map((f) => ({
|
||||
type: 'Feature',
|
||||
properties: { ...f.properties, listId: zone.id, zoneKey: zone.zoneKey, zoneColor: zone.color, zoneLabel: zone.label },
|
||||
geometry: f.geometry,
|
||||
})),
|
||||
}
|
||||
return { adm1, zoneFC }
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} o
|
||||
* records (selector) => Promise<rows[]> the shared provider; asked for the state's slugs
|
||||
* assetsBase () => string where boundaries/ is served from
|
||||
* states () => states[] | null the state table's entries (library/data/indiaStates.js)
|
||||
* region the WAGF region the hue comes from (India's: 'Asia'), so a state prints in India's hue
|
||||
* @returns {(item, format) => Promise<string>}
|
||||
*/
|
||||
export function makeIndiaStateSheet({ records, assetsBase, states, region = 'Asia' }) {
|
||||
return async function indiaStateSheet(item, format) {
|
||||
const all = states() || []
|
||||
const state = all.find((s) => s.id === (item && item.id))
|
||||
if (!state || !state.count) return ''
|
||||
const [{ generateSheet, loadBoundaries, pinsFrom, countryRegionColor }, { ISO3_NAME }, rows] = await Promise.all([
|
||||
import('@map/packet/posterSheets.js'), import('@map/data/iso3-name.js'),
|
||||
records({ type: 'pg_slugs', values: state.slugs }),
|
||||
])
|
||||
if (!rows || !rows.length) return ''
|
||||
const bounds = await loadBoundaries(assetsBase())
|
||||
const zone = stateZone(state, countryRegionColor(region))
|
||||
const geo = stateGeometry(zone, bounds.adm1Ind, state.shapeName)
|
||||
if (!geo) return '' // no outline to draw: fall through to the stored preview, never a guessed map
|
||||
return generateSheet({
|
||||
zone, pins: pinsFrom(rows), zoneFC: geo.zoneFC, format, seriesId: 'indiaState', ISO3_NAME,
|
||||
adm0: bounds.adm0, adm1Ind: geo.adm1, adm1Chn: bounds.adm1Chn,
|
||||
series: stateSeries(all, state), assetsBase: assetsBase(),
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue