tech explorers, welcome!

Autor: TRW (Página 4 de 4)

Widgets personalizados para tu sitio WordPress

Para prescindir de plugins externos innecesarios, aquí verás cómo hacer tus propios widgets para insertar en tu página de WordPress o dentro de cualquier post.

Sólo tienes que saber algo de HTML y de javascript. Vamos a ello!

Custom HTML

Si has visto algún otro post de este blog ya habrás visto la tabla de contenido de arriba, o la notificación de cookies en la barra lateral.

Ambos son elementos dinámicos que he programado con HTML y javascript y que puedes insertar en múltiples sitios de tu WordPress usando un bloque "Custom HTML".

Simplemente inserta este bloque y dentro podrás introducir tu código HTML personalizado y programable con javascript. Veamos unos ejemplos.

Notificación de cookies

La barra lateral de este blog muestra un mensaje de consentimiento de cookies, con un enlace a la política de privacidad.

Puede que hayas visto que al cambiar el idioma de la página, se cambia el contenido de dicho mensaje automáticamente.

Haz click derecho para inspeccionar la estructura de la página.

Para que funcione correctamente, imagina que el bloque Custom HTML que insertas es un iframe o una especie de página insertada; una sub-página dentro de nuestra página.

Editamos el contenido del Custom HTML como si de otra página se tratara, y podemos añadir libremente lo que queramos.

Para la notificación, solo quiero una línea de texto, a la que asigno la id "cookie-txt". Es bueno añadir toda la estructura HTML básica, aunque se puede prescindir de muchos elementos.

<html>
<body style="background-color:'lightgrey';width:100%;">
<p id="cookie-txt"></p>
</body>
</html>

Ahora añado una función javascript que lea el idioma de la página y rellene esta línea en consecuencia.

Puedes hacer distintas pruebas usando la consola y el inspector del navegador, haciendo click derecho en la página.

Una forma rápida de obtener el idioma es leyendo la propiedad lang="es-ES" del elemento <html> de la página (o lang="en-GB" para inglés).

El resto es una condición:

  • Si se detecta "es-ES", definimos el mensaje en Español.
  • Si se detecta "en-GB", definimos el mensaje en Inglés.

El contenido del mensaje lo definimos mediante la propiedad innerHTML del elemento <p>, por lo que podemos darle cualquier formato HTML, como incrustarle un enlace o resaltar en negrita.

<script>
var lang=document.getElementsByTagName("html")[0].lang;
if(lang=="es-ES"){
document.getElementById("cookie-txt").innerHTML="🍪 <a href='https://theroamingworkshop.cloud/b/?page_id=1225' target='_blank'>Política</a> <b>anti-Cookies</b> aceptada al navegar.</p>";
}else if(lang=="en-GB"){
document.getElementById("cookie-txt").innerHTML="<b>🍪 anti-Cookies</b> <a href='https://theroamingworkshop.cloud/b/?page_id=3' target='_blank'>policy</a> accepted while browsing.";
}
</script>

Por lo que quedaría algo así:

El código completo del Custom HTML que muestra la notificación de cookies es este:

<html>
<body style="background-color:'lightgrey';width:100%;">
<p id="cookie-txt"></p>
</body>
<script>
var lang=document.getElementsByTagName("html")[0].lang;
if(lang=="es-ES"){
document.getElementById("cookie-txt").innerHTML="🍪 <a href='https://theroamingworkshop.cloud/b/?page_id=1225' target='_blank'>Política</a> <b>anti-Cookies</b> aceptada al navegar.</p>";
}else if(lang=="en-GB"){
document.getElementById("cookie-txt").innerHTML="<b>🍪 anti-Cookies</b> <a href='https://theroamingworkshop.cloud/b/?page_id=3' target='_blank'>policy</a> accepted while browsing.";
}
</script>
</html>

El bloque Custom HTML, en este caso, lo inserto como un elemento de la barra lateral:

Tabla de contenido

La tabla de contenido que aparece en cada post viene a ser más o menos lo mismo que lo anterior.

En primer lugar, defino la tabla dándole formato, y dejando el contenido vacío para rellenar luego con una función.

<div id="menu" style="padding:20px 20px 20px 20px; border-left:2px solid darkgrey;">
<p style="font-weight:bold;">Contenido</p>
</div>

En este caso, lo que haré es buscar los "headings" o títulos de cada apartado, en concreto los de etiqueta <h2>. Por ello, al redactar los posts debo usar siempre este tipo de encabezado (que es el habitual) si quiero que aparezca en la tabla.

Además, defino un ancla o html Anchor, que se asignará como "id", lo cual nos permitirá crear un enlace que nos lleve hasta dicho encabezado.

