Créer une carte web interactive avec MapLibre

Le pendant MapLibre du tutoriel Leaflet : style, sources et couches, marqueurs, popups, GeoJSON coloré par attribut et carte orientable.

MapLibre GL JS dessine la carte avec la carte graphique (WebGL) : rotation, inclinaison, styles modifiables à la volée, des dizaines de milliers d'objets sans ralentir. Un peu plus de code que Leaflet, mais une fois les trois notions clés comprises (style, sources, couches), tout devient logique. Même programme que le tutoriel Leaflet : fond, marqueurs, popups, GeoJSON et choix des couches.

La page minimale

Créez un fichier carte.html :

<!doctype html>
<html lang="fr">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Ma première carte MapLibre</title>
  <link rel="stylesheet" href="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.css">
  <style>
    html, body, #map { height: 100%; margin: 0; }
  </style>
</head>
<body>
  <div id="map"></div>
  <script src="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.js"></script>
  <script src="app.js"></script>
</body>
</html>

Comme avec Leaflet, le conteneur doit avoir une hauteur définie. Sans la feuille maplibre-gl.css, les contrôles et les popups s'affichent n'importe où.

Initialiser la carte

Avec Leaflet, on ajoute un fond de tuiles. Avec MapLibre, on donne un style : un objet JSON qui liste les sources (d'où viennent les données) et les couches (comment les dessiner, dans quel ordre). Voici l'équivalent du fond OpenStreetMap :

const map = new maplibregl.Map({
  container: 'map',
  center: [4.8357, 45.764],   // [longitude, latitude] — l'inverse de Leaflet !
  zoom: 12,
  style: {
    version: 8,
    sources: {
      osm: {
        type: 'raster',
        tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'],
        tileSize: 256,
        attribution: '&copy; contributeurs OpenStreetMap'
      }
    },
    layers: [{ id: 'fond-osm', type: 'raster', source: 'osm' }]
  }
});

map.addControl(new maplibregl.NavigationControl());               // zoom + boussole
map.addControl(new maplibregl.ScaleControl({ unit: 'metric' }));   // échelle

Vous préférez un vrai fond vectoriel, net à tous les zooms ? Remplacez tout l'objet style par l'adresse d'un style public : style: 'https://tiles.openfreemap.org/styles/liberty'.

Pour tourner la carte : clic droit + glisser (ou deux doigts sur tablette). La boussole du NavigationControl remet le nord en haut.

Marqueurs et popups

// Un marqueur avec sa popup
new maplibregl.Marker({ color: '#8b6cff' })
  .setLngLat([4.8227, 45.7623])
  .setPopup(new maplibregl.Popup().setHTML('<strong>Basilique de Fourvière</strong><br>Construite entre 1872 et 1884'))
  .addTo(map);

// Réagir au clic sur la carte
map.on('click', (e) => {
  new maplibregl.Popup()
    .setLngLat(e.lngLat)
    .setHTML(`Lat ${e.lngLat.lat.toFixed(5)}, Lon ${e.lngLat.lng.toFixed(5)}`)
    .addTo(map);
});

Un Marker est un élément HTML posé sur la carte : parfait pour quelques points. Pour des centaines d'objets, on passe par une source GeoJSON et des couches, dessinées par la carte graphique.

Charger du GeoJSON

Deux étapes : ajouter une source, puis une ou plusieurs couches qui la dessinent. Tout se fait dans l'évènement load, une fois le style chargé :

map.on('load', () => {
  map.addSource('stations', { type: 'geojson', data: 'stations.geojson' });

  map.addLayer({
    id: 'stations',
    type: 'circle',                 // fill (surfaces), line (lignes), circle (points), symbol (textes, icônes)
    source: 'stations',
    paint: {
      // le rayon dépend de l'attribut « velos » : c'est une « expression »
      'circle-radius': ['*', ['sqrt', ['get', 'velos']], 2],
      'circle-color': '#8b6cff',
      'circle-opacity': 0.6,
      'circle-stroke-color': '#ffffff',
      'circle-stroke-width': 1
    }
  });
});

Appeler addSource ou addLayer avant load provoque l'erreur « Style is not done loading ». C'est le piège n°1 de MapLibre (nous l'avons nous-mêmes rencontré en construisant les outils de ce site).

Pour cadrer la carte sur les données, calculez leur emprise :

fetch('stations.geojson').then((r) => r.json()).then((data) => {
  const b = new maplibregl.LngLatBounds();
  data.features.forEach((f) => b.extend(f.geometry.coordinates));   // points
  map.fitBounds(b, { padding: 40, maxZoom: 16 });
});

Popups sur les objets

On écoute le clic sur une couche : l'évènement fournit les objets cliqués et leurs attributs.

map.on('click', 'stations', (e) => {
  const p = e.features[0].properties;
  new maplibregl.Popup()
    .setLngLat(e.lngLat)
    .setHTML(`${p.nom} : ${p.velos} vélos`)
    .addTo(map);
});

// Curseur « main » au survol
map.on('mouseenter', 'stations', () => { map.getCanvas().style.cursor = 'pointer'; });
map.on('mouseleave', 'stations', () => { map.getCanvas().style.cursor = ''; });

Comme avec Leaflet, fetch et l'option data: 'stations.geojson' ne fonctionnent pas en ouvrant le fichier directement (file://) : servez le dossier avec XAMPP, php -S localhost:8000 ou npx serve.

Colorer selon un attribut

Là où Leaflet demande une fonction JavaScript par objet, MapLibre utilise des expressions calculées par la carte graphique :

map.addLayer({
  id: 'batiments',
  type: 'fill',
  source: 'batiments',
  paint: {
    'fill-color': [
      'match', ['get', 'nature'],
      'Maison', '#e0553d',
      'Garage', '#3d8bfd',
      'Jardin', '#2fb673',
      '#999999'                       // toutes les autres valeurs
    ],
    'fill-opacity': 0.5
  }
});

C'est exactement ce que fait la colorisation par attribut de la visionneuse SIG de CartoForge.

Afficher / masquer des couches

MapLibre n'a pas de contrôle de couches tout fait : on change la propriété visibility d'une couche. Quelques lignes suffisent :

<label class="choix"><input type="checkbox" id="voir-stations" checked> Stations</label>
document.getElementById('voir-stations').addEventListener('change', (e) => {
  map.setLayoutProperty('stations', 'visibility', e.target.checked ? 'visible' : 'none');
});

Pour changer de fond, déclarez plusieurs fonds dans le style (chacun dans sa couche raster) et n'en laissez qu'un visible : c'est le principe du bouton 🗂️ des outils de ce site.

Leaflet → MapLibre : les correspondances

LeafletMapLibre
L.map('map').setView([lat, lon], z)new maplibregl.Map({ container, center: [lon, lat], zoom })
L.tileLayer(url).addTo(map)source raster + couche raster dans le style
L.marker([lat, lon])new maplibregl.Marker().setLngLat([lon, lat])
L.geoJSON(data, { style })addSource + addLayer avec expressions
layer.bindPopup()map.on('click', 'couche', …) + Popup
map.fitBounds(layer.getBounds())map.fitBounds(LngLatBounds)
map.removeLayer(layer)setLayoutProperty(id, 'visibility', 'none')
Pas de rotationbearing, pitch, rotateTo()

Leaflet ou MapLibre ?

  • Leaflet : la simplicité. Idéal pour une carte avec quelques centaines d'objets, un fond de tuiles et des popups. Énorme écosystème de plugins.
  • MapLibre : la puissance. Carte orientable, inclinée, en 3D, styles vectoriels, des dizaines de milliers d'objets fluides, étiquettes placées sans chevauchement.

Tous les outils de CartoForge utilisent aujourd'hui MapLibre, par exemple le croquis de terrain, qui a besoin de faire tourner la carte. Pour aller plus loin : 10 outils à intégrer à une carte MapLibre (recherche d'adresse, clusters, mesure, relief 3D…) et les tuiles vectorielles.

Démonstration

Le code de ce tutoriel, assemblé et en fonctionnement (les stations sont fictives) :

À essayer : cliquez une station (taille selon le nombre de vélos, couleur selon le type), cliquez ailleurs pour lire les coordonnées, masquez les stations ou passez au Plan IGN dans le panneau, et faites tourner la carte (clic droit + glisser, ou deux doigts) ; la boussole remet le nord en haut.