feat(data): inclure les trains SNCF (TER/TGV) autour de Grenoble #15

Open
opened 2026-08-23 21:45:20 +02:00 by CyrilLeblanc · 0 comments
Owner

Contexte

L'app n'affiche aucun train SNCF aujourd'hui, par construction : le générateur de données statiques (scripts/generate_transit_data.py) filtre les routes sur URBAN_PREFIXES = {"SEM", "GSV", "SE2", "MCO", "TPV"} et prune ensuite toute ligne sans géométrie (lignes 415-426). Les 20 routes SNCF de l'API M réso (préfixe SNC:) sont donc doublement exclues — alors qu'elles desservent Grenoble (Grenoble Gare / Gières / Échirolles…) et que leurs horaires temps réel fonctionnent sur la même API que les bus.

Faits vérifiés le 2026-08-23 (ne pas re-vérifier) :

  • L'API routers/default/index/routes expose 20 routes SNC:* : TGV Paris-Grenoble (621A, 770A), TER Lyon (K6), Chambéry (C1, C31), Valence/Genève (K11, K21), Saint-Marcellin (C6, C11), Gap/Briançon (P24, P25), etc.
  • L'endpoint stoptimes fonctionne pour les clusters SNCF avec temps réel : clusters/SNC:OCE87747006/stoptimes (Grenoble Gare) renvoie des patterns SNC avec realtime=True (TGV INOUI, TER INTER). Le pipeline ETA existant (repository → GetEtasEtaGrouper) fonctionne donc sans modification de fond.
  • L'endpoint lines/json?codes=SNC_* ne renvoie AUCUNE géométrie pour les lignes SNCF (testé SNC_K6, SNC:C1, SNC_K21 → 0 feature). C'est le vrai blocker : le pruning du générateur supprimerait ces lignes même sans le filtre de préfixe.
  • ficheHoraires/json?route=SNC:K6 fonctionne et donne les arrêts ordonnés par direction avec positions (ex. direction 0 : Grenoble → Voiron → La Tour-du-Pin…). Une géométrie de substitution (polyligne reliant les arrêts dans l'ordre) est donc constructible.
  • Les clusters SNCF suivent le format SNC:OCE<code> (nomenclature SNCF « OCE »), ex. SNC:OCE87747006 (Grenoble), avec des enfants physiques du type SNC:OCETGVINOUI-87747006 / SNC:OCETRAINTER-87747006 (un enfant par type de service).

Comportement attendu

  1. Le générateur (scripts/generate_transit_data.py) inclut les routes SNC:* :
    • le filtre de préfixes devient URBAN_PREFIXES ∪ {"SNC"} ;
    • quand lines/json ne renvoie pas de géométrie pour une ligne (cas SNCF), construire une géométrie de substitution : un segment unique reliant les positions des arrêts du premier ficheHoraires disponible, dans l'ordre (résolution ~stations, pas du tracé réel — acceptable pour une ligne ferroviaire dont on montre la desserte) ;
    • le pruning « sans géométrie » s'applique à la géométrie réelle OU de substitution ;
    • le validateur (scripts/validate_transit_data.py) : les seuils minimaux (≥150 lignes…) restent valables ; les compteurs ±15 % vont sauter au premier run avec les SNCF incluses (+20 lignes ≈ +11 % — dans la tolérance) ; vérifier que les checks structurels passent (les stops SNCF auront leurs enfants clusterStops via l'endpoint routes/{id}/stops, déjà le cas dans le flow existant).
  2. L'app affiche les lignes SNCF comme les autres :
    • Écran d'arrêt (ETAs) : les groupes SNCF apparaissent avec destination et heures temps réel ; le badge utilise un style distinct (les shortName SNCF du type K6, C1, P24 se prêtent au badge, mais la confusion avec les Chrono C1 urbains est réelle — voir « Pistes ») ;
    • Recherche de ligne : les lignes SNCF cherchables comme les autres ;
    • Map : la polyligne SNCF (géométrie de substitution) s'affiche selon les règles existantes de visibilité par zoom ; les lignes SNCF ne sont pas ajoutées à ALWAYS_VISIBLE_IDS (trop longue portée — n'afficher que sur demande / zoom adapté) ;
    • Planificateur (PLAN) : hors périmètre de ce ticket (l'OTP route déjà les trains côté API ; l'affichage des legs SNCF par l'app est à valider séparément si besoin).
  3. Direction/bearing : Bearings.kt fonctionne sur les séquences — les séquences SNCF existeront (ficheHoraires par direction), le bearing s'en déduira naturellement.

Pistes d'implémentation

  • Générateur : la géométrie de substitution se construit depuis fetch_direction_stops déjà présent (il retourne lat/lon par arrêt ordonné). Marquer ces géométries (ex. champ synthetic: true dans le JSON geometries) pour permettre à l'app de styliser différemment (trait pointillé ?) et au validateur de les compter séparément.
  • Modèle/format : bump "version": 3 ; champ generatedAt inchangé. L'app lit ignoreUnknownKeys = true — champ synthetic optionnel côté Kotlin.
  • EtaGrouper / affichage : lineName = routeId.substringAfter(":") donnerait OCE87747006-adjacent pour les groupes — non : pour les groupes SNCF, le nom affiché doit venir du shortName de la ligne (K6, C31…), déjà le comportement via lines[routeId]. Vérifier lineSortKey pour un tri lisible des trains (peut-être un groupe distinct « Trains » en tête de feuille d'arrêt).
  • Conflit de nommage C1 : la ligne SNCF SNC:C1 (Grenoble-Chambéry) vs le Chrono urbain SEM:C1. Les IDs restent distincts (SNC:C1SEM:C1) donc pas de collision technique, mais l'affichage doit lever l'ambiguïté visuelle (préfixe TER / icône train / couleur SNCF vs couleur M réso).
  • Clusters gares : les clusters SNCF ne fusionneront pas avec les clusters urbains adjacents (noms différents : « Grenoble » vs « Grenoble, Gare Europole ») — c'est acceptable (points séparés), mais vérifier visuellement que deux points proches ne se superposent pas mal à Grenoble Gare (le merge flou names_match + 200 m pourrait fusionner certains couples — observer le comportement au premier import).
  • Update de données : la CI republiera (ticket #12) ; l'app fraîchement installée récupère tout via le bootstrap (#13). Les installs existantes attendront le refresh (#14).

Critères d'acceptation

  • Le JSON généré contient les ~20 lignes SNC:* avec géométries (réelle ou substitution), séquences par direction, stops et clusterStops.
  • validate_transit_data.py passe sur le JSON étendu (structurel + drift vs version précédente, en tenant compte du saut attendu de compteurs).
  • À l'écran d'arrêt d'une gare (ex. Grenoble Gare / Gières), les trains apparaissent avec destinations et heures temps réel distinctes des bus/trams.
  • Sur la map, la polyligne d'une ligne SNCF s'affiche (sur zoom/demande) et ne parasite pas les trams/Chrono en vue large.
  • La recherche de ligne retrouve « K6 » / « Grenoble Chambéry » (par shortName ou longName).
  • Aucun C1 SNCF n'est confondu avec le Chrono C1 à l'affichage (libellé ou style distinct).
  • Les gares ont leurs arrêts physiques (enfants) dans clusterStops et le drawer d'arrêt ne casse pas sur un enfant au nom long (SNC:OCETGVINOUI-…).
  • La taille du JSON reste raisonnable (< ~4 Mo) et le premier lancement (#13) télécharge/importe sans timeout.
  • ./gradlew assembleDebug passe ; pas de régression sur les lignes urbaines existantes (compteurs identiques ±0 hors ajout SNCF).

Notes / captures (optionnel)

  • Tests API du 2026-08-23 : SNC:OCE87747006 stoptimes OK (realtime=True), ficheHoraires OK pour SNC:K6, lines/json vide pour tous les codes SNCF testés.
  • L'endpoint routes/{id}/stops des routes SNC renvoie les arrêts avec cluster → le mécanisme existant d'enfants physiques s'applique tel quel.
  • Out of scope : C38 (cars interurbains du Département — 222 routes, volume et pertinence à débattre séparément), TRA (remontées mécaniques), FUN/BUL (funiculaire/téléphérique urbains — candidats à un ticket dédié, BUL le « Bulle » grenoblois étant pertinent en zone urbaine).
  • Dépend du format v3 → coordonner avec #13 (bootstrap, format v2 en cours d'intégration) pour ne pas se marcher dessus sur le bump de version.
## Contexte L'app n'affiche aucun train SNCF aujourd'hui, par construction : le générateur de données statiques (`scripts/generate_transit_data.py`) filtre les routes sur `URBAN_PREFIXES = {"SEM", "GSV", "SE2", "MCO", "TPV"}` et **prune ensuite toute ligne sans géométrie** (lignes 415-426). Les 20 routes SNCF de l'API M réso (préfixe `SNC:`) sont donc doublement exclues — alors qu'elles desservent Grenoble (Grenoble Gare / Gières / Échirolles…) et que leurs horaires temps réel **fonctionnent sur la même API que les bus**. **Faits vérifiés le 2026-08-23** (ne pas re-vérifier) : - L'API `routers/default/index/routes` expose 20 routes `SNC:*` : TGV Paris-Grenoble (`621A`, `770A`), TER Lyon (`K6`), Chambéry (`C1`, `C31`), Valence/Genève (`K11`, `K21`), Saint-Marcellin (`C6`, `C11`), Gap/Briançon (`P24`, `P25`), etc. - **L'endpoint stoptimes fonctionne pour les clusters SNCF avec temps réel** : `clusters/SNC:OCE87747006/stoptimes` (Grenoble Gare) renvoie des patterns SNC avec `realtime=True` (TGV INOUI, TER INTER). Le pipeline ETA existant (repository → `GetEtas` → `EtaGrouper`) fonctionne donc sans modification de fond. - **L'endpoint `lines/json?codes=SNC_*` ne renvoie AUCUNE géométrie** pour les lignes SNCF (testé `SNC_K6`, `SNC:C1`, `SNC_K21` → 0 feature). C'est le vrai blocker : le pruning du générateur supprimerait ces lignes même sans le filtre de préfixe. - `ficheHoraires/json?route=SNC:K6` **fonctionne** et donne les arrêts ordonnés par direction avec positions (ex. direction 0 : Grenoble → Voiron → La Tour-du-Pin…). Une géométrie de substitution (polyligne reliant les arrêts dans l'ordre) est donc constructible. - Les clusters SNCF suivent le format `SNC:OCE<code>` (nomenclature SNCF « OCE »), ex. `SNC:OCE87747006` (Grenoble), avec des enfants physiques du type `SNC:OCETGVINOUI-87747006` / `SNC:OCETRAINTER-87747006` (un enfant par type de service). ## Comportement attendu 1. **Le générateur** (`scripts/generate_transit_data.py`) inclut les routes `SNC:*` : - le filtre de préfixes devient `URBAN_PREFIXES ∪ {"SNC"}` ; - quand `lines/json` ne renvoie pas de géométrie pour une ligne (cas SNCF), construire une **géométrie de substitution** : un segment unique reliant les positions des arrêts du premier `ficheHoraires` disponible, dans l'ordre (résolution ~stations, pas du tracé réel — acceptable pour une ligne ferroviaire dont on montre la desserte) ; - le pruning « sans géométrie » s'applique à la géométrie réelle OU de substitution ; - le validateur (`scripts/validate_transit_data.py`) : les seuils minimaux (≥150 lignes…) restent valables ; les compteurs ±15 % vont sauter au premier run avec les SNCF incluses (+20 lignes ≈ +11 % — dans la tolérance) ; vérifier que les checks structurels passent (les stops SNCF auront leurs enfants `clusterStops` via l'endpoint routes/{id}/stops, déjà le cas dans le flow existant). 2. **L'app** affiche les lignes SNCF comme les autres : - **Écran d'arrêt (ETAs)** : les groupes SNCF apparaissent avec destination et heures temps réel ; le badge utilise un style distinct (les `shortName` SNCF du type `K6`, `C1`, `P24` se prêtent au badge, mais la confusion avec les Chrono `C1` urbains est réelle — voir « Pistes ») ; - **Recherche de ligne** : les lignes SNCF cherchables comme les autres ; - **Map** : la polyligne SNCF (géométrie de substitution) s'affiche selon les règles existantes de visibilité par zoom ; les lignes SNCF ne sont **pas** ajoutées à `ALWAYS_VISIBLE_IDS` (trop longue portée — n'afficher que sur demande / zoom adapté) ; - **Planificateur (PLAN)** : hors périmètre de ce ticket (l'OTP route déjà les trains côté API ; l'affichage des legs SNCF par l'app est à valider séparément si besoin). 3. **Direction/bearing** : `Bearings.kt` fonctionne sur les séquences — les séquences SNCF existeront (ficheHoraires par direction), le bearing s'en déduira naturellement. ## Pistes d'implémentation - **Générateur** : la géométrie de substitution se construit depuis `fetch_direction_stops` déjà présent (il retourne lat/lon par arrêt ordonné). Marquer ces géométries (ex. champ `synthetic: true` dans le JSON `geometries`) pour permettre à l'app de styliser différemment (trait pointillé ?) et au validateur de les compter séparément. - **Modèle/format** : bump `"version": 3` ; champ `generatedAt` inchangé. L'app lit `ignoreUnknownKeys = true` — champ `synthetic` optionnel côté Kotlin. - **EtaGrouper / affichage** : `lineName = routeId.substringAfter(":")` donnerait `OCE87747006`-adjacent pour les groupes — non : pour les groupes SNCF, le nom affiché doit venir du `shortName` de la ligne (K6, C31…), déjà le comportement via `lines[routeId]`. Vérifier `lineSortKey` pour un tri lisible des trains (peut-être un groupe distinct « Trains » en tête de feuille d'arrêt). - **Conflit de nommage `C1`** : la ligne SNCF `SNC:C1` (Grenoble-Chambéry) vs le Chrono urbain `SEM:C1`. Les IDs restent distincts (`SNC:C1` ≠ `SEM:C1`) donc pas de collision technique, mais l'affichage doit lever l'ambiguïté visuelle (préfixe TER / icône train / couleur SNCF vs couleur M réso). - **Clusters gares** : les clusters SNCF ne fusionneront pas avec les clusters urbains adjacents (noms différents : « Grenoble » vs « Grenoble, Gare Europole ») — c'est acceptable (points séparés), mais vérifier visuellement que deux points proches ne se superposent pas mal à Grenoble Gare (le merge flou `names_match` + 200 m pourrait fusionner certains couples — observer le comportement au premier import). - **Update de données** : la CI republiera (ticket #12) ; l'app fraîchement installée récupère tout via le bootstrap (#13). Les installs existantes attendront le refresh (#14). ## Critères d'acceptation - [ ] Le JSON généré contient les ~20 lignes `SNC:*` avec géométries (réelle ou substitution), séquences par direction, stops et `clusterStops`. - [ ] `validate_transit_data.py` passe sur le JSON étendu (structurel + drift vs version précédente, en tenant compte du saut attendu de compteurs). - [ ] À l'écran d'arrêt d'une gare (ex. Grenoble Gare / Gières), les trains apparaissent avec destinations et heures temps réel distinctes des bus/trams. - [ ] Sur la map, la polyligne d'une ligne SNCF s'affiche (sur zoom/demande) et ne parasite pas les trams/Chrono en vue large. - [ ] La recherche de ligne retrouve « K6 » / « Grenoble Chambéry » (par shortName ou longName). - [ ] Aucun `C1` SNCF n'est confondu avec le Chrono `C1` à l'affichage (libellé ou style distinct). - [ ] Les gares ont leurs arrêts physiques (enfants) dans `clusterStops` et le drawer d'arrêt ne casse pas sur un enfant au nom long (`SNC:OCETGVINOUI-…`). - [ ] La taille du JSON reste raisonnable (< ~4 Mo) et le premier lancement (#13) télécharge/importe sans timeout. - [ ] `./gradlew assembleDebug` passe ; pas de régression sur les lignes urbaines existantes (compteurs identiques ±0 hors ajout SNCF). ## Notes / captures (optionnel) - Tests API du 2026-08-23 : `SNC:OCE87747006` stoptimes OK (`realtime=True`), `ficheHoraires` OK pour `SNC:K6`, `lines/json` vide pour tous les codes SNCF testés. - L'endpoint `routes/{id}/stops` des routes SNC renvoie les arrêts avec `cluster` → le mécanisme existant d'enfants physiques s'applique tel quel. - Out of scope : C38 (cars interurbains du Département — 222 routes, volume et pertinence à débattre séparément), TRA (remontées mécaniques), FUN/BUL (funiculaire/téléphérique urbains — candidats à un ticket dédié, BUL le « Bulle » grenoblois étant pertinent en zone urbaine). - Dépend du format v3 → coordonner avec #13 (bootstrap, format v2 en cours d'intégration) pour ne pas se marcher dessus sur le bump de version.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
CyrilLeblanc/gresit#15
No description provided.