Hoy comparto este simple pero efectivo y elegante soporte para mando de PS5.
Algunas imágenes/diseños de concepto:
Se sostiene por sí solo sin tornillos ni pegamento. Se cuelga directamente del lateral de la PS5 y es expandible por diseño (probablemente quepan hasta 4 mandos en cada lado).
La verdad es que imprimir con resina es otro nivel. A pesar de las complicaciones de la limpieza, el resultado es realmente increíble, y hoy te enseño mi primera impresión con la Anycubic Photon Mono X2: un Batman para colgar de tu salón con su pistola gancho.
Contenido
Modelos
La impresión que ves consta de dos modelos gratuitos. Por ello, mi enorme agradecimiento a los autores y mi referencia a su trabajo:
Lo único que he hecho yo es añadirle un esqueleto al modelo y darle la postura deseada, uniéndole luego la pistola a la mano. Asique aquí tienes mi modelo para que lo puedas imprimir tal y como se muestra en las imágenes.
Para completar el modelo, deberás crearle una capa y un gancho o pinza con el cual colgarlo.
Capa
Para la capa yo he recortado un trozo de tela de una prenda de deporte antigua. Solemos tener prendas de este estilo en color negro y suelen tener también una buena textura y brillo que dan el pego.
Empieza cortando un rectángulo y luego dale forma.
En la parte superior, enrolla un alambre o un clip que te permita ajustar la capa alrededor del cuello de la figura.
Gancho
Simplemente busca un enganche que te venga bien para colgarlo. Yo he usado una pinza a la cual he atado un fino hilo negro, lo que me permite enrollarlo sobre sí para ajustar la altura. De esta forma lo puedo colgar de algún libro de una estantería.
Y así tienes tu genial Batman colgando de tu salón. Espero te guste! Otras ideas o comentarios? Al twitter!
PAL es una versión simplificada de mi asistente del hogar en python, que se ejecuta en una placa UNIHIKER de DFRobot, y que lanzo en código abierto gratuito.
Esta es solo una pequeña demostración de la simplicidad de la ejecución de comandos con reconocimiento de voz en python, y con suerte servirá de guía para tu propio asistente.
Version actual: v0.2.0(actualizado septiembre 2024)
Contenido
Características
La versión actual incluye lo siguiente:
Reconocimiento de voz: usando la librería de código abierto SpeechRecognition, se obtiene una lista con las cadenas de texto reconocidas.
Pronóstico del tiempo: usando la API de datos de World Meteorological Organization, devuelve el tiempo de hoy y el pronóstico de los próximos 3 días. Incluye los iconos de la WMO.
Temperatura local: reads local BMP-280 temperature sensor to provide a room temperature indicator.
Comandos HTTP para IoT: flujo de trabajo básico para controlar dispositivos inteligentes de IoT mediante comandos HTTP. Actualmente ENCIENDE y APAGA un interruptor inteligente Shelly2.5.
Modo ahorro de energía: controla el brillo para reducir el consumo de energía.
Gestor de conexión: periódicamente comprueba el wifi y llama a internet para restaurar la conexión cuando se pierda.
Muestra de voces de PAL: voz clonada de PAL de "Los Mitchells contra las Máquinas" usando el modelo de IA de voz CoquiAI-TTS v2.
Botones UNIHIKER: botón A despliega un simple menú (está pensado para desarrollar un menú más complejo en el futuro).
Controles táctiles: restaura el brillo (centro), cambia de programa (izquierda) y cierra el programa (derecha) pulsando en distintas zonas de la pantalla.
En este momento, PAL v0.2.0 solo incluye compatibilidad con Shelly2.5 para fines demostrativos.
Usa las variables lampBrand, lampChannel y lampIP para ajustar tu configuración de Shelly2.5.
Esto es solo un ejemplo para mostrar cómo se pueden configurar distintos dispositivos. Estas variables se deberían usar para cambiar las peculiaridades de los comandos HTTP que se envían a cada dispositivo IoT.
Se añadirán más dispositivos en futuras actualizaciones, como Shelly1, ShellyDimmer, Sonoff D1, etc.
Modo ahorro de energía
El ahorro de energía disminuye el brillo del dispositivo para reducir el consumo de energía de la UNIHIKER. Esto se hace usando el comando del sistema "brightness".
Cambia la variable "ps_mode" para habilitar ("1") o deshabilitar ("0") el modo ahorro de energía.
Temperatura de habitación
Cambia la variable "room_temp" para habilitar ("1") o deshabilitar ("0") el módulo de lectura de sensor de temperatura. Esto requiere un sensor BMP-280 instalado mediante el conector I2C.
Comprueba este otro post para los detalles de la instalación del sensor:
Se habilita la personalización de la imagen de fondo que representa los ojos de PAL.
Usa las variables "eyesA" y "eyesB" para especificar uno de los siguientes valores y cambia la expresión del PAL:
"happy"
"angry"
"surprised"
"sad"
"happy""angry""surprised""sad"
"eyesA" se usa como fondo por defecto y "eyesB" se usa como transición cuando el reconocimiento de voz se activa y PAL habla.
El valor por defecto de "eyesA" es "surprised" y cambiará a "happy" cuando se reconoce un comando.
Comandos personalizados
Añadir tus propios comandos a PAL es simple usando la función "comandos".
Cada audio reconocido por SpeechRecognition se envía como una cadena a la función "comandos", que filtra y lanza uno u otro comando coincidente con la orden.
Solo define todas las posibles cadenas que pueden ser reconozidas para lanzar tu comando (a veces SpeechRecognition devuelve transcripciones erróneas o imprecisas).
Finalmente, define el comando que se lanza cuando la cadena coincide.
def comandos(msg):
# LAMP ON
if any(keyword in msg for keyword in ["turn on the lamp", "turn the lights on","turn the light on", "turn on the light", "turn on the lights"]):
turnLAMP("on")
os.system("aplay '/root/upload/PAL/mp3/Turn_ON_lights.wav'")
Palabra clave para la activación
Puedes personalizar las palabras clave o cadenas que activan las funciones de comandos. Si cualquiera de las palabras clave en la lista es reconocida, toda la frase se envía a la función "comandos" para buscar un comando coincidente y ejecutarlo.
En caso de PAL v0.2, estas son las palabras clave que lo activan (el 90% de las veces es Paypal...):
Puedes cambiar esto a cualquier otra frase o nombre para activar a PAL cuando la llames de esa manera.
Voz de PAL
Usa la muestra de audio "PAL_full" abajo (también en el repositorio de github en /mp3) como audio de referencia para la clonación de voz de CoquiAI-TTS v2 y genera tus propias voces:
A continuación hay algunos ejemplos de consultas y respuestas de PAL:
"Hey PAL, turn on the lights!""Hey PAL, turn the lights off""Hey PAL, what's the weather like?"
Futuros lanzamientos (To-Do list)
Iré desarrollando estas características en mi asistente personal, e iré actualizando la versión de código abierto de vez en cuando. Ponte en contacto via github si tienes especial interés en alguna de ellas:
Menú avanzado: añadir configuración y comandos manuales.
Dispositivos IoT: incluir todos los comandos HTTP de Shelly y Sonoff.
Consultar la hora: requiere clonar la voz de todas las combinaciones de números...
Consulta de Wikipedia/internet: requiere generar voz en tiempo real...
Mejoradas animaciones / temas.
Comentarios, problemas o mejoras, me encantará recibirlos en github o Twitter!
Tras varios artículos sobre el uso de información satélite vamos a ver cómo unirlo (casi) todo en un ejemplo práctico e impactante. Y es que el tamaño de los últimos incendios ocurridos en España han llamado mucho mi atención y no era capaz de hacerme una idea de lo brutales que han sido (aunque nada que ver con otros sitios como Chile, Australia, o EEUU). Pero hagamos el ejercicio sin gastar demasiados GB de información geográfica.
Lo que quiero es mostrar la extensión del incendio ocurrido en Asturias en marzo, pero quiero también intentar mostrar el impacto retirando los árboles afectados por el fuego. ¡Vamos allá!
Contenido
Descarga de datos
Usaré un Modelo Digital de Superficies (que incluye árboles y estructuras), una ortofoto tomada durante el incendio, y un Modelo Digital del Terreno (al que han eliminado los árboles y las estructuras) para reemplazarlo por las zonas afectadas por el incendio.
1. Modelos del terreno
Usaré los modelos del genial IGN, descargando los productos MDS5 y MDT5 de la zona.
Usaremos el proceso i.group de GRASS en QGIS para agrupar las distintas bandas capturadas por el satélite en un único ráster RGB, tal y como vimos en este post:
Tendremos que hacerlo para cada una de las regiones descargadas, cuatro en mi caso, que luego volveremos a unir usando el proceso Construir ráster virtual.
1. Imagen de color verdadero (TCI)
Combinamos las bandas 4, 3, 2.
2. Imagen de falso color
Combinamos las bandas 5, 4, 3.
3. Ajuste de la tonalidad
Para obtener un mejor resultado, puedes regular los valores mínimos y máximos que se consideran en cada banda que compone la imagen. Estos valores se encuentran en el Histograma de las propiedades de la capa.
Aquí te dejo los valores que yo he usado para obtener el resultado de arriba:
Banda
TCI min
TCI max
FC min
FC max
1 Rojo
-100
1500
-50
4000
2 Verde
0
1500
-100
2000
3 Azul
-10
1200
0
1200
Extensión del incendio
Como ves, la imagen en falso color nos muestra claramente la extensión del incendio. Con ella, vamos a generar un polígono que delimite el alcance del incendio.
Primero consultaremos los valores de la banda 1 (rojo) que ofrece mayor contraste para la zona del incendio. Más o menos están en el rango 300-1300.
Usando el proceso Reclasificar por tabla, asignaremos el valor 1 a las celdas dentro del rango, y el valor 0 al resto.
Vectorizamos el resultado con el proceso Poligonizar y, contrastando con la imagen satélite, seleccionamos aquellos polígonos que correspondan con el incendio.
Usaremos la herramienta Disolver para unir todos los polígonos en un elemento, y Suavizar para redondear ligeramente los contornos.
Ahora obtenemos su inverso. Extraemos la extensión de la capa Landsat y, posteriormente, hacemos la Diferencia con el polígono del incendio.
Procesar terreno
1. Combinar los datos del terreno
Lo primero que haremos es combinar los distintos archivos que conforman los modelos en un archivo único (un único fichero MDS y un único fichero MDT).
Usamos el proceso GDAL - Miscelánea ráster - Construir ráster virtual
2. Extraer datos del terreno
Extraemos los datos que nos interesan de cada modelo:
Del MDS extraemos la superficie afectada por el incendio, de modo que quitaremos los árboles que hayan en él.
Con el MDT hacemos lo inverso: dejamos el terreno (sin árboles) de la zona del incendio, para sustituir los huecos generados en el otro modelo.
Usaremos el proceso Cortar ráster por capa de máscara empleando las capas generadas en el apartado anterior.
Finalmente unimos ambas capas ráster, para que rellenen una a la otra, usando Construir ráster virtual.
Dale vida con Cesium JS
Ya deberíamos tener un modelo de superficies sin árboles en la zona del incendio, pero vamos a intentar verlo de forma interactiva.
Ya mostré un ejemplo parecido, usando un Modelo Digital del Terreno personalizado, así como una imagen satélite reciente, del volcán Tajogaite de La Palma:
En este caso volveré a usar Cesium JS para poder interactuar fácilmente con el mapa (sigue el post anterior para ver cómo subir tus ficheros personalizados al visor Cesium JS).
Para esta ocasión he creado una pantalla dividida (usando dos instancias de CesiumJS) para poder comparar el antes y el después del incendio. Aquí tienes una vista previa:
Espero que te guste! Aquí tienes el código completo y el enlace a github para que puedas descargarlo. Y recuerda, comparte tus dudas o comentarios en twitter!
Estaba preparando un artículo donde quería insertar un modelo 3D para ilustrarlo mejor, y pensaba incluso en hacer un visor yo mismo. Pero no tuve que surfear mucho para encontrarme con Three.js.
Para este ejemplo, haremos un visor "portable" enlazando las librerías al CDN oficial, en lugar de tener que descargarnos los ficheros a nuestro servidor.
De esta forma, el archivo de ejemplo te servirá en cualquier lugar con conexión a internet. Vamos a crear un fichero .html básico como el que nos sugieren en la documentación:
Vamos a seguir con el ejemplo y rellenamos el segundo bloque <script> definiendo una escena con un cubo animado en rotación:
<script type="module">
import * as THREE from 'three';
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
const renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
const geometry = new THREE.BoxGeometry( 1, 1, 1 );
const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
const cube = new THREE.Mesh( geometry, material );
scene.add( cube );
camera.position.z = 5;
function animate() {
requestAnimationFrame( animate );
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
renderer.render( scene, camera );
};
animate();
</script>
Todo eso, junto, queda así:
Añade controles de arrastre y un fondo
Ahora tenemos una base para trabajar. Puede añadir más funcionalidad insertando el módulo OrbitControls que maneja la rotación del modelo y de la cámara.
//Importa nuevos módulos al principio del script
import { OrbitControls } from 'https://unpkg.com/[email protected]/examples/jsm/controls/OrbitControls.js';
//luego, añade los controles del cursor después de declarar la cámara y el renderizador
const controls = new OrbitControls( camera, renderer.domElement );
También puedes modificar el fondo fácilmente, pero necesitas hospedar la imagen junto a la aplicación en un servidor, o ejecutarlo localmente, debido al CORS. Yo usaré la imagen de la cabecera del blog, que saqué de Stellarium.
Primero, define una textura. Luego, añádela a la escena:
//añade esto antes de renderizar, mientras defines la escena
//define la textura
const texture = new THREE.TextureLoader().load( "https://theroamingworkshop.cloud/demos/Unity1-north.png" );
//añade la textura a la escena
scene.background=texture;
Código completo:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>My first three.js app</title>
<style>
body { margin: 0; }
</style>
</head>
<body>
<script async src="https://unpkg.com/[email protected]/dist/es-module-shims.js"></script>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/[email protected]/build/three.module.js"
}
}
</script>
<body style="margin: 0; width:100%;height:300px;">
<script async src="https://unpkg.com/[email protected]/dist/es-module-shims.js"></script>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/[email protected]/build/three.module.js"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'https://unpkg.com/[email protected]/examples/jsm/controls/OrbitControls.js';
const scene = new THREE.Scene();
const texture = new THREE.TextureLoader().load( "https://theroamingworkshop.cloud/demos/Unity1-north.png" );
scene.background=texture;
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
const renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
const controls = new OrbitControls( camera, renderer.domElement );
const geometry = new THREE.BoxGeometry( 1, 1, 1 );
const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
const cube = new THREE.Mesh( geometry, material );
scene.add( cube );
camera.position.z = 5;
function animate() {
requestAnimationFrame( animate );
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
renderer.render( scene, camera );
};
animate();
</script>
</body>
</html>
Insertar un modelo 3D
Ahora vamos a sustituir este cubo por un modelo 3D propio, que en el caso de Three.js, debe tener un formato glTF (.GLB o .GLTF), que es el formato más soportado y que renderiza más rápidamente (aunque también hay soporte para .fbx, .stl, .obj y demás).
Yo exportaré a .glb esta carcasa básica de Raspberry Pi 4B que hice hace un tiempo usando Blender:
Ahora, para insertar el modelo sustituimos el bloque <script> anterior basándonos en el ejemplo "webgl_loader_gltf" que se ve al inicio del post:
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:
Vamos a buscar un WMS con la información en formato ráster,
A replicar su leyenda mediante el uso de lienzos <canvas> html,
Y obtener el valor del pixel seleccionado que corresponda con la leyenda.
Contenido
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.
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).
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.
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>:
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.
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>:
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:
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 🐦
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:
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.
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í:
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:
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.