#!/usr/bin/env node // Génère un SVG paths data pour la carte des départements français. // Source GeoJSON : github.com/gregoiredavid/france-geojson (Licence Ouverte v2.0). // Projection : Mercator approchée. Suffisant pour visualisation, pas pour cartographie précise. // // Usage : node scripts/build-france-map.mjs // Output : src/data/france-map.json (paths par code INSEE, viewBox) import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); const INPUT = resolve(__dirname, '../data-sources/departements-simplifie.geojson'); const OUTPUT = resolve(__dirname, '../src/data/france-map.json'); const W = 1000; const H = 1000; function project(lng, lat) { // Mercator (lng/lat en degrés → coordonnées planes en radians) const x = (lng * Math.PI) / 180; const y = Math.log(Math.tan(Math.PI / 4 + (lat * Math.PI) / 180 / 2)); return [x, y]; } function ringToPath(ring) { return ring .map(([lng, lat], i) => { const [x, y] = project(lng, lat); return `${i === 0 ? 'M' : 'L'}${x.toFixed(5)},${y.toFixed(5)}`; }) .join('') + 'Z'; } function geometryToPath(geom) { if (geom.type === 'Polygon') { return geom.coordinates.map(ringToPath).join(''); } if (geom.type === 'MultiPolygon') { return geom.coordinates.map((poly) => poly.map(ringToPath).join('')).join(''); } return ''; } const geojson = JSON.parse(readFileSync(INPUT, 'utf-8')); // First pass: compute bounding box on projected coords. let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity; for (const f of geojson.features) { const polys = f.geometry.type === 'Polygon' ? [f.geometry.coordinates] : f.geometry.coordinates; for (const poly of polys) { for (const ring of poly) { for (const [lng, lat] of ring) { const [x, y] = project(lng, lat); if (x < minX) minX = x; if (x > maxX) maxX = x; if (y < minY) minY = y; if (y > maxY) maxY = y; } } } } const scale = Math.min(W / (maxX - minX), H / (maxY - minY)); const offsetX = -minX * scale; const offsetY = maxY * scale; // invert Y (SVG top-down) function scaledRingToPath(ring) { return ring .map(([lng, lat], i) => { const [x, y] = project(lng, lat); const sx = (x * scale + offsetX).toFixed(2); const sy = (offsetY - y * scale).toFixed(2); return `${i === 0 ? 'M' : 'L'}${sx},${sy}`; }) .join('') + 'Z'; } function scaledGeometryToPath(geom) { if (geom.type === 'Polygon') { return geom.coordinates.map(scaledRingToPath).join(''); } if (geom.type === 'MultiPolygon') { return geom.coordinates.map((poly) => poly.map(scaledRingToPath).join('')).join(''); } return ''; } const paths = {}; for (const f of geojson.features) { const code = String(f.properties.code); paths[code] = { d: scaledGeometryToPath(f.geometry), name: f.properties.nom, }; } const finalW = (maxX - minX) * scale; const finalH = (maxY - minY) * scale; const output = { viewBox: `0 0 ${finalW.toFixed(2)} ${finalH.toFixed(2)}`, width: finalW.toFixed(0), height: finalH.toFixed(0), paths, }; mkdirSync(dirname(OUTPUT), { recursive: true }); writeFileSync(OUTPUT, JSON.stringify(output)); console.log(`Wrote ${OUTPUT} — ${Object.keys(paths).length} départements, viewBox ${output.viewBox}`);