minotor 7.0.1 → 8.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 (59) 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 +1268 -290
  5. package/dist/cli.mjs.map +1 -1
  6. package/dist/gtfs/transfers.d.ts +13 -4
  7. package/dist/gtfs/trips.d.ts +12 -7
  8. package/dist/parser.cjs.js +519 -95
  9. package/dist/parser.cjs.js.map +1 -1
  10. package/dist/parser.esm.js +519 -95
  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__/tripId.test.d.ts +1 -0
  25. package/dist/timetable/io.d.ts +4 -2
  26. package/dist/timetable/proto/timetable.d.ts +13 -1
  27. package/dist/timetable/route.d.ts +41 -8
  28. package/dist/timetable/timetable.d.ts +18 -3
  29. package/dist/timetable/tripId.d.ts +15 -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 +259 -1
  35. package/src/gtfs/__tests__/transfers.test.ts +468 -12
  36. package/src/gtfs/__tests__/trips.test.ts +350 -28
  37. package/src/gtfs/parser.ts +16 -4
  38. package/src/gtfs/transfers.ts +61 -18
  39. package/src/gtfs/trips.ts +97 -22
  40. package/src/router.ts +2 -2
  41. package/src/routing/__tests__/plotter.test.ts +230 -0
  42. package/src/routing/__tests__/result.test.ts +486 -125
  43. package/src/routing/__tests__/route.test.ts +7 -3
  44. package/src/routing/__tests__/router.test.ts +378 -172
  45. package/src/routing/plotter.ts +279 -48
  46. package/src/routing/result.ts +114 -34
  47. package/src/routing/route.ts +0 -3
  48. package/src/routing/router.ts +332 -209
  49. package/src/timetable/__tests__/io.test.ts +33 -1
  50. package/src/timetable/__tests__/route.test.ts +10 -3
  51. package/src/timetable/__tests__/timetable.test.ts +225 -57
  52. package/src/timetable/__tests__/tripId.test.ts +27 -0
  53. package/src/timetable/io.ts +71 -10
  54. package/src/timetable/proto/timetable.proto +14 -2
  55. package/src/timetable/proto/timetable.ts +218 -20
  56. package/src/timetable/route.ts +152 -19
  57. package/src/timetable/time.ts +23 -9
  58. package/src/timetable/timetable.ts +46 -9
  59. package/src/timetable/tripId.ts +29 -0
package/dist/cli.mjs CHANGED
@@ -30,18 +30,6 @@ PERFORMANCE OF THIS SOFTWARE.
30
30
  /* global Reflect, Promise, SuppressedError, Symbol, Iterator */
31
31
 
32
32
 
