The whole-India poster is withheld for now: "Too many people groups to fit on one poster legibly"

India's whole-country poster does not ship yet; a solution is in progress. countrySheet.js gains WITHHELD (India)
and sheetWithheld(item):
- makeCountrySheet draws nothing for a withheld country, in any format, before fetching a single record.
- largeSheet() is null for it, so the Country Posters wall and the Regions list show their existing
  messages.countryPosterTooMany ("Too many people groups to fit on one poster legibly — a solution is in
  progress.", all eleven locales) on the card instead of a poster.
- Both callers also withhold its list, packet and cards with the same sentence (about a thousand pages, offered
  when the poster is).
- India's STATE and union-territory posters are separate cards (indiaState) and are untouched; so are the India
  section and its first place.
- sheetFormatFor keeps the large-sheet size rule itself (it returns with the poster).
check:country-sheet-one-path arm G: India stays withheld (no sheet in any of 7 formats, 0 records fetched, its
state cards never withheld, both callers show the sentence and reject its rows), with a poison for un-withholding
it and one for a wall that prints its list.
This commit is contained in:
Doxa Maps 2026-10-01 22:18:31 +00:00
parent 96de705202
commit fe743ff48d
4 changed files with 55 additions and 11 deletions

View file

@ -80,11 +80,22 @@ export function seriesFor(countries, item) {
* 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 function largeSheet(item) { return (item && LARGE_SHEETS[String(item.iso3 || '').toUpperCase()]) || null }
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
* not fit one poster legibly yet; a solution is in progress. Everywhere a reader could print or preview it, it is
* withheld: makeCountrySheet draws nothing, largeSheet() is null (so every caller shows
* messages.countryPosterTooMany, from the country's own unsupportedReason), and the callers withhold its list,
* 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) }
// 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) {
const big = largeSheet(item)
const big = largeOf(item) // the size rule itself, independent of withholding (it is what returns with the poster)
if (!big || format === 'packet') return format
return big.formats.includes(format) ? format : big.format
}
@ -104,6 +115,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
// 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 } from '@map/packet/countrySheet.js'
import { makeCountrySheet, largeSheet, sheetWithheld } 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.
@ -167,7 +167,10 @@ const phoneBlocked = (c) => !c.indiaState && ON_PHONE && Boolean(largeSheet(c)?.
// A blocked card's list, packet and cards REJECT with the notice (never resolve [], which the print path would
// report as "no people groups" on a card that has hundreds), before any record is fetched.
const desktopOnly = () => Promise.reject(keyedError('poster.phoneLimitAll', 'This needs a desktop or laptop computer.'))
const countryRecords = (c) => (phoneBlocked(c) ? desktopOnly() : c.indiaState
// 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
? (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
@ -241,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.') : 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.') : sheetWithheld(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

@ -179,6 +179,30 @@ 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).
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')
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`)
}
return fails
}
export function armE(countries, states, wallSrc, posterWallSrc, H) {
const fails = []
if (!/:section-total=/.test(wallSrc)) fails.push('country-poster-wall.vue does not pass :section-total, so India is counted twice')
@ -200,7 +224,8 @@ export function armE(countries, states, wallSrc, posterWallSrc, H) {
async function main() {
const selfTest = process.argv.includes('--self-test')
const { seriesFor, sheetFormatFor } = await import(join(ROOT, 'library/packet/countrySheet.js'))
const CS = await import(join(ROOT, 'library/packet/countrySheet.js'))
const { seriesFor, sheetFormatFor } = CS
const data = JSON.parse(readFileSync(join(ROOT, 'library/data/country-posters.json'), 'utf8'))
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 }
@ -236,11 +261,13 @@ 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 })],
['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) {
const f = run()
const f = await run()
console.log(`${f.length ? '✓ caught' : '✗ MISSED'} ${name}${f.length ? ` -> ${f[0]}` : ''}`)
if (!f.length) bad++
}
@ -254,7 +281,8 @@ async function main() {
const callers = []
const E = armE(countries, states, files['plugin-designs/country-posters/country-poster-wall.vue'], readFileSync(join(ROOT, 'library/components/poster/PosterWall.vue'), 'utf8'), H)
const fails = [...armA(files, callers), ...armB(seriesFor, countries, sheetFormatFor), ...armC(H, states, adm1Ind), ...E.fails, ...armD(states, areas, meta), ...armF({ wall: files['plugin-designs/country-posters/country-poster-wall.vue'], regions: REG })]
const G = await armG(CS, { wall: files['plugin-designs/country-posters/country-poster-wall.vue'], regions: REG })
const fails = [...armA(files, callers), ...armB(seriesFor, countries, sheetFormatFor), ...armC(H, states, adm1Ind), ...G, ...E.fails, ...armD(states, areas, meta), ...armF({ wall: files['plugin-designs/country-posters/country-poster-wall.vue'], regions: REG })]
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 }

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 } from '@map/packet/countrySheet.js'
import { makeCountrySheet, largeSheet, sheetWithheld } 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,10 +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) => (phoneBlocked(c) ? tKey('poster.phoneLimitAll')
const posterUnsupported = (c) => (sheetWithheld(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).
const posterRows = (item) => (phoneBlocked(item) ? Promise.reject(keyedError('poster.phoneLimitAll', 'This needs a desktop or laptop computer.')) : countryRecords({ type: 'country', values: [item.iso3] }))
// 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] }))
// 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.