#!/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}`);