La función hará lo siguiente:

  1. Buscar los elementos <h2> uno por uno.
    for (let i=0;i < window.document.getElementsByTagName("h2").length; i++) { element = window.document.getElementsByTagName("h2")[i];
    ...
  2. Tomar el texto que contiene.
    text = "▹ "+element.innerHTML;
  3. Crear el nuevo elemento de tipo hiperenlace (<a>) que irá a la tabla de contenido, con el texto anterior.
    var newelement = document.createElement("a"); newelement.innerHTML=text;
  4. Tomar la "id" del post y del encabezado para crear un enlace a dicho encabezado.
    var postid = window.document.getElementsByTagName("article")[0].id;
    var url = "https://theroamingworkshop.cloud/b/?p="+postid.substr(5,postid.length)+"#"+element.id;
  5. Añadirlo todo en una línea al elemento de la tabla de contenido
    newelement.setAttribute("href", url); newelement.appendChild(document.createElement("br")); window.document.getElementById("menu").appendChild(newelement);

TIP! Añadiendo #id-de-la-sección al final de una URL te dirije a dicho apartado.

  1. Por último, algunas páginas tardan un tiempo en cargar y no están todos los encabezados disponibles al inicio, por lo que meto todo el código anterior en una función, y la invoco con un tiempo de retardo.
    window.setTimeout(fillmenu,500);

El código del bloque completo sería el siguiente:

<div id="menu" style="padding:20px 20px 20px 20px; border-left:2px solid darkgrey;">
<p style="font-weight:bold;">Contenido</p>
</div>
<script>
var text;
var element;

function fillmenu(){

for (let i=0; i<window.document.getElementsByTagName("h2").length; i++){
element = window.document.getElementsByTagName("h2")[i];
text = "▹ "+element.innerHTML;
var newelement = document.createElement("a");
newelement.innerHTML=text;
var postid=window.document.getElementsByTagName("article")[0].id;
var url = "https://theroamingworkshop.cloud/b/?p="+postid.substr(5,postid.length)+"#"+element.id;
newelement.setAttribute("href", url);
newelement.appendChild(document.createElement("br"));
window.document.getElementById("menu").appendChild(newelement);
}
}

window.setTimeout(fillmenu,500);

</script>

TIP! Añade este bloque HTML a tus bloques reusables y lo podrás insertar en todos tus posts. Este bloque de TIP! es otro ejemplo.

Bloque de TIP!

TIP! Este bloque de TIP! es otro ejemplo. Tendrás que reconvertir a un bloque regular si quieres alterar el contenido solo en el bloque insertado

Aquí su código:

<p style="border-left:3px solid orange;padding-left:5px;font-size:14px;"><i><b>TIP! </b>Este bloque de <b>TIP!</b> es otro ejemplo. Tendrás que reconvertir a un bloque regular si quieres alterar el contenido <b>solo</b> en el bloque insertado</i></p>

Conclusión

Como verás, las posibilidades son realmente ilimitadas así que queda todo a expensas de tu imaginación.

Se creativo y diseña tus propios widgets, ya verás lo bien complementan tu blog!

Como siempre, dudas o comentarios al Twitter 🐦 ¡Hasta otra!

(el bloque de Twitter también es un reutilizable 👀 )

🐦 @RoamingWorkshop

Copia tu Raspberry Pi a una nueva tarjeta SD

Hay muchas explicaciones tediosas en la red de cómo clonar un SO Linux a un nuevo dispositivo, pero me topé con esta genial herramienta on mi Raspberry que reduce todo a unos pocos minutos y sin necesidad apenas de teclear.

Es el programa «SD Card Copier» que puedes usar de manera regular para hacer copias de seguridad de tu SO, o trasladar tu sistema de archivos a un dispositivo de mayor almacenamiento, como es mi caso.

1. Requisitos y configuración.

Quiero mover mi Raspbian OS de mi actual tarjeta SD a una mayor y más rápida, por lo que debo poder acceder a ambas tarjetas simultáneamente. Por ello, he seguido esta configuración:

  • Raspberry Pi 4B.
  • Tarjeta SD de 32Gb en la ranura de la Raspberry con Raspbian 10 Buster.
  • Tarjeta SD rápida de 128Gb (nueva y vacía) conectada mediante adaptador USB a la Raspberry.
    • Opcionalmente, puedes transferir todo el sistema de archivos a un tercer USB para recuperar los ficheros en caso de fallo.
  • Además, instalaremos piclone si no tenemos el programa SD Card Copier por defecto en nuestro SO.
sudo apt-get update

sudo apt-get install piclone

[No comprobado!] Piclone debería funcionar en otras distribuciones de Linux, si tienes la habilidad suficiente de montar todas las dependencias que requiere. En ese caso, puedes seguir las instrucciones que se detallan en su propio github:

https://github.com/raspberrypi-ui/piclone

2. Transferir tu SO via piclone.

  • Abrimos el SD Card Copier asegurándonos que tenemos permisos de superusuario:
sudo piclone

El programa es muy sencillo y se ve así:

This image has an empty alt attribute; its file name is piclone1.png
  • Selecciona cada dispositivo de los desplegables y clicka "Start".
  • Después de un rato, tendrás una copia ejeutable de tu SO en el nuevo dispositivo.

La documentación menciona unos 10-15 minutos de espera, pero tardará más según el tamaño del almacenamiento. En mi caso tardó bastante más.

3. Lanzar el nuevo SO.

Por último:

  • apaga la Raspberry,
  • introduce la nueva tarjeta en la ranura SD de la Raspberry,
  • enciende la Raspberry,
  • comprueba que están todas las aplicaciones corriendo,
  • y el almacenamiento ha aumentado.
This image has an empty alt attribute; its file name is piclone3.png
Uso de GParted para comprobar los detalles de la nueva partición.

Stellarium: qué cielo te espera esta noche con el planetario libre

¿Te preguntabas de dónde he sacado la foto de fondo del blog?

No es de internet ni de ningún telescopio. Es del genial programa Stellarium, el planetario de código abierto de precisión astronómica y realismo sin igual.

Requisitos

Stellarium está disponible para Windows, Mac y Linux, así como en una versión web igualmente impresionante, pero sin todo su potencial de edición que ofrecen las versiones de escritorio gracias a numerosos plugins que aporta la comunidad.

Según nos indican en su página web, los requisitos mínimos son los siguientes:

  • Linux/Unix; Windows 7 o superior; Mac OS X 10.12.0 o superior
  • Tarjeta gráfica con 3D y soporte para OpenGL 3.0 y GLSL 1.3 o OpenGL ES 2.0
  • 512 MiB RAM
  • 600 MiB de espacio en disco
  • Teclado
  • Ratón, touchpad o similar

Y los recomendados:

  • Sistema operativo de 64 bits
  • Linux/Unix; Windows 7 o superior; Mac OS X 10.12.0 o superior
  • Tarjeta gráfica con 3D y soporte para OpenGL 3.3 o superior
  • 1 GiB RAM o más
  • 1.5 GiB de espacio en disco
  • Teclado
  • Ratón, touchpad o similar
  • Ambiente moderadamente oscuro

Vamos, que funciona casi en cualquier dispositivo y lo podemos descargar desde su página web mediante los botones del panel superior.

Configuración inicial

Al inicio se sincroniza automáticamente la posición de todos los cuerpos celestes con la hora de nuestro PC y se actualizarán en tiempo real.

Mediante el menú inferior (o sus [atajos] ) podremos activar o desactivar distintos elementos de la vista. Yo te recomiendo que juegues un poco con ellas y lo configures como prefieras:

  • Lineas de constelaciones [C].
  • Etiquetas de constelación [V].
  • Dibujos de constelaciones [R].
  • Cuadrícula ecuatorial [E].
  • Cuadrícula azimutal [Z].
  • Suelo [G].
  • Atmósfera [A].
  • Puntos cardinales [Q].
  • Objetos de Espacio profundo [D].
  • Etiquetas de planetas [Alt+P].
  • Alternar entre montura ecuatorial o azimutal [Ctrl+M].
  • Centrar sobre el objeto seleccionado [Espacio].
  • Modo nocturno [Ctrl+N].
  • Modo pantalla completa [F11].
  • Mostrar exoplanetas [Ctrl+Alt+E].
  • Mostrar lluvia de meteoros [Ctrl+Shift+M].
  • Mostrar diálogo de búsqueda de lluvia de meteoros [Ctrl+Alt+M].
  • Posiciones de satélites [Ctrl+Z].
  • Disminuir velocidad del tiempo [J].
  • Establecer ritmo normal de tiempo [N].
  • Establecer tiempo en ahora [8].
  • Aumentar velocidad del tiempo [L].
  • Salir [Ctrl+Q].

Por otro lado, mediante el menú lateral tenemos las siguientes opciones:

  • Ventana de ubicación [F6]. Aquí podemos especificar la localización de nuestra vista, pudiendo ser un lugar de la tierra o de algún otro planeta de la lista.
  • Ventana fecha/hora [F5]. Podemos elegir cualquier fecha del pasado o del futuro ya que las órbitas de los objetos están perfectamente definidas desde hace milenios.
  • Ventana de opciones de cielo y vista [F4]. Nos permite calibrar la visibilidad de los objetos, la proyección o la imagen de paisaje, entre otras.
  • Ventana de búsqueda [F3]. Nos localizará un objeto del espacio por su nombre.
  • Ventana de configuración [F2]. Muestra los ajustes del programa como idioma, fecha del equipo o el uso de extras o scripts.
  • Ventana de cálculo astronómicos [F10]. Muestra los detalles de los cálculos realizados por el programa, como las posiciones de los cuerpos, o eventos que ocurran hoy.
  • Ventana de ayuda [F1].

Usos

Podemos desactivar la atmósfera [A] para curiosear los cuerpos del espacio pulsando con el botón izquierdo del ratón, lo que nos dará información sobre el cuerpo seleccionado. Si pulsamos [Espacio], la vista lo seguirá.

También podemos activar las líneas [C] y etiquetas de las constelaciones [V], mostrar las lluvias de meteoros [Ctrl+Shift+M] y adelantar el tiempo hasta que anochezca [F5] o [L]. Así podremos averiguar si habrá lluvia de estrellas esta noche (o en cualquier otra fecha) y conocer su orientación.

Ahora, en agosto de 2022, estamos a punto de llegar al máximo de actividad de las Perseidas, que se producirá el 13 de agosto según estos cálculos realizados desde la España peninsular, y las podremos ver desplazándose desde el Norte hacia el Noreste.

O simplemente juega con las muchas posibilidades que da el programa y obtén una impresionante imagen de fondo como la que conforma el encabezado de este blog.

¿Se te ocurren otras posibilidades para este genial programa? ¿Algún truco de configuración o algún script que merezca la pena compartir? Dejálo caer en Twitter 🐦 ¡Hasta pronto!

🐦 @RoamingWorkshop

Obtén la temperatura media del mar (o de cualquier mapa ráster)

ACTUALIZACIÓN 2023! Se elimina el servicio WMS horario y se actualiza la versión diaria a V2. Version WMS actualizada a 1.3.0

Estas semanas está de moda comentar el calentamiento que está sufriendo el Mar Mediterráneo y he encontrado muy pocos mapas que me permita pinchar fácilmente sobre él para saber la temperatura que pretende indicar.

Así que he hecho el mío propio:

  1. Vamos a buscar un WMS con la información en formato ráster,
  2. A replicar su leyenda mediante el uso de lienzos <canvas> html,
  3. Y obtener el valor del pixel seleccionado que corresponda con la leyenda.

Los datos

Existen muchas y muy liosas fuentes de información.

  • Puertos del Estado:
    Mi primer intento fue acudir a Puertos del Estado, pero lo único bueno que encontré es que usan Leaflet.. Tienen un visor en tiempo real, pero es muy complicado acceder a los datos para hacer algo con ellos (aunque ya veremos cómo ).
  • Copernicus:
    "Copernicus es el Programa de Observación de la Tierra de la Unión Europea", como bien dicen en su página web, y aglutinan la mayoría de información geográfica y medioambiental producida por los paises miembros de la UE.
    En su sección marítima, encontramos datos de Temperatura Superficial del Mar (SST: Sea Surface Temperature) obtenidas por satélite y otros sensores de diferentes organismos europeos.

Global Ocean OSTIA Diurnal Skin Sea Surface Temperature

Después de probar todos, el más completo es el mapa global generado por la MetOffice (la AEMET inglesa).

Según indican, se muestra la media horaria en un mapa sin huecos, usando datos de campo, satelitales y de medidores infra-rojo.

El mapa base

Voy a crear un mapa base como ya hemos visto en otros post.

Como voy a añadir una capa ráster WMS, que consiste en píxeles de poca resolución, añado una capa de países del mundo de Eurostat.

La descargo en formato geoJSON y, para que sea fácil insertarla, la convierto en una variable llamada "countries" en un nuevo fichero CNTR_RG_01M_2020_4326.js insertándole este encabezado (ten en cuenta que hay que cerrar el objeto JSON con "]}"para que se lea correctamente).

var countries = {
"type": "FeatureCollection",
"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } },
"features": [
{"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type": "MultiPolygon", "coordinates": [[[[51.590556, 24.242975],   ......   ]}

Creado el fichero, estará disponible para nuestro mapa enlazándolo así en el <head> de nuestro .html:

<script src="./CNTR_RG_01M_2020_4326.js"></script>

TIP! Si tienes problemas creando el archivo de países, descárgalo de este servidor o añade el link completo en el head script

Este mapa base va quedando así:

SST raster data
<!DOCTYPE html>
<html>
<head>

<title>SST raster data</title>

<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-hoalWLoI8r4UszCkZ5kL8vayOGVae1oxXe/2A4AO6J9+580uKHDO3JdHb7NzwwzK5xr/Fs0W40kiNHxM9vyTtQ=="
   crossorigin=""/>
   
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"
   integrity="sha512-BB3hKbKWOc9Ez/TAwyWxNXeoV9c1v6FIeYiBieIWkpLjauysF18NzgR1MBNBXf8/KABdlkX68nAhlwcDFLGPCQ=="
   crossorigin=""></script>

<script src="./CNTR_RG_01M_2020_4326.js"></script>

</head>

<style>
body{
	margin:0;
}
#base1{
	width:100vw;
	height:100vh;
}
</style>

<body>
	<div id="base1"></div>
</body>
<script>

	//Crear mapa de Leaflet
	var map = L.map('base1').setView([48, 10], 5);
	
	//Añadimos capa de paises
	L.geoJSON(countries, {	//usa la variable "countries" que está definida en el archivo 'CNTR_RG_01M_2020_4326.js'
		style: function(){	//sobreescribimos el estilo por defecto de Leaflet con algo más estético
			return {
			fillColor: "BurlyWood",
			color: "bisque",
			fillOpacity: 1,
			};
		}
	}).addTo(map);

</script>

</html>

Consultar y añadir un servicio WMS

Voy a añadir el mapa diario global del MetOffice que nos facilita el programa Copernicus.

Para consultar los detalles del WMS, debemos buscar el fichero de metadatos, que en este caso está en la pestaña "DATA-ACCESS".

En este fichero hacemos una búsqueda por "WMS" para encontrar el enlace que nos ofrece este servicio.

Donde vemos que el enlace es el siguiente:

http://nrt.cmems-du.eu/thredds/wms/METOFFICE-GLO-SST-L4-NRT-OBS-SST-V2

A este enlace le añadimos las siguientes funciones de GeoServer "?service=WMS&request=GetCapabilities" lo cual nos dará la información disponible en dicho servicio WMS, como capas, leyendas, estilos, unidades, etc.

http://nrt.cmems-du.eu/thredds/wms/METOFFICE-GLO-SST-L4-NRT-OBS-SST-V2?service=WMS&request=GetCapabilities

Añadimos la capa WMS como nos indica la documentación de Leaflet:

	var base = L.tileLayer.wms(URL, {
		layers: 'analysed_sst', //cambiar la capa. Comprobar con ?service=WMS&request=GetCapabilities
		format: 'image/png',
		transparent: true,
        styles: 'boxfill/rainbow',
        //time: '2022-08-02T16:30:00.000Z',
		attribution: "© <a target='_blank' href='https://resources.marine.copernicus.eu/product-detail/SST_GLO_SST_L4_NRT_OBSERVATIONS_010_001/INFORMATION'>Copernicus Marine Service & MetOffice ◳</a>"
	}).addTo(map);

Para saber que opciones debemos introducir, consultamos el fichero de metadatos, en concreto las etiquetas:

  • <Layer queryable="1"><Name> nos dará el nombre de las capas que introduciremos en la propiedad "layers:"
  • <Style><Name> nos dará los nombres de los distintos estilos disponibles para la representación del ráster, que podemos especificar en la propiedad "styles:".
  • <LegenURL> nos dará el enlace a la leyenda que emplea ese estilo.
  • <Dimension> nos dará unidades que podemos consultar en el servicio WMS. En nuestro caso la unidad es temporal, ya que podemos variar la fecha de los datos representados. Lo voy a dejar comentado, por lo que tomará la última fecha disponible, pero luego lo utilizaremos para personalizar la consulta.
SST raster data

Por último, vamos a añadirle la leyenda al mapa para tener una referencia visual.

La insertamos como imagen en el <body> y añadimos el CSS al <style>:

<style>
body{
	margin:0;
}
#base{
	width:100%;
	height:350px;
}
.leyenda{
	position:absolute;
	width:110px;
	height:264px;
	top:5px;
	right:5px;
	z-Index:1001;
	display:flex;
	justify-content:flex-end;
}
</style>

<body>
    <div class="leyenda">
		<img src="https://nrt.cmems-du.eu/thredds/wms/METOFFICE-GLO-SST-L4-NRT-OBS-SST-V2?REQUEST=GetLegendGraphic&LAYER=analysed_sst&PALETTE=rainbow&transparent=true"></img>
	</div>

	<div id="base"></div>
</body>
SST raster data

Como verás, así es muy complicado intentar averiguar la temperatura de un punto concreto.

Replicar la leyenda para consultar los datos

Para poder conocer la temperatura correspondiente a un color concreto, deberíamos conocer la posición de dicho color en la leyenda y calcular su valor proporcional a los valores extremos (310kPa= 36.85ºC y 210kPa=-3.15ºC).

El problema es que por política CORS, no podemos hacer una consulta sobre la imagen, que está fuera del dominio de nuestra aplicación. Por otro lado, si añadimos la imagen a nuestro dominio, esta tendrá resolución determinada y puede restringir la precisión de los colores consultados.

Por ello, para replicar la leyenda añado un elemento de lienzo o <canvas> junto a la leyenda.

    <div class="leyenda">
		<canvas id="gradientC"></canvas>
		<img src="https://nrt.cmems-du.eu/thredds/wms/METOFFICE-GLO-SST-L4-NRT-OBS-SST-V2?REQUEST=GetLegendGraphic&LAYER=analysed_sst&PALETTE=rainbow&transparent=true"></img>
	</div>

Y mediante javascript, ayudándonos del HTML Color Picker, vamos definiendo las distintas paradas de un relleno tipo "gradiente" que reemplazará a nuestra leyenda (la añado como función para poder usarla de forma dinámica).

	function grad(){
		//Generamos la leyenda en el canvas 'gradientC'
		var ctx=document.getElementById("gradientC").getContext('2d');
		//Definimos un gradiente lineal
		var grd=ctx.createLinearGradient(0,150,0,0);
		//Calibrar las paradas del gradiente tomando muestras de la imagen
		//Descomentar la imagen que va junto al canvas en el html
		//Usar HTML color picker: https://www.w3schools.com/colors/colors_picker.asp
		grd.addColorStop(0, "rgb(0, 0, 146)");			//0 -> -3.15
		grd.addColorStop(0.09, "rgb(0, 0, 247)");		//1
		grd.addColorStop(0.185, "rgb(1, 61, 255)");		//2
		grd.addColorStop(0.26, "rgb(0, 146, 254)");		//3
		grd.addColorStop(0.3075, "rgb(0, 183, 255)");		//4
		grd.addColorStop(0.375, "rgb(3, 251, 252)");	//5
		grd.addColorStop(0.5, "rgb(111, 255, 144)");	//6 -> 20.0
		grd.addColorStop(0.575, "rgb(191, 255, 62)");	//7
		grd.addColorStop(0.64, "rgb(255, 255, 30)");	//8
		grd.addColorStop(0.74, "rgb(255, 162, 1)");		//9
		grd.addColorStop(0.805, "rgb(255, 83, 0)");		//10
		grd.addColorStop(0.90, "rgb(252, 4, 1)");		//11
		grd.addColorStop(1, "rgb(144, 0, 0)");			//12 -> 36.85
		
		//añadir el gradiente al canvas
		ctx.fillStyle = grd;
		ctx.fillRect(0,0,255,255);
	}
	
	//ejecutamos la funcion del gradiente al inicio
	grad();
SST raster data

Crear selector de pixel y calculador de temperatura

Una vez tenemos una leyenda propia, vamos a crear un elemento que muestre el color del pixel seleccionado, así como la temperatura obtenida.

Para poder obtener el color del pixel seleccionado, éste también lo insertamos en un lienzo en el body:

	<canvas id="temp">
		<img id="pixel" src="" ALT="CLICK PARA OBTENER TEMPERATURA"></img>
	</canvas>
	
	<div id="tempTxt">PINCHA SOBRE EL MAR PARA CONOCER SU TEMPERATURA</div>

Y damos un formato adecuado a los nuevos elementos en el <style>:

#temp{
	position:absolute;
	bottom:10px;
	left:10px;
	width:150px;
	height:150px;
	background-color: lightgrey;
	border: 2px solid white;
	border-radius: 5px;
	z-Index: 1001;
}
#tempTxt{
	display:flex;
	position:absolute;
	bottom:10px;
	left:10px;
	width:150px;
	height:150px;
	padding: 2px 2px 2px 2px;
	z-Index:1001;
	font-family:verdana;
	font-size:16px;
	font-weight:bold;
	text-align:center;
	align-items:center;
    word-wrap:break-word;
    word-break:break-word;
}
SST raster data
CLICK PARA OBTENER TEMPERATURA
PINCHA SOBRE EL MAR PARA CONOCER SU TEMPERATURA

Ahora añadimos dos funciones:

  • onMapClick() para obtener el pixel seleccionado y trasladarlo al recuadro inferior. Esto lo hacemos con una petición GET al servicio WMS con las coordenadas correspondientes al pixel donde hacemos click. Es importante tener en cuenta el sistema de referencia que use el mapa del servicio WMS, que en nuestro caso no es el habitual, sino que es EPSG:3857, para la conversión de unidades.
	//Añadimos función al hacer click en el mapa
	map.addEventListener('click', onMapClick);
	
	function onMapClick(e) {
		//ejecutamos la función grad con cada click
		grad();
		//Obtenemos las coordenadas del pinto seleccionado
        var latlngStr = '(' + e.latlng.lat.toFixed(3) + ', ' + e.latlng.lng.toFixed(3) + ')';
		//console.log(latlngStr);

		//Definir el CRS para enviar la consulta al WMS
		const proj = L.CRS.EPSG3857;
        //const proj = L.CRS.EPSG4326;
		
		//Definimos los límites del mapa que pediremos al WMS para que sea aproximadamente de 1 pixel
		var BBOX=((proj.project(e.latlng).x)-10)+","+((proj.project(e.latlng).y)-10)+","+((proj.project(e.latlng).x)+10)+","+((proj.project(e.latlng).y)+10);
		
		//console.log(BBOX);
		
		//Restablecemos la imagen en cada click
		var tTxt=document.getElementById("tempTxt");
		var pix= document.getElementById("pixel");
		var ctx=document.getElementById("temp").getContext("2d");
		//pix.src="";
		ctx.fillStyle="lightgrey";
		ctx.fillRect(0,0,300,300);
		
		//Realizamos la petición del pixel seleccionado
		var xPix= new XMLHttpRequest();
		xPix.onreadystatechange = function(){
			if (this.readyState == 4 && this.status == 200) {
				pix.src=URL+WMS+BBOX;
				pix.onload=function(){
					ctx.drawImage(pix,0,0,300,300);
					tTxt.innerHTML="INTERPRETANDO LEYENDA...";
					//Interpretamos el pixel según la leyenda
					leyenda();
				}
				pix.crossOrigin="anonymous";
			}
		};
		
		xPix.open("GET", URL+WMS+BBOX);
		xPix.send();
		
		tTxt.innerHTML="CARGANDO TEMPERATURA...";
		



    }
  • leyenda() calcula la temperatura que corresponde a dicho pixel seleccionado según la leyenda que hemos creado anteriormente. Muestra el valor de la temperatura en el recuadro inferior y también se indica en la leyenda con una franja blanca.
    El algoritmo de cálculo consiste en recorrer la leyenda pixel a pixel (verticalmente) y comparar la diferencia de los valores rgb(x,y,z) de la leyenda con los valores rgb del pixel seleccionado. Iremos guardando el valor que vaya obteniendo una menor diferencia hasta llegar al final, por lo que habrá casos en que la solución no sea 100% exacta. No es la mejor manera pero es rápida (de entender y de ejecutar) y bastante efectiva.
	function leyenda(){
		var ctx=document.getElementById("temp").getContext("2d");
		var tTxt=document.getElementById("tempTxt");
		//obtenemos el valor RGB del pixel seleccionado
		var RGB=ctx.getImageData(5,5,1,-1).data;
		//console.log(ctx.getImageData(10,10,1,-1).data);
		
		var key=document.getElementById("gradientC").getContext("2d");
		var max=150;
		
		var min=1000;//la máxima diferencia sólo puede ser de 255x3=765
		var dif="";
		var val="";
		
		//recorremos el gradiente de la leyenda pixel a pixel para obtener el valor de temperatura
		for(var p=1;p<=max;p++){
			//obtenemos el valor actual
			var temp=key.getImageData(1,p,1,-1).data;
			//console.log(temp);
			//comparamos con el seleccionado y obtenemos la diferencia total
			dif=Math.abs(parseInt(temp[0])-parseInt(RGB[0]))+Math.abs(parseInt(temp[1])-parseInt(RGB[1]))+Math.abs(parseInt(temp[2])-parseInt(RGB[2]));
			if(dif<min){
				min=dif;
				val=p;
				//console.log("Obj:"+RGB[0]+","+RGB[1]+","+RGB[2]+"\nTemp:"+temp[0]+","+temp[1]+","+temp[2]+"\nDif:"+dif);
			}
		}
		var T=36.85-(val*40/max);
		T=T.toFixed(2);
		//pintamos una línea de referencia en la leyenda
		key.fillStyle="white";
		key.fillRect(0,val,255,1);
		
		//definimos la temperatura en el texto
		//si el color da gris, hemos pinchado en la tierra
		//console.log("T= "+T);
		if(RGB[0]==211&RGB[1]==211&RGB[2]==211){
			tTxt.innerHTML="PINCHA SOBRE EL MAR PARA CONOCER SU TEMPERATURA";
		}else if(typeof T == "undefined"){
			tTxt.innerHTML="¡ERROR!<BR>PRUEBA OTRO PUNTO DEL MAR.";
		}else{
			tTxt.innerHTML="TEMPERATURA APROXIMADA: <br><br>"+T+" ºC";
		}
		//console.log(key.getImageData(1,150,1,-1).data);
	}

Además, omitimos la imagen original de la leyenda y añadimos nuestros propios rótulos.

Por último, añadimos un controlador de fecha en el body:

    <div id="timeBlock">
        <button id="reini" onclick="reiniT()">↺</button>
	    <input type="datetime-local" id="timeInp" name="maptime">
    </div>

Y su CSS correspondiente:

#tempTxt{
	display:flex;
	position:absolute;
	bottom:10px;
	left:10px;
	width:150px;
	height:150px;
	padding: 2px 2px 2px 2px;
	z-Index:1001;
	font-family:verdana;
	font-size:16px;
	font-weight:bold;
	text-align:center;
	align-items:center;
    word-wrap:break-word;
    word-break:break-word;
}
#timeBlock{
    display:flex;
    flex-direction:row;
	position:absolute;
    width:250px;
    top:10px;
    left:50%;
    margin-left:-125px;
    justify-content:center;
	z-Index:1002;
    font-family:verdana;
    font-size:14px;
}
#timeInp{
	width:200px;
    height:30px;
	text-align:center;
}
#reini{
    width:35px;
    height:35px;
    margin-right:5px;
    font-weight:bold;
    font-size:18px;
    padding:0;
}

