La Agencia Estatal de Meteorología española (AEMET) cuenta con una API de datos abiertos con la que podemos acceder a la mayoría de datos que publican en su web.
De esta forma, cualquier usuario puede crear apps muy sencillas sólo con la información que necesita y sin tener que acceder a su web.
En mi caso, quiero mostrar de forma intuitiva los datos de las estaciones en un mapa web que pueda ver desde cualquier dispositivo con internet como mi móvil. Aquí un anticipo para que sigas leyendo:
Definir un mapa base
Como no podía ser de otra forma, vamos a usar todo el potencial de Leaflet JS y los mapas abiertos del Centro de Decargas del IGN.
En este caso, sólo necesitamos un mapa muy sencillo, por lo que:
- Creamos nuestro documento .html con su estructura básica:
<!DOCTYPE HTML>
<html>
<head>
<style>
</style>
</head>
<body>
</body>
<script>
</script>
</html>
- Añadimos los enlaces a las librerías de Leaflet dentro de las etiquetas <head>:
También añado las propiedades <meta> charset(1) y viewport(2) para (1)interpretar correctamente los caracteres especiales como tildes y, (2) ajustar correctamente la ventana a dispositivos móviles.
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css"
integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A=="
crossorigin=""/>
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"
integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA=="
crossorigin=""></script>
<style>
</style>
</head>
- Definimos la ubicación del mapa de Leaflet dentro del <body>
<body style="border:0;margin:0;">
<div id="mapa" style="width:100vw; height:100vh;z-index:0"></div>
</body>
Como verás, añado border:0
y margin:0
al estilo del <body>. Esto ajusta el mapa perfectamente a la ventana, sin ningún tipo de espacio en blanco.
Height:100vh
y width:100vw
ajustarán el mapa al 100% del tamaño de la ventana (view height y view width). El z-index
nos servirá para ordenar los objetos y que no se tapen entre ellos.
- Ya podemos generar el mapa mediante javascript. Pasamos al bloque <script>
Con L.map
definimos el mapa y su vista inicial (que he centrado en Madrid).
Con L.tileLayer.wms
añadimos el servicio WMS del IGN para obtener un mapa base "online". Puedes encontrar las URL completas de cada servicio consultando los metadatos que acompañan a cada producto.
Además, es habitual que los WMS cuenten con varias capas, por lo que debemos definir una. Podemos obtener el listado de capas usando la función GetCapabilities de los wms. En nuestro caso usamos la <Layer>
con <Name>
"IGNBaseTodo-gris" que identificamos aquí:
https://www.ign.es/wms-inspire/ign-base?service=WMS&request=GetCapabilities
<script>
var o = L.map('mapa').setView([40.5, -3.5], 8);
var IGN = window.L.tileLayer.wms("https://www.ign.es/wms-inspire/ign-base", {
layers: 'IGNBaseTodo-gris',
format: 'image/png',
transparent: true,
attribution: "BCN IGN © 2022"
});
IGN.addTo(o);
</script>
Uniendo todo esto ya tendremos un mapa básico:
Aplicación leafMET.js
Ahora añadimos el plugin de descarga y representación de datos de AEMET leafMET que encontrarás en mi github.
Podemos descargar el fichero leafMET.js y añadirlo como script local, o podemos enlazarlo directamente desde esta web. Dentro de las etiquetas <head> añadimos lo siguiente:
<script type="application/javascript" src="https://theroamingworkshop.cloud/leafMET/leafMET.js"></script>
Acceso directo .html
Ahora, para ver la aplicación con un sólo click desde el móvil, debemos copiar el contenido de leafMET.js dentro de las etiquetas <script> del documento .html
Así, tendremos un único archivo con todo lo necesario para correr nuestra webapp. He añadido las etiquetas <title> y "icon" en <head> para mostrar un título de página y que el acceso directo herede el icono que definamos.
Debería quedar algo así:
<!DOCTYPE HTML>
<html lang="en">
<head>
<title>leafMET</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🌤</text></svg>">
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css"
integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A=="
crossorigin=""/>
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"
integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA=="
crossorigin=""></script>
<script type="application/javascript" src="https://theroamingworkshop.cloud/leafMET/leafMET.js"></script>
<style>
</style>
</head>
<body style="border:0;margin:0;">
<div id="mapa" style="width:100vw; height:100vh;z-index:0"></div>
</body>
<script>
//Create Leaflet map and load OSM layer
var o = L.map('mapa').setView([40.5, -3.5], 8);
var IGN = window.L.tileLayer.wms("https://www.ign.es/wms-inspire/ign-base", {
layers: 'IGNBaseTodo-gris',
format: 'image/png',
transparent: true,
attribution: "BCN IGN © 2022"
});
IGN.addTo(o);
leafMET();
o.on('moveend', colocaEnLaVista);
var datosMET=null;
var sta=[];
var pinta="cross";
var ini=0;
function leafMET(){
//Create button
var btmet = window.document.createElement("BUTTON");
btmet.id="btmet";
btmet.title="Cargar datos meteorológicos";
btmet.innerHTML="<b>🌤</b>";
btmet.style.zIndex="1000";
btmet.style.position="absolute";
btmet.style.top="100px";
btmet.style.left="10px";
btmet.style.fontSize="16px";
btmet.style.textAlign="center";
btmet.style.width="35px";
btmet.style.height="35px";
btmet.style.background="Turquoise";
btmet.style.border="0px solid black";
btmet.style.borderRadius="5px";
btmet.style.cursor="pointer";
btmet.addEventListener("click", pintaMET);
window.document.body.appendChild(btmet);
//Create subtitle
var mtxt = window.document.createElement("P");
mtxt.id="mtxt";
mtxt.innerHTML="<b>Carga</b>";
mtxt.title="Cargar datos meteorológicos";
mtxt.style.zIndex="1000";
mtxt.style.position="absolute";
mtxt.style.top="130px";
mtxt.style.left="7px";
mtxt.style.fontSize="10px";
mtxt.style.textAlign="center";
mtxt.style.width="40px";
mtxt.style.height="15px";
mtxt.style.background="DarkOrange";
mtxt.style.border="0px solid black";
mtxt.style.borderRadius="2px";
mtxt.style.cursor="context-menu";
mtxt.style.fontFamily="Arial";
window.document.body.appendChild(mtxt);
//Create key
for (var i=1; i<=6;i++){
var mkey = window.document.createElement("P");
mkey.id="mkey"+i;
mkey.innerHTML="<b>"+i+"</b>";
mkey.style.zIndex="1000";
mkey.style.position="absolute";
var pos = 140+i*16;
pos = pos+"px";
mkey.style.top=pos;
mkey.style.left="7px";
mkey.style.fontSize="10px";
mkey.style.textAlign="center";
mkey.style.textIndent="0px";
mkey.style.width="40px";
mkey.style.height="15px";
mkey.style.background="DarkOrange";
mkey.style.opacity="0.75";
mkey.style.border="0px solid black";
mkey.style.borderRadius="2px";
mkey.style.cursor="context-menu";
mkey.style.fontFamily="Arial";
mkey.style.fontWeight="bold";
mkey.style.color="black";
mkey.style.display="none";
window.document.body.appendChild(mkey);
}
}
//Create loading message
function loader(){
var ldmet = window.document.createElement("P");
ldmet.id="ldmet";
ldmet.innerHTML="CARGANDO DATOS METEOROLÓGICOS";
ldmet.style.zIndex="1000";
ldmet.style.position="relative";
ldmet.style.top="-50vh";
ldmet.style.width="250px";
ldmet.style.margin="auto";
ldmet.style.textAlign="center";
ldmet.style.background="DarkOrange";
ldmet.style.fontWeight="bold";
ldmet.style.fontSize="11px";
ldmet.style.padding="4px 4px 4px 4px";
ldmet.style.fontFamily="Arial";
window.document.body.appendChild(ldmet);
}
function getAEMET(){
//show loading message
loader();
//http GET request to data
var data = null;
var xhr = new XMLHttpRequest();
var xhd = new XMLHttpRequest();
xhr.withCredentials = true;
xhd.withCredentials = true;
xhr.onload= function () {
var consulta= JSON.parse(this.responseText);
xhd.open("GET", consulta.datos);
xhd.send();
}
xhd.onload= function () {
data= JSON.parse(this.responseText);
loadAEMET(data);
}
xhr.open("GET", "https://opendata.aemet.es/opendata/api/observacion/convencional/todas?api_key=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJyb21hbmhkZXpnb3JyaW5AZ21haWwuY29tIiwianRpIjoiZmFiMTM1N2QtNTJhMC00ZWQ1LWFkNzYtNjY5YTAzNGI4YTFlIiwiaXNzIjoiQUVNRVQiLCJpYXQiOjE1NzAxMjM2MzIsInVzZXJJZCI6ImZhYjEzNTdkLTUyYTAtNGVkNS1hZDc2LTY2OWEwMzRiOGExZSIsInJvbGUiOiIifQ.-7vQF_TJLghx3g4t3GiHzlWt52LpMChqqtfNUhW07LQ");
xhr.setRequestHeader("cache-control", "no-cache");
xhr.send(data);
}
function loadAEMET(d){
var obj=d.length;
//loop to plot stations (s) as circles using station coordinate data
for (var s=0; s < obj; s++){
sta[s]=window.L.circle([d[s].lat, d[s].lon],{radius: 5000, weight: 0, opcaity: 0.0, fillColor: "RoyalBlue", fillOpacity: 0.01});
//add name data
sta[s].title=d[s].ubi;
//add data values (temp, wind speed and rain)
sta[s].temp=d[s].ta;
sta[s].viento=d[s].vmax;
sta[s].lluvia=d[s].prec;
var prop="<p><b>"+d[s].ubi+"</b></p>";
prop=prop+"<p>"+d[s].fint+"</p>";
prop=prop+"<p><b>Temperatura: </b>"+sta[s].temp+" ºC</p>";
prop=prop+"<p><b>Viento: </b>"+sta[s].viento+" km/h</p>";
prop=prop+"<p><b>Lluvia: </b>"+sta[s].lluvia+" mm</p>";
sta[s].bindPopup(prop);
}
//remove loading message
window.document.getElementById("ldmet").style.display="none";
//update stations visible in view
colocaEnLaVista();
}
function colocaEnLaVista(){
//Plot only stations visible in view bounds
var vista=o.getBounds();
for (var s in sta){
sta[s].setStyle({fillOpacity: 0.01,fill: 0.01});
if(vista.contains(sta[s].getLatLng())){
var cobertura=o.distance(o.getBounds().getNorthEast(),o.getBounds().getSouthEast());
sta[s].setRadius(cobertura/30);
sta[s].addTo(o);
}else if(sta[s]){
sta[s].remove();
}
//edit stations display color according to data value
switch(pinta){
case "viento":
if (sta[s].viento==null || sta[s].viento<0){
sta[s].setStyle({fillOpacity: 0.0,fill: 0.0, cursor: "none"});
}else if (sta[s].viento>=0 && sta[s].viento<5){
sta[s].setStyle({fillColor: "green"});
}else if (sta[s].viento>=5 && sta[s].viento<10){
sta[s].setStyle({fillColor: "yellow"});
}else if (sta[s].viento>=10 && sta[s].viento<20){
sta[s].setStyle({fillColor: "orange"});
}else if (sta[s].viento>=20 && sta[s].viento<30){
sta[s].setStyle({fillColor: "red"});
}else if (sta[s].viento>=30 && sta[s].viento<500){
sta[s].setStyle({fillColor: "purple"});
}
break;
case "lluvia":
if (sta[s].lluvia==null || sta[s].lluvia<=0){
sta[s].setStyle({fillOpacity: 0.0, fill: 0.0, cursor: "none"});
}else if (sta[s].lluvia>0 && sta[s].lluvia<2){
sta[s].setStyle({fillColor: "LightCyan"});
}else if (sta[s].lluvia>=2 && sta[s].lluvia<5){
sta[s].setStyle({fillColor: "LightSkyBlue"});
}else if (sta[s].lluvia>=5 && sta[s].lluvia<10){
sta[s].setStyle({fillColor: "SkyBlue"});
}else if (sta[s].lluvia>=10 && sta[s].lluvia<20){
sta[s].setStyle({fillColor: "DodgerBlue"});
}else if (sta[s].lluvia>=20 && sta[s].lluvia<30){
sta[s].setStyle({fillColor: "Blue"});
}else if (sta[s].lluvia>=30 && sta[s].lluvia<500){
sta[s].setStyle({fillColor: "DarkBlue"});
}
break;
case "temp":
if (sta[s].temp==null || sta[s].temp<=0){
sta[s].setStyle({fillOpacity: 0.0, fill: 0.0, cursor: "none"});
}else if (sta[s].temp>0 && sta[s].temp<5){
sta[s].setStyle({fillColor: "LightCyan"});
}else if (sta[s].temp>=5 && sta[s].temp<10){
sta[s].setStyle({fillColor: "DodgerBlue"});
}else if (sta[s].temp>=10 && sta[s].temp<20){
sta[s].setStyle({fillColor: "SpringGreen"});
}else if (sta[s].temp>=20 && sta[s].temp<30){
sta[s].setStyle({fillColor: "Gold"});
}else if (sta[s].temp>=30 && sta[s].temp<100){
sta[s].setStyle({fillColor: "DarkOrange"});
}
break;
case "cross":
sta[s].remove();
break;
}
}
}
function pintaMET(){
//only download data at start
if(ini==0){
getAEMET();
ini=1;
}
//empty legend
for (var i=1; i<=6;i++){
window.document.getElementById("mkey"+i).style.display= "none";
window.document.getElementById("mkey"+i).style.color="black";
}
//generate new legend for each type of data
//(using the current icon to define the next key)
if (pinta=="cross"){
pinta="temp";
window.document.getElementById("btmet").innerHTML="🌡";
window.document.getElementById("btmet").title="Temperatura (ºC)";
window.document.getElementById("mtxt").innerHTML="<b>T (ºC)</b>";
window.document.getElementById("mtxt").title="Temperatura (ºC)";
//set key
mkey1.style.display="block";
mkey1.style.background="LightCyan";
mkey1.innerHTML="0 - 5";
mkey2.style.display="block";
mkey2.style.background="DodgerBlue";
mkey2.innerHTML="5 - 10";
mkey3.style.display="block";
mkey3.style.background="SpringGreen";
mkey3.innerHTML="10 - 20";
mkey4.style.display="block";
mkey4.style.background="Gold";
mkey4.innerHTML="20 - 30";
mkey5.style.display="block";
mkey5.style.background="DarkOrange";
mkey5.innerHTML="> 30";
}else if(pinta=="temp"){
pinta="viento";
window.document.getElementById("btmet").innerHTML="🌪";
window.document.getElementById("btmet").title="Viento (Km/h)";
window.document.getElementById("mtxt").innerHTML="<b>V(km/h)</b>";
window.document.getElementById("mtxt").title="Viento (Km/h)";
//set key
mkey1.style.display="block";
mkey1.style.background="green";
mkey1.innerHTML="0 - 5";
mkey2.style.display="block";
mkey2.style.background="yellow";
mkey2.innerHTML="5 - 10";
mkey3.style.display="block";
mkey3.style.background="orange";
mkey3.innerHTML="10 - 20";
mkey4.style.display="block";
mkey4.style.background="red";
mkey4.innerHTML="20 - 30";
mkey4.style.color="white";
mkey5.style.display="block";
mkey5.style.background="purple";
mkey5.innerHTML="> 30";
mkey5.style.color="white";
}else if(pinta=="lluvia"){
pinta="cross";
window.document.getElementById("btmet").innerHTML="❌";
window.document.getElementById("btmet").title="Sin clima";
window.document.getElementById("mtxt").innerHTML="<b>-</b>";
window.document.getElementById("mtxt").title="Sin clima";
}else if(pinta=="viento"){
pinta="lluvia";
window.document.getElementById("btmet").innerHTML="☔";
window.document.getElementById("btmet").title="Precipitación (l/m²)";
window.document.getElementById("mtxt").innerHTML="<b>P (mm)</b>";
window.document.getElementById("mtxt").title="Precipitación (l/m²)";
//set key
mkey1.style.display="block";
mkey1.style.background="LightCyan";
mkey1.innerHTML="0 - 2";
mkey2.style.display="block";
mkey2.style.background="LightSkyBlue";
mkey2.innerHTML="2 - 5";
mkey3.style.display="block";
mkey3.style.background="SkyBlue";
mkey3.innerHTML="5 - 10";
mkey4.style.display="block";
mkey4.style.background="DodgerBlue";
mkey4.innerHTML="10 - 20";
mkey5.style.display="block";
mkey5.style.background="Blue";
mkey5.innerHTML="20 - 30";
mkey5.style.color="white";
mkey6.style.display="block";
mkey6.style.background="DarkBlue";
mkey6.innerHTML="> 30";
mkey6.style.color="white";
}
//update stations visible in view
colocaEnLaVista();
}
</script>
</html>
También puedes descargarte el fichero leafMET.html de mi github directamente en tu móvil y listo!
¿Te ha servido este post? ¿Tienes dudas o comentarios? Pásate por Twitter y hazme llegar tus inquietudes! ¡Hasta otra!