doxa-maps/library/packet/countrySheet.js
Doxa Maps 6d0253d909 countrySheet: a requested large size wins; LARGE_SHEETS only supplies India's 60x96 default
India can print at 60x96 (the default), 66x66 or 36x60. makeCountrySheet used to map any non-packet request to the
default, so a 66x66 or 36x60 request drew the 60x96 sheet. sheetFormatFor(item, format) (exported, pure) now keeps
a requested size the country can print at, falls back to its default otherwise, keeps a packet page a page, and
returns exactly what was asked for every other country.
check:country-sheet-one-path asserts that table (India 66x66/36x60/60x96/letter/3x4/packet; Nepal 3x3/letter;
China 3x4), with two poisons: India ignoring a requested 66x66, and a packet page turned into a large sheet.
2026-10-01 06:07:48 +00:00

128 lines
8 KiB
JavaScript

/**
* countrySheet.js — a country's map sheet, GENERATED in the browser at the click.
*
* SHARED (@map/packet/countrySheet.js). Its callers are the regions dashboard's regions-list and the
* Country Posters wall (country-poster-wall.vue), which used to define its own inline copy.
*
* ⚠ THE MOVE HERE WAS NEVER FINISHED, and it cost a real defect. This module's comment used to claim
* two consumers -- the country-poster wall and <doxa-country-map> -- and a CENSUS of the tree found
* it had NONE: nothing imported it at all. regions-list kept importing a byte-identical plugin-local
* copy (src/country-sheet-print.js), and country-poster-wall.vue defines its own inline countrySheet
* that calls generateCountrySheet directly. So this file was an ORPHAN wearing a comment that said
* otherwise.
* That mattered the moment the country poster's hue became data-driven: the region key was added
* HERE, where nothing read it, while the copy regions-list actually imported went on building
* `country` without a region -- so countryRegionColor(undefined) returned the old flat teal and
* warned. The same country then printed HUED from Country Posters and TEAL from the Regions tab,
* which is exactly the "two different posters for one country" the hue change exists to refuse.
* The copy is deleted, and regions-list and the wall both import this (check:country-sheet-one-path
* refuses a caller that draws a country sheet any other way). Do not re-copy it: a duplicate of this file is
* indistinguishable from it until one of them is edited, and then it is a silent divergence.
*
* The print standard is generate, store only previews: no country sheet is shipped, and none should
* be. MEASURED before this existed: data/posters holds 28 .html sheets and all 28 are ZONES, while
* emit-countries.mjs writes only country-posters.json, whose `sheet` field is a PAPER SIZE ('3x3')
* rather than a filename. So a packet's page-1 lift for a country asked for posters/country-<iso3>.html
* and got a 404 for all 175 of them. This draws it instead.
*
* Same shape as my-upg-100-list's zone sheet (data/zone-sheet-print.js), deliberately, so the two
* surfaces cannot draw the "same" sheet two ways:
*
* const sheet = makeCountrySheet({ records, assetsBase: () => base })
* usePosterPrints({ …, sheet }) // (item, format) -> html
* <PosterWall :sheet="sheet" />
*
* The country case is simpler than the zone case: generateCountrySheet resolves the zone and its
* geometry from the country entry itself, so there is no assignment step to keep in sync.
*/
/**
* @param {object} o
* records (selector) => Promise<rows[]> the shared provider; this asks it for ONE country
* assetsBase () => string where boundaries/ is served from
* @returns {(item, format) => Promise<string>} the sheet's HTML, or '' when it cannot be drawn
*/
/**
* The sheet's SERIES facts, computed exactly as the Country Posters wall computes them
* (country-poster-wall.vue): the tier is every country printed at the same sheet size, the total is
* that tier's length, and the index is the country's position in it by id. So a country's printed
* "map N of M" and "one of a ~M-map set" agree with the wall's print and with its own thumbnail.
*
* ★ WHY THIS EXISTS: without a series, generateCountrySheet falls back to totalLists 0, and every
* Regions-tab print read "map 1 of 0" and "one of a 0-map set called the Country Maps" -- nonsense
* on a printed poster, on the demo's showcase flow (Regions -> a country -> Poster Map). This was
* not introduced by the one-definition change: the plugin-local copy regions-list used before it
* passed no series either. The wall was never affected because it computes its own.
*
* Returns undefined when the item is not in the list (a host's country with no poster), so that row
* 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 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.
*/
export function seriesFor(countries, item) {
if (!item || !Array.isArray(countries)) return undefined
const tier = countries.filter((x) => (x.sheet ?? null) === (item.sheet ?? null))
const index = tier.findIndex((x) => x.id === item.id) + 1
if (!index) return undefined
return { totalGroups: tier.reduce((a, x) => a + (x.count || 0), 0), totalLists: tier.length, index }
}
/**
* ★ COUNTRIES TOO LARGE FOR ANY PAPER SHEET print as ONE LARGE side-lane sheet instead (poster-render's 60x96 /
* 66x66 / 36x60: every group named beside the map, crossing-free leaders). ONE source of truth for every caller (the
* Country Posters wall, the Regions list), so one country never gets two answers. `desktopOnly`: its poster, list,
* 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 function largeSheet(item) { return (item && LARGE_SHEETS[String(item.iso3 || '').toUpperCase()]) || null }
// 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)
if (!big || format === 'packet') return format
return big.formats.includes(format) ? format : big.format
}
// The facts row's "Administrative Areas" cell for a large-sheet country whose divisions we know (India: its states and
// union territories, counted from the shared state table). English, like the rest of the sheet.
async function adminAreasFor(iso3, base) {
if (iso3 !== 'IND') return undefined
try {
const { loadIndiaStates } = await import('@map/data/indiaStates.js')
const n = ((await loadIndiaStates(base)).states || []).length
return n ? `${n} states and union territories` : undefined
} catch (_) { return undefined } // the cell falls back to the country's name
}
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
// 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')
const [{ ISO3_NAME }, rows] = await Promise.all([
import('@map/data/iso3-name.js'),
records({ type: 'country', values: [iso3] }),
])
if (!rows || !rows.length) return '' // nothing to pin: fall through rather than draw an empty country
const big = largeSheet(item)
const adminAreas = item.adminAreas || (big ? await adminAreasFor(String(iso3).toUpperCase(), assetsBase()) : undefined)
return generateCountrySheet({
// region travels: countryZone colours the sheet from it, and a country arriving without one
// silently falls back to the old flat teal for that poster alone.
country: { id: item.id, iso3, label: item.label, count: item.count, region: item.region, adminAreas },
records: rows,
assetsBase: assetsBase(),
ISO3_NAME,
format: sheetFormatFor(item, format),
series: seriesFor(countries, item),
})
}
}