#!/usr/bin/env node /** * Génère `src/lib/map/regions-geo.json` : contours simplifiés des communes des * 4 régions JWE, pour la carte SVG statique des écrans hors-jeu (ADR-002). * * Pipeline : geo.api.gouv.fr (contours communes, gratuit, sans clé) * → simplification mapshaper (Visvalingam, `keep-shapes`) * → anneaux externes uniquement, MultiPolygons aplatis, coords à 5 décimales * → JSON minifié versionné dans le repo. * * Rejouable : `node scripts/build-carte-regions.mjs [pourcentage]` * pourcentage = seuil mapshaper (défaut 7%, calibré : ~90 KB gzip -9). * Le script affiche le poids brut et gzip -9 (budget ADR-002 V1 : ≤ 150 KB gzippé). */ import { writeFile } from 'node:fs/promises'; import { gzipSync } from 'node:zlib'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import mapshaper from 'mapshaper'; const REGIONS = { '01': { cle: 'GUADELOUPE', nom: 'Guadeloupe', creole: 'Gwadloup' }, '02': { cle: 'MARTINIQUE', nom: 'Martinique', creole: 'Matinik' }, '03': { cle: 'GUYANE', nom: 'Guyane', creole: 'Giyan' }, '04': { cle: 'REUNION', nom: 'La Réunion', creole: 'Léryinyon' } }; const USER_AGENT = 'JWE-OKI/2.0 (https://labola.o-k-i.net ; scripts/build-carte-regions)'; const BUDGET_GZ = 150 * 1024; const SORTIE = path.join( path.dirname(fileURLToPath(import.meta.url)), '..', 'src', 'lib', 'map', 'regions-geo.json' ); const pourcentage = process.argv[2] ?? '7%'; async function fetchCommunes(codeRegion) { const url = `https://geo.api.gouv.fr/communes?codeRegion=${codeRegion}&fields=nom,code,contour&format=json&geometry=contour`; const res = await fetch(url, { headers: { 'User-Agent': USER_AGENT, Accept: 'application/json' } }); if (!res.ok) throw new Error(`geo.api.gouv.fr région ${codeRegion} : HTTP ${res.status}`); return res.json(); } function applyCommands(cmd, inputs) { return new Promise((resolve, reject) => { mapshaper.applyCommands(cmd, inputs, (err, out) => (err ? reject(err) : resolve(out))); }); } /** Simplifie un GeoJSON avec mapshaper et renvoie le GeoJSON parsé. */ async function simplifier(featureCollection) { const cmd = `-i communes.geojson -simplify ${pourcentage} keep-shapes -o sortie.geojson`; const out = await applyCommands(cmd, { 'communes.geojson': featureCollection }); const brut = out['sortie.geojson']; // mapshaper renvoie un Buffer en sortie fichier (Node), une string ou un objet selon le format if (Buffer.isBuffer(brut)) return JSON.parse(brut.toString('utf8')); return typeof brut === 'string' ? JSON.parse(brut) : brut; } const r5 = (n) => Math.round(n * 1e5) / 1e5; /** Anneaux externes seuls ; MultiPolygon → plusieurs polygones ; coords arrondies. */ function extrairePolygones(geojson) { const polygons = []; for (const feature of geojson.features) { const g = feature.geometry; const multi = g?.type === 'MultiPolygon' ? g.coordinates : g?.type === 'Polygon' ? [g.coordinates] : []; for (const poly of multi) { const anneau = poly[0]; if (!Array.isArray(anneau) || anneau.length < 4) continue; polygons.push(anneau.map(([lon, lat]) => [r5(lon), r5(lat)])); } } return polygons; } const resultat = {}; for (const [codeRegion, info] of Object.entries(REGIONS)) { const communes = await fetchCommunes(codeRegion); const fc = { type: 'FeatureCollection', features: communes .filter((c) => c.contour) .map((c) => ({ type: 'Feature', properties: { code: c.code, nom: c.nom }, geometry: c.contour })) }; const simplifie = await simplifier(fc); const polygons = extrairePolygones(simplifie); const points = polygons.reduce((s, p) => s + p.length, 0); resultat[info.cle] = { nom: info.nom, creole: info.creole, polygons }; console.log( `${info.cle.padEnd(11)} ${String(fc.features.length).padStart(3)} communes → ` + `${String(polygons.length).padStart(3)} polygones, ${points} points` ); } const json = JSON.stringify(resultat); await writeFile(SORTIE, json + '\n', 'utf8'); const brut = Buffer.byteLength(json, 'utf8'); const gz = gzipSync(json, { level: 9 }).length; console.log(`\n${path.relative(process.cwd(), SORTIE)}`); console.log(`Brut : ${(brut / 1024).toFixed(1)} KB`); console.log(`gzip -9 : ${(gz / 1024).toFixed(1)} KB (budget V1 : 150 KB)`); if (gz > BUDGET_GZ) { console.error('DÉPASSEMENT DU BUDGET — relancer avec un pourcentage plus faible.'); process.exitCode = 1; }