Introduction
Toute la chaîne est en place : les données sont propres dans PostGIS et publiées par GeoServer en GeoJSON. Reste l’étape que verra l’utilisateur final : une carte interactive dans le navigateur, qu’on peut déplacer, zoomer, et où chaque objet réagit au clic. MapLibre GL JS est la bibliothèque qui rend cela possible. C’est un projet open source, né d’un fork de Mapbox GL JS au moment où ce dernier est passé sous licence propriétaire, et il ne demande aucune clé d’API ni abonnement pour fonctionner — un argument décisif quand on veut maîtriser ses coûts.
À la fin de ce tutoriel, vous aurez une page web autonome affichant vos communes colorées selon leur densité d’écoles, vos écoles en points cliquables, et des fenêtres d’information au clic. Le tout alimenté en direct par les services GeoServer construits à l’étape précédente, sans aucune donnée codée en dur.
🎯 Ce que vous allez apprendre
- Initialiser une carte MapLibre GL JS depuis un simple fichier HTML.
- Comprendre pourquoi le web cartographique parle en longitude/latitude et reprojeter vos services en conséquence.
- Charger des données GeoJSON distantes (depuis GeoServer) comme sources de carte.
- Styliser des couches par les données elles-mêmes : couleur selon une valeur d’attribut.
- Rendre les objets interactifs avec des fenêtres d’information au clic, et servir la page proprement.
🛠️ Ce que vous allez construire
Une page web cartographique unique (un fichier HTML) : un fond de carte, vos communes en aplats colorés du clair au foncé selon le nombre d’écoles, vos écoles en cercles colorés selon leur type, et un clic qui ouvre une bulle d’information. C’est l’interface visible du portail territorial bâti tout au long de la série.
Prérequis
- Un GeoServer publiant vos couches en WFS (voir le tutoriel précédent de la série).
- Un éditeur de texte et des bases de HTML et de JavaScript.
- Python installé (pour lancer un petit serveur web local) — ou tout autre serveur statique.
- MapLibre GL JS 5.24, chargé depuis un CDN, aucune installation requise.
- Test express : si vous savez écrire une fonction JavaScript et ouvrir la console du navigateur, vous êtes prêt.
- ⏱️ Temps estimé : ~50 minutes.
Étape 1 — Le squelette HTML
MapLibre s’intègre par deux fichiers servis depuis un CDN : une feuille de style pour l’apparence des contrôles, et le script de la bibliothèque. La carte a besoin d’un conteneur HTML auquel on donne une taille explicite : sans hauteur définie, le conteneur fait zéro pixel de haut et la carte reste invisible — l’erreur de débutant la plus universelle.
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>Portail territorial</title>
<link rel="stylesheet" href="https://unpkg.com/maplibre-gl@5.24.0/dist/maplibre-gl.css">
<script src="https://unpkg.com/maplibre-gl@5.24.0/dist/maplibre-gl.js"></script>
<style> html, body, #map { margin:0; height:100%; } </style>
</head>
<body>
<div id="map"></div>
<script src="app.js"></script>
</body>
</html>
La règle CSS donne au conteneur #map 100 % de la hauteur de la page. Le script app.js, qu’on remplit aux étapes suivantes, contiendra toute la logique de la carte. Enregistrez ce fichier sous index.html.
✅ Point d’étape — Vous avez un fichier HTML qui charge MapLibre et réserve un conteneur de carte plein écran.
Étape 2 — Initialiser la carte
On crée maintenant l’objet carte. Il faut lui fournir un style : la description du fond de carte. MapLibre fournit un style de démonstration gratuit, parfait pour démarrer. Le centre s’exprime en coordonnées longitude puis latitude — dans cet ordre, contre-intuitif quand on a l’habitude de dire « latitude, longitude ».
// app.js
const map = new maplibregl.Map({
container: 'map',
style: 'https://demotiles.maplibre.org/style.json',
center: [-17.45, 14.69], // [longitude, latitude]
zoom: 11
});
map.addControl(new maplibregl.NavigationControl());
Ouvrez la page (voir l’étape 8 pour la servir correctement) : la carte s’affiche, centrée sur votre zone, avec des boutons de zoom ajoutés par NavigationControl. Le style de démonstration ne contient que les contours des pays jusqu’au niveau de zoom 6 ; à un zoom de ville comme ici, son fond apparaît donc quasi vide — c’est normal, ce sont vos données, ajoutées à l’étape suivante, qui rempliront la vue. Si rien ne s’affiche du tout, ouvrez la console du navigateur : une erreur sur le conteneur signale presque toujours une hauteur CSS oubliée.
✅ Point d’étape — Une carte interactive s’affiche, centrée sur votre zone, déplaçable et zoomable.
Étape 3 — Le pont entre projections : servir en longitude/latitude
Voici le point conceptuel qui bloque le plus de débutants. MapLibre, comme toutes les cartes web, raisonne en longitude/latitude (le système WGS84, EPSG:4326) qu’il projette à l’écran en Web Mercator. Or vos données dans PostGIS sont en UTM 28N (EPSG:32628), un système en mètres. Si vous envoyez à MapLibre des coordonnées en mètres, il les interprétera comme des degrés et placera vos objets quelque part près du méridien d’origine, au large de l’Afrique.
La solution est propre et tient en un paramètre : demander à GeoServer de reprojeter à la volée en sortie. Le service WFS accepte un paramètre srsName qui force le système de coordonnées renvoyé.
http://localhost:8080/geoserver/portail/ows?service=WFS&version=2.0.0
&request=GetFeature&typeNames=portail:communes
&outputFormat=application/json&srsName=EPSG:4326
Avec srsName=EPSG:4326, le GeoJSON renvoyé contient des longitudes et latitudes, directement consommables par MapLibre. Aucune reprojection côté navigateur n’est nécessaire — GeoServer fait le travail. Retenez ce paramètre : il est la clé de voûte de l’intégration.
✅ Point d’étape — Vous savez construire une URL WFS qui renvoie du GeoJSON en longitude/latitude.
Étape 4 — Afficher les communes
On ajoute les données comme une source, puis on dessine cette source via une ou plusieurs couches. Cette séparation est au cœur de MapLibre : une même source peut nourrir plusieurs couches (un remplissage et un contour, par exemple). Tout ajout doit attendre l’événement load, sans quoi la carte n’est pas prête à recevoir des couches.
const WFS = 'http://localhost:8080/geoserver/portail/ows?service=WFS'
+ '&version=2.0.0&request=GetFeature&outputFormat=application/json'
+ '&srsName=EPSG:4326&typeNames=';
map.on('load', function () {
map.addSource('communes', { type: 'geojson', data: WFS + 'portail:communes' });
map.addLayer({
id: 'communes-fill',
type: 'fill',
source: 'communes',
paint: {
'fill-color': ['interpolate', ['linear'], ['get', 'nb_ecoles'],
0, '#edf8fb', 5, '#66c2a4', 15, '#005824'],
'fill-opacity': 0.7
}
});
});
La propriété fill-color n’est pas une couleur fixe mais une expression : MapLibre lit l’attribut nb_ecoles de chaque commune et interpole une couleur entre le clair (peu d’écoles) et le foncé (beaucoup). C’est le style piloté par les données — la carte choroplèthe se construit côté navigateur, sans image pré-calculée.
✅ Point d’étape — Vos communes s’affichent en aplats colorés selon leur densité d’écoles.
Étape 5 — Ajouter les contours
Les aplats seuls manquent de lisibilité : les limites entre communes se devinent mal. On ajoute une seconde couche, de type ligne, branchée sur la même source. C’est l’avantage de la séparation source/couche : aucune donnée n’est rechargée.
map.addLayer({
id: 'communes-contour',
type: 'line',
source: 'communes',
paint: { 'line-color': '#08519c', 'line-width': 1 }
});
Un trait bleu foncé souligne désormais chaque commune. L’ordre d’ajout compte : une couche ajoutée après une autre se dessine au-dessus, donc le contour passe par-dessus le remplissage, comme voulu.
✅ Point d’étape — Les limites de communes sont nettes par-dessus les aplats colorés.
Étape 6 — Afficher les écoles en points
On ajoute la couche d’écoles, cette fois en cercles, avec une couleur qui dépend du type d’établissement. L’expression match joue le rôle d’un aiguillage : à chaque valeur du champ type correspond une couleur, avec une couleur par défaut pour les cas non prévus.
map.addSource('ecoles', { type: 'geojson', data: WFS + 'portail:ecoles' });
map.addLayer({
id: 'ecoles-pts',
type: 'circle',
source: 'ecoles',
paint: {
'circle-radius': 6,
'circle-stroke-width': 1,
'circle-stroke-color': '#ffffff',
'circle-color': ['match', ['get', 'type'],
'publique', '#1f78b4',
'prive', '#e31a1c',
'#999999']
}
});
Les écoles publiques apparaissent en bleu, les privées en rouge, le reste en gris. Le contour blanc de chaque cercle les détache du fond coloré. Placez bien ces lignes à l’intérieur du map.on('load', ...), après les communes.
✅ Point d’étape — Les écoles s’affichent en cercles colorés par type, par-dessus les communes.
Étape 7 — Rendre les points cliquables
Une carte devient utile quand on peut interroger ses objets. On écoute l’événement click sur la couche des écoles : MapLibre fournit alors l’entité cliquée, dont on lit les attributs pour les afficher dans une bulle (Popup). On change aussi le curseur en main au survol pour signaler l’interactivité.
map.on('click', 'ecoles-pts', function (e) {
const p = e.features[0].properties;
new maplibregl.Popup()
.setLngLat(e.lngLat)
.setHTML('<strong>' + p.nom + '</strong><br>Type : ' + p.type)
.addTo(map);
});
map.on('mouseenter', 'ecoles-pts', function () {
map.getCanvas().style.cursor = 'pointer';
});
map.on('mouseleave', 'ecoles-pts', function () {
map.getCanvas().style.cursor = '';
});
Au clic sur une école, une bulle affiche son nom et son type ; le curseur devient une main au survol. On construit le contenu HTML par simple concaténation des attributs lus dans e.features[0].properties — les mêmes colonnes que dans votre table PostGIS, transportées intactes tout au long de la chaîne.
✅ Point d’étape — Cliquer sur une école ouvre une bulle d’information avec ses attributs.
Étape 8 — Servir la page et autoriser le partage entre origines
Ouvrir index.html directement par un double-clic (protocole file://) empêche le navigateur de charger des données distantes par sécurité. Il faut servir la page par HTTP. Le plus simple, sans rien installer de plus, est le serveur intégré de Python.
# Dans le dossier du projet
python -m http.server 8000
# Puis ouvrir http://localhost:8000 dans le navigateur
Reste un dernier obstacle : votre page (sur le port 8000) demande des données à GeoServer (sur le port 8080). Le navigateur considère ces deux origines comme distinctes et bloque la requête tant que GeoServer n’autorise pas explicitement le partage entre origines (CORS). GeoServer embarque un filtre CORS qu’il suffit d’activer dans son fichier web.xml (les sections sont présentes mais commentées par défaut), puis de redémarrer. En production, on évite ce problème en plaçant la page et les services derrière le même domaine via un proxy inverse.
✅ Point d’étape final — La page est servie en HTTP, GeoServer autorise les requêtes, et la carte affiche vos données en direct. Le portail est complet.
GeoJSON ou vraies tuiles vectorielles ?
Jusqu’ici, la carte charge ses données en GeoJSON : un seul gros fichier que le navigateur télécharge en entier. C’est parfait pour quelques centaines d’objets, mais cela s’effondre dès qu’on dépasse plusieurs milliers d’entités — le téléchargement devient lourd et le rendu saccade. La solution professionnelle est la tuile vectorielle (format MVT, pour Mapbox Vector Tile) : au lieu d’un fichier unique, le serveur découpe les données en petits carreaux par niveau de zoom, et le navigateur ne charge que les carreaux visibles à l’écran, déjà simplifiés pour le zoom courant.
La bonne nouvelle : on ne change presque rien au code. GeoServer sait servir des tuiles vectorielles, et MapLibre les consomme via une source de type vector au lieu de geojson, en pointant vers un gabarit d’URL contenant les variables de tuile {z}/{x}/{y}. Les couches fill, line et circle, ainsi que les expressions de style pilotées par les données, restent identiques — il faut seulement préciser le nom de la couche source dans la définition, car une tuile vectorielle peut en contenir plusieurs. Commencez en GeoJSON pour apprendre, basculez en tuiles vectorielles quand le volume l’exige : c’est la trajectoire naturelle d’un projet qui grandit.
🐞 Pièges fréquents
| Symptôme / erreur | Cause probable | Correctif |
|---|---|---|
| Carte invisible (zone grise) | Conteneur sans hauteur CSS | Donner une hauteur explicite à #map |
| Données absentes, erreur CORS en console | GeoServer n’autorise pas l’origine | Activer le filtre CORS dans web.xml, ou passer par un proxy |
| Objets placés au large de l’Afrique | Données envoyées en mètres (UTM) | Ajouter srsName=EPSG:4326 à la requête WFS |
| « Cannot read properties of undefined » | addLayer appelé avant l’événement load | Tout placer dans map.on('load', ...) |
| Couleurs uniformes malgré l’expression | Nom d’attribut erroné dans ['get', ...] |
Vérifier le nom exact de la colonne dans le GeoJSON |
🌍 Adaptation au contexte local
MapLibre ne facture rien et ne réclame aucune clé, contrairement aux services cartographiques commerciaux dont les quotas se transforment vite en factures en devises. Le fond de carte de démonstration suffit pour apprendre ; pour la production, hébergez vos propres tuiles — la pile Protomaps permet de servir un fond de carte issu d’OpenStreetMap depuis un simple fichier, sans serveur de tuiles dédié, ce qui réduit les coûts à presque rien. Côté performance, MapLibre rend les vecteurs directement dans le navigateur via l’accélération graphique : même sur un ordinateur d’entrée de gamme, l’affichage reste fluide tant que les volumes restent raisonnables. Pour de grandes quantités d’objets, préférez le WMS de GeoServer (des images) au WFS (des vecteurs), ou passez par des tuiles vectorielles afin de ménager la bande passante.
✅ Récapitulatif
Vous avez construit, à partir d’un fichier HTML et d’un fichier JavaScript, une carte web interactive complète : initialisation de MapLibre, compréhension du pont entre projections, chargement des données GeoServer en GeoJSON, style piloté par les attributs, points interactifs avec bulles d’information, et mise en service propre avec gestion du partage entre origines. Cette page est l’aboutissement visible de toute la chaîne : la donnée préparée dans QGIS, stockée et routée dans PostGIS, publiée par GeoServer, s’affiche enfin entre les mains de l’utilisateur, sans aucune licence à payer.
🧾 Aide-mémoire
| Tâche | Code |
|---|---|
| Charger MapLibre | CSS + JS depuis unpkg.com/maplibre-gl@5.24.0 |
| Créer la carte | new maplibregl.Map({container, style, center:[lng,lat], zoom}) |
| Données en lng/lat | WFS avec srsName=EPSG:4326 |
| Ajouter une source | map.addSource('id', {type:'geojson', data:url}) |
| Dessiner | map.addLayer({id, type:'fill'|'line'|'circle', source, paint}) |
| Style par donnée | ['interpolate', ...] ou ['match', ['get','champ'], ...] |
| Bulle au clic | map.on('click', 'couche', e => new maplibregl.Popup()...) |
💪 À vous de jouer
Premier défi : ajoutez une légende HTML fixe expliquant l’échelle de couleurs des communes et la signification des cercles. Second défi : branchez la fonction d’itinéraire PostGIS construite précédemment — au clic sur deux écoles successives, affichez le trajet renvoyé en GeoJSON comme une nouvelle couche ligne.
Voir une piste de solution
Pour la légende : ajoutez une div positionnée en absolu par-dessus la carte, avec des pastilles de couleur correspondant à vos paliers d’interpolation. Pour l’itinéraire : exposez la fonction itineraire(...) via une petite API (ou un service GeoServer SQL view), récupérez son GeoJSON par fetch, puis map.addSource('trajet', {type:'geojson', data:reponse}) suivi d’une couche line bien visible.
Tutoriels frères
- GeoServer : publier des couches WMS et WFS depuis PostGIS — la source des données affichées ici.
- QGIS : importer, styliser et analyser des données spatiales — préparer la donnée en amont.
Pour aller plus loin
- 🔝 Retour au guide principal : La chaîne géospatiale open source
- Documentation officielle : maplibre.org/maplibre-gl-js/docs (référence du style et exemples)
- Pour calculer des trajets à afficher : le tutoriel pgRouting de la série.
FAQ
Q : MapLibre ou Leaflet ?
R : Leaflet excelle avec des tuiles raster et reste très simple. MapLibre brille avec les tuiles vectorielles et le style piloté par les données, rendu en accélération graphique : couleurs dynamiques, rotation, inclinaison. Pour une carte choroplèthe stylée côté client comme ici, MapLibre est plus adapté.
Q : Pourquoi mes coordonnées sont-elles inversées par rapport à d’habitude ?
R : MapLibre attend l’ordre longitude puis latitude (X, Y), alors qu’on énonce souvent « latitude, longitude » à l’oral. C’est une source d’erreur classique : un point qui apparaît dans le mauvais hémisphère vient presque toujours de cette inversion.
Q : Faut-il un serveur pour héberger la page ?
R : Pour le développement, le serveur intégré de Python suffit. En production, n’importe quel hébergement statique convient (la page est de simples fichiers), pourvu que les services GeoServer soient accessibles et que le partage entre origines soit réglé, idéalement via un même domaine.
Q : MapLibre a-t-il besoin d’une clé d’API ?
R : Non. C’est tout son intérêt face à des solutions propriétaires : la bibliothèque est libre et gratuite. Seul le fournisseur de tuiles de fond peut imposer ses conditions ; en hébergeant vos propres tuiles, vous êtes totalement autonome.