DOCUMENTATION

RÉFÉRENCE py-osrm-backend 0.1.1 · Python ≥ 3.8

Documentation

Tout ce qu'il faut pour router sur des données OpenStreetMap depuis Python : le modèle de graphe, l'extraction, les moteurs de routage et le serveur REST.

01

Installation

$ pip install py-osrm-backend

Deux dépendances sont installées : flask pour le serveur REST et osmium pour les fichiers PBF. Les fichiers XML n'ont besoin que de la bibliothèque standard. Le paquet s'importe sous le nom osrm.

terminaldev

# Depuis les sources, avec pytest, build et twine
git clone https://github.com/GalTechDev/py-osrm-backend
cd py-osrm-backend
pip install -e ".[dev]"
02

Architecture

Le paquet reprend le découpage d'OSRM : un extractor transforme la carte en graphe, un engine répond aux requêtes, un server les expose en HTTP. Graph, GraphBuilder et DijkstraEngine sont ré-exportés à la racine.

Module Contenu
osrm.structures.graph Node, Edge, Graph, haversine_distance
osrm.extractor.graph_builder GraphBuilder
osrm.extractor.parser OSMParser (XML)
osrm.extractor.pbf_parser OSMPBFParser (PBF, osmium)
osrm.engine.dijkstra DijkstraEngine
osrm.engine.contraction_hierarchies ContractionHierarchies, CHNode, Shortcut
osrm.server.api app Flask, init_app()
03

Le graphe

Un graphe orienté stocké sous forme de liste d'adjacence. Les nœuds sont indexés par leur identifiant OSM ; chaque Edge porte un poids (en kilomètres quand il vient d'une carte) et le nom de la rue. Une rue à double sens correspond simplement à deux arêtes.

Node
id: int, lat: float, lon: float, tags: Dict[str, str]
Edge
source: int, target: int, weight: float, name: str = ""

codeexample.py

from osrm import Graph
from osrm.structures.graph import haversine_distance

g = Graph()
g.add_node(1, 48.8566, 2.3522)                   # Paris
g.add_node(2, 45.7640, 4.8357, {"name": "Lyon"})

d = haversine_distance(48.8566, 2.3522, 45.7640, 4.8357)  # ~392 km
g.add_edge(1, 2, d, name="A6")
g.add_edge(2, 1, d, name="A6")                # sens inverse

len(g)                  # 2
g.get_edges(1)          # [Edge(source=1, target=2, ...)]
add_node(id, lat, lon, tags=None) Ajoute (ou remplace) un nœud et crée sa liste d'adjacence vide.
add_edge(source, target, weight, name="") Ajoute une arête orientée. Lève ValueError si l'un des nœuds est absent.
get_edges(node_id) Arêtes sortantes d'un nœud (liste vide s'il est inconnu).
get_node(node_id) Le Node, ou None.
nodes / adj_list Les dictionnaires sous-jacents : {id: Node} et {id: [Edge]}.
haversine_distance(lat1, lon1, lat2, lon2) Distance orthodromique en kilomètres (rayon terrestre 6371 km).
04

Extraction OSM

GraphBuilder.build_graph(file_path) lit une carte et renvoie un Graph prêt à router. Les fichiers terminant par .pbf passent par osmium ; tout le reste est lu comme de l'OSM XML.

Étapes de construction
  1. 1. Chaque nœud est ajouté au graphe avec ses coordonnées et ses tags.
  2. 2. Seules les voies portant un tag highway sont conservées (routes, chemins, trottoirs…).
  3. 3. Chaque paire de nœuds consécutifs devient une arête pondérée par la distance haversine et nommée d'après le tag name de la voie.
  4. 4. Une arête inverse est ajoutée sauf si la voie a oneway=yes. Les segments qui référencent des nœuds hors de l'extrait sont ignorés.

codeexample.py

from osrm import GraphBuilder
from osrm.extractor.parser import OSMParser

# Pipeline complet
graph = GraphBuilder().build_graph("new-caledonia.osm.pbf")

# Ou les parsers seuls, pour vos propres filtres
parser = OSMParser()
nodes = parser.parse_nodes("map.osm")  # {id: Node}
ways = parser.parse_ways("map.osm")    # [{"id", "nodes", "tags"}]
bus = [w for w in ways if w["tags"].get("bus") == "yes"]

OSMParser (XML) et OSMPBFParser (PBF) partagent la même interface : parse_nodes(path) et parse_ways(path). Le parser XML lit le fichier en flux avec iterparse et libère chaque élément une fois lu.

05

Dijkstra

Le moteur par défaut, et celui qu'utilise le serveur REST. Aucun pré-traitement : créez-le et interrogez-le tout de suite. La recherche s'arrête dès que la destination est atteinte.

codesignature

DijkstraEngine(graph: Graph)

engine.shortest_path(
    start_node_id: int,
    end_node_id: int,
) -> Tuple[float, List[int]]
Cas Résultat
Chemin trouvé (distance, [départ, …, arrivée])
Aucun chemin (inf, [])
Nœud inconnu ValueError

codeexample.py

import math
from osrm import DijkstraEngine

engine = DijkstraEngine(graph)
distance, path = engine.shortest_path(start_id, end_id)

if math.isinf(distance):
    print("Aucun itinéraire")
else:
    coords = [(graph.nodes[n].lat, graph.nodes[n].lon) for n in path]
06

Contraction Hierarchies

La technique d'accélération sur laquelle repose OSRM. preprocess() contracte les nœuds du moins au plus important et ajoute un raccourci pour chaque chemin qui passait par eux. query() lance ensuite une recherche bidirectionnelle qui ne fait que monter dans la hiérarchie.

codeexample.py

from osrm.engine.contraction_hierarchies import ContractionHierarchies

ch = ContractionHierarchies(graph)
ch.preprocess()                  # une fois

distance, path = ch.query(start_id, end_id)
print(len(ch.shortcuts), "raccourcis")
preprocess(max_nodes=None) Calcule l'ordre de contraction (différence d'arêtes + voisins contractés, mis à jour paresseusement) et construit les listes d'arêtes montantes/descendantes. max_nodes limite le travail, pour les tests.
query(start, end) Renvoie (distance, path) comme Dijkstra, ou (inf, []). Lève ValueError pour un nœud inconnu.
ch_nodes {id: CHNode(id, level, contracted)} : le rang de chaque nœud dans la hiérarchie.
shortcuts Liste de Shortcut(source, target, weight, via), où via est le nœud contracté.
info

Le Graph fourni n'est jamais modifié : les raccourcis vivent dans l'objet ContractionHierarchies, le même graphe peut donc toujours alimenter un DijkstraEngine. Le chemin renvoyé est déplié et ne contient que des nœuds d'origine. Une witness search (limitée à WITNESS_SEARCH_LIMIT = 500 nœuds visités) évite les raccourcis inutiles.

07

Lancer le serveur

osrm.server.api contient une app Flask au niveau du module. Appelez init_app(path) une fois pour construire le graphe et le moteur Dijkstra, puis lancez l'app comme n'importe quelle application Flask.

coderun_server.py

from osrm.server.api import app, init_app

init_app("data/new-caledonia.osm.pbf")  # FileNotFoundError si absent
app.run(host="0.0.0.0", port=5000)

Ou directement en ligne de commande, configuré par variables d'environnement :

OSRM_FILE=map.osm.pbf python -m osrm.server.api
Variable Défaut Description
OSRM_FILE data.osm Carte à charger (.osm ou .osm.pbf).
OSRM_HOST 127.0.0.1 Adresse d'écoute (0.0.0.0 pour l'exposer).
OSRM_PORT 5000 Port d'écoute.
FLASK_DEBUG — 1 active le mode debug de Flask.

GET /health renvoie {"status": "ok", "nodes": N} une fois le graphe chargé, ou 503 avant.

08

Endpoint route

GET /route/v1/driving/{lon},{lat};{lon},{lat}[;…]

Même forme d'URL qu'OSRM : longitude en premier, coordonnées séparées par ;. Deux points de passage ou plus sont acceptés ; chacun est accroché au nœud le plus proche relié à au moins une arête, et les tronçons sont enchaînés en un seul itinéraire.

terminal200 OK

$ curl "localhost:5000/route/v1/driving/166.44,-22.27;166.46,-22.29;166.47,-22.30"

{
  "code": "Ok",
  "routes": [{
    "distance": 4870.2,
    "duration": 352.9,
    "geometry": {
      "type": "LineString",
      "coordinates": [[166.4401, -22.2703], ...]
    }
  }]
}
Champ Description
distance Longueur totale en mètres, comme OSRM.
duration Temps estimé en secondes, à 50 km/h constants (AVERAGE_SPEED_MPS = 13,8 m/s) : il n'y a pas encore de profil de vitesse.
geometry LineString GeoJSON de paires [lon, lat], prête pour Leaflet ou MapLibre.

Codes d'erreur

400 InvalidRequest Moins de deux coordonnées, ou une coordonnée qui n'est pas au format lon,lat.
400 NoSegment Aucun nœud routable n'a été trouvé pour une coordonnée.
404 NoRoute Deux points de passage consécutifs ne sont pas reliés dans le graphe.
500 Error Exception inattendue ; message contient son texte.

Chaque corps d'erreur a la forme {"code": …, "message": …}.

09

Docker

Le dépôt fournit un Dockerfile et un docker-compose.yml. Placez votre carte dans data/map.osm (ou changez OSRM_FILE) et démarrez le service : l'API écoute sur le port 5000 et le healthcheck du conteneur appelle /health.

docker compose up -d

# Mode dev : sources montées, debug Flask, port 5001
docker compose --profile dev up py-osrm-backend-dev
10

Tests

La suite couvre les structures de graphe, l'extracteur XML, les deux moteurs et l'API multi-points. Elle tourne avec unittest comme avec pytest.

python -m unittest discover -s tests -v
# ou
pytest
11

Limites

py-osrm-backend privilégie la lisibilité à la vitesse brute. Bon à savoir avant de charger un pays entier :

  • chevron_right Le graphe entier vit en mémoire sous forme d'objets Python.
  • chevron_right Pas de profil de véhicule : toute voie highway est utilisable, les poids sont des distances et seul oneway=yes est compris.
  • chevron_right Le serveur accroche les coordonnées par un parcours linéaire de tous les nœuds, et route toujours avec Dijkstra.
  • chevron_right Le pré-traitement CH tourne en Python pur avec un ordre de contraction simple : il reste lent sur de gros extraits, idéal pour apprendre et pour des cartes régionales.