fix: anomalie fenêtres 3j+7j (worst-case) + carte plus large PC
Some checks are pending
Deploy info-canicule / deploy (push) Waiting to run
Some checks are pending
Deploy info-canicule / deploy (push) Waiting to run
- normales.ts : computeAnomaly évalue 2 fenêtres (3j pour détecter un pic récent, 7j pour la tendance lissée), retient la pire catégorie. Évite de noyer un +15°C sur 3 jours dans 4 jours précédents normaux. - FranceMap : max-w-3xl → max-w-5xl (1024px sur PC, plus lisible). Mapping dept → station SYNOP (script build-stations-synop.mjs + json statique) pour préparer l'intégration hourly via API Météo France officielle. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c2b489f9b9
commit
9cfd4f8385
6 changed files with 592 additions and 33 deletions
91
scripts/build-stations-synop.mjs
Normal file
91
scripts/build-stations-synop.mjs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
#!/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
|
||||
const rows = readFileSync(STATIONS_CSV, 'utf-8').trim().split('\n');
|
||||
const header = rows[0].split(';');
|
||||
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],
|
||||
name: c[idx.name],
|
||||
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}`);
|
||||
Loading…
Add table
Add a link
Reference in a new issue