1/* main script for QRA locator map 2* by TK5EP 2026/01 3* 4*/ 5 // declare variables 6 let currentRadarLayer = null; 7 let radarOpacity = 0.75; // Variable pour stocker la valeur d'opacité (persistance entre les rafraîchissements) 8 let radarTimeControl = null; 9 let radarInterval = null; // Variable pour stocker l'intervalle 10 let measure; // flag bouton pour le ruler RS 11 12 13 14 15// fonction pour limiter le nombre de calculs lors du déplacement de la souris 16// avec mapmove 17function throttle(func, limit) { 18 let inThrottle; 19 return function(event) { 20 if (!inThrottle) { 21 func(event); 22 inThrottle = true; 23 setTimeout(() => inThrottle = false, limit); 24 } 25 } 26} 27 28// Create Basic layer 29var basicMap = L.tileLayer( 30 // 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', { 31 'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png?key=cb1_2p42_1_5afff3a438a2f51487f8e7b9', { 32 attribution: '© OpenStreetMap © CARTO' 33 } 34); 35 36// create streets layer 37 var streets = L.tileLayer('https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token=pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycXBndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw', { 38 id: 'mapbox/streets-v11', 39 tileSize: 512, 40 zoomOffset: -1, 41 attribution: 'Map data © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, Imagery © <a href="https://www.mapbox.com/">Mapbox</a>' 42 }); 43 44// create openstreetmap layer 45 var osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { 46 attribution: 'Geocoding: <a href="https://nominatim.org">Nominatim</a> | Map data © <a href="https://openstreetmap.org">OpenStreetMap</a> contributors', 47 minZoom: 3, 48 maxZoom: 19, 49 }); 50 51// create cyclemap layer 52 var cycle = L.tileLayer('https://tile.thunderforest.com/cycle/{z}/{x}/{y}.png?apikey=1362d6dfa9bf498f8eeeb33eb4a21f6b', { 53 maxZoom: 19, 54 attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>' 55 }); 56 57/* 58// create satellite layer 59 var satellite = L.tileLayer('https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token=pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycXBndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw', { 60 id: 'mapbox/satellite-v9', 61 tileSize: 512, 62 maxZoom: 20, 63 zoomOffset: -1, 64 attribution: 'Map data © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, Imagery © <a href="https://www.mapbox.com/">Mapbox</a>' 65 }); 66*/ 67 68// create ERSI satellite view 69 var Esri_WorldImagery = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { 70 maxZoom: 19, 71 attribution: 'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community' 72 }); 73 74// create OpenTopMap layer 75 var OpenTopoMap = L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', { 76 maxZoom: 17, 77 attribution: 'Map data: © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, <a href="http://viewfinderpanoramas.org">SRTM</a> | Map style: © <a href="https://opentopomap.org">OpenTopoMap</a> (<a href="https://creativecommons.org/licenses/by-sa/3.0/">CC-BY-SA</a>)' 78 }); 79 80// create Jawglab terrain Map 81 var terrainMap = L.tileLayer('https://tile.jawg.io/jawg-terrain/{z}/{x}/{y}{r}.png?access-token=LcawXkSWAVk87KWu1WbNt0cfZNn1InfBAortu4RpTO79yi9qAWjz2z22obLJqbEK', { 82 maxZoom: 18, 83 attribution : '<a href=\"https://www.jawg.io\" target=\"_blank\">© Jawg</a> - <a href=\"https://www.openstreetmap.org\" target=\"_blank\">© OpenStreetMap</a> contributors)' 84 }); 85 86/* 87// Import rain layer 88 var imageUrl = 'https://lecluster.f5len.org/png/rain.png', 89 imageBounds = [[40.0, -7.6], [52.6, 11.6]]; 90 var rainLayer = L.imageOverlay(imageUrl, imageBounds, { 91 opacity:1, 92 zIndex: 100 93 }); 94*/ 95 96/* 97// add europe rain layer from OpenWeatherMap 98var rainLayer = L.tileLayer('https://tile.openweathermap.org/map/rain_new/{z}/{x}/{y}.png?appid=005ad84764448b1e2f94a80c063fcf6d',{ 99 opacity: 0.95, 100 attribution: 'Météo © <a href="https://openweathermap.org">OpenWeatherMap</a>', 101 maxZoom: 19 102}); 103*/ 104 105 106// create WGS grids 107var wgsgrid = L.latlngGraticule({ 108 showLabel: true, 109 color: '#ff0000', 110 zoomInterval: [ 111 {start: 2, end: 5, interval: 10}, // labels simplifiés pour zoom faible 112 {start: 6, end: 9, interval: 1}, // labels détaillés 113 {start: 10, end: 20, interval: 0.1} // labels très détaillés 114 ], 115 style: function(latlng) { 116 // On peut réduire la complexité : plus fin à zoom élevé seulement 117 return { 118 color: '#ff0000', 119 weight: 0.5, 120 opacity: 0.7 121 }; 122 }, 123 labelStyle: function(zoom, latlng) { 124 // Labels uniquement aux zoom >= 6 125 if (zoom < 6) return {display: 'none'}; 126 return { 127 color: '#ff0000', 128 fontSize: '10px', 129 fontWeight: 'bold' 130 }; 131 } 132}); 133 134 135// --- Récupération des préférences --- 136// Gestion du stockage local (remplace les cookies) 137const Storage = { 138 set: (key, value) => localStorage.setItem(key, value), 139 get: (key) => localStorage.getItem(key), 140 // Pour les nombres, on force la conversion, si 0 alors c'est le chiffre 0 et pas false 141 //getInt: (key, defaultValue) => parseInt(localStorage.getItem(key)) || defaultValue 142 getInt: (key, defaultValue) => { 143 const value = localStorage.getItem(key); 144 return value === null ? defaultValue : Number(value); 145 } 146}; 147 148// home QRA 149let homeQRA = Storage.getInt("homeqra"); 150if (homeQRA) { 151 const coords = homeQRA.split("/"); 152 latQRA = parseFloat(coords[0]); 153 lngQRA = parseFloat(coords[1]); 154 } 155else { 156 console.log("No home QRA set"); 157 Storage.set("homeQRA", '42/9'); 158} 159 160// Ãtat de la couche pluie (0 ou 1) 161let rainLayerCookie = Storage.getInt("rainCookie", 0); 162 163 164 165// Niveau de zoom 166let zoomLevel = Storage.getInt("zoomLevel", 3); 167 168// Centre de la carte 169let centerlat = 60; 170let centerlng = 15; 171const savedCenter = Storage.get("center"); 172if (savedCenter) { 173 const coords = savedCenter.split("/"); 174 centerlat = parseFloat(coords[0]); 175 centerlng = parseFloat(coords[1]); 176} 177 178 179// create map and load layers 180var map = L.map('map',{ 181 minZoom :3, 182/* layers: [osm] */ 183 layers: [basicMap] 184}); 185
186//map.setView([60,15],3); 187map.setView([centerlat,centerlng], zoomLevel); 188 189// add scale bottom left 190L.control.scale().addTo(map); 191 192// Création d'un contrôle générique pour du texte en superposition 193const OverlayTextControl = L.Control.extend({ 194 options: { 195 position: 'bottomright', // Position par défaut 196 content: 'Texte par défaut' 197 }, 198 onAdd: function (map) { 199 const div = L.DomUtil.create('div', 'map-overlay-text'); 200 div.innerHTML = this.options.content; 201 return div; 202 } 203}); 204 205// Ajout du texte en bas à droite 206 207const myCopyright = new OverlayTextControl({ 208 position: 'bottomright', 209 content: 'by TK5EP ' 210}); 211// myCopyright.addTo(map); 212 213const locatorClick = new OverlayTextControl({ 214 position: 'bottomright', 215 content: '' 216}); 217locatorClick.addTo(map); 218 219/* 220// si besoin de rajouter un texte en overlay 221const myMention = new OverlayTextControl({ 222 position: 'topleft', 223 content: 'Données en temps réel' 224}); 225myMention.addTo(map); 226*/ 227 228// create maidenhead overlay and load it by default on map 229var maidenhead = L.maidenhead({color : 'rgba(255, 0, 0, 0.4)'}).addTo(map); 230 231 232// Création du contrôle (position bas-gauche) 233const RadarTimeLabel = L.Control.extend({ 234 options: { position: 'bottomleft' }, 235 onAdd: function (map) { 236 this._div = L.DomUtil.create('div', 'radar-time-display'); 237 return this._div; 238 }, 239 update: function (content) { 240 if (content) { 241 this._div.innerHTML = content; 242 this._div.style.display = 'block'; 243 } else { 244 this._div.style.display = 'none'; 245 } 246 } 247}); 248// Initialisation immédiate sur la carte 249radarTimeControl = new RadarTimeLabel(); 250radarTimeControl.addTo(map); 251 252 253 254// --- CRÃATION DU CONTRÃLE D'OPACITà --- 255const OpacitySliderControl = L.Control.extend({ 256 options: { position: 'topright' }, 257 258 onAdd: function (map) { 259 const container = L.DomUtil.create('div', 'radar-opacity-control'); 260 261 container.innerHTML = ` 262 <label>Opacity : <span id="opacity-val">${Math.round(radarOpacity * 100)}%</span></label> 263 <input type="range" id="opacity-slider" min="0" max="1" step="0.05" value="${radarOpacity}"> 264 265 <div class="radar-legend"> 266 <div class="legend-bar"></div> 267 <div class="legend-labels"> 268 <span>Low</span> 269 <span>Heavy</span> 270 </div> 271 </div> 272 `; 273 L.DomEvent.disableClickPropagation(container); 274 L.DomEvent.disableScrollPropagation(container); 275 return container; 276 } 277}); 278 279// Ajout du contrôle à la carte 280const opacityControl = new OpacitySliderControl(); 281opacityControl.addTo(map); 282 283// --- GESTIONNAIRE D'ÃVÃNEMENTS POUR LE SLIDER --- 284document.addEventListener('input', function (e) { 285 if (e.target && e.target.id === 'opacity-slider') { 286 radarOpacity = parseFloat(e.target.value); 287 288 // Mise à jour du texte 289 document.getElementById('opacity-val').innerText = Math.round(radarOpacity * 100) + '%'; 290 291 // Application sur la couche si elle est active 292 if (currentRadarLayer) { 293 currentRadarLayer.setOpacity(radarOpacity); 294 } 295 } 296}); 297 298 299// create terminator layer and update timer 300var terminator = L.terminator(); 301setInterval(function() { 302 terminator.setTime(); 303 }, 60000); // Every minute 304 305 306 307// =========================================== 308// ajout des boutons easyButton 309// =========================================== 310 311// Bouton pour RAZ carte 312var resetButton = L.easyButton({ 313 states: [{ 314 stateName: 'reset', 315 icon: 'fas fa-sync', 316 title: 'Reset position', 317 onClick: function(btn, map){ 318 document.getElementById('find_locator').value = ""; 319 document.getElementById('find_address').value = "";
320 map.setView([60,15], 3); 321 } 322 }] 323}); 324resetButton.addTo(map); 325 326 327const rulerButton = L.easyButton({ 328 states: [{ 329 stateName: 'off', 330 icon: 'fa-ruler', 331 title: 'RS ruler ON', 332 onClick: btn => { 333 measure.toggle(); 334 btn.state('on'); 335 } 336 },{ 337 stateName: 'on', 338 icon: 'fa-times-circle icon-rouge', 339 title: 'RS ruler OFF', 340 onClick: btn => { 341 measure.toggle(); 342 btn.state('off'); 343 } 344 }] 345}).addTo(map); 346 347measure = new RulerDist(map, rulerButton); 348 349 350// add button to toggle terminator 351var toggle_terminator = L.easyButton({ 352 states: [{ 353 stateName: 'add-terminator', 354 title:'Add terminator', 355 icon:'fa-moon', 356 onClick: function(control) { 357 terminator.addTo(map); 358 control.state('remove-terminator'); 359 } 360 }, 361 { 362 icon:'fa-sun', 363 stateName: 'remove-terminator', 364 title:'Remove terminator', 365 onClick: function(control) { 366 terminator.remove(map); 367 control.state('add-terminator'); 368 }, 369 }] 370}); 371toggle_terminator.addTo(map); 372 373 374// add button to toggle maidenhead grid 375var toggle_mh = L.easyButton({ 376 states: [{ 377 stateName: 'remove-mh', 378 title:'Remove MH grid', 379 //icon: 'fas fa-border-all', 380 icon: 'fas fa-border-none', 381 onClick: function(control) { 382 maidenhead.remove(map); 383 control.state('add-mh'); 384 } 385 }, { 386 //icon: 'fas fa-border-none', 387 icon: 'fas fa-border-all', 388 stateName: 'add-mh', 389 title:'Add MH grid', 390 onClick: function(control) { 391 maidenhead.addTo(map); 392 control.state('remove-mh'); 393 }, 394 }] 395}); 396toggle_mh.addTo(map); 397 398 399// EasyButton toggle grille WGS 400L.easyButton({ 401 states: [ 402 { 403 stateName: 'off', 404 icon: 'fas fa-globe', 405 title: 'Add WGS grid', 406 onClick: function(btn, map) { 407 wgsgrid.addTo(map); // affiche la grille WGS 408 btn.state('on'); // passe l'état à "on" 409 } 410 }, 411 { 412 stateName: 'on', 413 icon: 'fas fa-globe', 414 title: 'Remove WGS grid', 415 onClick: function(btn, map) { 416 map.removeLayer(wgsgrid); // masque la grille WGS 417 btn.state('off'); // repasse à l'état "off" 418 } 419 } 420 ] 421}).addTo(map); 422 423 424// EasyButton toggle rain layer 425L.easyButton( 426 'fa-solid fa-cloud-showers-heavy', // icône FontAwesome 427 function(btn, map) { 428 toggleRain(); 429 }, 430 'Add/mask rain layer' // titre (tooltip) au survol 431).addTo(map); 432 433 434 435// EasyButton ruler RS 436L.easyButton({ 437 states: [{ 438 stateName: 'off', 439 icon: 'fa-ruler-combined', 440 title: 'RS ruler ON', 441 onClick: btn => { 442 isToolActive = true; 443 btn.state('on'); 444 map.getContainer().style.cursor = 'crosshair'; 445 } 446 },{ 447 stateName: 'on', 448 icon: 'fa-times-circle icon-rouge', 449 title: 'RS ruler OFF', 450 onClick: btn => { 451 isToolActive = false; 452 btn.state('off'); 453 map.getContainer().style.cursor = ''; 454 resetTool(); 455 } 456 }] 457}).addTo(map); 458 459 460 461// create basemaps list 462var baseMaps = { 463 "Basic" : basicMap, 464 "OpenStreetMap": osm, 465 "OpenTopoMap": OpenTopoMap, 466 // "Opencyclemap": cycle, 467 "Terrain map": terrainMap, 468 "Satellite": Esri_WorldImagery, 469 //"MapBox satellite view" : satellite 470 //'Streets': streets, 471}; 472 473// add layercontrol load layers and overlays 474var layerControl = L.control.layers(baseMaps).addTo(map); 475 476 477// Application de la fonction mousemove avec throttle sur la carte 478map.on('mousemove', throttle(onMapMove, 50)); 479 480// on zoom change, store new cookie "zoomLevel" value 481map.on('zoomend', function() { 482 Storage.set("zoomLevel", map.getZoom()); 483 }); 484 485 486// when map moves, update the "center" cookie 487map.on('moveend',function() { 488const c = map.getCenter(); 489 Storage.set("center", `${c.lat}/${c.lng}`); 490}); 491 492// Lance la couche pluie au démarrage si le cookie est à 1 493if (Storage.getInt("rainCookie", 0) === 1) { 494 updateRadarLayer(true).then(() => startAutoRefresh()); 495} 496 497/* 498// Puis toutes les 10 minutes (600000 ms) 499if (rainLayerCookie) { 500 loadRainViewerFixedLayer(); 501 console.log("Rain layer refresh set"); 502 setInterval(loadRainViewerFixedLayer, 600000); 503} 504*/ 505 506 507// fonction qui détecte un mouvement de la souris et agit en conséquence 508 function onMapMove(event) { 509 var LatLng = event.latlng; 510 var lat = LatLng.lat; 511 var lng = LatLng.lng; 512 if (lng<-180) {lng = lng+360;} 513 if (lng>180) {lng = lng-360;} 514 515 /** 516 * Formate une coordonnée avec des zéros non significatifs 517 * @param {number} valeur - La latitude ou longitude 518 * @param {number} padding - 2 pour la latitude, 3 pour la longitude 519 * @returns {string} Coordonnée formatée (ex: "-002.12345") 520 */ 521 function formatWGS84(valeur, padding) { 522 // 1. On travaille sur la valeur absolue pour le padding 523 let absVal = Math.abs(valeur).toFixed(5); 524 let [entiere, decimale] = absVal.split('.'); 525 526 // 2. On gère le signe séparément 527 //let signe = valeur < 0 ? "-" : ""; 528 let signe = valeur < 0 ? "-" : " "; 529 530 // 3. On assemble le tout 531 return signe + entiere.padStart(padding, '0') + '.' + decimale; 532 } 533 // affichage dans les champs lat/lng 534 document.getElementById("WGS84_lat").innerHTML = formatWGS84(lat, 2); 535 document.getElementById("WGS84_lng").innerHTML = formatWGS84(lng, 3); 536
537 // displays Lat/Lon in DMS 538 document.getElementById("WGS84DMS_lat").innerHTML = dec2DMS(lat,1); 539 document.getElementById("WGS84DMS_lng").innerHTML = dec2DMS(lng); 540 541 // displays Lat/Lon in DMS 542 document.getElementById("WGS84DDMM_lat").innerHTML = dec2DDMMMM(lat,1); 543 document.getElementById("WGS84DDMM_lng").innerHTML = dec2DDMMMM(lng); 544 545 // displays Lat/Lon in APRS 546 document.getElementById("APRS_lat").innerHTML = dec2APRS(lat,1); 547 document.getElementById("APRS_lng").innerHTML = dec2APRS(lng); 548 549 // calculate and display WWloc. Only 6 of 8 possible chars of the locator are displayed 550 //var locator = latlng2loc(lat,lng); 551 var locator = getLocator(lat,lng); 552 //alert(locator); 553 //document.getElementById("locator").innerHTML = locator.substring(0,8); 554 document.getElementById("locator").innerHTML = locator; 555 556 var UTM = latlng2UTM(lat,lng); 557 document.getElementById("UTM_Z").innerHTML=UTM[2]; 558 document.getElementById("UTM_E").innerHTML=Math.floor(UTM[0]) + " m E"; 559 document.getElementById("UTM_N").innerHTML=Math.floor(UTM[1]) + " m N"; 560 561 var Lambert = LatLng2Lambert(lat,lng); 562 document.getElementById("lambert_lat").innerHTML = Math.round(Lambert[0])+" m"; 563 document.getElementById("lambert_lng").innerHTML = Math.round(Lambert[1])+" m"; 564 565 var Lambert93 = wgs84ToLambert93(lat,lng); 566 document.getElementById("lambert93_lat").innerHTML = Math.round(Lambert93[0])+" m"; 567 document.getElementById("lambert93_lng").innerHTML = Math.round(Lambert93[1])+" m"; 568 569 570 // calcul carroyage DFCI 571 var DFCI = Lambert2DFCI(Lambert[0],Lambert[1]); 572 document.getElementById("DFCI").innerHTML = DFCI; 573 574 /* 575 // display tests 576 var test = "Zoom = " + map.getZoom(); 577 document.getElementById("test").innerHTML = test; 578 //alert (test); 579 */ 580 581 } 582 583 584 585// Détection précise pour afficher/cacher le slider 586map.on('layeradd', function(event) { 587 if (event.layer.options && event.layer.options.isRain) { 588 const el = document.querySelector('.radar-opacity-control'); 589 if (el) el.classList.add('active'); 590 } 591}); 592 593map.on('layerremove', function(event) { 594 if (event.layer.options && event.layer.options.isRain) { 595 const el = document.querySelector('.radar-opacity-control'); 596 if (el) el.classList.remove('active'); 597 } 598}); 599 600// Détection précise pour afficher/cacher le slider 601map.on('layeradd', function(event) { 602 if (event.layer.options && event.layer.options.isRain) { 603 const el = document.querySelector('.radar-opacity-control'); 604 if (el) el.classList.add('active'); 605 } 606}); 607 608map.on('layerremove', function(event) { 609 if (event.layer.options && event.layer.options.isRain) { 610 const el = document.querySelector('.radar-opacity-control'); 611 if (el) el.classList.remove('active'); 612 } 613}); 614 615///////////////////////////////////////////// 616function onMapClick(event) { 617 var LatLng = event.latlng; 618 var lat = LatLng.lat; 619 var lng = LatLng.lng; 620// Pone el valor del locator en la ventana de locator 621 var locator = getLocator(lat,lng, 6); 622 locator = locator.substring(0,10); 623 var locator1 =locator.substring(0,4); 624 var locator2 = locator.substring(4,6).toLowerCase(); 625 var locator3 = locator.substring(6,8); 626 var locator4 = locator.substring(8,10).toLowerCase();; 627 var locFormatted = locator1+locator2+locator3+locator4; 628 document.getElementById('find_locator').value = locFormatted; 629} 630map.on('click', onMapClick); 631///////////////////////////////////////////// 632// --- LOGIQUE RADAR SANS PROXY (CORRECTION ANTI-403) --- 633 634// 1. Récupération directe des données depuis le navigateur du client 635async function fetchRadarData() { 636 console.log("[Radar] ð°ï¸ Tentative de récupération directe chez RainViewer..."); 637 try { 638 // APPEL DIRECT : On n'utilise plus le fichier PHP d'OVH qui générait l'erreur 403
639 const res = await fetch('https://api.rainviewer.com/public/weather-maps.json'); 640 641 if (!res.ok) throw new Error(`Erreur HTTP RainViewer: ${res.status}`); 642 643 const data = await res.json(); 644 console.log("[Radar] â Données JSON reçues avec succès."); 645 646 const host = data.host || 'https://tilecache.rainviewer.com'; 647 648 // Tri de sécurité et récupération du dernier radar disponible (Past) 649 const sortedPast = data.radar.past.sort((a, b) => a.time - b.time); 650 const latest = sortedPast.at(-1); 651 652 if (!latest) { 653 console.warn("[Radar] â ï¸ Aucune image trouvée dans 'radar.past'."); 654 throw new Error("Pas de données radar"); 655 } 656 657 // Formatage de l'heure pour l'affichage (Format 24h respecté) 658 const timestamp = new Date(latest.time * 1000).toLocaleString('fr-FR', { 659 day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false 660 }); 661 662 // NOUVELLE STRUCTURE URL : Utilise l'ID de chemin (latest.path) et la taille 256 663 // NOTE : Les options de couleurs gratuites ayant été restreintes, on utilise la version universelle "1_0.png" 664 const tileUrl = `${host}${latest.path}/256/{z}/{x}/{y}/1/1_0.png`; 665 666 console.log(`[Radar] Dernière image : ${timestamp}`); 667 console.log(`[Radar] URL des tuiles générée : ${tileUrl}`); 668 669 return { url: tileUrl, timeStr: timestamp }; 670 } catch (err) { 671 console.error("[Radar] â Erreur lors du fetch direct :", err.message); 672 throw err; 673 } 674} 675 676// 2. Mise à jour de la couche sur la carte Leaflet 677async function updateRadarLayer(isInitial = false) { 678 try { 679 // Indicateur visuel de mise à jour (grise le point pendant le chargement) 680 if (radarTimeControl._div.innerHTML) { 681 radarTimeControl.update(radarTimeControl._div.innerHTML.replace('radar-pulse', '').replace('#2ecc71', '#555')); 682 } 683 684 const data = await fetchRadarData(); 685 686 if (isInitial || !currentRadarLayer) { 687 console.log("[Radar] â¹ï¸ Création d'une nouvelle couche TileLayer."); 688 currentRadarLayer = L.tileLayer(data.url, { 689 opacity: radarOpacity, 690 isRain: true, 691 zIndex: 100, 692 maxNativeZoom: 7, // CRUCIAL : L'API gratuite bloque le rendu au-delà du zoom 7 693 attribution: 'Radar © RainViewer' 694 }).addTo(map); 695 } else { 696 if (currentRadarLayer._url !== data.url) { 697 console.log("[Radar] ð Mise à jour de l'URL (.setUrl)"); 698 currentRadarLayer.setUrl(data.url); 699 } else { 700 console.log("[Radar] â¹ï¸ L'image est déjà à jour, pas de changement d'URL."); 701 } 702 } 703 704 const suffix = isInitial ? "" : " (Auto)"; 705 radarTimeControl.update(`Radar : ${data.timeStr} <span class="radar-pulse">â</span>${suffix}`); 706 console.log(`[Radar] â Affichage mis à jour : ${data.timeStr}`); 707 708 } catch (err) { 709 radarTimeControl.update("Radar : Indisponible â ï¸"); 710 // Tentative de reconnexion automatique dans 5 min si échec initial 711 if (isInitial) { 712 console.log("[Radar] Nouvelle tentative dans 5 mins..."); 713 setTimeout(() => updateRadarLayer(true), 300000); 714 } 715 } 716} 717 718// 3. Fonction de bascule (Toggle) 719function toggleRain() { 720 if (currentRadarLayer) { 721 console.log("[Radar] â¹ï¸ Désactivation du radar par l'utilisateur."); 722 map.removeLayer(currentRadarLayer); 723 currentRadarLayer = null; 724 725 if (radarInterval) { 726 clearInterval(radarInterval); 727 radarInterval = null; 728 console.log("[Radar] Intervalle de rafraîchissement stoppé."); 729 } 730 731 radarTimeControl.update(null); 732 Storage.set("rainCookie", 0); 733 } else { 734 console.log("[Radar] â¶ï¸ Activation du radar par l'utilisateur."); 735 updateRadarLayer(true); 736 startAutoRefresh();
737 Storage.set("rainCookie", 1); 738 } 739} 740 741// 4. Gestion de l'intervalle de mise à jour (5 minutes) 742function startAutoRefresh() { 743 if (radarInterval) { 744 console.log("[Radar] RAZ de l'intervalle"); 745 clearInterval(radarInterval); 746 } 747 748 console.log("[Radar] Démarrage de l'auto-refresh (5 minutes)."); 749 radarInterval = setInterval(() => { 750 if (currentRadarLayer) { 751 console.log("[Radar] ð Lancement du rafraîchissement automatique..."); 752 updateRadarLayer(false); 753 } 754 }, 300000); 755}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.