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
@@ -26,11 +26,14 @@ export const MUST_PHONE_AGENCY = 2;
26
26
  export const MUST_COORDINATE_WITH_DRIVER = 3;
27
27
 
28
28
  /*
29
- * A trip index corresponds to the index of the
30
- * first stop time in the trip divided by the number of stops
31
- * in the given route
29
+ * A trip route index corresponds to the index of a given trip in a route.
32
30
  */
33
- export type TripIndex = number;
31
+ export type TripRouteIndex = number;
32
+
33
+ /*
34
+ * A stop route index corresponds to the index of a given stop in a route.
35
+ */
36
+ export type StopRouteIndex = number;
34
37
 
35
38
  const pickUpDropOffTypeMap: PickUpDropOffType[] = [
36
39
  'REGULAR',
@@ -59,6 +62,7 @@ const toPickupDropOffType = (numericalType: number): PickUpDropOffType => {
59
62
  * A route identifies all trips of a given service route sharing the same list of stops.
60
63
  */
61
64
  export class Route {
65
+ public readonly id: RouteId;
62
66
  /**
63
67
  * Arrivals and departures encoded as minutes from midnight.
64
68
  * Format: [arrival1, departure1, arrival2, departure2, etc.]
@@ -90,7 +94,7 @@ export class Route {
90
94
  * ...
91
95
  * }
92
96
  */
93
- private readonly stopIndices: Map<StopId, number>;
97
+ private readonly stopIndices: Map<StopId, StopRouteIndex[]>;
94
98
  /**
95
99
  * The identifier of the route as a service shown to users.
96
100
  */
@@ -107,22 +111,131 @@ export class Route {
107
111
  private readonly nbTrips: number;
108
112
 
109
113
  constructor(
114
+ id: RouteId,
110
115
  stopTimes: Uint16Array,
111
116
  pickUpDropOffTypes: Uint8Array,
112
117
  stops: Uint32Array,
113
118
  serviceRouteId: ServiceRouteId,
114
119
  ) {
120
+ this.id = id;
115
121
  this.stopTimes = stopTimes;
116
122
  this.pickUpDropOffTypes = pickUpDropOffTypes;
117
123
  this.stops = stops;
118
124
  this.serviceRouteId = serviceRouteId;
119
125
  this.nbStops = stops.length;
120
126
  this.nbTrips = this.stopTimes.length / (this.stops.length * 2);
121
- this.stopIndices = new Map<number, number>();
127
+ this.stopIndices = new Map<StopId, StopRouteIndex[]>();
122
128
  for (let i = 0; i < stops.length; i++) {
123
129
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
124
- this.stopIndices.set(stops[i]!, i);
130
+ const stopId = stops[i]!;
131
+ const existingIndices = this.stopIndices.get(stopId);
132
+ if (existingIndices) {
133
+ existingIndices.push(i);
134
+ } else {
135
+ this.stopIndices.set(stopId, [i]);
136
+ }
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Creates a new route from multiple trips with their stops.
142
+ *
143
+ * @param params The route parameters including ID, service route ID, and trips.
144
+ * @returns The new route.
145
+ */
146
+ static of(params: {
147
+ id: RouteId;
148
+ serviceRouteId: ServiceRouteId;
149
+ trips: Array<{
150
+ stops: Array<{
151
+ id: StopId;
152
+ arrivalTime: Time;
153
+ departureTime: Time;
154
+ dropOffType?: number;
155
+ pickUpType?: number;
156
+ }>;
157
+ }>;
158
+ }): Route {
159
+ const { id, serviceRouteId, trips } = params;
160
+
161
+ if (trips.length === 0) {
162
+ throw new Error('At least one trip must be provided');
163
+ }
164
+
165
+ // All trips must have the same stops in the same order
166
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
167
+ const firstTrip = trips[0]!;
168
+ const stopIds = new Uint32Array(firstTrip.stops.map((stop) => stop.id));
169
+ const numStops = stopIds.length;
170
+
171
+ // Validate all trips have the same stops
172
+ for (let tripIndex = 1; tripIndex < trips.length; tripIndex++) {
173
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
174
+ const trip = trips[tripIndex]!;
175
+ if (trip.stops.length !== numStops) {
176
+ throw new Error(
177
+ `Trip ${tripIndex} has ${trip.stops.length} stops, expected ${numStops}`,
178
+ );
179
+ }
180
+ for (let stopIndex = 0; stopIndex < numStops; stopIndex++) {
181
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
182
+ if (trip.stops[stopIndex]!.id !== stopIds[stopIndex]) {
183
+ throw new Error(
184
+ `Trip ${tripIndex} has different stop at index ${stopIndex}`,
185
+ );
186
+ }
187
+ }
188
+ }
189
+
190
+ // Create stopTimes array with arrivals and departures for all trips
191
+ const stopTimes = new Uint16Array(trips.length * numStops * 2);
192
+ for (let tripIndex = 0; tripIndex < trips.length; tripIndex++) {
193
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
194
+ const trip = trips[tripIndex]!;
195
+ for (let stopIndex = 0; stopIndex < numStops; stopIndex++) {
196
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
197
+ const stop = trip.stops[stopIndex]!;
198
+ const baseIndex = (tripIndex * numStops + stopIndex) * 2;
199
+ stopTimes[baseIndex] = stop.arrivalTime.toMinutes();
200
+ stopTimes[baseIndex + 1] = stop.departureTime.toMinutes();
201
+ }
202
+ }
203
+
204
+ // Create pickUpDropOffTypes array (2-bit encoded) for all trips
205
+ const totalStopEntries = trips.length * numStops;
206
+ const pickUpDropOffTypes = new Uint8Array(Math.ceil(totalStopEntries / 2));
207
+
208
+ for (let tripIndex = 0; tripIndex < trips.length; tripIndex++) {
209
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
210
+ const trip = trips[tripIndex]!;
211
+ for (let stopIndex = 0; stopIndex < numStops; stopIndex++) {
212
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
213
+ const stop = trip.stops[stopIndex]!;
214
+ const globalIndex = tripIndex * numStops + stopIndex;
215
+ const pickUp = stop.pickUpType ?? REGULAR;
216
+ const dropOff = stop.dropOffType ?? REGULAR;
217
+ const byteIndex = Math.floor(globalIndex / 2);
218
+ const isSecondPair = globalIndex % 2 === 1;
219
+
220
+ if (isSecondPair) {
221
+ // Second pair: pickup in upper 2 bits, dropOff in bits 4-5
222
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
223
+ pickUpDropOffTypes[byteIndex]! |= (pickUp << 6) | (dropOff << 4);
224
+ } else {
225
+ // First pair: pickup in bits 2-3, dropOff in lower 2 bits
226
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
227
+ pickUpDropOffTypes[byteIndex]! |= (pickUp << 2) | dropOff;
228
+ }
229
+ }
125
230
  }
231
+
232
+ return new Route(
233
+ id,
234
+ stopTimes,
235
+ pickUpDropOffTypes,
236
+ stopIds,
237
+ serviceRouteId,
238
+ );
126
239
  }
127
240
 
128
241
  /**
@@ -140,35 +253,21 @@ export class Route {
140
253
  }
141
254
 
142
255
  /**
143
- * Checks if stop A is before stop B in the route.
256
+ * Retrieves the number of stops in the route.
144
257
  *
145
- * @param stopA - The StopId of the first stop.
146
- * @param stopB - The StopId of the second stop.
147
- * @returns True if stop A is before stop B, false otherwise.
258
+ * @returns The total number of stops in the route.
148
259
  */
149
- isBefore(stopA: StopId, stopB: StopId): boolean {
150
- const stopAIndex = this.stopIndices.get(stopA);
151
- if (stopAIndex === undefined) {
152
- throw new Error(
153
- `Stop index ${stopAIndex} not found in route ${this.serviceRouteId}`,
154
- );
155
- }
156
- const stopBIndex = this.stopIndices.get(stopB);
157
- if (stopBIndex === undefined) {
158
- throw new Error(
159
- `Stop index ${stopBIndex} not found in route ${this.serviceRouteId}`,
160
- );
161
- }
162
- return stopAIndex < stopBIndex;
260
+ getNbStops(): number {
261
+ return this.nbStops;
163
262
  }
164
263
 
165
264
  /**
166
- * Retrieves the number of stops in the route.
265
+ * Retrieves the number of trips in the route.
167
266
  *
168
- * @returns The total number of stops in the route.
267
+ * @returns The total number of trips in the route.
169
268
  */
170
- getNbStops(): number {
171
- return this.nbStops;
269
+ getNbTrips(): number {
270
+ return this.nbTrips;
172
271
  }
173
272
 
174
273
  /**
@@ -184,17 +283,16 @@ export class Route {
184
283
  /**
185
284
  * Retrieves the arrival time at a specific stop for a given trip.
186
285
  *
187
- * @param stopId - The identifier of the stop.
286
+ * @param stopIndex - The index of the stop in the route.
188
287
  * @param tripIndex - The index of the trip.
189
288
  * @returns The arrival time at the specified stop and trip as a Time object.
190
289
  */
191
- arrivalAt(stopId: StopId, tripIndex: TripIndex): Time {
192
- const arrivalIndex =
193
- (tripIndex * this.stops.length + this.stopIndex(stopId)) * 2;
290
+ arrivalAt(stopIndex: StopRouteIndex, tripIndex: TripRouteIndex): Time {
291
+ const arrivalIndex = (tripIndex * this.stops.length + stopIndex) * 2;
194
292
  const arrival = this.stopTimes[arrivalIndex];
195
293
  if (arrival === undefined) {
196
294
  throw new Error(
197
- `Arrival time not found for stop ${stopId} at trip index ${tripIndex} in route ${this.serviceRouteId}`,
295
+ `Arrival time not found for stop ${this.stopId(stopIndex)} (${stopIndex}) at trip index ${tripIndex} in route ${this.serviceRouteId}`,
198
296
  );
199
297
  }
200
298
  return Time.fromMinutes(arrival);
@@ -203,17 +301,16 @@ export class Route {
203
301
  /**
204
302
  * Retrieves the departure time at a specific stop for a given trip.
205
303
  *
206
- * @param stopId - The identifier of the stop.
304
+ * @param stopIndex - The index of the stop in the route.
207
305
  * @param tripIndex - The index of the trip.
208
306
  * @returns The departure time at the specified stop and trip as a Time object.
209
307
  */
210
- departureFrom(stopId: StopId, tripIndex: TripIndex): Time {
211
- const departureIndex =
212
- (tripIndex * this.stops.length + this.stopIndex(stopId)) * 2 + 1;
308
+ departureFrom(stopIndex: StopRouteIndex, tripIndex: TripRouteIndex): Time {
309
+ const departureIndex = (tripIndex * this.stops.length + stopIndex) * 2 + 1;
213
310
  const departure = this.stopTimes[departureIndex];
214
311
  if (departure === undefined) {
215
312
  throw new Error(
216
- `Departure time not found for stop ${stopId} at trip index ${tripIndex} in route ${this.serviceRouteId}`,
313
+ `Departure time not found for stop ${this.stopId(stopIndex)} (${stopIndex}) at trip index ${tripIndex} in route ${this.serviceRouteId}`,
217
314
  );
218
315
  }
219
316
  return Time.fromMinutes(departure);
@@ -222,19 +319,22 @@ export class Route {
222
319
  /**
223
320
  * Retrieves the pick-up type for a specific stop and trip.
224
321
  *
225
- * @param stopId - The identifier of the stop.
322
+ * @param stopIndex - The index of the stop in the route.
226
323
  * @param tripIndex - The index of the trip.
227
324
  * @returns The pick-up type at the specified stop and trip.
228
325
  */
229
- pickUpTypeFrom(stopId: StopId, tripIndex: TripIndex): PickUpDropOffType {
230
- const globalIndex = tripIndex * this.stops.length + this.stopIndex(stopId);
326
+ pickUpTypeFrom(
327
+ stopIndex: StopRouteIndex,
328
+ tripIndex: TripRouteIndex,
329
+ ): PickUpDropOffType {
330
+ const globalIndex = tripIndex * this.stops.length + stopIndex;
231
331
  const byteIndex = Math.floor(globalIndex / 2);
232
332
  const isSecondPair = globalIndex % 2 === 1;
233
333
 
234
334
  const byte = this.pickUpDropOffTypes[byteIndex];
235
335
  if (byte === undefined) {
236
336
  throw new Error(
237
- `Pick up type not found for stop ${stopId} at trip index ${tripIndex} in route ${this.serviceRouteId}`,
337
+ `Pick up type not found for stop ${this.stopId(stopIndex)} (${stopIndex}) at trip index ${tripIndex} in route ${this.serviceRouteId}`,
238
338
  );
239
339
  }
240
340
 
@@ -247,19 +347,22 @@ export class Route {
247
347
  /**
248
348
  * Retrieves the drop-off type for a specific stop and trip.
249
349
  *
250
- * @param stopId - The identifier of the stop.
350
+ * @param stopIndex - The index of the stop in the route.
251
351
  * @param tripIndex - The index of the trip.
252
352
  * @returns The drop-off type at the specified stop and trip.
253
353
  */
254
- dropOffTypeAt(stopId: StopId, tripIndex: TripIndex): PickUpDropOffType {
255
- const globalIndex = tripIndex * this.stops.length + this.stopIndex(stopId);
354
+ dropOffTypeAt(
355
+ stopIndex: StopRouteIndex,
356
+ tripIndex: TripRouteIndex,
357
+ ): PickUpDropOffType {
358
+ const globalIndex = tripIndex * this.stops.length + stopIndex;
256
359
  const byteIndex = Math.floor(globalIndex / 2);
257
360
  const isSecondPair = globalIndex % 2 === 1;
258
361
 
259
362
  const byte = this.pickUpDropOffTypes[byteIndex];
260
363
  if (byte === undefined) {
261
364
  throw new Error(
262
- `Drop off type not found for stop ${stopId} at trip index ${tripIndex} in route ${this.serviceRouteId}`,
365
+ `Drop off type not found for stop ${this.stopId(stopIndex)} (${stopIndex}) at trip index ${tripIndex} in route ${this.serviceRouteId}`,
263
366
  );
264
367
  }
265
368
 
@@ -274,7 +377,7 @@ export class Route {
274
377
  * optionally constrained by a latest trip index and a time before which the trip
275
378
  * should not depart.
276
379
  * *
277
- * @param stopId - The StopId of the stop where the trip should be found.
380
+ * @param stopIndex - The route index of the stop where the trip should be found.
278
381
  * @param [after=Time.origin()] - The earliest time after which the trip should depart.
279
382
  * If not provided, searches all available trips.
280
383
  * @param [beforeTrip] - (Optional) The index of the trip before which the search should be constrained.
@@ -282,10 +385,10 @@ export class Route {
282
385
  * @returns The index of the earliest trip meeting the criteria, or undefined if no such trip is found.
283
386
  */
284
387
  findEarliestTrip(
285
- stopId: StopId,
388
+ stopIndex: StopRouteIndex,
286
389
  after: Time = Time.origin(),
287
- beforeTrip?: TripIndex,
288
- ): TripIndex | undefined {
390
+ beforeTrip?: TripRouteIndex,
391
+ ): TripRouteIndex | undefined {
289
392
  if (this.nbTrips <= 0) return undefined;
290
393
 
291
394
  let hi = this.nbTrips - 1;
@@ -296,7 +399,7 @@ export class Route {
296
399
  let lb = -1;
297
400
  while (lo <= hi) {
298
401
  const mid = (lo + hi) >>> 1;
299
- const depMid = this.departureFrom(stopId, mid);
402
+ const depMid = this.departureFrom(stopIndex, mid);
300
403
  if (depMid.isBefore(after)) {
301
404
  lo = mid + 1;
302
405
  } else {
@@ -307,7 +410,7 @@ export class Route {
307
410
  if (lb === -1) return undefined;
308
411
 
309
412
  for (let t = lb; t < (beforeTrip ?? this.nbTrips); t++) {
310
- const pickup = this.pickUpTypeFrom(stopId, t);
413
+ const pickup = this.pickUpTypeFrom(stopIndex, t);
311
414
  if (pickup !== 'NOT_AVAILABLE') {
312
415
  return t;
313
416
  }
@@ -316,17 +419,30 @@ export class Route {
316
419
  }
317
420
 
318
421
  /**
319
- * Retrieves the index of a stop within the route.
422
+ * Retrieves the indices of a stop within the route.
320
423
  * @param stopId The StopId of the stop to locate in the route.
321
- * @returns The index of the stop in the route.
424
+ * @returns An array of indices where the stop appears in the route, or an empty array if the stop is not found.
322
425
  */
323
- public stopIndex(stopId: StopId): number {
426
+ public stopRouteIndices(stopId: StopId): StopRouteIndex[] {
324
427
  const stopIndex = this.stopIndices.get(stopId);
325
428
  if (stopIndex === undefined) {
429
+ return [];
430
+ }
431
+ return stopIndex;
432
+ }
433
+
434
+ /**
435
+ * Retrieves the id of a stop at a given index in a route.
436
+ * @param stopRouteIndex The route index of the stop.
437
+ * @returns The id of the stop at the given index in the route.
438
+ */
439
+ public stopId(stopRouteIndex: StopRouteIndex): StopId {
440
+ const stopId = this.stops[stopRouteIndex];
441
+ if (stopId === undefined) {
326
442
  throw new Error(
327
- `Stop index for ${stopId} not found in route ${this.serviceRouteId}`,
443
+ `StopId for stop at index ${stopRouteIndex} not found in route ${this.serviceRouteId}`,
328
444
  );
329
445
  }
330
- return stopIndex;
446
+ return stopId;
331
447
  }
332
448
  }
@@ -7,12 +7,15 @@ import {
7
7
  deserializeRoutesAdjacency,
8
8
  deserializeServiceRoutesMap,
9
9
  deserializeStopsAdjacency,
10
+ deserializeTripContinuations,
10
11
  serializeRoutesAdjacency,
11
12
  serializeServiceRoutesMap,
12
13
  serializeStopsAdjacency,
14
+ serializeTripContinuations,
13
15
  } from './io.js';
14
16
  import { Timetable as ProtoTimetable } from './proto/timetable.js';
15
- import { Route, RouteId } from './route.js';
17
+ import { Route, RouteId, StopRouteIndex, TripRouteIndex } from './route.js';
18
+ import { encode, TripBoardingId } from './tripBoardingId.js';
16
19
 
17
20
  export type TransferType =
18
21
  | 'RECOMMENDED'
@@ -26,11 +29,19 @@ export type Transfer = {
26
29
  minTransferTime?: Duration;
27
30
  };
28
31
 
32
+ export type TripBoarding = {
33
+ hopOnStopIndex: StopRouteIndex;
34
+ routeId: RouteId;
35
+ tripIndex: TripRouteIndex;
36
+ };
37
+
29
38
  export type StopAdjacency = {
30
- transfers: Transfer[];
39
+ transfers?: Transfer[];
31
40
  routes: RouteId[];
32
41
  };
33
42
 
43
+ export type TripContinuations = Map<TripBoardingId, TripBoarding[]>;
44
+
34
45
  export type ServiceRouteId = number;
35
46
 
36
47
  export type RouteType =
@@ -68,7 +79,9 @@ export const ALL_TRANSPORT_MODES: Set<RouteType> = new Set([
68
79
  'MONORAIL',
69
80
  ]);
70
81
 
71
- export const CURRENT_VERSION = '0.0.7';
82
+ const EMPTY_TRIP_CONTINUATIONS: TripBoarding[] = [];
83
+
84
+ export const CURRENT_VERSION = '0.0.9';
72
85
 
73
86
  /**
74
87
  * The internal transit timetable format.
@@ -77,21 +90,26 @@ export class Timetable {
77
90
  private readonly stopsAdjacency: StopAdjacency[];
78
91
  private readonly routesAdjacency: Route[];
79
92
  private readonly serviceRoutes: ServiceRoute[];
93
+ private readonly tripContinuations?: TripContinuations;
80
94
  private readonly activeStops: Set<StopId>;
81
95
 
82
96
  constructor(
83
97
  stopsAdjacency: StopAdjacency[],
84
98
  routesAdjacency: Route[],
85
99
  routes: ServiceRoute[],
100
+ tripContinuations?: TripContinuations,
86
101
  ) {
87
102
  this.stopsAdjacency = stopsAdjacency;
88
103
  this.routesAdjacency = routesAdjacency;
89
104
  this.serviceRoutes = routes;
105
+ this.tripContinuations = tripContinuations;
90
106
  this.activeStops = new Set<StopId>();
91
107
  for (let i = 0; i < stopsAdjacency.length; i++) {
92
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
93
108
  const stop = stopsAdjacency[i]!;
94
- if (stop.routes.length > 0 || stop.transfers.length > 0) {
109
+ if (
110
+ stop.routes.length > 0 ||
111
+ (stop.transfers && stop.transfers.length > 0)
112
+ ) {
95
113
  this.activeStops.add(i);
96
114
  }
97
115
  }
@@ -108,6 +126,9 @@ export class Timetable {
108
126
  stopsAdjacency: serializeStopsAdjacency(this.stopsAdjacency),
109
127
  routesAdjacency: serializeRoutesAdjacency(this.routesAdjacency),
110
128
  serviceRoutes: serializeServiceRoutesMap(this.serviceRoutes),
129
+ tripContinuations: serializeTripContinuations(
130
+ this.tripContinuations || new Map<TripBoardingId, TripBoarding[]>(),
131
+ ),
111
132
  };
112
133
  const writer = new BinaryWriter();
113
134
  ProtoTimetable.encode(protoTimetable, writer);
@@ -131,8 +152,8 @@ export class Timetable {
131
152
  return new Timetable(
132
153
  deserializeStopsAdjacency(protoTimetable.stopsAdjacency),
133
154
  deserializeRoutesAdjacency(protoTimetable.routesAdjacency),
134
-
135
155
  deserializeServiceRoutesMap(protoTimetable.serviceRoutes),
156
+ deserializeTripContinuations(protoTimetable.tripContinuations),
136
157
  );
137
158
  }
138
159
 
@@ -166,7 +187,33 @@ export class Timetable {
166
187
  * @returns An array of transfer options available at the stop.
167
188
  */
168
189
  getTransfers(stopId: StopId): Transfer[] {
169
- return this.stopsAdjacency[stopId]?.transfers ?? [];
190
+ const stopAdjacency = this.stopsAdjacency[stopId];
191
+ if (!stopAdjacency) {
192
+ throw new Error(`Stop ID ${stopId} not found`);
193
+ }
194
+ return stopAdjacency.transfers || [];
195
+ }
196
+
197
+ /**
198
+ * Retrieves all trip continuation options available at the specified stop for a given trip.
199
+ *
200
+ * @param stopIndex - The index in the route of the stop to get trip continuations for.
201
+ * @param routeId - The ID of the route to get continuations for.
202
+ * @param tripIndex - The index of the trip to get continuations for.
203
+ * @returns An array of trip continuation options available at the stop for the specified trip.
204
+ */
205
+ getContinuousTrips(
206
+ stopIndex: StopRouteIndex,
207
+ routeId: RouteId,
208
+ tripIndex: TripRouteIndex,
209
+ ): TripBoarding[] {
210
+ const tripContinuations = this.tripContinuations?.get(
211
+ encode(stopIndex, routeId, tripIndex),
212
+ );
213
+ if (!tripContinuations) {
214
+ return EMPTY_TRIP_CONTINUATIONS;
215
+ }
216
+ return tripContinuations;
170
217
  }
171
218
 
172
219
  /**
@@ -211,18 +258,18 @@ export class Timetable {
211
258
 
212
259
  /**
213
260
  * Finds routes that are reachable from a set of stop IDs.
214
- * Also identifies the first stop available to hop on each route among
261
+ * Also identifies the first stop index available to hop on each route among
215
262
  * the input stops.
216
263
  *
217
264
  * @param fromStops - The set of stop IDs to find reachable routes from.
218
265
  * @param transportModes - The set of transport modes to consider for reachable routes.
219
- * @returns A map of reachable routes to the first stop available to hop on each route.
266
+ * @returns A map of reachable routes to the first stop index available to hop on each route.
220
267
  */
221
268
  findReachableRoutes(
222
269
  fromStops: Set<StopId>,
223
270
  transportModes: Set<RouteType> = ALL_TRANSPORT_MODES,
224
- ): Map<Route, StopId> {
225
- const reachableRoutes = new Map<Route, StopId>();
271
+ ): Map<Route, StopRouteIndex> {
272
+ const reachableRoutes = new Map<Route, StopRouteIndex>();
226
273
  const fromStopsArray = Array.from(fromStops);
227
274
  for (let i = 0; i < fromStopsArray.length; i++) {
228
275
  const originStop = fromStopsArray[i]!;
@@ -234,14 +281,17 @@ export class Timetable {
234
281
  );
235
282
  for (let j = 0; j < validRoutes.length; j++) {
236
283
  const route = validRoutes[j]!;
237
- const hopOnStop = reachableRoutes.get(route);
238
- if (hopOnStop) {
239
- if (route.isBefore(originStop, hopOnStop)) {
284
+ const originStopIndices = route.stopRouteIndices(originStop);
285
+ const originStopIndex = originStopIndices[0]!;
286
+
287
+ const existingHopOnStopIndex = reachableRoutes.get(route);
288
+ if (existingHopOnStopIndex !== undefined) {
289
+ if (originStopIndex < existingHopOnStopIndex) {
240
290
  // if the current stop is before the existing hop on stop, replace it
241
- reachableRoutes.set(route, originStop);
291
+ reachableRoutes.set(route, originStopIndex);
242
292
  }
243
293
  } else {
244
- reachableRoutes.set(route, originStop);
294
+ reachableRoutes.set(route, originStopIndex);
245
295
  }
246
296
  }
247
297
  }
@@ -0,0 +1,94 @@
1
+ import { RouteId, StopRouteIndex, TripRouteIndex } from './route.js';
2
+
3
+ // Each value uses 20 bits, allowing values from 0 to 1,048,575 (2^20 - 1)
4
+ const VALUE_MASK = (1n << 20n) - 1n; // 0xFFFFF
5
+ const MAX_VALUE = 1_048_575; // 2^20 - 1
6
+
7
+ // Bit positions for each value in the 60-bit bigint
8
+ const TRIP_INDEX_SHIFT = 0n;
9
+ const ROUTE_ID_SHIFT = 20n;
10
+ const STOP_INDEX_SHIFT = 40n;
11
+
12
+ // A TripId encodes a stop index, route ID, and trip index into a single bigint value
13
+ export type TripBoardingId = bigint;
14
+
15
+ /**
16
+ * Validates that a value fits within 20 bits (0 to 1,048,575)
17
+ * @param value - The value to validate
18
+ * @param name - The name of the value for error reporting
19
+ * @throws Error if the value is out of range
20
+ */
21
+ const validateValue = (value: number, name: string): void => {
22
+ if (value < 0 || value > MAX_VALUE) {
23
+ throw new Error(`${name} must be between 0 and ${MAX_VALUE}, got ${value}`);
24
+ }
25
+ };
26
+
27
+ /**
28
+ * Encodes a stop index, route ID, and trip index into a single trip boarding ID.
29
+ * @param stopIndex - The index of the stop within the route (0 to 1,048,575)
30
+ * @param routeId - The route identifier (0 to 1,048,575)
31
+ * @param tripIndex - The index of the trip within the route (0 to 1,048,575)
32
+ * @returns The encoded trip ID as a bigint
33
+ */
34
+ export const encode = (
35
+ stopIndex: StopRouteIndex,
36
+ routeId: RouteId,
37
+ tripIndex: TripRouteIndex,
38
+ ): TripBoardingId => {
39
+ validateValue(stopIndex, 'stopIndex');
40
+ validateValue(routeId, 'routeId');
41
+ validateValue(tripIndex, 'tripIndex');
42
+
43
+ return (
44
+ (BigInt(stopIndex) << STOP_INDEX_SHIFT) |
45
+ (BigInt(routeId) << ROUTE_ID_SHIFT) |
46
+ (BigInt(tripIndex) << TRIP_INDEX_SHIFT)
47
+ );
48
+ };
49
+
50
+ /**
51
+ * Decodes a trip boarding ID back into its constituent stop index, route ID, and trip index.
52
+ * @param tripBoardingId - The encoded trip ID
53
+ * @returns A tuple containing [stopIndex, routeId, tripIndex]
54
+ */
55
+ export const decode = (
56
+ tripBoardingId: TripBoardingId,
57
+ ): [StopRouteIndex, RouteId, TripRouteIndex] => {
58
+ const stopIndex = Number((tripBoardingId >> STOP_INDEX_SHIFT) & VALUE_MASK);
59
+ const routeId = Number((tripBoardingId >> ROUTE_ID_SHIFT) & VALUE_MASK);
60
+ const tripIndex = Number((tripBoardingId >> TRIP_INDEX_SHIFT) & VALUE_MASK);
61
+
62
+ return [stopIndex, routeId, tripIndex];
63
+ };
64
+
65
+ /**
66
+ * Extracts just the stop index from a trip ID without full decoding.
67
+ * @param tripBoardingId - The encoded trip boarding ID
68
+ * @returns The stop index
69
+ */
70
+ export const getStopIndex = (
71
+ tripBoardingId: TripBoardingId,
72
+ ): StopRouteIndex => {
73
+ return Number((tripBoardingId >> STOP_INDEX_SHIFT) & VALUE_MASK);
74
+ };
75
+
76
+ /**
77
+ * Extracts just the route ID from a trip ID without full decoding.
78
+ * @param tripBoardingId - The encoded trip boarding ID
79
+ * @returns The route ID
80
+ */
81
+ export const getRouteId = (tripBoardingId: TripBoardingId): RouteId => {
82
+ return Number((tripBoardingId >> ROUTE_ID_SHIFT) & VALUE_MASK);
83
+ };
84
+
85
+ /**
86
+ * Extracts just the trip index from a trip ID without full decoding.
87
+ * @param tripBoardingId - The encoded trip boarding ID
88
+ * @returns The trip index
89
+ */
90
+ export const getTripIndex = (
91
+ tripBoardingId: TripBoardingId,
92
+ ): TripRouteIndex => {
93
+ return Number((tripBoardingId >> TRIP_INDEX_SHIFT) & VALUE_MASK);
94
+ };
package/tsconfig.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "include": ["./src/**/*.ts"],
3
3
  "compilerOptions": {
4
- "target": "es2015",
5
- "lib": ["es2019"],
4
+ "target": "es2022",
5
+ "lib": ["es2022"],
6
6
  "module": "NodeNext",
7
7
  "moduleResolution": "NodeNext",
8
8
  "rootDir": "./src",