33
- function __rest(s, e) {
34
- var t = {};
35
- for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
36
- t[p] = s[p];
37
- if (s != null && typeof Object.getOwnPropertySymbols === "function")
38
- for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
39
- if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
40
- t[p[i]] = s[p[i]];
41
- }
42
- return t;
43
- }
44
-
45
33
  function __awaiter(thisArg, _arguments, P, generator) {
46
34
  function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
47
35
  return new (P || (P = Promise))(function (resolve, reject) {
@@ -16618,41 +16606,191 @@ const Transfer = {
16618
16606
  return message;
16619
16607
  },
16620
16608
  };
16621
- function createBaseStopAdjacency() {
16622
- return { transfers: [], routes: [] };
16609
+ function createBaseTripBoarding() {
16610
+ return { hopOnStop: 0, routeId: 0, tripIndex: 0 };
16623
16611
  }
16624
- const StopAdjacency = {
16612
+ const TripBoarding = {
16625
16613
  encode(message, writer = new BinaryWriter()) {
16626
- for (const v of message.transfers) {
16627
- Transfer.encode(v, writer.uint32(10).fork()).join();
16614
+ if (message.hopOnStop !== 0) {
16615
+ writer.uint32(8).uint32(message.hopOnStop);
16628
16616
  }
16629
- writer.uint32(18).fork();
16630
- for (const v of message.routes) {
16631
- writer.uint32(v);
16617
+ if (message.routeId !== 0) {
16618
+ writer.uint32(16).uint32(message.routeId);
16619
+ }
16620
+ if (message.tripIndex !== 0) {
16621
+ writer.uint32(24).uint32(message.tripIndex);
16632
16622
  }
16633
- writer.join();
16634
16623
  return writer;
16635
16624
  },
16636
16625
  decode(input, length) {
16637
16626
  const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
16638
16627
  const end = length === undefined ? reader.len : reader.pos + length;
16639
- const message = createBaseStopAdjacency();
16628
+ const message = createBaseTripBoarding();
16640
16629
  while (reader.pos < end) {
16641
16630
  const tag = reader.uint32();
16642
16631
  switch (tag >>> 3) {
16643
16632
  case 1: {
16644
- if (tag !== 10) {
16633
+ if (tag !== 8) {
16645
16634
  break;
16646
16635
  }
16647
- message.transfers.push(Transfer.decode(reader, reader.uint32()));
16636
+ message.hopOnStop = reader.uint32();
16637
+ continue;
16638
+ }
16639
+ case 2: {
16640
+ if (tag !== 16) {
16641
+ break;
16642
+ }
16643
+ message.routeId = reader.uint32();
16644
+ continue;
16645
+ }
16646
+ case 3: {
16647
+ if (tag !== 24) {
16648
+ break;
16649
+ }
16650
+ message.tripIndex = reader.uint32();
16651
+ continue;
16652
+ }
16653
+ }
16654
+ if ((tag & 7) === 4 || tag === 0) {
16655
+ break;
16656
+ }
16657
+ reader.skip(tag & 7);
16658
+ }
16659
+ return message;
16660
+ },
16661
+ fromJSON(object) {
16662
+ return {
16663
+ hopOnStop: isSet(object.hopOnStop) ? globalThis.Number(object.hopOnStop) : 0,
16664
+ routeId: isSet(object.routeId) ? globalThis.Number(object.routeId) : 0,
16665
+ tripIndex: isSet(object.tripIndex) ? globalThis.Number(object.tripIndex) : 0,
16666
+ };
16667
+ },
16668
+ toJSON(message) {
16669
+ const obj = {};
16670
+ if (message.hopOnStop !== 0) {
16671
+ obj.hopOnStop = Math.round(message.hopOnStop);
16672
+ }
16673
+ if (message.routeId !== 0) {
16674
+ obj.routeId = Math.round(message.routeId);
16675
+ }
16676
+ if (message.tripIndex !== 0) {
16677
+ obj.tripIndex = Math.round(message.tripIndex);
16678
+ }
16679
+ return obj;
16680
+ },
16681
+ create(base) {
16682
+ return TripBoarding.fromPartial(base !== null && base !== void 0 ? base : {});
16683
+ },
16684
+ fromPartial(object) {
16685
+ var _a, _b, _c;
16686
+ const message = createBaseTripBoarding();
16687
+ message.hopOnStop = (_a = object.hopOnStop) !== null && _a !== void 0 ? _a : 0;
16688
+ message.routeId = (_b = object.routeId) !== null && _b !== void 0 ? _b : 0;
16689
+ message.tripIndex = (_c = object.tripIndex) !== null && _c !== void 0 ? _c : 0;
16690
+ return message;
16691
+ },
16692
+ };
16693
+ function createBaseTripContinuationEntry() {
16694
+ return { key: 0, value: [] };
16695
+ }
16696
+ const TripContinuationEntry = {
16697
+ encode(message, writer = new BinaryWriter()) {
16698
+ if (message.key !== 0) {
16699
+ writer.uint32(8).uint32(message.key);
16700
+ }
16701
+ for (const v of message.value) {
16702
+ TripBoarding.encode(v, writer.uint32(18).fork()).join();
16703
+ }
16704
+ return writer;
16705
+ },
16706
+ decode(input, length) {
16707
+ const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
16708
+ const end = length === undefined ? reader.len : reader.pos + length;
16709
+ const message = createBaseTripContinuationEntry();
16710
+ while (reader.pos < end) {
16711
+ const tag = reader.uint32();
16712
+ switch (tag >>> 3) {
16713
+ case 1: {
16714
+ if (tag !== 8) {
16715
+ break;
16716
+ }
16717
+ message.key = reader.uint32();
16648
16718
  continue;
16649
16719
  }
16650
16720
  case 2: {
16651
- if (tag === 16) {
16721
+ if (tag !== 18) {
16722
+ break;
16723
+ }
16724
+ message.value.push(TripBoarding.decode(reader, reader.uint32()));
16725
+ continue;
16726
+ }
16727
+ }
16728
+ if ((tag & 7) === 4 || tag === 0) {
16729
+ break;
16730
+ }
16731
+ reader.skip(tag & 7);
16732
+ }
16733
+ return message;
16734
+ },
16735
+ fromJSON(object) {
16736
+ return {
16737
+ key: isSet(object.key) ? globalThis.Number(object.key) : 0,
16738
+ value: globalThis.Array.isArray(object === null || object === void 0 ? void 0 : object.value) ? object.value.map((e) => TripBoarding.fromJSON(e)) : [],
16739
+ };
16740
+ },
16741
+ toJSON(message) {
16742
+ var _a;
16743
+ const obj = {};
16744
+ if (message.key !== 0) {
16745
+ obj.key = Math.round(message.key);
16746
+ }
16747
+ if ((_a = message.value) === null || _a === void 0 ? void 0 : _a.length) {
16748
+ obj.value = message.value.map((e) => TripBoarding.toJSON(e));
16749
+ }
16750
+ return obj;
16751
+ },
16752
+ create(base) {
16753
+ return TripContinuationEntry.fromPartial(base !== null && base !== void 0 ? base : {});
16754
+ },
16755
+ fromPartial(object) {
16756
+ var _a, _b;
16757
+ const message = createBaseTripContinuationEntry();
16758
+ message.key = (_a = object.key) !== null && _a !== void 0 ? _a : 0;
16759
+ message.value = ((_b = object.value) === null || _b === void 0 ? void 0 : _b.map((e) => TripBoarding.fromPartial(e))) || [];
16760
+ return message;
16761
+ },
16762
+ };
16763
+ function createBaseStopAdjacency() {
16764
+ return { routes: [], transfers: [], tripContinuations: [] };
16765
+ }
16766
+ const StopAdjacency = {
16767
+ encode(message, writer = new BinaryWriter()) {
16768
+ writer.uint32(10).fork();
16769
+ for (const v of message.routes) {
16770
+ writer.uint32(v);
16771
+ }
16772
+ writer.join();
16773
+ for (const v of message.transfers) {
16774
+ Transfer.encode(v, writer.uint32(18).fork()).join();
16775
+ }
16776
+ for (const v of message.tripContinuations) {
16777
+ TripContinuationEntry.encode(v, writer.uint32(26).fork()).join();
16778
+ }
16779
+ return writer;
16780
+ },
16781
+ decode(input, length) {
16782
+ const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
16783
+ const end = length === undefined ? reader.len : reader.pos + length;
16784
+ const message = createBaseStopAdjacency();
16785
+ while (reader.pos < end) {
16786
+ const tag = reader.uint32();
16787
+ switch (tag >>> 3) {
16788
+ case 1: {
16789
+ if (tag === 8) {
16652
16790
  message.routes.push(reader.uint32());
16653
16791
  continue;
16654
16792
  }
16655
- if (tag === 18) {
16793
+ if (tag === 10) {
16656
16794
  const end2 = reader.uint32() + reader.pos;
16657
16795
  while (reader.pos < end2) {
16658
16796
  message.routes.push(reader.uint32());
@@ -16661,6 +16799,20 @@ const StopAdjacency = {
16661
16799
  }
16662
16800
  break;
16663
16801
  }
16802
+ case 2: {
16803
+ if (tag !== 18) {
16804
+ break;
16805
+ }
16806
+ message.transfers.push(Transfer.decode(reader, reader.uint32()));
16807
+ continue;
16808
+ }
16809
+ case 3: {
16810
+ if (tag !== 26) {
16811
+ break;
16812
+ }
16813
+ message.tripContinuations.push(TripContinuationEntry.decode(reader, reader.uint32()));
16814
+ continue;
16815
+ }
16664
16816
  }
16665
16817
  if ((tag & 7) === 4 || tag === 0) {
16666
16818
  break;
@@ -16671,20 +16823,26 @@ const StopAdjacency = {
16671
16823
  },
16672
16824
  fromJSON(object) {
16673
16825
  return {
16826
+ routes: globalThis.Array.isArray(object === null || object === void 0 ? void 0 : object.routes) ? object.routes.map((e) => globalThis.Number(e)) : [],
16674
16827
  transfers: globalThis.Array.isArray(object === null || object === void 0 ? void 0 : object.transfers)
16675
16828
  ? object.transfers.map((e) => Transfer.fromJSON(e))
16676
16829
  : [],
16677
- routes: globalThis.Array.isArray(object === null || object === void 0 ? void 0 : object.routes) ? object.routes.map((e) => globalThis.Number(e)) : [],
16830
+ tripContinuations: globalThis.Array.isArray(object === null || object === void 0 ? void 0 : object.tripContinuations)
16831
+ ? object.tripContinuations.map((e) => TripContinuationEntry.fromJSON(e))
16832
+ : [],
16678
16833
  };
16679
16834
  },
16680
16835
  toJSON(message) {
16681
- var _a, _b;
16836
+ var _a, _b, _c;
16682
16837
  const obj = {};
16683
- if ((_a = message.transfers) === null || _a === void 0 ? void 0 : _a.length) {
16838
+ if ((_a = message.routes) === null || _a === void 0 ? void 0 : _a.length) {
16839
+ obj.routes = message.routes.map((e) => Math.round(e));
16840
+ }
16841
+ if ((_b = message.transfers) === null || _b === void 0 ? void 0 : _b.length) {
16684
16842
  obj.transfers = message.transfers.map((e) => Transfer.toJSON(e));
16685
16843
  }
16686
- if ((_b = message.routes) === null || _b === void 0 ? void 0 : _b.length) {
16687
- obj.routes = message.routes.map((e) => Math.round(e));
16844
+ if ((_c = message.tripContinuations) === null || _c === void 0 ? void 0 : _c.length) {
16845
+ obj.tripContinuations = message.tripContinuations.map((e) => TripContinuationEntry.toJSON(e));
16688
16846
  }
16689
16847
  return obj;
16690
16848
  },
@@ -16692,10 +16850,11 @@ const StopAdjacency = {
16692
16850
  return StopAdjacency.fromPartial(base !== null && base !== void 0 ? base : {});
16693
16851
  },
16694
16852
  fromPartial(object) {
16695
- var _a, _b;
16853
+ var _a, _b, _c;
16696
16854
  const message = createBaseStopAdjacency();
16697
- message.transfers = ((_a = object.transfers) === null || _a === void 0 ? void 0 : _a.map((e) => Transfer.fromPartial(e))) || [];
16698
- message.routes = ((_b = object.routes) === null || _b === void 0 ? void 0 : _b.map((e) => e)) || [];
16855
+ message.routes = ((_a = object.routes) === null || _a === void 0 ? void 0 : _a.map((e) => e)) || [];
16856
+ message.transfers = ((_b = object.transfers) === null || _b === void 0 ? void 0 : _b.map((e) => Transfer.fromPartial(e))) || [];
16857
+ message.tripContinuations = ((_c = object.tripContinuations) === null || _c === void 0 ? void 0 : _c.map((e) => TripContinuationEntry.fromPartial(e))) || [];
16699
16858
  return message;
16700
16859
  },
16701
16860
  };
@@ -17094,9 +17253,16 @@ class Time {
17094
17253
  if (times.length === 0) {
17095
17254
  throw new Error('At least one Time instance is required.');
17096
17255
  }
17097
- return times.reduce((maxTime, currentTime) => {
17098
- return currentTime.isAfter(maxTime) ? currentTime : maxTime;
17099
- });
17256
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
17257
+ let maxTime = times[0];
17258
+ for (let i = 1; i < times.length; i++) {
17259
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
17260
+ if (times[i].minutesSinceMidnight > maxTime.minutesSinceMidnight) {
17261
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
17262
+ maxTime = times[i];
17263
+ }
17264
+ }
17265
+ return maxTime;
17100
17266
  }
17101
17267
  /**
17102
17268
  * Computes the minimum Time instance among the provided Time instances.
@@ -17108,9 +17274,16 @@ class Time {
17108
17274
  if (times.length === 0) {
17109
17275
  throw new Error('At least one Time instance is required.');
17110
17276
  }
17111
- return times.reduce((minTime, currentTime) => {
17112
- return currentTime.isBefore(minTime) ? currentTime : minTime;
17113
- });
17277
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
17278
+ let minTime = times[0];
17279
+ for (let i = 1; i < times.length; i++) {
17280
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
17281
+ if (times[i].minutesSinceMidnight < minTime.minutesSinceMidnight) {
17282
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
17283
+ minTime = times[i];
17284
+ }
17285
+ }
17286
+ return minTime;
17114
17287
  }
17115
17288
  /**
17116
17289
  * Determines if the current Time instance is after another Time instance.
@@ -17119,7 +17292,7 @@ class Time {
17119
17292
  * @returns True if the current Time instance is after the other Time instance, otherwise false.
17120
17293
  */
17121
17294
  isAfter(otherTime) {
17122
- return this.minutesSinceMidnight > otherTime.toMinutes();
17295
+ return this.minutesSinceMidnight > otherTime.minutesSinceMidnight;
17123
17296
  }
17124
17297
  /**
17125
17298
  * Determines if the current Time instance is before another Time instance.
@@ -17128,7 +17301,7 @@ class Time {
17128
17301
  * @returns True if the current Time instance is before the other Time instance, otherwise false.
17129
17302
  */
17130
17303
  isBefore(otherTime) {
17131
- return this.minutesSinceMidnight < otherTime.toMinutes();
17304
+ return this.minutesSinceMidnight < otherTime.minutesSinceMidnight;
17132
17305
  }
17133
17306
  /**
17134
17307
  * Determines if the current Time instance is equal to another Time instance.
@@ -17137,7 +17310,7 @@ class Time {
17137
17310
  * @returns True if the current Time instance is equal to the other Time instance, otherwise false.
17138
17311
  */
17139
17312
  equals(otherTime) {
17140
- return this.minutesSinceMidnight === otherTime.toMinutes();
17313
+ return this.minutesSinceMidnight === otherTime.minutesSinceMidnight;
17141
17314
  }
17142
17315
  }
17143
17316
 
@@ -17170,7 +17343,8 @@ const toPickupDropOffType = (numericalType) => {
17170
17343
  * A route identifies all trips of a given service route sharing the same list of stops.
17171
17344
  */
17172
17345
  let Route$1 = class Route {
17173
- constructor(stopTimes, pickUpDropOffTypes, stops, serviceRouteId) {
17346
+ constructor(id, stopTimes, pickUpDropOffTypes, stops, serviceRouteId) {
17347
+ this.id = id;
17174
17348
  this.stopTimes = stopTimes;
17175
17349
  this.pickUpDropOffTypes = pickUpDropOffTypes;
17176
17350
  this.stops = stops;
@@ -17183,6 +17357,78 @@ let Route$1 = class Route {
17183
17357
  this.stopIndices.set(stops[i], i);
17184
17358
  }
17185
17359
  }
17360
+ /**
17361
+ * Creates a new route from multiple trips with their stops.
17362
+ *
17363
+ * @param params The route parameters including ID, service route ID, and trips.
17364
+ * @returns The new route.
17365
+ */
17366
+ static of(params) {
17367
+ var _a, _b;
17368
+ const { id, serviceRouteId, trips } = params;
17369
+ if (trips.length === 0) {
17370
+ throw new Error('At least one trip must be provided');
17371
+ }
17372
+ // All trips must have the same stops in the same order
17373
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
17374
+ const firstTrip = trips[0];
17375
+ const stopIds = new Uint32Array(firstTrip.stops.map((stop) => stop.id));
17376
+ const numStops = stopIds.length;
17377
+ // Validate all trips have the same stops
17378
+ for (let tripIndex = 1; tripIndex < trips.length; tripIndex++) {
17379
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
17380
+ const trip = trips[tripIndex];
17381
+ if (trip.stops.length !== numStops) {
17382
+ throw new Error(`Trip ${tripIndex} has ${trip.stops.length} stops, expected ${numStops}`);
17383
+ }
17384
+ for (let stopIndex = 0; stopIndex < numStops; stopIndex++) {
17385
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
17386
+ if (trip.stops[stopIndex].id !== stopIds[stopIndex]) {
17387
+ throw new Error(`Trip ${tripIndex} has different stop at index ${stopIndex}`);
17388
+ }
17389
+ }
17390
+ }
17391
+ // Create stopTimes array with arrivals and departures for all trips
17392
+ const stopTimes = new Uint16Array(trips.length * numStops * 2);
17393
+ for (let tripIndex = 0; tripIndex < trips.length; tripIndex++) {
17394
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
17395
+ const trip = trips[tripIndex];
17396
+ for (let stopIndex = 0; stopIndex < numStops; stopIndex++) {
17397
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
17398
+ const stop = trip.stops[stopIndex];
17399
+ const baseIndex = (tripIndex * numStops + stopIndex) * 2;
17400
+ stopTimes[baseIndex] = stop.arrivalTime.toMinutes();
17401
+ stopTimes[baseIndex + 1] = stop.departureTime.toMinutes();
17402
+ }
17403
+ }
17404
+ // Create pickUpDropOffTypes array (2-bit encoded) for all trips
17405
+ const totalStopEntries = trips.length * numStops;
17406
+ const pickUpDropOffTypes = new Uint8Array(Math.ceil(totalStopEntries / 2));
17407
+ for (let tripIndex = 0; tripIndex < trips.length; tripIndex++) {
17408
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
17409
+ const trip = trips[tripIndex];
17410
+ for (let stopIndex = 0; stopIndex < numStops; stopIndex++) {
17411
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
17412
+ const stop = trip.stops[stopIndex];
17413
+ const globalIndex = tripIndex * numStops + stopIndex;
17414
+ const pickUp = (_a = stop.pickUpType) !== null && _a !== void 0 ? _a : REGULAR;
17415
+ const dropOff = (_b = stop.dropOffType) !== null && _b !== void 0 ? _b : REGULAR;
17416
+ const byteIndex = Math.floor(globalIndex / 2);
17417
+ const isSecondPair = globalIndex % 2 === 1;
17418
+ if (isSecondPair) {
17419
+ // Second pair: pickup in upper 2 bits, dropOff in bits 4-5
17420
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
17421
+ pickUpDropOffTypes[byteIndex] |= (pickUp << 6) | (dropOff << 4);
17422
+ }
17423
+ else {
17424
+ // First pair: pickup in bits 2-3, dropOff in lower 2 bits
17425
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
17426
+ pickUpDropOffTypes[byteIndex] |= (pickUp << 2) | dropOff;
17427
+ }
17428
+ }
17429
+ }
17430
+ return new Route(id, stopTimes, pickUpDropOffTypes, stopIds, serviceRouteId);
17431
+ }
17186
17432
  /**
17187
17433
  * Serializes the Route into binary arrays.
17188
17434
  *
@@ -17206,11 +17452,11 @@ let Route$1 = class Route {
17206
17452
  isBefore(stopA, stopB) {
17207
17453
  const stopAIndex = this.stopIndices.get(stopA);
17208
17454
  if (stopAIndex === undefined) {
17209
- throw new Error(`Stop index ${stopAIndex} not found in route ${this.serviceRouteId}`);
17455
+ throw new Error(`Stop index not found for ${stopA} in route ${this.serviceRouteId}`);
17210
17456
  }
17211
17457
  const stopBIndex = this.stopIndices.get(stopB);
17212
17458
  if (stopBIndex === undefined) {
17213
- throw new Error(`Stop index ${stopBIndex} not found in route ${this.serviceRouteId}`);
17459
+ throw new Error(`Stop index not found for ${stopB} in route ${this.serviceRouteId}`);
17214
17460
  }
17215
17461
  return stopAIndex < stopBIndex;
17216
17462
  }
@@ -17222,6 +17468,14 @@ let Route$1 = class Route {
17222
17468
  getNbStops() {
17223
17469
  return this.nbStops;
17224
17470
  }
17471
+ /**
17472
+ * Retrieves the number of trips in the route.
17473
+ *
17474
+ * @returns The total number of trips in the route.
17475
+ */
17476
+ getNbTrips() {
17477
+ return this.nbTrips;
17478
+ }
17225
17479
  /**
17226
17480
  * Finds the ServiceRouteId of the route. It corresponds the identifier
17227
17481
  * of the service shown to the end user as a route.
@@ -17239,7 +17493,7 @@ let Route$1 = class Route {
17239
17493
  * @returns The arrival time at the specified stop and trip as a Time object.
17240
17494
  */
17241
17495
  arrivalAt(stopId, tripIndex) {
17242
- const arrivalIndex = (tripIndex * this.stops.length + this.stopIndex(stopId)) * 2;
17496
+ const arrivalIndex = (tripIndex * this.stops.length + this.stopRouteIndex(stopId)) * 2;
17243
17497
  const arrival = this.stopTimes[arrivalIndex];
17244
17498
  if (arrival === undefined) {
17245
17499
  throw new Error(`Arrival time not found for stop ${stopId} at trip index ${tripIndex} in route ${this.serviceRouteId}`);
@@ -17254,7 +17508,7 @@ let Route$1 = class Route {
17254
17508
  * @returns The departure time at the specified stop and trip as a Time object.
17255
17509
  */
17256
17510
  departureFrom(stopId, tripIndex) {
17257
- const departureIndex = (tripIndex * this.stops.length + this.stopIndex(stopId)) * 2 + 1;
17511
+ const departureIndex = (tripIndex * this.stops.length + this.stopRouteIndex(stopId)) * 2 + 1;
17258
17512
  const departure = this.stopTimes[departureIndex];
17259
17513
  if (departure === undefined) {
17260
17514
  throw new Error(`Departure time not found for stop ${stopId} at trip index ${tripIndex} in route ${this.serviceRouteId}`);
@@ -17269,7 +17523,7 @@ let Route$1 = class Route {
17269
17523
  * @returns The pick-up type at the specified stop and trip.
17270
17524
  */
17271
17525
  pickUpTypeFrom(stopId, tripIndex) {
17272
- const globalIndex = tripIndex * this.stops.length + this.stopIndex(stopId);
17526
+ const globalIndex = tripIndex * this.stops.length + this.stopRouteIndex(stopId);
17273
17527
  const byteIndex = Math.floor(globalIndex / 2);
17274
17528
  const isSecondPair = globalIndex % 2 === 1;
17275
17529
  const byte = this.pickUpDropOffTypes[byteIndex];
@@ -17289,7 +17543,7 @@ let Route$1 = class Route {
17289
17543
  * @returns The drop-off type at the specified stop and trip.
17290
17544
  */
17291
17545
  dropOffTypeAt(stopId, tripIndex) {
17292
- const globalIndex = tripIndex * this.stops.length + this.stopIndex(stopId);
17546
+ const globalIndex = tripIndex * this.stops.length + this.stopRouteIndex(stopId);
17293
17547
  const byteIndex = Math.floor(globalIndex / 2);
17294
17548
  const isSecondPair = globalIndex % 2 === 1;
17295
17549
  const byte = this.pickUpDropOffTypes[byteIndex];
@@ -17349,13 +17603,25 @@ let Route$1 = class Route {
17349
17603
  * @param stopId The StopId of the stop to locate in the route.
17350
17604
  * @returns The index of the stop in the route.
17351
17605
  */
17352
- stopIndex(stopId) {
17606
+ stopRouteIndex(stopId) {
17353
17607
  const stopIndex = this.stopIndices.get(stopId);
17354
17608
  if (stopIndex === undefined) {
17355
17609
  throw new Error(`Stop index for ${stopId} not found in route ${this.serviceRouteId}`);
17356
17610
  }
17357
17611
  return stopIndex;
17358
17612
  }
17613
+ /**
17614
+ * Retrieves the id of a stop at a given index in a route.
17615
+ * @param stopRouteIndex The route index of the stop.
17616
+ * @returns The id of the stop at the given index in the route.
17617
+ */
17618
+ stopId(stopRouteIndex) {
17619
+ const stopId = this.stops[stopRouteIndex];
17620
+ if (stopId === undefined) {
17621
+ throw new Error(`StopId for stop at index ${stopRouteIndex} not found in route ${this.serviceRouteId}`);
17622
+ }
17623
+ return stopId;
17624
+ }
17359
17625
  };
17360
17626
 
17361
17627
  const isLittleEndian = (() => {
@@ -17426,10 +17692,15 @@ const bytesToUint16Array = (bytes) => {
17426
17692
  const serializeStopsAdjacency = (stopsAdjacency) => {
17427
17693
  return stopsAdjacency.map((value) => {
17428
17694
  return {
17429
- transfers: value.transfers.map((transfer) => (Object.assign({ destination: transfer.destination, type: serializeTransferType(transfer.type) }, (transfer.minTransferTime !== undefined && {
17430
- minTransferTime: transfer.minTransferTime.toSeconds(),
17431
- })))),
17695
+ transfers: value.transfers
17696
+ ? value.transfers.map((transfer) => (Object.assign({ destination: transfer.destination, type: serializeTransferType(transfer.type) }, (transfer.minTransferTime !== undefined && {
17697
+ minTransferTime: transfer.minTransferTime.toSeconds(),
17698
+ }))))
17699
+ : [],
17432
17700
  routes: value.routes,
17701
+ tripContinuations: value.tripContinuations
17702
+ ? serializeTripContinuations(value.tripContinuations)
17703
+ : [],
17433
17704
  };
17434
17705
  });
17435
17706
  };
@@ -17469,10 +17740,17 @@ const deserializeStopsAdjacency = (protoStopsAdjacency) => {
17469
17740
  }));
17470
17741
  transfers.push(newTransfer);
17471
17742
  }
17472
- result.push({
17473
- transfers: transfers,
17743
+ const stopAdjacency = {
17474
17744
  routes: value.routes,
17475
- });
17745
+ };
17746
+ if (transfers.length > 0) {
17747
+ stopAdjacency.transfers = transfers;
17748
+ }
17749
+ const deserializedTripContinuations = deserializeTripContinuations(value.tripContinuations);
17750
+ if (deserializedTripContinuations.size > 0) {
17751
+ stopAdjacency.tripContinuations = deserializedTripContinuations;
17752
+ }
17753
+ result.push(stopAdjacency);
17476
17754
  }
17477
17755
  return result;
17478
17756
  };
@@ -17482,7 +17760,7 @@ const deserializeRoutesAdjacency = (protoRoutesAdjacency) => {
17482
17760
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
17483
17761
  const value = protoRoutesAdjacency[i];
17484
17762
  const stops = bytesToUint32Array(value.stops);
17485
- routesAdjacency.push(new Route$1(bytesToUint16Array(value.stopTimes), value.pickUpDropOffTypes, stops, value.serviceRouteId));
17763
+ routesAdjacency.push(new Route$1(i, bytesToUint16Array(value.stopTimes), value.pickUpDropOffTypes, stops, value.serviceRouteId));
17486
17764
  }
17487
17765
  return routesAdjacency;
17488
17766
  };
@@ -17576,7 +17854,48 @@ const serializeRouteType = (type) => {
17576
17854
  return RouteType.MONORAIL;
17577
17855
  }
17578
17856
  };
17857
+ const serializeTripContinuations = (tripContinuations) => {
17858
+ const result = [];
17859
+ for (const [key, value] of tripContinuations.entries()) {
17860
+ result.push({
17861
+ key: key,
17862
+ value: value.map((tripBoarding) => ({
17863
+ hopOnStop: tripBoarding.hopOnStop,
17864
+ routeId: tripBoarding.routeId,
17865
+ tripIndex: tripBoarding.tripIndex,
17866
+ })),
17867
+ });
17868
+ }
17869
+ return result;
17870
+ };
17871
+ const deserializeTripContinuations = (protoTripContinuations) => {
17872
+ const result = new Map();
17873
+ for (let i = 0; i < protoTripContinuations.length; i++) {
17874
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
17875
+ const entry = protoTripContinuations[i];
17876
+ const tripBoardings = entry.value.map((protoTripBoarding) => ({
17877
+ hopOnStop: protoTripBoarding.hopOnStop,
17878
+ routeId: protoTripBoarding.routeId,
17879
+ tripIndex: protoTripBoarding.tripIndex,
17880
+ }));
17881
+ result.set(entry.key, tripBoardings);
17882
+ }
17883
+ return result;
17884
+ };
17885
+
17886
+ // const ROUTE_ID_BITS = 17;
17887
+ const TRIP_INDEX_BITS = 15;
17888
+ /**
17889
+ * Encodes a route ID and trip index into a single trip ID.
17890
+ * @param routeId - The route identifier, needs to fit on 17 bits
17891
+ * @param tripIndex - The index of the trip within the route, needs to fit on 15 bits
17892
+ * @returns The encoded trip ID
17893
+ */
17894
+ const encode = (routeId, tripIndex) => {
17895
+ return (routeId << TRIP_INDEX_BITS) | tripIndex;
17896
+ };
17579
17897
 
17898
+ /* eslint-disable @typescript-eslint/no-non-null-assertion */
17580
17899
  const ALL_TRANSPORT_MODES = new Set([
17581
17900
  'TRAM',
17582
17901
  'SUBWAY',
@@ -17589,7 +17908,8 @@ const ALL_TRANSPORT_MODES = new Set([
17589
17908
  'TROLLEYBUS',
17590
17909
  'MONORAIL',
17591
17910
  ]);
17592
- const CURRENT_VERSION = '0.0.7';
17911
+ const EMPTY_TRIP_CONTINUATIONS = [];
17912
+ const CURRENT_VERSION = '0.0.8';
17593
17913
  /**
17594
17914
  * The internal transit timetable format.
17595
17915
  */
@@ -17600,9 +17920,10 @@ class Timetable {
17600
17920
  this.serviceRoutes = routes;
17601
17921
  this.activeStops = new Set();
17602
17922
  for (let i = 0; i < stopsAdjacency.length; i++) {
17603
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
17604
17923
  const stop = stopsAdjacency[i];
17605
- if (stop.routes.length > 0 || stop.transfers.length > 0) {
17924
+ if (stop.routes.length > 0 ||
17925
+ (stop.transfers && stop.transfers.length > 0) ||
17926
+ (stop.tripContinuations && stop.tripContinuations.size > 0)) {
17606
17927
  this.activeStops.add(i);
17607
17928
  }
17608
17929
  }
@@ -17665,8 +17986,27 @@ class Timetable {
17665
17986
  * @returns An array of transfer options available at the stop.
17666
17987
  */
17667
17988
  getTransfers(stopId) {
17668
- var _a, _b;
17669
- return (_b = (_a = this.stopsAdjacency[stopId]) === null || _a === void 0 ? void 0 : _a.transfers) !== null && _b !== void 0 ? _b : [];
17989
+ const stopAdjacency = this.stopsAdjacency[stopId];
17990
+ if (!stopAdjacency) {
17991
+ throw new Error(`Stop ID ${stopId} not found`);
17992
+ }
17993
+ return stopAdjacency.transfers || [];
17994
+ }
17995
+ /**
17996
+ * Retrieves all trip continuation options available at the specified stop for a given trip.
17997
+ *
17998
+ * @param stopId - The ID of the stop to get trip continuations for.
17999
+ * @param tripIndex - The index of the trip to get continuations for.
18000
+ * @returns An array of trip continuation options available at the stop for the specified trip.
18001
+ */
18002
+ getContinuousTrips(stopId, routeId, tripIndex) {
18003
+ var _a;
18004
+ const stopAdjacency = this.stopsAdjacency[stopId];
18005
+ if (!stopAdjacency) {
18006
+ throw new Error(`Stop ID ${stopId} not found`);
18007
+ }
18008
+ return (((_a = stopAdjacency.tripContinuations) === null || _a === void 0 ? void 0 : _a.get(encode(routeId, tripIndex))) ||
18009
+ EMPTY_TRIP_CONTINUATIONS);
17670
18010
  }
17671
18011
  /**
17672
18012
  * Retrieves the service route associated with the given route.
@@ -17681,9 +18021,7 @@ class Timetable {
17681
18021
  if (!serviceRoute) {
17682
18022
  throw new Error(`Service route not found for route ID: ${route.serviceRoute()}`);
17683
18023
  }
17684
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
17685
- const { routes } = serviceRoute, serviceRouteInfo = __rest(serviceRoute, ["routes"]);
17686
- return serviceRouteInfo;
18024
+ return { type: serviceRoute.type, name: serviceRoute.name };
17687
18025
  }
17688
18026
  /**
17689
18027
  * Finds all routes passing through a stop.
@@ -19954,6 +20292,7 @@ const parseGtfsLocationType = (gtfsLocationType) => {
19954
20292
  const parseTransfers = (transfersStream, stopsMap) => __awaiter(void 0, void 0, void 0, function* () {
19955
20293
  var _a, e_1, _b, _c;
19956
20294
  const transfers = new Map();
20295
+ const tripContinuations = new Map();
19957
20296
  try {
19958
20297
  for (var _d = true, _e = __asyncValues(parseCsv(transfersStream, [
19959
20298
  'transfer_type',
@@ -19967,25 +20306,48 @@ const parseTransfers = (transfersStream, stopsMap) => __awaiter(void 0, void 0,
19967
20306
  transferEntry.transfer_type === 5) {
19968
20307
  continue;
19969
20308
  }
19970
- if (transferEntry.from_trip_id && transferEntry.to_trip_id) {
19971
- console.warn(`Unsupported transfer between trips ${transferEntry.from_trip_id} and ${transferEntry.to_trip_id}.`);
19972
- continue;
19973
- }
19974
- if (transferEntry.from_route_id && transferEntry.to_route_id) {
19975
- console.warn(`Unsupported transfer between routes ${transferEntry.from_route_id} and ${transferEntry.to_route_id}.`);
19976
- continue;
19977
- }
19978
20309
  if (!transferEntry.from_stop_id || !transferEntry.to_stop_id) {
19979
20310
  console.warn(`Missing transfer origin or destination stop.`);
19980
20311
  continue;
19981
20312
  }
19982
- if (transferEntry.transfer_type === 2 && !transferEntry.min_transfer_time) {
19983
- console.info(`Missing minimum transfer time between ${transferEntry.from_stop_id} and ${transferEntry.to_stop_id}.`);
19984
- }
19985
20313
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
19986
20314
  const fromStop = stopsMap.get(transferEntry.from_stop_id);
19987
20315
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
19988
20316
  const toStop = stopsMap.get(transferEntry.to_stop_id);
20317
+ if (transferEntry.transfer_type === 4) {
20318
+ if (transferEntry.from_trip_id === undefined ||
20319
+ transferEntry.from_trip_id === '' ||
20320
+ transferEntry.to_trip_id === undefined ||
20321
+ transferEntry.to_trip_id === '') {
20322
+ console.warn(`Unsupported in-seat transfer, missing from_trip_id and/or to_trip_id.`);
20323
+ continue;
20324
+ }
20325
+ const tripBoardingEntry = {
20326
+ fromTrip: transferEntry.from_trip_id,
20327
+ toTrip: transferEntry.to_trip_id,
20328
+ hopOnStop: toStop.id,
20329
+ };
20330
+ const existingBoardings = tripContinuations.get(fromStop.id);
20331
+ if (existingBoardings) {
20332
+ existingBoardings.push(tripBoardingEntry);
20333
+ }
20334
+ else {
20335
+ tripContinuations.set(fromStop.id, [tripBoardingEntry]);
20336
+ }
20337
+ continue;
20338
+ }
20339
+ if (transferEntry.from_trip_id && transferEntry.to_trip_id) {
20340
+ console.warn(`Unsupported transfer of type ${transferEntry.transfer_type} between trips ${transferEntry.from_trip_id} and ${transferEntry.to_trip_id}.`);
20341
+ continue;
20342
+ }
20343
+ if (transferEntry.from_route_id && transferEntry.to_route_id) {
20344
+ console.warn(`Unsupported transfer of type ${transferEntry.transfer_type} between routes ${transferEntry.from_route_id} and ${transferEntry.to_route_id}.`);
20345
+ continue;
20346
+ }
20347
+ if (transferEntry.transfer_type === 2 &&
20348
+ transferEntry.min_transfer_time === undefined) {
20349
+ console.info(`Missing minimum transfer time between ${transferEntry.from_stop_id} and ${transferEntry.to_stop_id}.`);
20350
+ }
19989
20351
  const transfer = Object.assign({ destination: toStop.id, type: parseGtfsTransferType(transferEntry.transfer_type) }, (transferEntry.min_transfer_time && {
19990
20352
  minTransferTime: Duration.fromSeconds(transferEntry.min_transfer_time),
19991
20353
  }));
@@ -20001,7 +20363,10 @@ const parseTransfers = (transfersStream, stopsMap) => __awaiter(void 0, void 0,
20001
20363
  }
20002
20364
  finally { if (e_1) throw e_1.error; }
20003
20365
  }
20004
- return transfers;
20366
+ return {
20367
+ transfers,
20368
+ tripContinuations,
20369
+ };
20005
20370
  });
20006
20371
  const parseGtfsTransferType = (gtfsTransferType) => {
20007
20372
  switch (gtfsTransferType) {
@@ -20064,11 +20429,13 @@ const finalizeRouteFromBuilder = (builder) => {
20064
20429
  const stopTimesArray = new Uint16Array(stopsCount * tripsCount * 2);
20065
20430
  const allPickUpTypes = [];
20066
20431
  const allDropOffTypes = [];
20432
+ const gtfsTripIds = [];
20067
20433
  for (let tripIndex = 0; tripIndex < tripsCount; tripIndex++) {
20068
20434
  const trip = builder.trips[tripIndex];
20069
20435
  if (!trip) {
20070
20436
  throw new Error(`Missing trip data at index ${tripIndex}`);
20071
20437
  }
20438
+ gtfsTripIds.push(trip.gtfsTripId);
20072
20439
  const baseIndex = tripIndex * stopsCount * 2;
20073
20440
  for (let stopIndex = 0; stopIndex < stopsCount; stopIndex++) {
20074
20441
  const timeIndex = baseIndex + stopIndex * 2;
@@ -20090,12 +20457,15 @@ const finalizeRouteFromBuilder = (builder) => {
20090
20457
  }
20091
20458
  // Use 2-bit encoding for pickup/drop-off types
20092
20459
  const pickUpDropOffTypesArray = encodePickUpDropOffTypes(allPickUpTypes, allDropOffTypes);
20093
- return {
20094
- serviceRouteId: builder.serviceRouteId,
20095
- stops: stopsArray,
20096
- stopTimes: stopTimesArray,
20097
- pickUpDropOffTypes: pickUpDropOffTypesArray,
20098
- };
20460
+ return [
20461
+ {
20462
+ serviceRouteId: builder.serviceRouteId,
20463
+ stops: stopsArray,
20464
+ stopTimes: stopTimesArray,
20465
+ pickUpDropOffTypes: pickUpDropOffTypesArray,
20466
+ },
20467
+ gtfsTripIds,
20468
+ ];
20099
20469
  };
20100
20470
  /**
20101
20471
  * Parses the trips.txt file from a GTFS feed
@@ -20134,11 +20504,12 @@ const parseTrips = (tripsStream, serviceIds, validGtfsRoutes) => __awaiter(void
20134
20504
  }
20135
20505
  return trips;
20136
20506
  });
20137
- const buildStopsAdjacencyStructure = (serviceRoutes, routes, transfersMap, nbStops, activeStops) => {
20138
- // TODO somehow works when it's a map
20507
+ const buildStopsAdjacencyStructure = (tripsMapping, serviceRoutes, routes, transfersMap, tripContinuationsMap, nbStops, activeStops) => {
20139
20508
  const stopsAdjacency = new Array(nbStops);
20140
20509
  for (let i = 0; i < nbStops; i++) {
20141
- stopsAdjacency[i] = { routes: [], transfers: [] };
20510
+ stopsAdjacency[i] = {
20511
+ routes: [],
20512
+ };
20142
20513
  }
20143
20514
  for (let index = 0; index < routes.length; index++) {
20144
20515
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -20163,12 +20534,50 @@ const buildStopsAdjacencyStructure = (serviceRoutes, routes, transfersMap, nbSto
20163
20534
  const transfer = transfers[i];
20164
20535
  if (activeStops.has(stop) || activeStops.has(transfer.destination)) {
20165
20536
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
20166
- stopsAdjacency[stop].transfers.push(transfer);
20537
+ const stopAdj = stopsAdjacency[stop];
20538
+ if (!stopAdj.transfers) {
20539
+ stopAdj.transfers = [];
20540
+ }
20541
+ stopAdj.transfers.push(transfer);
20167
20542
  activeStops.add(transfer.destination);
20168
20543
  activeStops.add(stop);
20169
20544
  }
20170
20545
  }
20171
20546
  }
20547
+ for (const [stop, tripContinuations] of tripContinuationsMap) {
20548
+ for (let i = 0; i < tripContinuations.length; i++) {
20549
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
20550
+ const tripContinuation = tripContinuations[i];
20551
+ if (activeStops.has(stop) ||
20552
+ activeStops.has(tripContinuation.hopOnStop)) {
20553
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
20554
+ const stopAdj = stopsAdjacency[stop];
20555
+ if (!stopAdj.tripContinuations) {
20556
+ stopAdj.tripContinuations = new Map();
20557
+ }
20558
+ const originTrip = tripsMapping.get(tripContinuation.fromTrip);
20559
+ const destinationTrip = tripsMapping.get(tripContinuation.toTrip);
20560
+ if (destinationTrip === undefined || originTrip === undefined) {
20561
+ continue;
20562
+ }
20563
+ const tripBoarding = {
20564
+ hopOnStop: tripContinuation.hopOnStop,
20565
+ routeId: destinationTrip.routeId,
20566
+ tripIndex: destinationTrip.tripRouteIndex,
20567
+ };
20568
+ const tripId = encode(originTrip.routeId, originTrip.tripRouteIndex);
20569
+ const existingContinuations = stopAdj.tripContinuations.get(tripId);
20570
+ if (existingContinuations) {
20571
+ existingContinuations.push(tripBoarding);
20572
+ }
20573
+ else {
20574
+ stopAdj.tripContinuations.set(tripId, [tripBoarding]);
20575
+ }
20576
+ activeStops.add(tripContinuation.hopOnStop);
20577
+ activeStops.add(stop);
20578
+ }
20579
+ }
20580
+ }
20172
20581
  return stopsAdjacency;
20173
20582
  };
20174
20583
  /**
@@ -20227,6 +20636,7 @@ const parseStopTimes = (stopTimesStream, stopsMap, activeTripIds, activeStopIds)
20227
20636
  }
20228
20637
  routeBuilder.trips.push({
20229
20638
  firstDeparture,
20639
+ gtfsTripId: currentTripId,
20230
20640
  arrivalTimes: arrivalTimes,
20231
20641
  departureTimes: departureTimes,
20232
20642
  pickUpTypes: pickUpTypes,
@@ -20264,6 +20674,9 @@ const parseStopTimes = (stopTimesStream, stopsMap, activeTripIds, activeStopIds)
20264
20674
  continue;
20265
20675
  }
20266
20676
  if (line.pickup_type === '1' && line.drop_off_type === '1') {
20677
+ // Warning: could potentially lead to issues if there is an in-seat transfer
20678
+ // at this stop - it can be not boardable nor alightable but still useful for an in-seat transfer.
20679
+ // This doesn't seem to happen in practice for now so keeping this condition to save memory.
20267
20680
  continue;
20268
20681
  }
20269
20682
  if (currentTripId && line.trip_id !== currentTripId && stops.length > 0) {
@@ -20300,11 +20713,19 @@ const parseStopTimes = (stopTimesStream, stopsMap, activeTripIds, activeStopIds)
20300
20713
  addTrip(currentTripId);
20301
20714
  }
20302
20715
  const routesAdjacency = [];
20716
+ const tripsMapping = new Map();
20303
20717
  for (const [, routeBuilder] of routeBuilders) {
20304
- const routeData = finalizeRouteFromBuilder(routeBuilder);
20305
- routesAdjacency.push(new Route$1(routeData.stopTimes, routeData.pickUpDropOffTypes, routeData.stops, routeData.serviceRouteId));
20718
+ const [routeData, gtfsTripIds] = finalizeRouteFromBuilder(routeBuilder);
20719
+ const routeId = routesAdjacency.length;
20720
+ routesAdjacency.push(new Route$1(routeId, routeData.stopTimes, routeData.pickUpDropOffTypes, routeData.stops, routeData.serviceRouteId));
20721
+ gtfsTripIds.forEach((tripId, index) => {
20722
+ tripsMapping.set(tripId, {
20723
+ routeId,
20724
+ tripRouteIndex: index,
20725
+ });
20726
+ });
20306
20727
  }
20307
- return { routes: routesAdjacency, serviceRoutesMap };
20728
+ return { routes: routesAdjacency, serviceRoutesMap, tripsMapping };
20308
20729
  });
20309
20730
  const parsePickupDropOffType = (gtfsType) => {
20310
20731
  switch (gtfsType) {
@@ -20383,24 +20804,27 @@ class GtfsParser {
20383
20804
  const tripsEnd = performance.now();
20384
20805
  log.info(`${trips.size} valid trips. (${(tripsEnd - tripsStart).toFixed(2)}ms)`);
20385
20806
  let transfers = new Map();
20807
+ let tripContinuations = new Map();
20386
20808
  if (entries[TRANSFERS_FILE]) {
20387
20809
  log.info(`Parsing ${TRANSFERS_FILE}`);
20388
20810
  const transfersStart = performance.now();
20389
20811
  const transfersStream = yield zip.stream(TRANSFERS_FILE);
20390
- transfers = yield parseTransfers(transfersStream, parsedStops);
20812
+ const { transfers: parsedTransfers, tripContinuations: parsedTripContinuations, } = yield parseTransfers(transfersStream, parsedStops);
20813
+ transfers = parsedTransfers;
20814
+ tripContinuations = parsedTripContinuations;
20391
20815
  const transfersEnd = performance.now();
20392
- log.info(`${transfers.size} valid transfers. (${(transfersEnd - transfersStart).toFixed(2)}ms)`);
20816
+ log.info(`${transfers.size} valid transfers and ${tripContinuations.size} trip continuations. (${(transfersEnd - transfersStart).toFixed(2)}ms)`);
20393
20817
  }
20394
20818
  log.info(`Parsing ${STOP_TIMES_FILE}`);
20395
20819
  const stopTimesStart = performance.now();
20396
20820
  const stopTimesStream = yield zip.stream(STOP_TIMES_FILE);
20397
- const { routes, serviceRoutesMap } = yield parseStopTimes(stopTimesStream, parsedStops, trips, activeStopIds);
20821
+ const { routes, serviceRoutesMap, tripsMapping } = yield parseStopTimes(stopTimesStream, parsedStops, trips, activeStopIds);
20398
20822
  const serviceRoutes = indexRoutes(validGtfsRoutes, serviceRoutesMap);
20399
20823
  const stopTimesEnd = performance.now();
20400
20824
  log.info(`${routes.length} valid unique routes. (${(stopTimesEnd - stopTimesStart).toFixed(2)}ms)`);
20401
20825
  log.info('Building stops adjacency structure');
20402
20826
  const stopsAdjacencyStart = performance.now();
20403
- const stopsAdjacency = buildStopsAdjacencyStructure(serviceRoutes, routes, transfers, parsedStops.size, activeStopIds);
20827
+ const stopsAdjacency = buildStopsAdjacencyStructure(tripsMapping, serviceRoutes, routes, transfers, tripContinuations, parsedStops.size, activeStopIds);
20404
20828
  const stopsAdjacencyEnd = performance.now();
20405
20829
  log.info(`${stopsAdjacency.length} valid stops in the structure. (${(stopsAdjacencyEnd - stopsAdjacencyStart).toFixed(2)}ms)`);
20406
20830
  yield zip.close();
@@ -20481,53 +20905,242 @@ const chGtfsProfile = {
20481
20905
 
20482
20906
  class Plotter {
20483
20907
  constructor(result) {
20908
+ this.ROUND_COLORS = [
20909
+ '#60a5fa', // Round 1
20910
+ '#ff9800', // Round 2
20911
+ '#14b8a6', // Round 3
20912
+ '#fb7185', // Round 4
20913
+ '#ffdf00', // Round 5
20914
+ '#b600ff', // Round 6
20915
+ '#ee82ee', // Round 7+
20916
+ ];
20484
20917
  this.result = result;
20485
20918
  }
20486
20919
  /**
20487
- * Plots the path three as a DOT for debugging purposes.
20488
- *
20489
- * @returns A string representing the DOT graph of the path tree.
20920
+ * Gets the color for a round based on the specified palette.
20490
20921
  */
20491
- plotDotGraph() {
20492
- const earliestArrivalsPerRound = this.result.earliestArrivalsPerRound;
20493
- const dotParts = [
20494
- 'digraph PathTree {',
20495
- ' graph [overlap=false];',
20496
- ' node [shape=ellipse style=filled fillcolor=lightgrey];',
20922
+ getRoundColor(round) {
20923
+ var _a;
20924
+ if (round === 0)
20925
+ return '#888888';
20926
+ const colorIndex = Math.min(round - 1, this.ROUND_COLORS.length - 1);
20927
+ return (_a = this.ROUND_COLORS[colorIndex]) !== null && _a !== void 0 ? _a : '#ee82ee';
20928
+ }
20929
+ /**
20930
+ * Escapes special characters in DOT strings to prevent syntax errors.
20931
+ */
20932
+ escapeDotString(str) {
20933
+ return str
20934
+ .replace(/\\/g, '\\\\')
20935
+ .replace(/"/g, '\\"')
20936
+ .replace(/\n/g, '\\n')
20937
+ .replace(/\r/g, '\\r')
20938
+ .replace(/\t/g, '\\t');
20939
+ }
20940
+ /**
20941
+ * Determines station type (origin/destination) information.
20942
+ */
20943
+ getStationInfo(stopId) {
20944
+ var _a, _b;
20945
+ const isOrigin = (_b = (_a = this.result.routingState.graph[0]) === null || _a === void 0 ? void 0 : _a.has(stopId)) !== null && _b !== void 0 ? _b : false;
20946
+ const isDestination = this.result.routingState.destinations.includes(stopId);
20947
+ return { isOrigin, isDestination };
20948
+ }
20949
+ /**
20950
+ * Formats a stop name for display, including platform information.
20951
+ */
20952
+ formatStopName(stopId) {
20953
+ const stop = this.result.stopsIndex.findStopById(stopId);
20954
+ if (!stop)
20955
+ return `Unknown Stop (${stopId})`;
20956
+ const escapedName = this.escapeDotString(stop.name);
20957
+ const escapedPlatform = stop.platform
20958
+ ? this.escapeDotString(stop.platform)
20959
+ : '';
20960
+ return escapedPlatform
20961
+ ? `${escapedName}\\nPl. ${escapedPlatform}`
20962
+ : escapedName;
20963
+ }
20964
+ /**
20965
+ * Gets the appropriate fill color for a station based on its type.
20966
+ */
20967
+ getStationFillColor(isOrigin, isDestination) {
20968
+ if (isOrigin)
20969
+ return '#60a5fa';
20970
+ if (isDestination)
20971
+ return '#ee82ee';
20972
+ return 'white';
20973
+ }
20974
+ /**
20975
+ * Creates a DOT node for a station.
20976
+ */
20977
+ createStationNode(stopId) {
20978
+ const stop = this.result.stopsIndex.findStopById(stopId);
20979
+ if (!stop)
20980
+ return '';
20981
+ const displayName = this.formatStopName(stopId);
20982
+ const stopIdStr = this.escapeDotString(String(stopId));
20983
+ const nodeId = `s_${stopId}`;
20984
+ const stationInfo = this.getStationInfo(stopId);
20985
+ const fillColor = this.getStationFillColor(stationInfo.isOrigin, stationInfo.isDestination);
20986
+ return ` "${nodeId}" [label="${displayName}\\n${stopIdStr}" shape=box style=filled fillcolor="${fillColor}"];`;
20987
+ }
20988
+ /**
20989
+ * Creates a vehicle edge with route information oval in the middle.
20990
+ */
20991
+ createVehicleEdge(edge, round) {
20992
+ var _a;
20993
+ const fromNodeId = `s_${edge.from}`;
20994
+ const toNodeId = `s_${edge.to}`;
20995
+ const roundColor = this.getRoundColor(round);
20996
+ const routeOvalId = `e_${edge.from}_${edge.to}_${edge.routeId}_${round}`;
20997
+ const route = this.result.timetable.getRoute(edge.routeId);
20998
+ const serviceRouteInfo = route
20999
+ ? this.result.timetable.getServiceRouteInfo(route)
21000
+ : null;
21001
+ const routeName = (_a = serviceRouteInfo === null || serviceRouteInfo === void 0 ? void 0 : serviceRouteInfo.name) !== null && _a !== void 0 ? _a : `Route ${String(edge.routeId)}`;
21002
+ const routeType = (serviceRouteInfo === null || serviceRouteInfo === void 0 ? void 0 : serviceRouteInfo.type) || 'UNKNOWN';
21003
+ const departureTime = route
21004
+ ? route.departureFrom(edge.from, edge.tripIndex).toString()
21005
+ : 'N/A';
21006
+ const arrivalTime = edge.arrival.toString();
21007
+ const escapedRouteName = this.escapeDotString(routeName);
21008
+ const escapedRouteType = this.escapeDotString(routeType);
21009
+ const routeInfo = `${edge.routeId}:${edge.tripIndex}`;
21010
+ const ovalLabel = `${escapedRouteType} ${escapedRouteName}\\n${routeInfo}\\n${departureTime} → ${arrivalTime}`;
21011
+ return [
21012
+ ` "${routeOvalId}" [label="${ovalLabel}" shape=oval style=filled fillcolor="white" color="${roundColor}"];`,
21013
+ ` "${fromNodeId}" -> "${routeOvalId}" [color="${roundColor}"];`,
21014
+ ` "${routeOvalId}" -> "${toNodeId}" [color="${roundColor}"];`,
20497
21015
  ];
20498
- earliestArrivalsPerRound.forEach((arrivalsInRound, round) => {
20499
- arrivalsInRound.forEach((tripLeg) => {
20500
- const { origin, leg } = tripLeg;
20501
- if (!leg)
20502
- return; // Skip if leg is undefined
20503
- const fromStop = this.result['stopsIndex'].findStopById(leg.from.id);
20504
- const toStop = this.result['stopsIndex'].findStopById(leg.to.id);
20505
- const originStop = this.result['stopsIndex'].findStopById(origin);
20506
- if (fromStop && toStop && originStop) {
20507
- const fromName = fromStop.platform
20508
- ? `${fromStop.name} (Pl. ${fromStop.platform})`
20509
- : fromStop.name;
20510
- const toName = toStop.platform
20511
- ? `${toStop.name} (Pl. ${toStop.platform})`
20512
- : toStop.name;
20513
- const originName = originStop.platform
20514
- ? `${originStop.name} (Pl. ${originStop.platform})`
20515
- : originStop.name;
20516
- const isVehicle = 'route' in leg;
20517
- const routeLabelContent = isVehicle
20518
- ? `${leg.route.name}\n${leg.departureTime.toString()} - ${leg.arrivalTime.toString()}`
20519
- : leg.minTransferTime
20520
- ? leg.minTransferTime.toString()
20521
- : '';
20522
- const intermediateNode = `IntermediateNode${fromStop.id}_${toStop.id}`;
20523
- const lineColor = isVehicle ? '' : ', color="red", fontcolor="red"';
20524
- const labelColor = isVehicle ? '' : ' fontcolor="red"';
20525
- dotParts.push(` "${fromName} (Origin: ${originName}) [R${round}]\n(${fromStop.id})" -> "${intermediateNode}" [shape=point${lineColor}];`);
20526
- dotParts.push(` "${intermediateNode}" [label="${routeLabelContent}" shape=rect style=filled fillcolor=white${labelColor} border=0];`);
20527
- dotParts.push(` "${intermediateNode}" -> "${toName} (Origin: ${originName}) [R${round}]\n(${toStop.id})" [${lineColor.replace(', ', '')}];`);
21016
+ }
21017
+ /**
21018
+ * Creates a transfer edge with transfer information oval in the middle.
21019
+ */
21020
+ createTransferEdge(edge, round) {
21021
+ var _a;
21022
+ const fromNodeId = `s_${edge.from}`;
21023
+ const toNodeId = `s_${edge.to}`;
21024
+ const roundColor = this.getRoundColor(round);
21025
+ const transferOvalId = `e_${edge.from}_${edge.to}_${round}`;
21026
+ const transferTime = ((_a = edge.minTransferTime) === null || _a === void 0 ? void 0 : _a.toString()) || 'N/A';
21027
+ const escapedTransferTime = this.escapeDotString(transferTime);
21028
+ const ovalLabel = `Transfer\\n${escapedTransferTime}`;
21029
+ return [
21030
+ ` "${transferOvalId}" [label="${ovalLabel}" shape=oval style="dashed,filled" fillcolor="white" color="${roundColor}"];`,
21031
+ ` "${fromNodeId}" -> "${transferOvalId}" [color="${roundColor}" style="dashed"];`,
21032
+ ` "${transferOvalId}" -> "${toNodeId}" [color="${roundColor}" style="dashed"];`,
21033
+ ];
21034
+ }
21035
+ /**
21036
+ * Creates a continuation edge to visually link trip continuations.
21037
+ */
21038
+ createContinuationEdge(fromEdge, toEdge, round) {
21039
+ var _a, _b;
21040
+ const fromStationId = `s_${fromEdge.to}`;
21041
+ const toStationId = `s_${toEdge.from}`;
21042
+ const roundColor = this.getRoundColor(round);
21043
+ const continuationOvalId = `continuation_${fromEdge.to}_${toEdge.from}_${round}`;
21044
+ const fromRoute = this.result.timetable.getRoute(fromEdge.routeId);
21045
+ const toRoute = this.result.timetable.getRoute(toEdge.routeId);
21046
+ const fromServiceRouteInfo = fromRoute
21047
+ ? this.result.timetable.getServiceRouteInfo(fromRoute)
21048
+ : null;
21049
+ const toServiceRouteInfo = toRoute
21050
+ ? this.result.timetable.getServiceRouteInfo(toRoute)
21051
+ : null;
21052
+ const fromRouteName = (_a = fromServiceRouteInfo === null || fromServiceRouteInfo === void 0 ? void 0 : fromServiceRouteInfo.name) !== null && _a !== void 0 ? _a : `Route ${String(fromEdge.routeId)}`;
21053
+ const toRouteName = (_b = toServiceRouteInfo === null || toServiceRouteInfo === void 0 ? void 0 : toServiceRouteInfo.name) !== null && _b !== void 0 ? _b : `Route ${String(toEdge.routeId)}`;
21054
+ const fromRouteType = (fromServiceRouteInfo === null || fromServiceRouteInfo === void 0 ? void 0 : fromServiceRouteInfo.type) || 'UNKNOWN';
21055
+ const toRouteType = (toServiceRouteInfo === null || toServiceRouteInfo === void 0 ? void 0 : toServiceRouteInfo.type) || 'UNKNOWN';
21056
+ const fromArrivalTime = fromEdge.arrival.toString();
21057
+ const toDepartureTime = toRoute
21058
+ ? toRoute.departureFrom(toEdge.from, toEdge.tripIndex).toString()
21059
+ : 'N/A';
21060
+ const escapedFromRouteName = this.escapeDotString(fromRouteName);
21061
+ const escapedToRouteName = this.escapeDotString(toRouteName);
21062
+ const escapedFromRouteType = this.escapeDotString(fromRouteType);
21063
+ const escapedToRouteType = this.escapeDotString(toRouteType);
21064
+ const fromRouteInfo = `${fromEdge.routeId}:${fromEdge.tripIndex}`;
21065
+ const toRouteInfo = `${toEdge.routeId}:${toEdge.tripIndex}`;
21066
+ const ovalLabel = `${escapedFromRouteType} ${escapedFromRouteName} (${fromRouteInfo}) ${fromArrivalTime}\\n↓\\n${escapedToRouteType} ${escapedToRouteName} (${toRouteInfo}) ${toDepartureTime}`;
21067
+ return [
21068
+ ` "${continuationOvalId}" [label="${ovalLabel}" shape=oval style="filled,bold" fillcolor="#ffffcc" color="${roundColor}" penwidth="2"];`,
21069
+ ` "${fromStationId}" -> "${continuationOvalId}" [color="${roundColor}" style="bold" penwidth="3"];`,
21070
+ ` "${continuationOvalId}" -> "${toStationId}" [color="${roundColor}" style="bold" penwidth="3"];`,
21071
+ ];
21072
+ }
21073
+ /**
21074
+ * Collects all stations and edges for the graph.
21075
+ */
21076
+ collectGraphData() {
21077
+ const stations = new Set();
21078
+ const edges = [];
21079
+ const continuationEdges = [];
21080
+ const graph = this.result.routingState.graph;
21081
+ // Collect all stops that appear in the graph
21082
+ graph.forEach((roundMap) => {
21083
+ roundMap.forEach((edge, stopId) => {
21084
+ stations.add(stopId);
21085
+ if ('from' in edge && 'to' in edge) {
21086
+ stations.add(edge.from);
21087
+ stations.add(edge.to);
21088
+ }
21089
+ });
21090
+ });
21091
+ // Create edges for each round
21092
+ graph.forEach((roundMap, round) => {
21093
+ if (round === 0) {
21094
+ // Skip round 0 as it contains only origin nodes
21095
+ return;
21096
+ }
21097
+ roundMap.forEach((edge) => {
21098
+ if ('from' in edge && 'to' in edge) {
21099
+ if ('routeId' in edge) {
21100
+ const vehicleEdgeParts = this.createVehicleEdge(edge, round);
21101
+ edges.push(...vehicleEdgeParts);
21102
+ if (edge.continuationOf) {
21103
+ let currentEdge = edge;
21104
+ let previousEdge = edge.continuationOf;
21105
+ while (previousEdge) {
21106
+ const continuationEdgeParts = this.createContinuationEdge(previousEdge, currentEdge, round);
21107
+ continuationEdges.push(...continuationEdgeParts);
21108
+ currentEdge = previousEdge;
21109
+ previousEdge = previousEdge.continuationOf;
21110
+ }
21111
+ }
21112
+ }
21113
+ else {
21114
+ const transferEdgeParts = this.createTransferEdge(edge, round);
21115
+ edges.push(...transferEdgeParts);
21116
+ }
20528
21117
  }
20529
21118
  });
20530
21119
  });
21120
+ edges.push(...continuationEdges);
21121
+ return { stations, edges };
21122
+ }
21123
+ /**
21124
+ * Plots the routing graph as a DOT graph for visualization.
21125
+ */
21126
+ plotDotGraph() {
21127
+ const { stations, edges } = this.collectGraphData();
21128
+ const dotParts = [
21129
+ 'digraph RoutingGraph {',
21130
+ ' graph [overlap=false, splines=true, rankdir=TB, bgcolor=white, nodesep=0.8, ranksep=1.2, concentrate=true];',
21131
+ ' node [fontname="Arial" margin=0.1];',
21132
+ ' edge [fontname="Arial" fontsize=10];',
21133
+ '',
21134
+ ' // Stations',
21135
+ ];
21136
+ stations.forEach((stopId) => {
21137
+ const stationNode = this.createStationNode(stopId);
21138
+ if (stationNode) {
21139
+ dotParts.push(stationNode);
21140
+ }
21141
+ });
21142
+ dotParts.push('', ' // Edges');
21143
+ dotParts.push(...edges);
20531
21144
  dotParts.push('}');
20532
21145
  return dotParts.join('\n');
20533
21146
  }
@@ -20726,11 +21339,11 @@ class Route {
20726
21339
  }
20727
21340
 
20728
21341
  class Result {
20729
- constructor(query, earliestArrivals, earliestArrivalsPerRound, stopsIndex) {
21342
+ constructor(query, routingState, stopsIndex, timetable) {
20730
21343
  this.query = query;
20731
- this.earliestArrivals = earliestArrivals;
20732
- this.earliestArrivalsPerRound = earliestArrivalsPerRound;
21344
+ this.routingState = routingState;
20733
21345
  this.stopsIndex = stopsIndex;
21346
+ this.timetable = timetable;
20734
21347
  }
20735
21348
  /**
20736
21349
  * Reconstructs the best route to a stop.
@@ -20740,17 +21353,18 @@ class Result {
20740
21353
  * @returns a route to the destination stop if it exists.
20741
21354
  */
20742
21355
  bestRoute(to) {
20743
- var _a, _b, _c;
21356
+ var _a;
20744
21357
  const destinationList = to instanceof Set
20745
21358
  ? Array.from(to)
20746
21359
  : to
20747
21360
  ? [to]
20748
21361
  : Array.from(this.query.to);
20749
21362
  const destinations = destinationList.flatMap((destination) => this.stopsIndex.equivalentStops(destination));
21363
+ // find the first reached destination
20750
21364
  let fastestDestination = undefined;
20751
21365
  let fastestTime = undefined;
20752
21366
  for (const destination of destinations) {
20753
- const arrivalTime = this.earliestArrivals.get(destination.id);
21367
+ const arrivalTime = this.routingState.earliestArrivals.get(destination.id);
20754
21368
  if (arrivalTime !== undefined) {
20755
21369
  if (fastestTime === undefined ||
20756
21370
  arrivalTime.arrival.isBefore(fastestTime.arrival)) {
@@ -20765,19 +21379,84 @@ class Result {
20765
21379
  const route = [];
20766
21380
  let currentStop = fastestDestination;
20767
21381
  let round = fastestTime.legNumber;
20768
- while (fastestTime.origin !== currentStop) {
20769
- const tripLeg = (_a = this.earliestArrivalsPerRound[round]) === null || _a === void 0 ? void 0 : _a.get(currentStop);
20770
- if (!(tripLeg === null || tripLeg === void 0 ? void 0 : tripLeg.leg)) {
20771
- throw new Error(`No leg found for a trip leg: start stop=${(_c = (_b = tripLeg === null || tripLeg === void 0 ? void 0 : tripLeg.leg) === null || _b === void 0 ? void 0 : _b.from.id) !== null && _c !== void 0 ? _c : 'unknown'}, end stop=${currentStop}, round=${round}, origin=${fastestTime.origin}`);
21382
+ while (round > 0) {
21383
+ const edge = (_a = this.routingState.graph[round]) === null || _a === void 0 ? void 0 : _a.get(currentStop);
21384
+ if (!edge) {
21385
+ throw new Error(`No edge arriving at stop ${currentStop} at round ${round}`);
21386
+ }
21387
+ let leg;
21388
+ if ('routeId' in edge) {
21389
+ let vehicleEdge = edge;
21390
+ // Handle leg reconstruction for in-seat trip continuations
21391
+ const chainedEdges = [vehicleEdge];
21392
+ while ('routeId' in vehicleEdge && vehicleEdge.continuationOf) {
21393
+ chainedEdges.push(vehicleEdge.continuationOf);
21394
+ vehicleEdge = vehicleEdge.continuationOf;
21395
+ }
21396
+ leg = this.buildVehicleLeg(chainedEdges);
21397
+ }
21398
+ else if ('type' in edge) {
21399
+ leg = this.buildTransferLeg(edge);
20772
21400
  }
20773
- route.unshift(tripLeg.leg);
20774
- currentStop = tripLeg.leg.from.id;
20775
- if ('route' in tripLeg.leg) {
21401
+ else {
21402
+ break;
21403
+ }
21404
+ route.unshift(leg);
21405
+ currentStop = leg.from.id;
21406
+ if ('routeId' in edge) {
20776
21407
  round -= 1;
20777
21408
  }
20778
21409
  }
20779
21410
  return new Route(route);
20780
21411
  }
21412
+ /**
21413
+ * Builds a vehicle leg from a chain of vehicle edges.
21414
+ *
21415
+ * @param edges Array of vehicle edges representing continuous trips on transit vehicles
21416
+ * @returns A vehicle leg with departure/arrival information and route details
21417
+ * @throws Error if the edges array is empty
21418
+ */
21419
+ buildVehicleLeg(edges) {
21420
+ if (edges.length === 0) {
21421
+ throw new Error('Cannot build vehicle leg from empty edges');
21422
+ }
21423
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
21424
+ const firstEdge = edges[edges.length - 1];
21425
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
21426
+ const lastEdge = edges[0];
21427
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
21428
+ const firstRoute = this.timetable.getRoute(firstEdge.routeId);
21429
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
21430
+ const lastRoute = this.timetable.getRoute(lastEdge.routeId);
21431
+ return {
21432
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
21433
+ from: this.stopsIndex.findStopById(firstEdge.from),
21434
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
21435
+ to: this.stopsIndex.findStopById(lastEdge.to),
21436
+ // The route info comes from the first boarded route in case on continuous trips
21437
+ route: this.timetable.getServiceRouteInfo(firstRoute),
21438
+ departureTime: firstRoute.departureFrom(firstEdge.from, firstEdge.tripIndex),
21439
+ arrivalTime: lastEdge.arrival,
21440
+ pickUpType: firstRoute.pickUpTypeFrom(firstEdge.from, firstEdge.tripIndex),
21441
+ dropOffType: lastRoute.dropOffTypeAt(lastEdge.to, lastEdge.tripIndex),
21442
+ };
21443
+ }
21444
+ /**
21445
+ * Builds a transfer leg from a transfer edge.
21446
+ *
21447
+ * @param edge Transfer edge representing a walking connection between stops
21448
+ * @returns A transfer leg with from/to stops and transfer details
21449
+ */
21450
+ buildTransferLeg(edge) {
21451
+ return {
21452
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
21453
+ from: this.stopsIndex.findStopById(edge.from),
21454
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
21455
+ to: this.stopsIndex.findStopById(edge.to),
21456
+ minTransferTime: edge.minTransferTime,
21457
+ type: edge.type,
21458
+ };
21459
+ }
20781
21460
  /**
20782
21461
  * Returns the arrival time at any stop reachable in less time / transfers than the destination(s) of the query)
20783
21462
  *
@@ -20786,14 +21465,28 @@ class Result {
20786
21465
  * @returns The arrival time if the target stop is reachable, otherwise undefined.
20787
21466
  */
20788
21467
  arrivalAt(stop, maxTransfers) {
21468
+ var _a;
20789
21469
  const equivalentStops = this.stopsIndex.equivalentStops(stop);
20790
21470
  let earliestArrival = undefined;
20791
- const relevantArrivals = maxTransfers !== undefined
20792
- ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
20793
- this.earliestArrivalsPerRound[maxTransfers + 1]
20794
- : this.earliestArrivals;
20795
21471
  for (const equivalentStop of equivalentStops) {
20796
- const arrivalTime = relevantArrivals.get(equivalentStop.id);
21472
+ let arrivalTime;
21473
+ if (maxTransfers === undefined) {
21474
+ arrivalTime = this.routingState.earliestArrivals.get(equivalentStop.id);
21475
+ }
21476
+ else {
21477
+ // We have no guarantee that the stop was visited in the last round,
21478
+ // so we need to check all rounds if it's not found in the last one.
21479
+ for (let i = maxTransfers + 1; i >= 0; i--) {
21480
+ const arrivalEdge = (_a = this.routingState.graph[i]) === null || _a === void 0 ? void 0 : _a.get(equivalentStop.id);
21481
+ if (arrivalEdge !== undefined) {
21482
+ arrivalTime = {
21483
+ arrival: arrivalEdge.arrival,
21484
+ legNumber: i,
21485
+ };
21486
+ break;
21487
+ }
21488
+ }
21489
+ }
20797
21490
  if (arrivalTime !== undefined) {
20798
21491
  if (earliestArrival === undefined ||
20799
21492
  arrivalTime.arrival.isBefore(earliestArrival.arrival)) {
@@ -20807,9 +21500,9 @@ class Result {
20807
21500
 
20808
21501
  const UNREACHED = Time.infinity();
20809
21502
  /**
20810
- * A public transportation network router implementing the RAPTOR algorithm for
20811
- * efficient journey planning and routing. For more information on the RAPTOR
20812
- * algorithm, refer to its detailed explanation in the research paper:
21503
+ * A public transportation router implementing the RAPTOR algorithm.
21504
+ * For more information on the RAPTOR algorithm,
21505
+ * refer to its detailed explanation in the research paper:
20813
21506
  * https://www.microsoft.com/en-us/research/wp-content/uploads/2012/01/raptor_alenex.pdf
20814
21507
  */
20815
21508
  class Router {
@@ -20818,25 +21511,227 @@ class Router {
20818
21511
  this.stopsIndex = stopsIndex;
20819
21512
  }
20820
21513
  /**
20821
- * Evaluates possible transfers for a given query on a transport
20822
- * network, updating the earliest arrivals at various stops and marking new
20823
- * stops that can be reached through these transfers.
21514
+ * The main Raptor algorithm implementation.
21515
+ *
21516
+ * @param query The query containing the main parameters for the routing.
21517
+ * @returns A result object containing data structures allowing to reconstruct routes and .
21518
+ */
21519
+ route(query) {
21520
+ const routingState = this.initRoutingState(query);
21521
+ const markedStops = new Set();
21522
+ for (const originStop of routingState.graph[0].keys()) {
21523
+ markedStops.add(originStop);
21524
+ }
21525
+ // Initial transfer consideration for origins
21526
+ const newlyMarkedStops = this.considerTransfers(query, 0, markedStops, routingState);
21527
+ for (const newStop of newlyMarkedStops) {
21528
+ markedStops.add(newStop);
21529
+ }
21530
+ for (let round = 1; round <= query.options.maxTransfers + 1; round++) {
21531
+ const edgesAtCurrentRound = new Map();
21532
+ routingState.graph.push(edgesAtCurrentRound);
21533
+ const reachableRoutes = this.timetable.findReachableRoutes(markedStops, query.options.transportModes);
21534
+ markedStops.clear();
21535
+ // for each route that can be reached with at least round - 1 trips
21536
+ for (const [route, hopOnStop] of reachableRoutes) {
21537
+ const newlyMarkedStops = this.scanRoute(route, hopOnStop, round, routingState);
21538
+ for (const newStop of newlyMarkedStops) {
21539
+ markedStops.add(newStop);
21540
+ }
21541
+ }
21542
+ // process in-seat trip continuations
21543
+ let continuations = this.findTripContinuations(markedStops, edgesAtCurrentRound);
21544
+ while (continuations.length > 0) {
21545
+ const stopsFromContinuations = new Set();
21546
+ for (const continuation of continuations) {
21547
+ const route = this.timetable.getRoute(continuation.routeId);
21548
+ const routeScanResults = this.scanRoute(route, continuation.hopOnStop, round, routingState, continuation);
21549
+ for (const newStop of routeScanResults) {
21550
+ stopsFromContinuations.add(newStop);
21551
+ }
21552
+ }
21553
+ for (const newStop of stopsFromContinuations) {
21554
+ markedStops.add(newStop);
21555
+ }
21556
+ continuations = this.findTripContinuations(stopsFromContinuations, edgesAtCurrentRound);
21557
+ }
21558
+ const newlyMarkedStops = this.considerTransfers(query, round, markedStops, routingState);
21559
+ for (const newStop of newlyMarkedStops) {
21560
+ markedStops.add(newStop);
21561
+ }
21562
+ if (markedStops.size === 0)
21563
+ break;
21564
+ }
21565
+ return new Result(query, routingState, this.stopsIndex, this.timetable);
21566
+ }
21567
+ /**
21568
+ * Finds trip continuations for the given marked stops and edges at the current round.
21569
+ * @param markedStops The set of marked stops.
21570
+ * @param edgesAtCurrentRound The map of edges at the current round.
21571
+ * @returns An array of trip continuations.
21572
+ */
21573
+ findTripContinuations(markedStops, edgesAtCurrentRound) {
21574
+ const continuations = [];
21575
+ for (const stopId of markedStops) {
21576
+ const arrival = edgesAtCurrentRound.get(stopId);
21577
+ if (!arrival || !('routeId' in arrival))
21578
+ continue;
21579
+ const continuousTrips = this.timetable.getContinuousTrips(stopId, arrival.routeId, arrival.tripIndex);
21580
+ for (let i = 0; i < continuousTrips.length; i++) {
21581
+ const trip = continuousTrips[i];
21582
+ continuations.push({
21583
+ routeId: trip.routeId,
21584
+ hopOnStop: trip.hopOnStop,
21585
+ tripIndex: trip.tripIndex,
21586
+ previousEdge: arrival,
21587
+ });
21588
+ }
21589
+ }
21590
+ return continuations;
21591
+ }
21592
+ /**
21593
+ * Initializes the routing state for the RAPTOR algorithm.
21594
+ *
21595
+ * This method sets up the initial data structures needed for route planning,
21596
+ * including origin and destination stops (considering equivalent stops),
21597
+ * earliest arrival times, and marked stops for processing.
21598
+ *
21599
+ * @param query The routing query containing origin, destination, and departure time
21600
+ * @returns The initialized routing state with all necessary data structures
21601
+ */
21602
+ initRoutingState(query) {
21603
+ const { from, to, departureTime } = query;
21604
+ // Consider children or siblings of the "from" stop as potential origins
21605
+ const origins = this.stopsIndex
21606
+ .equivalentStops(from)
21607
+ .map((origin) => origin.id);
21608
+ // Consider children or siblings of the "to" stop(s) as potential destinations
21609
+ const destinations = Array.from(to)
21610
+ .flatMap((destination) => this.stopsIndex.equivalentStops(destination))
21611
+ .map((destination) => destination.id);
21612
+ const earliestArrivals = new Map();
21613
+ const earliestArrivalsWithoutAnyLeg = new Map();
21614
+ const earliestArrivalsPerRound = [earliestArrivalsWithoutAnyLeg];
21615
+ const initialState = {
21616
+ arrival: departureTime,
21617
+ legNumber: 0,
21618
+ };
21619
+ for (const originStop of origins) {
21620
+ earliestArrivals.set(originStop, initialState);
21621
+ earliestArrivalsWithoutAnyLeg.set(originStop, initialState);
21622
+ }
21623
+ return {
21624
+ destinations,
21625
+ earliestArrivals,
21626
+ graph: earliestArrivalsPerRound,
21627
+ };
21628
+ }
21629
+ /**
21630
+ * Scans a route to find the earliest possible trips (if not provided) and updates arrival times.
21631
+ *
21632
+ * This method implements the core route scanning logic of the RAPTOR algorithm.
21633
+ * It iterates through all stops on a given route starting from the hop-on stop,
21634
+ * maintaining the current best trip and updating arrival times when improvements
21635
+ * are found. The method also handles boarding new trips when earlier departures
21636
+ * are available if no given trip is provided as a parameter.
21637
+ *
21638
+ * @param route The route to scan for possible trips
21639
+ * @param hopOnStop The stop ID where passengers can board the route
21640
+ * @param round The current round number in the RAPTOR algorithm
21641
+ * @param routingState The current routing state containing arrival times and marked stops
21642
+ */
21643
+ scanRoute(route, hopOnStop, round, routingState, tripContinuation) {
21644
+ var _a, _b, _c;
21645
+ const newlyMarkedStops = new Set();
21646
+ let activeTrip = tripContinuation
21647
+ ? {
21648
+ routeId: route.id,
21649
+ hopOnStop,
21650
+ tripIndex: tripContinuation.tripIndex,
21651
+ }
21652
+ : undefined;
21653
+ const edgesAtCurrentRound = routingState.graph[round];
21654
+ const edgesAtPreviousRound = routingState.graph[round - 1];
21655
+ const startIndex = route.stopRouteIndex(hopOnStop);
21656
+ // Compute target pruning criteria only once per route
21657
+ const earliestArrivalAtAnyDestination = this.earliestArrivalAtAnyStop(routingState.earliestArrivals, routingState.destinations);
21658
+ for (let j = startIndex; j < route.getNbStops(); j++) {
21659
+ const currentStop = route.stops[j];
21660
+ // If we're currently on a trip,
21661
+ // check if arrival at the stop improves the earliest arrival time
21662
+ if (activeTrip !== undefined) {
21663
+ const arrivalTime = route.arrivalAt(currentStop, activeTrip.tripIndex);
21664
+ const dropOffType = route.dropOffTypeAt(currentStop, activeTrip.tripIndex);
21665
+ const earliestArrivalAtCurrentStop = (_b = (_a = routingState.earliestArrivals.get(currentStop)) === null || _a === void 0 ? void 0 : _a.arrival) !== null && _b !== void 0 ? _b : UNREACHED;
21666
+ if (dropOffType !== 'NOT_AVAILABLE' &&
21667
+ arrivalTime.isBefore(earliestArrivalAtCurrentStop) &&
21668
+ arrivalTime.isBefore(earliestArrivalAtAnyDestination)) {
21669
+ const edge = {
21670
+ arrival: arrivalTime,
21671
+ routeId: route.id,
21672
+ tripIndex: activeTrip.tripIndex,
21673
+ from: activeTrip.hopOnStop,
21674
+ to: currentStop,
21675
+ };
21676
+ if (tripContinuation) {
21677
+ // In case of continuous trip, we set a pointer to the previous edge
21678
+ edge.continuationOf = tripContinuation.previousEdge;
21679
+ }
21680
+ edgesAtCurrentRound.set(currentStop, edge);
21681
+ routingState.earliestArrivals.set(currentStop, {
21682
+ arrival: arrivalTime,
21683
+ legNumber: round,
21684
+ });
21685
+ newlyMarkedStops.add(currentStop);
21686
+ }
21687
+ }
21688
+ if (tripContinuation) {
21689
+ // If it's a trip continuation, no need to check for earlier trips
21690
+ continue;
21691
+ }
21692
+ // check if we can board an earlier trip at the current stop
21693
+ // if there was no current trip, find the first one reachable
21694
+ const earliestArrivalOnPreviousRound = (_c = edgesAtPreviousRound.get(currentStop)) === null || _c === void 0 ? void 0 : _c.arrival;
21695
+ // TODO if the last edge is not a transfer, and if there is no trip continuation of type 1 (guaranteed)
21696
+ // Add the minTransferTime to make sure there's at least 2 minutes to transfer.
21697
+ // If platforms are collapsed, make sure to apply the station level transfer time
21698
+ // (or later at route reconstruction time)
21699
+ if (earliestArrivalOnPreviousRound !== undefined &&
21700
+ (activeTrip === undefined ||
21701
+ earliestArrivalOnPreviousRound.isBefore(route.departureFrom(currentStop, activeTrip.tripIndex)) ||
21702
+ earliestArrivalOnPreviousRound.equals(route.departureFrom(currentStop, activeTrip.tripIndex)))) {
21703
+ const earliestTrip = route.findEarliestTrip(currentStop, earliestArrivalOnPreviousRound, activeTrip === null || activeTrip === void 0 ? void 0 : activeTrip.tripIndex);
21704
+ if (earliestTrip !== undefined) {
21705
+ activeTrip = {
21706
+ routeId: route.id,
21707
+ tripIndex: earliestTrip,
21708
+ hopOnStop: currentStop,
21709
+ };
21710
+ }
21711
+ }
21712
+ }
21713
+ return newlyMarkedStops;
21714
+ }
21715
+ /**
21716
+ * Processes all currently marked stops to find available transfers
21717
+ * and determines if using these transfers would result in earlier arrival times
21718
+ * at destination stops. It handles different transfer types including in-seat
21719
+ * transfers and walking transfers with appropriate minimum transfer times.
21720
+ *
21721
+ * @param query The routing query containing transfer options and constraints
21722
+ * @param round The current round number in the RAPTOR algorithm
21723
+ * @param routingState The current routing state containing arrival times and marked stops
20824
21724
  */
20825
- considerTransfers(query, markedStops, arrivalsAtCurrentRound, earliestArrivals, round) {
21725
+ considerTransfers(query, round, markedStops, routingState) {
20826
21726
  var _a, _b;
20827
21727
  const { options } = query;
21728
+ const arrivalsAtCurrentRound = routingState.graph[round];
20828
21729
  const newlyMarkedStops = new Set();
20829
- const markedStopsArray = Array.from(markedStops);
20830
- for (let i = 0; i < markedStopsArray.length; i++) {
20831
- const stop = markedStopsArray[i];
21730
+ for (const stop of markedStops) {
20832
21731
  const currentArrival = arrivalsAtCurrentRound.get(stop);
20833
- if (!currentArrival)
20834
- continue;
20835
21732
  // Skip transfers if the last leg was also a transfer
20836
- const previousLeg = currentArrival.leg;
20837
- if (previousLeg && !('route' in previousLeg)) {
21733
+ if (!currentArrival || 'type' in currentArrival)
20838
21734
  continue;
20839
- }
20840
21735
  const transfers = this.timetable.getTransfers(stop);
20841
21736
  for (let j = 0; j < transfers.length; j++) {
20842
21737
  const transfer = transfers[j];
@@ -20845,6 +21740,7 @@ class Router {
20845
21740
  transferTime = transfer.minTransferTime;
20846
21741
  }
20847
21742
  else if (transfer.type === 'IN_SEAT') {
21743
+ // TODO not needed anymore now that trip continuations are handled separately
20848
21744
  transferTime = Duration.zero();
20849
21745
  }
20850
21746
  else {
@@ -20853,32 +21749,22 @@ class Router {
20853
21749
  const arrivalAfterTransfer = currentArrival.arrival.plus(transferTime);
20854
21750
  const originalArrival = (_b = (_a = arrivalsAtCurrentRound.get(transfer.destination)) === null || _a === void 0 ? void 0 : _a.arrival) !== null && _b !== void 0 ? _b : UNREACHED;
20855
21751
  if (arrivalAfterTransfer.isBefore(originalArrival)) {
20856
- const origin = currentArrival.origin;
20857
21752
  arrivalsAtCurrentRound.set(transfer.destination, {
20858
21753
  arrival: arrivalAfterTransfer,
20859
- legNumber: round,
20860
- origin: origin,
20861
- leg: {
20862
- from: this.stopsIndex.findStopById(stop),
20863
- to: this.stopsIndex.findStopById(transfer.destination),
20864
- minTransferTime: transfer.minTransferTime,
20865
- type: transfer.type,
20866
- },
21754
+ from: stop,
21755
+ to: transfer.destination,
21756
+ minTransferTime: transfer.minTransferTime,
21757
+ type: transfer.type,
20867
21758
  });
20868
- earliestArrivals.set(transfer.destination, {
21759
+ routingState.earliestArrivals.set(transfer.destination, {
20869
21760
  arrival: arrivalAfterTransfer,
20870
21761
  legNumber: round,
20871
- origin: origin,
20872
21762
  });
20873
21763
  newlyMarkedStops.add(transfer.destination);
20874
21764
  }
20875
21765
  }
20876
21766
  }
20877
- const newlyMarkedStopsArray = Array.from(newlyMarkedStops);
20878
- for (let i = 0; i < newlyMarkedStopsArray.length; i++) {
20879
- const newStop = newlyMarkedStopsArray[i];
20880
- markedStops.add(newStop);
20881
- }
21767
+ return newlyMarkedStops;
20882
21768
  }
20883
21769
  /**
20884
21770
  * Finds the earliest arrival time at any stop from a given set of destinations.
@@ -20892,116 +21778,11 @@ class Router {
20892
21778
  let earliestArrivalAtAnyDestination = UNREACHED;
20893
21779
  for (let i = 0; i < destinations.length; i++) {
20894
21780
  const destination = destinations[i];
20895
- const arrival = (_b = (_a = earliestArrivals.get(destination.id)) === null || _a === void 0 ? void 0 : _a.arrival) !== null && _b !== void 0 ? _b : UNREACHED;
21781
+ const arrival = (_b = (_a = earliestArrivals.get(destination)) === null || _a === void 0 ? void 0 : _a.arrival) !== null && _b !== void 0 ? _b : UNREACHED;
20896
21782
  earliestArrivalAtAnyDestination = Time.min(earliestArrivalAtAnyDestination, arrival);
20897
21783
  }
20898
21784
  return earliestArrivalAtAnyDestination;
20899
21785
  }
20900
- /**
20901
- * The main Raptor algorithm implementation.
20902
- *
20903
- * @param query The query containing the main parameters for the routing.
20904
- * @returns A result object containing data structures allowing to reconstruct routes and .
20905
- */
20906
- route(query) {
20907
- var _a, _b, _c, _d, _e;
20908
- const { from, to, departureTime, options } = query;
20909
- // Consider children or siblings of the "from" stop as potential origins
20910
- const origins = this.stopsIndex.equivalentStops(from);
20911
- // Consider children or siblings of the "to" stop(s) as potential destinations
20912
- const destinations = Array.from(to).flatMap((destination) => this.stopsIndex.equivalentStops(destination));
20913
- const earliestArrivals = new Map();
20914
- const earliestArrivalsWithoutAnyLeg = new Map();
20915
- const earliestArrivalsPerRound = [earliestArrivalsWithoutAnyLeg];
20916
- // Stops that have been improved at round k-1
20917
- const markedStops = new Set();
20918
- for (let i = 0; i < origins.length; i++) {
20919
- const originStop = origins[i];
20920
- markedStops.add(originStop.id);
20921
- earliestArrivals.set(originStop.id, {
20922
- arrival: departureTime,
20923
- legNumber: 0,
20924
- origin: originStop.id,
20925
- });
20926
- earliestArrivalsWithoutAnyLeg.set(originStop.id, {
20927
- arrival: departureTime,
20928
- legNumber: 0,
20929
- origin: originStop.id,
20930
- });
20931
- }
20932
- // on the first round we need to first consider transfers to discover all possible route origins
20933
- this.considerTransfers(query, markedStops, earliestArrivalsWithoutAnyLeg, earliestArrivals, 0);
20934
- for (let round = 1; round <= options.maxTransfers + 1; round++) {
20935
- const arrivalsAtCurrentRound = new Map();
20936
- earliestArrivalsPerRound.push(arrivalsAtCurrentRound);
20937
- const arrivalsAtPreviousRound = earliestArrivalsPerRound[round - 1];
20938
- // Routes that contain at least one stop reached with at least round - 1 legs
20939
- // together with corresponding hop on stop index (earliest marked stop)
20940
- const reachableRoutes = this.timetable.findReachableRoutes(markedStops, options.transportModes);
20941
- markedStops.clear();
20942
- // for each route that can be reached with at least round - 1 trips
20943
- const reachableRoutesArray = Array.from(reachableRoutes.entries());
20944
- for (let i = 0; i < reachableRoutesArray.length; i++) {
20945
- const [route, hopOnStop] = reachableRoutesArray[i];
20946
- let currentTrip = undefined;
20947
- const startIndex = route.stopIndex(hopOnStop);
20948
- for (let j = startIndex; j < route.getNbStops(); j++) {
20949
- const currentStop = route.stops[j];
20950
- // If we're currently on a trip,
20951
- // check if arrival at the stop improves the earliest arrival time
20952
- if (currentTrip !== undefined) {
20953
- const currentArrivalTime = route.arrivalAt(currentStop, currentTrip.tripIndex);
20954
- const currentDropOffType = route.dropOffTypeAt(currentStop, currentTrip.tripIndex);
20955
- const earliestArrivalAtCurrentStop = (_b = (_a = earliestArrivals.get(currentStop)) === null || _a === void 0 ? void 0 : _a.arrival) !== null && _b !== void 0 ? _b : UNREACHED;
20956
- if (currentDropOffType !== 'NOT_AVAILABLE' &&
20957
- currentArrivalTime.isBefore(earliestArrivalAtCurrentStop) &&
20958
- currentArrivalTime.isBefore(this.earliestArrivalAtAnyStop(earliestArrivals, destinations))) {
20959
- const bestHopOnDepartureTime = route.departureFrom(currentTrip.bestHopOnStop, currentTrip.tripIndex);
20960
- arrivalsAtCurrentRound.set(currentStop, {
20961
- arrival: currentArrivalTime,
20962
- legNumber: round,
20963
- origin: currentTrip.origin,
20964
- leg: {
20965
- from: this.stopsIndex.findStopById(currentTrip.bestHopOnStop),
20966
- to: this.stopsIndex.findStopById(currentStop),
20967
- departureTime: bestHopOnDepartureTime,
20968
- arrivalTime: currentArrivalTime,
20969
- route: this.timetable.getServiceRouteInfo(route),
20970
- },
20971
- });
20972
- earliestArrivals.set(currentStop, {
20973
- arrival: currentArrivalTime,
20974
- legNumber: round,
20975
- origin: currentTrip.origin,
20976
- });
20977
- markedStops.add(currentStop);
20978
- }
20979
- }
20980
- // check if we can board an earlier trip at the current stop
20981
- // if there was no current trip, find the first one reachable
20982
- const earliestArrivalOnPreviousRound = (_c = arrivalsAtPreviousRound.get(currentStop)) === null || _c === void 0 ? void 0 : _c.arrival;
20983
- if (earliestArrivalOnPreviousRound !== undefined &&
20984
- (currentTrip === undefined ||
20985
- earliestArrivalOnPreviousRound.isBefore(route.departureFrom(currentStop, currentTrip.tripIndex)) ||
20986
- earliestArrivalOnPreviousRound.equals(route.departureFrom(currentStop, currentTrip.tripIndex)))) {
20987
- const earliestTrip = route.findEarliestTrip(currentStop, earliestArrivalOnPreviousRound, currentTrip === null || currentTrip === void 0 ? void 0 : currentTrip.tripIndex);
20988
- if (earliestTrip !== undefined) {
20989
- currentTrip = {
20990
- tripIndex: earliestTrip,
20991
- // we need to keep track of the best hop-on stop to reconstruct the route at the end
20992
- bestHopOnStop: currentStop,
20993
- origin: (_e = (_d = arrivalsAtPreviousRound.get(currentStop)) === null || _d === void 0 ? void 0 : _d.origin) !== null && _e !== void 0 ? _e : currentStop,
20994
- };
20995
- }
20996
- }
20997
- }
20998
- }
20999
- this.considerTransfers(query, markedStops, arrivalsAtCurrentRound, earliestArrivals, round);
21000
- if (markedStops.size === 0)
21001
- break;
21002
- }
21003
- return new Result(query, earliestArrivals, earliestArrivalsPerRound, this.stopsIndex);
21004
- }
21005
21786
  }
21006
21787
 
21007
21788
  /**
@@ -21138,7 +21919,6 @@ const startRepl = (stopsPath, timetablePath) => {
21138
21919
  replServer.defineCommand('route', {
21139
21920
  help: 'Find a route using .route from <stationIdOrName> to <stationIdOrName> at <HH:mm> [with <N> transfers]',
21140
21921
  action(routeQuery) {
21141
- var _a;
21142
21922
  this.clearBufferedCommand();
21143
21923
  const parts = routeQuery.split(' ').filter(Boolean);
21144
21924
  const withTransfersIndex = parts.indexOf('with');
@@ -21194,12 +21974,13 @@ const startRepl = (stopsPath, timetablePath) => {
21194
21974
  console.log(`Destination not reachable`);
21195
21975
  }
21196
21976
  else {
21197
- console.log(`Arriving to ${toStop.name} at ${arrivalTime.arrival.toString()} with ${arrivalTime.legNumber - 1} transfers from ${(_a = stopsIndex.findStopById(arrivalTime.origin)) === null || _a === void 0 ? void 0 : _a.name}.`);
21977
+ console.log(`Arriving to ${toStop.name} at ${arrivalTime.arrival.toString()} with ${arrivalTime.legNumber - 1} transfers from ${fromStop.name}.`);
21198
21978
  }
21199
21979
  const bestRoute = result.bestRoute(toStop.sourceStopId);
21200
21980
  if (bestRoute) {
21201
21981
  console.log(`Found route from ${fromStop.name} to ${toStop.name}:`);
21202
21982
  console.log(bestRoute.toString());
21983
+ console.log(bestRoute.asJson());
21203
21984
  }
21204
21985
  else {
21205
21986
  console.log('No route found');
@@ -21275,6 +22056,203 @@ const startRepl = (stopsPath, timetablePath) => {
21275
22056
  this.displayPrompt();
21276
22057
  },
21277
22058
  });
22059
+ const formatPickupDropoffType = (type) => {
22060
+ switch (type) {
22061
+ case 'REGULAR':
22062
+ return 'R';
22063
+ case 'NOT_AVAILABLE':
22064
+ return 'N';
22065
+ case 'MUST_PHONE_AGENCY':
22066
+ return 'A';
22067
+ case 'MUST_COORDINATE_WITH_DRIVER':
22068
+ return 'D';
22069
+ default:
22070
+ return '?';
22071
+ }
22072
+ };
22073
+ replServer.defineCommand('inspect', {
22074
+ help: 'Inspect a route or stop using .inspect route <routeId> or .inspect stop <stopId>',
22075
+ action(inspectQuery) {
22076
+ this.clearBufferedCommand();
22077
+ const parts = inspectQuery.trim().split(' ');
22078
+ if (parts.length !== 2) {
22079
+ console.log('Usage: .inspect route <routeId> or .inspect stop <stopId>');
22080
+ this.displayPrompt();
22081
+ return;
22082
+ }
22083
+ const [type, idStr] = parts;
22084
+ if (type !== 'route' && type !== 'stop') {
22085
+ console.log('Usage: .inspect route <routeId> or .inspect stop <stopId>');
22086
+ this.displayPrompt();
22087
+ return;
22088
+ }
22089
+ const inspectRoute = (routeIdStr) => {
22090
+ var _a, _b, _c;
22091
+ const routeId = parseInt(routeIdStr.trim());
22092
+ if (isNaN(routeId)) {
22093
+ console.log('Usage: .inspect route <routeId>');
22094
+ return;
22095
+ }
22096
+ const route = timetable.getRoute(routeId);
22097
+ if (!route) {
22098
+ console.log(`Route ${routeId} not found`);
22099
+ return;
22100
+ }
22101
+ const serviceRouteInfo = timetable.getServiceRouteInfo(route);
22102
+ const routeName = serviceRouteInfo.name;
22103
+ const routeType = serviceRouteInfo.type;
22104
+ console.log(`\n=== Route ${routeId} ===`);
22105
+ console.log(`Service Route: ${routeName}`);
22106
+ console.log(`Type: ${routeType}`);
22107
+ console.log(`Number of stops: ${route.getNbStops()}`);
22108
+ console.log(`Number of trips: ${route.getNbTrips()}`);
22109
+ console.log('\n--- Stops ---');
22110
+ for (let i = 0; i < route.stops.length; i++) {
22111
+ const stopId = route.stopId(i);
22112
+ const stop = stopsIndex.findStopById(stopId);
22113
+ const platform = (stop === null || stop === void 0 ? void 0 : stop.platform) ? ` (Pl. ${stop.platform})` : '';
22114
+ console.log(`${i + 1}. ${(_a = stop === null || stop === void 0 ? void 0 : stop.name) !== null && _a !== void 0 ? _a : 'Unknown'}${platform} (${stopId}, ${(_b = stop === null || stop === void 0 ? void 0 : stop.sourceStopId) !== null && _b !== void 0 ? _b : 'N/A'})`);
22115
+ }
22116
+ console.log('\n--- Trips ---');
22117
+ for (let tripIndex = 0; tripIndex < route.getNbTrips(); tripIndex++) {
22118
+ console.log(`\nTrip ${tripIndex}:`);
22119
+ for (let stopIndex = 0; stopIndex < route.stops.length; stopIndex++) {
22120
+ const stopId = route.stopId(stopIndex);
22121
+ const stop = stopsIndex.findStopById(stopId);
22122
+ const departure = route.departureFrom(stopId, tripIndex);
22123
+ const arrival = route.arrivalAt(stopId, tripIndex);
22124
+ const pickupType = route.pickUpTypeFrom(stopId, tripIndex);
22125
+ const dropOffType = route.dropOffTypeAt(stopId, tripIndex);
22126
+ const pickupStr = formatPickupDropoffType(pickupType);
22127
+ const dropOffStr = formatPickupDropoffType(dropOffType);
22128
+ console.log(` ${stopIndex + 1}. ${(_c = stop === null || stop === void 0 ? void 0 : stop.name) !== null && _c !== void 0 ? _c : 'Unknown'}: arr ${arrival.toString()} (${pickupStr}) → dep ${departure.toString()} (${dropOffStr})`);
22129
+ }
22130
+ }
22131
+ console.log();
22132
+ };
22133
+ const inspectStop = (stopIdStr) => {
22134
+ var _a, _b, _c;
22135
+ let stop;
22136
+ const stopBySourceId = stopsIndex.findStopBySourceStopId(stopIdStr);
22137
+ if (stopBySourceId !== undefined) {
22138
+ stop = stopBySourceId;
22139
+ }
22140
+ else if (!isNaN(Number(stopIdStr))) {
22141
+ const stopById = stopsIndex.findStopById(Number(stopIdStr));
22142
+ if (stopById !== undefined) {
22143
+ stop = stopById;
22144
+ }
22145
+ }
22146
+ else {
22147
+ const stops = stopsIndex.findStopsByName(stopIdStr);
22148
+ if (stops.length > 0) {
22149
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
22150
+ stop = stops[0];
22151
+ }
22152
+ }
22153
+ if (!stop) {
22154
+ console.log(`Stop not found: ${stopIdStr}`);
22155
+ return;
22156
+ }
22157
+ console.log(`\n=== Stop ${stop.id} ===`);
22158
+ console.log(`Name: ${stop.name}`);
22159
+ if (stop.platform) {
22160
+ console.log(`Platform: ${stop.platform}`);
22161
+ }
22162
+ console.log(`Source ID: ${stop.sourceStopId}`);
22163
+ const routes = timetable.routesPassingThrough(stop.id);
22164
+ console.log(`Number of routes: ${routes.length}`);
22165
+ const equivalentStops = stopsIndex
22166
+ .equivalentStops(stop.sourceStopId)
22167
+ .filter((equivStop) => equivStop.id !== stop.id);
22168
+ console.log(`Number of equivalent stops: ${equivalentStops.length}`);
22169
+ if (equivalentStops.length > 0) {
22170
+ console.log('\n--- Equivalent Stops ---');
22171
+ equivalentStops.forEach((equivStop, index) => {
22172
+ const platform = equivStop.platform
22173
+ ? ` (Pl. ${equivStop.platform})`
22174
+ : '';
22175
+ console.log(`${index + 1}. ${equivStop.name}${platform} (${equivStop.id}, ${equivStop.sourceStopId})`);
22176
+ });
22177
+ }
22178
+ if (routes.length > 0) {
22179
+ console.log('\n--- Routes ---');
22180
+ routes.forEach((route, index) => {
22181
+ const serviceRouteInfo = timetable.getServiceRouteInfo(route);
22182
+ console.log(`${index + 1}. Route ${route.id}: ${serviceRouteInfo.name} (${serviceRouteInfo.type})`);
22183
+ });
22184
+ }
22185
+ const transfers = timetable.getTransfers(stop.id);
22186
+ console.log(`Number of transfers: ${transfers.length}`);
22187
+ if (transfers.length > 0) {
22188
+ console.log('\n--- Transfers ---');
22189
+ transfers.forEach((transfer, index) => {
22190
+ var _a, _b;
22191
+ const destStop = stopsIndex.findStopById(transfer.destination);
22192
+ const platform = (destStop === null || destStop === void 0 ? void 0 : destStop.platform)
22193
+ ? ` (Pl. ${destStop.platform})`
22194
+ : '';
22195
+ const minTime = transfer.minTransferTime
22196
+ ? ` (min: ${Math.floor(transfer.minTransferTime.toSeconds() / 60)}min)`
22197
+ : '';
22198
+ console.log(`${index + 1}. ${transfer.type} to ${(_a = destStop === null || destStop === void 0 ? void 0 : destStop.name) !== null && _a !== void 0 ? _a : 'Unknown'}${platform} (${transfer.destination}, ${(_b = destStop === null || destStop === void 0 ? void 0 : destStop.sourceStopId) !== null && _b !== void 0 ? _b : 'N/A'})${minTime}`);
22199
+ });
22200
+ }
22201
+ let totalContinuations = 0;
22202
+ const continuationsByTrip = new Map();
22203
+ routes.forEach((route) => {
22204
+ for (let tripIndex = 0; tripIndex < route.getNbTrips(); tripIndex++) {
22205
+ const continuations = timetable.getContinuousTrips(stop.id, route.id, tripIndex);
22206
+ if (continuations.length > 0) {
22207
+ totalContinuations += continuations.length;
22208
+ const tripKey = `${route.id}-${tripIndex}`;
22209
+ continuationsByTrip.set(tripKey, {
22210
+ route,
22211
+ tripIndex,
22212
+ continuations,
22213
+ });
22214
+ }
22215
+ }
22216
+ });
22217
+ console.log(`Number of trip continuations: ${totalContinuations}`);
22218
+ if (totalContinuations > 0) {
22219
+ console.log('\n--- Trip Continuations ---');
22220
+ let continuationIndex = 1;
22221
+ for (const [, value] of continuationsByTrip) {
22222
+ const { route, tripIndex, continuations } = value;
22223
+ const serviceRouteInfo = timetable.getServiceRouteInfo(route);
22224
+ for (const continuation of continuations) {
22225
+ const destStop = stopsIndex.findStopById(continuation.hopOnStop);
22226
+ const destPlatform = (destStop === null || destStop === void 0 ? void 0 : destStop.platform)
22227
+ ? ` (Pl. ${destStop.platform})`
22228
+ : '';
22229
+ const destRoute = timetable.getRoute(continuation.routeId);
22230
+ const destServiceRouteInfo = destRoute
22231
+ ? timetable.getServiceRouteInfo(destRoute)
22232
+ : null;
22233
+ const originTime = route.departureFrom(stop.id, tripIndex);
22234
+ const continuationTime = destRoute === null || destRoute === void 0 ? void 0 : destRoute.departureFrom(continuation.hopOnStop, continuation.tripIndex);
22235
+ const continuationTimeStr = continuationTime
22236
+ ? ` at ${continuationTime.toString()}`
22237
+ : '';
22238
+ console.log(`${continuationIndex}. From Route ${route.id} (${serviceRouteInfo.name}) Trip ${tripIndex} at ${originTime.toString()} → ` +
22239
+ `Route ${continuation.routeId} (${(_a = destServiceRouteInfo === null || destServiceRouteInfo === void 0 ? void 0 : destServiceRouteInfo.name) !== null && _a !== void 0 ? _a : 'Unknown'}) Trip ${continuation.tripIndex}${continuationTimeStr} ` +
22240
+ `at ${(_b = destStop === null || destStop === void 0 ? void 0 : destStop.name) !== null && _b !== void 0 ? _b : 'Unknown'}${destPlatform} (${continuation.hopOnStop}, ${(_c = destStop === null || destStop === void 0 ? void 0 : destStop.sourceStopId) !== null && _c !== void 0 ? _c : 'N/A'})`);
22241
+ continuationIndex++;
22242
+ }
22243
+ }
22244
+ }
22245
+ console.log();
22246
+ };
22247
+ if (type === 'route') {
22248
+ inspectRoute(idStr !== null && idStr !== void 0 ? idStr : '');
22249
+ }
22250
+ else {
22251
+ inspectStop(idStr !== null && idStr !== void 0 ? idStr : '');
22252
+ }
22253
+ this.displayPrompt();
22254
+ },
22255
+ });
21278
22256
  };
21279
22257
 
21280
22258
  const program = new Command();