Melomic.fr
La boite à Tutti - Live - Test carte perturbation

Test carte perturbation

<!-- Leaflet CSS -->
<link
 rel="stylesheet"
 href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
 integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
 crossorigin=""
>

<style>
 .meteo-france-extreme {
   max-width: 1200px;
   margin: 30px auto;
   padding: 20px;
 }

 .meteo-france-extreme h2,
 .meteo-france-extreme h3 {
   color: #fff;
 }

 .meteo-france-actions,
 .meteo-france-options {
   display: flex;
   flex-wrap: wrap;
   gap: 10px;
   margin-bottom: 14px;
 }

 .meteo-france-actions button,
 .meteo-france-actions a,
 .meteo-france-options select,
 .meteo-france-options input {
   padding: 9px 12px;
   border: 1px solid #ccc;
   border-radius: 8px;
   background: #fff;
   color: #111;
   font-weight: bold;
   text-decoration: none;
 }

 .meteo-france-actions button {
   cursor: pointer;
 }

 .meteo-france-check {
   display: flex;
   align-items: center;
   gap: 6px;
   color: #fff;
 }

 #meteo-france-status,
 #meteo-france-radar-status {
   margin: 8px 0;
   color: #fff;
   font-weight: bold;
 }

 #map-meteo-france-extreme {
   width: 100%;
   height: 560px;
   position: relative;
   overflow: hidden;
   background: #aad3df;
   border-radius: 14px;
   margin-bottom: 20px;
   border: 1px solid #ddd;
   z-index: 1;
 }

 #map-meteo-france-extreme.leaflet-container {
   width: 100%;
   height: 560px;
 }

 #map-meteo-france-extreme .leaflet-tile,
 #map-meteo-france-extreme .leaflet-marker-icon,
 #map-meteo-france-extreme .leaflet-marker-shadow,
 #map-meteo-france-extreme .leaflet-pane img {
   max-width: none !important;
   max-height: none !important;
   margin: 0 !important;
   padding: 0 !important;
   border-radius: 0 !important;
   box-shadow: none !important;
   object-fit: initial !important;
 }

 #map-meteo-france-extreme .leaflet-tile {
   width: 256px !important;
   height: 256px !important;
 }

 .mf-grid {
   display: grid;
   grid-template-columns: 1fr 1fr;
   gap: 12px;
   margin-bottom: 24px;
 }

 .mf-card,
 .mf-ligne {
   border: 1px solid #ddd;
   border-radius: 12px;
   padding: 14px;
   background: rgba(255,255,255,0.96);
   color: #111;
 }

 .mf-card h3,
 .mf-card p,
 .mf-ligne h4,
 .mf-ligne p,
 .mf-card strong,
 .mf-ligne strong {
   color: #111 !important;
 }

 .mf-card h3,
 .mf-ligne h4 {
   margin: 0 0 8px;
 }

 .mf-card p,
 .mf-ligne p {
   margin: 5px 0;
 }

 .mf-liste {
   display: grid;
   grid-template-columns: 1fr 1fr;
   gap: 12px;
 }

 .mf-badge {
   display: inline-block;
   padding: 4px 9px;
   border-radius: 999px;
   font-weight: bold;
   margin: 3px 4px 3px 0;
   border: 1px solid rgba(0,0,0,0.2);
 }

 .mf-vert {
   background: #39b54a;
   color: #fff;
 }

 .mf-jaune {
   background: #f4c430;
   color: #111;
 }

 .mf-orange {
   background: #ff7b00;
   color: #111;
 }

 .mf-rouge {
   background: #d00000;
   color: #fff;
 }

 .mf-marker {
   background: transparent !important;
   border: none !important;
 }

 .mf-pin {
   width: 30px;
   height: 30px;
   border-radius: 50%;
   border: 3px solid #fff;
   box-shadow: 0 0 12px rgba(0,0,0,0.75);
   display: flex;
   align-items: center;
   justify-content: center;
   font-size: 16px;
   font-weight: bold;
 }

 .mf-pin.mf-vert {
   background: #39b54a;
   color: #fff;
 }

 .mf-pin.mf-jaune {
   background: #f4c430;
   color: #111;
 }

 .mf-pin.mf-orange {
   background: #ff7b00;
   color: #111;
   animation: mfPulse 1.6s infinite;
 }

 .mf-pin.mf-rouge {
   background: #d00000;
   color: #fff;
   animation: mfPulse 1.2s infinite;
 }

 @keyframes mfPulse {
   0% {
     box-shadow: 0 0 0 0 rgba(255,255,255,0.8);
   }
   70% {
     box-shadow: 0 0 0 14px rgba(255,255,255,0);
   }
   100% {
     box-shadow: 0 0 0 0 rgba(255,255,255,0);
   }
 }

 @media (max-width: 800px) {
   .mf-grid,
   .mf-liste {
     grid-template-columns: 1fr;
   }

   #map-meteo-france-extreme,
   #map-meteo-france-extreme.leaflet-container {
     height: 420px;
   }
 }

 @media (max-width: 600px) {
   #map-meteo-france-extreme,
   #map-meteo-france-extreme.leaflet-container {
     height: 360px;
   }
 }
