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, - ); - } -} 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/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/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, 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]); }, ); });