Ce que fait la page arrets-gpx.html, de la lecture du fichier jusqu'à l'affichage : comment le tracé est reconstitué, comment chaque arrêt est rattaché au tracé, et comment sont déterminés le côté (G / D) et les discontinuités.
Le traitement est le même quel que soit le format d'entrée. Seules les deux premières étapes changent.
Le format est reconnu sur le contenu du fichier, pas sur son extension.
| Format | Détection | Tracé | Arrêts |
|---|---|---|---|
| GPX | autre cas | <trkpt>, sinon <rtept>, dans l'ordre du fichier | <wpt>, dans l'ordre du fichier |
| GeoJSON | commence par { | LineString / MultiLineString | Point / MultiPoint ; nom lu dans name, nom, stop_name, title ou label |
| XML OSM | contient <osm au début | ways de la relation, dans l'ordre des membres | membres platform* ou stop* |
Pour un GPX, l'ordre du fichier est supposé être l'ordre de parcours. Pour un GeoJSON qui contient une seule ligne, l'ordre des points est aussi conservé tel quel.
C'est le format le plus fiable, car il conserve l'ordre et les rôles des membres. Il s'obtient avec https://api.openstreetmap.org/api/0.6/relation/ID/full, soit en déposant le fichier, soit avec le champ « Relation OSM ».
S'il y a plusieurs relations dans la réponse (sous-relations), la première avec le tag type=route est retenue, sinon la première relation.
way dont le rôle ne commence ni par platform ni par stop (rôle vide, forward, backward…).platform* sont préférés (platform, platform_entry_only, platform_exit_only…), car ils sont posés de part et d'autre de la route et permettent de déterminer le côté.stop* (stop_position) qui sont utilisés. Ils sont sur la chaussée : la plupart seront donc classés « axe ».Le titre affiché dans le panneau est ref + name (ou from → to si le nom est absent).
Les tags de la relation dont la clé commence par gtfs:shape_id (par exemple gtfs:shape_id:IDFM) sont affichés sous les liens du panneau, avec leur clé complète ; la valeur se sélectionne d'un clic pour être copiée. S'il n'y en a pas, le panneau l'indique.
Un export de relation contient un LineString par way, dans un ordre et un sens quelconques. La page les reconstitue.
public_transport=platform ou highway=bus_stop, seuls ceux-là sont gardés.stop_position).t sur ce segment).Un rond-point tracé d'un seul way est un anneau fermé : son premier et son dernier point sont identiques. Le traiter comme une simple ligne créerait deux faux trous, un à l'entrée et un à la sortie.
Quand un way fermé apparaît dans la relation :
Seule la portion réellement empruntée est ajoutée au tracé. Un rond-point découpé en plusieurs ways n'a pas besoin de ce traitement : les arcs se raccordent par leurs nœuds communs.
Une discontinuité est repérée à chaque raccord entre deux tronçons dont les extrémités sont à plus de 2 m l'une de l'autre. En dessous, elles sont considérées comme jointes.
Causes fréquentes : un way manquant dans la relation, des ways dans le désordre, un tronçon non connecté dans OSM (nœud non partagé).
Les GPX et les GeoJSON à ligne unique n'ont pas de raccords, donc aucune discontinuité n'est signalée.
Les calculs de distance se font en mètres dans un plan, pas en degrés. La page utilise une projection équirectangulaire centrée sur la moyenne des points du tracé :
ky = R · π/180 (mètres par degré de latitude)
kx = ky · cos(latitude_ref) (mètres par degré de longitude)
x = (lon − lon_ref) · kx
y = (lat − lat_ref) · ky avec R = 6 371 008,8 m
Sur une ligne de bus, l'erreur est de l'ordre du mètre sur quelques dizaines de kilomètres, ce qui suffit ici.
Pour chaque arrêt P et chaque segment [A, B] du tracé, le point le plus proche est calculé :
t = ((P − A) · (B − A)) / |B − A|² puis t borné à [0, 1]
C = A + t · (B − A) (point d'accroche)
distance = |P − C|
Le borne sur t garde le point sur le segment : si la perpendiculaire tombe hors du segment, le point d'accroche est une extrémité. Le segment retenu est celui qui minimise la distance, sous la contrainte d'ordre ci-dessous.
Chaque arrêt est candidat sur tous les segments. Choisir simplement le plus proche donnerait des erreurs aux terminus, aux boucles et aux endroits où la ligne repasse près d'elle-même.
Par défaut (option « Respecter l'ordre des arrêts »), une programmation dynamique choisit l'ensemble des accrochages qui minimise la somme des distances, avec la contrainte que l'indice de segment ne décroît jamais d'un arrêt au suivant.
coût[j][k] = distance(arrêt j, segment k) + min sur k' ≤ k de coût[j−1][k']
Le meilleur prédécesseur est maintenu par un minimum cumulé, donc la complexité est en O(arrêts × segments). Le chemin optimal est reconstruit à rebours.
Décocher l'option accroche chaque arrêt indépendamment sur son segment le plus proche.
Le côté se déduit du signe du produit vectoriel entre la direction du segment retenu et le vecteur segment → arrêt :
signé = (dx · (Py − Ay) − dy · (Px − Ax)) / |B − A| avec (dx, dy) = B − A
| Valeur | Résultat |
|---|---|
signé > 1 m | G à gauche du sens de circulation |
signé < −1 m | D à droite |
|signé| ≤ 1 m | axe (point gris) |
Gauche et droite sont relatifs au sens du tracé. Si le tracé est dans le sens inverse de la ligne, les côtés sont inversés ; l'option « Inverser le sens du tracé » corrige cela.
Deux distances renseignent sur la couverture du tracé par les arrêts :
Elles sont mesurées le long du tracé : somme des longueurs des segments jusqu'au point d'accroche de l'arrêt (indice de segment + fraction t), pas en ligne droite. Le « 1er » et le « dernier » arrêt sont ceux de la liste, donc dans l'ordre de parcours (relation OSM) ou trié le long du tracé (export Overpass). Elles tiennent compte de l'option « Inverser le sens du tracé ».
Elles apparaissent dans le texte d'état, et sous forme de deux lignes grisées en tête et en fin de la liste (un clic centre la carte sur l'extrémité du tracé). Elles sont aussi exposées aux scripts : arrets.state.startToFirst et arrets.state.lastToEnd, en mètres.
Une distance importante signale un tracé qui dépasse les arrêts (terminus mal placé, tronçon en trop), ou un arrêt manquant en début ou fin de ligne.
Ils passent par la télécommande de JOSM (http://127.0.0.1:8111). JOSM doit être lancé, avec la télécommande activée (Préférences → Télécommande, option de chargement des données).
load_and_zoom avec une boîte de 50 m autour de l'arrêt (environ 100 m de côté, avec correction de longitude selon la latitude). Les données arrivent dans le calque actif.load_object avec relation_members=true et new_layer=false, qui télécharge la relation et ses membres dans le calque actif.L'appel part en arrière-plan. Si le navigateur le bloque, l'URL s'ouvre dans un onglet à la place.
| Paramètre | Effet |
|---|---|
?relation=21744664 (ou ?rel=, ou #relation=) | Charge la relation au démarrage ; l'URL est mise à jour quand on en charge une avec le bouton. |
#map=zoom/lat/lon | Position et zoom, mis à jour à chaque fin de déplacement. Au chargement, la vue de l'URL n'est pas écrasée par le recadrage automatique sur le tracé. |
?bg=none ou #bg=none&map=13/44.8/-0.5 (alias ?nobasemap) | Aucun fond de carte : plan et ortho ne sont ni déclarés ni demandés (aucune tuile chargée), le sélecteur de fond est masqué. ?nobasemap=0 (ou false, no, non) le désactive. |
bg=osmfr ou bg=ortho | Force le fond au chargement, sans modifier le choix mémorisé dans le navigateur. |
Ces paramètres se lisent dans la requête (?…) ou dans le fragment (#…). Le paramètre bg est conservé quand la position de l'URL est mise à jour.
En file://, certains navigateurs refusent de réécrire l'URL : le chargement fonctionne, mais l'adresse ne se met pas à jour.
La page peut être pilotée et suivie par un script (Playwright, Selenium, console…). Pour éviter tout chargement de tuiles, ouvrir par exemple arrets-gpx.html?bg=none.
Ils sont émis sur document (ils remontent jusqu'à window). event.detail contient les champs ci-dessous, plus state : une copie de l'état courant.
| Événement | Moment | Détail |
|---|---|---|
arrets:busy | un traitement démarre (passage de libre à occupé) | phase, message |
arrets:progress | avancement | phase, value (0 à 1), message |
arrets:idle | plus aucun traitement en cours | — |
arrets:ready | carte initialisée, prête à charger un tracé | — |
arrets:error | erreur d'un traitement | phase, message |
Une série de chargements enchaînés (traités l'un après l'autre) ne produit qu'un seul couple busy / idle. L'erreur est effacée de l'état au démarrage du traitement suivant, et un error est toujours suivi d'un idle.
| Phase | Valeur | Signification |
|---|---|---|
init, ready | —, 1 | initialisation de la carte, puis carte prête |
fetch | 0,1 | téléchargement de la relation sur l'API OSM |
receive | 0,05 | lecture du fichier ou réception d'une shape envoyée par la page GTFS |
parse | 0,2 à 0,3 | lecture du GPX, du GeoJSON ou du XML OSM |
snap | 0,45 | tri et accrochage des arrêts sur le tracé |
render | 0,65 | affichage sur la carte et dans la liste |
fit | 0,8 | cadrage sur le tracé (absent si la vue vient de l'URL ou au recalcul) |
tiles | 0,9 | attente de la fin du rendu (au plus 8 s) |
done | 1 | terminé |
josm | 0,3 à 1 | appel de la télécommande JOSM depuis un popup ou le panneau |
Changer une option (prolongement, ordre, inversion) relance un traitement court : snap, render, tiles, done.
window.arrets.state est une copie de l'état :
| Champ | Contenu |
|---|---|
ready, busy | carte prête ; traitement en cours |
phase, value, message | dernière progression |
title | titre du tracé chargé |
points, stops | nombre de points du tracé et d'arrêts |
left, right, axis | arrêts à gauche, à droite, sur l'axe |
gaps | nombre de discontinuités |
startToFirst, lastToEnd | distances (m) début du tracé → 1er arrêt et dernier arrêt → fin du tracé, le long du tracé |
basemap | none, osmfr ou ortho |
error | message de la dernière erreur, sinon null |
Les attributs html[data-arrets-state="busy|idle"] et html[data-arrets-phase] reflètent le même état, pratique pour waitForSelector.
| Méthode | Effet |
|---|---|
arrets.whenReady() | promesse résolue quand la carte est prête |
arrets.whenIdle() | promesse résolue (avec l'état) quand plus aucun traitement n'est en cours |
arrets.loadText(texte, titre?) | charge un GPX, un GeoJSON ou un XML OSM fourni sous forme de texte |
arrets.loadRelation(id) | charge une relation depuis l'API OSM |
arrets.load(msg) | comme un message de la page GTFS : { gpx, name, shape_id } (ou directement le texte GPX) |
await page.goto('file:///…/arrets-gpx.html?bg=none');
await page.waitForSelector('html[data-arrets-state="idle"]');
await page.evaluate(t => arrets.loadText(t), gpxTexte);
const etat = await page.evaluate(() => arrets.whenIdle());
console.log(etat.stops, etat.left, etat.right, etat.error);
?bg=none.