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/router.ts
CHANGED
|
@@ -1,36 +1,64 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
2
|
-
import {
|
|
2
|
+
import { StopId } from '../stops/stops.js';
|
|
3
3
|
import { StopsIndex } from '../stops/stopsIndex.js';
|
|
4
4
|
import { Duration } from '../timetable/duration.js';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
Route,
|
|
7
|
+
RouteId,
|
|
8
|
+
StopRouteIndex,
|
|
9
|
+
TripRouteIndex,
|
|
10
|
+
} from '../timetable/route.js';
|
|
6
11
|
import { Time } from '../timetable/time.js';
|
|
7
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
Timetable,
|
|
14
|
+
TransferType,
|
|
15
|
+
TripBoarding,
|
|
16
|
+
} from '../timetable/timetable.js';
|
|
8
17
|
import { Query } from './query.js';
|
|
9
18
|
import { Result } from './result.js';
|
|
10
|
-
import { Leg } from './route.js';
|
|
11
19
|
|
|
12
20
|
const UNREACHED = Time.infinity();
|
|
13
21
|
|
|
14
|
-
export type
|
|
15
|
-
|
|
22
|
+
export type OriginNode = { arrival: Time };
|
|
23
|
+
|
|
24
|
+
export type VehicleEdge = {
|
|
25
|
+
arrival: Time;
|
|
26
|
+
from: StopRouteIndex;
|
|
27
|
+
to: StopRouteIndex;
|
|
28
|
+
routeId: RouteId;
|
|
29
|
+
tripIndex: TripRouteIndex;
|
|
30
|
+
continuationOf?: VehicleEdge;
|
|
16
31
|
};
|
|
32
|
+
export type TransferEdge = {
|
|
33
|
+
arrival: Time;
|
|
34
|
+
from: StopId;
|
|
35
|
+
to: StopId;
|
|
36
|
+
type: TransferType;
|
|
37
|
+
minTransferTime?: Duration;
|
|
38
|
+
};
|
|
39
|
+
export type RoutingEdge = OriginNode | VehicleEdge | TransferEdge;
|
|
17
40
|
|
|
18
|
-
|
|
41
|
+
type TripContinuation = TripBoarding & {
|
|
42
|
+
previousEdge: VehicleEdge;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type Arrival = {
|
|
19
46
|
arrival: Time;
|
|
20
47
|
legNumber: number;
|
|
21
|
-
origin: StopId;
|
|
22
48
|
};
|
|
23
49
|
|
|
24
|
-
type
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
50
|
+
type Round = number;
|
|
51
|
+
|
|
52
|
+
export type RoutingState = {
|
|
53
|
+
earliestArrivals: Map<StopId, Arrival>;
|
|
54
|
+
graph: Map<StopId, RoutingEdge>[];
|
|
55
|
+
destinations: StopId[];
|
|
28
56
|
};
|
|
29
57
|
|
|
30
58
|
/**
|
|
31
|
-
* A public transportation
|
|
32
|
-
*
|
|
33
|
-
*
|
|
59
|
+
* A public transportation router implementing the RAPTOR algorithm.
|
|
60
|
+
* For more information on the RAPTOR algorithm,
|
|
61
|
+
* refer to its detailed explanation in the research paper:
|
|
34
62
|
* https://www.microsoft.com/en-us/research/wp-content/uploads/2012/01/raptor_alenex.pdf
|
|
35
63
|
*/
|
|
36
64
|
export class Router {
|
|
@@ -43,30 +71,305 @@ export class Router {
|
|
|
43
71
|
}
|
|
44
72
|
|
|
45
73
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
74
|
+
* The main Raptor algorithm implementation.
|
|
75
|
+
*
|
|
76
|
+
* @param query The query containing the main parameters for the routing.
|
|
77
|
+
* @returns A result object containing data structures allowing to reconstruct routes and .
|
|
78
|
+
*/
|
|
79
|
+
route(query: Query): Result {
|
|
80
|
+
const routingState = this.initRoutingState(query);
|
|
81
|
+
const markedStops = new Set<StopId>();
|
|
82
|
+
for (const originStop of routingState.graph[0]!.keys()) {
|
|
83
|
+
markedStops.add(originStop);
|
|
84
|
+
}
|
|
85
|
+
// Initial transfer consideration for origins
|
|
86
|
+
const newlyMarkedStops = this.considerTransfers(
|
|
87
|
+
query,
|
|
88
|
+
0,
|
|
89
|
+
markedStops,
|
|
90
|
+
routingState,
|
|
91
|
+
);
|
|
92
|
+
for (const newStop of newlyMarkedStops) {
|
|
93
|
+
markedStops.add(newStop);
|
|
94
|
+
}
|
|
95
|
+
for (let round = 1; round <= query.options.maxTransfers + 1; round++) {
|
|
96
|
+
const edgesAtCurrentRound = new Map<StopId, RoutingEdge>();
|
|
97
|
+
routingState.graph.push(edgesAtCurrentRound);
|
|
98
|
+
const reachableRoutes = this.timetable.findReachableRoutes(
|
|
99
|
+
markedStops,
|
|
100
|
+
query.options.transportModes,
|
|
101
|
+
);
|
|
102
|
+
markedStops.clear();
|
|
103
|
+
// for each route that can be reached with at least round - 1 trips
|
|
104
|
+
for (const [route, hopOnStopIndex] of reachableRoutes) {
|
|
105
|
+
const newlyMarkedStops = this.scanRoute(
|
|
106
|
+
route,
|
|
107
|
+
hopOnStopIndex,
|
|
108
|
+
round,
|
|
109
|
+
routingState,
|
|
110
|
+
);
|
|
111
|
+
for (const newStop of newlyMarkedStops) {
|
|
112
|
+
markedStops.add(newStop);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// process in-seat trip continuations
|
|
116
|
+
let continuations = this.findTripContinuations(
|
|
117
|
+
markedStops,
|
|
118
|
+
edgesAtCurrentRound,
|
|
119
|
+
);
|
|
120
|
+
while (continuations.length > 0) {
|
|
121
|
+
const stopsFromContinuations: Set<StopId> = new Set();
|
|
122
|
+
for (const continuation of continuations) {
|
|
123
|
+
const route = this.timetable.getRoute(continuation.routeId)!;
|
|
124
|
+
const routeScanResults = this.scanRoute(
|
|
125
|
+
route,
|
|
126
|
+
continuation.hopOnStopIndex,
|
|
127
|
+
round,
|
|
128
|
+
routingState,
|
|
129
|
+
continuation,
|
|
130
|
+
);
|
|
131
|
+
for (const newStop of routeScanResults) {
|
|
132
|
+
stopsFromContinuations.add(newStop);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
for (const newStop of stopsFromContinuations) {
|
|
136
|
+
markedStops.add(newStop);
|
|
137
|
+
}
|
|
138
|
+
continuations = this.findTripContinuations(
|
|
139
|
+
stopsFromContinuations,
|
|
140
|
+
edgesAtCurrentRound,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
const newlyMarkedStops = this.considerTransfers(
|
|
144
|
+
query,
|
|
145
|
+
round,
|
|
146
|
+
markedStops,
|
|
147
|
+
routingState,
|
|
148
|
+
);
|
|
149
|
+
for (const newStop of newlyMarkedStops) {
|
|
150
|
+
markedStops.add(newStop);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (markedStops.size === 0) break;
|
|
154
|
+
}
|
|
155
|
+
return new Result(query, routingState, this.stopsIndex, this.timetable);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Finds trip continuations for the given marked stops and edges at the current round.
|
|
160
|
+
* @param markedStops The set of marked stops.
|
|
161
|
+
* @param edgesAtCurrentRound The map of edges at the current round.
|
|
162
|
+
* @returns An array of trip continuations.
|
|
163
|
+
*/
|
|
164
|
+
private findTripContinuations(
|
|
165
|
+
markedStops: Set<StopId>,
|
|
166
|
+
edgesAtCurrentRound: Map<StopId, RoutingEdge>,
|
|
167
|
+
): TripContinuation[] {
|
|
168
|
+
const continuations: TripContinuation[] = [];
|
|
169
|
+
for (const stopId of markedStops) {
|
|
170
|
+
const arrival = edgesAtCurrentRound.get(stopId);
|
|
171
|
+
if (!arrival || !('routeId' in arrival)) continue;
|
|
172
|
+
|
|
173
|
+
const continuousTrips = this.timetable.getContinuousTrips(
|
|
174
|
+
arrival.to,
|
|
175
|
+
arrival.routeId,
|
|
176
|
+
arrival.tripIndex,
|
|
177
|
+
);
|
|
178
|
+
for (let i = 0; i < continuousTrips.length; i++) {
|
|
179
|
+
const trip = continuousTrips[i]!;
|
|
180
|
+
continuations.push({
|
|
181
|
+
routeId: trip.routeId,
|
|
182
|
+
hopOnStopIndex: trip.hopOnStopIndex,
|
|
183
|
+
tripIndex: trip.tripIndex,
|
|
184
|
+
previousEdge: arrival,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return continuations;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Initializes the routing state for the RAPTOR algorithm.
|
|
193
|
+
*
|
|
194
|
+
* This method sets up the initial data structures needed for route planning,
|
|
195
|
+
* including origin and destination stops (considering equivalent stops),
|
|
196
|
+
* earliest arrival times, and marked stops for processing.
|
|
197
|
+
*
|
|
198
|
+
* @param query The routing query containing origin, destination, and departure time
|
|
199
|
+
* @returns The initialized routing state with all necessary data structures
|
|
200
|
+
*/
|
|
201
|
+
private initRoutingState(query: Query): RoutingState {
|
|
202
|
+
const { from, to, departureTime } = query;
|
|
203
|
+
// Consider children or siblings of the "from" stop as potential origins
|
|
204
|
+
const origins = this.stopsIndex
|
|
205
|
+
.equivalentStops(from)
|
|
206
|
+
.map((origin) => origin.id);
|
|
207
|
+
// Consider children or siblings of the "to" stop(s) as potential destinations
|
|
208
|
+
const destinations = Array.from(to)
|
|
209
|
+
.flatMap((destination) => this.stopsIndex.equivalentStops(destination))
|
|
210
|
+
.map((destination) => destination.id);
|
|
211
|
+
|
|
212
|
+
const earliestArrivals = new Map<StopId, Arrival>();
|
|
213
|
+
const earliestArrivalsWithoutAnyLeg = new Map<StopId, RoutingEdge>();
|
|
214
|
+
const earliestArrivalsPerRound = [earliestArrivalsWithoutAnyLeg];
|
|
215
|
+
|
|
216
|
+
const initialState = {
|
|
217
|
+
arrival: departureTime,
|
|
218
|
+
legNumber: 0,
|
|
219
|
+
};
|
|
220
|
+
for (const originStop of origins) {
|
|
221
|
+
earliestArrivals.set(originStop, initialState);
|
|
222
|
+
earliestArrivalsWithoutAnyLeg.set(originStop, initialState);
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
destinations,
|
|
226
|
+
earliestArrivals,
|
|
227
|
+
graph: earliestArrivalsPerRound,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Scans a route to find the earliest possible trips (if not provided) and updates arrival times.
|
|
233
|
+
*
|
|
234
|
+
* This method implements the core route scanning logic of the RAPTOR algorithm.
|
|
235
|
+
* It iterates through all stops on a given route starting from the hop-on stop,
|
|
236
|
+
* maintaining the current best trip and updating arrival times when improvements
|
|
237
|
+
* are found. The method also handles boarding new trips when earlier departures
|
|
238
|
+
* are available if no given trip is provided as a parameter.
|
|
239
|
+
*
|
|
240
|
+
* @param route The route to scan for possible trips
|
|
241
|
+
* @param hopOnStopIndex The stop index where passengers can board the route
|
|
242
|
+
* @param round The current round number in the RAPTOR algorithm
|
|
243
|
+
* @param routingState The current routing state containing arrival times and marked stops
|
|
244
|
+
*/
|
|
245
|
+
private scanRoute(
|
|
246
|
+
route: Route,
|
|
247
|
+
hopOnStopIndex: StopRouteIndex,
|
|
248
|
+
round: Round,
|
|
249
|
+
routingState: RoutingState,
|
|
250
|
+
tripContinuation?: TripContinuation,
|
|
251
|
+
): Set<StopId> {
|
|
252
|
+
const newlyMarkedStops = new Set<StopId>();
|
|
253
|
+
let activeTrip: TripBoarding | undefined = tripContinuation
|
|
254
|
+
? {
|
|
255
|
+
routeId: route.id,
|
|
256
|
+
hopOnStopIndex,
|
|
257
|
+
tripIndex: tripContinuation.tripIndex,
|
|
258
|
+
}
|
|
259
|
+
: undefined;
|
|
260
|
+
const edgesAtCurrentRound = routingState.graph[round]!;
|
|
261
|
+
const edgesAtPreviousRound = routingState.graph[round - 1]!;
|
|
262
|
+
// Compute target pruning criteria only once per route
|
|
263
|
+
const earliestArrivalAtAnyDestination = this.earliestArrivalAtAnyStop(
|
|
264
|
+
routingState.earliestArrivals,
|
|
265
|
+
routingState.destinations,
|
|
266
|
+
);
|
|
267
|
+
for (
|
|
268
|
+
let currentStopIndex = hopOnStopIndex;
|
|
269
|
+
currentStopIndex < route.getNbStops();
|
|
270
|
+
currentStopIndex++
|
|
271
|
+
) {
|
|
272
|
+
const currentStop: StopId = route.stops[currentStopIndex]!;
|
|
273
|
+
// If we're currently on a trip,
|
|
274
|
+
// check if arrival at the stop improves the earliest arrival time
|
|
275
|
+
if (activeTrip !== undefined) {
|
|
276
|
+
const arrivalTime = route.arrivalAt(
|
|
277
|
+
currentStopIndex,
|
|
278
|
+
activeTrip.tripIndex,
|
|
279
|
+
);
|
|
280
|
+
const dropOffType = route.dropOffTypeAt(
|
|
281
|
+
currentStopIndex,
|
|
282
|
+
activeTrip.tripIndex,
|
|
283
|
+
);
|
|
284
|
+
const earliestArrivalAtCurrentStop =
|
|
285
|
+
routingState.earliestArrivals.get(currentStop)?.arrival ?? UNREACHED;
|
|
286
|
+
if (
|
|
287
|
+
dropOffType !== 'NOT_AVAILABLE' &&
|
|
288
|
+
arrivalTime.isBefore(earliestArrivalAtCurrentStop) &&
|
|
289
|
+
arrivalTime.isBefore(earliestArrivalAtAnyDestination)
|
|
290
|
+
) {
|
|
291
|
+
const edge = {
|
|
292
|
+
arrival: arrivalTime,
|
|
293
|
+
routeId: route.id,
|
|
294
|
+
tripIndex: activeTrip.tripIndex,
|
|
295
|
+
from: activeTrip.hopOnStopIndex,
|
|
296
|
+
to: currentStopIndex,
|
|
297
|
+
} as VehicleEdge;
|
|
298
|
+
if (tripContinuation) {
|
|
299
|
+
// In case of continuous trip, we set a pointer to the previous edge
|
|
300
|
+
edge.continuationOf = tripContinuation.previousEdge;
|
|
301
|
+
}
|
|
302
|
+
edgesAtCurrentRound.set(currentStop, edge);
|
|
303
|
+
|
|
304
|
+
routingState.earliestArrivals.set(currentStop, {
|
|
305
|
+
arrival: arrivalTime,
|
|
306
|
+
legNumber: round,
|
|
307
|
+
});
|
|
308
|
+
newlyMarkedStops.add(currentStop);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (tripContinuation) {
|
|
312
|
+
// If it's a trip continuation, no need to check for earlier trips
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
// check if we can board an earlier trip at the current stop
|
|
316
|
+
// if there was no current trip, find the first one reachable
|
|
317
|
+
const earliestArrivalOnPreviousRound =
|
|
318
|
+
edgesAtPreviousRound.get(currentStop)?.arrival;
|
|
319
|
+
// TODO if the last edge is not a transfer, and if there is no trip continuation of type 1 (guaranteed)
|
|
320
|
+
// Add the minTransferTime to make sure there's at least 2 minutes to transfer.
|
|
321
|
+
// If platforms are collapsed, make sure to apply the station level transfer time
|
|
322
|
+
// (or later at route reconstruction time)
|
|
323
|
+
if (
|
|
324
|
+
earliestArrivalOnPreviousRound !== undefined &&
|
|
325
|
+
(activeTrip === undefined ||
|
|
326
|
+
earliestArrivalOnPreviousRound.isBefore(
|
|
327
|
+
route.departureFrom(currentStopIndex, activeTrip.tripIndex),
|
|
328
|
+
) ||
|
|
329
|
+
earliestArrivalOnPreviousRound.equals(
|
|
330
|
+
route.departureFrom(currentStopIndex, activeTrip.tripIndex),
|
|
331
|
+
))
|
|
332
|
+
) {
|
|
333
|
+
const earliestTrip = route.findEarliestTrip(
|
|
334
|
+
currentStopIndex,
|
|
335
|
+
earliestArrivalOnPreviousRound,
|
|
336
|
+
activeTrip?.tripIndex,
|
|
337
|
+
);
|
|
338
|
+
if (earliestTrip !== undefined) {
|
|
339
|
+
activeTrip = {
|
|
340
|
+
routeId: route.id,
|
|
341
|
+
tripIndex: earliestTrip,
|
|
342
|
+
hopOnStopIndex: currentStopIndex,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return newlyMarkedStops;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Processes all currently marked stops to find available transfers
|
|
352
|
+
* and determines if using these transfers would result in earlier arrival times
|
|
353
|
+
* at destination stops. It handles different transfer types including in-seat
|
|
354
|
+
* transfers and walking transfers with appropriate minimum transfer times.
|
|
355
|
+
*
|
|
356
|
+
* @param query The routing query containing transfer options and constraints
|
|
357
|
+
* @param round The current round number in the RAPTOR algorithm
|
|
358
|
+
* @param routingState The current routing state containing arrival times and marked stops
|
|
49
359
|
*/
|
|
50
360
|
private considerTransfers(
|
|
51
361
|
query: Query,
|
|
52
|
-
markedStops: Set<StopId>,
|
|
53
|
-
arrivalsAtCurrentRound: Map<StopId, TripLeg>,
|
|
54
|
-
earliestArrivals: Map<StopId, ReachingTime>,
|
|
55
362
|
round: number,
|
|
56
|
-
|
|
363
|
+
markedStops: Set<StopId>,
|
|
364
|
+
routingState: RoutingState,
|
|
365
|
+
): Set<StopId> {
|
|
57
366
|
const { options } = query;
|
|
367
|
+
const arrivalsAtCurrentRound = routingState.graph[round]!;
|
|
58
368
|
const newlyMarkedStops: Set<StopId> = new Set();
|
|
59
|
-
const
|
|
60
|
-
for (let i = 0; i < markedStopsArray.length; i++) {
|
|
61
|
-
const stop = markedStopsArray[i]!;
|
|
369
|
+
for (const stop of markedStops) {
|
|
62
370
|
const currentArrival = arrivalsAtCurrentRound.get(stop);
|
|
63
|
-
if (!currentArrival) continue;
|
|
64
371
|
// Skip transfers if the last leg was also a transfer
|
|
65
|
-
|
|
66
|
-
if (previousLeg && !('route' in previousLeg)) {
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
|
|
372
|
+
if (!currentArrival || 'type' in currentArrival) continue;
|
|
70
373
|
const transfers = this.timetable.getTransfers(stop);
|
|
71
374
|
for (let j = 0; j < transfers.length; j++) {
|
|
72
375
|
const transfer = transfers[j]!;
|
|
@@ -74,6 +377,7 @@ export class Router {
|
|
|
74
377
|
if (transfer.minTransferTime) {
|
|
75
378
|
transferTime = transfer.minTransferTime;
|
|
76
379
|
} else if (transfer.type === 'IN_SEAT') {
|
|
380
|
+
// TODO not needed anymore now that trip continuations are handled separately
|
|
77
381
|
transferTime = Duration.zero();
|
|
78
382
|
} else {
|
|
79
383
|
transferTime = options.minTransferTime;
|
|
@@ -83,32 +387,22 @@ export class Router {
|
|
|
83
387
|
arrivalsAtCurrentRound.get(transfer.destination)?.arrival ??
|
|
84
388
|
UNREACHED;
|
|
85
389
|
if (arrivalAfterTransfer.isBefore(originalArrival)) {
|
|
86
|
-
const origin = currentArrival.origin;
|
|
87
390
|
arrivalsAtCurrentRound.set(transfer.destination, {
|
|
88
391
|
arrival: arrivalAfterTransfer,
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
to: this.stopsIndex.findStopById(transfer.destination)!,
|
|
94
|
-
minTransferTime: transfer.minTransferTime,
|
|
95
|
-
type: transfer.type,
|
|
96
|
-
},
|
|
392
|
+
from: stop,
|
|
393
|
+
to: transfer.destination,
|
|
394
|
+
minTransferTime: transfer.minTransferTime,
|
|
395
|
+
type: transfer.type,
|
|
97
396
|
});
|
|
98
|
-
earliestArrivals.set(transfer.destination, {
|
|
397
|
+
routingState.earliestArrivals.set(transfer.destination, {
|
|
99
398
|
arrival: arrivalAfterTransfer,
|
|
100
399
|
legNumber: round,
|
|
101
|
-
origin: origin,
|
|
102
400
|
});
|
|
103
401
|
newlyMarkedStops.add(transfer.destination);
|
|
104
402
|
}
|
|
105
403
|
}
|
|
106
404
|
}
|
|
107
|
-
|
|
108
|
-
for (let i = 0; i < newlyMarkedStopsArray.length; i++) {
|
|
109
|
-
const newStop = newlyMarkedStopsArray[i]!;
|
|
110
|
-
markedStops.add(newStop);
|
|
111
|
-
}
|
|
405
|
+
return newlyMarkedStops;
|
|
112
406
|
}
|
|
113
407
|
|
|
114
408
|
/**
|
|
@@ -119,14 +413,13 @@ export class Router {
|
|
|
119
413
|
* @returns The earliest arrival time among the provided destinations.
|
|
120
414
|
*/
|
|
121
415
|
private earliestArrivalAtAnyStop(
|
|
122
|
-
earliestArrivals: Map<StopId,
|
|
123
|
-
destinations:
|
|
416
|
+
earliestArrivals: Map<StopId, Arrival>,
|
|
417
|
+
destinations: StopId[],
|
|
124
418
|
): Time {
|
|
125
419
|
let earliestArrivalAtAnyDestination = UNREACHED;
|
|
126
420
|
for (let i = 0; i < destinations.length; i++) {
|
|
127
421
|
const destination = destinations[i]!;
|
|
128
|
-
const arrival =
|
|
129
|
-
earliestArrivals.get(destination.id)?.arrival ?? UNREACHED;
|
|
422
|
+
const arrival = earliestArrivals.get(destination)?.arrival ?? UNREACHED;
|
|
130
423
|
earliestArrivalAtAnyDestination = Time.min(
|
|
131
424
|
earliestArrivalAtAnyDestination,
|
|
132
425
|
arrival,
|
|
@@ -134,164 +427,4 @@ export class Router {
|
|
|
134
427
|
}
|
|
135
428
|
return earliestArrivalAtAnyDestination;
|
|
136
429
|
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* The main Raptor algorithm implementation.
|
|
140
|
-
*
|
|
141
|
-
* @param query The query containing the main parameters for the routing.
|
|
142
|
-
* @returns A result object containing data structures allowing to reconstruct routes and .
|
|
143
|
-
*/
|
|
144
|
-
route(query: Query): Result {
|
|
145
|
-
const { from, to, departureTime, options } = query;
|
|
146
|
-
// Consider children or siblings of the "from" stop as potential origins
|
|
147
|
-
const origins = this.stopsIndex.equivalentStops(from);
|
|
148
|
-
// Consider children or siblings of the "to" stop(s) as potential destinations
|
|
149
|
-
const destinations = Array.from(to).flatMap((destination) =>
|
|
150
|
-
this.stopsIndex.equivalentStops(destination),
|
|
151
|
-
);
|
|
152
|
-
const earliestArrivals = new Map<StopId, ReachingTime>();
|
|
153
|
-
|
|
154
|
-
const earliestArrivalsWithoutAnyLeg = new Map<StopId, TripLeg>();
|
|
155
|
-
const earliestArrivalsPerRound = [earliestArrivalsWithoutAnyLeg];
|
|
156
|
-
// Stops that have been improved at round k-1
|
|
157
|
-
const markedStops = new Set<StopId>();
|
|
158
|
-
|
|
159
|
-
for (let i = 0; i < origins.length; i++) {
|
|
160
|
-
const originStop = origins[i]!;
|
|
161
|
-
markedStops.add(originStop.id);
|
|
162
|
-
earliestArrivals.set(originStop.id, {
|
|
163
|
-
arrival: departureTime,
|
|
164
|
-
legNumber: 0,
|
|
165
|
-
origin: originStop.id,
|
|
166
|
-
});
|
|
167
|
-
earliestArrivalsWithoutAnyLeg.set(originStop.id, {
|
|
168
|
-
arrival: departureTime,
|
|
169
|
-
legNumber: 0,
|
|
170
|
-
origin: originStop.id,
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
// on the first round we need to first consider transfers to discover all possible route origins
|
|
174
|
-
this.considerTransfers(
|
|
175
|
-
query,
|
|
176
|
-
markedStops,
|
|
177
|
-
earliestArrivalsWithoutAnyLeg,
|
|
178
|
-
earliestArrivals,
|
|
179
|
-
0,
|
|
180
|
-
);
|
|
181
|
-
|
|
182
|
-
for (let round = 1; round <= options.maxTransfers + 1; round++) {
|
|
183
|
-
const arrivalsAtCurrentRound = new Map<StopId, TripLeg>();
|
|
184
|
-
earliestArrivalsPerRound.push(arrivalsAtCurrentRound);
|
|
185
|
-
const arrivalsAtPreviousRound = earliestArrivalsPerRound[round - 1]!;
|
|
186
|
-
// Routes that contain at least one stop reached with at least round - 1 legs
|
|
187
|
-
// together with corresponding hop on stop index (earliest marked stop)
|
|
188
|
-
const reachableRoutes = this.timetable.findReachableRoutes(
|
|
189
|
-
markedStops,
|
|
190
|
-
options.transportModes,
|
|
191
|
-
);
|
|
192
|
-
markedStops.clear();
|
|
193
|
-
const earliestArrivalAtAnyDestination = this.earliestArrivalAtAnyStop(
|
|
194
|
-
earliestArrivals,
|
|
195
|
-
destinations,
|
|
196
|
-
);
|
|
197
|
-
// for each route that can be reached with at least round - 1 trips
|
|
198
|
-
const reachableRoutesArray = Array.from(reachableRoutes.entries());
|
|
199
|
-
for (let i = 0; i < reachableRoutesArray.length; i++) {
|
|
200
|
-
const [route, hopOnStop] = reachableRoutesArray[i]!;
|
|
201
|
-
let currentTrip: CurrentTrip | undefined = undefined;
|
|
202
|
-
const startIndex = route.stopIndex(hopOnStop);
|
|
203
|
-
for (let j = startIndex; j < route.getNbStops(); j++) {
|
|
204
|
-
const currentStop = route.stops[j]!;
|
|
205
|
-
// If we're currently on a trip,
|
|
206
|
-
// check if arrival at the stop improves the earliest arrival time
|
|
207
|
-
if (currentTrip !== undefined) {
|
|
208
|
-
const currentArrivalTime = route.arrivalAt(
|
|
209
|
-
currentStop,
|
|
210
|
-
currentTrip.tripIndex,
|
|
211
|
-
);
|
|
212
|
-
const currentDropOffType = route.dropOffTypeAt(
|
|
213
|
-
currentStop,
|
|
214
|
-
currentTrip.tripIndex,
|
|
215
|
-
);
|
|
216
|
-
const earliestArrivalAtCurrentStop =
|
|
217
|
-
earliestArrivals.get(currentStop)?.arrival ?? UNREACHED;
|
|
218
|
-
if (
|
|
219
|
-
currentDropOffType !== 'NOT_AVAILABLE' &&
|
|
220
|
-
currentArrivalTime.isBefore(earliestArrivalAtCurrentStop) &&
|
|
221
|
-
currentArrivalTime.isBefore(earliestArrivalAtAnyDestination)
|
|
222
|
-
) {
|
|
223
|
-
const bestHopOnDepartureTime = route.departureFrom(
|
|
224
|
-
currentTrip.bestHopOnStop,
|
|
225
|
-
currentTrip.tripIndex,
|
|
226
|
-
);
|
|
227
|
-
arrivalsAtCurrentRound.set(currentStop, {
|
|
228
|
-
arrival: currentArrivalTime,
|
|
229
|
-
legNumber: round,
|
|
230
|
-
origin: currentTrip.origin,
|
|
231
|
-
leg: {
|
|
232
|
-
from: this.stopsIndex.findStopById(
|
|
233
|
-
currentTrip.bestHopOnStop,
|
|
234
|
-
)!,
|
|
235
|
-
to: this.stopsIndex.findStopById(currentStop)!,
|
|
236
|
-
departureTime: bestHopOnDepartureTime,
|
|
237
|
-
arrivalTime: currentArrivalTime,
|
|
238
|
-
route: this.timetable.getServiceRouteInfo(route),
|
|
239
|
-
},
|
|
240
|
-
});
|
|
241
|
-
earliestArrivals.set(currentStop, {
|
|
242
|
-
arrival: currentArrivalTime,
|
|
243
|
-
legNumber: round,
|
|
244
|
-
origin: currentTrip.origin,
|
|
245
|
-
});
|
|
246
|
-
markedStops.add(currentStop);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
// check if we can board an earlier trip at the current stop
|
|
250
|
-
// if there was no current trip, find the first one reachable
|
|
251
|
-
const earliestArrivalOnPreviousRound =
|
|
252
|
-
arrivalsAtPreviousRound.get(currentStop)?.arrival;
|
|
253
|
-
if (
|
|
254
|
-
earliestArrivalOnPreviousRound !== undefined &&
|
|
255
|
-
(currentTrip === undefined ||
|
|
256
|
-
earliestArrivalOnPreviousRound.isBefore(
|
|
257
|
-
route.departureFrom(currentStop, currentTrip.tripIndex),
|
|
258
|
-
) ||
|
|
259
|
-
earliestArrivalOnPreviousRound.equals(
|
|
260
|
-
route.departureFrom(currentStop, currentTrip.tripIndex),
|
|
261
|
-
))
|
|
262
|
-
) {
|
|
263
|
-
const earliestTrip = route.findEarliestTrip(
|
|
264
|
-
currentStop,
|
|
265
|
-
earliestArrivalOnPreviousRound,
|
|
266
|
-
currentTrip?.tripIndex,
|
|
267
|
-
);
|
|
268
|
-
if (earliestTrip !== undefined) {
|
|
269
|
-
currentTrip = {
|
|
270
|
-
tripIndex: earliestTrip,
|
|
271
|
-
// we need to keep track of the best hop-on stop to reconstruct the route at the end
|
|
272
|
-
bestHopOnStop: currentStop,
|
|
273
|
-
origin:
|
|
274
|
-
arrivalsAtPreviousRound.get(currentStop)?.origin ??
|
|
275
|
-
currentStop,
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
this.considerTransfers(
|
|
282
|
-
query,
|
|
283
|
-
markedStops,
|
|
284
|
-
arrivalsAtCurrentRound,
|
|
285
|
-
earliestArrivals,
|
|
286
|
-
round,
|
|
287
|
-
);
|
|
288
|
-
if (markedStops.size === 0) break;
|
|
289
|
-
}
|
|
290
|
-
return new Result(
|
|
291
|
-
query,
|
|
292
|
-
earliestArrivals,
|
|
293
|
-
earliestArrivalsPerRound,
|
|
294
|
-
this.stopsIndex,
|
|
295
|
-
);
|
|
296
|
-
}
|
|
297
430
|
}
|
|
@@ -6,13 +6,16 @@ import {
|
|
|
6
6
|
deserializeRoutesAdjacency,
|
|
7
7
|
deserializeServiceRoutesMap,
|
|
8
8
|
deserializeStopsAdjacency,
|
|
9
|
+
deserializeTripContinuations,
|
|
9
10
|
serializeRoutesAdjacency,
|
|
10
11
|
serializeServiceRoutesMap,
|
|
11
12
|
serializeStopsAdjacency,
|
|
13
|
+
serializeTripContinuations,
|
|
12
14
|
} from '../io.js';
|
|
13
15
|
import { REGULAR, Route } from '../route.js';
|
|
14
16
|
import { Time } from '../time.js';
|
|
15
|
-
import { ServiceRoute, StopAdjacency } from '../timetable.js';
|
|
17
|
+
import { ServiceRoute, StopAdjacency, TripBoarding } from '../timetable.js';
|
|
18
|
+
import { encode } from '../tripBoardingId.js';
|
|
16
19
|
|
|
17
20
|
describe('Timetable IO', () => {
|
|
18
21
|
const stopsAdjacency: StopAdjacency[] = [
|
|
@@ -33,6 +36,7 @@ describe('Timetable IO', () => {
|
|
|
33
36
|
];
|
|
34
37
|
const routesAdjacency = [
|
|
35
38
|
new Route(
|
|
39
|
+
0,
|
|
36
40
|
new Uint16Array([
|
|
37
41
|
Time.fromHMS(16, 40, 0).toMinutes(),
|
|
38
42
|
Time.fromHMS(16, 50, 0).toMinutes(),
|
|
@@ -42,6 +46,7 @@ describe('Timetable IO', () => {
|
|
|
42
46
|
0,
|
|
43
47
|
),
|
|
44
48
|
new Route(
|
|
49
|
+
1,
|
|
45
50
|
new Uint16Array([
|
|
46
51
|
Time.fromHMS(15, 20, 0).toMinutes(),
|
|
47
52
|
Time.fromHMS(15, 30, 0).toMinutes(),
|
|
@@ -113,6 +118,34 @@ describe('Timetable IO', () => {
|
|
|
113
118
|
assert.deepStrictEqual(deserializedData, stopsAdjacency);
|
|
114
119
|
});
|
|
115
120
|
|
|
121
|
+
it('should serialize and deserialize tripContinuations correctly', () => {
|
|
122
|
+
const tripContinuations = new Map<bigint, TripBoarding[]>();
|
|
123
|
+
tripContinuations.set(encode(1, 0, 2), [
|
|
124
|
+
{ hopOnStopIndex: 1, routeId: 0, tripIndex: 2 },
|
|
125
|
+
{ hopOnStopIndex: 3, routeId: 1, tripIndex: 1 },
|
|
126
|
+
]);
|
|
127
|
+
tripContinuations.set(encode(2, 0, 0), [
|
|
128
|
+
{ hopOnStopIndex: 2, routeId: 0, tripIndex: 0 },
|
|
129
|
+
]);
|
|
130
|
+
|
|
131
|
+
const serialized = serializeTripContinuations(tripContinuations);
|
|
132
|
+
const deserialized = deserializeTripContinuations(serialized);
|
|
133
|
+
|
|
134
|
+
assert.deepStrictEqual(deserialized, tripContinuations);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should handle empty StopAdjacency without transfers or tripContinuations', () => {
|
|
138
|
+
const emptyStopsAdjacency: StopAdjacency[] = [
|
|
139
|
+
{ routes: [0] },
|
|
140
|
+
{ routes: [1] },
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
const serialized = serializeStopsAdjacency(emptyStopsAdjacency);
|
|
144
|
+
const deserialized = deserializeStopsAdjacency(serialized);
|
|
145
|
+
|
|
146
|
+
assert.deepStrictEqual(deserialized, emptyStopsAdjacency);
|
|
147
|
+
});
|
|
148
|
+
|
|
116
149
|
it('should serialize a routes adjacency matrix to a Uint8Array', () => {
|
|
117
150
|
const serializedData = serializeRoutesAdjacency(routesAdjacency);
|
|
118
151
|
assert.deepStrictEqual(serializedData, routesAdjacencyProto);
|