From 383c99d9e2081c1587bf36aa71dcb7ee6e73c7d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Henriot?= <142150521+shenriotpro@users.noreply.github.com> Date: Thu, 10 Oct 2024 15:11:18 +0200 Subject: [PATCH] feat: Implement Origin/Destination matrix (#301) --- documentation/ORIGIN_DESTINATION_MATRIX.md | 7 +- src/app/services/data/trainrun.service.ts | 22 + .../editor-tools-view.component.html | 5 +- .../editor-tools-view.component.ts | 50 +- src/app/view/util/origin-destination-graph.ts | 263 ++++ .../netzgrafik.unit.testing.od.matrix.ts | 1159 +++++++++++++++++ .../origin.destination.csv.test.spec.ts | 233 ++++ 7 files changed, 1729 insertions(+), 10 deletions(-) create mode 100644 src/app/view/util/origin-destination-graph.ts create mode 100644 src/integration-testing/netzgrafik.unit.testing.od.matrix.ts create mode 100644 src/integration-testing/origin.destination.csv.test.spec.ts diff --git a/documentation/ORIGIN_DESTINATION_MATRIX.md b/documentation/ORIGIN_DESTINATION_MATRIX.md index fdd7a4f2..a4961d2e 100644 --- a/documentation/ORIGIN_DESTINATION_MATRIX.md +++ b/documentation/ORIGIN_DESTINATION_MATRIX.md @@ -2,7 +2,7 @@ The Origin Destination Matrix shows the optimized travel time (total cost) between each pair of nodes. -The optimized travel time includes a fixed penalty for each connection, but the Origin Destination Matrix also shows the corresponding effective travel time and number of connections. +The optimized travel time includes a fixed penalty (5 minutes) for each connection, but the Origin Destination Matrix also shows the corresponding effective travel time and number of connections. ### Filtering @@ -11,3 +11,8 @@ If some nodes are selected, then the Origin Destination Matrix will only show re The Origin Destination Matrix will only show results between visible nodes (but other nodes may be used to compute paths). The Origin Destination Matrix will only use visible trainruns to compute paths. + +### Caveats + +Split trainruns are not supported at the moment: https://github.com/SchweizerischeBundesbahnen/netzgrafik-editor-frontend/issues/285. +As a simplification, we currently consider trains run at their frequency for a fixed schedule duration (10 hours). diff --git a/src/app/services/data/trainrun.service.ts b/src/app/services/data/trainrun.service.ts index e3e702cc..dd3463be 100644 --- a/src/app/services/data/trainrun.service.ts +++ b/src/app/services/data/trainrun.service.ts @@ -289,6 +289,10 @@ export class TrainrunService { return Object.assign({}, this.trainrunsStore).trainruns; } + getVisibleTrainruns(): Trainrun[] { + return this.getTrainruns().filter((t) => this.filterService.filterTrainrun(t)); + } + getAllTrainrunLabels(): string[] { let trainrunLabels = []; this.getTrainruns().forEach((t) => @@ -769,6 +773,24 @@ export class TrainrunService { return new NonStopTrainrunIterator(this.logService, node, trainrunSection); } + // For each trainrun, get iterators from the source (trainruns may be split). + public getRootIterators(): Map { + const trainrunSections = this.trainrunSectionService.getTrainrunSections(); + const iterators = new Map(); + trainrunSections.forEach((ts) => { + const node = ts.getSourceNode(); + if (node.isEndNode(ts)) { + const it = iterators.get(ts.getTrainrunId()); + if (it === undefined) { + iterators.set(ts.getTrainrunId(), [this.getIterator(node, ts)]); + } else { + it.push(this.getIterator(node, ts)); + } + } + }); + return iterators; + } + getBothEndNodesWithTrainrunId(trainrunId: number) { const trainrunSections: TrainrunSection[] = this.trainrunSectionService.getTrainrunSections(); diff --git a/src/app/view/editor-tools-view-component/editor-tools-view.component.html b/src/app/view/editor-tools-view-component/editor-tools-view.component.html index 735ab888..04347475 100644 --- a/src/app/view/editor-tools-view-component/editor-tools-view.component.html +++ b/src/app/view/editor-tools-view-component/editor-tools-view.component.html @@ -71,8 +71,7 @@

{{ 'app.view.editor-side-view.editor-tools-view-compone {{ 'app.view.editor-side-view.editor-tools-view-component.export-trainruns-as-csv-excel' | translate }} - - + {{ 'app.view.editor-side-view.editor-tools-view-component.export-origin-destination-as-csv' | translate }} {{ 'app.view.editor-side-view.editor-tools-view-component.base-data' | translate }} diff --git a/src/app/view/editor-tools-view-component/editor-tools-view.component.ts b/src/app/view/editor-tools-view-component/editor-tools-view.component.ts index 973f8140..00795a35 100644 --- a/src/app/view/editor-tools-view-component/editor-tools-view.component.ts +++ b/src/app/view/editor-tools-view-component/editor-tools-view.component.ts @@ -21,6 +21,8 @@ import {LabelService} from "../../services/data/label.serivce"; import {NetzgrafikColoringService} from "../../services/data/netzgrafikColoring.service"; import {ViewportCullService} from "../../services/ui/viewport.cull.service"; import {LevelOfDetailService} from "../../services/ui/level.of.detail.service"; +import {_ViewRepeaterItemContext} from "@angular/cdk/collections"; +import {buildEdges, computeNeighbors, computeShortestPaths, topoSort} from "../util/origin-destination-graph"; @Component({ selector: "sbb-editor-tools-view-component", @@ -495,7 +497,16 @@ export class EditorToolsViewComponent { return this.buildCSVString(headers, rows); } + // Split trainruns are not supported at the moment: + // https://github.com/SchweizerischeBundesbahnen/netzgrafik-editor-frontend/issues/285 private convertToOriginDestinationCSV(): string { + // The cost to add for each connection. + // TODO: this may belong to the grafix metadata. + const connectionPenalty = 5; + // Duration of the schedule to consider (in minutes). + // TODO: ideally this would be 24 hours, but performance is a concern. + const timeLimit = 10*60; + const headers: string[] = []; headers.push($localize`:@@app.view.editor-side-view.editor-tools-view-component.origin:Origin`); headers.push($localize`:@@app.view.editor-side-view.editor-tools-view-component.destination:Destination`); @@ -503,16 +514,43 @@ export class EditorToolsViewComponent { headers.push($localize`:@@app.view.editor-side-view.editor-tools-view-component.connections:Connections`); headers.push($localize`:@@app.view.editor-side-view.editor-tools-view-component.totalCost:Total cost`); + const nodes = this.nodeService.getNodes(); const selectedNodes = this.nodeService.getSelectedNodes(); - const nodes = selectedNodes.length > 0 ? selectedNodes : this.nodeService.getVisibleNodes(); + const odNodes = selectedNodes.length > 0 ? selectedNodes : this.nodeService.getVisibleNodes(); + const trainruns = this.trainrunService.getVisibleTrainruns(); + + const edges = buildEdges(nodes, odNodes, trainruns, connectionPenalty, this.trainrunService, timeLimit); + + const neighbors = computeNeighbors(edges); + const vertices = topoSort(neighbors); + // In theory we could parallelize the pathfindings, but the overhead might be too big. + const res = new Map(); + odNodes.forEach((origin) => { + computeShortestPaths(origin.getId(), neighbors, vertices).forEach((value, key) => { + res.set([origin.getId(), key].join(","), value); + }); + }); - // TODO: implement the actual shortest path algorithm. const rows = []; - nodes.forEach((origin) => { - nodes.forEach((destination) => { - const row = [origin.getFullName(), destination.getFullName(), "-1", "-1", "-1"]; + odNodes.sort((a, b) => a.getBetriebspunktName().localeCompare(b.getBetriebspunktName())); + odNodes.forEach((origin) => { + odNodes.forEach((destination) => { + if (origin.getId() === destination.getId()) { + return; + } + const costs = res.get([origin.getId(), destination.getId()].join(",")); + if (costs === undefined) { + // Keep empty if no path is found. + rows.push([origin.getBetriebspunktName(), destination.getBetriebspunktName(), "", "", ""]); + return; + } + const [totalCost, connections] = costs; + const row = [origin.getBetriebspunktName(), destination.getBetriebspunktName(), + (totalCost-connections*connectionPenalty).toString(), + connections.toString(), totalCost.toString()]; rows.push(row); - });}); + }); + }); return this.buildCSVString(headers, rows); } diff --git a/src/app/view/util/origin-destination-graph.ts b/src/app/view/util/origin-destination-graph.ts new file mode 100644 index 00000000..1f50f08d --- /dev/null +++ b/src/app/view/util/origin-destination-graph.ts @@ -0,0 +1,263 @@ +import {Trainrun} from "src/app/models/trainrun.model"; +import {TrainrunService} from "src/app/services/data/trainrun.service"; +import {TrainrunIterator} from "src/app/services/util/trainrun.iterator"; +import {Node} from "src/app/models/node.model"; + +// A vertex indicates a "state": e.g. arriving at a node at a certain time and from a given trainrun. +export class Vertex { + constructor( + public nodeId: number, + // Indicates if we depart or arrive at the node. + public isDeparture: boolean, + // Optional fields are undefined for "convenience" vertices. + // Absolute time (duration from the start of the schedule) in minutes. + public time?: number, + // Negative trainrun ids are used for reverse directions. + public trainrunId?: number + ){} +} + +export class Edge { + constructor( + public v1: Vertex, + public v2: Vertex, + // The weight represents the cost of the edge, it is similar to a duration in minutes + // but it may include a connection penalty cost. + public weight: number + ){} +} + +export const buildEdges = (nodes: Node[], odNodes: Node[], trainruns: Trainrun[], connectionPenalty: number, trainrunService: TrainrunService, + timeLimit: number +): Edge[] => { + let edges = buildSectionEdges(trainruns, trainrunService, timeLimit); + + // TODO: organize by trainrun and sort + const verticesDepartureByNode = new Map(); + const verticesArrivalByNode = new Map(); + edges.forEach((edge) => { + const src = edge.v1; + const tgt = edge.v2; + if (src.isDeparture !== true) { + console.log("src is not a departure: ", src); + } + if (tgt.isDeparture !== false) { + console.log("tgt is not an arrival: ", tgt); + } + const departures = verticesDepartureByNode.get(src.nodeId); + if (departures === undefined) { + verticesDepartureByNode.set(src.nodeId, [src]); + } else { + departures.push(src); + } + const arrivals = verticesArrivalByNode.get(tgt.nodeId); + if (arrivals === undefined) { + verticesArrivalByNode.set(tgt.nodeId, [tgt]); + } else { + arrivals.push(tgt); + } + }); + + // Note: pushing too many elements at once does not work well. + edges = [...edges, ...buildConvenienceEdges(odNodes, verticesDepartureByNode, verticesArrivalByNode)]; + edges = [...edges, ...buildConnectionEdges(nodes, verticesDepartureByNode, verticesArrivalByNode, connectionPenalty)]; + + return edges; +}; + +// Given edges, return the neighbors (with weights) for each vertex, if any (outgoing adjacency list). +export const computeNeighbors = (edges: Edge[]): Map => { + const neighbors = new Map(); + edges.forEach((edge) => { + const v1 = JSON.stringify(edge.v1); + const v1Neighbors = neighbors.get(v1); + if (v1Neighbors === undefined) { + neighbors.set(v1, [[edge.v2, edge.weight]]); + } else { + v1Neighbors.push([edge.v2, edge.weight]); + } + }); + return neighbors; +}; + +// Given a graph (adjacency list), return the vertices in topological order. +// Note: sorting vertices by time would be enough for our use case. +export const topoSort = (graph: Map): Vertex[] => { + const res = []; + const visited = new Set(); + for (const node of graph.keys()) { + if (!visited.has(node)) { + depthFirstSearch(graph, JSON.parse(node) as Vertex, visited, res); + } + } + 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. +export const computeShortestPaths = (from: number, neighbors: Map, vertices: Vertex[]): +Map => { + const res = new Map(); + const dist = new Map(); + let started = false; + vertices.forEach((vertex) => { + const key = JSON.stringify(vertex); + if (!started) { + if (from === vertex.nodeId && vertex.isDeparture === true && vertex.time === undefined) { + started = true; + dist.set(key, [0, 0]); + } else { + return; + } + } + if (vertex.isDeparture === false && vertex.time === undefined && dist.get(key) !== undefined + && vertex.nodeId !== from) { + res.set(vertex.nodeId, dist.get(key)); + } + const neighs = neighbors.get(key); + if (neighs === undefined || dist.get(key) === undefined) { + return; + } + neighs.forEach(([neighbor, weight]) => { + const alt = dist.get(key)[0] + weight; + const neighborKey = JSON.stringify(neighbor); + if (dist.get(neighborKey) === undefined || alt < dist.get(neighborKey)[0]) { + let connection = 0; + if (vertex.trainrunId !== undefined && neighbor.trainrunId !== undefined && vertex.trainrunId !== neighbor.trainrunId) { + connection = 1; + } + dist.set(neighborKey, [alt, dist.get(key)[1] + connection]); + } + }); + }); + return res; +}; + +const buildSectionEdges = (trainruns: Trainrun[], trainrunService: TrainrunService, timeLimit: number): Edge[] => { + const edges = []; + const its = trainrunService.getRootIterators(); + trainruns.forEach((trainrun) => { + const tsIterators = its.get(trainrun.getId()); + if (tsIterators === undefined) { + console.log("Ignoring trainrun (no root found): ", trainrun.getId()); + return; + } + tsIterators.forEach((tsIterator) => { + edges.push(...buildSectionEdgesFromIterator(tsIterator, false, timeLimit)); + const ts = tsIterator.current().trainrunSection; + const nextIterator = trainrunService.getIterator(ts.getTargetNode(), ts); + edges.push(...buildSectionEdgesFromIterator(nextIterator, true, timeLimit)); + }); + }); + return edges; +}; + +const buildSectionEdgesFromIterator = (tsIterator: TrainrunIterator, reverse: boolean, timeLimit: number): Edge[] => { + const edges = []; + let nonStopV1Time = -1; + let nonStopV1Node = -1; + while (tsIterator.hasNext()) { + tsIterator.next(); + const ts = tsIterator.current().trainrunSection; + const trainrunId = reverse ? -ts.getTrainrunId() : ts.getTrainrunId(); + const v1Time = reverse ? ts.getTargetDepartureDto().consecutiveTime : ts.getSourceDepartureDto().consecutiveTime; + const v1Node = reverse ? ts.getTargetNodeId() : ts.getSourceNodeId(); + if (reverse ? ts.getSourceNode().isNonStop(ts) : ts.getTargetNode().isNonStop(ts)) { + if (nonStopV1Time === -1) { + nonStopV1Time = v1Time; + nonStopV1Node = v1Node; + } + continue; + } + let v1 = new Vertex(v1Node, true, v1Time, trainrunId); + let nonStop = false; + if (nonStopV1Time !== -1) { + v1 = new Vertex(nonStopV1Node, true, nonStopV1Time, trainrunId); + nonStopV1Time = -1; + nonStop = true; + } + const v2Time = reverse ? ts.getSourceArrivalDto().consecutiveTime : ts.getTargetArrivalDto().consecutiveTime; + const v2Node = reverse ? ts.getSourceNodeId() : ts.getTargetNodeId(); + const v2 = new Vertex(v2Node, false, v2Time, trainrunId); + + for (let i = 0; i*ts.getTrainrun().getFrequency() < timeLimit; i++) { + const newV1 = new Vertex(v1.nodeId, v1.isDeparture, v1.time+i*ts.getTrainrun().getFrequency(), v1.trainrunId); + const newV2 = new Vertex(v2.nodeId, v2.isDeparture, v2.time+i*ts.getTrainrun().getFrequency(), v2.trainrunId); + const edge = new Edge(newV1, newV2, newV2.time - newV1.time); + edges.push(edge); + } + } + return edges; +}; + +const buildConvenienceEdges = (nodes: Node[], verticesDepartureByNode: Map, + verticesArrivalByNode: Map): Edge[] => { + const edges = []; + nodes.forEach((node) => { + const v1 = new Vertex(node.getId(), true); + const v2 = new Vertex(node.getId(), false); + const edge = new Edge(v1, v2, 0); + edges.push(edge); + const departures = verticesDepartureByNode.get(node.getId()); + if (departures !== undefined) { + const srcVertex = new Vertex(node.getId(), true); + departures.forEach((departure) => { + const edge = new Edge(srcVertex, departure, 0); + edges.push(edge); + }); + } + const arrivals = verticesArrivalByNode.get(node.getId()); + if (arrivals !== undefined) { + const tgtVertex = new Vertex(node.getId(), false); + arrivals.forEach((arrival) => { + const edge = new Edge(arrival, tgtVertex, 0); + edges.push(edge); + }); + } + }); + return edges; +}; + +const buildConnectionEdges = (nodes: Node[], verticesDepartureByNode: Map, + verticesArrivalByNode: Map, connectionPenalty: number): Edge[] => { + const edges = []; + nodes.forEach((node) => { + const departures = verticesDepartureByNode.get(node.getId()); + const arrivals = verticesArrivalByNode.get(node.getId()); + if (departures !== undefined && arrivals !== undefined) { + departures.forEach((departure) => { + arrivals.forEach((arrival) => { + if (departure.trainrunId === arrival.trainrunId) { + if (departure.time < arrival.time) { + return; + } + const edge = new Edge(arrival, departure, departure.time - arrival.time); + edges.push(edge); + return; + } + if (departure.time < arrival.time + node.getConnectionTime()) { + return; + } + const edge = new Edge(arrival, departure, departure.time - arrival.time + connectionPenalty); + edges.push(edge); + }); + }); + } + }); + return edges; +}; + +const depthFirstSearch = (graph: Map, root: Vertex, visited: Set, res: Vertex[]): void => { + const key = JSON.stringify(root); + visited.add(key); + const neighbors = graph.get(key); + if (neighbors !== undefined) { + neighbors.forEach(([neighbor, weight]) => { + if (!visited.has(JSON.stringify(neighbor))) { + depthFirstSearch(graph, neighbor, visited, res); + } + }); + } + // Note that the order is important for topological sorting. + res.push(root); +}; diff --git a/src/integration-testing/netzgrafik.unit.testing.od.matrix.ts b/src/integration-testing/netzgrafik.unit.testing.od.matrix.ts new file mode 100644 index 00000000..6ed8ca06 --- /dev/null +++ b/src/integration-testing/netzgrafik.unit.testing.od.matrix.ts @@ -0,0 +1,1159 @@ +import { + HaltezeitFachCategories, + LabelRef, + LinePatternRefs, + NetzgrafikDto, +} from "../app/data-structures/business.data.structures"; +import {TrainrunSectionText} from "../app/data-structures/technical.data.structures"; + +export class NetzgrafikUnitTestingOdMatrix { + static getUnitTestNetzgrafik(): NetzgrafikDto { + return { + "nodes": [ + { + "id": 11, + "betriebspunktName": "C", + "fullName": "C", + "positionX": 1536, + "positionY": 864, + "ports": [ + { + "id": 14, + "trainrunSectionId": 7, + "positionIndex": 0, + "positionAlignment": 2 + }, + { + "id": 20, + "trainrunSectionId": 10, + "positionIndex": 1, + "positionAlignment": 2 + }, + { + "id": 15, + "trainrunSectionId": 8, + "positionIndex": 2, + "positionAlignment": 2 + }, + { + "id": 21, + "trainrunSectionId": 11, + "positionIndex": 3, + "positionAlignment": 2 + } + ], + "transitions": [ + { + "id": 4, + "port1Id": 20, + "port2Id": 21, + "isNonStopTransit": true + } + ], + "connections": [], + "resourceId": 12, + "perronkanten": 5, + "connectionTime": 3, + "trainrunCategoryHaltezeiten": { + "HaltezeitIPV": { + "haltezeit": 3, + "no_halt": false + }, + "HaltezeitA": { + "haltezeit": 2, + "no_halt": false + }, + "HaltezeitB": { + "haltezeit": 2, + "no_halt": false + }, + "HaltezeitC": { + "haltezeit": 1, + "no_halt": false + }, + "HaltezeitD": { + "haltezeit": 1, + "no_halt": false + }, + "HaltezeitUncategorized": { + "haltezeit": 0, + "no_halt": true + } + }, + "symmetryAxis": null, + "warnings": null, + "labelIds": [] + }, + { + "id": 12, + "betriebspunktName": "A", + "fullName": "A", + "positionX": 1120, + "positionY": 672, + "ports": [ + { + "id": 17, + "trainrunSectionId": 9, + "positionIndex": 0, + "positionAlignment": 1 + }, + { + "id": 12, + "trainrunSectionId": 6, + "positionIndex": 0, + "positionAlignment": 2 + }, + { + "id": 13, + "trainrunSectionId": 7, + "positionIndex": 0, + "positionAlignment": 3 + }, + { + "id": 19, + "trainrunSectionId": 10, + "positionIndex": 1, + "positionAlignment": 3 + } + ], + "transitions": [ + { + "id": 3, + "port1Id": 12, + "port2Id": 13, + "isNonStopTransit": false + } + ], + "connections": [], + "resourceId": 13, + "perronkanten": 5, + "connectionTime": 8, + "trainrunCategoryHaltezeiten": { + "HaltezeitIPV": { + "haltezeit": 3, + "no_halt": false + }, + "HaltezeitA": { + "haltezeit": 2, + "no_halt": false + }, + "HaltezeitB": { + "haltezeit": 2, + "no_halt": false + }, + "HaltezeitC": { + "haltezeit": 1, + "no_halt": false + }, + "HaltezeitD": { + "haltezeit": 1, + "no_halt": false + }, + "HaltezeitUncategorized": { + "haltezeit": 0, + "no_halt": true + } + }, + "symmetryAxis": null, + "warnings": null, + "labelIds": [] + }, + { + "id": 13, + "betriebspunktName": "D", + "fullName": "D", + "positionX": 1088, + "positionY": 1088, + "ports": [ + { + "id": 18, + "trainrunSectionId": 9, + "positionIndex": 0, + "positionAlignment": 0 + }, + { + "id": 22, + "trainrunSectionId": 11, + "positionIndex": 0, + "positionAlignment": 3 + } + ], + "transitions": [], + "connections": [], + "resourceId": 14, + "perronkanten": 5, + "connectionTime": 3, + "trainrunCategoryHaltezeiten": { + "HaltezeitIPV": { + "haltezeit": 3, + "no_halt": false + }, + "HaltezeitA": { + "haltezeit": 2, + "no_halt": false + }, + "HaltezeitB": { + "haltezeit": 2, + "no_halt": false + }, + "HaltezeitC": { + "haltezeit": 1, + "no_halt": false + }, + "HaltezeitD": { + "haltezeit": 1, + "no_halt": false + }, + "HaltezeitUncategorized": { + "haltezeit": 0, + "no_halt": true + } + }, + "symmetryAxis": null, + "warnings": null, + "labelIds": [] + }, + { + "id": 14, + "betriebspunktName": "B", + "fullName": "B", + "positionX": 800, + "positionY": 864, + "ports": [ + { + "id": 11, + "trainrunSectionId": 6, + "positionIndex": 0, + "positionAlignment": 3 + }, + { + "id": 16, + "trainrunSectionId": 8, + "positionIndex": 1, + "positionAlignment": 3 + } + ], + "transitions": [], + "connections": [], + "resourceId": 15, + "perronkanten": 5, + "connectionTime": 3, + "trainrunCategoryHaltezeiten": { + "HaltezeitIPV": { + "haltezeit": 3, + "no_halt": false + }, + "HaltezeitA": { + "haltezeit": 2, + "no_halt": false + }, + "HaltezeitB": { + "haltezeit": 2, + "no_halt": false + }, + "HaltezeitC": { + "haltezeit": 1, + "no_halt": false + }, + "HaltezeitD": { + "haltezeit": 1, + "no_halt": false + }, + "HaltezeitUncategorized": { + "haltezeit": 0, + "no_halt": true + } + }, + "symmetryAxis": null, + "warnings": null, + "labelIds": [] + } + ], + "trainrunSections": [ + { + "id": 6, + "sourceNodeId": 14, + "sourcePortId": 11, + "targetNodeId": 12, + "targetPortId": 12, + "travelTime": { + "time": 2, + "consecutiveTime": 1, + "lock": true, + "warning": null, + "timeFormatter": null + }, + "sourceDeparture": { + "time": 0, + "consecutiveTime": 0, + "lock": false, + "warning": null, + "timeFormatter": null + }, + "sourceArrival": { + "time": 0, + "consecutiveTime": 60, + "lock": false, + "warning": null, + "timeFormatter": null + }, + "targetDeparture": { + "time": 58, + "consecutiveTime": 58, + "lock": false, + "warning": null, + "timeFormatter": null + }, + "targetArrival": { + "time": 2, + "consecutiveTime": 2, + "lock": false, + "warning": null, + "timeFormatter": null + }, + "numberOfStops": 0, + "trainrunId": 4, + "resourceId": 0, + "specificTrainrunSectionFrequencyId": null, + "path": { + "path": [ + { + "x": 898, + "y": 880 + }, + { + "x": 962, + "y": 880 + }, + { + "x": 1054, + "y": 688 + }, + { + "x": 1118, + "y": 688 + } + ], + "textPositions": { + "0": { + "x": 916, + "y": 892 + }, + "1": { + "x": 944, + "y": 868 + }, + "2": { + "x": 1100, + "y": 676 + }, + "3": { + "x": 1072, + "y": 700 + }, + "4": { + "x": 1008, + "y": 772 + }, + "5": { + "x": 1008, + "y": 772 + }, + "6": { + "x": 1008, + "y": 796 + } + } + }, + "warnings": null + }, + { + "id": 7, + "sourceNodeId": 12, + "sourcePortId": 13, + "targetNodeId": 11, + "targetPortId": 14, + "travelTime": { + "time": 4, + "consecutiveTime": 1, + "lock": true, + "warning": null, + "timeFormatter": null + }, + "sourceDeparture": { + "time": 3, + "consecutiveTime": 3, + "lock": false, + "warning": null, + "timeFormatter": null + }, + "sourceArrival": { + "time": 57, + "consecutiveTime": 57, + "lock": false, + "warning": null, + "timeFormatter": null + }, + "targetDeparture": { + "time": 53, + "consecutiveTime": 53, + "lock": false, + "warning": null, + "timeFormatter": null + }, + "targetArrival": { + "time": 7, + "consecutiveTime": 7, + "lock": false, + "warning": null, + "timeFormatter": null + }, + "numberOfStops": 0, + "trainrunId": 4, + "resourceId": 0, + "specificTrainrunSectionFrequencyId": null, + "path": { + "path": [ + { + "x": 1218, + "y": 688 + }, + { + "x": 1282, + "y": 688 + }, + { + "x": 1470, + "y": 880 + }, + { + "x": 1534, + "y": 880 + } + ], + "textPositions": { + "0": { + "x": 1236, + "y": 700 + }, + "1": { + "x": 1264, + "y": 676 + }, + "2": { + "x": 1516, + "y": 868 + }, + "3": { + "x": 1488, + "y": 892 + }, + "4": { + "x": 1376, + "y": 772 + }, + "5": { + "x": 1376, + "y": 772 + }, + "6": { + "x": 1376, + "y": 796 + } + } + }, + "warnings": null + }, + { + "id": 8, + "sourceNodeId": 11, + "sourcePortId": 15, + "targetNodeId": 14, + "targetPortId": 16, + "travelTime": { + "time": 6, + "consecutiveTime": 1, + "lock": true, + "warning": null, + "timeFormatter": null + }, + "sourceDeparture": { + "time": 0, + "consecutiveTime": 60, + "lock": false, + "warning": null, + "timeFormatter": null + }, + "sourceArrival": { + "time": 0, + "consecutiveTime": 60, + "lock": false, + "warning": null, + "timeFormatter": null + }, + "targetDeparture": { + "time": 54, + "consecutiveTime": 54, + "lock": false, + "warning": null, + "timeFormatter": null + }, + "targetArrival": { + "time": 6, + "consecutiveTime": 66, + "lock": false, + "warning": null, + "timeFormatter": null + }, + "numberOfStops": 0, + "trainrunId": 5, + "resourceId": 0, + "specificTrainrunSectionFrequencyId": null, + "path": { + "path": [ + { + "x": 1534, + "y": 944 + }, + { + "x": 1470, + "y": 944 + }, + { + "x": 962, + "y": 912 + }, + { + "x": 898, + "y": 912 + } + ], + "textPositions": { + "0": { + "x": 1516, + "y": 932 + }, + "1": { + "x": 1488, + "y": 956 + }, + "2": { + "x": 916, + "y": 924 + }, + "3": { + "x": 944, + "y": 900 + }, + "4": { + "x": 1216, + "y": 916 + }, + "5": { + "x": 1216, + "y": 916 + }, + "6": { + "x": 1216, + "y": 940 + } + } + }, + "warnings": null + }, + { + "id": 9, + "sourceNodeId": 12, + "sourcePortId": 17, + "targetNodeId": 13, + "targetPortId": 18, + "travelTime": { + "time": 2, + "consecutiveTime": 1, + "lock": true, + "warning": null, + "timeFormatter": null + }, + "sourceDeparture": { + "time": 8, + "consecutiveTime": 8, + "lock": false, + "warning": null, + "timeFormatter": null + }, + "sourceArrival": { + "time": 52, + "consecutiveTime": 52, + "lock": false, + "warning": null, + "timeFormatter": null + }, + "targetDeparture": { + "time": 50, + "consecutiveTime": 50, + "lock": false, + "warning": null, + "timeFormatter": null + }, + "targetArrival": { + "time": 10, + "consecutiveTime": 10, + "lock": false, + "warning": null, + "timeFormatter": null + }, + "numberOfStops": 0, + "trainrunId": 6, + "resourceId": 0, + "specificTrainrunSectionFrequencyId": null, + "path": { + "path": [ + { + "x": 1136, + "y": 766 + }, + { + "x": 1136, + "y": 830 + }, + { + "x": 1104, + "y": 1022 + }, + { + "x": 1104, + "y": 1086 + } + ], + "textPositions": { + "0": { + "x": 1124, + "y": 784 + }, + "1": { + "x": 1148, + "y": 812 + }, + "2": { + "x": 1116, + "y": 1068 + }, + "3": { + "x": 1092, + "y": 1040 + }, + "4": { + "x": 1108, + "y": 926 + }, + "5": { + "x": 1108, + "y": 926 + }, + "6": { + "x": 1132, + "y": 926 + } + } + }, + "warnings": null + }, + { + "id": 10, + "sourceNodeId": 12, + "sourcePortId": 19, + "targetNodeId": 11, + "targetPortId": 20, + "travelTime": { + "time": 5, + "consecutiveTime": 1, + "lock": true, + "warning": null, + "timeFormatter": null + }, + "sourceDeparture": { + "time": 0, + "consecutiveTime": 0, + "lock": false, + "warning": null, + "timeFormatter": null + }, + "sourceArrival": { + "time": 0, + "consecutiveTime": 60, + "lock": false, + "warning": null, + "timeFormatter": null + }, + "targetDeparture": { + "time": 55, + "consecutiveTime": 55, + "lock": false, + "warning": null, + "timeFormatter": null + }, + "targetArrival": { + "time": 5, + "consecutiveTime": 5, + "lock": false, + "warning": null, + "timeFormatter": null + }, + "numberOfStops": 0, + "trainrunId": 7, + "resourceId": 0, + "specificTrainrunSectionFrequencyId": null, + "path": { + "path": [ + { + "x": 1218, + "y": 720 + }, + { + "x": 1282, + "y": 720 + }, + { + "x": 1470, + "y": 912 + }, + { + "x": 1534, + "y": 912 + } + ], + "textPositions": { + "0": { + "x": 1236, + "y": 732 + }, + "1": { + "x": 1264, + "y": 708 + }, + "2": { + "x": 1516, + "y": 900 + }, + "3": { + "x": 1488, + "y": 924 + }, + "4": { + "x": 1376, + "y": 804 + }, + "5": { + "x": 1376, + "y": 804 + }, + "6": { + "x": 1376, + "y": 828 + } + } + }, + "warnings": null + }, + { + "id": 11, + "sourceNodeId": 11, + "sourcePortId": 21, + "targetNodeId": 13, + "targetPortId": 22, + "travelTime": { + "time": 4, + "consecutiveTime": 1, + "lock": true, + "warning": null, + "timeFormatter": null + }, + "sourceDeparture": { + "time": 5, + "consecutiveTime": 5, + "lock": false, + "warning": null, + "timeFormatter": null + }, + "sourceArrival": { + "time": 55, + "consecutiveTime": 55, + "lock": false, + "warning": null, + "timeFormatter": null + }, + "targetDeparture": { + "time": 51, + "consecutiveTime": 51, + "lock": false, + "warning": null, + "timeFormatter": null + }, + "targetArrival": { + "time": 9, + "consecutiveTime": 9, + "lock": false, + "warning": null, + "timeFormatter": null + }, + "numberOfStops": 0, + "trainrunId": 7, + "resourceId": 0, + "specificTrainrunSectionFrequencyId": null, + "path": { + "path": [ + { + "x": 1534, + "y": 976 + }, + { + "x": 1470, + "y": 976 + }, + { + "x": 1250, + "y": 1104 + }, + { + "x": 1186, + "y": 1104 + } + ], + "textPositions": { + "0": { + "x": 1516, + "y": 964 + }, + "1": { + "x": 1488, + "y": 988 + }, + "2": { + "x": 1204, + "y": 1116 + }, + "3": { + "x": 1232, + "y": 1092 + }, + "4": { + "x": 1360, + "y": 1028 + }, + "5": { + "x": 1360, + "y": 1028 + }, + "6": { + "x": 1360, + "y": 1052 + } + } + }, + "warnings": null + } + ], + "trainruns": [ + { + "id": 4, + "name": "1", + "categoryId": 1, + "frequencyId": 3, + "trainrunTimeCategoryId": 0, + "labelIds": [] + }, + { + "id": 5, + "name": "2", + "categoryId": 3, + "frequencyId": 2, + "trainrunTimeCategoryId": 0, + "labelIds": [] + }, + { + "id": 6, + "name": "4", + "categoryId": 6, + "frequencyId": 3, + "trainrunTimeCategoryId": 0, + "labelIds": [] + }, + { + "id": 7, + "name": "3", + "categoryId": 5, + "frequencyId": 0, + "trainrunTimeCategoryId": 0, + "labelIds": [] + } + ], + "resources": [ + { + "id": 1, + "capacity": 2 + }, + { + "id": 2, + "capacity": 2 + }, + { + "id": 3, + "capacity": 2 + }, + { + "id": 4, + "capacity": 2 + }, + { + "id": 5, + "capacity": 2 + }, + { + "id": 6, + "capacity": 2 + }, + { + "id": 7, + "capacity": 2 + }, + { + "id": 8, + "capacity": 2 + }, + { + "id": 9, + "capacity": 2 + }, + { + "id": 10, + "capacity": 2 + }, + { + "id": 11, + "capacity": 2 + }, + { + "id": 12, + "capacity": 2 + }, + { + "id": 13, + "capacity": 2 + }, + { + "id": 14, + "capacity": 2 + }, + { + "id": 15, + "capacity": 2 + } + ], + "metadata": { + "trainrunCategories": [ + { + "id": 0, + "order": 0, + "shortName": "EC", + "name": "International", + "colorRef": "EC", + "fachCategory": HaltezeitFachCategories.IPV, + "minimalTurnaroundTime": 4, + "nodeHeadwayStop": 2, + "nodeHeadwayNonStop": 2, + "sectionHeadway": 2 + }, + { + "id": 1, + "order": 1, + "shortName": "IC", + "name": "InterCity", + "colorRef": "IC", + "fachCategory": HaltezeitFachCategories.A, + "minimalTurnaroundTime": 4, + "nodeHeadwayStop": 2, + "nodeHeadwayNonStop": 2, + "sectionHeadway": 2 + }, + { + "id": 2, + "order": 2, + "shortName": "IR", + "name": "InterRegio", + "colorRef": "IR", + "fachCategory": HaltezeitFachCategories.B, + "minimalTurnaroundTime": 4, + "nodeHeadwayStop": 2, + "nodeHeadwayNonStop": 2, + "sectionHeadway": 2 + }, + { + "id": 3, + "order": 3, + "shortName": "RE", + "name": "RegioExpress", + "colorRef": "RE", + "fachCategory": HaltezeitFachCategories.C, + "minimalTurnaroundTime": 4, + "nodeHeadwayStop": 2, + "nodeHeadwayNonStop": 2, + "sectionHeadway": 2 + }, + { + "id": 4, + "order": 4, + "shortName": "S", + "name": "RegioUndSBahnverkehr", + "colorRef": "S", + "fachCategory": HaltezeitFachCategories.D, + "minimalTurnaroundTime": 4, + "nodeHeadwayStop": 2, + "nodeHeadwayNonStop": 2, + "sectionHeadway": 2 + }, + { + "id": 5, + "order": 5, + "shortName": "GEX", + "name": "GüterExpress", + "colorRef": "GEX", + "fachCategory": HaltezeitFachCategories.Uncategorized, + "minimalTurnaroundTime": 4, + "nodeHeadwayStop": 3, + "nodeHeadwayNonStop": 3, + "sectionHeadway": 3 + }, + { + "id": 6, + "order": 6, + "shortName": "G", + "name": "Güterverkehr", + "colorRef": "G", + "fachCategory": HaltezeitFachCategories.Uncategorized, + "minimalTurnaroundTime": 4, + "nodeHeadwayStop": 3, + "nodeHeadwayNonStop": 3, + "sectionHeadway": 3 + } + ], + "trainrunFrequencies": [ + { + "id": 0, + "order": 0, + "frequency": 15, + "offset": 0, + "shortName": "15", + "name": "verkehrt viertelstündlich", + "linePatternRef": LinePatternRefs.Freq15 + }, + { + "id": 1, + "order": 0, + "frequency": 20, + "offset": 0, + "shortName": "20", + "name": "verkehrt im 20 Minuten Takt", + "linePatternRef": LinePatternRefs.Freq20 + }, + { + "id": 2, + "order": 0, + "frequency": 30, + "offset": 0, + "shortName": "30", + "name": "verkehrt halbstündlich", + "linePatternRef": LinePatternRefs.Freq30 + }, + { + "id": 3, + "order": 0, + "frequency": 60, + "offset": 0, + "shortName": "60", + "name": "verkehrt stündlich", + "linePatternRef": LinePatternRefs.Freq60 + }, + { + "id": 4, + "order": 0, + "frequency": 120, + "offset": 0, + "shortName": "120", + "name": "verkehrt zweistündlich (gerade)", + "linePatternRef": LinePatternRefs.Freq120 + }, + { + "id": 5, + "order": 0, + "frequency": 120, + "offset": 60, + "shortName": "120+", + "name": "verkehrt zweistündlich (ungerade)", + "linePatternRef": LinePatternRefs.Freq120 + } + ], + "trainrunTimeCategories": [ + { + "id": 0, + "order": 0, + "shortName": "7/24", + "name": "verkehrt uneingeschränkt", + "dayTimeInterval": [], + "weekday": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7 + ], + "linePatternRef": LinePatternRefs.TimeCat7_24 + }, + { + "id": 1, + "order": 0, + "shortName": "HVZ", + "name": "verkehrt zur Hauptverkehrszeit", + "dayTimeInterval": [ + { + "from": 360, + "to": 420 + }, + { + "from": 960, + "to": 1140 + } + ], + "weekday": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7 + ], + "linePatternRef": LinePatternRefs.TimeCatHVZ + }, + { + "id": 2, + "order": 0, + "shortName": "zeitweise", + "name": "verkehrt zeitweise", + "dayTimeInterval": [], + "weekday": [], + "linePatternRef": LinePatternRefs.TimeZeitweise + } + ], + "netzgrafikColors": [] + }, + "freeFloatingTexts": [], + "labels": [], + "labelGroups": [], + "filterData": { + "filterSettings": [] + } + }; + } +} diff --git a/src/integration-testing/origin.destination.csv.test.spec.ts b/src/integration-testing/origin.destination.csv.test.spec.ts new file mode 100644 index 00000000..0e7cbb9d --- /dev/null +++ b/src/integration-testing/origin.destination.csv.test.spec.ts @@ -0,0 +1,233 @@ +import {LogPublishersService} from "src/app/logger/log.publishers.service"; +import {LogService} from "src/app/logger/log.service"; +import {DataService} from "src/app/services/data/data.service"; +import {LabelService} from "src/app/services/data/label.serivce"; +import {LabelGroupService} from "src/app/services/data/labelgroup.service"; +import {NetzgrafikColoringService} from "src/app/services/data/netzgrafikColoring.service"; +import {NodeService} from "src/app/services/data/node.service"; +import {NoteService} from "src/app/services/data/note.service"; +import {ResourceService} from "src/app/services/data/resource.service"; +import {StammdatenService} from "src/app/services/data/stammdaten.service"; +import {TrainrunService} from "src/app/services/data/trainrun.service"; +import {TrainrunSectionService} from "src/app/services/data/trainrunsection.service"; +import {FilterService} from "src/app/services/ui/filter.service"; +import {buildEdges, computeNeighbors, computeShortestPaths, Edge, topoSort, Vertex} from "src/app/view/util/origin-destination-graph"; +import {NetzgrafikUnitTestingOdMatrix} from "./netzgrafik.unit.testing.od.matrix"; + +describe("Origin Destination CSV Test", () => { + let dataService: DataService = null; + let resourceService: ResourceService = null; + let nodeService: NodeService = null; + let logService: LogService = null; + let logPublishersService: LogPublishersService = null; + let trainrunService: TrainrunService = null; + let labelService: LabelService = null; + let labelGroupService: LabelGroupService = null; + let filterService: FilterService = null; + let trainrunSectionService: TrainrunSectionService = null; + let stammdatenService: StammdatenService = null; + let noteService: NoteService = null; + let netzgrafikColoringService: NetzgrafikColoringService = null; + + beforeEach(() => { + 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, + ); + stammdatenService = new StammdatenService(); + noteService = new NoteService(logService, labelService, filterService); + netzgrafikColoringService = new NetzgrafikColoringService(logService); + dataService = new DataService( + resourceService, + nodeService, + trainrunSectionService, + trainrunService, + stammdatenService, + noteService, + labelService, + labelGroupService, + filterService, + netzgrafikColoringService, + ); + }); + + it("integration test", () => { + dataService.loadNetzgrafikDto( + NetzgrafikUnitTestingOdMatrix.getUnitTestNetzgrafik(), + ); + const nodes = nodeService.getNodes(); + const trainruns = trainrunService.getTrainruns(); + const connectionPenalty = 5; + const timeLimit = 60*10; + + const edges = buildEdges(nodes, nodes, trainruns, connectionPenalty, trainrunService, timeLimit); + + const neighbors = computeNeighbors(edges); + const vertices = topoSort(neighbors); + + const res = new Map(); + nodes.forEach((origin) => { + computeShortestPaths(origin.getId(), neighbors, vertices).forEach((value, key) => { + res.set([origin.getId(), key].join(","), value); + }); + }); + + // Note: there may be some other equivalent solutions, depending on connections. + // See https://github.com/SchweizerischeBundesbahnen/netzgrafik-editor-frontend/issues/199 + expect(res).toEqual(new Map([ + ["11,13", [22, 1]], ["11,14", [6, 0]], ["11,12", [4, 0]], ["12,13", [2, 0]], ["12,14", [2, 0]], + ["12,11", [4, 0]], ["13,14", [29, 1]], ["13,11", [22, 1]], ["13,12", [2, 0]], ["14,13", [29, 1]], + ["14,11", [6, 0]], ["14,12", [2, 0]] + ])); + }); + + it("integration test with selected nodes", () => { + dataService.loadNetzgrafikDto( + NetzgrafikUnitTestingOdMatrix.getUnitTestNetzgrafik(), + ); + nodeService.selectNode(13); + nodeService.selectNode(14); + const nodes = nodeService.getNodes(); + const odNodes = nodeService.getSelectedNodes(); + const trainruns = trainrunService.getTrainruns(); + const connectionPenalty = 5; + const timeLimit = 60*10; + + const edges = buildEdges(nodes, odNodes, trainruns, connectionPenalty, trainrunService, timeLimit); + + const neighbors = computeNeighbors(edges); + const vertices = topoSort(neighbors); + + const res = new Map(); + nodes.forEach((origin) => { + computeShortestPaths(origin.getId(), neighbors, vertices).forEach((value, key) => { + res.set([origin.getId(), key].join(","), value); + }); + }); + + // Note: there may be some other equivalent solutions, depending on connections. + // See https://github.com/SchweizerischeBundesbahnen/netzgrafik-editor-frontend/issues/199 + expect(res).toEqual(new Map([["13,14", [29, 1]], ["14,13", [29, 1]]])); + }); + + it("simple path unit test", () => { + const v1 = new Vertex(0, true); + const v2 = new Vertex(0, true, 0, 0); + const v3 = new Vertex(1, false, 15, 0); + const v4 = new Vertex(1, false); + const e1 = new Edge(v1, v2, 0); + const e2 = new Edge(v2, v3, 15); + const e3 = new Edge(v3, v4, 0); + const edges = [e1, e2, e3]; + + const neighbors = computeNeighbors(edges); + + // v4 has no outgoing edges. + expect(neighbors).toHaveSize(3); + expect(neighbors.get(JSON.stringify(v1))).toHaveSize(1); + expect(neighbors.get(JSON.stringify(v1))).toContain([v2, 0]); + expect(neighbors.get(JSON.stringify(v2))).toHaveSize(1); + expect(neighbors.get(JSON.stringify(v2))).toContain([v3, 15]); + expect(neighbors.get(JSON.stringify(v3))).toHaveSize(1); + expect(neighbors.get(JSON.stringify(v3))).toContain([v4, 0]); + + const topoVertices = topoSort(neighbors); + + expect(topoVertices).toHaveSize(4); + edges.forEach((edge) => { + const v1Index = topoVertices.findIndex((value, index, obj) => {return value === edge.v1;}); + const v2Index = topoVertices.findIndex((value, index, obj) => {return value === edge.v2;}); + expect(v1Index).toBeLessThan(v2Index); + }); + + const distances0 = computeShortestPaths(0, neighbors, topoVertices); + + expect(distances0).toHaveSize(1); + expect(distances0.get(1)).toEqual([15, 0]); + }); + + it("connection unit test", () => { + // trainrun 0 + const v1 = new Vertex(0, true); + const v2 = new Vertex(0, true, 0, 0); + const v3 = new Vertex(1, false, 15, 0); + const v4 = new Vertex(1, true, 16, 0); + const v5 = new Vertex(2, false, 30, 0); + const v6 = new Vertex(2, false); + const e1 = new Edge(v1, v2, 0); + const e2 = new Edge(v2, v3, 15); + const e3 = new Edge(v3, v4, 1); + const e4 = new Edge(v4, v5, 14); + const e5 = new Edge(v5, v6, 0); + // trainrun 1 + const v7 = new Vertex(3, true); + const v8 = new Vertex(3, true, 0, 1); + const v9 = new Vertex(1, false, 10, 1); + const e6 = new Edge(v7, v8, 0); + const e7 = new Edge(v8, v9, 10); + // connection + const e8 = new Edge(v9, v4, 6+5); + // convenience + const v10 = new Vertex(1, false); + const e9 = new Edge(v3, v10, 0); + const e10 = new Edge(v9, v10, 0); + const v11 = new Vertex(1, true); + const e11 = new Edge(v11, v4, 0); + const edges = [e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11]; + + const neighbors = computeNeighbors(edges); + expect(neighbors).toHaveSize(9); + expect(neighbors.get(JSON.stringify(v1))).toHaveSize(1); + expect(neighbors.get(JSON.stringify(v2))).toHaveSize(1); + expect(neighbors.get(JSON.stringify(v3))).toHaveSize(2); + expect(neighbors.get(JSON.stringify(v4))).toHaveSize(1); + expect(neighbors.get(JSON.stringify(v5))).toHaveSize(1); + expect(neighbors.get(JSON.stringify(v7))).toHaveSize(1); + expect(neighbors.get(JSON.stringify(v8))).toHaveSize(1); + expect(neighbors.get(JSON.stringify(v9))).toHaveSize(2); + expect(neighbors.get(JSON.stringify(v11))).toHaveSize(1); + + const topoVertices = topoSort(neighbors); + expect(topoVertices).toHaveSize(11); + edges.forEach((edge) => { + const v1Index = topoVertices.findIndex((value, index, obj) => {return value === edge.v1;}); + const v2Index = topoVertices.findIndex((value, index, obj) => {return value === edge.v2;}); + expect(v1Index).toBeLessThan(v2Index); + }); + + const distances0 = computeShortestPaths(0, neighbors, topoVertices); + expect(distances0).toHaveSize(2); + expect(distances0.get(1)).toEqual([15, 0]); + expect(distances0.get(2)).toEqual([30, 0]); + + const distances1 = computeShortestPaths(1, neighbors, topoVertices); + expect(distances1).toHaveSize(1); + expect(distances1.get(2)).toEqual([14, 0]); + + const distances3 = computeShortestPaths(3, neighbors, topoVertices); + expect(distances3).toHaveSize(2); + expect(distances3.get(1)).toEqual([10, 0]); + // connection + expect(distances3.get(2)).toEqual([30+5, 1]); + }); +});