Así como algunas líneas de javascript para:

  • obtener la última fecha disponible y mostrarla,
  • actualizar el mapa cuando modifiquemos la fecha
  • y definir los valores máximos y mínimos permitidos
	//Obtener última hora de actualizacion del mapa
	var timeInp=document.getElementById("timeInp");
	var t;
    var maxT;
    var minT;
	var xTime = new XMLHttpRequest();
		xTime.onreadystatechange = function(){
			if (this.readyState == 4 && this.status == 200) {
				//convertimos el XML según https://www.w3schools.com/xml/xml_parser.asp
                var xmlDoc=this.responseXML;
                t=xmlDoc.children[0].children[1].children[2].children[12].children[1].children[5].attributes[4].value;
                //Lo convertimos en un objeto fecha quitando los segundos
        		t=new Date(t);
                t=t.toISOString().substring(0,t.toISOString().length-8);
                //Lo pasamos al selector y lo establecemos como máximo
                timeInp.max=t;
                timeInp.value=t;
                maxT=new Date(t);

                //también establecemos el minimo
                t=xmlDoc.children[0].children[1].children[2].children[12].children[1].children[5].innerHTML.trim();
                t=t.substring(0,16);
                timeInp.min=t;
                minT=new Date(t);
			}
		};
		
		xTime.open("GET", URL+"?service=WMS&request=GetCapabilities");
		xTime.send();
	
	//Selector de fecha para WMS
		
	timeInp.addEventListener("change", function(){
		t=new Date(timeInp.value.toString());
		t.setHours(12-t.getTimezoneOffset()/60);

        t=t.toISOString().substring(0,t.toISOString().length-8);
	    timeInp.value=t;
	    t=new Date(timeInp.value);
            t.setHours(12-t.getTimezoneOffset()/60);

        //si estamos en el rango de datos..
        if(t>=minT && t<=maxT){
		    t=t.toISOString();
            //actualziamos el mapa
		    base.setParams({
			    time: t,
			    styles: "boxfill/rainbow",
		    },0);
        }else{//mostramos error
            alert("La fecha introducida está fuera de rango.");
        }
	});

    //funcion de reinicio de fecha
    function reiniT(){
        timeInp.value=maxT.toISOString().substring(0,maxT.toISOString().length-13)+"23:30";
    }

