info-canicule/scripts/build-stations-synop.mjs
Florian a007f340dc
Some checks are pending
Deploy info-canicule / deploy (push) Waiting to run
fix(stations): parse CSV en CRLF robust (le name était stripped \r → undefined)
Le CSV liste-stations-synop de Météo France arrive en CRLF, le split('\n')
laissait un \r en fin de chaque header → header.indexOf('name') = -1
→ stations[].name = undefined → 'station undefined' affiché côté front.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 00:37:09 +02:00

92 lines
3.9 KiB
JavaScript

#!/usr/bin/env node
// Calcule la station SYNOP Météo France la plus proche du centroïde de chaque département.
//
// Inputs :
// - data-sources/stations-synop.csv (188 stations SYNOP en France, format: lat;lon;geo_id_wmo;geo_id_wigos;name)
// - data-sources/departements-simplifie.geojson (96 départements métropole)
// Output :
// - src/data/stations-synop.json { "01": { stationId, name, distKm }, "02": ..., ... }
//
// Notes :
// - 188 stations / 96 depts ⇒ ~2 stations/dept en moyenne, certains dépts ruraux n'ont aucune station SYNOP
// dans leurs frontières (rabattu sur la plus proche, distance Haversine en km).
// - Pour les DROM (971-976), la liste SYNOP couvre quelques stations (à vérifier en regardant les ids).
// - Le mapping est rejoué uniquement si la liste SYNOP change (rare).
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const STATIONS_CSV = resolve(__dirname, '../data-sources/stations-synop.csv');
const GEOJSON = resolve(__dirname, '../data-sources/departements-simplifie.geojson');
const OUT = resolve(__dirname, '../src/data/stations-synop.json');
// Haversine — distance grand-cercle entre 2 points en km.
function distKm(lat1, lon1, lat2, lon2) {
const R = 6371;
const toRad = (d) => (d * Math.PI) / 180;
const dLat = toRad(lat2 - lat1);
const dLon = toRad(lon2 - lon1);
const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2;
return 2 * R * Math.asin(Math.sqrt(a));
}
// Parse CSV stations
// CSV Météo France peut être en CRLF — split robuste sur les 2 EOLs.
const rows = readFileSync(STATIONS_CSV, 'utf-8').trim().split(/\r?\n/);
const header = rows[0].split(';').map((h) => h.trim());
const idx = {
lat: header.indexOf('lat'),
lon: header.indexOf('lon'),
wmo: header.indexOf('geo_id_wmo'),
name: header.indexOf('name'),
};
const stations = rows.slice(1).map((line) => {
const c = line.split(';');
return {
id: (c[idx.wmo] ?? '').trim(),
name: (c[idx.name] ?? '').trim(),
lat: parseFloat(c[idx.lat]),
lon: parseFloat(c[idx.lon]),
};
}).filter((s) => Number.isFinite(s.lat) && Number.isFinite(s.lon) && s.id);
console.log(`Loaded ${stations.length} SYNOP stations`);
// Centroïde par dept depuis le GeoJSON (moyenne simple des coords du contour externe).
const geojson = JSON.parse(readFileSync(GEOJSON, 'utf-8'));
const centroids = {};
for (const f of geojson.features) {
const code = String(f.properties.code);
const polys = f.geometry.type === 'Polygon' ? [f.geometry.coordinates] : f.geometry.coordinates;
let sumLat = 0, sumLon = 0, n = 0;
for (const poly of polys) {
const ring = poly[0]; // contour externe
for (const [lng, lat] of ring) { sumLat += lat; sumLon += lng; n++; }
}
if (n) centroids[code] = { lat: sumLat / n, lon: sumLon / n };
}
console.log(`Computed ${Object.keys(centroids).length} dept centroids`);
// Plus proche station par dept.
const mapping = {};
for (const [code, c] of Object.entries(centroids)) {
let best = null;
for (const s of stations) {
const d = distKm(c.lat, c.lon, s.lat, s.lon);
if (!best || d < best.distKm) best = { stationId: s.id, name: s.name, distKm: +d.toFixed(1) };
}
mapping[code] = best;
}
mkdirSync(dirname(OUT), { recursive: true });
writeFileSync(OUT, JSON.stringify(mapping, null, 0));
// Stats : moyenne, médiane, max des distances.
const dists = Object.values(mapping).map((m) => m.distKm).sort((a, b) => a - b);
const med = dists[Math.floor(dists.length / 2)];
const max = dists[dists.length - 1];
const farDepts = Object.entries(mapping).filter(([, m]) => m.distKm > 50).map(([k, m]) => `${k}=${m.distKm}km`);
console.log(`distKm: median=${med}, max=${max}, depts >50km away from station: ${farDepts.join(', ')}`);
console.log(`Wrote ${OUT}`);