diff --git a/Données_d’observation_swagger.json b/Données_d’observation_swagger.json new file mode 100644 index 0000000..18dbff1 --- /dev/null +++ b/Données_d’observation_swagger.json @@ -0,0 +1,460 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "DonneesPubliquesObservation", + "version": "1" + }, + "servers": [ + { + "url": "https://public-api.meteofrance.fr/public/DPObs/v1" + } + ], + "security": [ + { + "default": [] + } + ], + "tags": [ + { + "name": "Produits Obs", + "description": "Services de téléchargement des données d'observation à la date demandée." + }, + { + "name": "Produits Synop", + "description": "Services de téléchargement des messages internationaux d’observation en surface, SYNOP, à la date demandée." + }, + { + "name": "Produits Bouees", + "description": "ProduitBouees - Service de téléchargement des données d'observations météorologiques effectuées par les bouées Météo-France, à la date demandée" + } + ], + "paths": { + "/station/infrahoraire-6m": { + "get": { + "tags": [ + "Produits Obs" + ], + "summary": "Télécharger le fichier TEXTE (CSV ou JSON ou GEOJSON) des données d'observation pour tous les paramètres disponibles, à la fréquence 6 minutes, pour une station, à la date la plus proche de la date demandée.", + "description": "Renvoie tous les paramètres disponibles pour la station demandée et pour :\n- la date/heure la plus proche de la date demandée selon les données disponibles.", + "parameters": [ + { + "name": "id_station", + "in": "query", + "description": "Identifiant de la station (nomenclature : 8 chiffres selon DDCCCNNN = insee de la commune (DD département, CCC n° de la commune dans le département et NNN n° de la station dans la commune)", + "required": true, + "style": "form", + "explode": true, + "schema": { + "type": "string" + } + }, + { + "name": "date", + "in": "query", + "description": "Date demandée (au format ISO 8601 avec TZ UTC : AAAA-MM-JJThh:mm:ssZ).\n\nPar défaut = date courante", + "required": false, + "style": "form", + "explode": true, + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "format", + "in": "query", + "description": "Format de retour des données (JSON ou CSV ou GEOJSON)", + "required": true, + "style": "form", + "explode": true, + "schema": { + "type": "string", + "enum": [ + "json", + "csv", + "geojson" + ] + } + } + ], + "responses": { + "200": { + "description": "Si une liste vide est renvoyée : station absente ou inexistante\n \nSinon : OK" + }, + "400": { + "description": "Contrôle de paramètres en erreur" + }, + "404": { + "description": "Jeu de données inexistant" + } + }, + "security": [ + { + "default": [] + } + ], + "x-auth-type": "Application & Application User", + "x-throttling-tier": "Unlimited" + } + }, + "/station/horaire": { + "get": { + "tags": [ + "Produits Obs" + ], + "summary": "Télécharger le fichier TEXTE (CSV ou JSON ou GEOJSON) des données d’observation pour tous les paramètres disponibles, à la fréquence horaire, pour une station, à la date la plus proche de la date demandée.", + "description": "Renvoie tous les paramètres disponibles pour la station demandée et pour :\n- la date/heure la plus proche de la date demandée selon les données disponibles.", + "parameters": [ + { + "name": "id_station", + "in": "query", + "description": "Identifiant de la station (nomenclature : 8 chiffres selon DDCCCNNN = insee de la commune (DD département, CCC n° de la commune dans le département et NNN n° de la station dans la commune)", + "required": true, + "style": "form", + "explode": true, + "schema": { + "type": "string" + } + }, + { + "name": "date", + "in": "query", + "description": "Date demandée (au format ISO 8601 avec TZ UTC : AAAA-MM-JJThh:mm:ssZ).\n\nPar défaut = date courante", + "required": false, + "style": "form", + "explode": true, + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "format", + "in": "query", + "description": "Format de retour des données (JSON ou CSV ou GEOJSON)", + "required": true, + "style": "form", + "explode": true, + "schema": { + "type": "string", + "enum": [ + "json", + "csv", + "geojson" + ] + } + } + ], + "responses": { + "200": { + "description": "Si une liste vide est renvoyée : station absente ou inexistante\n \nSinon : OK" + }, + "400": { + "description": "Contrôle de paramètres en erreur" + }, + "404": { + "description": "Jeu de données inexistant" + } + }, + "security": [ + { + "default": [] + } + ], + "x-auth-type": "Application & Application User", + "x-throttling-tier": "Unlimited" + } + }, + "/liste-stations": { + "get": { + "tags": [ + "Produits Obs" + ], + "summary": "Télécharger le fichier TEXTE (CSV) de la liste des stations d'observation.", + "description": "Renvoie la liste des stations.", + "parameters": [], + "responses": { + "200": { + "description": "OK" + }, + "404": { + "description": "La liste est indisponible" + } + }, + "security": [ + { + "default": [] + } + ], + "x-auth-type": "Application & Application User", + "x-throttling-tier": "Unlimited" + } + }, + "/liste-stations-synop": { + "get": { + "tags": [ + "Produits Synop" + ], + "summary": "Télécharger le fichier TEXTE de la liste des stations d'observation.", + "description": "Renvoie la liste des stations.", + "parameters": [ + { + "name": "format", + "in": "query", + "description": "Format de retour des données (JSON ou CSV ou GEOJSON)", + "required": true, + "style": "form", + "explode": true, + "schema": { + "type": "string", + "enum": [ + "json", + "csv", + "geojson" + ] + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "404": { + "description": "La liste est indisponible" + } + }, + "security": [ + { + "default": [] + } + ], + "x-auth-type": "Application & Application User", + "x-throttling-tier": "Unlimited" + } + }, + "/synop": { + "get": { + "tags": [ + "Produits Synop" + ], + "summary": "Télécharger le fichier TEXTE (CSV ou JSON ou GEOJSON) des données SYNOP pour une station, à la date demandée.", + "description": "Renvoie tous les paramètres disponibles pour la station et pour la date/heure demandées.", + "parameters": [ + { + "name": "format", + "in": "query", + "description": "Format de retour des données (JSON ou CSV ou GEOJSON)", + "required": true, + "style": "form", + "explode": true, + "schema": { + "type": "string", + "enum": [ + "json", + "csv", + "geojson" + ] + } + }, + { + "name": "id_station", + "in": "query", + "description": "Identifiant(s) de la station (séparés par des , par exemple id_station=07002,07003)", + "required": false, + "style": "form", + "explode": true, + "schema": { + "type": "string" + } + }, + { + "name": "date_debut", + "in": "query", + "description": "Date de début d'observation au format AAAA-MM-JJTHH:mm:00Z.", + "required": false, + "style": "form", + "explode": true, + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "date_fin", + "in": "query", + "description": "Date de fin d'observation au format AAAA-MM-JJTHH:mm:00Z.", + "required": false, + "style": "form", + "explode": true, + "schema": { + "type": "string", + "format": "date-time" + } + } + ], + "responses": { + "200": { + "description": "OK (liste vide si pas de donnée correspondant à la requête)" + }, + "400": { + "description": "Contrôle de paramètres en erreur" + }, + "401": { + "description": "Non autorisé - informations d'identification non valides" + }, + "403": { + "description": "Accès interdit" + }, + "404": { + "description": "Jeu de données inexistant et/ou jeu de données indisponible" + }, + "500": { + "description": "Erreur interne au serveur" + }, + "502": { + "description": "Erreur de passerelle" + }, + "503": { + "description": "Service indisponible" + }, + "504": { + "description": "Temps trop long" + } + }, + "security": [ + { + "default": [] + } + ], + "x-auth-type": "Application & Application User", + "x-throttling-tier": "Unlimited" + } + }, + "/liste-bouees": { + "get": { + "tags": [ + "Produits Bouees" + ], + "summary": "Télécharger le fichier TEXTE de la liste des stations d'observation.", + "description": "Renvoie la liste des stations et bouées.", + "parameters": [ + { + "name": "format", + "in": "query", + "required": true, + "style": "form", + "explode": true, + "schema": { + "type": "string", + "enum": [ + "json", + "csv", + "geojson" + ] + } + } + ], + "responses": { + "200": { + "description": "ok" + }, + "404": { + "description": "La liste est indisponible" + } + }, + "security": [ + { + "default": [] + } + ], + "x-auth-type": "Application & Application User", + "x-throttling-tier": "Unlimited" + } + }, + "/bouees": { + "get": { + "tags": [ + "Produits Bouees" + ], + "summary": "Télécharger le fichier TEXTE (CSV ou JSON ou GEOJSON) des données d'observations pour une station, à la date demandée.", + "description": "Renvoie tous les paramètres disponibles pour la station à la date heure demandée.", + "parameters": [ + { + "name": "format", + "in": "query", + "description": "Format de retour des données (JSON ou CSV ou GEOJSON)", + "required": true, + "style": "form", + "explode": true, + "schema": { + "type": "string", + "enum": [ + "json", + "csv", + "geojson" + ] + } + }, + { + "name": "date_debut", + "in": "query", + "description": "Date de début d'observation au format AAAA-MM-JJTHH:mm:00Z", + "required": false, + "style": "form", + "explode": true, + "schema": { + "type": "string" + } + }, + { + "name": "date_fin", + "in": "query", + "description": "Date de fin d'observation au format AAAA-MM-JJTHH:mm:00Z", + "required": false, + "style": "form", + "explode": true, + "schema": { + "type": "string" + } + }, + { + "name": "id_bouees", + "in": "query", + "description": "Identifiant(s) de la station (séparés par des , par exemple id_station=6100001,6100002)", + "required": false, + "style": "form", + "explode": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "ok (liste vide si aucune donnée ne correspondent à la requête)" + } + }, + "security": [ + { + "default": [] + } + ], + "x-auth-type": "Application & Application User", + "x-throttling-tier": "Unlimited" + } + } + }, + "components": { + "securitySchemes": { + "default": { + "type": "oauth2", + "flows": { + "implicit": { + "authorizationUrl": "https://public-api.meteofrance.fr/authorize", + "scopes": {} + } + } + } + } + } +} \ No newline at end of file diff --git a/Données_d’observation_swagger.json:Zone.Identifier b/Données_d’observation_swagger.json:Zone.Identifier new file mode 100644 index 0000000..d6c1ec6 Binary files /dev/null and b/Données_d’observation_swagger.json:Zone.Identifier differ diff --git a/scripts/build-stations-synop.mjs b/scripts/build-stations-synop.mjs new file mode 100644 index 0000000..286f021 --- /dev/null +++ b/scripts/build-stations-synop.mjs @@ -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}`); diff --git a/src/components/FranceMap.astro b/src/components/FranceMap.astro index 7d07e95..c11476b 100644 --- a/src/components/FranceMap.astro +++ b/src/components/FranceMap.astro @@ -48,7 +48,7 @@ for (const [code] of entries) { xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Carte des départements français colorée selon le niveau Vigilance" - class="h-auto w-full max-w-3xl mx-auto" + class="h-auto w-full max-w-5xl mx-auto" id="france-map" >