Si has usado software GIS de escritorio, te sorprenderá saber que muchas de esas herramientas geoespaciales también están disponibles para tus mapas web mediante una librería javascript gratuita y de código abierto.

Hoy te enseño TurfJS. Pulsa el botón 🌤 del mapa incrustado y verás una de sus muchas posibilidades: la interpolación por inverso de la distancia al cuadrado o IDW.

Instalación

TurfJS cuenta con una práctica web que nos explica todas las funciones disponibles, así como la configuración inicial:

https://turfjs.org/getting-started

Para usar Turf sólo necesitamos enlazar la librería a nuestro mapa usando el enlace CDN que dan en su web:

<script src='https://unpkg.com/@turf/turf@6/turf.min.js'></script>

Mapa base

Puedes crear un mapa base, por ejemplo, con Leaflet. Además, TurfJS utiliza sintaxis JSON, por lo que es altamente compatible con las funciones geoJSON de Leaflet.

TIP! Leaflet ha sido recientemente actualizado a la versión 1.9.1. Este post todavía no está actualizado y utiliza la versión 1.7.1.

Yo voy a reciclar este ejemplo de uno de mis primeros posts con algunos cambios:

  • Añado el enlace a la librería Turf en el <head>.
  • Defino los marcadores en una matriz o array de datos, similar a como se obtendrían de una fuente de datos en formato JSON:

https://www.w3schools.com/js/js_arrays.asp

var markers=[
{"name":"Estacion1",
"lat":25.77,
"lon":-80.13,
"dato":26.4},
{"name":"Estacion2",
"lat":25.795,
"lon":-80.21,
"dato":29.8},
{"name":"Estacion3",
"lat":25.70,
"lon":-80.275,
"dato":31.35}
];

TIP! Los valores numéricos NO irán "entrecomillados", o de lo contrario Turf los reconocerá como String (en Leaflet no ocurre).

  • Añado los marcadores al mapa con un bucle, usando la función forEach(), muy habitual para estos casos:

https://www.w3schools.com/jsref/jsref_foreach.asp

markers.forEach(function(m){
    var marker=L.marker([m.lat, m.lon]).addTo(map);
    marker.bindTooltip("<b>"+m.name+"</b><br>"+m.dato).openTooltip();
});

//ASÍ AÑADÍAMOS UN MARCADOR ÚNICO A LEAFLET
//var marker = L.marker([25.77, -80.13]).addTo(map);
//marker.bindTooltip("<b>Estacion1</b><br>m.dato").openTooltip();

Quedando así:

y siendo éste su código:

<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>
<script src='https://unpkg.com/@turf/turf@6/turf.min.js'></script>
</head>

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

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

<script>
var map = L.map('map').setView([25.75, -80.2], 12);
L.tileLayer("http://a.tile.openstreetmap.org/{z}/{x}/{y}.png", {/*no properties*/
}).addTo(map);

var markers=[
{"name":"Estacion1",
"lat":25.77,
"lon":-80.13,
"dato":26.4},
{"name":"Estacion2",
"lat":25.795,
"lon":-80.21,
"dato":29.8},
{"name":"Estacion3",
"lat":25.70,
"lon":-80.275,
"dato":31.35}
];

markers.forEach(function(m){
    var marker=L.marker([m.lat, m.lon]).addTo(map);
    marker.bindTooltip("<b>"+m.name+"</b><br>"+m.dato).openTooltip();
});
</script>

Interpolación IDW con TurfJS

En la forma anterior, los datos se pueden usar muy fácilmente con TurfJS.

Una de las herramientas más interesantes que facilita Turf es la interpolación de los datos en una malla regular.

https://turfjs.org/docs/#interpolate

Esto lo hace empleando uno de los métodos más habituales: el inverso de la distancia al cuadrado (IDW) o inverse distance weighting para una potencia de 2.

IDW de Wikipedia

Seguimos el ejemplo que nos da la web de TurfJS:

  1. Convertir los datos a una capa de puntos de Turf, una FeatureCollection:

https://turfjs.org/docs/#featureCollection

//vector de puntos vacío
var points=[];

//bucle para rellenar el vector de puntos con los datos
markers.forEach(function(m){
    points.push(turf.point([m.lon, m.lat],{name: m.name, dato: m.dato}));
});

//convertir a featureCollection de Turf
points=turf.featureCollection(points);

TIP! Fíjate que Turf recibe las coordenadas en longitud-latitud, en lugar del habitual latitud-longitud!

  1. Definir las opciones de la interpolación, entre las que podemos elegir las siguientes:
    • gridType: o tipo de malla
      • points (puntos)
      • square (cuadrados)
      • hex (hexágonos)
      • triangle (triángulos)
    • property: propiedad (o campo) del objeto de puntos que contiene los valores a interpolar.
    • units: unidades espaciales usadas en la interpolación
      • miles: millas
      • kilometers: kilómetros
      • radians: radianes
      • degrees: grados
    • weight: peso o potencia a la que se eleva la distancia, habitualmente 2, aunque valores más altos darán un resultado más suavizado.
