
Chismes del Lince 04: Cazador de sol
Categorías: AIDARAC – Ciencia Ciudadana · Los Chismes del Lince · Proyecto AIDARAC
Superman se carga de poderes con el Sol. Los vampiros de las películas se achicharran con él. Y el Lince de la Noche, que trabaja cuando todos duermen, tiene un problema serio: de día también se cruza con el Sol, y el Sol de Estepona no perdona.
Así que hoy el lince se prepara para el día. No para quitarse del medio, sino para saber dónde está el Sol, cuánta radiación ultravioleta (UV) nos manda y cómo protegernos sin quedarnos encerrados. Y como siempre en esta serie, con el móvil que llevas en el bolsillo.
Por qué este chisme
Los Chismes del Lince son las herramientas que lleva el lince en el zurrón para hacer sus misiones. Este chisme conecta con la Misión 11 (radiación UV y protección solar), pero aquí no hace falta montar ningún circuito. La página te dice dos cosas:
- Dónde está el Sol ahora mismo respecto a ti, para que lo busques (sin mirarlo).
- Cuánta radiación UV hay: la de los últimos días, la que tocaría hoy según un cálculo y la que se espera para los próximos días.
Y hay algo más importante que el aparato: entender por qué el UV cambia tanto entre enero y julio, o entre las nueve de la mañana y las tres de la tarde. Eso es lo que te protege de verdad.
Chisme 04: Cazador de sol
La página tiene tres partes:
- Dónde está el Sol (brújula). Un dial con el Norte marcado y el Sol como un punto naranja. Al pulsar Activar brújula, el dial gira contigo y un punto verde marca hacia dónde apunta el móvil. Te dice el rumbo del Sol (“al suroeste, 231°”) y su altura sobre el horizonte. Si lo tienes justo delante, te avisa de que no lo mires directamente.
- UV estimado ahora (cálculo). Un número grande, con su color y un consejo corto. Sale de una fórmula que puedes ver en pantalla, con los números sustituidos.
- UV: últimos días, hoy y próximos días. Un gráfico por horas con varias líneas: los días anteriores (datos), hoy (nuestro cálculo), los próximos días (previsión) y, si tienes el Worker montado, el UV medido por AEMET en Málaga. Debajo hay una tabla con el máximo de cada día.
Pruébalo en tu móvil: montesteam.org/app/chisme04sol.html
Tu ubicación se usa solo para calcular la posición del Sol y pedir el dato de UV, con las coordenadas redondeadas a unos 1 km. No se guarda nada tuyo.
Cómo se ve el Sol desde Estepona
Estepona está a unos 36,4° de latitud norte. Eso decide lo que ves cada día:
| Época | Altura del Sol al mediodía solar | Amanece por | Día dura | UV con la fórmula (cielo despejado) |
|---|---|---|---|---|
| Junio (solsticio de verano) | unos 77° | nordeste (unos 60°) | unas 14,5 h | unos 12 |
| Marzo y septiembre (equinoccios) | unos 54° | este (90°) | 12 h | unos 7 u 8 |
| Diciembre (solsticio de invierno) | unos 30° | sureste (unos 120°) | unas 9,5 h | unos 2 o 3 |
Algunas cosas que se ven en la tabla:
- El Sol nunca llega al cénit. Para eso habría que estar entre los trópicos, y Estepona está más al norte. Lo más alto que llega son unos 77° en junio.
- Al mediodía solar siempre está justo al sur. Pero el mediodía solar no coincide con las 12:00 del reloj: en Estepona cae hacia las 14:20 en verano y hacia las 13:20 en invierno, con unos minutos de diferencia según el día. Si oyes “evita el sol entre las 12 y las 16”, ahora sabes por qué es así: el momento peor va desplazado por la hora oficial.
- La sombra lo cuenta. Con 1,70 m de altura, tu sombra al mediodía mide unos 40 cm en junio y casi 3 metros en diciembre. Regla práctica: si tu sombra es más corta que tú, el Sol está alto y el UV suele ser alto. No es un dato exacto, pero sirve.
Qué es el índice UV
El Sol emite en muchas longitudes de onda, y la radiación ultravioleta se divide en tres zonas según su energía: los UVC, los más energéticos, casi no llegan al suelo porque la atmósfera los frena; los UVB, que son los que más queman; y los UVA, que penetran más hondo en la piel.
El índice UV (UVI) mide la intensidad de esa radiación que llega al suelo, ponderada según lo dañina que es para las personas. Es el estándar internacional que elaboraron la OMS, el Programa de las Naciones Unidas para el Medio Ambiente y la Organización Meteorológica Mundial. Empieza en 0 y no tiene tope. El valor que suelen dar los pronósticos es el máximo del día, que llega en las horas centrales, alrededor del mediodía solar. Se agrupa así:
- Bajo (0 a 2): no necesitas protección especial.
- Moderado (3 a 5): gorra y crema, y sombra al mediodía.
- Alto (6 a 7): crema, gorra, gafas y sombra.
- Muy alto (8 a 10): evita el sol en las horas centrales.
- Extremo (11 o más): mejor a la sombra y con protección máxima.
Por qué cambia tanto. Cuando el Sol está bajo, sus rayos atraviesan más atmósfera y llegan más debilitados. Cuando está alto, el recorrido es corto y llega mucha más radiación. Por eso el UV depende sobre todo de la altura del Sol.
La fórmula del lince
Para hoy, la app calcula el UV con esta fórmula sencilla, que vale para cielo despejado:
UV ≈ 12,5 · sen(altura del Sol)^2,42
Si el Sol está a 54° (septiembre, mediodía): sen 54° = 0,81, y 0,81 elevado a 2,42 da 0,60. Multiplicado por 12,5 sale 7,5. Con el Sol a 30°: 12,5 · 0,5^2,42 = 2,3.
Es una aproximación. Sobreestima un poco, porque no cuenta las nubes, el ozono, el polvo del Sáhara ni la contaminación. En la práctica, los máximos de verano en Málaga suelen quedarse en 9 o 10, no en 12. Por eso el gráfico junta el cálculo con datos medidos y previstos: para que compares lo que pasa con lo que “debería” pasar.
Cómo protegerse de los rayos UV
La OMS recomienda protegerse del sol cuando el índice UV es igual o superior a 3. Por debajo de ese valor, en general no hace falta protección especial. Estas son sus medidas básicas frente a la radiación ultravioleta:
- Limita el tiempo al sol del mediodía. Ojo con el reloj: el mediodía solar en Estepona cae hacia las 14:20 en verano, así que las horas peores van desplazadas respecto a las 12:00.
- Busca la sombra. Pero ni los árboles, ni las sombrillas ni los toldos protegen del todo.
- Ponte ropa que proteja. Mejor holgada y de tejido tupido, y que cubra la mayor parte del cuerpo posible.
- Lleva un sombrero de ala ancha, que protege los ojos, la cara, las orejas y la parte de atrás del cuello.
- Usa gafas de sol envolventes, o con paneles laterales, que den una protección del 99 % al 100 % frente a los rayos UVA y UVB.
- Aplica crema solar de amplio espectro, con factor 30 o más, en la piel que no cubra la ropa. Extiéndela generosamente y repítela cada dos horas, y después de nadar, sudar o hacer ejercicio al aire libre.
Según la OMS, corren más riesgo las personas con muchos lunares, las que toman medicamentos que sensibilizan la piel al sol y las que tienen antecedentes familiares de cáncer de piel. Si es tu caso, habla con el personal sanitario. Estas son recomendaciones generales y no sustituyen a una consulta.
Y algunas cosas más que conviene saber:
- Mira el número antes de salir. Desde 3 empieza a hacer falta protección. Con un índice muy alto (8 a 10) o extremo (11 o más), lo mejor es evitar el sol en las horas centrales.
- Los niños necesitan cuidado especial. La OMS insiste en protegerlos. A los bebés no se les pone al sol.
- El agua, la arena y la altura empujan el UV hacia arriba. Reflejan parte de la radiación, y en la sierra hay menos atmósfera por encima que en la playa, así que en Sierra Bermeja, con más razón.
- Las nubes no te salvan del todo. Con el cielo tapado llega menos UV, pero no cero, y muchas quemaduras ocurren en días nublados.
- La crema no es un permiso para quedarte más tiempo al sol. Reduce el daño, pero no lo elimina.
- Un bronceado no es una protección. Es la piel avisando de que ha recibido daño.
- No hace falta esconderse. Unos minutos al aire libre ayudan a fabricar vitamina D, y en Estepona, con tanto sol, no suele ser un problema. Lo importante es la moderación.
Fuentes de la OMS
- OMS: nota descriptiva Radiación ultravioleta (who.int/es/news-room/fact-sheets/detail/ultraviolet-radiation)
- OMS, OMM, PNUMA e ICNIRP: Índice UV solar mundial. Guía práctica (versión en español publicada por AEMET: aemet.es/documentos/es/eltiempo/observacion/radiacion/El_Indice_UV_OMS_v1.pdf)
- OPS/OMS: Recomendaciones de la OMS para protegerse de los rayos solares (paho.org/es/noticias/29-12-2010-recomendaciones-oms-para-protegerse-rayos-solares)
Mini-retos para clase
- La sombra del Lince: mide tu sombra a distintas horas y compárala con la altura del Sol que dice la app.
- Fórmula frente a AEMET: compara el máximo calculado con el medido en Málaga durante una semana. ¿Cuándo se parecen? ¿Cuándo no? ¿Qué días hay nubes o polvo?
- Caza el Sol: con la brújula activada, localiza el Sol sin mirarlo y comprueba que coincide con el rumbo que dice la página.
- Mediodía solar: anota a qué hora del reloj el Sol está al sur y comprueba que no son las 12:00.
Cómo se monta todo, paso a paso
(Aquí va la parte técnica. Es más larga que la de la app, pero puedes parar en el Paso 1 y ya tienes una versión que funciona.)
Hay dos formas de tenerlo:
- Versión sencilla (Paso 1). Sube la página a tu web y funciona ya: brújula, UV calculado, y datos de UV recientes y previstos del modelo europeo CAMS a través de Open-Meteo.
- Versión con datos reales (Pasos 2 a 9). Añade un Worker de Cloudflare que guarda cada día el UV medido y previsto por AEMET en Málaga, para compararlos con nuestra fórmula. Es la misma tecnología del Chisme 03.
Paso 1: sube la página
Copia el archivo completo, llámalo chisme04sol.html y súbelo a tu carpeta app, como en los chismes anteriores. Si lo incrustas dentro de otra página con un iframe, añade allow="accelerometer; magnetometer; gyroscope; geolocation", o la brújula y la ubicación no funcionarán.
[AQUÍ VA EL CÓDIGO: chisme04sol.html]
<!DOCTYPE html>
<html lang="es"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Cazador de sol · Misiones del Lince</title>
<style>
:root{--bg:#f3f1e4;--card:#fffdf5;--ink:#26301f;--mut:#65705c;--line:#dcdbc6;--g:#2f6b3f;--e:#9a6b3c;--b:#1f7aa8;--sun:#e19a00;--sunbg:#f8edc9}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--ink);font:16px/1.5 system-ui,-apple-system,'Segoe UI',sans-serif}
.w{max-width:720px;margin:0 auto;padding:16px 14px 40px}
h1{font:700 1.6rem/1.15 Georgia,serif;margin:.2em 0}
h2{font:700 1.1rem Georgia,serif;margin:0 0 6px}
.pill{font-size:13px;font-weight:600;background:var(--sunbg);color:#7a5200;padding:2px 10px;border-radius:99px}
section{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:14px;margin-top:14px}
.mut{color:var(--mut);font-size:14px}
button{font:600 15px system-ui,sans-serif;min-height:40px;padding:0 14px;border-radius:10px;border:1px solid var(--line);background:var(--card);color:var(--ink);cursor:pointer}
button:hover{background:var(--sunbg)}
:focus-visible{outline:3px solid var(--b);outline-offset:2px}
.row{display:flex;flex-wrap:wrap;gap:8px;margin:8px 0}
.dial{max-width:300px;margin:0 auto;display:block;width:100%}
.big{font:700 3rem/1 system-ui,sans-serif}
.chip{display:inline-block;color:#fff;font-weight:600;font-size:13px;padding:1px 10px;border-radius:99px}
.formula{background:var(--sunbg);border-radius:10px;padding:8px 12px;font:italic 1.05rem Georgia,serif;margin:8px 0}
.warn{border-left:5px solid #c0392b;background:#fbeae7;padding:8px 12px;border-radius:8px;font-size:14px;margin-top:14px}
table{width:100%;border-collapse:collapse;font-size:14px}
td,th{padding:6px 4px;border-bottom:1px solid var(--line);text-align:left}
td:last-child,th:last-child{text-align:right}
svg{display:block;width:100%;height:auto}
.leg{display:flex;flex-wrap:wrap;gap:4px 14px;font-size:13px;margin-top:6px}
.leg i{display:inline-block;width:18px;height:4px;border-radius:2px;vertical-align:middle;margin-right:5px}
</style></head><body><div class="w">
<span class="pill">Las Misiones del Lince · Cazador de sol</span>
<h1>¿Dónde está el Sol y cuánta radiación UV hay?</h1>
<p class="mut" id="eclipse"></p>
<section aria-label="Dirección del Sol">
<h2>Dónde está el Sol ahora</h2>
<svg class="dial" viewBox="-110 -110 220 220" role="img" aria-label="Brújula con la posición del Sol">
<g id="dial"><circle r="92" fill="none" stroke="var(--line)" stroke-width="2"/>
<text y="-76" text-anchor="middle" font-weight="700" fill="#c0392b">N</text><text x="80" y="6" text-anchor="middle" fill="var(--mut)">E</text><text y="90" text-anchor="middle" fill="var(--mut)">S</text><text x="-80" y="6" text-anchor="middle" fill="var(--mut)">O</text>
<g id="sun"><circle r="13" fill="var(--sun)"/><circle r="19" fill="none" stroke="var(--sun)" stroke-width="2" stroke-dasharray="3 4"/></g></g>
<path d="M0 -20 L8 6 L0 1 L-8 6Z" fill="var(--g)"/></svg>
<p id="sundesc" style="text-align:center;margin:6px 0;font-weight:600"></p>
<div class="row" style="justify-content:center"><button id="bc">Activar brújula</button><button id="bg">Usar mi ubicación</button></div>
<p class="mut" id="msg" style="text-align:center">Sitúate en un lugar abierto y sujeta el móvil plano, con la parte de arriba hacia delante. El punto verde marca hacia dónde miras.</p>
</section>
<section aria-label="UV ahora">
<h2>UV estimado ahora (calculado)</h2>
<div class="big" id="uvn">–</div><p id="uvc"></p>
<div class="formula">UV ≈ 12,5 · sen(altura del Sol)<sup>2,42</sup></div>
<p class="mut" id="uvf"></p>
<p class="mut">Es una estimación para cielo despejado. Las nubes, el ozono y el polvo la bajan, así que en la realidad suele ser algo menor.</p>
</section>
<section aria-label="Últimos días y próximos días">
<h2>UV: últimos días, hoy y próximos días</h2>
<p class="mut" id="dnote"></p>
<svg id="ch" viewBox="0 0 700 270" role="img" aria-label="Gráfico del índice UV por horas"></svg>
<div class="leg"><span><i style="background:var(--e)"></i>Días anteriores (datos)</span><span><i style="background:var(--sun)"></i>Hoy (calculado con la fórmula)</span><span><i style="background:var(--b)"></i>Próximos días (previsión)</span><span><i style="background:#b9b8a2"></i>Hoy según el modelo</span><span><i style="background:#7d3c98"></i>Medido por AEMET (Málaga)</span></div>
<table style="margin-top:10px" id="tb"></table>
</section>
<div class="warn"><b>Seguridad:</b> no mires directamente al Sol, ni con el móvil ni a simple vista. Para un eclipse se necesitan gafas certificadas ISO 12312-2.</div>
<p class="mut" style="margin-top:12px">Los datos de UV recientes y previstos vienen del modelo CAMS (Copernicus) a través de Open-Meteo. Los valores medidos y previstos por AEMET proceden de la Agencia Estatal de Meteorología (estación de Málaga, unos 70 km al este de Estepona; valores enteros). Tu ubicación no se guarda; para pedir el dato se redondea a 2 decimales (unos 1 km).</p>
</div>
<script>
const $=s=>document.querySelector(s),R=Math.PI/180;
const WORKER='https://lince-uv.jluserotrabajo.workers.dev';
let lat=36.43,lon=-5.15,head=null,data=null,demo=false,base=72,calc=[],real=null;
const f1=x=>x.toFixed(1).replace('.',',');
function sunPos(dt,la,lo){const d=dt.getTime()/864e5+2440587.5-2451545,L=(280.46+0.9856474*d)%360,g=((357.528+0.9856003*d)%360)*R,
lam=(L+1.915*Math.sin(g)+0.02*Math.sin(2*g))*R,eps=(23.439-4e-7*d)*R,ra=Math.atan2(Math.cos(eps)*Math.sin(lam),Math.cos(lam)),dec=Math.asin(Math.sin(eps)*Math.sin(lam)),
H=(((18.697374558+24.06570982441908*d)%24)*15+lo)*R-ra,p=la*R;
return{el:Math.asin(Math.sin(p)*Math.sin(dec)+Math.cos(p)*Math.cos(dec)*Math.cos(H))/R,az:(Math.atan2(-Math.sin(H),Math.tan(dec)*Math.cos(p)-Math.sin(p)*Math.cos(H))/R+360)%360}}
const uvf=el=>el>0?12.5*Math.pow(Math.sin(el*R),2.42):0;
function cat(u){return u<3?['Bajo','#3a9d4a','Sin protección especial.']:u<6?['Moderado','#b39200','Gorra y crema; busca sombra al mediodía.']:u<8?['Alto','#e07b00','Crema FPS 30+, gorra, gafas y sombra.']:u<11?['Muy alto','#c0392b','Evita el sol de 12 a 16 h y protégete del todo.']:['Extremo','#7d3c98','Mejor a la sombra, con protección máxima.']}
const chip=u=>{const c=cat(u);return`<span class="chip" style="background:${c[1]}">${c[0]}</span>`};
const names=['Norte','Noreste','Este','Sureste','Sur','Suroeste','Oeste','Noroeste'];
function drawSun(){const s=sunPos(new Date(),lat,lon),u=uvf(s.el);
$('#dial').setAttribute('transform',`rotate(${head==null?0:-head})`);
$('#sun').setAttribute('transform',`translate(${92*Math.sin(s.az*R)},${-92*Math.cos(s.az*R)})`);
$('#sun').style.opacity=s.el>0?1:.3;
let t=s.el>0?`Sol al ${names[Math.round(s.az/45)%8].toLowerCase()} (${Math.round(s.az)}°), a ${Math.round(s.el)}° sobre el horizonte`:'El Sol está bajo el horizonte';
if(head!=null&&s.el>0){const d=Math.abs(((s.az-head+540)%360)-180);if(d<15)t+='. Lo tienes delante: no lo mires directamente.'}
$('#sundesc').textContent=t;
$('#uvn').textContent=f1(u);$('#uvc').innerHTML=chip(u)+' '+cat(u)[2];
$('#uvf').textContent=`Ahora: 12,5 · sen(${s.el.toFixed(1).replace('.',',')}°)^2,42 = ${f1(u)}`}
function dayCalc(){const n=new Date();return Array.from({length:24},(_,h)=>uvf(sunPos(new Date(n.getFullYear(),n.getMonth(),n.getDate(),h,0),lat,lon).el))}
function fake(){const c=[.9,.6,.85,1,.8,.55,.95],t=[],u=[],n=new Date();for(let j=-3;j<=3;j++)for(let h=0;h<24;h++){const d=new Date(n.getFullYear(),n.getMonth(),n.getDate()+j,h);
t.push(d.toLocaleDateString('sv-SE')+'T'+String(h).padStart(2,'0')+':00');u.push(uvf(sunPos(d,lat,lon).el)*c[j+3])}return{t,u}}
async function load(){
const url=`https://air-quality-api.open-meteo.com/v1/air-quality?latitude=${lat.toFixed(2)}&longitude=${lon.toFixed(2)}&hourly=uv_index&past_days=3&forecast_days=4&timezone=Europe%2FMadrid`;
try{const r=await fetch(url);if(!r.ok)throw 0;const j=await r.json();data={t:j.hourly.time,u:j.hourly.uv_index.map(v=>v==null?0:v)};demo=false}catch(e){data=fake();demo=true}
const td=new Date().toLocaleDateString('sv-SE');base=data.t.findIndex(s=>s.startsWith(td));if(base<0)base=72;drawAll();loadReal()}
async function loadReal(){if(!WORKER||WORKER.includes('TU-'))return;try{const r=await fetch(WORKER+'/uv?days=10');if(!r.ok)throw 0;real=await r.json();drawAll()}catch(e){real=null}}
function drawAll(){drawSun();if(!data)return;calc=dayCalc();
$('#dnote').textContent=demo?'No he podido conectar con el servicio de datos: se muestran datos de demostración.':'Datos horarios del modelo. Hoy, la línea naranja es el cálculo con la fórmula.';
const n=data.u.length,W=700,L=38,B=225,T=14,x=i=>L+i/(n-1)*(W-L-8),mx=Math.max(8,Math.ceil(Math.max(...data.u,...calc,...(real&&real.obs?real.obs.map(o=>o.max):[])))+1),y=v=>B-v/mx*(B-T),
pl=(a,z,arr,o)=>arr.slice(a-o,z-o).map((v,k)=>x(a+k)+','+y(v)).join(' ');
let s=`<rect x="${x(base)}" y="${T}" width="${x(base+24)-x(base)}" height="${B-T}" fill="var(--sunbg)"/>`;
for(let g=0;g<=mx;g+=2)s+=`<line x1="${L}" x2="${W-8}" y1="${y(g)}" y2="${y(g)}" stroke="var(--line)"/><text x="${L-6}" y="${y(g)+4}" text-anchor="end" font-size="12" fill="var(--mut)">${g}</text>`;
const k0=Math.round(base/24),nd=Math.round(n/24);
for(let j=0;j<nd;j++){const st=base-24*k0+24*j,d=new Date(data.t[st].slice(0,10)+'T12:00');s+=`<text x="${x(st+12)}" y="${B+20}" text-anchor="middle" font-size="12" fill="var(--ink)">${d.toLocaleDateString('es-ES',{weekday:'short',day:'numeric'})}</text><line x1="${x(st)}" x2="${x(st)}" y1="${T}" y2="${B}" stroke="var(--line)" stroke-dasharray="2 4"/>`}
s+=`<polyline points="${pl(0,base+1,data.u,0)}" fill="none" stroke="var(--e)" stroke-width="2.5"/>
<polyline points="${pl(base+23,n,data.u,0)}" fill="none" stroke="var(--b)" stroke-width="2.5"/>
<polyline points="${pl(base,base+24,data.u,0)}" fill="none" stroke="#b9b8a2" stroke-width="1.5"/>
<polyline points="${calc.map((v,k)=>x(base+k)+','+y(v)).join(' ')}" fill="none" stroke="var(--sun)" stroke-width="3.5"/>`;
const now=new Date(),ni=base+now.getHours()+now.getMinutes()/60;
s+=`<line x1="${x(ni)}" x2="${x(ni)}" y1="${T}" y2="${B}" stroke="var(--ink)" stroke-dasharray="4 3"/><text x="${x(ni)}" y="${T-2}" text-anchor="middle" font-size="12" fill="var(--ink)">ahora</text>`;
if(real&&real.obs)real.obs.forEach(o=>{const pts=[];o.hours.forEach((v,k)=>{const i=data.t.indexOf(o.date+'T'+String(7+k).padStart(2,'0')+':00');if(i>=0)pts.push([x(i),y(v)])});
if(pts.length)s+=`<polyline points="${pts.map(q=>q.join(',')).join(' ')}" fill="none" stroke="#7d3c98" stroke-width="2"/>`+pts.map(q=>`<circle cx="${q[0]}" cy="${q[1]}" r="3" fill="#7d3c98"/>`).join('')});
$('#ch').innerHTML=s;
let r='<tr><th>Día</th><th>Origen</th><th>UV máx.</th><th>AEMET Málaga</th></tr>';
for(let j=0;j<nd;j++){const st=base-24*k0+24*j,d=new Date(data.t[st].slice(0,10)+'T12:00'),lb=d.toLocaleDateString('es-ES',{weekday:'long',day:'numeric'}),
mm=j===k0?Math.max(...calc):Math.max(...data.u.slice(st,st+24)),o=j<k0?'Dato reciente':j===k0?'Hoy · fórmula':'Previsión';
const ds=data.t[st].slice(0,10),ob=real&&real.obs.find(q=>q.date===ds),pr=real&&real.pred&&real.pred[ds],ps=[];if(ob)ps.push('medido '+ob.max);if(pr&&pr.aemet)ps.push('previsto '+pr.aemet.v);
r+=`<tr><td>${lb}</td><td>${o}</td><td>${f1(mm)} ${chip(mm)}</td><td>${ps.join(' · ')||'–'}</td></tr>`}
$('#tb').innerHTML=r}
function onOri(e){let h=null;if(e.webkitCompassHeading!=null)h=e.webkitCompassHeading;else if(e.alpha!=null&&(e.absolute||e.type==='deviceorientationabsolute'))h=(360-e.alpha)%360;if(h!=null){head=h;drawSun()}}
$('#bc').onclick=async()=>{try{if(window.DeviceOrientationEvent&&DeviceOrientationEvent.requestPermission){if(await DeviceOrientationEvent.requestPermission()!=='granted'){$('#msg').textContent='Sin permiso para la brújula. Actívalo en los ajustes del navegador.';return}}
addEventListener('deviceorientationabsolute',onOri,true);addEventListener('deviceorientation',onOri,true);$('#msg').textContent='Brújula activada. Gira sobre ti mismo y busca el Sol (sin mirarlo).'}catch(e){$('#msg').textContent='Este dispositivo no permite usar la brújula.'}};
$('#bg').onclick=()=>navigator.geolocation?navigator.geolocation.getCurrentPosition(p=>{lat=p.coords.latitude;lon=p.coords.longitude;$('#msg').textContent='Ubicación actualizada.';load()},()=>{$('#msg').textContent='No he podido obtener la ubicación. Se usa Estepona.'}):0;
const dl=Math.ceil((new Date(2027,7,2,10,50)-new Date())/864e5);
$('#eclipse').textContent=dl>0?`Eclipse total de Sol: 2 de agosto de 2027. Faltan ${dl} días.`:'';
load();setInterval(drawAll,60000);
</script></body></html>
Ábrela desde el móvil con https. Pulsa Activar brújula y acepta los permisos. Si no hay Worker, verás los datos del modelo y el cálculo, y falta la línea morada de AEMET. Con esto ya puedes usarla en clase.
Paso 2: entra en tu cuenta de Cloudflare
Entra en dash.cloudflare.com. Es la misma cuenta del Chisme 03. Si no la tienes, es gratis y no hace falta mover tu dominio.
Paso 3: crea el Worker
Workers y Pages → Crear → Crear Worker. Ponle el nombre lince-uv y pulsa Desplegar. Después pulsa Editar código, borra todo lo que hay y pega este archivo completo. Vuelve a pulsar Desplegar.
[AQUÍ VA EL CÓDIGO: worker.js]
Apunta la dirección de tu Worker (algo como https://lince-uv.tu-cuenta.workers.dev).
// Misiones del Lince · API de UV real (AEMET, estación de Málaga) · Cloudflare Worker
// Cron diario: lee la tabla horaria de UVI observado y la previsión de AEMET, y guarda el histórico en KV.
// Endpoint público: GET /uv?days=10 · Prueba manual: GET /run?key=ADMIN_KEY
const AEMET='https://www.aemet.es';
const OPT={headers:{'User-Agent':'MonteSTEAM-Lince/1.0 (proyecto educativo, montesteam.org)'}};
const MES={january:1,february:2,march:3,april:4,may:5,june:6,july:7,august:8,september:9,october:10,november:11,december:12};
const strip=s=>s.replace(/<[^>]*>/g,' ').replace(/ /g,' ').replace(/\s+/g,' ').trim();
const hoy=()=>new Date().toLocaleDateString('sv-SE',{timeZone:'Europe/Madrid'});
const suma=(iso,n)=>{const d=new Date(iso+'T12:00:00Z');d.setUTCDate(d.getUTCDate()+n);return d.toISOString().slice(0,10)};
async function texto(url){const r=await fetch(url,OPT);if(!r.ok)throw new Error(url+' → '+r.status);return new TextDecoder('iso-8859-15').decode(await r.arrayBuffer())}
// UVI observado: 16 valores enteros (columnas 07..22 de la tabla) + máximo del día
async function recogerObs(env){
const h=await texto(AEMET+'/en/eltiempo/observacion/radiacion/ultravioleta?datos=tabla');
const f=strip(h).match(/Date:\s*[A-Za-z]+,\s*(\d{1,2})\s+([A-Za-z]+)\s+(\d{4})/);
if(!f||!MES[f[2].toLowerCase()])throw new Error('No encuentro la fecha de la tabla');
const date=`${f[3]}-${String(MES[f[2].toLowerCase()]).padStart(2,'0')}-${f[1].padStart(2,'0')}`;
const fila=h.split(/<\/tr>/i).find(r=>/l=malaga(?![\w-])/i.test(r));
if(!fila)throw new Error('No encuentro la fila de Málaga');
const n=(strip(fila.replace(/<a[\s\S]*?<\/a>/i,' ')).match(/\d+/g)||[]).map(Number);
if(n.length!==17)throw new Error('Se esperaban 17 valores y hay '+n.length);
await env.UV.put('obs:'+date,JSON.stringify({date,hours:n.slice(0,16),max:n[16],fetched:new Date().toISOString()}),{expirationTtl:34000000});
return date}
// Previsión: AEMET (máximo en cielo despejado, días +1 a +4) y Open-Meteo (máximo de mañana)
async function guardarPred(env,date,fuente,dato){const k='pred:'+date,cur=JSON.parse(await env.UV.get(k)||'{}');cur[fuente]=dato;await env.UV.put(k,JSON.stringify(cur),{expirationTtl:34000000})}
async function recogerPred(env){
const t=hoy(),out=[];
for(let k=1;k<=4;k++){
try{const h=await texto(`${AEMET}/es/eltiempo/prediccion/radiacionuv?w=${k}&datos=det`);
const m=h.split(/<\/tr>/i).map(strip).map(r=>r.match(/^M.laga\s+(\d+)$/)).find(Boolean);
if(!m)throw new Error('sin fila de Málaga');
await guardarPred(env,suma(t,k),'aemet',{v:+m[1],issued:t});out.push(k+':ok')}
catch(e){out.push(k+':'+e.message)}}
try{const j=await(await fetch('https://air-quality-api.open-meteo.com/v1/air-quality?latitude=36.43&longitude=-5.15&hourly=uv_index&forecast_days=2&timezone=Europe%2FMadrid')).json();
const d=suma(t,1),v=Math.max(0,...j.hourly.time.map((s,i)=>s.startsWith(d)?(j.hourly.uv_index[i]||0):0));
await guardarPred(env,d,'om',{v:Math.round(v*10)/10,issued:t});out.push('om:ok')}
catch(e){out.push('om:'+e.message)}
return out}
async function recoger(env){const r={};
try{r.obs=await recogerObs(env)}catch(e){r.obs='error: '+e.message}
try{r.pred=await recogerPred(env)}catch(e){r.pred='error: '+e.message}
return r}
async function listar(env,pref,n){const l=await env.UV.list({prefix:pref}),ks=l.keys.map(k=>k.name).sort().slice(-n);
return Promise.all(ks.map(async k=>[k.slice(pref.length),JSON.parse(await env.UV.get(k))]))}
export default{
async fetch(req,env){
const u=new URL(req.url),h={'Access-Control-Allow-Origin':env.ALLOW_ORIGIN||'*','Content-Type':'application/json; charset=utf-8'};
if(u.pathname==='/run'){
if(!env.ADMIN_KEY||u.searchParams.get('key')!==env.ADMIN_KEY)return new Response('No autorizado',{status:401});
return new Response(JSON.stringify(await recoger(env)),{headers:h})}
if(u.pathname==='/uv'){
const n=Math.min(30,Math.max(1,+u.searchParams.get('days')||10));
const obs=(await listar(env,'obs:',n)).map(e=>e[1]),pred=Object.fromEntries(await listar(env,'pred:',n+5));
return new Response(JSON.stringify({station:'AEMET · Málaga',obs,pred}),{headers:{...h,'Cache-Control':'public, max-age=900'}})}
return new Response('Misiones del Lince · API de UV',{status:200})},
async scheduled(ev,env,ctx){ctx.waitUntil(recoger(env))}
};
Paso 4: crea el almacén de datos (KV)
El Worker necesita un sitio donde guardar los datos de cada día. En el menú de la izquierda ve a Almacenamiento y bases de datos → KV → Crear. Ponle el nombre lince-uv-datos.
Luego vincúlalo al Worker: entra en lince-uv → Configuración → Enlaces → Añadir → Espacio de nombres KV. En Nombre de variable escribe exactamente UV (en mayúsculas) y elige lince-uv-datos.
Paso 5: dos variables
En Configuración → Variables y secretos, añade:
- Tipo Texto:
ALLOW_ORIGIN, con el valorhttps://montesteam.org. Debe coincidir con cómo se ve tu web en la barra del navegador. Si empieza porwww, ponlo también. - Tipo Secreto:
ADMIN_KEY, con una contraseña larga inventada. Es solo para ti: no la escribas nunca en el HTML.
Paso 6: que se ejecute solo cada día (cron)
En Configuración → Desencadenadores → Cron Triggers → Añadir, elige la pestaña Cron expression (no la de Schedule) y escribe:
30 21 * * *
Son cinco huecos: minuto, hora, día del mes, mes y día de la semana. Un asterisco significa “cualquiera”. Este se lee: minuto 30, hora 21, todos los días. Cloudflare usa hora UTC, así que son las 23:30 en España en verano. Ya ha pasado la última hora de la tabla de AEMET (las 22 h) y el día está completo.
Añade un segundo con 0 6 * * * (todos los días a las 06:00 UTC), por si la tabla aún enseña el día anterior.
Paso 7: pruébalo sin esperar a la noche
Escribe en el navegador la dirección de tu Worker seguida de /run?key= y tu contraseña ADMIN_KEY:
https://lince-uv.tu-cuenta.workers.dev/run?key=TU_CONTRASEÑA
Al pulsar Enter, el Worker va a buscar los datos a AEMET y los guarda. Debe contestar con un texto corto donde aparece ok varias veces. Después abre la misma dirección terminada en /uv, sin contraseña:
https://lince-uv.tu-cuenta.workers.dev/uv
Deberías ver los datos guardados, con AEMET · Málaga. /run le dice al Worker “ve a buscar los datos ahora” y /uv le dice “enséñame lo que tienes guardado”. Sin haber lanzado /run antes, /uv saldrá vacío, y es normal.
Paso 8: la única línea que cambias en el HTML
Abre chisme04sol.html con un editor de texto y busca esta línea:
const WORKER='https://TU-WORKER.TU-CUENTA.workers.dev';
Cambia la dirección por la de tu Worker, sin barra al final. Guarda y vuelve a subir el archivo.
Paso 9: comprueba en el móvil
Abre la página con https. Si todo va bien, verás la línea morada de AEMET y la columna AEMET Málaga en la tabla. La primera vez solo habrá el día de hoy. Cada noche se añade uno más, así que el histórico se construye desde el día que montes el Worker.
Si algo falla
- “No autorizado” al abrir
/run: la contraseña de la dirección no coincide con la deADMIN_KEY. - La palabra
erroren la respuesta de/run: copia el mensaje completo. Puede ser que AEMET haya cambiado su página, y hay que ajustar el código. /uvsale vacío: aún no has lanzado/run, o no se vinculó el KV con el nombre exactoUV.- No sale la línea morada: revisa
ALLOW_ORIGINy que la dirección deWORKEResté bien escrita y sin barra final. - La brújula no responde: pulsa el botón, acepta el permiso y comprueba que la página va con https.
Ojo con los límites del chisme
- El UV de la fórmula es una estimación de cielo despejado. Sobreestima. Los datos medidos son los que mandan.
- La estación de AEMET está en Málaga, unos 70 km al este de Estepona, en la costa. Las nubes o el polvo pueden ser distintos aquí, y sus valores son enteros.
- Los datos de AEMET solo han pasado controles automáticos. No se garantiza que no tengan errores.
- Los datos recientes y previstos del modelo vienen de CAMS (Copernicus) a través de Open-Meteo. Un modelo no es una medida.
Y ahora, ¿qué?
El 2 de agosto de 2027 habrá un eclipse total de Sol visible desde el sur de España, y Estepona está cerca de la franja de totalidad. Para ese día queremos añadir un modo eclipse a esta misma página: las horas, cuánto se tapa el Sol y cómo cae el UV. Y más adelante, comparar todo con el sensor UV de la Misión 11, que sí mide lo que pasa aquí mismo.
Todos los chismes estarán en El Zurrón del Lince: montesteam.org/app
Conclusión
⚠️ ANTES DE USARLO, LEE ESTO
NO MIRES AL SOL
Ni a simple vista ni con el móvil ni con unos prismáticos. La brújula sirve para saber dónde está, no para mirarlo. Para un eclipse hacen falta gafas certificadas ISO 12312-2, y no vale ninguna gafa de sol normal.
ES UNA ESTIMACIÓN, NO UN APARATO DE MEDIDA
La fórmula del lince es una aproximación. Los datos de AEMET son de otra ciudad, con valores enteros y sin revisión final. Si la app dice “Bajo” y estás rojo como un cangrejo, hazle caso a tu piel.
NO ES CONSEJO MÉDICO
Las recomendaciones de protección son generales. Ante dudas sobre lunares, manchas o quemaduras, consulta al personal sanitario.
TU UBICACIÓN
La página calcula el Sol y pide los datos con tus coordenadas redondeadas a unos 1 km, y no guarda nada tuyo. Aun así, si publicas tu propia versión, cuéntaselo claramente a quien la use, sobre todo si son menores.
TU CLAVE ES TUYA
La contraseña
ADMIN_KEYno se comparte ni se escribe en el HTML. Si sospechas que se ha filtrado, cámbiala en Cloudflare.RESPETA A QUIEN NOS DA LOS DATOS
Los datos medidos y previstos son de la Agencia Estatal de Meteorología (AEMET), y su uso exige citarla como autora. El Worker solo les pide los datos dos veces al día: no lo cambies para que lo haga más.
ESTO ES UN EXPERIMENTO EDUCATIVO
Lo hemos montado para aprender cómo se combinan un sensor del móvil, una fórmula, datos abiertos y un pequeño servidor. Puede fallar, puede caerse y puede equivocarse. Fórmate el hábito de contrastar cualquier dato, el nuestro incluido.



