117 lines
4.3 KiB
JavaScript
117 lines
4.3 KiB
JavaScript
#!/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;
|
|
}
|