The whole-India poster ships on an India tab (66x66 by default); its list, packet and cards stay withheld

countrySheet.js WITHHELD now says WHAT of a whole country is withheld: { IND: { poster: false, rows: true } }, with
posterWithheld() and rowsWithheld() (sheetWithheld() keeps meaning the poster, the name Poster Design reads).
- India's whole-country POSTER prints again, 66x66 by default (60x96 and 36x60 on request); largeSheet() names it.
- Its ROWS (the prayer list, packet and cards: about a thousand pages) stay withheld with the same sentence, in the
  wall, the Regions list and the 'country' poster type. India's state posters are unchanged.
- Country Posters gains an INDIA tab: the same wall filtered to India, labelled with India's own name in the
  reader's language (Intl), between the posters and the map.
check:country-sheet-one-path arm G checks the split (poster live at 66x66, rows withheld, state cards never
withheld, both callers read the right half), with poisons for un-withheld rows and a wall that prints them;
check:poster-section arm C follows. The by-state, leaderless India layout follows as its own PR.
This commit is contained in:
Doxa Maps 2026-10-02 07:51:10 +00:00
parent 3056db68a6
commit 2992f5b941
7 changed files with 50 additions and 41 deletions

View file

@ -8,7 +8,7 @@
* a country with no region is 'other'.
*/
import data from '@map/data/country-posters.json'
import { makeCountrySheet, sheetWithheld } from '@map/packet/countrySheet.js'
import { makeCountrySheet, largeSheet, posterWithheld, rowsWithheld } from '@map/packet/countrySheet.js'
import { createPeopleGroupRecords } from '@map/lists/peopleGroupRecords.js'
import { keyedError } from '@map/i18n/keyedError.js'
import { countryName } from '@map/utils/countryName.js'
@ -38,11 +38,11 @@ export default {
const sheet = makeCountrySheet({ records: recordsFor, assetsBase, countries: data.countries })
const byId = new Map(data.countries.map((c) => [c.id, c]))
return {
// a withheld whole-country poster withholds its list, packet and cards too (countrySheet.js WITHHELD)
records: (c) => (sheetWithheld(c) ? Promise.reject(keyedError('messages.countryPosterTooMany', TOO_MANY)) : recordsFor({ type: 'country', values: [c.iso3] })),
// a whole country's withheld rows (countrySheet.js WITHHELD: India's list, packet and cards) say so, unfetched
records: (c) => (rowsWithheld(c) ? Promise.reject(keyedError('messages.countryPosterTooMany', TOO_MANY)) : recordsFor({ type: 'country', values: [c.iso3] })),
// the sheet gets the data's own record (English, the series index by id), as on the wall
sheet: (card, format) => sheet(byId.get(card.id) || card, format),
unsupported: (c) => (sheetWithheld(c) || c.unsupportedReason ? t('messages.countryPosterTooMany') : null),
unsupported: (c) => (posterWithheld(c) || (c.unsupportedReason && !largeSheet(c)) ? t('messages.countryPosterTooMany') : null),
unit: 'UPG', itemCountKey: 'regions.countryCount', eventName: 'doxa-country-posters:show-on-map', downloadPrefix: 'doxa-country', cardStocks: true,
}
},

View file

