// Builds the shapes for historical World Game maps from aourednik/historical-basemaps (GPL-3.0: // https://github.com/aourednik/historical-basemaps). The derived files in src/data/maps/ are shared under the // same licence and credited on the site. // // The snapshots' names are not always right for their year (e.g. the 1914 file shows Armenia as independent), // so only the SHAPES are trusted here. Every map's names, flags and statuses are curated in src/data/world.ts, // where shapes can be merged (never split). // // Cleaning: Antarctica is dropped; small unnamed islands join the nearest named shape; larger unnamed areas // become "Unclaimed". Then the shapes are simplified to about 60 KB per map. // // Run: node scripts/build-historical-bases.mjs 1914 1938 … (needs internet and npx) import { execFileSync } from 'node:child_process'; import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; const years = process.argv.slice(2); if (!years.length) throw new Error('Give the snapshot years to build, e.g. 1914 1938'); const out = new URL('../src/data/maps/', import.meta.url); mkdirSync(out, { recursive: true }); const work = mkdtempSync(join(tmpdir(), 'eradle-hb-')); function bbox(geometry) { const polys = geometry.type === 'MultiPolygon' ? geometry.coordinates : [geometry.coordinates]; let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity; for (const poly of polys) for (const ring of poly) for (const [x, y] of ring) { x0 = Math.min(x0, x); y0 = Math.min(y0, y); x1 = Math.max(x1, x); y1 = Math.max(y1, y); } return { x0, y0, x1, y1, cx: (x0 + x1) / 2, cy: (y0 + y1) / 2, area: (x1 - x0) * (y1 - y0) }; } for (const year of years) { const url = `https://raw.githubusercontent.com/aourednik/historical-basemaps/master/geojson/world_${year}.geojson`; const data = await fetch(url).then((r) => { if (!r.ok) throw new Error(`${url}: ${r.status}`); return r.json(); }); const named = []; const unnamed = []; for (const f of data.features) { const b = bbox(f.geometry); if (b.y1 < -60) continue; // Antarctica const name = f.properties.NAME; (name ? named : unnamed).push({ f, b }); } for (const u of unnamed) { if (u.b.area < 5) { const nearest = named.reduce((a, n) => ((n.b.cx - u.b.cx) ** 2 + (n.b.cy - u.b.cy) ** 2 < (a.b.cx - u.b.cx) ** 2 + (a.b.cy - u.b.cy) ** 2 ? n : a)); u.f.properties.NAME = nearest.f.properties.NAME; } else { u.f.properties.NAME = 'Unclaimed'; } } const features = [...named, ...unnamed].map(({ f }) => ({ type: 'Feature', properties: { name: f.properties.NAME, ruler: f.properties.SUBJECTO ?? null }, geometry: f.geometry, })); const src = join(work, `hb-${year}.geojson`); writeFileSync(src, JSON.stringify({ type: 'FeatureCollection', features })); const dest = new URL(`hb-${year}.json`, out).pathname; execFileSync('npx', ['--yes', 'mapshaper@0.6', src, '-clean', '-simplify', '4%', 'keep-shapes', '-rename-layers', 'countries', '-o', dest, 'format=topojson', 'quantization=1e5'], { stdio: 'inherit' }); console.log(`${year}: ${features.length} shapes (${unnamed.length} unnamed cleaned)`); }