From 298b1d51741ed6996d9282a1cc5f7439a6b54322 Mon Sep 17 00:00:00 2001 From: Alexis Jacomy Date: Thu, 26 Mar 2026 14:40:21 +0100 Subject: [PATCH 1/3] refactor(od-graph): add path tracking to shortest path computation This commit prepares the computeShortestPaths algorithm to replace the algorithms currently used for analytics. The main goal is to allow retrieving the paths (trainrun section IDs) in addition to the total spent time. Details: - Track all traversed trainrun sections in computeShortestPaths, to allow reconstructing the paths in the end of the function - Extend computeShortestPaths to return Map rather than Map - Adds connections count as a tiebreaker for paths with similar cost Signed-off-by: Alexis Jacomy --- .../components/origin-destination.service.ts | 4 +- .../data/origin-destination.service.ts | 4 +- src/app/view/util/origin-destination-graph.ts | 95 ++++++++++++++++--- .../origin.destination.csv.test.spec.ts | 74 +++++++++++---- 4 files changed, 144 insertions(+), 33 deletions(-) diff --git a/src/app/services/analytics/origin-destination/components/origin-destination.service.ts b/src/app/services/analytics/origin-destination/components/origin-destination.service.ts index 394b0503b..bafd10b03 100644 --- a/src/app/services/analytics/origin-destination/components/origin-destination.service.ts +++ b/src/app/services/analytics/origin-destination/components/origin-destination.service.ts @@ -85,8 +85,8 @@ export class OriginDestinationService { const res = new Map(); odNodes.forEach((origin) => { computeShortestPaths(origin.getId(), neighbors, vertices, tsSuccessor).forEach( - (value, key) => { - res.set([origin.getId(), key].join(","), value); + ([cost, connections, _trainrunSectionIds], key) => { + res.set([origin.getId(), key].join(","), [cost, connections]); }, ); }); diff --git a/src/app/services/data/origin-destination.service.ts b/src/app/services/data/origin-destination.service.ts index 7be15af28..c87e3def2 100644 --- a/src/app/services/data/origin-destination.service.ts +++ b/src/app/services/data/origin-destination.service.ts @@ -71,8 +71,8 @@ export class OriginDestinationService { const res = new Map(); odNodes.forEach((origin) => { computeShortestPaths(origin.getId(), neighbors, vertices, tsSuccessor).forEach( - (value, key) => { - res.set([origin.getId(), key].join(","), value); + ([cost, connections, _trainrunSectionIds], key) => { + res.set([origin.getId(), key].join(","), [cost, connections]); }, ); }); diff --git a/src/app/view/util/origin-destination-graph.ts b/src/app/view/util/origin-destination-graph.ts index 4a986dbe2..9493cc350 100644 --- a/src/app/view/util/origin-destination-graph.ts +++ b/src/app/view/util/origin-destination-graph.ts @@ -31,7 +31,9 @@ export class Edge { } // In addition to edges, return a map of trainrunSection ids to their successor -// (in the forward direction), so we can check for connections. +// (in the forward direction), so we can check for connections, +// and a map of section ids to all section ids they cover (for non-stop +// traversals). export const buildEdges = ( nodes: Node[], odNodes: Node[], @@ -39,8 +41,14 @@ export const buildEdges = ( connectionPenalty: number, trainrunService: TrainrunService, timeLimit: number, -): [Edge[], Map] => { - const [sectionEdges, tsSuccessor] = buildSectionEdges(trainruns, trainrunService, timeLimit); +): [Edge[], Map, Map] => { + const sectionExpansion = new Map(); + const [sectionEdges, tsSuccessor] = buildSectionEdges( + trainruns, + trainrunService, + timeLimit, + sectionExpansion, + ); let edges = sectionEdges; // Both trainrun and trainrunSection ids are encoded in JSON keys. @@ -116,7 +124,7 @@ export const buildEdges = ( ), ]; - return [edges, tsSuccessor]; + return [edges, tsSuccessor, sectionExpansion]; }; // Given edges, return the neighbors (with weights) for each vertex, if any (outgoing adjacency list). @@ -148,19 +156,25 @@ export const topoSort = (graph: Map): Vertex[] => { return res.reverse(); }; -// Given a graph (adjacency list), and vertices in topological order, return the shortest paths (and connections) -// from a given node to other nodes. +// Given a graph (adjacency list), and vertices in topological order, return the +// shortest paths (and connections) from a given node to other nodes. Each +// result entry is [cost, connections, trainrunSectionIds]. export const computeShortestPaths = ( from: number, neighbors: Map, vertices: Vertex[], tsSuccessor: Map, -): Map => { + sectionExpansion?: Map, +): Map => { const tsPredecessor = new Map(); tsSuccessor.forEach((v, k) => { tsPredecessor.set(v, k); }); - const res = new Map(); + // Maps each reached nodeId to its arrival "convenience" vertex. + const destinations = new Map(); + // Maps each vertex to its best predecessor, for path reconstruction. + const prev = new Map(); + const res = new Map(); const dist = new Map(); let started = false; vertices.forEach((vertex) => { @@ -180,7 +194,7 @@ export const computeShortestPaths = ( dist.get(vertex) !== undefined && vertex.nodeId !== from ) { - res.set(vertex.nodeId, dist.get(vertex)); + destinations.set(vertex.nodeId, vertex); } const neighs = neighbors.get(vertex); if (neighs === undefined || dist.get(vertex) === undefined) { @@ -190,7 +204,8 @@ export const computeShortestPaths = ( // plus the weight of the edge connecting the neighbor to this vertex. neighs.forEach(([neighbor, weight]) => { const alt = dist.get(vertex)[0] + weight; - if (dist.get(neighbor) === undefined || alt < dist.get(neighbor)[0]) { + const prevDist = dist.get(neighbor); + if (prevDist === undefined || alt <= prevDist[0]) { let connection = 0; let successor = tsSuccessor; if (vertex.trainrunId < 0) { @@ -205,10 +220,43 @@ export const computeShortestPaths = ( ) { connection = 1; } - dist.set(neighbor, [alt, dist.get(vertex)[1] + connection]); + const newConnections = dist.get(vertex)[1] + connection; + // We use the connections count as a tiebreaker for paths with equal + // cost. + if (prevDist === undefined || alt < prevDist[0] || newConnections < prevDist[1]) { + dist.set(neighbor, [alt, newConnections]); + prev.set(neighbor, vertex); + } } }); }); + + // Walk back through prev to collect the trainrun section IDs forming each + // path. A section edge goes from a departure vertex to an arrival vertex, so + // we record the section ID whenever we step from an arrival back to a + // departure. + destinations.forEach((destVertex, nodeId) => { + const [cost, connections] = dist.get(destVertex); + const tsIds: number[] = []; + let current = destVertex; + while (prev.has(current)) { + const predecessor = prev.get(current); + if ( + predecessor.isDeparture && + !current.isDeparture && + current.trainrunSectionId !== undefined + ) { + const expanded = sectionExpansion?.get(current.trainrunSectionId); + if (expanded) { + tsIds.push(...expanded); + } else { + tsIds.push(current.trainrunSectionId); + } + } + current = predecessor; + } + res.set(nodeId, [cost, connections, tsIds.reverse()]); + }); return res; }; @@ -216,6 +264,7 @@ const buildSectionEdges = ( trainruns: Trainrun[], trainrunService: TrainrunService, timeLimit: number, + sectionExpansion: Map, ): [Edge[], Map] => { const edges = []; const its = trainrunService.getRootIterators(); @@ -227,14 +276,28 @@ const buildSectionEdges = ( return; } // Forward edges are calculated first, so we can use the successor map. - const forwardEdges = buildSectionEdgesFromIterator(tsIterator, false, timeLimit, tsSuccessor); + const forwardEdges = buildSectionEdgesFromIterator( + tsIterator, + false, + timeLimit, + tsSuccessor, + sectionExpansion, + ); // Add forward edges to round trip and one-way trainruns. edges.push(...forwardEdges); if (!trainrun.isRoundTrip()) return; // Don't forget the reverse direction for round trip trainruns. const ts = tsIterator.current().trainrunSection; const nextIterator = trainrunService.getIterator(tsIterator.current().node, ts); - edges.push(...buildSectionEdgesFromIterator(nextIterator, true, timeLimit, tsSuccessor)); + edges.push( + ...buildSectionEdgesFromIterator( + nextIterator, + true, + timeLimit, + tsSuccessor, + sectionExpansion, + ), + ); }); return [edges, tsSuccessor]; }; @@ -244,11 +307,13 @@ const buildSectionEdgesFromIterator = ( reverseIterator: boolean, timeLimit: number, tsSuccessor: Map, + sectionExpansion: Map, ): Edge[] => { const edges = []; let nonStopV1Time = -1; let nonStopV1Node = -1; let nonStopV1TsId = -1; + let nonStopTsIds: number[] = []; let previousTsId = -1; while (tsIterator.hasNext()) { tsIterator.next(); @@ -272,6 +337,7 @@ const buildSectionEdgesFromIterator = ( nonStopV1Node = v1Node; nonStopV1TsId = tsId; } + nonStopTsIds.push(tsId); continue; } let v1 = new Vertex(v1Node, true, v1Time, trainrunId, tsId); @@ -283,7 +349,10 @@ const buildSectionEdgesFromIterator = ( tsId = nonStopV1TsId; } v1 = new Vertex(nonStopV1Node, true, nonStopV1Time, trainrunId, tsId); + // Register all traversed sections for this representative id. + sectionExpansion.set(tsId, [...nonStopTsIds, ts.getId()]); nonStopV1Time = -1; + nonStopTsIds = []; } const v2Time = reverseSection ? ts.getSourceArrivalDto().consecutiveTime diff --git a/src/integration-testing/origin.destination.csv.test.spec.ts b/src/integration-testing/origin.destination.csv.test.spec.ts index b97b13f0d..d47f259ce 100644 --- a/src/integration-testing/origin.destination.csv.test.spec.ts +++ b/src/integration-testing/origin.destination.csv.test.spec.ts @@ -98,8 +98,8 @@ describe("Origin Destination CSV Test", () => { const res = new Map(); nodes.forEach((origin) => { computeShortestPaths(origin.getId(), neighbors, vertices, tsSuccessor).forEach( - (value, key) => { - res.set([origin.getId(), key].join(","), value); + ([cost, connections, _trainrunSectionIds], key) => { + res.set([origin.getId(), key].join(","), [cost, connections]); }, ); }); @@ -149,8 +149,8 @@ describe("Origin Destination CSV Test", () => { const res = new Map(); nodes.forEach((origin) => { computeShortestPaths(origin.getId(), neighbors, vertices, tsSuccessor).forEach( - (value, key) => { - res.set([origin.getId(), key].join(","), value); + ([cost, connections, _trainrunSectionIds], key) => { + res.set([origin.getId(), key].join(","), [cost, connections]); }, ); }); @@ -206,8 +206,8 @@ describe("Origin Destination CSV Test", () => { const res = new Map(); nodes.forEach((origin) => { computeShortestPaths(origin.getId(), neighbors, vertices, tsSuccessor).forEach( - (value, key) => { - res.set([origin.getId(), key].join(","), value); + ([cost, connections, _trainrunSectionIds], key) => { + res.set([origin.getId(), key].join(","), [cost, connections]); }, ); }); @@ -260,8 +260,8 @@ describe("Origin Destination CSV Test", () => { const res = new Map(); nodes.forEach((origin) => { computeShortestPaths(origin.getId(), neighbors, vertices, tsSuccessor).forEach( - (value, key) => { - res.set([origin.getId(), key].join(","), value); + ([cost, connections, _trainrunSectionIds], key) => { + res.set([origin.getId(), key].join(","), [cost, connections]); }, ); }); @@ -313,7 +313,7 @@ describe("Origin Destination CSV Test", () => { const distances0 = computeShortestPaths(0, neighbors, topoVertices, tsSuccessor); expect(distances0).toHaveSize(1); - expect(distances0.get(1)).toEqual([15, 0]); + expect(distances0.get(1)).toEqual([15, 0, [0]]); }); it("connection unit test", () => { @@ -372,18 +372,60 @@ describe("Origin Destination CSV Test", () => { const distances0 = computeShortestPaths(0, neighbors, topoVertices, tsSuccessor); expect(distances0).toHaveSize(2); - expect(distances0.get(1)).toEqual([15, 0]); - expect(distances0.get(2)).toEqual([30, 0]); + expect(distances0.get(1)).toEqual([15, 0, [0]]); + expect(distances0.get(2)).toEqual([30, 0, [0, 1]]); const distances1 = computeShortestPaths(1, neighbors, topoVertices, tsSuccessor); expect(distances1).toHaveSize(1); - expect(distances1.get(2)).toEqual([14, 0]); + expect(distances1.get(2)).toEqual([14, 0, [1]]); const distances3 = computeShortestPaths(3, neighbors, topoVertices, tsSuccessor); expect(distances3).toHaveSize(2); - expect(distances3.get(1)).toEqual([10, 0]); + expect(distances3.get(1)).toEqual([10, 0, [2]]); // connection - expect(distances3.get(2)).toEqual([30 + 5, 1]); + expect(distances3.get(2)).toEqual([30 + 5, 1, [2, 1]]); + }); + + it("tiebreaker prefers fewer connections when costs are equal", () => { + // Graph: node 0 -> node 1 -> node 2 + // Trainrun 0 goes 0->1->2 (no transfer at node 1). + // Trainrun 1 also goes 1->2 with equal cost, but requires a transfer at node 1. + // The tiebreaker should prefer trainrun 0 (0 connections) over trainrun 1 (1 connection). + + // trainrun 0: node 0 -> node 1 (section 0), node 1 -> node 2 (section 1) + const v1 = new Vertex(0, true); + const v2 = new Vertex(0, true, 0, 0, 0); + const v3 = new Vertex(1, false, 10, 0, 0); + const v4 = new Vertex(1, true, 11, 0, 1); + const v5 = new Vertex(2, false, 21, 0, 1); + const v6 = new Vertex(2, false); + const e1 = new Edge(v1, v2, 0); + const e2 = new Edge(v2, v3, 10); + const e3 = new Edge(v3, v4, 1); // same-train continuation + const e4 = new Edge(v4, v5, 10); + const e5 = new Edge(v5, v6, 0); + + // trainrun 1: node 1 -> node 2 (section 2), same total cost but requires transfer + const v7 = new Vertex(1, true, 12, 1, 2); + const v8 = new Vertex(2, false, 21, 1, 2); + const e6 = new Edge(v3, v7, 1); // transfer from trainrun 0 to trainrun 1 + const e7 = new Edge(v7, v8, 10); + const e8 = new Edge(v8, v6, 0); + + // convenience + const v9 = new Vertex(1, false); + const e9 = new Edge(v3, v9, 0); + + const edges = [e1, e2, e3, e4, e5, e6, e7, e8, e9]; + const tsSuccessor = new Map([[0, 1]]); + + const neighbors = computeNeighbors(edges); + const topoVertices = topoSort(neighbors); + const distances = computeShortestPaths(0, neighbors, topoVertices, tsSuccessor); + + // Node 2 should be reached via trainrun 0 (sections [0, 1], 0 connections), + // not via trainrun 1 (sections [0, 2], 1 connection). + expect(distances.get(2)).toEqual([21, 0, [0, 1]]); }); it("round trips and one-ways test", () => { @@ -412,8 +454,8 @@ describe("Origin Destination CSV Test", () => { const res = new Map(); nodes.forEach((origin) => { computeShortestPaths(origin.getId(), neighbors, vertices, tsSuccessor).forEach( - (value, key) => { - res.set([origin.getId(), key].join(","), value); + ([cost, connections, _trainrunSectionIds], key) => { + res.set([origin.getId(), key].join(","), [cost, connections]); }, ); }); From dd0148b592518c3d12b7c26447c454a0102d6e86 Mon Sep 17 00:00:00 2001 From: Alexis Jacomy Date: Thu, 26 Mar 2026 17:29:00 +0100 Subject: [PATCH 2/3] refactor(analytics): rewrite shortest path to use OD graph algorithm This commit replaces ShortestTravelTimeSearch with the existing OD graph utilities (buildEdges, computeNeighbors, topoSort, computeShortestPaths). This removes the FilterService dependency from AnalyticsService, as visibility filtering is now handled upstream by getVisibleTrainruns. Signed-off-by: Alexis Jacomy --- .../services/analytics/analytics.service.ts | 91 ++++++++++++++----- src/app/services/ui/filter.service.spec.ts | 9 +- .../ui/ui.interaction.service.spec.ts | 7 +- 3 files changed, 70 insertions(+), 37 deletions(-) diff --git a/src/app/services/analytics/analytics.service.ts b/src/app/services/analytics/analytics.service.ts index d18cb43a7..db1a738b1 100644 --- a/src/app/services/analytics/analytics.service.ts +++ b/src/app/services/analytics/analytics.service.ts @@ -2,10 +2,14 @@ import {Injectable, OnDestroy} from "@angular/core"; import {NodeService} from "../data/node.service"; import {TrainrunSectionService} from "../data/trainrunsection.service"; import {TrainrunService} from "../data/trainrun.service"; -import {ShortestTravelTimeSearch} from "./algorithms/shortest-travel-time-search"; import {BehaviorSubject, Subject} from "rxjs"; import {ShortestDistanceNode} from "./algorithms/shortest-distance-node"; -import {FilterService} from "../ui/filter.service"; +import { + buildEdges, + computeNeighbors, + computeShortestPaths, + topoSort, +} from "src/app/view/util/origin-destination-graph"; @Injectable({ providedIn: "root", @@ -19,7 +23,6 @@ export class AnalyticsService implements OnDestroy { private nodeService: NodeService, private trainrunSectionService: TrainrunSectionService, private trainrunService: TrainrunService, - private filterService: FilterService, ) {} ngOnDestroy() { @@ -28,36 +31,76 @@ export class AnalyticsService implements OnDestroy { } calculateShortestDistanceNodesFromStartingNode(departureNodeId: number) { - const shortestTravelTimeSearch = new ShortestTravelTimeSearch( - this.trainrunService, - this.trainrunSectionService, - this.nodeService, - this.filterService, - ); - this.allShortestDistanceNodesUpdated( - shortestTravelTimeSearch.calculateShortestDistanceNodesFromStartingNode(departureNodeId), - ); + this.findAllShortestDistanceNodesSubject.next(this.findShortestDistanceNodes(departureNodeId)); } calculateShortestDistanceNodesFromStartingTrainrunSection( trainrunSectionId: number, departureNodeId: number, ) { - const shortestTravelTimeSearch = new ShortestTravelTimeSearch( + this.findAllShortestDistanceNodesSubject.next( + this.findShortestDistanceNodes(departureNodeId, trainrunSectionId), + ); + } + + /** + * Compute shortest paths from a departure node using the OD graph algorithm. + * When startTrainrunSectionId is set, the first hop is constrained to that + * section, and paths back to the departure node are excluded. + */ + private findShortestDistanceNodes( + departureNodeId: number, + startTrainrunSectionId?: number, + ): ShortestDistanceNode[] { + const nodes = this.nodeService.getNodes(); + const trainruns = this.trainrunService.getVisibleTrainruns(); + // No connection penalty. + const connectionPenalty = 0; + // Same time window as the OD matrix. + const timeLimit = 16 * 60; + + const [edges, tsSuccessor, sectionExpansion] = buildEdges( + nodes, + nodes, + trainruns, + connectionPenalty, this.trainrunService, - this.trainrunSectionService, - this.nodeService, - this.filterService, + timeLimit, ); - this.allShortestDistanceNodesUpdated( - shortestTravelTimeSearch.calculateShortestDistanceNodesFromStartingTrainrunSection( - trainrunSectionId, - departureNodeId, - ), + const filteredEdges = + startTrainrunSectionId !== undefined + ? edges.filter(({v1, v2}) => { + // Exclude all edges leaving the departure node, except the one that + // follows the trainrun section. + if (v1.nodeId === departureNodeId && v1.isDeparture) { + return v2.trainrunSectionId === startTrainrunSectionId; + } + // Exclude all edges entering the departure node. + const arrivesBackAtDeparture = !v2.isDeparture && v2.nodeId === departureNodeId; + return !arrivesBackAtDeparture; + }) + : edges; + const neighbors = computeNeighbors(filteredEdges); + const vertices = topoSort(neighbors); + const results = computeShortestPaths( + departureNodeId, + neighbors, + vertices, + tsSuccessor, + sectionExpansion, ); - } - private allShortestDistanceNodesUpdated(shortestDistanceNodes: ShortestDistanceNode[]) { - this.findAllShortestDistanceNodesSubject.next(Object.assign([], shortestDistanceNodes)); + const departureNode = this.nodeService.getNodeFromId(departureNodeId); + const shortestDistanceNodes: ShortestDistanceNode[] = [ + new ShortestDistanceNode(departureNode, 0), + ]; + results.forEach(([cost, _connections, tsIds], nodeId) => { + const sdn = new ShortestDistanceNode(this.nodeService.getNodeFromId(nodeId), cost); + sdn.setPath(tsIds.map((id) => this.trainrunSectionService.getTrainrunSectionFromId(id))); + shortestDistanceNodes.push(sdn); + }); + shortestDistanceNodes.sort((a, b) => a.distance - b.distance); + + return shortestDistanceNodes; } } diff --git a/src/app/services/ui/filter.service.spec.ts b/src/app/services/ui/filter.service.spec.ts index da247a1dd..9330f4884 100644 --- a/src/app/services/ui/filter.service.spec.ts +++ b/src/app/services/ui/filter.service.spec.ts @@ -12,7 +12,7 @@ import {LogPublishersService} from "../../logger/log.publishers.service"; import {LabelGroupService} from "../data/labelgroup.service"; import {LabelService} from "../data/label.service"; import {NetzgrafikUnitTesting} from "../../../integration-testing/netzgrafik.unit.testing"; -import {FilterService} from "../ui/filter.service"; +import {FilterService} from "./filter.service"; import {AnalyticsService} from "../analytics/analytics.service"; import {NetzgrafikColoringService} from "../data/netzgrafikColoring.service"; import {FilterSetting} from "../../models/filterSettings.model"; @@ -69,12 +69,7 @@ describe("FilterService", () => { netzgrafikColoringService, ); nodeService.nodes.subscribe((updatesNodes) => (nodes = updatesNodes)); - analyticsService = new AnalyticsService( - nodeService, - trainrunSectionService, - trainrunService, - filterService, - ); + analyticsService = new AnalyticsService(nodeService, trainrunSectionService, trainrunService); nodeService.nodes.subscribe((updatesNodes) => (nodes = updatesNodes)); trainrunSectionService.trainrunSections.subscribe( diff --git a/src/app/services/ui/ui.interaction.service.spec.ts b/src/app/services/ui/ui.interaction.service.spec.ts index 6e82c4d65..4a0e8a223 100644 --- a/src/app/services/ui/ui.interaction.service.spec.ts +++ b/src/app/services/ui/ui.interaction.service.spec.ts @@ -60,12 +60,7 @@ describe("UiInteractionService", () => { filterService, ); noteService = new NoteService(logService, labelService, filterService); - analyticsService = new AnalyticsService( - nodeService, - trainrunSectionService, - trainrunService, - filterService, - ); + analyticsService = new AnalyticsService(nodeService, trainrunSectionService, trainrunService); netzgrafikColoringService = new NetzgrafikColoringService(logService); dataService = new DataService( resourceService, From 0ec5f3e990b8d050f5c80257d71c116224ed4f7d Mon Sep 17 00:00:00 2001 From: Alexis Jacomy Date: Thu, 26 Mar 2026 17:35:05 +0100 Subject: [PATCH 3/3] refactor(analytics): cleans no longer used shortest path algorithm code This commit removes ShortestTravelTimeSearch and ShortestDistanceEdge, replaced with the OD graph utilities in the previous commit. Signed-off-by: Alexis Jacomy --- .../algorithms/shortest-distance-edge.spec.ts | 46 --- .../algorithms/shortest-distance-edge.ts | 67 ---- .../shortest-travel-time-search.spec.ts | 246 ------------ .../algorithms/shortest-travel-time-search.ts | 370 ------------------ 4 files changed, 729 deletions(-) delete mode 100644 src/app/services/analytics/algorithms/shortest-distance-edge.spec.ts delete mode 100644 src/app/services/analytics/algorithms/shortest-distance-edge.ts delete mode 100644 src/app/services/analytics/algorithms/shortest-travel-time-search.spec.ts delete mode 100644 src/app/services/analytics/algorithms/shortest-travel-time-search.ts diff --git a/src/app/services/analytics/algorithms/shortest-distance-edge.spec.ts b/src/app/services/analytics/algorithms/shortest-distance-edge.spec.ts deleted file mode 100644 index 6782c2464..000000000 --- a/src/app/services/analytics/algorithms/shortest-distance-edge.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -import {Node} from "../../../models/node.model"; -import {ShortestDistanceEdge} from "./shortest-distance-edge"; - -describe("ShortestDistanceEdge", () => { - it("getFromNode", () => { - const node1 = new Node(); - const node2 = new Node(); - const edge: ShortestDistanceEdge = new ShortestDistanceEdge(node1, node2, 19, 78, []); - expect(edge.getFromNode().getId()).toBe(node1.getId()); - }); - - it("getToNode", () => { - const node1 = new Node(); - const node2 = new Node(); - const edge: ShortestDistanceEdge = new ShortestDistanceEdge(node1, node2, 19, 78, []); - expect(edge.getToNode().getId()).toBe(node2.getId()); - }); - - it("getArrivalTime", () => { - const node1 = new Node(); - const node2 = new Node(); - const edge: ShortestDistanceEdge = new ShortestDistanceEdge(node1, node2, 19, 78, []); - expect(edge.getArrivalTime()).toBe(78); - }); - - it("getFullDistance", () => { - const node1 = new Node(); - const node2 = new Node(); - const edge: ShortestDistanceEdge = new ShortestDistanceEdge(node1, node2, 19, 78, []); - expect(edge.getFullDistance()).toBe(59); - }); - - it("getFromNodeDepartingTrainrunSection - case 1", () => { - const node1 = new Node(); - const node2 = new Node(); - const edge: ShortestDistanceEdge = new ShortestDistanceEdge(node1, node2, 19, 78, []); - expect(edge.getFromNodeDepartingTrainrunSection()).toBe(undefined); - }); - - it("getFromNodeDepartingTrainrunSection - case 2", () => { - const node1 = new Node(); - const node2 = new Node(); - const edge: ShortestDistanceEdge = new ShortestDistanceEdge(node1, node2, 19, 78, []); - expect(edge.getFromNodeDepartingTrainrunSection()).toBe(undefined); - }); -}); diff --git a/src/app/services/analytics/algorithms/shortest-distance-edge.ts b/src/app/services/analytics/algorithms/shortest-distance-edge.ts deleted file mode 100644 index 97d3737c6..000000000 --- a/src/app/services/analytics/algorithms/shortest-distance-edge.ts +++ /dev/null @@ -1,67 +0,0 @@ -import {Node} from "../../../models/node.model"; -import {TrainrunSection} from "../../../models/trainrunsection.model"; - -export class ShortestDistanceEdge { - private readonly fullPath: TrainrunSection[]; - private fullDistance: number; - - constructor( - private fromNode: Node, - private toNode: Node, - private departureTime: number, - private arrivalTime: number, - private path: TrainrunSection[], - ) { - this.fullPath = Object.assign([], this.path); - this.fullDistance = this.getTravelTime(); - } - - mergePathAndUpdateCost(inPath: TrainrunSection[], inDistance: number) { - this.fullPath.reverse(); - Object.assign([], inPath) - .reverse() - .forEach((ts: TrainrunSection) => { - this.fullPath.push(ts); - }); - this.fullPath.reverse(); - this.fullDistance += inDistance; - } - - getFullPath() { - return Object.assign([], this.fullPath); - } - - getFullDistance(): number { - return this.fullDistance; - } - - getToNode(): Node { - return this.toNode; - } - - getToNodeArrivingTrainrunSection(): TrainrunSection { - if (this.path.length === 0) { - return undefined; - } - return this.path[this.path.length - 1]; - } - - getFromNode(): Node { - return this.fromNode; - } - - getFromNodeDepartingTrainrunSection(): TrainrunSection { - if (this.path.length === 0) { - return undefined; - } - return this.path[0]; - } - - getArrivalTime(): number { - return this.arrivalTime; - } - - private getTravelTime(): number { - return this.arrivalTime - this.departureTime; - } -} diff --git a/src/app/services/analytics/algorithms/shortest-travel-time-search.spec.ts b/src/app/services/analytics/algorithms/shortest-travel-time-search.spec.ts deleted file mode 100644 index cc80b6b5f..000000000 --- a/src/app/services/analytics/algorithms/shortest-travel-time-search.spec.ts +++ /dev/null @@ -1,246 +0,0 @@ -import {DataService} from "../../data/data.service"; -import {NodeService} from "../../data/node.service"; -import {ResourceService} from "../../data/resource.service"; -import {TrainrunService} from "../../data/trainrun.service"; -import {TrainrunSectionService} from "../../data/trainrunsection.service"; -import {StammdatenService} from "../../data/stammdaten.service"; -import {NoteService} from "../../data/note.service"; -import {Node} from "../../../models/node.model"; -import {TrainrunSection} from "../../../models/trainrunsection.model"; -import {LogService} from "../../../logger/log.service"; -import {LogPublishersService} from "../../../logger/log.publishers.service"; -import {LabelGroupService} from "../../data/labelgroup.service"; -import {LabelService} from "../../data/label.service"; -import {NetzgrafikUnitTesting} from "../../../../integration-testing/netzgrafik.unit.testing"; -import {AnalyticsService} from "../analytics.service"; -import {FilterService} from "../../ui/filter.service"; -import {ShortestDistanceNode} from "./shortest-distance-node"; -import {ShortestDistanceEdge} from "./shortest-distance-edge"; -import {NetzgrafikColoringService} from "../../data/netzgrafikColoring.service"; - -describe("ShortestTravelTimeSearch", () => { - let dataService: DataService; - let nodeService: NodeService; - let resourceService: ResourceService; - let trainrunService: TrainrunService; - let trainrunSectionService: TrainrunSectionService; - let stammdatenService: StammdatenService; - let noteService: NoteService; - let nodes: Node[] = null; - let trainrunSections: TrainrunSection[] = null; - let logService: LogService = null; - let logPublishersService: LogPublishersService = null; - let labelGroupService: LabelGroupService = null; - let labelService: LabelService = null; - let analyticsService: AnalyticsService = null; - let filterService: FilterService = null; - let netzgrafikColoringService: NetzgrafikColoringService = null; - - beforeEach(() => { - stammdatenService = new StammdatenService(); - resourceService = new ResourceService(); - logPublishersService = new LogPublishersService(); - logService = new LogService(logPublishersService); - labelGroupService = new LabelGroupService(logService); - labelService = new LabelService(logService, labelGroupService); - filterService = new FilterService(labelService, labelGroupService); - trainrunService = new TrainrunService(logService, labelService, filterService); - trainrunSectionService = new TrainrunSectionService(logService, trainrunService, filterService); - nodeService = new NodeService( - logService, - resourceService, - trainrunService, - trainrunSectionService, - labelService, - filterService, - ); - noteService = new NoteService(logService, labelService, filterService); - analyticsService = new AnalyticsService( - nodeService, - trainrunSectionService, - trainrunService, - filterService, - ); - netzgrafikColoringService = new NetzgrafikColoringService(logService); - dataService = new DataService( - resourceService, - nodeService, - trainrunSectionService, - trainrunService, - stammdatenService, - noteService, - labelService, - labelGroupService, - filterService, - netzgrafikColoringService, - ); - nodeService.nodes.subscribe((updatesNodes) => (nodes = updatesNodes)); - - nodeService.nodes.subscribe((updatesNodes) => (nodes = updatesNodes)); - trainrunSectionService.trainrunSections.subscribe( - (updatesTrainrunSections) => (trainrunSections = updatesTrainrunSections), - ); - }); - - it("ShortestDistanceEdge", () => { - const node1 = new Node(); - const node2 = new Node(); - const edge: ShortestDistanceEdge = new ShortestDistanceEdge(node1, node2, 19, 78, []); - expect(edge.getFromNode().getId()).toBe(node1.getId()); - expect(edge.getToNode().getId()).toBe(node2.getId()); - expect(edge.getArrivalTime()).toBe(78); - expect(edge.getFromNodeDepartingTrainrunSection()).toBe(undefined); - expect(edge.getFullDistance()).toBe(59); - }); - - it("ShortestDistanceEdge - 2", () => { - const node1 = new Node(); - const node2 = new Node(); - const ts = new TrainrunSection(); - const edge: ShortestDistanceEdge = new ShortestDistanceEdge(node1, node2, 19, 78, [ts]); - expect(edge.getFromNode().getId()).toBe(node1.getId()); - expect(edge.getToNode().getId()).toBe(node2.getId()); - expect(edge.getArrivalTime()).toBe(78); - expect(edge.getFromNodeDepartingTrainrunSection().getId()).toBe(ts.getId()); - expect(edge.getFullDistance()).toBe(59); - }); - - it("Search shortest distance nodes: Starting trainrun section (ZUE -> OL)", () => { - dataService.loadNetzgrafikDto(NetzgrafikUnitTesting.getUnitTestNetzgrafik()); - expect(nodes.length).toBe(5); - expect(trainrunSections.length).toBe(8); - - let shortestDistancenodeData: ShortestDistanceNode[] = []; - analyticsService.shortestDistanceNode.subscribe((data) => (shortestDistancenodeData = data)); - analyticsService.calculateShortestDistanceNodesFromStartingTrainrunSection(1, 2); - shortestDistancenodeData.forEach((sdn) => { - switch (sdn.node.getId()) { - case 0: - if (sdn !== undefined) { - expect(sdn.node.getBetriebspunktName()).toBe("BN"); - expect(sdn.distance).toBe(22); - } - break; - case 1: - if (sdn !== undefined) { - expect(sdn.node.getBetriebspunktName()).toBe("OL"); - expect(sdn.distance).toBe(10); - } - break; - case 2: - if (sdn !== undefined) { - expect(sdn.node.getBetriebspunktName()).toBe("ZUE"); - expect(sdn.distance).toBe(0); - } - break; - case 3: - if (sdn !== undefined) { - expect(sdn.node.getBetriebspunktName()).toBe("SG"); - expect("not reachable").toBe( - "this is a bug in the method, should no occur in the ShortestDistanceNode[]", - ); - } - break; - case 4: - if (sdn !== undefined) { - expect(sdn.node.getBetriebspunktName()).toBe("CH"); - expect("not reachable").toBe( - "this is a bug in the method, should no occur in the ShortestDistanceNode[]", - ); - } - break; - } - }); - - expect(shortestDistancenodeData.length === nodes.length); - }); - - it("Search shortest distance nodes: Starting Node: BN", () => { - dataService.loadNetzgrafikDto(NetzgrafikUnitTesting.getUnitTestNetzgrafik()); - expect(nodes.length).toBe(5); - expect(trainrunSections.length).toBe(8); - - let shortestDistancenodeData: ShortestDistanceNode[] = []; - analyticsService.shortestDistanceNode.subscribe((data) => (shortestDistancenodeData = data)); - analyticsService.calculateShortestDistanceNodesFromStartingNode(0); - - shortestDistancenodeData.forEach((sdn) => { - switch (sdn.node.getId()) { - case 0: - expect(sdn.node.getBetriebspunktName()).toBe("BN"); - expect(sdn.distance).toBe(0); - break; - case 1: - expect(sdn.node.getBetriebspunktName()).toBe("OL"); - expect(sdn.distance).toBe(10); - break; - case 2: - expect(sdn.node.getBetriebspunktName()).toBe("ZUE"); - expect(sdn.distance).toBe(22); - break; - case 4: - expect(sdn.node.getBetriebspunktName()).toBe("CH"); - expect(sdn.distance).toBe(60); - break; - case 3: - expect(sdn.node.getBetriebspunktName()).toBe("SG"); - expect(sdn.distance).toBe(81); - break; - } - }); - - expect(shortestDistancenodeData.length === nodes.length); - }); - - it("Search shortest distance nodes: Starting Node: BN with filtering", () => { - dataService.loadNetzgrafikDto(NetzgrafikUnitTesting.getUnitTestNetzgrafik()); - expect(nodes.length).toBe(5); - expect(trainrunSections.length).toBe(8); - - filterService.resetFilterTrainrunCategory(); - filterService.disableFilterTrainrunCategory(dataService.getTrainrunCategory(1)); - filterService.disableFilterTrainrunCategory(dataService.getTrainrunCategory(3)); - - let shortestDistancenodeData: ShortestDistanceNode[] = []; - analyticsService.shortestDistanceNode.subscribe((data) => (shortestDistancenodeData = data)); - analyticsService.calculateShortestDistanceNodesFromStartingNode(0); - shortestDistancenodeData.forEach((sdn) => { - switch (sdn.node.getId()) { - case 0: - if (sdn !== undefined) { - expect(sdn.node.getBetriebspunktName()).toBe("BN"); - expect(sdn.distance).toBe(0); - } - break; - case 1: - if (sdn !== undefined) { - expect(sdn.node.getBetriebspunktName()).toBe("OL"); - expect(sdn.distance).toBe(75); - } - break; - case 2: - if (sdn !== undefined) { - expect(sdn.node.getBetriebspunktName()).toBe("ZUE"); - expect(sdn.distance).toBe(49); - } - break; - case 3: - if (sdn !== undefined) { - expect(sdn.node.getBetriebspunktName()).toBe("SG"); - expect(sdn.distance).toBe(101); - } - break; - case 4: - if (sdn !== undefined) { - expect(sdn.node.getBetriebspunktName()).toBe("CH"); - expect("not reachable").toBe( - "this is a bug in the method, should no occur in the ShortestDistanceNode[]", - ); - } - break; - } - }); - - expect(shortestDistancenodeData.length === nodes.length); - }); -}); diff --git a/src/app/services/analytics/algorithms/shortest-travel-time-search.ts b/src/app/services/analytics/algorithms/shortest-travel-time-search.ts deleted file mode 100644 index 2414d804e..000000000 --- a/src/app/services/analytics/algorithms/shortest-travel-time-search.ts +++ /dev/null @@ -1,370 +0,0 @@ -import {TrainrunSection} from "../../../models/trainrunsection.model"; -import {Node} from "../../../models/node.model"; -import {Port} from "../../../models/port.model"; -import {TrainrunSectionService} from "../../data/trainrunsection.service"; -import {NodeService} from "../../data/node.service"; -import {TrainrunService} from "../../data/trainrun.service"; -import {ShortestDistanceNode} from "./shortest-distance-node"; -import {ShortestDistanceEdge} from "./shortest-distance-edge"; -import {FilterService} from "../../ui/filter.service"; -import {Direction} from "src/app/data-structures/business.data.structures"; - -// -// The shortest travel time search method is based on the Dijkstra Algorithm. -// https://de.wikipedia.org/wiki/Dijkstra-Algorithmus -// -export class ShortestTravelTimeSearch { - private simulationChangeTrainPenalty: number; - private simulationDepartureMinute: number; - - constructor( - private trainrunService: TrainrunService, - private trainrunSectionService: TrainrunSectionService, - private nodeService: NodeService, - private filterService: FilterService, - ) { - this.simulationDepartureMinute = 0; - this.simulationChangeTrainPenalty = 0; - } - - private static getInitialShortestDistanceEdges( - node: Node, - initDepartureTime: number, - ): ShortestDistanceEdge[] { - const initialEdges: ShortestDistanceEdge[] = []; - initialEdges.push( - new ShortestDistanceEdge(node, node, initDepartureTime, initDepartureTime, []), - ); - return initialEdges; - } - - private static correctHourOverflowFromToTime(fromTime: number, toTime: number) { - let correctToTime = toTime; - if (fromTime > toTime) { - correctToTime += 60; - } - return correctToTime; - } - - private static getTransitionCost( - incomingEdge: ShortestDistanceEdge, - outgoingEdge: ShortestDistanceEdge, - currentFrequency: number, - changeTrainPenalty: number, - ) { - let arrivalTime = incomingEdge.getArrivalTime(); - let arrivalTrainrunId = -1; - if (incomingEdge.getToNodeArrivingTrainrunSection() !== undefined) { - arrivalTime = incomingEdge - .getToNode() - .getArrivalTime(incomingEdge.getToNodeArrivingTrainrunSection()); - arrivalTrainrunId = incomingEdge.getToNodeArrivingTrainrunSection().getTrainrunId(); - } - - const departureTimeOrg = outgoingEdge - .getFromNode() - .getDepartureTime(outgoingEdge.getFromNodeDepartingTrainrunSection()); - const departureTime = ShortestTravelTimeSearch.correctHourOverflowFromToTime( - arrivalTime, - (departureTimeOrg + currentFrequency) % 60, - ); - - let transitionCost = departureTime - arrivalTime; - if ( - arrivalTrainrunId !== -1 && - arrivalTrainrunId !== outgoingEdge.getFromNodeDepartingTrainrunSection().getTrainrunId() - ) { - if (transitionCost < outgoingEdge.getFromNode().getConnectionTime()) { - return undefined; - } - transitionCost += changeTrainPenalty; - } - - return transitionCost; - } - - private static updateFinalAndStackData( - outgoingEdge: ShortestDistanceEdge, - finalShortestDistanceNodes: ShortestDistanceNode[], - shortestDistanceEdgeStack: ShortestDistanceEdge[], - ) { - const shortestDistanceNode = new ShortestDistanceNode( - outgoingEdge.getToNode(), - outgoingEdge.getFullDistance(), - ); - shortestDistanceNode.setPath(outgoingEdge.getFullPath()); - - const existingShortestDistanceNode = finalShortestDistanceNodes.find( - (sdn: ShortestDistanceNode) => sdn.node === shortestDistanceNode.node, - ); - if (existingShortestDistanceNode === undefined) { - finalShortestDistanceNodes.push(shortestDistanceNode); - shortestDistanceEdgeStack.push(outgoingEdge); - } else { - if (existingShortestDistanceNode.distance > shortestDistanceNode.distance) { - finalShortestDistanceNodes = finalShortestDistanceNodes.filter( - (sdn: ShortestDistanceNode) => sdn.node !== shortestDistanceNode.node, - ); - finalShortestDistanceNodes.push(shortestDistanceNode); - shortestDistanceEdgeStack.push(outgoingEdge); - } - } - finalShortestDistanceNodes.sort( - (a: ShortestDistanceNode, b: ShortestDistanceNode) => a.distance - b.distance, - ); - return finalShortestDistanceNodes; - } - - private static isEdgeChangeAllowed( - incomingEdge: ShortestDistanceEdge, - departureTrainrunSection: TrainrunSection, - ): boolean { - const trans = incomingEdge.getToNode().getTransition(departureTrainrunSection.getId()); - if (trans === undefined) { - return true; - } - return !trans.getIsNonStopTransit(); - } - - private static isDirectionCompatible( - currentNode: Node, - departureTrainrunSection: TrainrunSection, - ): boolean { - // ROUND_TRIP: always compatible - // ONE_WAY: only allow if currentNode is the source node - return ( - departureTrainrunSection.getTrainrun().isRoundTrip() || - departureTrainrunSection.getSourceNodeId() === currentNode.getId() - ); - } - - calculateShortestDistanceNodesFromStartingNode(departureNodeId: number): ShortestDistanceNode[] { - const departureNode = this.nodeService.getNodeFromId(departureNodeId); - const initialShortestDistanceNode = new ShortestDistanceNode(departureNode, 0); - const shortestDistanceEdgeStack: ShortestDistanceEdge[] = - ShortestTravelTimeSearch.getInitialShortestDistanceEdges( - departureNode, - this.getSimulationDepartureMinute(), - ); - const initialFinalShortestDistanceNodes: ShortestDistanceNode[] = [initialShortestDistanceNode]; - return Object.assign( - [], - this.findAllShortestDistanceNodes( - initialFinalShortestDistanceNodes, - shortestDistanceEdgeStack, - ), - ); - } - - calculateShortestDistanceNodesFromStartingTrainrunSection( - trainrunSectionId: number, - departureNodeId: number, - ): ShortestDistanceNode[] { - const departureNode = this.nodeService.getNodeFromId(departureNodeId); - const departureTrainrunSection = - this.trainrunSectionService.getTrainrunSectionFromId(trainrunSectionId); - const initialShortestDistanceNodeFrom = new ShortestDistanceNode(departureNode, 0); - const initialShortestDistanceNodeTo = new ShortestDistanceNode( - departureNode.getOppositeNode(departureTrainrunSection), - departureTrainrunSection.getTravelTime(), - ); - initialShortestDistanceNodeTo.setPath([departureTrainrunSection]); - const outgoingEdge = this.getOutgoingEdge(departureTrainrunSection, departureNode); - const shortestDistanceEdgeStack: ShortestDistanceEdge[] = [outgoingEdge]; - let initialFinalShortestDistanceNodes: ShortestDistanceNode[] = [ - initialShortestDistanceNodeFrom, - initialShortestDistanceNodeTo, - ]; - initialFinalShortestDistanceNodes = ShortestTravelTimeSearch.updateFinalAndStackData( - outgoingEdge, - initialFinalShortestDistanceNodes, - shortestDistanceEdgeStack, - ); - return Object.assign( - [], - this.findAllShortestDistanceNodes( - initialFinalShortestDistanceNodes, - shortestDistanceEdgeStack, - ), - ); - } - - // For debugging purposes only - private printFinalInfo(finalShortestDistanceNodes: ShortestDistanceNode[]) { - finalShortestDistanceNodes.forEach((snd: ShortestDistanceNode) => { - console.log( - snd.node.getBetriebspunktName(), - "Distanz/Kosten:", - snd.distance, - "Anzahl Umsteigen:", - snd.path - .map((ts: TrainrunSection) => ts.getTrainrunId()) - .filter((v, i, a) => a.indexOf(v) === i).length - 1, - "Fahrplan:", - this.printFinalInfoTimetable(snd), - ); - }); - } - - // For debugging purposes only - private printFinalInfoTimetable(snd: ShortestDistanceNode): string { - const pathArray: string[] = []; - const revTrainrunSections = Object.assign([], snd.path).reverse(); - let nodeID: number = snd.node.getId(); - let trainID: number; - pathArray.push("]"); - revTrainrunSections.forEach((ts: TrainrunSection) => { - if (trainID !== ts.getTrainrunId()) { - if (trainID !== undefined) { - pathArray.push("]["); - } - pathArray.push( - "(" + ts.getTrainrun().getCategoryShortName() + ts.getTrainrun().getTitle() + ")", - ); - trainID = ts.getTrainrunId(); - } - if (ts.getSourceNodeId() !== nodeID) { - pathArray.push( - ts.getSourceNode().getBetriebspunktName() + - "-" + - ts.getTargetNode().getBetriebspunktName(), - ); - nodeID = ts.getSourceNodeId(); - } else { - pathArray.push( - ts.getTargetNode().getBetriebspunktName() + - "-" + - ts.getSourceNode().getBetriebspunktName(), - ); - nodeID = ts.getTargetNodeId(); - } - }); - pathArray.push("["); - - pathArray.reverse(); - let retString = ""; - pathArray.forEach((str) => (retString += str + " ")); - return retString; - } - - private getNextShortestDistanceEdge(edges: ShortestDistanceEdge[]): ShortestDistanceEdge { - edges.sort( - (a: ShortestDistanceEdge, b: ShortestDistanceEdge) => - b.getFullDistance() - a.getFullDistance(), - ); - return edges.pop(); - } - - private checkNextEdge(outgoingEdge: ShortestDistanceEdge, transitionTime: number): boolean { - if (transitionTime === undefined) { - return false; - } - return this.filterService.filterTrainrun( - outgoingEdge.getFromNodeDepartingTrainrunSection().getTrainrun(), - ); - } - - private findAllShortestDistanceNodes( - initialFinalShortestDistanceNodes: ShortestDistanceNode[], - shortestDistanceEdgeStack: ShortestDistanceEdge[], - ): ShortestDistanceNode[] { - // Recommended to read : https://de.wikipedia.org/wiki/Dijkstra-Algorithmus#Algorithmus_in_Pseudocode - let finalShortestDistanceNodes = Object.assign([], initialFinalShortestDistanceNodes); - - // start with the first edge (if there are more than one edge on the stack, pop the first edge with smallest distance) - let incomingEdge: ShortestDistanceEdge = - this.getNextShortestDistanceEdge(shortestDistanceEdgeStack); - - // loop as long there are unvisited edge on the stack - while (incomingEdge !== undefined) { - // loop over all (allowed) neighbors (edges) - incomingEdge - .getToNode() - .getPorts() - .filter((p: Port) => { - // only allow sections that go "away" from the current node - const isDirectionCompatible = ShortestTravelTimeSearch.isDirectionCompatible( - incomingEdge.getToNode(), - p.getTrainrunSection(), - ); - const isEdgeChangeAllowed = ShortestTravelTimeSearch.isEdgeChangeAllowed( - incomingEdge, - p.getTrainrunSection(), - ); - - return isDirectionCompatible && isEdgeChangeAllowed; - }) - .forEach((p: Port) => { - const outgoingTrainrunSection: TrainrunSection = p.getTrainrunSection(); - - // loop over all frequency (unroll frequency) - const frequencyObject = p.getTrainrunSection().getTrainrun().getTrainrunFrequency(); - const frequency = frequencyObject.frequency + frequencyObject.offset; - let currentFrequency = 0; - while (currentFrequency < 120) { - // two hour - // calculate transition cost (from incoming edge to outgoing edge : arrival at node --> transfer/transit -> departure node - const outgoingEdge = this.getOutgoingEdge( - outgoingTrainrunSection, - incomingEdge.getToNode(), - ); - const transitionCost = ShortestTravelTimeSearch.getTransitionCost( - incomingEdge, - outgoingEdge, - currentFrequency, - this.getSimulationChangeTrainPenalty(), - ); - - // check if the new calculated edge is allowed to travers (Business rules) - if (this.checkNextEdge(outgoingEdge, transitionCost)) { - // update cost / merge visited paths - outgoingEdge.mergePathAndUpdateCost( - incomingEdge.getFullPath(), - incomingEdge.getFullDistance() + transitionCost, - ); - - // if new edge is not yet visited (calculated) -> add it on the stack - finalShortestDistanceNodes = ShortestTravelTimeSearch.updateFinalAndStackData( - outgoingEdge, - finalShortestDistanceNodes, - shortestDistanceEdgeStack, - ); - } - currentFrequency += frequency; - } - }); - - // get next edge to visit -> visited the closed (smallest distance) next edge before others (pop) - incomingEdge = this.getNextShortestDistanceEdge(shortestDistanceEdgeStack); - } - - // this.printFinalInfo(finalShortestDistanceNodes); - return finalShortestDistanceNodes; - } - - private getSimulationDepartureMinute(): number { - return this.simulationDepartureMinute; - } - - private getSimulationChangeTrainPenalty(): number { - return this.simulationChangeTrainPenalty; - } - - private getOutgoingEdge(trainrunSection: TrainrunSection, node: Node): ShortestDistanceEdge { - const path: TrainrunSection[] = []; - const iterator = this.trainrunService.getNonStopIterator(node, trainrunSection); - - while (iterator.hasNext()) { - iterator.next(); - path.push(iterator.current().trainrunSection); - } - - return new ShortestDistanceEdge( - node, - iterator.current().node, - node.getDepartureConsecutiveTime(trainrunSection), - iterator.current().node.getArrivalConsecutiveTime(iterator.current().trainrunSection), - path, - ); - } -}