1/* radar.js â Radar meteorológico ANIMADO (RainViewer) como toggle sobre un mapa Leaflet. 2 window.CWRadar.attach(map, {es, pos}) agrega UN botón compacto "Radar": al prenderlo superpone la lluvia 3 (últimas ~2h) animándose y muestra la hora inline. Capa semi-transparente en pane propio (debajo de focos). 4 Datos: RainViewer (API pública gratis, sin key; tiles por su CDN). Aditivo + carga al click (cero peso hasta pedirlo). */ 5window.CWRadar = (function () { 6 var API = 'https://api.rainviewer.com/public/weather-maps.json'; 7 var COLOR = 4, OPTS = '1_1', SIZE = 512, TILE = 256, RADAR_OPACITY = 0.62; // color 4 = The Weather Channel; smooth 8 // â ï¸ RainViewer SOLO tiene tiles hasta z7: de z8 para arriba devuelve un PNG que dice "Zoom Level Not Supported" 9 // (verificado: mismo hash en Spokane/Miami/Madrid/Londres). En las fichas (zâ11) tapaba el mapa entero. 10 // maxNativeZoom => Leaflet pide el tile de z7 y lo escala. SIZE 512 en la URL dibujado a TILE 256 = doble 11 // resolución de origen, asà el escalado en zooms altos se ve la mitad de borroso (y nÃtido en pantallas retina). 12 var MAX_NATIVE = 7; 13 var RAIN = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242"/><path d="M16 14v6"/><path d="M8 14v6"/><path d="M12 16v6"/></svg>'; 14 var POSNAMES = { tr2: 1, tl2: 1, tl: 1, tr: 1, bl: 1, br: 1, tr3: 1, home: 1 }; // whitelist de posiciones (clases cwr-<pos>) 15 16 function injectCSS() { 17 if (document.getElementById('cwr-css')) return; 18 var s = document.createElement('style'); s.id = 'cwr-css'; 19 s.textContent = 20 ".cwr-btn{position:absolute;z-index:1000;display:inline-flex;align-items:center;gap:6px;background:rgba(20,20,20,.9);" + 21 "color:#f0f0f0;border:1px solid rgba(255,255,255,.15);border-radius:10px;padding:8px 12px;font:600 12px 'Inter',system-ui,sans-serif;" + 22 "cursor:pointer;box-shadow:0 4px 14px rgba(0,0,0,.4);white-space:nowrap;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px)}" + 23 ".cwr-btn:hover{border-color:rgba(255,255,255,.35)}" + 24 ".cwr-btn.active{background:#3aa0ff;color:#0c0d0e;border-color:#3aa0ff}" + 25 ".cwr-btn svg{width:15px;height:15px;flex-shrink:0}" + 26 ".cwr-btn .cwr-t{font-variant-numeric:tabular-nums;opacity:.9}" + 27 ".cwr-tr2{top:56px;right:14px}.cwr-tr{top:14px;right:14px}.cwr-tl{top:14px;left:14px}" + 28 ".cwr-tl2{top:56px;left:14px}.cwr-bl{bottom:20px;left:14px}.cwr-br{bottom:20px;right:14px}" + 29 ".cwr-tr3{top:66px;right:14px}" + // home: debajo de la hamburguesa (fijo, no se mueve en mobile) 30 // home responsive: desktop = centro-arriba debajo del nav (única zona libre: filtros van a la izq, stats a la 31 // der, ad+zoom abajo); mobile = debajo de la hamburguesa (der). No lo tapa el anuncio. 32 ".cwr-home{top:18px;left:50%;transform:translateX(-50%)}" + // desktop: centro-arriba, alineado con las cards laterales 33 "@media(max-width:640px){.cwr-home{top:100px;left:14px;right:auto;transform:none}}" + // mobile: arriba-izq, debajo de la card del header 34 35 // en mobile: el toggle de basemap ("Light map") se OCULTA â el radar sube a la esquina; y queda SOLO EL ÃCONO 36 // (sin texto ni hora), igual que el botón del loop, por consistencia. Activo = Ãcono azul. 37 "@media(max-width:640px){.cwr-tr2{top:14px}.cwr-tl2{top:14px}.cwr-btn span{display:none}.cwr-btn{padding:8px 10px}}"; 38 document.head.appendChild(s); 39 } 40 41 function attach(map, opts) { 42 opts = opts || {}; 43 if (!map || !window.L) return; 44 var es = !!opts.es; 45 var T = { radar: 'Radar', loading: es ? 'Cargandoâ¦' : 'Loadingâ¦', none: es ? 'Sin radar' : 'No radar', err: 'Error' }; 46 injectCSS(); 47 map.createPane('cwrRadar'); var pane = map.getPane('cwrRadar'); pane.style.zIndex = 250; pane.style.pointerEvents = 'none'; 48 49 var pos = POSNAMES[opts.pos] ? opts.pos : 'tr2'; 50 var btn = document.createElement('button'); btn.type = 'button'; btn.className = 'cwr-btn cwr-' + pos; 51 var lbl = document.createElement('span'); lbl.textContent = T.radar; 52 var tspan = document.createElement('span'); tspan.className = 'cwr-t'; 53 btn.innerHTML = RAIN; btn.appendChild(lbl); btn.appendChild(tspan); 54 map.getContainer().appendChild(btn);
55 L.DomEvent.disableClickPropagation(btn); L.DomEvent.disableScrollPropagation(btn); 56 57 var layers = [], idx = 0, timer = null, loaded = false, loading = false, on = false; 58 function fmtTime(ts) { var d = new Date(ts * 1000); return ('0' + d.getHours()).slice(-2) + ':' + ('0' + d.getMinutes()).slice(-2); } 59 function show(i) { if (layers[idx]) layers[idx].setOpacity(0); idx = i; layers[idx].setOpacity(RADAR_OPACITY); tspan.textContent = ' · ' + fmtTime(layers[idx]._ts); } 60 function startTimer() { stopTimer(); timer = setInterval(function () { if (on && layers.length) show((idx + 1) % layers.length); }, 480); } 61 function stopTimer() { if (timer) { clearInterval(timer); timer = null; } } 62 63 function activate() { 64 on = true; btn.classList.add('active'); 65 if (loaded) { show(idx); startTimer(); return; } 66 if (loading) return; 67 loading = true; tspan.textContent = ' · ' + T.loading; 68 fetch(API).then(function (r) { return r.json(); }).then(function (data) { 69 if (!on) { loading = false; return; } 70 var host = data.host, past = (data.radar && data.radar.past) || [], now = (data.radar && data.radar.nowcast) || []; 71 var frames = past.concat(now); 72 if (!host || !frames.length) { tspan.textContent = ' · ' + T.none; loading = false; return; } 73 frames.forEach(function (f) { 74 var url = host + f.path + '/' + SIZE + '/{z}/{x}/{y}/' + COLOR + '/' + OPTS + '.png'; 75 var ly = L.tileLayer(url, { opacity: 0, pane: 'cwrRadar', tileSize: TILE, crossOrigin: true, 76 maxNativeZoom: MAX_NATIVE, maxZoom: 22, // 22 = por encima del maxZoom de cualquiera de nuestros mapas 77 attribution: 'Radar © <a href="https://www.rainviewer.com/" target="_blank" rel="noopener">RainViewer</a>' }).addTo(map); 78 ly._ts = f.time; layers.push(ly); 79 }); 80 loaded = true; loading = false; show(0); startTimer(); 81 }).catch(function () { tspan.textContent = ' · ' + T.err; loading = false; }); 82 } 83 function deactivate() { 84 on = false; stopTimer(); btn.classList.remove('active'); tspan.textContent = ''; 85 layers.forEach(function (ly) { ly.setOpacity(0); }); 86 } 87 btn.onclick = function () { on ? deactivate() : activate(); }; 88 } 89 90 return { attach: attach }; 91})();
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.