Resultado

Con todo esto, nos quedará algo así, que puedes ver a pantalla completa aquí:

Si te ha gustado, tienes dudas, o alguna idea para mejorar este mapa, deja tus inquietudes en Twitter 🐦

🐦 @RoamingWorkshop

Aquí te dejo el código completo de la aplicación, uniendo todo lo de arriba. ¡Hasta la próxima!

<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8'\>
<title>SST raster data</title>

<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-hoalWLoI8r4UszCkZ5kL8vayOGVae1oxXe/2A4AO6J9+580uKHDO3JdHb7NzwwzK5xr/Fs0W40kiNHxM9vyTtQ=="
   crossorigin=""/>
   
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"
   integrity="sha512-BB3hKbKWOc9Ez/TAwyWxNXeoV9c1v6FIeYiBieIWkpLjauysF18NzgR1MBNBXf8/KABdlkX68nAhlwcDFLGPCQ=="
   crossorigin=""></script>

<script src="./CNTR_RG_01M_2020_4326.js"></script>

</head>

<style>
body{
	margin:0;
}
#temp{
	position:absolute;
	bottom:10px;
	left:10px;
	width:150px;
	height:150px;
	background-color: lightgrey;
	border: 2px solid white;
	border-radius: 5px;
	z-Index: 1001;
}
#map{
	width:100vw;
	height:100vh;
}
.leyenda{
	position:absolute;
	top:10px;
	z-Index:1001;
	display:flex;
	flex-direction:row;
	width:50px;
	height:255px;
	right: 50px;
	font-family:verdana;
	font-size:11px;
}
.leyenda-txt{
	display:flex;
	flex-direction:column;
	align-items: stretch;
	justify-content:center;
	text-align:right;
	font-weight:bold;
	text-shadow: 0 0 6px white;
	margin: 0px 3px 0px 3px;
}
.leyenda-val{
	height:255px;
	margin-top:-5px;
}
#gradientC{
	z-Index=1002;
	width:25px;
	height:255px;
	margin:0;
}
#tempTxt{
	display:flex;
	position:absolute;
	bottom:10px;
	left:10px;
	width:150px;
	height:150px;
	padding: 2px 2px 2px 2px;
	z-Index:1001;
	font-family:verdana;
	font-size:16px;
	font-weight:bold;
	text-align:center;
	align-items:center;
    word-wrap:break-word;
    word-break:break-word;
}
#timeBlock{
    display:flex;
    flex-direction:row;
	position:absolute;
    width:250px;
    top:10px;
    left:50%;
    margin-left:-125px;
    justify-content:center;
	z-Index:1002;
    font-family:verdana;
    font-size:14px;
}
#timeInp{
	width:200px;
    height:30px;
	text-align:center;
}
#reini{
    width:35px;
    height:35px;
    margin-right:5px;
    font-weight:bold;
    font-size:18px;
    padding:0;
}
</style>

