|
Simulateur Ferroviaire
Reconstruction et visualisation d'un réseau ferroviaire à partir de données GeoJSON — Win32 / WebView2 / Leaflet
|
Le pipeline GeoParser v2 transforme un fichier GeoJSON contenant la géométrie brute d'un réseau ferroviaire (polylignes OSM) en une topologie structurée de blocs (StraightBlock, SwitchBlock) prête à être consommée par le module PCC et le rendu WebView.
Le pipeline est séquentiel et se déroule en 8 phases numérotées, chacune encapsulée dans une classe statique PhaseN_Xxx. Les données intermédiaires transitent via le PipelineContext, qui est l'unique objet partagé entre les phases.
Invariant critique :
Phase8::resolve()doit précéderPhase7(le processeur d'aiguillages a besoin des pointeurs résolus pour le recul des tips CDC).Phase8::transfer()doit toujours être en dernier.
PipelineContext est le sac de données partagé entre toutes les phases. Il évolue à chaque phase : les champs produits par une phase deviennent des entrées de la suivante. Certains champs sont explicitement libérés par la phase qui les a consommés (ex. topoGraph, classifiedNodes, splitNetwork sont effacés en fin de Phase 6 pour libérer la mémoire).
Structure simplifiée :
Classe : Phase1_GeoLoader
Charge le fichier GeoJSON depuis le disque et désérialise chaque Feature de type LineString en une liste de GeoFeature (polyligne WGS-84).
Les features non-linéaires (points, polygones) sont ignorées avec un warning.
Entrées : ctx.filePath Sorties : ctx.rawFeatures
Classe : Phase2_GeometricIntersector
Détecte et matérialise les intersections géométriques entre segments de polylignes distincts qui se croisent sans partager de nœud commun dans le GeoJSON source.
Pour chaque paire de segments, un test d'intersection 2D (sur coordonnées UTM) est effectué. Si l'intersection tombe dans le segment (hors extrémités à intersectionEpsilon près), un nouveau point est inséré dans les deux polylignes à la position exacte de croisement.
Paramètre clé : config.intersectionEpsilon
Entrées : ctx.rawFeatures Sorties : ctx.rawFeatures (modifiées in place)
Classe : Phase3_NetworkSplitter
Fusionne les extrémités proches (snapTolerance) en un nœud unique, puis découpe les polylignes aux points de fusion pour obtenir un réseau d'segments atomiques (un segment = deux extrémités, pas de nœud interne).
À l'issue de cette phase, tout point présent à l'intérieur d'une polyligne est forcément une extrémité d'un autre segment.
Paramètre clé : config.snapTolerance
Entrées : ctx.rawFeatures Sorties : ctx.splitNetwork (collection de AtomicSegment)
Classe : Phase4_TopologyBuilder
Construit le graphe topologique (TopoGraph) à partir du réseau atomique. Chaque nœud du graphe correspond à une extrémité physique unique (après snap) ; chaque arête correspond à un AtomicSegment.
Le graphe est non-orienté : les adjacences sont bidirectionnelles.
Entrées : ctx.splitNetwork Sorties : ctx.topoGraph
Classe : Phase5_SwitchClassifier
Classifie chaque nœud du graphe en l'une des trois catégories :
NodeClass | Degré | Condition |
|---|---|---|
ENDPOINT | 1 | Terminus de ligne |
STRAIGHT | 2 | Nœud de passage — pas d'intersection |
SWITCH | 3+ | Bifurcation — angle entre branches ≥ minSwitchAngle |
Un nœud de degré 3 dont toutes les branches sont colinéaires (angle < seuil) est reclassifié en STRAIGHT pour éviter de créer un aiguillage fantôme sur une légère imperfection géométrique OSM.
Paramètres clés : config.minSwitchAngle, config.minBranchLength
Entrées : ctx.topoGraph Sorties : ctx.classifiedNodes
Classe : Phase6_BlockExtractor
Produit les StraightBlock et SwitchBlock à partir du graphe classifié.
DFS entre nœuds frontières (classe ≠ STRAIGHT). La déduplication repose sur les indices d'arêtes (usedEdges) et non sur la clé de paire de nœuds : cette approche permet de créer deux straights distincts entre les mêmes deux nœuds frontières dans les configurations crossover (voie double).
Si la longueur totale UTM du straight assemblé dépasse maxSegmentLength, il est subdivisé en N sous-blocs chaînés via prev/next. Seuls les sous-blocs de tête et de queue possèdent des BlockEndpoint avec frontierNodeId valide (≠ SIZE_MAX).
Un SwitchBlock par nœud SWITCH. Pour chaque branche, une traversal des nœuds intermédiaires remonte jusqu'au nœud frontière adjacent, puis straightByDirectedPair (multi-valué) fournit le StraightBlock adjacent. Un ensemble usedStraights local garantit qu'un même straight n'est pas attribué à deux branches du même switch (cas crossover).
| Index | Clé | Valeur |
|---|---|---|
straightsByNode | nodeId | vector<StraightBlock*> (multi-valué) |
straightByEndpointPair | Cantor(min,max) | StraightBlock* premier depuis A |
straightByDirectedPair | from*1e6 + to | vector<StraightBlock*> (multi-valué) |
switchByNode | nodeId | SwitchBlock* |
Libération mémoire : topoGraph, classifiedNodes, splitNetwork sont vidés en fin de phase.
Classe : Phase8_RepositoryTransfer, méthode resolve()
Résout les pointeurs inter-blocs en deux passes :
frontierNodeId valide, cherche le voisin dans switchByNode ou straightsByNode et appelle setNeighbourPrev/Next. Les endpoints internes (frontierNodeId == SIZE_MAX) sont ignorés — la chaîne prev/next posée par Phase 6 est préservée.root, normal, deviation depuis les IDs stockés dans les endpoints de branches.Correctif v2 : La vérification de
frontierNodeId != SIZE_MAXavant tout appel àsetNeighbour*est essentielle pour ne pas écraser la chaîne de subdivision posée par Phase 6.
Classe : Phase7_SwitchProcessor
Traite les aiguillages en plusieurs sous-phases séquentielles (G→A→B→C→D→E→F) :
| Sous-phase | Rôle |
|---|---|
| G — DoubleSwitchDetector | Détecte les paires d'aiguilles doubles et marque l'absorption |
| A — OrientationResolver | Identifie root/normal/deviation par analyse angulaire |
| B — CDCTipInterpolator | Interpole les tips CDC à distance switchSideSize |
| C — StraightTrimmer | Recule les extrémités des straights adjacents |
| D — OverlapResolver | Résout les chevauchements restants |
| E — BranchValidator | Valide la cohérence des branches |
| F — DoubleAbsorber | Absorbe les coordonnées du segment de liaison |
Paramètres clés : config.junctionTrimMargin, config.switchSideSize, config.doubleSwitchRadius
Issue connue (différé) :
DoubleSwitchDetectoréchoue sur certaines configurations de crossover où la branche déviation du premier switch pointe vers le straight de liaison plutôt que vers le second switch.
Classe : Phase8_RepositoryTransfer, méthode transfer()
Transfère l'ownership des blocs depuis ctx.blocks vers TopologyRepository par déplacement (std::move). Appelle ensuite TopologyData::buildIndex() pour construire les maps id → ptr.
Après transfer(), le PipelineContext est vide et le TopologyRepository est le seul détenteur des blocs.
GeoParsingTask encapsule l'exécution du pipeline dans un thread séparé pour ne pas bloquer le thread UI Win32. Elle :
parse() dans std::async.WM_PROGRESS_UPDATE, WM_PARSING_SUCCESS ou WM_PARSING_ERROR vers la HWND de la MainWindow.cancelToken (shared_ptr<atomic<bool>>) vérifié entre chaque phase — l'annulation propre lève GeoParser::CancelledException.| Phase | Progression |
|---|---|
| Phase 1 | 0 % |
| Phase 2 | 5 % |
| Phase 3 | 30 % |
| Phase 4 | 45 % |
| Phase 5 | 58 % |
| Phase 6 | 65 % |
| Phase 8a + Phase 7 | 75 % |
| Phase 8b | 90 % |
| Terminé | 100 % |