minotor 7.0.2 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cspell.json +11 -1
- package/CHANGELOG.md +8 -3
- package/README.md +26 -24
- package/dist/cli.mjs +1786 -791
- package/dist/cli.mjs.map +1 -1
- package/dist/gtfs/transfers.d.ts +29 -5
- package/dist/gtfs/trips.d.ts +10 -5
- package/dist/parser.cjs.js +972 -525
- package/dist/parser.cjs.js.map +1 -1
- package/dist/parser.esm.js +972 -525
- package/dist/parser.esm.js.map +1 -1
- package/dist/router.cjs.js +1 -1
- package/dist/router.cjs.js.map +1 -1
- package/dist/router.d.ts +2 -2
- package/dist/router.esm.js +1 -1
- package/dist/router.esm.js.map +1 -1
- package/dist/router.umd.js +1 -1
- package/dist/router.umd.js.map +1 -1
- package/dist/routing/__tests__/plotter.test.d.ts +1 -0
- package/dist/routing/plotter.d.ts +42 -3
- package/dist/routing/result.d.ts +23 -7
- package/dist/routing/route.d.ts +2 -0
- package/dist/routing/router.d.ts +78 -19
- package/dist/timetable/__tests__/tripBoardingId.test.d.ts +1 -0
- package/dist/timetable/io.d.ts +4 -2
- package/dist/timetable/proto/timetable.d.ts +15 -1
- package/dist/timetable/route.d.ts +48 -23
- package/dist/timetable/timetable.d.ts +24 -7
- package/dist/timetable/tripBoardingId.d.ts +34 -0
- package/package.json +1 -1
- package/src/__e2e__/router.test.ts +114 -105
- package/src/__e2e__/timetable/stops.bin +2 -2
- package/src/__e2e__/timetable/timetable.bin +2 -2
- package/src/cli/repl.ts +245 -1
- package/src/gtfs/__tests__/parser.test.ts +19 -4
- package/src/gtfs/__tests__/transfers.test.ts +773 -37
- package/src/gtfs/__tests__/trips.test.ts +308 -27
- package/src/gtfs/parser.ts +36 -6
- package/src/gtfs/transfers.ts +193 -19
- package/src/gtfs/trips.ts +58 -21
- package/src/router.ts +2 -2
- package/src/routing/__tests__/plotter.test.ts +230 -0
- package/src/routing/__tests__/result.test.ts +486 -125
- package/src/routing/__tests__/route.test.ts +7 -3
- package/src/routing/__tests__/router.test.ts +380 -172
- package/src/routing/plotter.ts +279 -48
- package/src/routing/result.ts +114 -34
- package/src/routing/route.ts +0 -3
- package/src/routing/router.ts +344 -211
- package/src/timetable/__tests__/io.test.ts +34 -1
- package/src/timetable/__tests__/route.test.ts +74 -81
- package/src/timetable/__tests__/timetable.test.ts +232 -61
- package/src/timetable/__tests__/tripBoardingId.test.ts +57 -0
- package/src/timetable/io.ts +72 -10
- package/src/timetable/proto/timetable.proto +16 -2
- package/src/timetable/proto/timetable.ts +256 -22
- package/src/timetable/route.ts +174 -58
- package/src/timetable/timetable.ts +66 -16
- package/src/timetable/tripBoardingId.ts +94 -0
- package/tsconfig.json +2 -2
package/src/routing/plotter.ts
CHANGED
|
@@ -1,68 +1,299 @@
|
|
|
1
1
|
import { StopId } from '../stops/stops.js';
|
|
2
2
|
import { Result } from './result.js';
|
|
3
|
-
import {
|
|
3
|
+
import { RoutingEdge, TransferEdge, VehicleEdge } from './router.js';
|
|
4
4
|
|
|
5
5
|
export class Plotter {
|
|
6
6
|
private result: Result;
|
|
7
|
+
private readonly ROUND_COLORS = [
|
|
8
|
+
'#60a5fa', // Round 1
|
|
9
|
+
'#ff9800', // Round 2
|
|
10
|
+
'#14b8a6', // Round 3
|
|
11
|
+
'#fb7185', // Round 4
|
|
12
|
+
'#ffdf00', // Round 5
|
|
13
|
+
'#b600ff', // Round 6
|
|
14
|
+
'#ee82ee', // Round 7+
|
|
15
|
+
];
|
|
7
16
|
|
|
8
17
|
constructor(result: Result) {
|
|
9
18
|
this.result = result;
|
|
10
19
|
}
|
|
11
20
|
|
|
12
21
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* @returns A string representing the DOT graph of the path tree.
|
|
22
|
+
* Gets the color for a round based on the specified palette.
|
|
16
23
|
*/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
private getRoundColor(round: number): string {
|
|
25
|
+
if (round === 0) return '#888888';
|
|
26
|
+
|
|
27
|
+
const colorIndex = Math.min(round - 1, this.ROUND_COLORS.length - 1);
|
|
28
|
+
return this.ROUND_COLORS[colorIndex] ?? '#ee82ee';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Escapes special characters in DOT strings to prevent syntax errors.
|
|
33
|
+
*/
|
|
34
|
+
private escapeDotString(str: string): string {
|
|
35
|
+
return str
|
|
36
|
+
.replace(/\\/g, '\\\\')
|
|
37
|
+
.replace(/"/g, '\\"')
|
|
38
|
+
.replace(/\n/g, '\\n')
|
|
39
|
+
.replace(/\r/g, '\\r')
|
|
40
|
+
.replace(/\t/g, '\\t');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Determines station type (origin/destination) information.
|
|
45
|
+
*/
|
|
46
|
+
private getStationInfo(stopId: StopId): {
|
|
47
|
+
isOrigin: boolean;
|
|
48
|
+
isDestination: boolean;
|
|
49
|
+
} {
|
|
50
|
+
const isOrigin = this.result.routingState.graph[0]?.has(stopId) ?? false;
|
|
51
|
+
const isDestination =
|
|
52
|
+
this.result.routingState.destinations.includes(stopId);
|
|
53
|
+
return { isOrigin, isDestination };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Formats a stop name for display, including platform information.
|
|
58
|
+
*/
|
|
59
|
+
private formatStopName(stopId: StopId): string {
|
|
60
|
+
const stop = this.result.stopsIndex.findStopById(stopId);
|
|
61
|
+
if (!stop) return `Unknown Stop (${stopId})`;
|
|
62
|
+
|
|
63
|
+
const escapedName = this.escapeDotString(stop.name);
|
|
64
|
+
const escapedPlatform = stop.platform
|
|
65
|
+
? this.escapeDotString(stop.platform)
|
|
66
|
+
: '';
|
|
67
|
+
return escapedPlatform
|
|
68
|
+
? `${escapedName}\\nPl. ${escapedPlatform}`
|
|
69
|
+
: escapedName;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Gets the appropriate fill color for a station based on its type.
|
|
74
|
+
*/
|
|
75
|
+
private getStationFillColor(
|
|
76
|
+
isOrigin: boolean,
|
|
77
|
+
isDestination: boolean,
|
|
78
|
+
): string {
|
|
79
|
+
if (isOrigin) return '#60a5fa';
|
|
80
|
+
if (isDestination) return '#ee82ee';
|
|
81
|
+
return 'white';
|
|
82
|
+
}
|
|
20
83
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Creates a DOT node for a station.
|
|
86
|
+
*/
|
|
87
|
+
private createStationNode(stopId: StopId): string {
|
|
88
|
+
const stop = this.result.stopsIndex.findStopById(stopId);
|
|
89
|
+
if (!stop) return '';
|
|
90
|
+
|
|
91
|
+
const displayName = this.formatStopName(stopId);
|
|
92
|
+
const stopIdStr = this.escapeDotString(String(stopId));
|
|
93
|
+
const nodeId = `s_${stopId}`;
|
|
94
|
+
const stationInfo = this.getStationInfo(stopId);
|
|
95
|
+
const fillColor = this.getStationFillColor(
|
|
96
|
+
stationInfo.isOrigin,
|
|
97
|
+
stationInfo.isDestination,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
return ` "${nodeId}" [label="${displayName}\\n${stopIdStr}" shape=box style=filled fillcolor="${fillColor}"];`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Creates a vehicle edge with route information oval in the middle.
|
|
105
|
+
*/
|
|
106
|
+
private createVehicleEdge(edge: VehicleEdge, round: number): string[] {
|
|
107
|
+
const fromNodeId = `s_${edge.from}`;
|
|
108
|
+
const toNodeId = `s_${edge.to}`;
|
|
109
|
+
const roundColor = this.getRoundColor(round);
|
|
110
|
+
const routeOvalId = `e_${edge.from}_${edge.to}_${edge.routeId}_${round}`;
|
|
111
|
+
|
|
112
|
+
const route = this.result.timetable.getRoute(edge.routeId);
|
|
113
|
+
const serviceRouteInfo = route
|
|
114
|
+
? this.result.timetable.getServiceRouteInfo(route)
|
|
115
|
+
: null;
|
|
116
|
+
|
|
117
|
+
const routeName = serviceRouteInfo?.name ?? `Route ${String(edge.routeId)}`;
|
|
118
|
+
const routeType = serviceRouteInfo?.type || 'UNKNOWN';
|
|
119
|
+
|
|
120
|
+
const departureTime = route
|
|
121
|
+
? route.departureFrom(edge.from, edge.tripIndex).toString()
|
|
122
|
+
: 'N/A';
|
|
123
|
+
const arrivalTime = edge.arrival.toString();
|
|
124
|
+
|
|
125
|
+
const escapedRouteName = this.escapeDotString(routeName);
|
|
126
|
+
const escapedRouteType = this.escapeDotString(routeType);
|
|
127
|
+
const routeInfo = `${edge.routeId}:${edge.tripIndex}`;
|
|
128
|
+
const ovalLabel = `${escapedRouteType} ${escapedRouteName}\\n${routeInfo}\\n${departureTime} → ${arrivalTime}`;
|
|
129
|
+
|
|
130
|
+
return [
|
|
131
|
+
` "${routeOvalId}" [label="${ovalLabel}" shape=oval style=filled fillcolor="white" color="${roundColor}"];`,
|
|
132
|
+
` "${fromNodeId}" -> "${routeOvalId}" [color="${roundColor}"];`,
|
|
133
|
+
` "${routeOvalId}" -> "${toNodeId}" [color="${roundColor}"];`,
|
|
25
134
|
];
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Creates a transfer edge with transfer information oval in the middle.
|
|
139
|
+
*/
|
|
140
|
+
private createTransferEdge(edge: TransferEdge, round: number): string[] {
|
|
141
|
+
const fromNodeId = `s_${edge.from}`;
|
|
142
|
+
const toNodeId = `s_${edge.to}`;
|
|
143
|
+
const roundColor = this.getRoundColor(round);
|
|
144
|
+
const transferOvalId = `e_${edge.from}_${edge.to}_${round}`;
|
|
145
|
+
|
|
146
|
+
const transferTime = edge.minTransferTime?.toString() || 'N/A';
|
|
147
|
+
const escapedTransferTime = this.escapeDotString(transferTime);
|
|
148
|
+
const ovalLabel = `Transfer\\n${escapedTransferTime}`;
|
|
149
|
+
|
|
150
|
+
return [
|
|
151
|
+
` "${transferOvalId}" [label="${ovalLabel}" shape=oval style="dashed,filled" fillcolor="white" color="${roundColor}"];`,
|
|
152
|
+
` "${fromNodeId}" -> "${transferOvalId}" [color="${roundColor}" style="dashed"];`,
|
|
153
|
+
` "${transferOvalId}" -> "${toNodeId}" [color="${roundColor}" style="dashed"];`,
|
|
154
|
+
];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Creates a continuation edge to visually link trip continuations.
|
|
159
|
+
*/
|
|
160
|
+
private createContinuationEdge(
|
|
161
|
+
fromEdge: VehicleEdge,
|
|
162
|
+
toEdge: VehicleEdge,
|
|
163
|
+
round: number,
|
|
164
|
+
): string[] {
|
|
165
|
+
const fromStationId = `s_${fromEdge.to}`;
|
|
166
|
+
const toStationId = `s_${toEdge.from}`;
|
|
167
|
+
const roundColor = this.getRoundColor(round);
|
|
168
|
+
const continuationOvalId = `continuation_${fromEdge.to}_${toEdge.from}_${round}`;
|
|
169
|
+
|
|
170
|
+
const fromRoute = this.result.timetable.getRoute(fromEdge.routeId);
|
|
171
|
+
const toRoute = this.result.timetable.getRoute(toEdge.routeId);
|
|
172
|
+
|
|
173
|
+
const fromServiceRouteInfo = fromRoute
|
|
174
|
+
? this.result.timetable.getServiceRouteInfo(fromRoute)
|
|
175
|
+
: null;
|
|
176
|
+
const toServiceRouteInfo = toRoute
|
|
177
|
+
? this.result.timetable.getServiceRouteInfo(toRoute)
|
|
178
|
+
: null;
|
|
179
|
+
|
|
180
|
+
const fromRouteName =
|
|
181
|
+
fromServiceRouteInfo?.name ?? `Route ${String(fromEdge.routeId)}`;
|
|
182
|
+
const toRouteName =
|
|
183
|
+
toServiceRouteInfo?.name ?? `Route ${String(toEdge.routeId)}`;
|
|
184
|
+
|
|
185
|
+
const fromRouteType = fromServiceRouteInfo?.type || 'UNKNOWN';
|
|
186
|
+
const toRouteType = toServiceRouteInfo?.type || 'UNKNOWN';
|
|
187
|
+
|
|
188
|
+
const fromArrivalTime = fromEdge.arrival.toString();
|
|
189
|
+
const toDepartureTime = toRoute
|
|
190
|
+
? toRoute.departureFrom(toEdge.from, toEdge.tripIndex).toString()
|
|
191
|
+
: 'N/A';
|
|
192
|
+
|
|
193
|
+
const escapedFromRouteName = this.escapeDotString(fromRouteName);
|
|
194
|
+
const escapedToRouteName = this.escapeDotString(toRouteName);
|
|
195
|
+
const escapedFromRouteType = this.escapeDotString(fromRouteType);
|
|
196
|
+
const escapedToRouteType = this.escapeDotString(toRouteType);
|
|
197
|
+
|
|
198
|
+
const fromRouteInfo = `${fromEdge.routeId}:${fromEdge.tripIndex}`;
|
|
199
|
+
const toRouteInfo = `${toEdge.routeId}:${toEdge.tripIndex}`;
|
|
200
|
+
|
|
201
|
+
const ovalLabel = `${escapedFromRouteType} ${escapedFromRouteName} (${fromRouteInfo}) ${fromArrivalTime}\\n↓\\n${escapedToRouteType} ${escapedToRouteName} (${toRouteInfo}) ${toDepartureTime}`;
|
|
202
|
+
|
|
203
|
+
return [
|
|
204
|
+
` "${continuationOvalId}" [label="${ovalLabel}" shape=oval style="filled,bold" fillcolor="#ffffcc" color="${roundColor}" penwidth="2"];`,
|
|
205
|
+
` "${fromStationId}" -> "${continuationOvalId}" [color="${roundColor}" style="bold" penwidth="3"];`,
|
|
206
|
+
` "${continuationOvalId}" -> "${toStationId}" [color="${roundColor}" style="bold" penwidth="3"];`,
|
|
207
|
+
];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Collects all stations and edges for the graph.
|
|
212
|
+
*/
|
|
213
|
+
private collectGraphData(): {
|
|
214
|
+
stations: Set<StopId>;
|
|
215
|
+
edges: string[];
|
|
216
|
+
} {
|
|
217
|
+
const stations = new Set<StopId>();
|
|
218
|
+
const edges: string[] = [];
|
|
219
|
+
const continuationEdges: string[] = [];
|
|
220
|
+
const graph: Map<StopId, RoutingEdge>[] = this.result.routingState.graph;
|
|
221
|
+
|
|
222
|
+
// Collect all stops that appear in the graph
|
|
223
|
+
graph.forEach((roundMap) => {
|
|
224
|
+
roundMap.forEach((edge, stopId) => {
|
|
225
|
+
stations.add(stopId);
|
|
226
|
+
if ('from' in edge && 'to' in edge) {
|
|
227
|
+
stations.add(edge.from);
|
|
228
|
+
stations.add(edge.to);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// Create edges for each round
|
|
234
|
+
graph.forEach((roundMap, round) => {
|
|
235
|
+
if (round === 0) {
|
|
236
|
+
// Skip round 0 as it contains only origin nodes
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
roundMap.forEach((edge) => {
|
|
241
|
+
if ('from' in edge && 'to' in edge) {
|
|
242
|
+
if ('routeId' in edge) {
|
|
243
|
+
const vehicleEdgeParts = this.createVehicleEdge(edge, round);
|
|
244
|
+
edges.push(...vehicleEdgeParts);
|
|
245
|
+
if (edge.continuationOf) {
|
|
246
|
+
let currentEdge = edge;
|
|
247
|
+
let previousEdge: VehicleEdge | undefined = edge.continuationOf;
|
|
248
|
+
|
|
249
|
+
while (previousEdge) {
|
|
250
|
+
const continuationEdgeParts = this.createContinuationEdge(
|
|
251
|
+
previousEdge,
|
|
252
|
+
currentEdge,
|
|
253
|
+
round,
|
|
254
|
+
);
|
|
255
|
+
continuationEdges.push(...continuationEdgeParts);
|
|
256
|
+
|
|
257
|
+
currentEdge = previousEdge;
|
|
258
|
+
previousEdge = previousEdge.continuationOf;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
} else {
|
|
262
|
+
const transferEdgeParts = this.createTransferEdge(edge, round);
|
|
263
|
+
edges.push(...transferEdgeParts);
|
|
264
|
+
}
|
|
63
265
|
}
|
|
64
266
|
});
|
|
65
267
|
});
|
|
268
|
+
edges.push(...continuationEdges);
|
|
269
|
+
|
|
270
|
+
return { stations, edges };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Plots the routing graph as a DOT graph for visualization.
|
|
275
|
+
*/
|
|
276
|
+
plotDotGraph(): string {
|
|
277
|
+
const { stations, edges } = this.collectGraphData();
|
|
278
|
+
|
|
279
|
+
const dotParts = [
|
|
280
|
+
'digraph RoutingGraph {',
|
|
281
|
+
' graph [overlap=false, splines=true, rankdir=TB, bgcolor=white, nodesep=0.8, ranksep=1.2, concentrate=true];',
|
|
282
|
+
' node [fontname="Arial" margin=0.1];',
|
|
283
|
+
' edge [fontname="Arial" fontsize=10];',
|
|
284
|
+
'',
|
|
285
|
+
' // Stations',
|
|
286
|
+
];
|
|
287
|
+
|
|
288
|
+
stations.forEach((stopId) => {
|
|
289
|
+
const stationNode = this.createStationNode(stopId);
|
|
290
|
+
if (stationNode) {
|
|
291
|
+
dotParts.push(stationNode);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
dotParts.push('', ' // Edges');
|
|
296
|
+
dotParts.push(...edges);
|
|
66
297
|
|
|
67
298
|
dotParts.push('}');
|
|
68
299
|
return dotParts.join('\n');
|
package/src/routing/result.ts
CHANGED
|
@@ -1,25 +1,26 @@
|
|
|
1
|
+
import { Timetable } from '../router.js';
|
|
1
2
|
import { SourceStopId, StopId } from '../stops/stops.js';
|
|
2
3
|
import { StopsIndex } from '../stops/stopsIndex.js';
|
|
3
4
|
import { Query } from './query.js';
|
|
4
|
-
import { Leg, Route } from './route.js';
|
|
5
|
-
import {
|
|
5
|
+
import { Leg, Route, Transfer, VehicleLeg } from './route.js';
|
|
6
|
+
import { Arrival, RoutingState, TransferEdge, VehicleEdge } from './router.js';
|
|
6
7
|
|
|
7
8
|
export class Result {
|
|
8
9
|
private readonly query: Query;
|
|
9
|
-
public readonly
|
|
10
|
-
public readonly
|
|
11
|
-
|
|
10
|
+
public readonly routingState: RoutingState;
|
|
11
|
+
public readonly stopsIndex: StopsIndex;
|
|
12
|
+
public readonly timetable: Timetable;
|
|
12
13
|
|
|
13
14
|
constructor(
|
|
14
15
|
query: Query,
|
|
15
|
-
|
|
16
|
-
earliestArrivalsPerRound: Map<StopId, TripLeg>[],
|
|
16
|
+
routingState: RoutingState,
|
|
17
17
|
stopsIndex: StopsIndex,
|
|
18
|
+
timetable: Timetable,
|
|
18
19
|
) {
|
|
19
20
|
this.query = query;
|
|
20
|
-
this.
|
|
21
|
-
this.earliestArrivalsPerRound = earliestArrivalsPerRound;
|
|
21
|
+
this.routingState = routingState;
|
|
22
22
|
this.stopsIndex = stopsIndex;
|
|
23
|
+
this.timetable = timetable;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
/**
|
|
@@ -39,11 +40,13 @@ export class Result {
|
|
|
39
40
|
const destinations = destinationList.flatMap((destination) =>
|
|
40
41
|
this.stopsIndex.equivalentStops(destination),
|
|
41
42
|
);
|
|
42
|
-
|
|
43
|
+
// find the first reached destination
|
|
43
44
|
let fastestDestination: StopId | undefined = undefined;
|
|
44
|
-
let fastestTime:
|
|
45
|
+
let fastestTime: Arrival | undefined = undefined;
|
|
45
46
|
for (const destination of destinations) {
|
|
46
|
-
const arrivalTime = this.earliestArrivals.get(
|
|
47
|
+
const arrivalTime = this.routingState.earliestArrivals.get(
|
|
48
|
+
destination.id,
|
|
49
|
+
);
|
|
47
50
|
if (arrivalTime !== undefined) {
|
|
48
51
|
if (
|
|
49
52
|
fastestTime === undefined ||
|
|
@@ -57,28 +60,96 @@ export class Result {
|
|
|
57
60
|
if (!fastestDestination || !fastestTime) {
|
|
58
61
|
return undefined;
|
|
59
62
|
}
|
|
60
|
-
|
|
61
63
|
const route: Leg[] = [];
|
|
62
64
|
let currentStop = fastestDestination;
|
|
63
65
|
let round = fastestTime.legNumber;
|
|
64
|
-
while (
|
|
65
|
-
const
|
|
66
|
-
if (!
|
|
66
|
+
while (round > 0) {
|
|
67
|
+
const edge = this.routingState.graph[round]?.get(currentStop);
|
|
68
|
+
if (!edge) {
|
|
67
69
|
throw new Error(
|
|
68
|
-
`No
|
|
69
|
-
tripLeg?.leg?.from.id ?? 'unknown'
|
|
70
|
-
}, end stop=${currentStop}, round=${round}, origin=${fastestTime.origin}`,
|
|
70
|
+
`No edge arriving at stop ${currentStop} at round ${round}`,
|
|
71
71
|
);
|
|
72
72
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
let leg: Leg;
|
|
74
|
+
if ('routeId' in edge) {
|
|
75
|
+
let vehicleEdge = edge;
|
|
76
|
+
// Handle leg reconstruction for in-seat trip continuations
|
|
77
|
+
const chainedEdges = [vehicleEdge];
|
|
78
|
+
while ('routeId' in vehicleEdge && vehicleEdge.continuationOf) {
|
|
79
|
+
chainedEdges.push(vehicleEdge.continuationOf);
|
|
80
|
+
vehicleEdge = vehicleEdge.continuationOf;
|
|
81
|
+
}
|
|
82
|
+
leg = this.buildVehicleLeg(chainedEdges);
|
|
83
|
+
} else if ('type' in edge) {
|
|
84
|
+
leg = this.buildTransferLeg(edge);
|
|
85
|
+
} else {
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
route.unshift(leg);
|
|
89
|
+
currentStop = leg.from.id;
|
|
90
|
+
if ('routeId' in edge) {
|
|
76
91
|
round -= 1;
|
|
77
92
|
}
|
|
78
93
|
}
|
|
79
94
|
return new Route(route);
|
|
80
95
|
}
|
|
81
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Builds a vehicle leg from a chain of vehicle edges.
|
|
99
|
+
*
|
|
100
|
+
* @param edges Array of vehicle edges representing continuous trips on transit vehicles
|
|
101
|
+
* @returns A vehicle leg with departure/arrival information and route details
|
|
102
|
+
* @throws Error if the edges array is empty
|
|
103
|
+
*/
|
|
104
|
+
private buildVehicleLeg(edges: VehicleEdge[]): VehicleLeg {
|
|
105
|
+
if (edges.length === 0) {
|
|
106
|
+
throw new Error('Cannot build vehicle leg from empty edges');
|
|
107
|
+
}
|
|
108
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
109
|
+
const firstEdge = edges[edges.length - 1]!;
|
|
110
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
111
|
+
const lastEdge = edges[0]!;
|
|
112
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
113
|
+
const firstRoute = this.timetable.getRoute(firstEdge.routeId)!;
|
|
114
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
115
|
+
const lastRoute = this.timetable.getRoute(lastEdge.routeId)!;
|
|
116
|
+
return {
|
|
117
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
118
|
+
from: this.stopsIndex.findStopById(firstRoute.stopId(firstEdge.from))!,
|
|
119
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
120
|
+
to: this.stopsIndex.findStopById(lastRoute.stopId(lastEdge.to))!,
|
|
121
|
+
// The route info comes from the first boarded route in case on continuous trips
|
|
122
|
+
route: this.timetable.getServiceRouteInfo(firstRoute),
|
|
123
|
+
departureTime: firstRoute.departureFrom(
|
|
124
|
+
firstEdge.from,
|
|
125
|
+
firstEdge.tripIndex,
|
|
126
|
+
),
|
|
127
|
+
arrivalTime: lastEdge.arrival,
|
|
128
|
+
pickUpType: firstRoute.pickUpTypeFrom(
|
|
129
|
+
firstEdge.from,
|
|
130
|
+
firstEdge.tripIndex,
|
|
131
|
+
),
|
|
132
|
+
dropOffType: lastRoute.dropOffTypeAt(lastEdge.to, lastEdge.tripIndex),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Builds a transfer leg from a transfer edge.
|
|
138
|
+
*
|
|
139
|
+
* @param edge Transfer edge representing a walking connection between stops
|
|
140
|
+
* @returns A transfer leg with from/to stops and transfer details
|
|
141
|
+
*/
|
|
142
|
+
private buildTransferLeg(edge: TransferEdge): Transfer {
|
|
143
|
+
return {
|
|
144
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
145
|
+
from: this.stopsIndex.findStopById(edge.from)!,
|
|
146
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
147
|
+
to: this.stopsIndex.findStopById(edge.to)!,
|
|
148
|
+
minTransferTime: edge.minTransferTime,
|
|
149
|
+
type: edge.type,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
82
153
|
/**
|
|
83
154
|
* Returns the arrival time at any stop reachable in less time / transfers than the destination(s) of the query)
|
|
84
155
|
*
|
|
@@ -86,21 +157,30 @@ export class Result {
|
|
|
86
157
|
* @param maxTransfers The optional maximum number of transfers allowed.
|
|
87
158
|
* @returns The arrival time if the target stop is reachable, otherwise undefined.
|
|
88
159
|
*/
|
|
89
|
-
arrivalAt(
|
|
90
|
-
stop: SourceStopId,
|
|
91
|
-
maxTransfers?: number,
|
|
92
|
-
): ReachingTime | undefined {
|
|
160
|
+
arrivalAt(stop: SourceStopId, maxTransfers?: number): Arrival | undefined {
|
|
93
161
|
const equivalentStops = this.stopsIndex.equivalentStops(stop);
|
|
94
|
-
let earliestArrival:
|
|
95
|
-
|
|
96
|
-
const relevantArrivals =
|
|
97
|
-
maxTransfers !== undefined
|
|
98
|
-
? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
99
|
-
this.earliestArrivalsPerRound[maxTransfers + 1]!
|
|
100
|
-
: this.earliestArrivals;
|
|
162
|
+
let earliestArrival: Arrival | undefined = undefined;
|
|
101
163
|
|
|
102
164
|
for (const equivalentStop of equivalentStops) {
|
|
103
|
-
|
|
165
|
+
let arrivalTime;
|
|
166
|
+
if (maxTransfers === undefined) {
|
|
167
|
+
arrivalTime = this.routingState.earliestArrivals.get(equivalentStop.id);
|
|
168
|
+
} else {
|
|
169
|
+
// We have no guarantee that the stop was visited in the last round,
|
|
170
|
+
// so we need to check all rounds if it's not found in the last one.
|
|
171
|
+
for (let i = maxTransfers + 1; i >= 0; i--) {
|
|
172
|
+
const arrivalEdge = this.routingState.graph[i]?.get(
|
|
173
|
+
equivalentStop.id,
|
|
174
|
+
);
|
|
175
|
+
if (arrivalEdge !== undefined) {
|
|
176
|
+
arrivalTime = {
|
|
177
|
+
arrival: arrivalEdge.arrival,
|
|
178
|
+
legNumber: i,
|
|
179
|
+
};
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
104
184
|
if (arrivalTime !== undefined) {
|
|
105
185
|
if (
|
|
106
186
|
earliestArrival === undefined ||
|
package/src/routing/route.ts
CHANGED
|
@@ -38,11 +38,8 @@ export type VehicleLeg = BaseLeg & {
|
|
|
38
38
|
route: ServiceRouteInfo;
|
|
39
39
|
departureTime: Time;
|
|
40
40
|
arrivalTime: Time;
|
|
41
|
-
// TODO support pick up and drop off types
|
|
42
|
-
/*
|
|
43
41
|
pickUpType: PickUpDropOffType;
|
|
44
42
|
dropOffType: PickUpDropOffType;
|
|
45
|
-
*/
|
|
46
43
|
};
|
|
47
44
|
|
|
48
45
|
export type Leg = Transfer | VehicleLeg;
|