Web-sized boundary copies: every feature and island kept, a fifth of the bytes
rega measured ~3.6 MB of boundary GeoJSON costing 5-6 s of MapLibre worker parsing before the first territory draws, on every partner page. These copies are for interactive maps; the full files stay for posters, print and build-time point-in-polygon. world-adm0-web.geojson 1,866 KB -> 383 KB (gzip 596 -> 134) 162 features, 572/572 parts gb-IND-ADM1-full-web.geojson 1,796 KB -> 227 KB (gzip 451 -> 69) 36 features, 452/452 parts gb-IND-ADM1-web.geojson 1,628 KB -> 178 KB (gzip 412 -> 55) 34 features, 40/40 parts, listId kept (gb-CHN-ADM1 is 56 KB already: no copy) library/scripts/boundaries/build-web-boundaries.mjs writes them with mapshaper (pinned 0.6.121, via npx; not a repo dependency): -explode, then topology- preserving Visvalingam with keep-shapes, then -dissolve with every other property copied. The explode makes each island its own shape, so keep-shapes protects every one: without it, the world drops from 572 to 240 parts. The run FAILS on a changed feature count, a lost key, a changed property or a dropped part. Negative-tested (without -explode: 297/572 parts -> FAIL), and a second run writes identical bytes. Measured (_qa-cycle/03-deployment-cycle/REPORT-WEB-BOUNDARIES.md): median area error 0.57% (world), 0.04% (India). Of 14,486 real people-group points, 95.9% stay in the same country in the world file and 99.9% in the India file; the misses are archipelago coasts. No runtime code does point-in-polygon on these files, so this is visual only. Not wired: consumers still fetch the full files, and generate-tree does not stage the -web copies yet. Both are in the report's switch table.
This commit is contained in:
parent
0f136d2513
commit
5410842aef
4 changed files with 302 additions and 0 deletions
38
data/boundaries/gb-IND-ADM1-full-web.geojson
Normal file
38
data/boundaries/gb-IND-ADM1-full-web.geojson
Normal file
File diff suppressed because one or more lines are too long
64
library/scripts/boundaries/build-web-boundaries.mjs
Normal file
64
library/scripts/boundaries/build-web-boundaries.mjs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
#!/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
|
||||
* _qa-cycle/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`)
|
||||
}
|
||||
164
plugin-designs/my-upg-100-list/data/adm0/world-adm0-web.geojson
vendored
Normal file
164
plugin-designs/my-upg-100-list/data/adm0/world-adm0-web.geojson
vendored
Normal file
File diff suppressed because one or more lines are too long
36
plugin-designs/my-upg-100-list/data/gb-IND-ADM1-web.geojson
vendored
Normal file
36
plugin-designs/my-upg-100-list/data/gb-IND-ADM1-web.geojson
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue