Développement Web

PostGIS et pgRouting : calculer des itinéraires sur un réseau routier

16 min de lecture
📍 Guide principal : La chaîne géospatiale open source : QGIS, PostGIS, GeoServer, MapLibre. Ce tutoriel ajoute le calcul d’itinéraires au cœur de la base de données.

Introduction

Stocker des points et mesurer une distance à vol d’oiseau dans PostGIS est une chose ; calculer le trajet réel qu’emprunte un livreur sur un réseau de rues en est une autre. La distance directe entre deux points dit « 800 mètres », mais la route, elle, fait peut-être 2,3 kilomètres parce qu’il faut contourner un marché, suivre les sens uniques et franchir le seul pont disponible. C’est exactement le problème que résout pgRouting : une extension qui transforme une couche de routes PostGIS en graphe navigable et calcule le plus court chemin entre deux points.

Si vous débutez avec PostGIS — installation, stockage de points, recherche par distance, index spatial — commencez par notre tutoriel PostGIS pour la cartographie, qui pose ces fondations. Le présent tutoriel suppose ces bases acquises et va plus loin : construire une topologie de réseau et router dessus. À la fin, vous saurez calculer un itinéraire optimal sur un vrai réseau routier et le restituer en GeoJSON, prêt à être affiché sur une carte web.

🎯 Ce que vous allez apprendre

  • Activer pgRouting au-dessus de PostGIS et comprendre ce qu’est une topologie de graphe.
  • Importer un réseau routier dans PostgreSQL et le préparer (colonnes source, target, cost).
  • Construire la topologie avec pgr_createTopology et la diagnostiquer avec pgr_analyzeGraph.
  • Trouver le nœud du graphe le plus proche d’un point quelconque grâce à l’opérateur de plus proche voisin.
  • Calculer le plus court chemin avec pgr_dijkstra, en reconstituer la géométrie et la longueur, puis l’exporter en GeoJSON.

🛠️ Ce que vous allez construire

Un moteur d’itinéraire minimal côté base de données : à partir de deux paires de coordonnées (un départ, une arrivée), une requête SQL renvoie la ligne du trajet optimal et sa distance totale. C’est la brique qui, branchée à une interface, permet de répondre « voici le chemin le plus court entre ces deux adresses ».

Prérequis

  • PostgreSQL 16 ou 17 avec PostGIS 3.6 installé (PostGIS exige PostgreSQL 12 à 18, GEOS 3.8+ et PROJ 6.1+).
  • pgRouting 4.0.1 (la version stable au moment d’écrire ; elle requiert PostgreSQL 13 ou supérieur).
  • Un client SQL : psql en ligne de commande, ou pgAdmin / DBeaver en interface.
  • Une couche de routes au format LineString — par exemple le GeoPackage produit dans le tutoriel QGIS, ou un export OpenStreetMap.
  • Test express : si vous savez écrire un SELECT ... WHERE et activer une extension PostgreSQL, vous êtes prêt.
  • ⏱️ Temps estimé : ~60 minutes.

Étape 1 — Activer PostGIS et pgRouting

pgRouting ne fonctionne pas seul : il s’appuie sur les types géométriques de PostGIS. On active donc les deux extensions dans la base de travail, dans cet ordre. Les extensions sont des modules livrés avec le serveur ; CREATE EXTENSION les rend disponibles dans la base courante uniquement, pas pour tout le cluster.

-- Connecté à la base "gis" (psql -d gis)
CREATE EXTENSION IF NOT EXISTS postgis;
CREATE EXTENSION IF NOT EXISTS pgrouting;

-- Vérifier les versions installées
SELECT postgis_full_version();
SELECT pgr_version();

La requête pgr_version() doit renvoyer une chaîne de la forme « 4.0.1 ». Si CREATE EXTENSION pgrouting échoue avec « extension introuvable », c’est que le paquet système de pgRouting n’est pas installé : sur Debian/Ubuntu, ajoutez postgresql-NN-pgrouting (où NN est la version majeure de PostgreSQL) puis recommencez.

Point d’étapepgr_version() renvoie un numéro de version. Les deux extensions sont actives dans votre base.

Étape 2 — Importer le réseau routier

pgRouting a besoin d’un graphe : des arêtes (les tronçons de route) reliées par des nœuds (les intersections). On part d’une couche de lignes. L’outil ogr2ogr, livré avec GDAL et présent dans toute installation QGIS, importe presque n’importe quel format vers PostGIS en une commande.

# Importer le GeoPackage de routes dans la table "routes", en UTM 28N (mètres)
ogr2ogr -f PostgreSQL "PG:dbname=gis user=postgres" routes.gpkg   -nln routes -nlt LINESTRING   -lco GEOMETRY_NAME=geom -lco FID=id   -t_srs EPSG:32628

L’option -t_srs EPSG:32628 reprojette en UTM zone 28N : un système métrique, indispensable pour que les longueurs servant de coût soient exprimées en mètres. -nln routes nomme la table, -nlt LINESTRING force le type ligne, et GEOMETRY_NAME=geom nomme la colonne géométrique. Après import, vérifiez le nombre de tronçons chargés :

SELECT count(*) AS nb_troncons, ST_SRID(geom) AS srid FROM routes;

Le SRID renvoyé doit être 32628. Un SRID à 0 signifie que la projection n’a pas été appliquée — pgRouting refusera ensuite de construire la topologie. Dans ce cas, réimportez en vérifiant l’option -t_srs.

Point d’étape — La table routes contient vos tronçons avec un SRID 32628 et une colonne geom.

Étape 3 — Préparer les colonnes du graphe

Un graphe routier exige trois informations par arête : son nœud de départ (source), son nœud d’arrivée (target) et son coût (cost). Les colonnes source et target seront remplies automatiquement à l’étape suivante, mais elles doivent d’abord exister. Le coût, lui, est ce que l’algorithme minimise : le plus naturel est la longueur du tronçon en mètres.

ALTER TABLE routes ADD COLUMN IF NOT EXISTS source integer;
ALTER TABLE routes ADD COLUMN IF NOT EXISTS target integer;
ALTER TABLE routes ADD COLUMN IF NOT EXISTS cost double precision;
ALTER TABLE routes ADD COLUMN IF NOT EXISTS reverse_cost double precision;

-- Le coût = longueur en mètres (CRS métrique oblige)
UPDATE routes SET cost = ST_Length(geom);
-- Réseau bidirectionnel par défaut : même coût dans les deux sens
UPDATE routes SET reverse_cost = ST_Length(geom);

La colonne reverse_cost gère les sens de circulation : un coût positif autorise le passage dans le sens inverse, une valeur négative l’interdit. En la fixant égale à cost, on déclare un réseau entièrement bidirectionnel — un point de départ simple, qu’on raffinera plus tard pour les sens uniques.

Point d’étape — Les colonnes source, target, cost et reverse_cost existent ; cost et reverse_cost sont remplies avec la longueur des tronçons.

Étape 4 — Construire la topologie

C’est l’étape qui transforme une simple collection de lignes en graphe navigable. pgr_createTopology parcourt tous les tronçons, identifie les extrémités qui coïncident (à une tolérance près), leur attribue un identifiant de nœud commun, et remplit les colonnes source et target. Sans cette étape, pgRouting ne voit que des segments isolés et ne peut router.

SELECT pgr_createTopology('routes', 0.0001, 'geom', 'id');

Les paramètres, dans l’ordre : le nom de la table, la tolérance (en unités de projection — ici en mètres, donc 0,0001 m, soit un dixième de millimètre, pour ne fusionner que des extrémités vraiment confondues), le nom de la colonne géométrique, et le nom de la clé primaire. La fonction renvoie OK quand elle réussit et crée au passage une table routes_vertices_pgr contenant tous les nœuds du réseau avec leur géométrie ponctuelle.