//opciones de interpolación: malla rectangular de la variable 'dato' en kilómetros usando potencia de 2

var options = {gridType: 'square', property: 'dato', units: 'kilometers', weight: 2};
  1. Por último, generar la malla interpolada y añadirla al mapa, siendo el valor numérico el tamaño de las celdas de la malla interpolada (0.5 Km):
//crear malla de Turf
var malla=turf.interpolate(points, 0.5, options);

//añadir a Leaflet
L.geoJSON(malla).addTo(map);

Siendo este el resultado (nada concluyente):

Dar formato a la malla y mostrar valores

Aunque no lo parezca, la interpolación está hecha, aunque no se está mostrando correctamente.

Voy a añadir una escala de color que represente los datos, y también a mostrar los valores al pasar por encima de las celdas. Para ello, modificamos las propiedades del objeto geoJSON de Leaflet:

  • style: función para modificar el estilo de los objetos, evaluando su valor.
  • onEachFeature: función que se añade a cada objeto para interactuar con él.

https://leafletjs.com/reference.html#geojson

https://leafletjs.com/examples/choropleth/

L.geoJSON(malla,{
  style:function(feature){
  var val=feature.properties.dato;
  if(val>30){
    return {fillColor: "orangered", fillOpacity: 0.5, weight:0};
  }else if(val>28 && val < 30){
    return {fillColor: "yellowgreen", fillOpacity: 0.5, weight:0};
  }else if(val>26 && val < 28){
    return {fillColor: "darkgreen", fillOpacity: 0.5, weight:0};
  }
},
onEachFeature:function(feature,layer){
  layer.on({
  mouseover:function(e){
    val=e.target.feature.properties.dato.toFixed(2);
    e.target.bindTooltip(val).openTooltip();
  }
  });
}
}).addTo(map);

Quedando así:

Y aquí el código completo:

<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>
<script src='https://unpkg.com/@turf/turf@6/turf.min.js'></script>
</head>

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

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

<script>
var map = L.map('map').setView([25.75, -80.2], 12);
L.tileLayer("http://a.tile.openstreetmap.org/{z}/{x}/{y}.png", {/*no properties*/
}).addTo(map);

var markers=[
{"name":"Estacion1",
"lat":25.77,
"lon":-80.13,
"dato":26.4},
{"name":"Estacion2",
"lat":25.795,
"lon":-80.21,
"dato":29.8},
{"name":"Estacion3",
"lat":25.70,
"lon":-80.275,
"dato":31.35}
];

markers.forEach(function(m){
    var marker=L.marker([m.lat, m.lon]).addTo(map3);
    marker.bindTooltip("<b>"+m.name+"</b><br>"+m.dato).openTooltip();
});

//Datos para interpolación de Turf
//vector de puntos vacío
var points=[];
//bucle para rellenar el vector de puntos con los datos
markers.forEach(function(m){
points.push(turf.point([m.lon, m.lat],{name: m.name, dato: m.dato}));
});
//convertir a featureCollection de Turf

points=turf.featureCollection(points);

//opciones de interpolación
var options = {gridType:"square",property:"dato",units:"kilometers",weight:2};

//crear malla de Turf
var malla=turf.interpolate(points, 0.5, options);

//añadir a Leaflet
malla=L.geoJSON(malla,{
  style:function(feature){
  var val=feature.properties.dato;
  if(val>30){
    return {fillColor: "orangered", fillOpacity: 0.5, weight:0};
  }else if(val>28 && val < 30){
    return {fillColor: "yellowgreen", fillOpacity: 0.5, weight:0};
  }else if(val>26 && val < 28){
    return {fillColor: "darkgreen", fillOpacity: 0.5, weight:0};
  }
},
onEachFeature:function(feature,layer){
  layer.on({
  mouseover:function(e){
    val=e.target.feature.properties.dato.toFixed(2);
    e.target.bindTooltip(val).openTooltip();
  }
  });
}
}).addTo(map);

</script>

Aplicaciones

Como ejemplo aplicado a datos reales, he modificado la librería leafMET para incorporar la interpolación y añadido isobandas.

Es el mapa que has visto al inicio y que puedes abrir en pantalla completa:

https://theroamingworkshop.cloud/leafMET/leafMET+Turf.html

Además, el código está disponible en github como variante de leafMET:

https://github.com/TheRoam/leafMET/tree/leafMET+Turf

Y eso es todo! Espero que te sea útil!

Cualquier duda o comentario te espero en 🐦 Twitter!

🐦 @RoamingWorkshop