<body>

    <div id="timeBlock">
        <button id="reini" onclick="reiniT()">↺</button>
	    <input type="datetime-local" id="timeInp" name="maptime">
    </div>

	<div class="leyenda">
		<div class="leyenda-txt">
			<div class="leyenda-val">36.85</div>
			<div class="leyenda-val">20.0</div>
			<div>-3.15</div>
			</div>
		<!--Descomentar para mostrar la imagen descargada de la leyenda y calibrar el gradiente-->
		<!--<img src="leyenda.jpg"></img>-->
		
		<canvas id="gradientC"></canvas>
			
		<div class="leyenda-txt" style="margin-top:-10px;"><div >(ºC)</div></div>
	</div>
	
	<canvas id="temp">
		<img id="pixel" src="" ALT="CLICK PARA OBTENER TEMPERATURA"></img>
	</canvas>
	
	<div id="tempTxt">PINCHA SOBRE EL MAR PARA CONOCER SU TEMPERATURA</div>
	
	<div id="map"></div>
</body>
<script>

	//Crear mapa de Leaflet
	var map = L.map('map').setView([48, 10], 5);
	
	//Añadimos capa de paises
	L.geoJSON(countries, {	//usa la variable "countries" que está definida en el archivo 'CNTR_RG_01M_2020_4326.js'
		style: function(){	//sobreescribimos el estilo por defecto de Leaflet con algo más estético
			return {
			fillColor: "BurlyWood",
			color: "bisque",
			fillOpacity: 1,
			};
		}
	}).addTo(map);
	
	//Añadimos servicio WMS siguiendo https://leafletjs.com/examples/wms/wms.html
	
	//OSTIA Global daily mean SST
	var URL="https://nrt.cmems-du.eu/thredds/wms/METOFFICE-GLO-SST-L4-NRT-OBS-SST-V2"
	var WMS = '?service=WMS&request=GetMap&version=1.3.0&layers=analysed_sst&styles=&format=image%2Fpng&transparent=true&width=200&height=200&CRS=EPSG:3857&bbox=';
	
	//OSTIA Global hourly mean diurnal skin SST
	//var URL="https://nrt.cmems-du.eu/thredds/wms/METOFFICE-GLO-SST-L4-NRT-OBS-SKIN-DIU-FV01.1"
	//var WMS = '?service=WMS&request=GetMap&version=1.1.1&layers=analysed_sst&styles=&format=image%2Fpng&transparent=true&width=200&height=200&srs=EPSG:3857&bbox=';

    //Global Ocean - SST Multi-sensor L3 Observations
    //var URL="http://nrt.cmems-du.eu/thredds/wms/IFREMER-GLOB-SST-L3-NRT-OBS_FULL_TIME_SERIE";
    //var WMS="?service=WMS&request=GetMap&version=1.1.1&layers=adjusted_sea_surface_temperature&styles=&format=image%2Fpng&transparent=true&width=200&height=200&srs=EPSG:4326&bbox=";
	
	var base = L.tileLayer.wms(URL, {
		layers: 'analysed_sst', //cambiar la capa. Comprobar con ?service=WMS&request=GetCapabilities
		format: 'image/png',
		transparent: true,
        styles: 'boxfill/rainbow',
    	version: "1.3.0",
        //time: '2022-08-02T16:00:00.000Z',
		attribution: "© <a target='_blank' href='https://resources.marine.copernicus.eu/product-detail/SST_GLO_SST_L4_NRT_OBSERVATIONS_010_001/INFORMATION'>Copernicus Marine Service & MetOffice ◳</a>"
	}).addTo(map);
	
	//Obtener última hora de actualizacion del mapa
	var timeInp=document.getElementById("timeInp");
	var t;
    var maxT;
    var minT;
	var xTime = new XMLHttpRequest();
		xTime.onreadystatechange = function(){
			if (this.readyState == 4 && this.status == 200) {
				//convertimos el XML según https://www.w3schools.com/xml/xml_parser.asp
                var xmlDoc=this.responseXML;
                t=xmlDoc.children[0].children[1].children[2].children[12].children[1].children[5].attributes[4].value;
                //Lo convertimos en un objeto fecha quitando los segundos
        		t=new Date(t);
                t=t.toISOString().substring(0,t.toISOString().length-8);
            	console.log(t);
                //Lo pasamos al selector y lo establecemos como máximo
                timeInp.max=t;
                timeInp.value=t;
                maxT=new Date(t);

                //también establecemos el minimo
                t=xmlDoc.children[0].children[1].children[2].children[12].children[1].children[5].innerHTML.trim();
                t=t.substring(0,16);
                timeInp.min=t;
                minT=new Date(t);
			}
		};
		
		xTime.open("GET", URL+"?service=WMS&request=GetCapabilities");
		xTime.send();
	
	//Selector de fecha para WMS
		
	timeInp.addEventListener("change", function(){
		t=new Date(timeInp.value.toString());
        t.setHours(12-t.getTimezoneOffset()/60);
        t=t.toISOString().substring(0,t.toISOString().length-8);
	    timeInp.value=t;
	    t=new Date(timeInp.value);
    	t.setHours(12-t.getTimezoneOffset()/60);
        //t=t.toISOString().substring(0,t.toISOString().length-13)+"14:00";

        //si estamos en el rango de datos..
        if(t>=minT && t<=maxT){
		    t=t.toISOString();
            //actualziamos el mapa
		    base.setParams({
			    time: t,
			    styles: "boxfill/rainbow",
		    },0);
        }else{//mostramos error
            alert("La fecha introducida está fuera de rango.");
        }
	});

    //funcion de reinicio de fecha
    function reiniT(){
        timeInp.value=maxT.toISOString().substring(0,maxT.toISOString().length-13)+"12:00";
    }

    //Generar la leyenda
	
	function grad(){
		//Generamos la leyenda en el canvas 'gradientC'
		var ctx=document.getElementById("gradientC").getContext('2d');
		//Definimos un gradiente lineal
		var grd=ctx.createLinearGradient(0,150,0,0);
		//Calibrar las paradas del gradiente tomando muestras de la imagen
		//Descomentar la imagen que va junto al canvas en el html
		//Usar HTML color picker: https://www.w3schools.com/colors/colors_picker.asp
		grd.addColorStop(0, "rgb(0, 0, 146)");			//0 -> -3.15
		grd.addColorStop(0.09, "rgb(0, 0, 247)");		//1
		grd.addColorStop(0.185, "rgb(1, 61, 255)");		//2
		grd.addColorStop(0.26, "rgb(0, 146, 254)");		//3
		grd.addColorStop(0.3075, "rgb(0, 183, 255)");		//4
		grd.addColorStop(0.375, "rgb(3, 251, 252)");	//5
		grd.addColorStop(0.5, "rgb(111, 255, 144)");	//6 -> 20.0
		grd.addColorStop(0.575, "rgb(191, 255, 62)");	//7
		grd.addColorStop(0.64, "rgb(255, 255, 30)");	//8
		grd.addColorStop(0.74, "rgb(255, 162, 1)");		//9
		grd.addColorStop(0.805, "rgb(255, 83, 0)");		//10
		grd.addColorStop(0.90, "rgb(252, 4, 1)");		//11
		grd.addColorStop(1, "rgb(144, 0, 0)");			//12 -> 36.85
		
		//añadir el gradiente al canvas
		ctx.fillStyle = grd;
		ctx.fillRect(0,0,255,255);
	}
	
	//ejecutamos la funcion del gradiente al inicio
	grad();
	
	//Añadimos función al hacer click en el mapa
	map.addEventListener('click', onMapClick);
	
	function onMapClick(e) {
		//ejecutamos la función grad con cada click
		grad();
		//Obtenemos las coordenadas del pinto seleccionado
        var latlngStr = '(' + e.latlng.lat.toFixed(3) + ', ' + e.latlng.lng.toFixed(3) + ')';
		//console.log(latlngStr);

		//Definir el CRS para enviar la consulta al WMS
		const proj = L.CRS.EPSG3857;
        //const proj = L.CRS.EPSG4326;
		
		//Definimos los límites del mapa que pediremos al WMS para que sea aproximadamente de 1 pixel
		var BBOX=((proj.project(e.latlng).x)-10)+","+((proj.project(e.latlng).y)-10)+","+((proj.project(e.latlng).x)+10)+","+((proj.project(e.latlng).y)+10);
		
		//Restablecemos la imagen en cada click
		var tTxt=document.getElementById("tempTxt");
		var pix= document.getElementById("pixel");
		var ctx=document.getElementById("temp").getContext("2d");
		ctx.fillStyle="lightgrey";
		ctx.fillRect(0,0,300,300);
		
		//Realizamos la petición del pixel seleccionado
		var xPix= new XMLHttpRequest();
		xPix.onreadystatechange = function(){
			if (this.readyState == 4 && this.status == 200) {
				pix.src=URL+WMS+BBOX;
				pix.onload=function(){
					ctx.drawImage(pix,0,0,300,300);
					tTxt.innerHTML="INTERPRETANDO LEYENDA...";
					//Interpretamos el pixel según la leyenda
					leyenda();
				}
				pix.crossOrigin="anonymous";
			}
		};
		
		xPix.open("GET", URL+WMS+BBOX);
		xPix.send();
		
		tTxt.innerHTML="CARGANDO TEMPERATURA...";
		



    }
	function leyenda(){
		var ctx=document.getElementById("temp").getContext("2d");
		var tTxt=document.getElementById("tempTxt");
		//obtenemos el valor RGB del pixel seleccionado
		var RGB=ctx.getImageData(5,5,1,-1).data;
		
		var key=document.getElementById("gradientC").getContext("2d");
		var max=150;
		
		var min=1000;//la máxima diferencia sólo puede ser de 255x3=765
		var dif="";
		var val="";
		
		//recorremos el gradiente de la leyenda pixel a pixel para obtener el valor de temperatura
		for(var p=1;p<=max;p++){
			//obtenemos el valor actual
			var temp=key.getImageData(1,p,1,-1).data;
			//comparamos con el seleccionado y obtenemos la diferencia total
			dif=Math.abs(parseInt(temp[0])-parseInt(RGB[0]))+Math.abs(parseInt(temp[1])-parseInt(RGB[1]))+Math.abs(parseInt(temp[2])-parseInt(RGB[2]));
			if(dif<min){
				min=dif;
				val=p;
			}
		}
		var T=36.85-(val*40/max);
		T=T.toFixed(2);
		//pintamos una línea de referencia en la leyenda
		key.fillStyle="#ffffff";
		key.fillRect(0,val,255,1);
		
		//definimos la temperatura en el texto
		//si el color da gris, hemos pinchado en la tierra
		if(RGB[0]==211&RGB[1]==211&RGB[2]==211){
			tTxt.innerHTML="PINCHA SOBRE EL MAR PARA CONOCER SU TEMPERATURA";
		}else if(typeof T == "undefined"){
			tTxt.innerHTML="¡ERROR!<BR>PRUEBA OTRO PUNTO DEL MAR.";
		}else{
			tTxt.innerHTML="TEMPERATURA APROXIMADA: <br><br>"+T+" ºC";
		}
	
	}

