init: info-canicule MVP (Vigilance + climato + conseils)
Astro 5 SSR + ioredis cache Valkey, déployable sur shared-net. - Vigilance temps réel via Opendatasoft (no-auth, LOv2) - Carte SVG des 96 départements (gregoiredavid/france-geojson) - Climato T° 30j par dept (CSV.GZ Météo France, cache 24h) - Conseils officiels par phénomène (7 types Vigilance) - /api/health (UptimeRobot) + /api/vigilance (JSON public CORS *) - Dockerfile multi-stage, CI Forgejo deploy.yml (pattern Reteno) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
e075d963bc
37 changed files with 6730 additions and 0 deletions
111
scripts/build-france-map.mjs
Normal file
111
scripts/build-france-map.mjs
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
#!/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}`);
|
||||
Loading…
Add table
Add a link
Reference in a new issue