PageSourceSearch

https://www.ea2cw.eus/locator/js/main.js

js ea2cw.eus collected 2026-10-06 13:15:06 UTC 25,234 bytes, 755 lines download raw bytes

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 &copy; <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: '&copy; <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 &copy; <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 &copy; Esri &mdash; 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: &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, <a href="http://viewfinderpanoramas.org">SRTM</a> | Map style: &copy; <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\">&copy; Jawg</a> - <a href=\"https://www.openstreetmap.org\" target=\"_blank\">&copy; OpenStreetMap</a>&nbsp;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 &copy; <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 ? "-" : "&nbsp;";
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.