Si vos données comportent des tronçons qui se croisent sans nœud commun (un pont au-dessus d’une route, par exemple, ou de petits écarts de numérisation), augmentez prudemment la tolérance — mais une tolérance trop large fusionnera des intersections distinctes et faussera les trajets. En pratique, sur des données bien numérisées en mètres, une valeur entre 0,0001 et 1 convient.

Point d’étapepgr_createTopology renvoie OK, les colonnes source/target sont remplies, et la table routes_vertices_pgr existe.

Étape 5 — Diagnostiquer le graphe

Un réseau qui paraît correct à l’œil peut être truffé d’impasses involontaires et de composantes déconnectées — des îlots de rues qu’aucun chemin ne relie au reste. Router entre deux îlots renvoie alors « pas de chemin ». La fonction pgr_analyzeGraph dresse ce bilan avant qu’il ne gâche vos requêtes.

SELECT pgr_analyzeGraph('routes', 0.0001, 'geom', 'id');

-- Repérer les nœuds isolés (impasses) : cnt = 1 dans la table des sommets
SELECT count(*) AS impasses FROM routes_vertices_pgr WHERE cnt = 1;

Le rapport indique le nombre d’arêtes isolées, d’extrémités mortes (dead ends) et de problèmes potentiels. Quelques impasses sont normales (vraies rues sans issue) ; un grand nombre de composantes déconnectées révèle des trous dans la numérisation à corriger dans QGIS avant d’aller plus loin. Tant que le diagnostic n’est pas propre, les itinéraires longue distance échoueront silencieusement.

Point d’étape — Vous connaissez l’état de connexité de votre réseau et avez identifié d’éventuelles zones isolées.

Étape 6 — Trouver les nœuds de départ et d’arrivée

pgr_dijkstra ne raisonne pas en coordonnées mais en identifiants de nœuds. Or l’utilisateur fournit des coordonnées (un clic sur la carte, une adresse géocodée). Il faut donc, pour chaque point, trouver le nœud du graphe le plus proche. L’opérateur de plus proche voisin <-> de PostGIS, accéléré par l’index spatial, fait cela très efficacement.

-- Nœud le plus proche d'un point de départ (coordonnées en UTM 28N)
SELECT id
FROM routes_vertices_pgr
ORDER BY the_geom <-> ST_SetSRID(ST_MakePoint(264500, 1627800), 32628)
LIMIT 1;

La requête trie tous les sommets par distance croissante au point fourni et ne garde que le premier. Grâce à l’index sur the_geom (créé automatiquement par pgr_createTopology), l’opérateur <-> n’examine pas toute la table mais seulement les candidats proches. Notez l’identifiant renvoyé pour le départ, puis répétez avec les coordonnées d’arrivée. Si vos points sont en degrés (EPSG:4326), reprojetez-les d’abord avec ST_Transform(..., 32628).

Point d’étape — Vous avez deux identifiants de nœuds : un départ et une arrivée, tous deux ancrés sur le réseau.

Étape 7 — Calculer l’itinéraire avec pgr_dijkstra

Tout est en place pour le cœur du sujet. pgr_dijkstra prend une requête décrivant les arêtes, un nœud de départ, un nœud d’arrivée, et un drapeau indiquant si le réseau est orienté. Elle renvoie la séquence ordonnée des arêtes du plus court chemin, avec le coût cumulé.

