MapLibre GL JS (Mapbox GL)
MapLibre GL JS es una librería de JavaScript que utiliza WebGL para representar mapas interactivos a partir de mosaicos vectoriales y estilos MapLibre. Es parte del ecosistema MapLibre GL. 1
Info
En diciembre de 2020 Mapbox lanza la versión 2 de la librería y cambia la licencia y las condiciones de uso (Ahora es necesario tener un token de Mapbox para que funcione). Hasta la versión 1.13 la librería tiene una licencia BSD y se puede usar sin un token de Mapbox
Debido al cambio de licencia parte de la comunidad lanza MapLibre GL JS 2 que es un fork de Mapbox GL JS en su versión 1.13
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Maplibre</title>
<link
href="https://unpkg.com/maplibre-gl@5.18.0/dist/maplibre-gl.css"
rel="stylesheet"
/>
<script src="https://unpkg.com/maplibre-gl@5.18.0/dist/maplibre-gl.js"></script>
<style>
#map {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
<script>
const map = new maplibregl.Map({
container: "map",
style: "https://geoserveis.icgc.cat/contextmaps/icgc_mapa_estandard_general.json",
center: [2.16859, 41.3954],
zoom: 13,
maxZoom: 14,
hash: true,
pitch: 45,
});
</script>
</body>
</html>
¿Qué es un estilo?
Un estilo MapLibre es un JSON que define la apariencia visual de un mapa: qué datos dibujar, el orden para dibujarlos y cómo diseñar los datos al dibujarlos. Esta especificación define y describe estas propiedades. 3
Note
Al declarar un mapa no es necesario declarar el estilo
Un estilo MapLibre es un JSON que describe:
-
Las fuentes de datos (
sources), -
Las capas que se dibujan (
layers), -
Cómo se ven (
paint,layout, filtros), -
Metadatos y otras opciones.
Ejemplos de estructura minima
{
"version": 8,
"name": "Mi estilo",
"sources": {
"puntos": {
"type": "geojson",
"data": "puntos.geojson"
}
},
"layers": [
{
"id": "puntos-circulo",
"type": "circle",
"source": "puntos"
}
]
}
MapLibre GL JS no dibuja nada que no esté en layers, y no puede dibujar una
layer sin una source.
Raíz (elementos del primer nivel)
Las propiedades de nivel raíz de un estilo Mapbox especifican las capas del mapa, las fuentes de datos y otros recursos, como por ejemplo los valores predeterminados para la posición inicial de la cámara.
Ejemplo
{
"version": 8,
"name": "NOMBRE_DEL_ESTILO",
"sprite": "URL_DEL_SPRITE",
"glyphs": "URL_DE_LAS_FUENTES/{fontstack}/{range}.pbf",
"sources": {...},
"layers": [...]
}
Version
Debe ser 8 para MapLibre GL JS, indica la versión del Style Specification. No es la versión de MapLibre ni del SDK. Si el número no coincide, el estilo no se carga.
Sprite (iconos y patrones)
Un sprite es una imagen única que contiene todos los iconos incluidos en un estilo. Al combinar muchas imágenes pequeñas en una sola imagen (el objeto), puede reducir la cantidad de solicitudes necesarias para obtener todas las imágenes, lo que mejora el rendimiento y hace que su mapa se cargue más rápidamente.4
Una URL base para recuperar la imagen y los metadatos del sprite. Las extensiones .png, .json y el factor de escala @ 2x.png se agregarán automáticamente. Esta propiedad es necesaria si alguna capa utiliza las propiedades patrón de fondo, patrón de relleno, patrón de línea, patrón de extrusión de relleno o imagen de icono. La URL debe ser absoluta
"sprite": "https://geoserveis.icgc.cat/contextmaps/sprites/sprite@1",
https://geoserveis.icgc.cat/contextmaps/sprites/sprite@1.png https://geoserveis.icgc.cat/contextmaps/sprites/sprite@1.json
Ejemplo de como usarlo
"layout": {
"icon-image": "hospital"
}
El nombre "hospital" debe existir dentro del sprite.json.
Múltiples sprites
También puedes proporcionar una matriz de pares { id: ..., url: ... } para
cargar varios sprites:
"sprite": [
{
"id": "roadsigns",
"url": "https://example.com/myroadsigns"
},
{
"id": "shops",
"url": "https://example2.com/someurl"
},
{
"id": "default",
"url": "https://example2.com/anotherurl"
}
]
Como puedes ver, cada sprite tiene un id. Todas las imágenes que contiene un
sprite también tienen un id. Al usar varios sprites, debes anteponer el id de la
imagen con el id del sprite que la contiene, seguido de dos puntos. Por ejemplo,
para referenciar la imagen de la señal de stop (stop_sign) en el sprite de las
señales de tráfico (roadsigns), deberás usar roadsigns:stop_sign.
El sprite con id default tiene la particularidad de no necesitar prefijar las
imágenes que contiene. Por ejemplo, para referenciar la imagen con id aeropuerto
(airport) en el sprite predeterminado anterior, simplemente puedes usar
airport.
Glyphs (fuentes tipográficas)
Los glyphs son archivos binarios (.pbf) que contienen los caracteres de
una fuente. En tipografía, un glifo es una representación gráfica de un
carácter.
La propiedad de glifos de un estilo proporciona una plantilla de URL para cargar conjuntos de glifos de campos de distancia firmados en formato PBF.
"glyphs": "https://geoserveis.icgc.cat/contextmaps/glyphs/{fontstack}/{range}.pbf",
Ejemplo de uso
"layout": {
"text-field": ["get", "name"],
"text-font": ["Open Sans Regular"]
}
Reglas importantes
-
{fontstack}→ nombre exacto de la fuente -
{range}→ rango Unicode (MapLibre lo gestiona automáticamente) -
Si falta un glyph → el texto no se dibuja
Sources
Objeto que define los orígenes de los datos.
Una fuente (source) indica qué datos debe mostrar el mapa. Se debe especifique el tipo de fuente con la propiedad "type". Lo tipos de fuentes deben ser:
| Tipo | Uso |
|---|---|
geojson |
Datos vectoriales simples |
vector |
Tiles vectoriales (MBTiles / tileserver) |
raster |
Teselas raster |
raster-dem |
Modelos de elevación |
image |
Imagen georreferenciada |
video |
Vídeo georreferenciado |
https://maplibre.org/maplibre-style-spec/sources/
Una source define:
- El tipo de datos
- Cómo se cargan
- En qué formato están
Ejemplo
"sources": {
"openmaptiles": {
"type": "vector",
"tiles": ["https://geoserveis.icgc.cat/servei/catalunya/contextmaps_v1/vt/{z}/{x}/{y}.pbf"]
},
"ortoICGC": {
"type": "raster",
"tiles": [
"https://geoserveis.icgc.cat/icc_mapesmultibase/noutm/wmts/orto/GRID3857/{z}/{x}/{y}.jpeg"
],
"tileSize": 256,
"attribution": "<b>Ortofoto Catalunya</b>:<a href=\"https://www.icgc.cat/Aplicacions/Visors/ContextMaps\">Institut Cartogràfic i Geològic de Catalunya</a> |",
"minzoom": 13.1,
"maxzoom": 20
}
}
Las sources no se dibujan. Solo proporcionan datos a las layers.
Layers
Matriz que contiene las reglas de simbolización. El orden dentro de la matriz es importante ya que la forma en que se van dibujando, hace que la primera regla quede por debajo del todo y la última regla quede por encima del todo. De esta manera, la primera regla suele ser el color de fondo del mapa, y las últimas suelen ser la toponimia o los PoIs. El tipo de capa se especifica mediante la propiedad "type" y debe ser de fbackground, fill, line, symbol, raster, circle, fill-extrusion, heatmap, hillshade, sky.
https://maplibre.org/maplibre-style-spec/layers/
Una layer es una instrucción de dibujo: qué geometría usar, de qué fuente, en qué orden y con qué estilo.
Tipos pricipales de layers
| Tipo | Geometría |
|---|---|
circle |
Puntos |
symbol |
Texto e iconos |
line |
Líneas |
fill |
Polígonos |
fill-extrusion |
Polígonos 3D |
heatmap |
Densidad |
background |
Fondo del mapa |
Estructura interna de una layer
{
"id": "ciudades",
"type": "circle",
"source": "ciudades",
"minzoom": 5,
"paint": {
"circle-radius": 5,
"circle-color": "#ff0000"
}
}
Campos clave
-
id→ identificador único -
type→ cómo se renderiza -
source→ de dónde vienen los datos -
source-layer→ solo en tiles vectoriales -
layout→ visibilidad, texto, iconos -
paint→ color, tamaño, opacidad -
filter→ qué features se dibujan
Ejemplo
"layers": [
{
"id": "background",
"type": "background",
"paint": {
"background-color": "#F4F9F4"
}
},
{
"id": "ortoICGC",
"type": "raster",
"source": "ortoICGC",
"minzoom": 13.1,
"maxzoom": 19,
"layout": {
"visibility": "visible"
}
},
{
"id": "park-outline",
"type": "line",
"source": "openmaptiles",
"source-layer": "park",
"filter": [
"==",
"$type",
"Polygon"
],
"layout": {},
"paint": {
"line-color": {
"base": 1,
"stops": [
[
6,
"hsla(96, 40%, 49%, 0.36)"
],
[
8,
"hsla(96, 40%, 49%, 0.66)"
]
]
},
"line-dasharray": [
3,
3
]
}
},
{
"id": "landcover-grass-park",
"type": "fill",
"source": "openmaptiles",
"source-layer": "park",
"filter": [
"==",
"class",
"public_park"
],
"paint": {
"fill-color": "#d8e8c8",
"fill-opacity": 0.8
}
},
...
Note
Las layers se dibujan en el orden del array:
- Las últimas layers tapan a las anteriores
- El orden es cartografía pura, no lógica
MapLibre GL JS no reordena automáticamente.
Otras propiedades
Habitualmente también se usan otras propiedades para indicar la vista inicial del mapa. Por ejemplo:
{
...
"center": [2.15, 41.39],
"zoom": 12,
"bearing": -45,
"pitch": 0
}
Agregar capas y fuentes
Sources (fuentes)
Ya vimos como agregar fuentes de datos en el estilo pero también podemos agregar fuentes de datos al mapa luego de cargar el estilo inicial del mapa.
Para agregar una fuete al mapa se usa el método addSource.
Sintaxis
map.addSource("ID_DE_LA_FUENTE", opciones);
Ejemplo
map.addSource("muncat", {
type: "geojson",
data: "https://raw.githubusercontent.com/geostarters/dades/master/Municipis_Catalunya_EPSG4326.geojson",
});
Note
Si el mapa tiene un estilo ya cargado las fuentes y las capas se deben cargar luego del el evento load del mapa que solo se llama la primera vez que se carga el mapa
map.on('load', function() {
// the rest of the code will go in here
map.addSource('muncat', {
type: 'geojson',
data: 'https://raw.githubusercontent.com/geostarters/dades/master/Municipis_Catalunya_EPSG4326.geojson'
});
});
Layers (capas)
Al igual que las fuentes de datos podemos agregar capas al mapa una vez cargado el estilo inicial del mapa. Para ellos usaremos el método addLayer
Sintaxis
map.addLayer(opciones);
Ejemplo
map.addLayer({
id: "municipis",
type: "fill",
source: "muncat",
paint: {
"fill-color": "#888888",
"fill-opacity": 0.4,
},
filter: ["==", "$type", "Polygon"],
});
Las capas tienen dos subpropiedades que determinan cómo se procesan los datos de esa capa: propiedades de diseño y pintura.
Layout properties: Las propiedades de diseño aparecen en el objeto "layout" de la capa. Definen la estructura y organización de los elements en el mapa, como su visibilidad o la ubicación de etiquetas. Se aplican al principio del proceso de renderizado y definen cómo se pasan los datos de esa capa a la GPU. Los cambios en una propiedad de diseño requieren un paso de "diseño" asincrónico.
Paint properties: Las propiedades de pintura aparecen en el objeto "paint" de la capa. Controlan la apariencia visual de los elementos en el mapa, como colores, opacidad y tamaños. Las propiedades de la pintura se aplican más adelante en el proceso de renderizado. Los cambios en una propiedad de pintura son baratos y ocurren de forma sincrónica.
Expresiones
Una expresión es una fórmula en JSON que calcula un valor dinámico para una propiedad. Se usa en:
- Propiedades de
paintylayout - Propiedades de
filter - Puede combinar operadores matemáticos, lógicos y de acceso a datos.
Sintaxis base:
["operator", argumento1, argumento2, ...]
Note
Las expresiones en MapLibre usan la notación polaca o notación prefija donde el operador precede a los operandos. Ej. + A B en lugar de la notación infija convencional A + B.
Las expresiones se representan como matrices JSON. El primer elemento de una matriz de expresión es una cadena que nombra al operador de expresión, por ejemplo, "get" o "==". Los elementos que siguen (si los hay) son los argumentos de la expresión. Cada argumento es un valor literal (una cadena, un número, booleano o nulo) u otra matriz de expresión.
Los operadores de expresión proporcionados por MapLibre GL incluyen:
- Operadores matemáticos para realizar operaciones aritméticas y otras operaciones con valores numéricos
- Operadores lógicos para manipular valores booleanos y tomar decisiones condicionales
- Operadores de cadenas para manipular cadenas
- Operadores de datos para proporcionar acceso a las propiedades de las características de origen
- Operadores de cámara para proporcionar acceso a los parámetros que definen la vista del mapa actual
https://maplibre.org/maplibre-style-spec/expressions/
Temáticos (data-driven styling)
Ejemplo: Queremos colorear círculos según un atributo provincia.
map.addLayer({
id: "municipis",
type: "fill",
source: "muncat",
paint: {
"fill-color": [
"match",
["get", "provincia"],
"25",
"#ff0000",
"17",
"#00ff00",
"08",
"#0000ff",
"43",
"#ff00ff",
"#ccc",
],
"fill-opacity": 0.4,
},
filter: ["==", "$type", "Polygon"],
});
Filtros
Una expresión que especifica las condiciones de los features. Solo se muestran las features que coinciden con el filtro. Las expresiones de zoom en los filtros solo se evalúan en niveles de zoom enteros
Ejemplo: Mostrar solo los municipios de la provincia de Lleida
map.addLayer({
id: "municipis",
type: "fill",
source: "muncat",
paint: {
"fill-color": [
"match",
["get", "provincia"],
"25",
"#ff0000",
"17",
"#00ff00",
"08",
"#0000ff",
"43",
"#ff00ff",
"#ccc",
],
"fill-opacity": 0.4,
},
filter: [
"all",
["==", "$type", "Polygon"],
["==", ["get", "provincia"], 25],
],
});
Otro ejemplo: Mostrar municipios de Girona y Lleida
'filter': [
'all',
['==', '$type', 'Polygon'],
['in', 'provincia', '25', '17'],
]
Cambiar estilo de elementos en el cliente
Para cambiar el estilo o la visualización de un elemento podemos usar los métodos setPaintProperty o setLayoutProperty dependiendo de que propiedad queremos actualizar.
La sintaxis es igual para los dos casos
map.setPaintProperty("LAYER_ID", "NOMBRE_DE_LA_PROPIEDAD", "VALOR");
map.setLayoutProperty("LAYER_ID", "NOMBRE_DE_LA_PROPIEDAD", "VALOR");
Ejemplo
map.setPaintProperty("my-layer", "fill-color", "#faafee");
map.setLayoutProperty("my-layer", "visibility", "none");
<div>
<input type="color" id="water-color" />
<label for="water-color">Color del agua</label>
</div>
<div>
<input type="checkbox" id="edificios" checked />
<label for="edificios">Edificios</label>
</div>
function cambiaColorAgua(evt) {
const color = evt.target.value;
console.log(color);
map.setPaintProperty("water-ocean", "fill-color", color);
}
function muestraEdificios(evt) {
const isChecked = evt.target.checked;
if (isChecked) {
map.setLayoutProperty("building-residential", "visibility", "visible");
} else {
map.setLayoutProperty("building-residential", "visibility", "none");
}
}
document
.getElementById("water-color")
.addEventListener("change", cambiaColorAgua, false);
document
.getElementById("water-color")
.addEventListener("input", cambiaColorAgua, false);
document
.getElementById("edificios")
.addEventListener("change", muestraEdificios, false);
Cambiar estilo mantener datos
En Mapbox GL no hay el concepto de capas Base y de capas overlay, por el contrario todo pertenece al estilo.
Cuando agregamos una capa al mapa la estamos agregando al estilo por lo tanto si cambiamos de estilo perderemos todas capas que hemos agregado al estilo.
Ejemplo
<style>
#menu {
position: absolute;
z-index: 1000;
background: #fff;
padding: 10px;
font-family: "Open Sans", sans-serif;
top: 5px;
left: 5px;
border-radius: 7px;
-webkit-box-shadow: 5px 5px 5px -5px rgba(0, 0, 0, 0.75);
-moz-box-shadow: 5px 5px 5px -5px rgba(0, 0, 0, 0.75);
box-shadow: 5px 5px 5px -5px rgba(0, 0, 0, 0.75);
}
</style>
<div id="menu">
<input
id="icgc"
type="radio"
name="rtoggle"
value="icgc_mapa_estandard_general"
checked="checked"
/>
<label for="icgc">icgc</label>
<input
id="fulldark"
type="radio"
name="rtoggle"
value="icgc_mapa_base_fosc"
/>
<label for="fulldark">fulldark</label>
</div>
document.getElementsByName("rtoggle").forEach((elem) => {
elem.addEventListener("change", function (event) {
const item = event.target.value;
switchLayer(item);
});
});
function switchLayer(layer) {
map.setStyle("https://geoserveis.icgc.cat/contextmaps/" + layer + ".json");
}
Ahora si cambiamos de estilo vemos que hemos perdido la capa de los municipios. Para evitar esto podemos usar el evento styledata (que se lanzaz cuando el estilo del mapa se carga o cambia) en lugar del evento load. Debido a que la carga del estilo es asyncrona debemos esperar a que el estilo del mapa esté cargado por completo (map.isStyleLoaded()) para luego cargar nuestras capas.
/*
map.on('styledata', () => {
const waiting = () => {
if (!map.isStyleLoaded()) {
setTimeout(waiting, 200);
} else {
cargarDatos();
}
};
waiting();
});
*/
function switchLayer(layer) {
map.once("styledata", () => {
cargarDatos();
});
map.setStyle("https://geoserveis.icgc.cat/contextmaps/" + layer + ".json");
}
function cargarDatos() {
//comprobamos que el source no existe para evitar cargar un mismo source 2 veces que da un mensaje de error
if (!map.getSource("muncat")) {
map.addSource("muncat", {
type: "geojson",
data: "https://raw.githubusercontent.com/geostarters/dades/master/Municipis_Catalunya_EPSG4326.geojson",
});
} else {
const geojsonData = map.getSource("muncat")._data; // Guardar los datos actuales
map.addSource("muncat", {
type: "geojson",
data: geojsonData,
});
}
//la misma comprobación para las capas
if (!map.getLayer("municipis")) {
map.addLayer({
id: "municipis",
type: "fill",
source: "muncat",
paint: {
"fill-color": [
"match",
["get", "provincia"],
"25",
"#ff0000",
"17",
"#00ff00",
"08",
"#0000ff",
"43",
"#ff00ff",
"#ccc",
],
"fill-opacity": 0.4,
},
filter: [
"all",
["==", "$type", "Polygon"],
["in", "provincia", "25", "17"],
],
});
}
}
Eventos
click
Opcionalmente se puede indicar el id de la capa en la que se quiere capturar el evento. Si no se especifica es en todo el mapa.
map.on("click", "municipis", function (e) {
new maplibregl.Popup()
.setLngLat(e.lngLat)
.setHTML(e.features[0].properties.nomn_muni)
.addTo(map);
});
mouseenter & mouseleave
En este caso siempre se debe especificar el id de la capa.
// Change the cursor to a pointer when the mouse is over the layer.
map.on("mouseenter", "municipis", function () {
map.getCanvas().style.cursor = "pointer";
});
// Change it back to a pointer when it leaves.
map.on("mouseleave", "municipis", function () {
map.getCanvas().style.cursor = "";
});
mousemove
Opcionalmente se puede indicar el id de la capa en la que se quiere capturar el evento. Si no se especifica es en todo el mapa.
<style>
#features {
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 50%;
overflow: auto;
background: rgba(255, 255, 255, 0.8);
}
</style>
<pre id="features"></pre>
map.on("mousemove", function (e) {
const features = map.queryRenderedFeatures(e.point);
// Limit the number of properties we're displaying for
// legibility and performance
const displayProperties = [
"type",
"properties",
"id",
"layer",
"source",
"sourceLayer",
"state",
];
const displayFeatures = features.map(function (feat) {
const displayFeat = {};
displayProperties.forEach(function (prop) {
displayFeat[prop] = feat[prop];
});
return displayFeat;
});
document.getElementById("features").innerHTML = JSON.stringify(
displayFeatures,
null,
2,
);
});
Resaltar los municipios de la misma comarca
map.addLayer({
id: "municipis-highlighted",
type: "fill",
source: "muncat",
paint: {
"fill-outline-color": "#484896",
"fill-color": "#6e599f",
"fill-opacity": 0.75,
},
// Display none by adding a
// filter with an empty string.
filter: ["in", "comarca", ""],
});
map.on("mousemove", "municipis", function (e) {
// Use the first found feature.
const feature = e.features[0];
// Query the counties layer visible in the map.
// Use filter to collect only results
// with the same county name.
const relatedFeatures = map.querySourceFeatures("muncat", {
sourceLayer: "municipis",
filter: ["in", "comarca", feature.properties.comarca],
});
console.log(relatedFeatures);
// Add features with the same comarca
// to the highlighted layer.
map.setFilter("municipis-highlighted", [
"in",
"comarca",
feature.properties.comarca,
]);
});
map.on("mouseleave", "municipis", function () {
map.setFilter("municipis-highlighted", ["in", "comarca", ""]);
});
Ejercicio 2.5 pts
- Preparar un mapa interactivo de coropletas. Replicar el resultado final de este tutorial de Leaflet https://leafletjs.com/examples/choropleth/ pero usando MapLibre. (2.5 pt)
Tip: mirar este ejemplo: https://maplibre.org/maplibre-gl-js/docs/examples/create-a-hover-effect/
Más recursos
Tutorial rápido: https://geoinquiets.github.io/vt-hackato-atm/
Tutorial más completo: https://geoinquiets.github.io/taller-vt/