Skip to content

Commit

Permalink
feat: Implement Origin/Destination matrix (#301)
Browse files Browse the repository at this point in the history
  • Loading branch information
shenriotpro authored Oct 10, 2024
1 parent 03d44a0 commit 383c99d
Show file tree
Hide file tree
Showing 7 changed files with 1,729 additions and 10 deletions.
7 changes: 6 additions & 1 deletion documentation/ORIGIN_DESTINATION_MATRIX.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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).
22 changes: 22 additions & 0 deletions src/app/services/data/trainrun.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down Expand Up @@ -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<number, TrainrunIterator[]> {
const trainrunSections = this.trainrunSectionService.getTrainrunSections();
const iterators = new Map<number, TrainrunIterator[]>();
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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,16 +71,15 @@ <h2 class="SummaryTitle">{{ 'app.view.editor-side-view.editor-tools-view-compone
<sbb-tooltip
>{{ 'app.view.editor-side-view.editor-tools-view-component.export-trainruns-as-csv-excel' | translate }}</sbb-tooltip
>
<!-- TODO: uncomment when the feature is implemented. -->
<!-- <br />
<br />
<button
(click)="onExportOriginDestination()"
[title]="'app.view.editor-side-view.editor-tools-view-component.export-origin-destination' | translate"
class="TrainrunDialog EditorToolButton"
>
<sbb-icon svgIcon="five-circles-interconnected-small"></sbb-icon>
</button>
{{ 'app.view.editor-side-view.editor-tools-view-component.export-origin-destination-as-csv' | translate }} -->
{{ 'app.view.editor-side-view.editor-tools-view-component.export-origin-destination-as-csv' | translate }}
</sbb-expansion-panel>
<sbb-expansion-panel [expanded]="false">
<sbb-expansion-panel-header>{{ 'app.view.editor-side-view.editor-tools-view-component.base-data' | translate }}</sbb-expansion-panel-header>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -495,24 +497,60 @@ 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`);
headers.push($localize`:@@app.view.editor-side-view.editor-tools-view-component.travelTime:Travel time`);
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<string, [number, number]>();
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);
}
Expand Down
Loading

0 comments on commit 383c99d

Please sign in to comment.