SELECT seq, node, edge, cost, agg_cost
FROM pgr_dijkstra(
  'SELECT id, source, target, cost, reverse_cost FROM routes',
  100,   -- nœud de départ (résultat de l'étape 6)
  742,   -- nœud d'arrivée
  directed := true
);

La requête interne doit impérativement renvoyer les colonnes id, source, target, cost, reverse_cost — ce sont les noms qu’attend pgRouting. La sortie comporte une ligne par étape du trajet : seq et path_seq donnent l’ordre, node le nœud traversé, edge l’arête empruntée pour atteindre le suivant (la dernière ligne porte edge = -1), cost le coût du tronçon et agg_cost le coût cumulé depuis le départ. La toute dernière valeur d’agg_cost est donc la distance totale du trajet, en mètres.

Mais une liste d’identifiants d’arêtes ne se dessine pas. On rejoint le résultat à la table routes pour récupérer la géométrie de chaque tronçon et la fusionner en une seule ligne continue :

SELECT ST_AsGeoJSON(ST_LineMerge(ST_Union(r.geom))) AS itineraire,
       round(SUM(r.cost)::numeric, 1) AS metres
FROM pgr_dijkstra(
  'SELECT id, source, target, cost, reverse_cost FROM routes',
  100, 742, directed := true
) AS d
JOIN routes r ON r.id = d.edge
WHERE d.edge != -1;

Le filtre edge != -1 écarte la ligne finale qui ne porte pas d’arête. ST_Union regroupe les tronçons, ST_LineMerge les recoud en une polyligne ordonnée, et ST_AsGeoJSON produit la géométrie directement consommable par une carte web. La colonne metres donne la longueur réelle du trajet.

Point d’étape final — Une seule requête renvoie le tracé de l’itinéraire en GeoJSON et sa distance en mètres. Vous avez un moteur de routage fonctionnel.

Étape 8 — Emballer dans une fonction réutilisable

Recopier cette requête à chaque appel est fragile. On l’encapsule dans une fonction SQL qui prend quatre coordonnées (départ et arrivée) et renvoie l’itinéraire. C’est cette fonction qu’appellera plus tard une API ou un service cartographique.

CREATE OR REPLACE FUNCTION itineraire(x1 float, y1 float, x2 float, y2 float)
RETURNS TABLE(geojson text, metres numeric) AS $func$
  WITH d AS (SELECT (SELECT id FROM routes_vertices_pgr
                     ORDER BY the_geom <-> ST_SetSRID(ST_MakePoint(x1,y1),32628) LIMIT 1) AS s,
                    (SELECT id FROM routes_vertices_pgr
                     ORDER BY the_geom <-> ST_SetSRID(ST_MakePoint(x2,y2),32628) LIMIT 1) AS t)
  SELECT ST_AsGeoJSON(ST_LineMerge(ST_Union(r.geom))),
         round(SUM(r.cost)::numeric, 1)
  FROM d, pgr_dijkstra('SELECT id, source, target, cost, reverse_cost FROM routes',
                       (SELECT s FROM d), (SELECT t FROM d), true) AS p
  JOIN routes r ON r.id = p.edge
  WHERE p.edge != -1;
$func$ LANGUAGE sql STABLE;

-- Appel : itinéraire entre deux paires de coordonnées UTM
SELECT * FROM itineraire(264500, 1627800, 267200, 1630100);

La fonction calcule les nœuds proches en interne, route, et renvoie le GeoJSON et la distance en une ligne. Déclarée STABLE, elle peut être appelée en lecture dans une transaction sans effet de bord. C’est la surface d’API minimale du moteur de routage.

🐞 Pièges fréquents

Symptôme / erreur Cause probable Correctif
« Could not determine the SRID » Géométrie sans SRID (SRID = 0) Reprojeter à l’import (-t_srs) ou UPDATE routes SET geom = ST_SetSRID(geom, 32628)
pgr_dijkstra renvoie zéro ligne Départ et arrivée sur des composantes déconnectées Lancer pgr_analyzeGraph, recoudre le réseau dans QGIS
Trajet anormalement long ou faux Tolérance trop grande à la topologie (intersections fusionnées à tort) Reconstruire la topologie avec une tolérance plus fine
Distances en valeurs minuscules Réseau en degrés (EPSG:4326) Reprojeter en UTM métrique avant de calculer cost
« column source does not exist » Colonnes du graphe non créées avant la topologie Exécuter les ALTER TABLE de l’étape 3 d’abord

🌍 Adaptation au contexte local

Le réseau routier d’OpenStreetMap couvre désormais très bien les grandes villes du continent et constitue une source gratuite et téléchargeable de tronçons exploitables. Extrayez la zone qui vous intéresse via l’extension QuickOSM de QGIS, ne gardez que les routes carrossables, exportez en GeoPackage et suivez ce tutoriel. Côté hébergement, pgRouting tourne sur un VPS modeste : un serveur à 2 vCPU et 4 Go de RAM gère sans peine le réseau d’une ville moyenne, pour un coût mensuel réglable en mobile money chez plusieurs hébergeurs régionaux. Pour le coût des arêtes, pensez au-delà de la distance : pondérer par un temps de parcours (longueur divisée par une vitesse selon le type de route) reflète mieux la réalité, où une voie rapide bat un raccourci de ruelles encombrées.

✅ Récapitulatif

Vous avez activé pgRouting sur PostGIS, importé un réseau routier en projection métrique, préparé ses colonnes de graphe, construit puis diagnostiqué la topologie, et calculé un plus court chemin avec pgr_dijkstra. Surtout, vous savez reconstituer la géométrie du trajet et sa longueur, et encapsuler le tout dans une fonction appelable. Le moteur de routage que vous venez de bâtir vit entièrement dans la base de données — il ne reste qu’à l’exposer, ce que feront les tutoriels suivants via un service cartographique puis une carte web.

🧾 Aide-mémoire

Tâche Commande SQL
Activer les extensions CREATE EXTENSION postgis; CREATE EXTENSION pgrouting;
Construire la topologie pgr_createTopology('routes', 0.0001, 'geom', 'id')
Diagnostiquer pgr_analyzeGraph('routes', 0.0001, 'geom', 'id')
Nœud le plus proche ORDER BY the_geom <-> ST_SetSRID(ST_MakePoint(x,y),32628) LIMIT 1
Plus court chemin pgr_dijkstra('SELECT id,source,target,cost,reverse_cost FROM routes', dep, arr, true)
Géométrie du trajet ST_AsGeoJSON(ST_LineMerge(ST_Union(geom)))

💪 À vous de jouer

Premier défi : remplacez le coût « distance » par un coût « temps » en divisant la longueur par une vitesse dépendant du type de route (par exemple 50 km/h sur une voie principale, 20 km/h en zone dense). Second défi : utilisez pgr_drivingDistance pour calculer la zone atteignable en moins de 10 minutes depuis un point (une isochrone).

Voir une piste de solution

Pour le coût-temps : ajoutez une colonne vitesse selon le type, puis UPDATE routes SET cost = ST_Length(geom) / (vitesse * 1000 / 3600) pour obtenir un coût en secondes. Pour l’isochrone : SELECT * FROM pgr_drivingDistance('SELECT id, source, target, cost, reverse_cost FROM routes', noeud_depart, 600) renvoie tous les nœuds atteignables sous 600 secondes ; enveloppez-les dans un ST_ConcaveHull pour dessiner la zone.

Tutoriels frères

Pour aller plus loin

FAQ

Q : Dijkstra ou A* ?
R : pgr_dijkstra explore le graphe sans heuristique et garantit le plus court chemin. pgr_aStar utilise la position des nœuds pour orienter la recherche et va plus vite sur de grands réseaux, au prix d’une géométrie de nœuds renseignée. Pour un réseau urbain de taille modeste, Dijkstra suffit largement.

Q : Comment gérer les sens uniques ?
R : Mettez reverse_cost à une valeur négative (par exemple -1) sur les tronçons à sens unique : pgRouting interdit alors le passage à contresens, tout en autorisant le sens normal via cost. Lancez la requête avec directed := true.

Q : Faut-il vraiment reprojeter en UTM ?
R : Oui, pour que le coût soit en mètres et que les distances aient un sens. Router en degrés produit des coûts incohérents car un degré de longitude ne vaut pas la même distance qu’un degré de latitude.

Q : pgRouting peut-il gérer des millions d’arêtes ?
R : Oui, mais soignez les index (source, target, et l’index spatial) et envisagez de restreindre la requête d’arêtes à une enveloppe autour du trajet pour les très grands réseaux. Sur une ville, aucune optimisation particulière n’est nécessaire.

Service ITSkillsCenter

Application mobile Android et iOS

Création d'application mobile Android et iOS. À partir de 350 000 FCFA.

Démarrer mon projet
Publicité