|
Simulateur Ferroviaire
Reconstruction et visualisation d'un réseau ferroviaire à partir de données GeoJSON — Win32 / WebView2 / Leaflet
|
Pipeline complet de transformation GeoJSON → modèle ferroviaire.
| Ordre | Phase | Classe | Input → Output |
|---|---|---|---|
| 1 | Chargement GeoJSON | Phase1_GeoLoader | fichier → RawNetwork (WGS84 + UTM) |
| 2 | Intersections géométriques | Phase2_GeometricIntersector | RawNetwork → IntersectionData (Cramer + grid binning) |
| 3 | Découpe des segments | Phase3_NetworkSplitter | RawNetwork + IntersectionData → SplitNetwork |
| 4 | Graphe planaire | Phase4_TopologyBuilder | SplitNetwork → TopologyGraph (Union-Find + snap) |
| 5 | Classification des nœuds | Phase5_SwitchClassifier | TopologyGraph → ClassifiedNodes (degré + angle) |
| 6 | Extraction des blocs | Phase6_BlockExtractor | ClassifiedNodes → BlockSet (DFS + subdivision) |
| 8a | Résolution des pointeurs | Phase8_RepositoryTransfer | BlockSet → pointeurs inter-blocs résolus |
| 7 | Traitement des switches | Phase7_SwitchProcessor | BlockSet → orientation + doubles + crossovers + tips |
| 8b | Transfert final | Phase8_RepositoryTransfer | BlockSet → TopologyRepository |
Ordre d'exécution réel dans GeoParser::parse() "GeoParser::parse()" : Phase1 → 2 → 3 → 4 → 5 → 6 → 8a::resolve → 7::run → 8b::transfer.
Phase 7 nécessite les pointeurs résolus par 8a (orientation géométrique et détection crossovers utilisent
getRootBlock()/getNormalBlock()/getDeviationBlock()). Phase 8b doit être la dernière — état final de TopologyRepository.
| Libéré après | Structure | Raison |
|---|---|---|
| Phase 3 | RawNetwork, IntersectionData | Consommées intégralement par Phase 3 |
| Phase 6 | TopologyGraph, ClassifiedNodes, SplitNetwork | Phase 5 utilise encore SplitNetwork; Phase 6 est la dernière à en avoir besoin |
| Phase 8b | BlockSet | Transféré vers TopologyRepository via std::move |
SplitNetworkest libéré en Phase 6 (et non Phase 4) parce que Phase5_SwitchClassifier l'utilise pour affiner les vecteurs d'angle.
GeoParsingTask lance GeoParser dans un thread détaché. Communication vers l'UI via PostMessage :
| Message | wParam | lParam | Signification |
|---|---|---|---|
WM_PROGRESS_UPDATE | 0-100 | std::wstring* label (à libérer) | Avancement |
WM_PARSING_SUCCESS | — | — | Parsing terminé |
WM_PARSING_ERROR | — | std::wstring* message (à libérer) | Échec |
WM_PARSING_CANCELLED | — | — | Annulation propre |
Annulation : GeoParsingTask::cancel() "GeoParsingTask::cancel()" positionne un shared_ptr<atomic<bool>> partagé avec GeoParser. Vérifié entre chaque phase via GeoParser::checkCancel() "GeoParser::checkCancel()" → lève GeoParser::CancelledException "GeoParser::CancelledException".
| Struct | Phase productrice | Contenu |
|---|---|---|
RawNetwork | Phase 1 | Polylignes WGS84 + UTM brutes |
IntersectionData | Phase 2 | Points d'intersection + grille spatiale |
SplitNetwork | Phase 3 | Segments atomiques sans intersection interne |
TopologyGraph | Phase 4 | Graphe planaire nœuds + arêtes + adjacence |
ClassifiedNodes | Phase 5 | NodeClass par nœud |
BlockSet | Phase 6 | unique_ptr<StraightBlock> + unique_ptr<SwitchBlock> + index lookup |
ParserConfig est un struct POD pur — transporteur de paramètres sans logique métier. ParserConfigIni gère uniquement la persistence .ini via SimpleIni.
| Paramètre | Défaut | Section .ini | Description |
|---|---|---|---|
snapTolerance | 3.0 m | [Topology] | Rayon de fusion des nœuds proches |
maxSegmentLength | 1000.0 m | [Topology] | Longueur maximale d'un StraightBlock avant subdivision |
intersectionEpsilon | 1.5 m | [Intersection] | Tolérance de détection d'intersection géométrique |
minSwitchAngle | 15.0° | [Switch] | Angle minimal pour identifier une bifurcation réelle |
junctionTrimMargin | 25.0 m | [Switch] | Marge de recadrage visuel aux jonctions |
doubleSwitchRadius | 50.0 m | [Switch] | Distance maximale entre deux switches pour former une double aiguille |
switchSideSize | 15.0 m | [Switch] | Longueur des branches CDC (tips root/normal/deviation) depuis la jonction |
minBranchLength | 100.0 m | [CDC] | Longueur minimale de branche pour la validation CDC |
Séparation POD / persistance (SRP) :
GeoParser reçoit ParserConfig par valeur — snapshot immuable pendant toute la durée d'un parsing, thread-safe par construction.
PipelineContext est le seul objet partagé entre toutes les phases.
PhaseStats — instrumentation intégrée :
Chaque phase enregistre sa durée et son compte d'éléments dans ctx.stats. GeoParser::logPerformanceSummary() "GeoParser::logPerformanceSummary()" produit le tableau de performance en fin de pipeline.
GeoParser possède PipelineContext et ParserConfig. Il enchaîne les phases et reporte la progression via callback.
| Méthode | Rôle |
|---|---|
| GeoParser::GeoParser() "GeoParser(config, logger, onProgress)" | Construction — snapshot de config |
| GeoParser::parse() "parse(filePath)" | Pipeline complet — phases 1 à 8 |
| GeoParser::reportProgress() "reportProgress(int)" | Callback UI + log de la dernière phase |
| GeoParser::logPerformanceSummary() "logPerformanceSummary()" | Tableau de performance final |
Fichiers : Phase1_GeoLoader.h/.cpp · RawNetwork.h
Charge le fichier GeoJSON, projette les coordonnées WGS-84 en UTM et produit le RawNetwork.
| Étape interne | Description |
|---|---|
| Lecture JSON | nlohmann::json::parse() — lève std::runtime_error si GeoJSON invalide |
| Filtrage | Seules les features LineString sont retenues |
| Détection zone UTM | Calculée depuis le premier point du premier segment |
| Projection | WGS-84 → UTM (formules ellipsoïde WGS-84 complètes, Phase1_GeoLoader::project) |
Sortie : ctx.rawNetwork — polylignes avec points WGS-84 et UTM synchronisés.
Fichiers : Phase2_GeometricIntersector.h/.cpp · IntersectionMap.h
Calcule tous les points d'intersection géométrique entre segments du RawNetwork.
| Mécanisme | Description |
|---|---|
| Algorithme | Cramer (résolution système linéaire 2×2) |
| Tolérance | config.intersectionEpsilon — évite les faux positifs sur flottants |
| Optimisation | Spatial grid binning : O(n·k) — seuls les segments de cellules adjacentes sont testés |
Sortie : ctx.intersections — map<SegmentId, vector<IntersectionPoint>>
Deux segments AB et CD se croisent si on peut écrire :
En égalisant :
det == 0 → segments parallèles. Sinon, t et u résolus par Cramer. Les segments se croisent si et seulement si t ∈ [0,1] et u ∈ [0,1].
Référence : https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection
Fichiers : Phase3_NetworkSplitter.h/.cpp · SplitNetwork.h
Découpe les polylignes brutes aux points d'intersection et aux dépassements de maxSegmentLength.
| Mécanisme | Description |
|---|---|
| Découpe aux intersections | Tri + dédoublonnage des paramètres t par distance curviligne |
| Filtrage micro-segments | Segments < 2 * intersectionEpsilon supprimés |
| Découpe par longueur | Interpolation linéaire UTM aux multiples de maxSegmentLength |
| Libération mémoire | ctx.rawNetwork.clear() + ctx.intersections.clear() après production |
Correction — cohérence globalIdx : Les polylignes dégénérées (pointsUTM.size() < 2) sont sautées sans incrémenter globalIdx, ce qui maintient la cohérence avec l'index calculé par Phase2_GeometricIntersector::globalSegmentIndex() "Phase2::globalSegmentIndex()". L'ancienne version incrémentait globalIdx dans ce cas, causant un décalage qui faisait ignorer des intersections valides.
Sortie : ctx.splitNetwork — vecteur d'AtomicSegment
Fichiers : Phase4_TopologyBuilder.h/.cpp · TopologyGraph.h
Construit le graphe planaire depuis les segments atomiques. Fusionne les extrémités proches via Union-Find + grid binning.
| Mécanisme | Description |
|---|---|
| Snap | Grid binning — nœuds dans un rayon snapTolerance mis en candidats |
| Union-Find | Fusion des nœuds candidats — path compression + union by rank |
| Graphe | Nœuds (positions UTM fusionnées) + arêtes (segments) + adjacence |
Correction : throw EXCEPTION_EXECUTE_FAULT remplacé par throw std::runtime_error(...) — catchable proprement par les handlers standards (catch (const std::exception&)) dans GeoParser::parse().
Note mémoire : splitNetwork n'est pas libéré ici (contrairement à l'ancienne version). Phase5_SwitchClassifier en a encore besoin pour les vecteurs d'angle.
Sortie : ctx.topoGraph — graphe planaire nœuds + arêtes UTM
Path compression : parent[x] = find(parent[x]) — aplatit l'arbre récursivement. Union by rank : la racine de rang inférieur pointe vers la racine de rang supérieur — évite les arbres dégénérés.
Référence : https://en.wikipedia.org/wiki/Disjoint-set_data_structure
Fichiers : Phase5_SwitchClassifier.h/.cpp · ClassifiedNodes.h
Attribue une NodeClass à chaque nœud du graphe planaire en combinant degré et angle.
Correction — suppression de la dépendance SplitNetwork : outVector() calculait auparavant la direction depuis les points intermédiaires du segment atomique (SplitNetwork). Il utilise maintenant directement la position UTM du nœud opposé dans le graphe — indépendant de SplitNetwork, et suffisamment précis après découpe par maxSegmentLength.
Sortie : ctx.classifiedNodes — unordered_map<size_t, NodeClass>
Fichiers : Phase6_BlockExtractor.h/.cpp · BlockSet.h
Transforme le graphe planaire classifié en blocs ferroviaires.
| Mécanisme | Description |
|---|---|
| Nœuds frontières | SWITCH, TERMINUS, CROSSING — délimitent les blocs |
| Nœuds transparents | STRAIGHT — traversés lors du DFS |
| DFS entre frontières | Parcours itératif — concatène les segments en voie droite |
| Subdivision | Si longueur > maxSegmentLength → N sous-blocs chaînés par prev/next |
| SwitchBlocks | Un bloc par nœud SWITCH |
| Libération | ctx.topoGraph.clear(), ctx.classifiedNodes.clear(), ctx.splitNetwork.clear() |
Sortie : ctx.blocks — BlockSet
L'ancienne déduplication processedPairs.insert(pairKey(nodeA, nodeB)) empêchait la création de deux straights entre les mêmes switches — cassant les configurations crossover (voie double). La marque à la place les arêtes utilisées (usedEdges) :
Deux straights empruntant des arêtes de départ distinctes peuvent ainsi coexister.
L'ancienne subdivision découpait par proportion de points (k * totalPts / N), ce qui produisait des sous-blocs inégaux si les points GeoJSON étaient mal répartis. La calcule d'abord les longueurs cumulées :
Puis pour le sous-bloc k, cherche le premier point i où cumLen[i] >= k/N * totalLen. Chaque sous-bloc a ainsi une longueur UTM quasi-identique indépendamment de la densité des points.
extractSwitches utilise straightByDirectedPair pour résoudre les endpoints de chaque branche. En cas de crossover (deux straights pour la même clé directionnelle), un ensemble usedStraights par switch garantit que chaque branche reçoit un straight distinct.
Les sous-blocs produits par subdivision sont chaînés immédiatement par prev/next dans registerStraight. Phase8_RepositoryTransfer::resolveStraight() "Phase8::resolveStraight()" ne surécrit ces pointeurs que si neighbourId est non vide — les endpoints internes (avec frontierNodeId == SIZE_MAX) ne sont jamais touchés, préservant la chaîne.
Fichiers : Phase7_SwitchProcessor.h/.cpp
Fusion de l'ancien Phase7_DoubleSwitchDetector et Phase8_SwitchOrientator. Les deux classes partageaient les mêmes préconditions (pointeurs résolus par Phase 8a) et s'enchaînaient naturellement — leur fusion réduit la surface du pipeline.
| Sous-phase | Description |
|---|---|
| G | Orientation géométrique root / normal / deviation |
| A | Détection des clusters double switch |
| B | Absorption du segment de liaison |
| C | Validation CDC (minBranchLength) |
| D | Détection des crossovers |
| E | Cohérence des crossovers (branches partagées → DEVIATION) |
| F | Calcul des tips CDC (switchSideSize) |
Root, normal et deviation sont déterminés par heuristique vectorielle sur les positions UTM des blocs adjacents — sans dépendance GeoJSON ni donnée de sens de circulation :
interpolateTip() parcourt la géométrie WGS84 du straight depuis l'extrémité la plus proche de la jonction et interpole le point à config.switchSideSize mètres (distance Haversine). Retourne l'extrémité distale si la branche est plus courte.
Fichiers : Phase8_RepositoryTransfer.h/.cpp
Transfère ctx.blocks vers TopologyRepository. Scindée en deux appels dans l'orchestrateur pour respecter la contrainte d'ordre avec Phase 7.
Ordre réel dans GeoParser::parse() "GeoParser::parse()" :
| Méthode | Rôle |
|---|---|
| Phase8_RepositoryTransfer::resolve() "resolve()" | Résolution des ShuntingElement* inter-blocs depuis les IDs câblés en Phase 6 |
| Phase8_RepositoryTransfer::transfer() "transfer()" | std::move des unique_ptr → TopologyData + buildIndex() |
resolveStraight() ne modifie setNeighbourPrev/Next que si neighbourId est non vide. Les sous-blocs internes d'un straight subdivisé (frontierNodeId == SIZE_MAX, neighbourId == "") ne sont jamais touchés — leur chaîne prev/next posée par Phase 6 est préservée.
Stabilité des adresses : TopologyData stocke en vector<unique_ptr<T>>. Après std::move, les adresses des objets alloués sont stables — les pointeurs résolus par resolve() restent valides après le transfert.
GeoParsingTask est le point d'entrée depuis MainWindow.
Cancel token partagé :
Le thread vérifie *m_cancelToken entre les phases. Si true, il envoie WM_PARSING_CANCELLED et s'arrête proprement sans altérer TopologyRepository.
| Message Win32 | Contenu | Handler |
|---|---|---|
WM_PROGRESS_UPDATE | Avancement 0–100 | MainWindow::onProgressUpdate() |
WM_PARSING_SUCCESS | — | MainWindow::onParsingSuccess() |
WM_PARSING_ERROR | std::string* (à libérer) | MainWindow::onParsingError() |
WM_PARSING_CANCELLED | — | MainWindow::onParsingCancelled() |
Fichiers : Engine/HMI/Dialogs/ParserSettingsDialog.h/.cpp
Dialogue modal Win32 permettant à l'utilisateur de modifier les 8 paramètres de ParserConfig depuis l'interface.
Comportement :
EDIT Win32 (dont IDC_EDIT_SWITCH_SIDE_SIZE pour switchSideSize)intersectionEpsilon < snapTolerance)ParserConfigIni::save() → rechargement dans MainWindowPattern : DialogBoxParam / WM_INITDIALOG / IDOK / IDCANCEL.