</style>

<section class="meteo-france-extreme">
 <h2>Météo extrême France : orages, tempêtes, pluie, cyclones et foudre</h2>

 <div class="meteo-france-actions">
   <button id="btn-mf-reload" type="button">Recharger vigilances</button>
   <button id="btn-mf-radar" type="button">Afficher / masquer radar</button>
   <button id="btn-mf-radar-play" type="button">Animer radar</button>
   <button id="btn-mf-radar-stop" type="button">Stop radar</button>
   <button id="btn-mf-france" type="button">Recentrer France</button>

   <a href="https://map.blitzortung.org/" target="_blank" rel="noopener">
     Carte foudre temps réel
   </a>

   <a href="https://vigilance.meteofrance.fr/fr" target="_blank" rel="noopener">
     Vigilance officielle
   </a>
 </div>

 <div class="meteo-france-options">
   <select id="mf-echeance">
     <option value="toutes">Aujourd’hui + demain</option>
     <option value="J">Aujourd’hui</option>
     <option value="J1">Demain</option>
   </select>

   <select id="mf-niveau-min">
     <option value="1">Vert et plus</option>
     <option value="2" selected>Jaune et plus</option>
     <option value="3">Orange et rouge</option>
     <option value="4">Rouge seulement</option>
   </select>

   <input id="mf-dep-search" type="text" placeholder="Département : 75, 13, 2A, 971...">

   <label class="meteo-france-check">
     <input id="mf-filtre-orage" type="checkbox" checked>
     Orages
   </label>

   <label class="meteo-france-check">
     <input id="mf-filtre-vent" type="checkbox" checked>
     Vent / tempête
   </label>

   <label class="meteo-france-check">
     <input id="mf-filtre-pluie" type="checkbox" checked>
     Pluie / inondation
   </label>

   <label class="meteo-france-check">
     <input id="mf-filtre-cyclone" type="checkbox" checked>
     Cyclone / submersion
   </label>
 </div>

 <div id="meteo-france-status">Chargement des vigilances...</div>
 <div id="meteo-france-radar-status">Radar non chargé.</div>

 <div id="map-meteo-france-extreme"></div>

 <div class="mf-grid">
   <article id="mf-resume" class="mf-card">
     <h3>Résumé</h3>
     <p>Chargement...</p>
   </article>

   <article class="mf-card">
     <h3>Légende</h3>
     <p><span class="mf-badge mf-jaune">Jaune</span> vigilance à surveiller</p>
     <p><span class="mf-badge mf-orange">Orange</span> phénomène dangereux</p>
     <p><span class="mf-badge mf-rouge">Rouge</span> danger exceptionnel</p>
     <p><small>La foudre exacte n’est pas affichée ici sans API dédiée : le bouton ouvre une carte externe.</small></p>
   </article>
 </div>

 <h3>Départements concernés</h3>
 <div id="mf-liste" class="mf-liste"></div>
</section>

<!-- Leaflet JS -->
<script
 src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
 integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
 crossorigin="">
</script>

