From e1d0bc203f41211fd9373110cb35bc0aa89fad1c Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Mon, 28 Sep 2026 16:11:59 +0000 Subject: [PATCH] Read the zone tables from the asset pool, and name zones from their slugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit L62 moved zone-regions.json and groupings.json into data/tables/, so the tree copies them into the asset pool and both plugins read one file. This fetches them rather than importing: a relative import into another plugin's folder is refused, and a per-plugin copy is the drift the single-source move exists to prevent. groupings.label is a definition, not a name, and using it as one would have made the legend unreadable. Measured on the published file, the labels run from 7 to 206 characters and the long ones are the zone's member list — Tibet's names ten provinces and three countries. Exactly one of the fourteen is a plain name. So the tree names a zone from its slug, which is uniform by construction, and the label is carried separately as the definition it actually is, for somewhere with room to say what a zone contains. The tree is built and counted; nothing filters on it yet. Mapping a group to a zone still runs through the older partition, which places 69 groups in the wrong printable zone, and that is the Driver's open question. --- .../gospel-internet/src/zoneData.js | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 plugin-designs/gospel-internet/src/zoneData.js diff --git a/plugin-designs/gospel-internet/src/zoneData.js b/plugin-designs/gospel-internet/src/zoneData.js new file mode 100644 index 000000000..91feab87b --- /dev/null +++ b/plugin-designs/gospel-internet/src/zoneData.js @@ -0,0 +1,60 @@ +/** + * zoneData.js — fetch the published UPG-100 zone tables and build the legend's tree. + * + * The tables belong to upg100 and are now published: L62 moved them into data/tables/, so the + * tree copies them into the asset pool and both plugins read ONE file. That is why this fetches + * rather than imports — a relative import into another plugin's folder is refused by + * check:no-cross-plugin-imports, and a per-plugin copy is the drift the single-source move exists + * to prevent. + * + * FETCHED ONCE and shared, like the neighbour table beside it. + * + * ⚠ THE TREE IS READY; THE FILTER IS NOT. Mapping a GROUP to a zone still runs through + * admin-to-list.json, which exp measured as an older partition — 69 groups land in the wrong + * printable zone and 436 in Unassigned. That is the Driver's row 87. So this builds and counts + * the tree, and the caller must not filter rows on it until the fold lands. + */ +import { resolveAssetsBase } from '@map/utils/embedBase.js' +import { buildZoneTree } from './zoneTree.js' + +let pending = null + +/** { nodes, allZoneIds, definitions } from the published tables. Rejects loudly. */ +export function loadZoneTree(cfg = {}, counts = {}) { + if (!pending) { + pending = (async () => { + const base = resolveAssetsBase(cfg) + const [zr, gr] = await Promise.all([ + fetch(cfg.zoneRegionsUrl || `${base}/tables/zone-regions.json`).then(okJson), + fetch(cfg.groupingsUrl || `${base}/tables/groupings.json`).then(okJson), + ]) + // ★ groupings.label IS A DEFINITION, NOT A NAME, and using it as one would make the + // legend unreadable. Measured on the published file: the labels run from 7 to 206 + // characters, and the long ones are the zone's MEMBER LIST — Tibet's reads "Tibet + // Autonomous Region + Sichuan Province + Xinjiang Uyghur Autonomous Region + ... + + // North Korea". Only one of the fourteen is a plain name. + // So the tree's NAME comes from the slug, which is uniform by construction, and the + // label is carried separately as the zone's DEFINITION — which is what it actually is, + // and useful where there is room to say what a zone contains. + const definitions = {} + for (const [id, g] of Object.entries(gr || {})) if (g && g.label) definitions[id] = g.label + return { zoneRegions: zr || {}, definitions } + })() + } + return pending.then(({ zoneRegions, definitions }) => ({ + zoneRegions, + definitions, + // No `labels` passed: buildZoneTree falls back to humanising the slug, which gives every + // zone a name of the same shape rather than one name and thirteen member lists. + nodes: buildZoneTree(zoneRegions, { counts }), + // EVERY zone the data can name, not just the ones filed under a region. The legend renders + // the difference as a named bucket; passing only the tree's ids would drop the unfiled ones + // with no error and no empty state — just a shorter list. + allZoneIds: Object.keys(zoneRegions), + })) +} + +function okJson(res) { + if (!res.ok) throw new Error(`${res.status} ${res.url}`) + return res.json() +}