@ -79,7 +79,7 @@ export function seriesFor(countries, item) {
* packet and cards are offered on a desktop or laptop only (a phone cannot draw a sheet that big).
* The data (country-posters.json) still records why no PAPER sheet fits; this table is what prints instead.
*/
export const LARGE_SHEETS = { IND: { format: '60x96', formats: ['60x96', '66x66', '36x60'], desktopOnly: true } }
export const LARGE_SHEETS = { IND: { format: '66x66', formats: ['66x66', '60x96', '36x60'], desktopOnly: true } }
const largeOf = (item) => (item && LARGE_SHEETS[String(item.iso3 || '').toUpperCase()]) || null
/**
* ★ WITHHELD: a whole-country poster that is NOT shipped yet, though the renderer can draw it. India's 855 groups do
@ -89,9 +89,15 @@ const largeOf = (item) => (item && LARGE_SHEETS[String(item.iso3 || '').toUpperC
* packet and cards with the same sentence. India's STATE and union-territory posters are separate cards
* (`indiaState`) and are not affected. check:country-sheet-one-path fails if India's poster comes back unannounced.
*/
export const WITHHELD = new Set(['IND'])
export function sheetWithheld(item) { return Boolean(item && !item.indiaState && WITHHELD.has(String(item.iso3 || '').toUpperCase())) }
export function largeSheet(item) { return sheetWithheld(item) ? null : largeOf(item) }
// WHAT of a whole country is withheld: its POSTER, and/or its ROWS (its prayer list, packet and cards). India's
// whole-country poster ships (66x66 by default); its list, packet and cards (about a thousand pages) stay withheld.
export const WITHHELD = { IND: { poster: false, rows: true } }
const withheldOf = (item) => (item && !item.indiaState && WITHHELD[String(item.iso3 || '').toUpperCase()]) || null
export function posterWithheld(item) { return Boolean(withheldOf(item)?.poster) }
export function rowsWithheld(item) { return Boolean(withheldOf(item)?.rows) }
/** The poster's withholding (the name Poster Design reads). */
export function sheetWithheld(item) { return posterWithheld(item) }
export function largeSheet(item) { return posterWithheld(item) ? null : largeOf(item) }
// The sheet format a request gets: a large-sheet country prints at the large size ASKED FOR when it has one (66x66,
// 36x60), else at its default (60x96); a packet's page 1 stays a page; every other country gets exactly what it asked.
export function sheetFormatFor(item, format) {
@ -115,7 +121,7 @@ export function makeCountrySheet({ records, assetsBase, countries = null }) {
return async function countrySheet(item, format) {
const iso3 = item && item.iso3
if (!iso3) return '' // a selection or a row without a country: no sheet
if (sheetWithheld(item)) return '' // withheld (above): nothing is fetched or drawn
if (posterWithheld(item)) return '' // a withheld poster (above): nothing is fetched or drawn
// Lazy, like the zone sheet: the generator and the boundary loader are only pulled when a
// reader actually prints, so they stay out of every wall's initial download.
const { generateCountrySheet } = await import('@map/packet/posterSheets.js')

View file

@ -44,7 +44,7 @@ import { getRuntimeLocale, detectLocale, tStatic } from '@map/i18n/index.js'
// library/. Generated by library/scripts/posters/emit-countries.mjs, which now writes there.
import data from '@map/data/country-posters.json'
import { resolveAssetsBase } from '@map/utils/embedBase.js'
import { makeCountrySheet, largeSheet, sheetWithheld } from '@map/packet/countrySheet.js'
import { makeCountrySheet, largeSheet, posterWithheld, rowsWithheld } from '@map/packet/countrySheet.js'
import { isPhonePrint } from '@map/composables/printInPlace.js'
import { keyedError } from '@map/i18n/keyedError.js'
// India's states and union territories: the shared table (fetched when the wall shows them) and their sheet.
@ -170,7 +170,7 @@ const desktopOnly = () => Promise.reject(keyedError('poster.phoneLimitAll', 'Thi
// A WITHHELD whole-country poster (countrySheet.js WITHHELD: India) withholds its list, packet and cards too, with
// the same sentence the card shows (about a thousand pages, offered when the poster is).
const withheld = () => Promise.reject(keyedError('messages.countryPosterTooMany', 'Too many people groups to fit on one poster legibly — a solution is in progress.'))
const countryRecords = (c) => (sheetWithheld(c) ? withheld() : phoneBlocked(c) ? desktopOnly() : c.indiaState
const countryRecords = (c) => (rowsWithheld(c) ? withheld() : phoneBlocked(c) ? desktopOnly() : 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
@ -244,7 +244,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.noGroups ? t('countryPosters.indiaStateNoGroups') : c.noPoster ? tRegions('No poster has been generated for this country yet.') : sheetWithheld(c) ? t('messages.countryPosterTooMany') : phoneBlocked(c) ? t('poster.phoneLimitAll') : c.unsupportedReason && !largeSheet(c) ? t('messages.countryPosterTooMany') : null)
const unsupported = (c) => (c.noGroups ? t('countryPosters.indiaStateNoGroups') : c.noPoster ? tRegions('No poster has been generated for this country yet.') : posterWithheld(c) ? t('messages.countryPosterTooMany') : phoneBlocked(c) ? t('poster.phoneLimitAll') : c.unsupportedReason && !largeSheet(c) ? 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)

View file

@ -34,6 +34,8 @@ import { ref, computed, inject, onMounted, onBeforeUnmount } from 'vue'
import { buildPaneColumn, PANE_GRID_STYLES } from '@map/layouts/pane-wrapper/pane-wrapper.js'
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
import ViewSelect from '@map/components/ViewSelect.vue'
import { countryName } from '@map/utils/countryName.js'
import { getRuntimeLocale, detectLocale } from '@map/i18n/index.js'
import ShareButton from '@map/components/map-controls/ShareButton.vue'
import { useI18n } from 'vue-i18n'
import enTabs from '@map/i18n/locales/en/tabs.json'
@ -68,7 +70,10 @@ const spec = computed(() => ({
// strip desynchronises from the scroll column.
panes: [
{ id: 'wall', label: t('tabs.countryPosters'), at: [0, 0], config: { profile: 'country-poster-wall' } },
{ id: 'map', label: t('tabs.map'), at: [1, 0], config: { profile: 'countries-map' } },
// INDIA: the whole-India poster (66x66 by default) on its own tab, the same wall filtered to India. Its label is
// India's own name in the reader's language (Intl), so no tab key is needed.
{ id: 'india', label: countryName('IND', getRuntimeLocale() || detectLocale() || 'en') || 'India', at: [1, 0], config: { profile: 'country-poster-wall', countries: ['IND'] } },
{ id: 'map', label: t('tabs.map'), at: [2, 0], config: { profile: 'countries-map' } },
],
// The switcher lives in this element's own header, so the layout's "… below ↓"
// pill would be a second, redundant control (same call upg-100-mfe makes).

View file

@ -99,10 +99,10 @@ export function armB(seriesFor, countries, sheetFormatFor = null) {
const by = (iso) => countries.find((c) => c.iso3 === iso)
const ind = by('IND')
const s = ind && seriesFor(countries, ind)
// THE LARGE-SHEET FORMAT RULE: India prints the large size ASKED FOR, else its 60x96 default; a packet page stays a
// THE LARGE-SHEET FORMAT RULE: India prints the large size ASKED FOR, else its 66x66 default; a packet page stays a
// page; any other country gets exactly what it asked for.
if (sheetFormatFor) {
const want = [[ind, '66x66', '66x66'], [ind, '36x60', '36x60'], [ind, '60x96', '60x96'], [ind, 'letter', '60x96'], [ind, '3x4', '60x96'], [ind, 'packet', 'packet'],
const want = [[ind, '66x66', '66x66'], [ind, '36x60', '36x60'], [ind, '60x96', '60x96'], [ind, 'letter', '66x66'], [ind, '3x4', '66x66'], [ind, 'packet', 'packet'],
[by('NPL'), '3x3', '3x3'], [by('NPL'), 'letter', 'letter'], [by('CHN'), '3x4', '3x4']]
for (const [item, ask, exp] of want) { const got = sheetFormatFor(item, ask); if (got !== exp) fails.push(`sheetFormatFor(${item?.iso3}, ${ask}) = ${got}, expected ${exp}`) }
}
@ -179,26 +179,24 @@ export function armF(srcs) {
// ARM E: India is its OWN section, FIRST, counted ONCE. The India country card, the state cards and the none-listed
// cards group together; the section's total is India's own count (not the card sum, which counts 855 twice); on that
// true total it outranks every regional section. PosterWall's default (no sectionTotal) must still be the card sum.
// ARM G — A WITHHELD WHOLE-COUNTRY POSTER stays withheld (India: not shipped until it prints legibly). The library
// draws NOTHING for it in any format and fetches no record (0 big jobs); a state card is never withheld; and each
// caller shows messages.countryPosterTooMany on the card and rejects the country's rows with it (no list/packet/cards).
// ARM G — WHAT OF A WHOLE COUNTRY IS WITHHELD (countrySheet.js WITHHELD). India: its whole-country POSTER ships (66x66
// by default; largeSheet() names it), its ROWS (list, packet, cards: about a thousand pages) stay withheld with the
// sentence; a state card is never withheld. Each caller shows the sentence on the card only for a withheld POSTER and
// rejects the rows only for withheld ROWS.
export async function armG(cs, srcs) {
const fails = []
if (!(cs.WITHHELD instanceof Set) || !cs.WITHHELD.has('IND')) fails.push("countrySheet.js WITHHELD no longer holds India: its whole-country poster would ship")
let fetched = 0
const sheet = cs.makeCountrySheet({ records: () => { fetched++; return Promise.resolve([{ name: 'x', latitude: 20, longitude: 78 }]) }, assetsBase: () => '', countries: [] })
for (const f of ['3x3', '3x4', 'letter', 'packet', '60x96', '66x66', '36x60']) {
let out
try { out = await sheet({ id: 'country-ind', iso3: 'IND', label: 'India' }, f) } catch (e) { out = 'THREW ' + e.message }
if (out !== '') fails.push(`makeCountrySheet drew something for withheld India at ${f}`)
}
if (fetched) fails.push(`makeCountrySheet fetched records ${fetched}x for withheld India (a big job started)`)
if (cs.sheetWithheld({ iso3: 'IND', indiaState: true, id: 'india-state-gujarat' })) fails.push('an India STATE card is withheld (only the whole-country poster may be)')
if (cs.largeSheet({ iso3: 'IND' })) fails.push('largeSheet(India) is not null while withheld, so callers would offer its poster')
const ind = { id: 'country-ind', iso3: 'IND', label: 'India' }
if (!cs.WITHHELD || !cs.WITHHELD.IND) fails.push('countrySheet.js WITHHELD no longer lists India')
if (cs.posterWithheld(ind)) fails.push("India's whole-country poster is withheld again (it ships)")
if (!cs.rowsWithheld(ind)) fails.push("India's rows are no longer withheld: its list/packet/cards (~1,000 pages) would print")
const big = cs.largeSheet(ind)
if (!big || big.format !== '66x66') fails.push(`India's whole-country poster is not offered at 66x66 by default (${big && big.format})`)
if (cs.sheetWithheld(ind) !== cs.posterWithheld(ind)) fails.push('sheetWithheld() no longer means the poster')
if (cs.rowsWithheld({ iso3: 'IND', indiaState: true, id: 'india-state-gujarat' })) fails.push('an India STATE card has withheld rows (only the whole country may)')
for (const [name, src] of Object.entries(srcs)) {
if (!/sheetWithheld\((?:c|item)\) \? t(?:Key)?\('messages\.countryPosterTooMany'\)/.test(src)) fails.push(`${name}: a withheld country's card does not show messages.countryPosterTooMany`)
if (!/sheetWithheld\((?:c|item)\) \? (?:withheld\(\)|Promise\.reject\(keyedError\('messages\.countryPosterTooMany')/.test(src)) fails.push(`${name}: a withheld country's rows are not withheld (its list/packet/cards would print)`)
if (/const withheld = /.test(src) && !/const withheld = \(\) => Promise\.reject\(keyedError\('messages\.countryPosterTooMany'/.test(src)) fails.push(`${name}: withheld() does not reject with messages.countryPosterTooMany`)
if (!/posterWithheld\((?:c|item)\) \? t(?:Key)?\('messages\.countryPosterTooMany'\)/.test(src)) fails.push(`${name}: a withheld POSTER's card does not show messages.countryPosterTooMany`)
if (!/rowsWithheld\((?:c|item)\) \? (?:withheld\(\)|Promise\.reject\(keyedError\('messages\.countryPosterTooMany')/.test(src)) fails.push(`${name}: withheld ROWS are not withheld (its list/packet/cards would print)`)
if (/sheetWithheld\(/.test(src)) fails.push(`${name}: still reads sheetWithheld() (ambiguous now: poster or rows?)`)
}
return fails
}
@ -261,8 +259,8 @@ async function main() {
['the Regions list resolves [] for a phone-blocked card', () => armF({ wall: files[wall], regions: REG.replace("phoneBlocked(item) ? Promise.reject(keyedError('poster.phoneLimitAll'", "phoneBlocked(item) ? Promise.resolve([]) || (keyedError('poster.phoneLimitAll'") })],
['a locale loses a state name', () => armD(states, { ...areas, ro: { ...areas.ro, stateNames: Object.fromEntries(Object.entries(areas.ro.stateNames).filter(([k]) => k !== 'IN-GJ')) } }, meta)],
['the names lose their licence line', () => armD(states, areas, { ...meta, licence: '' })],
['the whole-India poster un-withheld', async () => armG({ ...CS, WITHHELD: new Set(), largeSheet: (it) => CS.LARGE_SHEETS[it?.iso3] || null, makeCountrySheet: (o) => async (it, f) => { await o.records({}); return '<svg/>' } }, { wall: files[wall], regions: REG })],
['the wall prints the list of a withheld country', async () => armG(CS, { wall: files[wall].replace("const countryRecords = (c) => (sheetWithheld(c) ? withheld() : ", "const countryRecords = (c) => ("), regions: REG })],
['India\'s rows un-withheld', async () => armG({ ...CS, rowsWithheld: () => false }, { wall: files[wall], regions: REG })],
['the wall prints the list of a country whose rows are withheld', async () => armG(CS, { wall: files[wall].replace('rowsWithheld(c) ? withheld() : ', ''), regions: REG })],
['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

View file

@ -3,8 +3,8 @@
// A. the TYPE REGISTRY: 'country' is registered, an unknown type is null, a plugin can register its own.
// B. SCOPES: DX Life's /regions slugs (africa … oceania, other) each resolve; every spelling of a region (its
// slug, its API value, its English name) resolves to the same countries; the scopes cover all countries once.
// C. a WITHHELD whole-country poster stays withheld through the type (the card's sentence; its rows rejected,
// no records fetched).
// C. India through the type: its whole-country POSTER card is live; its withheld ROWS are rejected with the sentence,
// no records fetched.
// D. the ELEMENT's attributes map to its config (collapsible on by default, collapsible="false", collapsed).
// E. THE LIFT: every name PosterSection.vue's template uses is injected from the wall (a section never reads
// something its wall does not provide).
@ -43,7 +43,7 @@ async function arms({ types, country, element, sectionSrc }) {
else {
let fetched = 0
const act = country.actions({ assetsBase: () => '', apiBase: () => { fetched++; return '' }, lang: () => 'en', profileConfig: null, t: (k) => k })
if (act.unsupported(ind) !== 'messages.countryPosterTooMany') fails.push("C: withheld India's card does not show messages.countryPosterTooMany")
if (act.unsupported(ind) !== null) fails.push("C: India's whole-country poster card is marked unsupported (its poster ships)")
let why = ''
try { await act.records(ind); why = 'resolved' } catch (e) { why = e && (e.i18nKey || e.message) }
if (!/countryPosterTooMany/.test(String(why))) fails.push(`C: withheld India's rows are not withheld (${why})`)
@ -93,7 +93,7 @@ try {
} else {
const f = await arms(real)
if (f.length) { f.forEach((x) => console.log('FAIL ' + x)); code = 1 }
else console.log(`PASS check:poster-section — the 'country' type; ${DXL.length} DX Life scopes cover every country once (and their other spellings); withheld India stays withheld; the element's attributes; PosterSection injects every name it uses`)
else console.log(`PASS check:poster-section — the 'country' type; ${DXL.length} DX Life scopes cover every country once (and their other spellings); India's poster live, its rows withheld; the element's attributes; PosterSection injects every name it uses`)
}
} finally { await server.close() }
process.exit(code)

View file

@ -70,7 +70,7 @@ import PosterWall from '@map/components/poster/PosterWall.vue'
import RegionSectionCard from '@map/components/regions/RegionSectionCard.vue'
import posterData from '@map/data/country-posters.json'
import { createPeopleGroupRecords } from '@map/lists/peopleGroupRecords.js'
import { makeCountrySheet, largeSheet, sheetWithheld } from '@map/packet/countrySheet.js'
import { makeCountrySheet, largeSheet, posterWithheld, rowsWithheld } from '@map/packet/countrySheet.js'
import { isPhonePrint } from '@map/composables/printInPlace.js'
import { keyedError } from '@map/i18n/keyedError.js'
import { resolveAssetsBase } from '@map/utils/embedBase.js'
@ -373,11 +373,11 @@ function postersFor(region) {
// its card says so and offers no rows, rather than failing (the Country Posters wall applies the same rule, same table).
const ON_PHONE = isPhonePrint()
const phoneBlocked = (c) => ON_PHONE && Boolean(largeSheet(c)?.desktopOnly)
const posterUnsupported = (c) => (sheetWithheld(c) ? tKey('messages.countryPosterTooMany') : phoneBlocked(c) ? tKey('poster.phoneLimitAll')
const posterUnsupported = (c) => (posterWithheld(c) ? tKey('messages.countryPosterTooMany') : phoneBlocked(c) ? tKey('poster.phoneLimitAll')
: c.unsupportedReason && !largeSheet(c) ? tKey('messages.countryPosterTooMany') : null)
// A blocked card's rows REJECT with the notice, never [] ("no people groups" on a card that has hundreds).
// A withheld whole-country poster (countrySheet.js WITHHELD: India) withholds its list, packet and cards too.
const posterRows = (item) => (sheetWithheld(item) ? Promise.reject(keyedError('messages.countryPosterTooMany', 'Too many people groups to fit on one poster legibly — a solution is in progress.')) : phoneBlocked(item) ? Promise.reject(keyedError('poster.phoneLimitAll', 'This needs a desktop or laptop computer.')) : countryRecords({ type: 'country', values: [item.iso3] }))
const posterRows = (item) => (rowsWithheld(item) ? Promise.reject(keyedError('messages.countryPosterTooMany', 'Too many people groups to fit on one poster legibly — a solution is in progress.')) : phoneBlocked(item) ? Promise.reject(keyedError('poster.phoneLimitAll', 'This needs a desktop or laptop computer.')) : countryRecords({ type: 'country', values: [item.iso3] }))
// One region's wall is already inside a region section, so it needs no second level of
// collapsing and no grouping: everything in it is that one region.