<script>
(function () {
 const API_RAINVIEWER = "https://api.rainviewer.com/public/weather-maps.json";

 const API_VIGILANCE =
   "https://public.opendatasoft.com/api/explore/v2.1/catalog/datasets/weatherref-france-vigilance-meteo-departement/records";

 const DEPARTEMENTS = {
   "01": ["Ain", 46.205, 5.225],
   "02": ["Aisne", 49.565, 3.624],
   "03": ["Allier", 46.568, 3.334],
   "04": ["Alpes-de-Haute-Provence", 44.092, 6.235],
   "05": ["Hautes-Alpes", 44.559, 6.079],
   "06": ["Alpes-Maritimes", 43.710, 7.262],
   "07": ["Ardèche", 44.735, 4.599],
   "08": ["Ardennes", 49.762, 4.722],
   "09": ["Ariège", 42.965, 1.607],
   "10": ["Aube", 48.297, 4.074],
   "11": ["Aude", 43.213, 2.352],
   "12": ["Aveyron", 44.349, 2.575],
   "13": ["Bouches-du-Rhône", 43.296, 5.370],
   "14": ["Calvados", 49.182, -0.370],
   "15": ["Cantal", 44.929, 2.444],
   "16": ["Charente", 45.648, 0.156],
   "17": ["Charente-Maritime", 46.160, -1.151],
   "18": ["Cher", 47.081, 2.398],
   "19": ["Corrèze", 45.267, 1.772],
   "21": ["Côte-d’Or", 47.322, 5.041],
   "22": ["Côtes-d’Armor", 48.514, -2.765],
   "23": ["Creuse", 46.170, 1.872],
   "24": ["Dordogne", 45.184, 0.721],
   "25": ["Doubs", 47.238, 6.024],
   "26": ["Drôme", 44.933, 4.892],
   "27": ["Eure", 49.027, 1.151],
   "28": ["Eure-et-Loir", 48.446, 1.489],
   "29": ["Finistère", 47.996, -4.100],
   "2A": ["Corse-du-Sud", 41.919, 8.738],
   "2B": ["Haute-Corse", 42.697, 9.450],
   "30": ["Gard", 43.837, 4.360],
   "31": ["Haute-Garonne", 43.604, 1.444],
   "32": ["Gers", 43.646, 0.586],
   "33": ["Gironde", 44.837, -0.579],
   "34": ["Hérault", 43.611, 3.877],
   "35": ["Ille-et-Vilaine", 48.117, -1.677],
   "36": ["Indre", 46.810, 1.691],
   "37": ["Indre-et-Loire", 47.394, 0.684],
   "38": ["Isère", 45.188, 5.724],
   "39": ["Jura", 46.675, 5.555],
   "40": ["Landes", 43.891, -0.500],
   "41": ["Loir-et-Cher", 47.594, 1.329],
   "42": ["Loire", 45.439, 4.387],
   "43": ["Haute-Loire", 45.043, 3.885],
   "44": ["Loire-Atlantique", 47.218, -1.553],
   "45": ["Loiret", 47.903, 1.909],
   "46": ["Lot", 44.447, 1.441],
   "47": ["Lot-et-Garonne", 44.204, 0.617],
   "48": ["Lozère", 44.518, 3.501],
   "49": ["Maine-et-Loire", 47.478, -0.563],
   "50": ["Manche", 49.116, -1.090],
   "51": ["Marne", 48.956, 4.363],
   "52": ["Haute-Marne", 48.111, 5.139],
   "53": ["Mayenne", 48.070, -0.770],
   "54": ["Meurthe-et-Moselle", 48.692, 6.184],
   "55": ["Meuse", 48.772, 5.162],
   "56": ["Morbihan", 47.658, -2.760],
   "57": ["Moselle", 49.119, 6.175],
   "58": ["Nièvre", 46.990, 3.159],
   "59": ["Nord", 50.629, 3.057],
   "60": ["Oise", 49.430, 2.083],
   "61": ["Orne", 48.432, 0.091],
   "62": ["Pas-de-Calais", 50.292, 2.778],
   "63": ["Puy-de-Dôme", 45.777, 3.087],
   "64": ["Pyrénées-Atlantiques", 43.296, -0.370],
   "65": ["Hautes-Pyrénées", 43.232, 0.078],
   "66": ["Pyrénées-Orientales", 42.688, 2.894],
   "67": ["Bas-Rhin", 48.573, 7.752],
   "68": ["Haut-Rhin", 48.079, 7.358],
   "69": ["Rhône", 45.764, 4.835],
   "70": ["Haute-Saône", 47.623, 6.155],
   "71": ["Saône-et-Loire", 46.306, 4.828],
   "72": ["Sarthe", 48.006, 0.199],
   "73": ["Savoie", 45.565, 5.917],
   "74": ["Haute-Savoie", 45.900, 6.129],
   "75": ["Paris", 48.856, 2.352],
   "76": ["Seine-Maritime", 49.443, 1.099],
   "77": ["Seine-et-Marne", 48.540, 2.660],
   "78": ["Yvelines", 48.804, 2.120],
   "79": ["Deux-Sèvres", 46.323, -0.464],
   "80": ["Somme", 49.895, 2.302],
   "81": ["Tarn", 43.929, 2.148],
   "82": ["Tarn-et-Garonne", 44.018, 1.355],
   "83": ["Var", 43.124, 5.928],
   "84": ["Vaucluse", 43.949, 4.805],
   "85": ["Vendée", 46.670, -1.426],
   "86": ["Vienne", 46.580, 0.340],
   "87": ["Haute-Vienne", 45.833, 1.261],
   "88": ["Vosges", 48.173, 6.449],
   "89": ["Yonne", 47.798, 3.574],
   "90": ["Territoire de Belfort", 47.638, 6.863],
   "91": ["Essonne", 48.630, 2.443],
   "92": ["Hauts-de-Seine", 48.892, 2.206],
   "93": ["Seine-Saint-Denis", 48.907, 2.445],
   "94": ["Val-de-Marne", 48.790, 2.455],
   "95": ["Val-d’Oise", 49.036, 2.063],
   "971": ["Guadeloupe", 16.000, -61.730],
   "972": ["Martinique", 14.616, -61.058],
   "973": ["Guyane", 4.933, -52.333],
   "974": ["La Réunion", -20.879, 55.448],
   "976": ["Mayotte", -12.781, 45.228]
 };

 const statusBox = document.getElementById("meteo-france-status");
 const radarStatus = document.getElementById("meteo-france-radar-status");
 const resumeBox = document.getElementById("mf-resume");
 const listeBox = document.getElementById("mf-liste");

 const btnReload = document.getElementById("btn-mf-reload");
 const btnRadar = document.getElementById("btn-mf-radar");
 const btnRadarPlay = document.getElementById("btn-mf-radar-play");
 const btnRadarStop = document.getElementById("btn-mf-radar-stop");
 const btnFrance = document.getElementById("btn-mf-france");

 const echeanceInput = document.getElementById("mf-echeance");
 const niveauMinInput = document.getElementById("mf-niveau-min");
 const depSearchInput = document.getElementById("mf-dep-search");

 const filtreOrage = document.getElementById("mf-filtre-orage");
 const filtreVent = document.getElementById("mf-filtre-vent");
 const filtrePluie = document.getElementById("mf-filtre-pluie");
 const filtreCyclone = document.getElementById("mf-filtre-cyclone");

 let vigilances = [];
 let radarFrames = [];
 let radarHost = "";
 let radarIndex = 0;
 let radarLayer = null;
 let radarTimer = null;
 let vigilanceLayer = null;

 const map = L.map("map-meteo-france-extreme", {
   worldCopyJump: true
 }).setView([46.7, 2.5], 6);

 L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
   maxZoom: 18,
   attribution: '&copy; OpenStreetMap'
 }).addTo(map);

 vigilanceLayer = L.layerGroup().addTo(map);

 function escapeHtml(value) {
   return String(value ?? "")
     .replaceAll("&", "&amp;")
     .replaceAll("<", "&lt;")
     .replaceAll(">", "&gt;")
     .replaceAll('"', "&quot;")
     .replaceAll("'", "&#039;");
 }

 function refreshMapSize() {
   setTimeout(function () {
     map.invalidateSize();
   }, 300);
 }

 function normalize(value) {
   return String(value || "")
     .normalize("NFD")
     .replace(/[\u0300-\u036f]/g, "")
     .toLowerCase();
 }

 function normaliserDep(value) {
   const v = String(value || "").trim().toUpperCase();

   if (/^[0-9]$/.test(v)) {
     return "0" + v;
   }

   return v;
 }

 function niveauDepuisTexte(value) {
   const v = normalize(value);

   if (v.includes("rouge")) return 4;
   if (v.includes("orange")) return 3;
   if (v.includes("jaune")) return 2;
   if (v.includes("vert")) return 1;

   return 0;
 }

 function couleurNiveau(niveau) {
   const n = Number(niveau);

   if (n >= 4) return "rouge";
   if (n === 3) return "orange";
   if (n === 2) return "jaune";

   return "vert";
 }

 function labelNiveau(niveau) {
   const c = couleurNiveau(niveau);

   if (c === "rouge") return "Rouge";
   if (c === "orange") return "Orange";
   if (c === "jaune") return "Jaune";

   return "Vert";
 }

 function badgeNiveau(niveau) {
   const c = couleurNiveau(niveau);

   return `<span class="mf-badge mf-${c}">${labelNiveau(niveau)}</span>`;
 }

 function formatDate(value) {
   if (!value) return "—";

   const d = new Date(value);

   if (Number.isNaN(d.getTime())) {
     return "—";
   }

   return d.toLocaleString("fr-FR", {
     dateStyle: "short",
     timeStyle: "short"
   });
 }

 function normaliserRecord(record) {
   const f = record.fields || record;

   const dep = normaliserDep(
     f.domain_id ??
     f.code_departement ??
     f.department_code ??
     f.departement ??
     f.dep ??
     ""
   );

   if (!dep) {
     return null;
   }

   const phenomenon = String(
     f.phenomenon ??
     f.phenomene ??
     f.phenomenon_name ??
     f.libelle_phenomene ??
     f.nom_phenomene ??
     "Phénomène météo"
   );

   let colorId = Number(
     f.color_id ??
     f.couleur_id ??
     f.phenomenon_color_id ??
     f.max_color_id ??
     0
   );

   if (!colorId) {
     colorId = niveauDepuisTexte(
       f.color ??
       f.couleur ??
       f.level ??
       f.niveau ??
       ""
     );
   }

   return {
     dep: dep,
     echeance: String(f.echeance ?? f.period ?? "").trim(),
     phenomenon: phenomenon,
     color_id: colorId || 1,
     color: String(f.color ?? f.couleur ?? "").trim(),
     begin_time: f.begin_time ?? f.debut_validite ?? f.start_time ?? "",
     end_time: f.end_time ?? f.fin_validite ?? f.end_time ?? "",
     product_datetime: f.product_datetime ?? f.date_production ?? f.updated_at ?? ""
   };
 }

 function phenomeneSelectionne(record) {
   const p = normalize(record.phenomenon);

   const isOrage = p.includes("orage");
   const isVent = p.includes("vent") || p.includes("tempete");
   const isPluie = p.includes("pluie") || p.includes("inondation") || p.includes("crue");
   const isCyclone = p.includes("cyclone") || p.includes("ouragan") || p.includes("submersion") || p.includes("vague");

   return (
     (filtreOrage.checked && isOrage) ||
     (filtreVent.checked && isVent) ||
     (filtrePluie.checked && isPluie) ||
     (filtreCyclone.checked && isCyclone)
   );
 }

 function filtrerVigilances() {
   const echeance = echeanceInput.value;
   const min = Number(niveauMinInput.value);
   const depSearch = normaliserDep(depSearchInput.value);

   return vigilances.filter(function (r) {
     if (echeance !== "toutes" && r.echeance !== echeance) {
       return false;
     }

     if (Number(r.color_id) < min) {
       return false;
     }

     if (depSearch && r.dep !== depSearch) {
       return false;
     }

     return phenomeneSelectionne(r);
   });
 }

 function grouperParDepartement(records) {
   const mapDep = new Map();

   records.forEach(function (r) {
     if (!mapDep.has(r.dep)) {
       mapDep.set(r.dep, {
         dep: r.dep,
         niveauMax: Number(r.color_id),
         records: []
       });
     }

     const item = mapDep.get(r.dep);
     item.records.push(r);
     item.niveauMax = Math.max(item.niveauMax, Number(r.color_id));
   });

   return Array.from(mapDep.values()).sort(function (a, b) {
     if (b.niveauMax !== a.niveauMax) {
       return b.niveauMax - a.niveauMax;
     }

     return a.dep.localeCompare(b.dep);
   });
 }

 async function chargerVigilances() {
   statusBox.textContent = "Chargement des vigilances météo France...";
   listeBox.innerHTML = "";
   vigilanceLayer.clearLayers();

   const limit = 100;
   let offset = 0;
   let total = Infinity;
   let resultats = [];

   while (offset < total) {
     const params = new URLSearchParams();
     params.set("limit", String(limit));
     params.set("offset", String(offset));

     const url = API_VIGILANCE + "?" + params.toString();

     console.log("URL VIGILANCE FRANCE :", url);

     const response = await fetch(url, {
       cache: "no-store"
     });

     if (!response.ok) {
       throw new Error("Erreur API vigilance : " + response.status);
     }

     const data = await response.json();

     const lignes = Array.isArray(data.results)
       ? data.results
       : Array.isArray(data.records)
         ? data.records
         : [];

     resultats = resultats.concat(
       lignes.map(normaliserRecord).filter(Boolean)
     );

     total = Number(data.total_count || resultats.length);

     if (!lignes.length) {
       break;
     }

     offset += limit;

     if (offset > 3000) {
       break;
     }
   }

   vigilances = resultats;

   afficherVigilances();
 }

 function afficherVigilances() {
   const filtrees = filtrerVigilances();
   const groupes = grouperParDepartement(filtrees);

   vigilanceLayer.clearLayers();

   const bounds = L.latLngBounds();

   groupes.forEach(function (groupe) {
     const infoDep = DEPARTEMENTS[groupe.dep];

     if (!infoDep) {
       return;
     }

     const couleur = couleurNiveau(groupe.niveauMax);
     const nomDep = infoDep[0];
     const lat = infoDep[1];
     const lon = infoDep[2];

     const details = groupe.records.map(function (r) {
       return `
         ${badgeNiveau(r.color_id)}
         ${escapeHtml(r.echeance || "—")}
         —
         ${escapeHtml(r.phenomenon)}
         <br>
         <small>
           Du ${escapeHtml(formatDate(r.begin_time))}
           au ${escapeHtml(formatDate(r.end_time))}
         </small>
       `;
     }).join("<hr>");

     const popupHtml = `
       <strong>${escapeHtml(groupe.dep)} — ${escapeHtml(nomDep)}</strong><br>
       Niveau maximum : ${badgeNiveau(groupe.niveauMax)}
       <hr>
       ${details}
     `;

     L.marker([lat, lon], {
       icon: L.divIcon({
         className: "mf-marker",
         html: `
           <div class="mf-pin mf-${couleur}">
             <span>!</span>
           </div>
         `,
         iconSize: [30, 30],
         iconAnchor: [15, 15]
       })
     })
     .bindPopup(popupHtml)
     .addTo(vigilanceLayer);

     bounds.extend([lat, lon]);
   });

   if (bounds.isValid()) {
     map.fitBounds(bounds, {
       padding: [30, 30],
       maxZoom: 7
     });
   } else {
     map.setView([46.7, 2.5], 6);
   }

   afficherListe(groupes);
   afficherResume(groupes, filtrees);

   statusBox.textContent =
     groupes.length +
     " département(s) affiché(s) — " +
     filtrees.length +
     " vigilance(s) correspondant aux filtres.";

   refreshMapSize();
 }

 function afficherResume(groupes, records) {
   const nbRouge = groupes.filter(function (g) { return g.niveauMax >= 4; }).length;
   const nbOrange = groupes.filter(function (g) { return g.niveauMax === 3; }).length;
   const nbJaune = groupes.filter(function (g) { return g.niveauMax === 2; }).length;

   resumeBox.innerHTML = `
     <h3>Résumé</h3>
     <p>${badgeNiveau(4)} ${escapeHtml(nbRouge)} département(s)</p>
     <p>${badgeNiveau(3)} ${escapeHtml(nbOrange)} département(s)</p>
     <p>${badgeNiveau(2)} ${escapeHtml(nbJaune)} département(s)</p>
     <p><strong>Total phénomènes filtrés :</strong> ${escapeHtml(records.length)}</p>
     <p><small>Carte basée sur les vigilances départementales, pas sur des impacts foudre exacts.</small></p>
   `;
 }

 function afficherListe(groupes) {
   if (!groupes.length) {
     listeBox.innerHTML = `
       <article class="mf-ligne">
         <h4>Aucun département concerné</h4>
         <p>Aucune vigilance ne correspond aux filtres choisis.</p>
       </article>
     `;
     return;
   }

   listeBox.innerHTML = groupes.map(function (groupe) {
     const infoDep = DEPARTEMENTS[groupe.dep];
     const nomDep = infoDep ? infoDep[0] : "Département";

     const phenomenes = groupe.records.map(function (r) {
       return `
         <p>
           ${badgeNiveau(r.color_id)}
           <strong>${escapeHtml(r.phenomenon)}</strong>
           —
           ${escapeHtml(r.echeance || "—")}
           <br>
           <small>
             Du ${escapeHtml(formatDate(r.begin_time))}
             au ${escapeHtml(formatDate(r.end_time))}
           </small>
         </p>
       `;
     }).join("");

     return `
       <article class="mf-ligne">
         <h4>${escapeHtml(groupe.dep)} — ${escapeHtml(nomDep)}</h4>
         <p><strong>Niveau maximum :</strong> ${badgeNiveau(groupe.niveauMax)}</p>
         ${phenomenes}
       </article>
     `;
   }).join("");
 }

 async function chargerRadar() {
   radarStatus.textContent = "Chargement du radar pluie / orages...";

   const response = await fetch(API_RAINVIEWER, {
     cache: "no-store"
   });

   if (!response.ok) {
     throw new Error("Erreur RainViewer : " + response.status);
   }

   const data = await response.json();

   radarHost = data.host || "";
   radarFrames = data.radar && Array.isArray(data.radar.past)
     ? data.radar.past
     : [];

   if (!radarFrames.length) {
     throw new Error("Aucune image radar disponible.");
   }

   radarIndex = radarFrames.length - 1;
   afficherFrameRadar(radarIndex);
 }

 function afficherFrameRadar(index) {
   if (!radarFrames.length) {
     return;
   }

   if (index < 0) {
     index = radarFrames.length - 1;
   }

   if (index >= radarFrames.length) {
     index = 0;
   }

   radarIndex = index;

   const frame = radarFrames[radarIndex];
   const url = radarHost + frame.path + "/512/{z}/{x}/{y}/2/1_1.png";

   if (radarLayer) {
     map.removeLayer(radarLayer);
     radarLayer = null;
   }

   radarLayer = L.tileLayer(url, {
     tileSize: 512,
     zoomOffset: -1,
     opacity: 0.65,
     zIndex: 20,
     maxNativeZoom: 7,
     attribution: "RainViewer"
   }).addTo(map);

   const dateRadar = frame.time
     ? new Date(frame.time * 1000).toLocaleString("fr-FR", {
         dateStyle: "short",
         timeStyle: "short"
       })
     : "date inconnue";

   radarStatus.textContent =
     "Radar pluie / orages affiché — image : " + dateRadar;
 }

 function lancerAnimationRadar() {
   if (!radarFrames.length) {
     chargerRadar()
       .then(function () {
         lancerAnimationRadar();
       })
       .catch(function (error) {
         radarStatus.textContent = error.message;
         console.error(error);
       });

     return;
   }

   arreterAnimationRadar();

   radarTimer = setInterval(function () {
     afficherFrameRadar(radarIndex + 1);
   }, 750);

   radarStatus.textContent = "Animation radar en cours...";
 }

 function arreterAnimationRadar() {
   if (radarTimer) {
     clearInterval(radarTimer);
     radarTimer = null;
   }
 }

 function masquerRadar() {
   arreterAnimationRadar();

   if (radarLayer) {
     map.removeLayer(radarLayer);
     radarLayer = null;
   }

   radarStatus.textContent = "Radar masqué.";
 }

 btnReload.addEventListener("click", function () {
   chargerVigilances().catch(function (error) {
     statusBox.textContent = error.message;
     console.error(error);
   });
 });

 btnRadar.addEventListener("click", function () {
   if (radarLayer) {
     masquerRadar();
     return;
   }

   chargerRadar().catch(function (error) {
     radarStatus.textContent = error.message;
     console.error(error);
   });
 });

 btnRadarPlay.addEventListener("click", function () {
   lancerAnimationRadar();
 });

 btnRadarStop.addEventListener("click", function () {
   arreterAnimationRadar();
   radarStatus.textContent = "Animation radar arrêtée.";
 });

 btnFrance.addEventListener("click", function () {
   map.setView([46.7, 2.5], 6);
   refreshMapSize();
 });

 echeanceInput.addEventListener("change", afficherVigilances);
 niveauMinInput.addEventListener("change", afficherVigilances);
 depSearchInput.addEventListener("input", afficherVigilances);
 filtreOrage.addEventListener("change", afficherVigilances);
 filtreVent.addEventListener("change", afficherVigilances);
 filtrePluie.addEventListener("change", afficherVigilances);
 filtreCyclone.addEventListener("change", afficherVigilances);

 chargerVigilances().catch(function (error) {
   statusBox.textContent = error.message;
   console.error(error);
 });

 chargerRadar().catch(function (error) {
   radarStatus.textContent = "Radar non chargé : " + error.message;
   console.warn(error);
 });

 refreshMapSize();
})();
</script>