</script>

</html>

Todas las estaciones de AEMET en tu móvil con un click

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.

API de AEMET OpenData

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:

  1. Creamos nuestro documento .html con su estructura básica:
<!DOCTYPE HTML>
<html>

<head>

<style>

</style>

</head>

<body>

</body>

<script>

</script>

</html>
  1. 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>
  1. 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.

  1. 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!

🐦 @RoamingWorkshop

Spotify para Raspberry sin Premium

Página de bloqueo al acceder a Spotify desde Firefox ESR o Chromium

Pensarías (yo al menos) que podías enchufar tu Raspberry a un monitor y poner un poco de música de fondo en Spotify con el navegador, como siempre, desde open.spotify.com. Bueno, lo más probable es que te haya salido el mensaje de arriba, hayas buscado innumerables soluciones en la web, hasta que has llegado aquí. Encantado de ayudar!

El aumento de la seguridad en Spotify solo lo hace disponible en versión web para navegadores Chrome, Firefox y Edge en dispositivos de 64bits, dificultando el hacking o el streaming fraudulento sin cuenta premium.

Con Premium.

Si tienes suscripción Premium, puedes tomar un atajo y simplemente usar una de las apps que te dan acceso desde tu Raspberry como Raspicast (que sincroniza desde el smartphone), Raspotify o spotify-qt, y te darán acceso a tu música con muy poca configuración.

