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.
Files changed (60) hide show
  1. package/.cspell.json +11 -1
  2. package/CHANGELOG.md +8 -3
  3. package/README.md +26 -24
  4. package/dist/cli.mjs +1786 -791
  5. package/dist/cli.mjs.map +1 -1
  6. package/dist/gtfs/transfers.d.ts +29 -5
  7. package/dist/gtfs/trips.d.ts +10 -5
  8. package/dist/parser.cjs.js +972 -525
  9. package/dist/parser.cjs.js.map +1 -1
  10. package/dist/parser.esm.js +972 -525
  11. package/dist/parser.esm.js.map +1 -1
  12. package/dist/router.cjs.js +1 -1
  13. package/dist/router.cjs.js.map +1 -1
  14. package/dist/router.d.ts +2 -2
  15. package/dist/router.esm.js +1 -1
  16. package/dist/router.esm.js.map +1 -1
  17. package/dist/router.umd.js +1 -1
  18. package/dist/router.umd.js.map +1 -1
  19. package/dist/routing/__tests__/plotter.test.d.ts +1 -0
  20. package/dist/routing/plotter.d.ts +42 -3
  21. package/dist/routing/result.d.ts +23 -7
  22. package/dist/routing/route.d.ts +2 -0
  23. package/dist/routing/router.d.ts +78 -19
  24. package/dist/timetable/__tests__/tripBoardingId.test.d.ts +1 -0
  25. package/dist/timetable/io.d.ts +4 -2
  26. package/dist/timetable/proto/timetable.d.ts +15 -1
  27. package/dist/timetable/route.d.ts +48 -23
  28. package/dist/timetable/timetable.d.ts +24 -7
  29. package/dist/timetable/tripBoardingId.d.ts +34 -0
  30. package/package.json +1 -1
  31. package/src/__e2e__/router.test.ts +114 -105
  32. package/src/__e2e__/timetable/stops.bin +2 -2
  33. package/src/__e2e__/timetable/timetable.bin +2 -2
  34. package/src/cli/repl.ts +245 -1
  35. package/src/gtfs/__tests__/parser.test.ts +19 -4
  36. package/src/gtfs/__tests__/transfers.test.ts +773 -37
  37. package/src/gtfs/__tests__/trips.test.ts +308 -27
  38. package/src/gtfs/parser.ts +36 -6
  39. package/src/gtfs/transfers.ts +193 -19
  40. package/src/gtfs/trips.ts +58 -21
  41. package/src/router.ts +2 -2
  42. package/src/routing/__tests__/plotter.test.ts +230 -0
  43. package/src/routing/__tests__/result.test.ts +486 -125
  44. package/src/routing/__tests__/route.test.ts +7 -3
  45. package/src/routing/__tests__/router.test.ts +380 -172
  46. package/src/routing/plotter.ts +279 -48
  47. package/src/routing/result.ts +114 -34
  48. package/src/routing/route.ts +0 -3
  49. package/src/routing/router.ts +344 -211
  50. package/src/timetable/__tests__/io.test.ts +34 -1
  51. package/src/timetable/__tests__/route.test.ts +74 -81
  52. package/src/timetable/__tests__/timetable.test.ts +232 -61
  53. package/src/timetable/__tests__/tripBoardingId.test.ts +57 -0
  54. package/src/timetable/io.ts +72 -10
  55. package/src/timetable/proto/timetable.proto +16 -2
  56. package/src/timetable/proto/timetable.ts +256 -22
  57. package/src/timetable/route.ts +174 -58
  58. package/src/timetable/timetable.ts +66 -16
  59. package/src/timetable/tripBoardingId.ts +94 -0
  60. package/tsconfig.json +2 -2
@@ -1,36 +1,64 @@
1
1
  /* eslint-disable @typescript-eslint/no-non-null-assertion */
2
- import { Stop, StopId } from '../stops/stops.js';
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 { TripIndex } from '../timetable/route.js';
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 { Timetable } from '../timetable/timetable.js';
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 TripLeg = ReachingTime & {
15
- leg?: Leg; // leg is not set for the very first segment
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
- export type ReachingTime = {
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 CurrentTrip = {
25
- tripIndex: TripIndex;
26
- origin: StopId;
27
- bestHopOnStop: StopId;
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 network router implementing the RAPTOR algorithm for
32
- * efficient journey planning and routing. For more information on the RAPTOR
33
- * algorithm, refer to its detailed explanation in the research paper:
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
- * Evaluates possible transfers for a given query on a transport
47
- * network, updating the earliest arrivals at various stops and marking new
48
- * stops that can be reached through these transfers.
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
- ): void {
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 markedStopsArray = Array.from(markedStops);
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
- const previousLeg = currentArrival.leg;
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
- legNumber: round,
90
- origin: origin,
91
- leg: {
92
- from: this.stopsIndex.findStopById(stop)!,
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
- const newlyMarkedStopsArray = Array.from(newlyMarkedStops);
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, ReachingTime>,
123
- destinations: Stop[],
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);