doxa-maps/library/scripts/boundaries/build-web-boundaries.mjs
Doxa Maps de284359dd
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Update from source f4871401
2026-09-27 04:49:00 +00:00

64 lines
4.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* build-web-boundaries.mjs — the INTERACTIVE-map copies of the boundary files: small enough to parse
* fast, with every feature, every island and every property kept.
*
* node library/scripts/boundaries/build-web-boundaries.mjs
*
* WHY. rega measured ~3.6 MB of boundary GeoJSON (world-adm0 1.9 MB, gb-IND-ADM1 1.6 MB) costing
* 5–6 s of MapLibre worker parsing before the first territory draws, on every page of every
* partner site. A web map drawn at zoom 1–7 needs a fraction of those vertices. The FULL files stay
* as they are: posters and print use them, and so does every build-time point-in-polygon.
*
* HOW (mapshaper, pinned below; fetched with npx, it is not a repo dependency):
* -explode split each multi-part feature into its parts, so every island is its own shape
* -simplify visvalingam weighted percentage=P keep-shapes
* topology-preserving (shared borders stay shared); keep-shapes stops any shape
* from vanishing, which, after the explode, means no island is ever dropped
* (without the explode, keep-shapes guards only whole features: 572 parts -> 240)
* -dissolve KEY copy-fields=... put each feature back together, same properties
* precision=0.0001 ~11 m, far below a pixel at the zooms these files are drawn at
*
* VALIDATED, the run FAILS otherwise: the same feature count, the same keys, the same polygon-part
* count (no dropped island), and the same property names and values on every feature.
* The measured report (size, parts, area error, point-in-polygon drift) is in
* internal-tracker/03-deployment-cycle/REPORT-WEB-BOUNDARIES.md.
*/
import { execFileSync } from 'node:child_process'
import { readFileSync, statSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../../..')
const MAPSHAPER = 'mapshaper@0.6.121'
const UPG = 'plugin-designs/my-upg-100-list/data'
const JOBS = [
// src, out, dissolve key, the other properties, simplification percentage
{ src: `${UPG}/adm0/world-adm0.geojson`, out: `${UPG}/adm0/world-adm0-web.geojson`, key: 'iso3', copy: ['shapeGroup', 'shapeName'], pct: '8%' },
{ src: 'data/boundaries/gb-IND-ADM1-full.geojson', out: 'data/boundaries/gb-IND-ADM1-full-web.geojson', key: 'shapeName', copy: ['iso3', 'shapeGroup', 'shapeISO'], pct: '10%' },
// UPG-100's curated partition: `listId` is load-bearing (zone-geometry.js), so it must survive.
{ src: `${UPG}/gb-IND-ADM1.geojson`, out: `${UPG}/gb-IND-ADM1-web.geojson`, key: 'shapeName', copy: ['iso3', 'shapeGroup', 'listId'], pct: '10%' },
// gb-CHN-ADM1.geojson is 56 KB (12 provinces): already web-sized, no copy made.
]
const fail = (m) => { console.error('build-web-boundaries: FAIL — ' + m); process.exit(1) }
const parts = (fc) => fc.features.reduce((t, f) => t + (f.geometry.type === 'Polygon' ? 1 : f.geometry.coordinates.length), 0)
for (const j of JOBS) {
const src = join(ROOT, j.src), out = join(ROOT, j.out)
execFileSync('npx', ['--yes', MAPSHAPER, src, '-explode',
'-simplify', 'visvalingam', 'weighted', `percentage=${j.pct}`, 'keep-shapes',
'-dissolve', j.key, `copy-fields=${j.copy.join(',')}`,
'-o', out, 'format=geojson', 'precision=0.0001'], { stdio: ['ignore', 'ignore', 'pipe'] })
const a = JSON.parse(readFileSync(src, 'utf8')), b = JSON.parse(readFileSync(out, 'utf8'))
if (a.features.length !== b.features.length) fail(`${j.out}: ${b.features.length} features, source has ${a.features.length}`)
const byKey = new Map(b.features.map((f) => [f.properties[j.key], f]))
for (const f of a.features) {
const g = byKey.get(f.properties[j.key])
if (!g) fail(`${j.out}: lost ${j.key}=${f.properties[j.key]}`)
for (const [k, v] of Object.entries(f.properties)) if (g.properties[k] !== v) fail(`${j.out}: ${f.properties[j.key]}.${k} is ${JSON.stringify(g.properties[k])}, source ${JSON.stringify(v)}`)
}
if (parts(a) !== parts(b)) fail(`${j.out}: ${parts(b)} polygon parts, source has ${parts(a)} (an island was dropped)`)
const kb = (p) => Math.round(statSync(p).size / 1024)
console.log(`${j.out}: ${kb(src)} KB -> ${kb(out)} KB · ${b.features.length} features · ${parts(b)} parts · properties identical`)
}