This image has an empty alt attribute; its file name is spotify-qt.png
spotify-qt de snapcraft.io

Sin Premium.

Para los que adoramos escuchar anuncios (y no pagar), el camino no es tan arduo. Solo debemos instalar la librería Widevine:

sudo apt-get install libwidevinecdm0

A continuación, reiniciamos el sistema y accedemos a open.spotify.com mediante Chromium.

Iniciamos sesión en nuestra cuenta, y deberíamos estar dentro de Spotify con su interfaz habitual, y sin el mensaje que nos salía anteriormente.

Por último, podemos instalar un acceso directo a Spotify en Chromium mediante el botón que aparece en la barra de navegación. Y listo! A disfrutar de Spotify en tu Raspberry!

Spotify web en Chromium Browser para Raspberry Pi

Cualquier duda o comentario, envíamela por Twitter 🐦! Hasta pronto!

🐦 @RoamingWorkshop

Servicios Web Map (WMS) y demos gratuitas para LeafletJS

Aquí iré volcando algunos ejemplos molones que se pueden conseguir con Leaflet JS.

1. Encabezado bloqueado del mundo en escala de grises.

Características:

  • Mapa de ESRI-OSM gratuito en escala de grises.
  • Nivel de zoom a escala global: L.map zoom level definido en 0.
  • Deshabilitados controles de usuario, creando una vista bloqueada.
  • Altura fija definida en el estilo del elemento.
  • Texto de atribuciones personalizado y con hipervínculo.

El código fuente detallado a continuación:

<!DOCTYPE html>
<html>

<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

<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>

</head>

<style>

</style>

<body>
<div id="mapid2" style="height:100px;"></div> <! fixed height set as a style property >
</body>

<script>

//leaflet map object
var map = L.map('mapid2',{
zoomControl:false,//disabling interaction
dragging:false,//disabling interaction
maxZoom:0,//disabling interaction
minZoom:0,//disabling interaction
doubleClickZoom:false,//disabling interaction
}).setView([20, -40], 0);//initial zoom level to 0

//tiled basemap object
var googleMaps = L.tileLayer("https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Dark_Gray_Base/MapServer/tile/{z}/{y}/{x}", {
attribution: 'Sample <b>worldwide locked demo</b> by <a href="https://theroamingworkshop.cloud">The Roaming Workshop</a>. Uses ESRI tiles.',
}).addTo(map); //custom attribution text with hyperlink

</script>
</html>

2. Identificador de paises interactivo con ESRI+National Geographic.


Características:

El código fuente detallado a continuación:

<!DOCTYPE html>
<html>

<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

<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>

<!-- Load Esri Leaflet from CDN -->
<script src="https://unpkg.com/esri-leaflet/dist/esri-leaflet.js"></script>
<script src="https://theroamingworkshop.cloud/b/wp-content/uploads/2021/08/countries.txt"></script>

<!-- Load Lealeft Control Custom by yigityuce -->
<script src="https://theroamingworkshop.cloud/b/wp-content/uploads/2021/08/Leaflet-Control-Custom.txt"></script>

<!-- FONT AWESOME CDN -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">

</head>

<style>

</style>

<body>
<div id="mapid3" style="height:600px;"></div> <! fixed size set as a style property >
</body>

<script>

//leaflet map object
var map = L.map('mapid3',{
zoomControl:false,
maxBounds:[[-90,-180],[90,180]],
});
map.setView([30, 0], 3);

//Define ESRI maps
var natgeomap = L.esri.basemapLayer("NationalGeographic");
var graymap = L.esri.basemapLayer("Terrain");

//Add unlabeled layer to map
graymap.addTo(map);


//Set map style
var layerstyle = {
weight: 0.5,
color: "grey",
dashArray: "5",
fillOpacity: 0,
};


function mouseOver(feature, layer, e){
layer.on({
mouseover: highlight,
mouseout: restore,
click: focus,
});
}

function highlight(e){
var layer = e.target;

layer.setStyle({
color:"red",
weight: 2,
});

coords=layer.getBounds().getCenter();
country=e.target.feature.properties.name;

//fix known issues with browsers

if (!L.Browser.ie && !L.Browser.opera && !L.Browser.edge) {
layer.bringToFront();
}
}

function restore(e){
var layer = e.target;

layer.setStyle(layerstyle);

//fix known issues with browsers

if (!L.Browser.ie && !L.Browser.opera && !L.Browser.edge) {
layer.bringToFront();
}
}

//instantiate label object
var label;

function focus(e){
label = L.tooltip().setContent(country+" !!").setLatLng(coords);
label.addTo(map);
map.fitBounds(e.target.getBounds());
// graymap.remove;
map.removeLayer(graymap);
natgeomap.addTo(map);
}

//Add home (reset) button

L.control.custom({
position: 'topleft',
content: '<button type="button" class="btn btn-default">'+
' <i class="fa fa-home"></i>'+
'</button>',
style:{

},
events:{
click: function(){
map.setView([30, 0], 3);
map.removeLayer(natgeomap);
graymap.addTo(map);
map.removeLayer(label);
}
},
}).addTo(map);

L.control.zoom().addTo(map);

//Add geoJson layer
L.geoJson(countries, {
style: layerstyle,
onEachFeature: mouseOver,
}).addTo(map);

</script>
</html>

LeafletJS: mapas gratis en tu web

Leaflet JS es uno de mis mayores descubrimientos de los últimos años. Es una de las librerías de mapas web más poderosas que encontrarás, compitiendo con la API de Google, ESRI o Mapbox gracias a su gran comunidad y los geniales plugins que van desarrollando.

El sofware libre se impone de nuevo 🤟

(Encuentra el código del mapa anterior en este post, con algunos otros ejemplos de Leaflet)

1. Requisitos.

Todo lo que necesitas es lo siguiente:

  • Un navegador web con compatibilidad html y javascript (cualquier navegador moderno como Firefox, Vivaldi, Opera, Chrome, Edge, etc servirá).
  • Un editor de texto (como el bloc de notas mismo).
    • [OPCIONAL] Un servidor web para alojar la página y hacerla accesible a través de internet. En todo caso, podrás abrir el fichero localmente con algunas funciones restringidas.

2. Integrar Leaflet en cualquier página.

Al tratarse de una librería es muy simple usarla en un entorno web. De hecho verás ejemplos incrustados directamente a lo largo del post.

Para empezar, crea un nuevo fichero de texto y cambia su extensión a .html.

Luego añade la estructura básica de un documento HTML, como la que encontrarás en w3schools, web que siempre recomiendo:

Añade las funcionalidades de Leaflet insertando los enlaces a las librerías dentro de la etiqueta <head> de la siguiente forma:

<head>
.....
<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>
.....
</head>

Este método lee la librería directamente de la web, pero también puedes descargar los archivos .css y .js y referirte a su ubicación local:

<link rel="stylesheet" href="/path/to/leaflet.css" />
<script src="/path/to/leaflet.js"></script>

Esta opción no se verá afectada por la conexión de red, o cambios en el fichero online, a la vez que nos permitirá realizar cambios personalizados en nuestro fichero local.

3. Añadir un mapa al cuerpo del HTML

Ubicado en html <body>

Leaflet añade sus componentes dentro del elemento <div> que asignemos dentro del <body> para acomodar nuestro diseño. Es necesario asignar una "id" a este <div> para que Leaflet lo identifique posteriormente. Voy a usar "mapid" para este ejemplo:

<div id="mapid" style="height:500px;"></div>

El mapa se verá afectado por las propiedades CSS que definamos para la etiqueta #mapid. Esto modifica cómo el elemento encaja y se ajusta a la página.

Como ejemplo, voy a modificar el contorno para que sea redondeado en lugar de rectangular, definiendo un radio de 30px al borde. Añado estos cambios dentro de las etiquetas CSS <style>. Puedes comentar o eliminar estas líneas para ver la diferencia.

<style>
#mapid{border-radius: 30px;}
</style>

Definir el mapa en javascript

Vamos a añadir el objeto del mapa en sí. Para ello usamos las funciones de la librería Leaflet (llamadas mediante L.). Añadimos esta función dentro de las etiquetas de <script> ya que hacen uso de javascript.

  • Definir el objeto de mapa. Añadimos el mapa con la función "L.map" y hacemos referencia al elemento "mapid". Luego definimos la vista y zoom iniciales con "L.setView".

    Voy a centrar la vista aproximadamente en Miami (haciendo una búsqueda en internet puedes encontrar una latitud y longitud aproximadas: 25.75, -80.2).
    El nivel de zoom lo defino en 12 para que esté bastante cerca. Valores altos harán más zoom y valores bajos harán menos zoom.
var map = L.map('mapid').setView([25.75, -80.2], 12);
  • Definir la capa base. Los mapas web hacen uso de imágenes teseladas que se van cargando de forma dinámica a medida que nos movemos por el mapa. Podemos usar la función "L.tileLayer" con cualquiera los servicios referidos en este post, incluyendo Open Street Maps (OSM), Google Maps, y ESRI. Usaremos OSM para este ejemplo:
L.tileLayer("http://a.tile.openstreetmap.org/{z}/{x}/{y}.png", {/*no properties*/
}).addTo(map);
  • Por último, vamos a añadir un marcador, una funcionalidad básica que querrás en muchos mapas. Definiremos el texto dentro de "bindPopup", pudiendo formatearlo con etiquetas HTML. La función "openPopup" muestra la etiqueta del marcador directamente al inicio.
//marcador
var marker = L.marker([25.77, -80.13]).addTo(map);

//etiqueta
marker.bindPopup("<b>I'm in</b><br>Miami Beach!").openPopup();

Resultado final.

El siguiente rectángulo es un bloque HTML insertado en wordpress con todo el código que hemos ido desarrollando. Debería mostrar el mapa base OSM centrado en Miami con un marcador en Miami Beach y con el borde redondeado.


El código exacto que contiene ese bloque HTML es el siguiente:

<!DOCTYPE html>
<html>

<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

<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>

</head>

<style>
#mapid{border-radius: 30px;}
</style>

<body>
	<div id="mapid" style="height:500px;"></div>
</body>

<script>

//leaflet map object
var map = L.map('mapid').setView([25.75, -80.2], 12);

//tiled basemap object
var googleMaps = L.tileLayer("http://a.tile.openstreetmap.org/{z}/{x}/{y}.png", {/*no properties*/
}).addTo(map);

//marker object
var marker = L.marker([25.77, -80.13]).addTo(map);

//pop up object attached to marker object
marker.bindPopup("<b>I'm in</b><br>Miami Beach!").openPopup();


</script>
</html>

TIP! Puedes copiar y pegar este código en un fichero de texto con extensión .html y abrirlo en tu navegador.

Y con esto.. a jugar! No te pierdas la documentación y los plugins en la web de Leaflet. Hay cantidad de características increíbles para añadir a tu mapa.

Aquí te indico las más interesantes que he probado:

Espero que hagas buen uso de él, y que puedas promover su uso y su desarrollo. Al fin y al cabo, el software libre es de todos! ¡Hasta la próxima! 👋

Referencias

Entradas siguientes »