minotor 7.0.2 → 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 (58) 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 +1243 -267
  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 +494 -71
  9. package/dist/parser.cjs.js.map +1 -1
  10. package/dist/parser.esm.js +494 -71
  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 -211
  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/timetable.ts +45 -6
  58. package/src/timetable/tripId.ts +29 -0
package/dist/cli.mjs CHANGED
@@ -16606,41 +16606,191 @@ const Transfer = {
16606
16606
  return message;
16607
16607
  },
16608
16608
  };
16609
- function createBaseStopAdjacency() {
16610
- return { transfers: [], routes: [] };
16609
+ function createBaseTripBoarding() {
16610
+ return { hopOnStop: 0, routeId: 0, tripIndex: 0 };
16611
16611
  }
16612
- const StopAdjacency = {
16612
+ const TripBoarding = {
16613
16613
  encode(message, writer = new BinaryWriter()) {
16614
- for (const v of message.transfers) {
16615
- Transfer.encode(v, writer.uint32(10).fork()).join();
16614
+ if (message.hopOnStop !== 0) {
16615
+ writer.uint32(8).uint32(message.hopOnStop);
16616
16616
  }
16617
- writer.uint32(18).fork();
16618
- for (const v of message.routes) {
16619
- 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);
16620
16622
  }
16621
- writer.join();
16622
16623
  return writer;
16623
16624
  },
16624
16625
  decode(input, length) {
16625
16626
  const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
16626
16627
  const end = length === undefined ? reader.len : reader.pos + length;
16627
- const message = createBaseStopAdjacency();
16628
+ const message = createBaseTripBoarding();
16628
16629
  while (reader.pos < end) {
16629
16630
  const tag = reader.uint32();
16630
16631
  switch (tag >>> 3) {
16631
16632
  case 1: {
16632
- if (tag !== 10) {
16633
+ if (tag !== 8) {
16633
16634
  break;
16634
16635
  }
16635
- 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();
16636
16718
  continue;
16637
16719
  }
16638
16720
  case 2: {
16639
- 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) {
16640
16790
  message.routes.push(reader.uint32());
16641
16791
  continue;
16642
16792
  }
16643
- if (tag === 18) {
16793
+ if (tag === 10) {
16644
16794
  const end2 = reader.uint32() + reader.pos;
16645
16795
  while (reader.pos < end2) {
16646
16796
  message.routes.push(reader.uint32());
@@ -16649,6 +16799,20 @@ const StopAdjacency = {
16649
16799
  }
16650
16800
  break;
16651
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
+ }
16652
16816
  }
16653
16817
  if ((tag & 7) === 4 || tag === 0) {
16654
16818
  break;
@@ -16659,20 +16823,26 @@ const StopAdjacency = {
16659
16823
  },
16660
16824
  fromJSON(object) {
16661
16825
  return {
16826
+ routes: globalThis.Array.isArray(object === null || object === void 0 ? void 0 : object.routes) ? object.routes.map((e) => globalThis.Number(e)) : [],
16662
16827
  transfers: globalThis.Array.isArray(object === null || object === void 0 ? void 0 : object.transfers)
16663
16828
  ? object.transfers.map((e) => Transfer.fromJSON(e))
16664
16829
  : [],
16665
- 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
+ : [],
16666
16833
  };
16667
16834
  },
16668
16835
  toJSON(message) {
16669
- var _a, _b;
16836
+ var _a, _b, _c;
16670
16837
  const obj = {};
16671
- 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) {
16672
16842
  obj.transfers = message.transfers.map((e) => Transfer.toJSON(e));
16673
16843
  }
16674
- if ((_b = message.routes) === null || _b === void 0 ? void 0 : _b.length) {
16675
- 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));
16676
16846
  }
16677
16847
  return obj;
16678
16848
  },
@@ -16680,10 +16850,11 @@ const StopAdjacency = {
16680
16850
  return StopAdjacency.fromPartial(base !== null && base !== void 0 ? base : {});
16681
16851
  },
16682
16852
  fromPartial(object) {
16683
- var _a, _b;
16853
+ var _a, _b, _c;
16684
16854
  const message = createBaseStopAdjacency();
16685
- message.transfers = ((_a = object.transfers) === null || _a === void 0 ? void 0 : _a.map((e) => Transfer.fromPartial(e))) || [];
16686
- 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))) || [];
16687
16858
  return message;
16688
16859
  },
16689
16860
  };
@@ -17172,7 +17343,8 @@ const toPickupDropOffType = (numericalType) => {
17172
17343
  * A route identifies all trips of a given service route sharing the same list of stops.
17173
17344
  */
17174
17345
  let Route$1 = class Route {
17175
- constructor(stopTimes, pickUpDropOffTypes, stops, serviceRouteId) {
17346
+ constructor(id, stopTimes, pickUpDropOffTypes, stops, serviceRouteId) {
17347
+ this.id = id;
17176
17348
  this.stopTimes = stopTimes;
17177
17349
  this.pickUpDropOffTypes = pickUpDropOffTypes;
17178
17350
  this.stops = stops;
@@ -17185,6 +17357,78 @@ let Route$1 = class Route {
17185
17357
  this.stopIndices.set(stops[i], i);
17186
17358
  }
17187
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
+ }
17188
17432
  /**
17189
17433
  * Serializes the Route into binary arrays.
17190
17434
  *
@@ -17208,11 +17452,11 @@ let Route$1 = class Route {
17208
17452
  isBefore(stopA, stopB) {
17209
17453
  const stopAIndex = this.stopIndices.get(stopA);
17210
17454
  if (stopAIndex === undefined) {
17211
- 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}`);
17212
17456
  }
17213
17457
  const stopBIndex = this.stopIndices.get(stopB);
17214
17458
  if (stopBIndex === undefined) {
17215
- 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}`);
17216
17460
  }
17217
17461
  return stopAIndex < stopBIndex;
17218
17462
  }
@@ -17224,6 +17468,14 @@ let Route$1 = class Route {
17224
17468
  getNbStops() {
17225
17469
  return this.nbStops;
17226
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
+ }
17227
17479
  /**
17228
17480
  * Finds the ServiceRouteId of the route. It corresponds the identifier
17229
17481
  * of the service shown to the end user as a route.
@@ -17241,7 +17493,7 @@ let Route$1 = class Route {
17241
17493
  * @returns The arrival time at the specified stop and trip as a Time object.
17242
17494
  */
17243
17495
  arrivalAt(stopId, tripIndex) {
17244
- const arrivalIndex = (tripIndex * this.stops.length + this.stopIndex(stopId)) * 2;
17496
+ const arrivalIndex = (tripIndex * this.stops.length + this.stopRouteIndex(stopId)) * 2;
17245
17497
  const arrival = this.stopTimes[arrivalIndex];
17246
17498
  if (arrival === undefined) {
17247
17499
  throw new Error(`Arrival time not found for stop ${stopId} at trip index ${tripIndex} in route ${this.serviceRouteId}`);
@@ -17256,7 +17508,7 @@ let Route$1 = class Route {
17256
17508
  * @returns The departure time at the specified stop and trip as a Time object.
17257
17509
  */
17258
17510
  departureFrom(stopId, tripIndex) {
17259
- const departureIndex = (tripIndex * this.stops.length + this.stopIndex(stopId)) * 2 + 1;
17511
+ const departureIndex = (tripIndex * this.stops.length + this.stopRouteIndex(stopId)) * 2 + 1;
17260
17512
  const departure = this.stopTimes[departureIndex];
17261
17513
  if (departure === undefined) {
17262
17514
  throw new Error(`Departure time not found for stop ${stopId} at trip index ${tripIndex} in route ${this.serviceRouteId}`);
@@ -17271,7 +17523,7 @@ let Route$1 = class Route {
17271
17523
  * @returns The pick-up type at the specified stop and trip.
17272
17524
  */
17273
17525
  pickUpTypeFrom(stopId, tripIndex) {
17274
- const globalIndex = tripIndex * this.stops.length + this.stopIndex(stopId);
17526
+ const globalIndex = tripIndex * this.stops.length + this.stopRouteIndex(stopId);
17275
17527
  const byteIndex = Math.floor(globalIndex / 2);
17276
17528
  const isSecondPair = globalIndex % 2 === 1;
17277
17529
  const byte = this.pickUpDropOffTypes[byteIndex];
@@ -17291,7 +17543,7 @@ let Route$1 = class Route {
17291
17543
  * @returns The drop-off type at the specified stop and trip.
17292
17544
  */
17293
17545
  dropOffTypeAt(stopId, tripIndex) {
17294
- const globalIndex = tripIndex * this.stops.length + this.stopIndex(stopId);
17546
+ const globalIndex = tripIndex * this.stops.length + this.stopRouteIndex(stopId);
17295
17547
  const byteIndex = Math.floor(globalIndex / 2);
17296
17548
  const isSecondPair = globalIndex % 2 === 1;
17297
17549
  const byte = this.pickUpDropOffTypes[byteIndex];
@@ -17351,13 +17603,25 @@ let Route$1 = class Route {
17351
17603
  * @param stopId The StopId of the stop to locate in the route.
17352
17604
  * @returns The index of the stop in the route.
17353
17605
  */
17354
- stopIndex(stopId) {
17606
+ stopRouteIndex(stopId) {
17355
17607
  const stopIndex = this.stopIndices.get(stopId);
17356
17608
  if (stopIndex === undefined) {
17357
17609
  throw new Error(`Stop index for ${stopId} not found in route ${this.serviceRouteId}`);
17358
17610
  }
17359
17611
  return stopIndex;
17360
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
+ }
17361
17625
  };
17362
17626
 
17363
17627
  const isLittleEndian = (() => {
@@ -17428,10 +17692,15 @@ const bytesToUint16Array = (bytes) => {
17428
17692
  const serializeStopsAdjacency = (stopsAdjacency) => {
17429
17693
  return stopsAdjacency.map((value) => {
17430
17694
  return {
17431
- transfers: value.transfers.map((transfer) => (Object.assign({ destination: transfer.destination, type: serializeTransferType(transfer.type) }, (transfer.minTransferTime !== undefined && {
17432
- minTransferTime: transfer.minTransferTime.toSeconds(),
17433
- })))),
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
+ : [],
17434
17700
  routes: value.routes,
17701
+ tripContinuations: value.tripContinuations
17702
+ ? serializeTripContinuations(value.tripContinuations)
17703
+ : [],
17435
17704
  };
17436
17705
  });
17437
17706
  };
@@ -17471,10 +17740,17 @@ const deserializeStopsAdjacency = (protoStopsAdjacency) => {
17471
17740
  }));
17472
17741
  transfers.push(newTransfer);
17473
17742
  }
17474
- result.push({
17475
- transfers: transfers,
17743
+ const stopAdjacency = {
17476
17744
  routes: value.routes,
17477
- });
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);
17478
17754
  }
17479
17755
  return result;
17480
17756
  };
@@ -17484,7 +17760,7 @@ const deserializeRoutesAdjacency = (protoRoutesAdjacency) => {
17484
17760
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
17485
17761
  const value = protoRoutesAdjacency[i];
17486
17762
  const stops = bytesToUint32Array(value.stops);
17487
- 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));
17488
17764
  }
17489
17765
  return routesAdjacency;
17490
17766
  };
@@ -17578,6 +17854,46 @@ const serializeRouteType = (type) => {
17578
17854
  return RouteType.MONORAIL;
17579
17855
  }
17580
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
+ };
17581
17897
 
17582
17898
  /* eslint-disable @typescript-eslint/no-non-null-assertion */
17583
17899
  const ALL_TRANSPORT_MODES = new Set([
@@ -17592,7 +17908,8 @@ const ALL_TRANSPORT_MODES = new Set([
17592
17908
  'TROLLEYBUS',
17593
17909
  'MONORAIL',
17594
17910
  ]);
17595
- const CURRENT_VERSION = '0.0.7';
17911
+ const EMPTY_TRIP_CONTINUATIONS = [];
17912
+ const CURRENT_VERSION = '0.0.8';
17596
17913
  /**
17597
17914
  * The internal transit timetable format.
17598
17915
  */
@@ -17603,9 +17920,10 @@ class Timetable {
17603
17920
  this.serviceRoutes = routes;
17604
17921
  this.activeStops = new Set();
17605
17922
  for (let i = 0; i < stopsAdjacency.length; i++) {
17606
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
17607
17923
  const stop = stopsAdjacency[i];
17608
- 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)) {
17609
17927
  this.activeStops.add(i);
17610
17928
  }
17611
17929
  }
@@ -17668,8 +17986,27 @@ class Timetable {
17668
17986
  * @returns An array of transfer options available at the stop.
17669
17987
  */
17670
17988
  getTransfers(stopId) {
17671
- var _a, _b;
17672
- 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);
17673
18010
  }
17674
18011
  /**
17675
18012
  * Retrieves the service route associated with the given route.
@@ -19955,6 +20292,7 @@ const parseGtfsLocationType = (gtfsLocationType) => {
19955
20292
  const parseTransfers = (transfersStream, stopsMap) => __awaiter(void 0, void 0, void 0, function* () {
19956
20293
  var _a, e_1, _b, _c;
19957
20294
  const transfers = new Map();
20295
+ const tripContinuations = new Map();
19958
20296
  try {
19959
20297
  for (var _d = true, _e = __asyncValues(parseCsv(transfersStream, [
19960
20298
  'transfer_type',
@@ -19968,25 +20306,48 @@ const parseTransfers = (transfersStream, stopsMap) => __awaiter(void 0, void 0,
19968
20306
  transferEntry.transfer_type === 5) {
19969
20307
  continue;
19970
20308
  }
19971
- if (transferEntry.from_trip_id && transferEntry.to_trip_id) {
19972
- console.warn(`Unsupported transfer between trips ${transferEntry.from_trip_id} and ${transferEntry.to_trip_id}.`);
19973
- continue;
19974
- }
19975
- if (transferEntry.from_route_id && transferEntry.to_route_id) {
19976
- console.warn(`Unsupported transfer between routes ${transferEntry.from_route_id} and ${transferEntry.to_route_id}.`);
19977
- continue;
19978
- }
19979
20309
  if (!transferEntry.from_stop_id || !transferEntry.to_stop_id) {
19980
20310
  console.warn(`Missing transfer origin or destination stop.`);
19981
20311
  continue;
19982
20312
  }
19983
- if (transferEntry.transfer_type === 2 && !transferEntry.min_transfer_time) {
19984
- console.info(`Missing minimum transfer time between ${transferEntry.from_stop_id} and ${transferEntry.to_stop_id}.`);
19985
- }
19986
20313
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
19987
20314
  const fromStop = stopsMap.get(transferEntry.from_stop_id);
19988
20315
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
19989
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
+ }
19990
20351
  const transfer = Object.assign({ destination: toStop.id, type: parseGtfsTransferType(transferEntry.transfer_type) }, (transferEntry.min_transfer_time && {
19991
20352
  minTransferTime: Duration.fromSeconds(transferEntry.min_transfer_time),
19992
20353
  }));
@@ -20002,7 +20363,10 @@ const parseTransfers = (transfersStream, stopsMap) => __awaiter(void 0, void 0,
20002
20363
  }
20003
20364
  finally { if (e_1) throw e_1.error; }
20004
20365
  }
20005
- return transfers;
20366
+ return {
20367
+ transfers,
20368
+ tripContinuations,
20369
+ };
20006
20370
  });
20007
20371
  const parseGtfsTransferType = (gtfsTransferType) => {
20008
20372
  switch (gtfsTransferType) {
@@ -20065,11 +20429,13 @@ const finalizeRouteFromBuilder = (builder) => {
20065
20429
  const stopTimesArray = new Uint16Array(stopsCount * tripsCount * 2);
20066
20430
  const allPickUpTypes = [];
20067
20431
  const allDropOffTypes = [];
20432
+ const gtfsTripIds = [];
20068
20433
  for (let tripIndex = 0; tripIndex < tripsCount; tripIndex++) {
20069
20434
  const trip = builder.trips[tripIndex];
20070
20435
  if (!trip) {
20071
20436
  throw new Error(`Missing trip data at index ${tripIndex}`);
20072
20437
  }
20438
+ gtfsTripIds.push(trip.gtfsTripId);
20073
20439
  const baseIndex = tripIndex * stopsCount * 2;
20074
20440
  for (let stopIndex = 0; stopIndex < stopsCount; stopIndex++) {
20075
20441
  const timeIndex = baseIndex + stopIndex * 2;
@@ -20091,12 +20457,15 @@ const finalizeRouteFromBuilder = (builder) => {
20091
20457
  }
20092
20458
  // Use 2-bit encoding for pickup/drop-off types
20093
20459
  const pickUpDropOffTypesArray = encodePickUpDropOffTypes(allPickUpTypes, allDropOffTypes);
20094
- return {
20095
- serviceRouteId: builder.serviceRouteId,
20096
- stops: stopsArray,
20097
- stopTimes: stopTimesArray,
20098
- pickUpDropOffTypes: pickUpDropOffTypesArray,
20099
- };
20460
+ return [
20461
+ {
20462
+ serviceRouteId: builder.serviceRouteId,
20463
+ stops: stopsArray,
20464
+ stopTimes: stopTimesArray,
20465
+ pickUpDropOffTypes: pickUpDropOffTypesArray,
20466
+ },
20467
+ gtfsTripIds,
20468
+ ];
20100
20469
  };
20101
20470
  /**
20102
20471
  * Parses the trips.txt file from a GTFS feed
@@ -20135,11 +20504,12 @@ const parseTrips = (tripsStream, serviceIds, validGtfsRoutes) => __awaiter(void
20135
20504
  }
20136
20505
  return trips;
20137
20506
  });
20138
- const buildStopsAdjacencyStructure = (serviceRoutes, routes, transfersMap, nbStops, activeStops) => {
20139
- // TODO somehow works when it's a map
20507
+ const buildStopsAdjacencyStructure = (tripsMapping, serviceRoutes, routes, transfersMap, tripContinuationsMap, nbStops, activeStops) => {
20140
20508
  const stopsAdjacency = new Array(nbStops);
20141
20509
  for (let i = 0; i < nbStops; i++) {
20142
- stopsAdjacency[i] = { routes: [], transfers: [] };
20510
+ stopsAdjacency[i] = {
20511
+ routes: [],
20512
+ };
20143
20513
  }
20144
20514
  for (let index = 0; index < routes.length; index++) {
20145
20515
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -20164,12 +20534,50 @@ const buildStopsAdjacencyStructure = (serviceRoutes, routes, transfersMap, nbSto
20164
20534
  const transfer = transfers[i];
20165
20535
  if (activeStops.has(stop) || activeStops.has(transfer.destination)) {
20166
20536
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
20167
- stopsAdjacency[stop].transfers.push(transfer);
20537
+ const stopAdj = stopsAdjacency[stop];
20538
+ if (!stopAdj.transfers) {
20539
+ stopAdj.transfers = [];
20540
+ }
20541
+ stopAdj.transfers.push(transfer);
20168
20542
  activeStops.add(transfer.destination);
20169
20543
  activeStops.add(stop);
20170
20544
  }
20171
20545
  }
20172
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
+ }
20173
20581
  return stopsAdjacency;
20174
20582
  };
20175
20583
  /**
@@ -20228,6 +20636,7 @@ const parseStopTimes = (stopTimesStream, stopsMap, activeTripIds, activeStopIds)
20228
20636
  }
20229
20637
  routeBuilder.trips.push({
20230
20638
  firstDeparture,
20639
+ gtfsTripId: currentTripId,
20231
20640
  arrivalTimes: arrivalTimes,
20232
20641
  departureTimes: departureTimes,
20233
20642
  pickUpTypes: pickUpTypes,
@@ -20265,6 +20674,9 @@ const parseStopTimes = (stopTimesStream, stopsMap, activeTripIds, activeStopIds)
20265
20674
  continue;
20266
20675
  }
20267
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.
20268
20680
  continue;
20269
20681
  }
20270
20682
  if (currentTripId && line.trip_id !== currentTripId && stops.length > 0) {
@@ -20301,11 +20713,19 @@ const parseStopTimes = (stopTimesStream, stopsMap, activeTripIds, activeStopIds)
20301
20713
  addTrip(currentTripId);
20302
20714
  }
20303
20715
  const routesAdjacency = [];
20716
+ const tripsMapping = new Map();
20304
20717
  for (const [, routeBuilder] of routeBuilders) {
20305
- const routeData = finalizeRouteFromBuilder(routeBuilder);
20306
- 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
+ });
20307
20727
  }
20308
- return { routes: routesAdjacency, serviceRoutesMap };
20728
+ return { routes: routesAdjacency, serviceRoutesMap, tripsMapping };
20309
20729
  });
20310
20730
  const parsePickupDropOffType = (gtfsType) => {
20311
20731
  switch (gtfsType) {
@@ -20384,24 +20804,27 @@ class GtfsParser {
20384
20804
  const tripsEnd = performance.now();
20385
20805
  log.info(`${trips.size} valid trips. (${(tripsEnd - tripsStart).toFixed(2)}ms)`);
20386
20806
  let transfers = new Map();
20807
+ let tripContinuations = new Map();
20387
20808
  if (entries[TRANSFERS_FILE]) {
20388
20809
  log.info(`Parsing ${TRANSFERS_FILE}`);
20389
20810
  const transfersStart = performance.now();
20390
20811
  const transfersStream = yield zip.stream(TRANSFERS_FILE);
20391
- transfers = yield parseTransfers(transfersStream, parsedStops);
20812
+ const { transfers: parsedTransfers, tripContinuations: parsedTripContinuations, } = yield parseTransfers(transfersStream, parsedStops);
20813
+ transfers = parsedTransfers;
20814
+ tripContinuations = parsedTripContinuations;
20392
20815
  const transfersEnd = performance.now();
20393
- 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)`);
20394
20817
  }
20395
20818
  log.info(`Parsing ${STOP_TIMES_FILE}`);
20396
20819
  const stopTimesStart = performance.now();
20397
20820
  const stopTimesStream = yield zip.stream(STOP_TIMES_FILE);
20398
- const { routes, serviceRoutesMap } = yield parseStopTimes(stopTimesStream, parsedStops, trips, activeStopIds);
20821
+ const { routes, serviceRoutesMap, tripsMapping } = yield parseStopTimes(stopTimesStream, parsedStops, trips, activeStopIds);
20399
20822
  const serviceRoutes = indexRoutes(validGtfsRoutes, serviceRoutesMap);
20400
20823
  const stopTimesEnd = performance.now();
20401
20824
  log.info(`${routes.length} valid unique routes. (${(stopTimesEnd - stopTimesStart).toFixed(2)}ms)`);
20402
20825
  log.info('Building stops adjacency structure');
20403
20826
  const stopsAdjacencyStart = performance.now();
20404
- const stopsAdjacency = buildStopsAdjacencyStructure(serviceRoutes, routes, transfers, parsedStops.size, activeStopIds);
20827
+ const stopsAdjacency = buildStopsAdjacencyStructure(tripsMapping, serviceRoutes, routes, transfers, tripContinuations, parsedStops.size, activeStopIds);
20405
20828
  const stopsAdjacencyEnd = performance.now();
20406
20829
  log.info(`${stopsAdjacency.length} valid stops in the structure. (${(stopsAdjacencyEnd - stopsAdjacencyStart).toFixed(2)}ms)`);
20407
20830
  yield zip.close();
@@ -20482,53 +20905,242 @@ const chGtfsProfile = {
20482
20905
 
20483
20906
  class Plotter {
20484
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
+ ];
20485
20917
  this.result = result;
20486
20918
  }
20487
20919
  /**
20488
- * Plots the path three as a DOT for debugging purposes.
20489
- *
20490
- * @returns A string representing the DOT graph of the path tree.
20920
+ * Gets the color for a round based on the specified palette.
20491
20921
  */
20492
- plotDotGraph() {
20493
- const earliestArrivalsPerRound = this.result.earliestArrivalsPerRound;
20494
- const dotParts = [
20495
- 'digraph PathTree {',
20496
- ' graph [overlap=false];',
20497
- ' 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}"];`,
20498
21015
  ];
20499
- earliestArrivalsPerRound.forEach((arrivalsInRound, round) => {
20500
- arrivalsInRound.forEach((tripLeg) => {
20501
- const { origin, leg } = tripLeg;
20502
- if (!leg)
20503
- return; // Skip if leg is undefined
20504
- const fromStop = this.result['stopsIndex'].findStopById(leg.from.id);
20505
- const toStop = this.result['stopsIndex'].findStopById(leg.to.id);
20506
- const originStop = this.result['stopsIndex'].findStopById(origin);
20507
- if (fromStop && toStop && originStop) {
20508
- const fromName = fromStop.platform
20509
- ? `${fromStop.name} (Pl. ${fromStop.platform})`
20510
- : fromStop.name;
20511
- const toName = toStop.platform
20512
- ? `${toStop.name} (Pl. ${toStop.platform})`
20513
- : toStop.name;
20514
- const originName = originStop.platform
20515
- ? `${originStop.name} (Pl. ${originStop.platform})`
20516
- : originStop.name;
20517
- const isVehicle = 'route' in leg;
20518
- const routeLabelContent = isVehicle
20519
- ? `${leg.route.name}\n${leg.departureTime.toString()} - ${leg.arrivalTime.toString()}`
20520
- : leg.minTransferTime
20521
- ? leg.minTransferTime.toString()
20522
- : '';
20523
- const intermediateNode = `IntermediateNode${fromStop.id}_${toStop.id}`;
20524
- const lineColor = isVehicle ? '' : ', color="red", fontcolor="red"';
20525
- const labelColor = isVehicle ? '' : ' fontcolor="red"';
20526
- dotParts.push(` "${fromName} (Origin: ${originName}) [R${round}]\n(${fromStop.id})" -> "${intermediateNode}" [shape=point${lineColor}];`);
20527
- dotParts.push(` "${intermediateNode}" [label="${routeLabelContent}" shape=rect style=filled fillcolor=white${labelColor} border=0];`);
20528
- 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);
20529
21088
  }
20530
21089
  });
20531
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
+ }
21117
+ }
21118
+ });
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);
20532
21144
  dotParts.push('}');
20533
21145
  return dotParts.join('\n');
20534
21146
  }
@@ -20727,11 +21339,11 @@ class Route {
20727
21339
  }
20728
21340
 
20729
21341
  class Result {
20730
- constructor(query, earliestArrivals, earliestArrivalsPerRound, stopsIndex) {
21342
+ constructor(query, routingState, stopsIndex, timetable) {
20731
21343
  this.query = query;
20732
- this.earliestArrivals = earliestArrivals;
20733
- this.earliestArrivalsPerRound = earliestArrivalsPerRound;
21344
+ this.routingState = routingState;
20734
21345
  this.stopsIndex = stopsIndex;
21346
+ this.timetable = timetable;
20735
21347
  }
20736
21348
  /**
20737
21349
  * Reconstructs the best route to a stop.
@@ -20741,17 +21353,18 @@ class Result {
20741
21353
  * @returns a route to the destination stop if it exists.
20742
21354
  */
20743
21355
  bestRoute(to) {
20744
- var _a, _b, _c;
21356
+ var _a;
20745
21357
  const destinationList = to instanceof Set
20746
21358
  ? Array.from(to)
20747
21359
  : to
20748
21360
  ? [to]
20749
21361
  : Array.from(this.query.to);
20750
21362
  const destinations = destinationList.flatMap((destination) => this.stopsIndex.equivalentStops(destination));
21363
+ // find the first reached destination
20751
21364
  let fastestDestination = undefined;
20752
21365
  let fastestTime = undefined;
20753
21366
  for (const destination of destinations) {
20754
- const arrivalTime = this.earliestArrivals.get(destination.id);
21367
+ const arrivalTime = this.routingState.earliestArrivals.get(destination.id);
20755
21368
  if (arrivalTime !== undefined) {
20756
21369
  if (fastestTime === undefined ||
20757
21370
  arrivalTime.arrival.isBefore(fastestTime.arrival)) {
@@ -20766,19 +21379,84 @@ class Result {
20766
21379
  const route = [];
20767
21380
  let currentStop = fastestDestination;
20768
21381
  let round = fastestTime.legNumber;
20769
- while (fastestTime.origin !== currentStop) {
20770
- const tripLeg = (_a = this.earliestArrivalsPerRound[round]) === null || _a === void 0 ? void 0 : _a.get(currentStop);
20771
- if (!(tripLeg === null || tripLeg === void 0 ? void 0 : tripLeg.leg)) {
20772
- 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);
21400
+ }
21401
+ else {
21402
+ break;
20773
21403
  }
20774
- route.unshift(tripLeg.leg);
20775
- currentStop = tripLeg.leg.from.id;
20776
- if ('route' in tripLeg.leg) {
21404
+ route.unshift(leg);
21405
+ currentStop = leg.from.id;
21406
+ if ('routeId' in edge) {
20777
21407
  round -= 1;
20778
21408
  }
20779
21409
  }
20780
21410
  return new Route(route);
20781
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
+ }
20782
21460
  /**
20783
21461
  * Returns the arrival time at any stop reachable in less time / transfers than the destination(s) of the query)
20784
21462
  *
@@ -20787,14 +21465,28 @@ class Result {
20787
21465
  * @returns The arrival time if the target stop is reachable, otherwise undefined.
20788
21466
  */
20789
21467
  arrivalAt(stop, maxTransfers) {
21468
+ var _a;
20790
21469
  const equivalentStops = this.stopsIndex.equivalentStops(stop);
20791
21470
  let earliestArrival = undefined;
20792
- const relevantArrivals = maxTransfers !== undefined
20793
- ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
20794
- this.earliestArrivalsPerRound[maxTransfers + 1]
20795
- : this.earliestArrivals;
20796
21471
  for (const equivalentStop of equivalentStops) {
20797
- 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
+ }
20798
21490
  if (arrivalTime !== undefined) {
20799
21491
  if (earliestArrival === undefined ||
20800
21492
  arrivalTime.arrival.isBefore(earliestArrival.arrival)) {
@@ -20808,9 +21500,9 @@ class Result {
20808
21500
 
20809
21501
  const UNREACHED = Time.infinity();
20810
21502
  /**
20811
- * A public transportation network router implementing the RAPTOR algorithm for
20812
- * efficient journey planning and routing. For more information on the RAPTOR
20813
- * 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:
20814
21506
  * https://www.microsoft.com/en-us/research/wp-content/uploads/2012/01/raptor_alenex.pdf
20815
21507
  */
20816
21508
  class Router {
@@ -20819,25 +21511,227 @@ class Router {
20819
21511
  this.stopsIndex = stopsIndex;
20820
21512
  }
20821
21513
  /**
20822
- * Evaluates possible transfers for a given query on a transport
20823
- * network, updating the earliest arrivals at various stops and marking new
20824
- * 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.
20825
21572
  */
20826
- considerTransfers(query, markedStops, arrivalsAtCurrentRound, earliestArrivals, round) {
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
21724
+ */
21725
+ considerTransfers(query, round, markedStops, routingState) {
20827
21726
  var _a, _b;
20828
21727
  const { options } = query;
21728
+ const arrivalsAtCurrentRound = routingState.graph[round];
20829
21729
  const newlyMarkedStops = new Set();
20830
- const markedStopsArray = Array.from(markedStops);
20831
- for (let i = 0; i < markedStopsArray.length; i++) {
20832
- const stop = markedStopsArray[i];
21730
+ for (const stop of markedStops) {
20833
21731
  const currentArrival = arrivalsAtCurrentRound.get(stop);
20834
- if (!currentArrival)
20835
- continue;
20836
21732
  // Skip transfers if the last leg was also a transfer
20837
- const previousLeg = currentArrival.leg;
20838
- if (previousLeg && !('route' in previousLeg)) {
21733
+ if (!currentArrival || 'type' in currentArrival)
20839
21734
  continue;
20840
- }
20841
21735
  const transfers = this.timetable.getTransfers(stop);
20842
21736
  for (let j = 0; j < transfers.length; j++) {
20843
21737
  const transfer = transfers[j];
@@ -20846,6 +21740,7 @@ class Router {
20846
21740
  transferTime = transfer.minTransferTime;
20847
21741
  }
20848
21742
  else if (transfer.type === 'IN_SEAT') {
21743
+ // TODO not needed anymore now that trip continuations are handled separately
20849
21744
  transferTime = Duration.zero();
20850
21745
  }
20851
21746
  else {
@@ -20854,32 +21749,22 @@ class Router {
20854
21749
  const arrivalAfterTransfer = currentArrival.arrival.plus(transferTime);
20855
21750
  const originalArrival = (_b = (_a = arrivalsAtCurrentRound.get(transfer.destination)) === null || _a === void 0 ? void 0 : _a.arrival) !== null && _b !== void 0 ? _b : UNREACHED;
20856
21751
  if (arrivalAfterTransfer.isBefore(originalArrival)) {
20857
- const origin = currentArrival.origin;
20858
21752
  arrivalsAtCurrentRound.set(transfer.destination, {
20859
21753
  arrival: arrivalAfterTransfer,
20860
- legNumber: round,
20861
- origin: origin,
20862
- leg: {
20863
- from: this.stopsIndex.findStopById(stop),
20864
- to: this.stopsIndex.findStopById(transfer.destination),
20865
- minTransferTime: transfer.minTransferTime,
20866
- type: transfer.type,
20867
- },
21754
+ from: stop,
21755
+ to: transfer.destination,
21756
+ minTransferTime: transfer.minTransferTime,
21757
+ type: transfer.type,
20868
21758
  });
20869
- earliestArrivals.set(transfer.destination, {
21759
+ routingState.earliestArrivals.set(transfer.destination, {
20870
21760
  arrival: arrivalAfterTransfer,
20871
21761
  legNumber: round,
20872
- origin: origin,
20873
21762
  });
20874
21763
  newlyMarkedStops.add(transfer.destination);
20875
21764
  }
20876
21765
  }
20877
21766
  }
20878
- const newlyMarkedStopsArray = Array.from(newlyMarkedStops);
20879
- for (let i = 0; i < newlyMarkedStopsArray.length; i++) {
20880
- const newStop = newlyMarkedStopsArray[i];
20881
- markedStops.add(newStop);
20882
- }
21767
+ return newlyMarkedStops;
20883
21768
  }
20884
21769
  /**
20885
21770
  * Finds the earliest arrival time at any stop from a given set of destinations.
@@ -20893,117 +21778,11 @@ class Router {
20893
21778
  let earliestArrivalAtAnyDestination = UNREACHED;
20894
21779
  for (let i = 0; i < destinations.length; i++) {
20895
21780
  const destination = destinations[i];
20896
- 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;
20897
21782
  earliestArrivalAtAnyDestination = Time.min(earliestArrivalAtAnyDestination, arrival);
20898
21783
  }
20899
21784
  return earliestArrivalAtAnyDestination;
20900
21785
  }
20901
- /**
20902
- * The main Raptor algorithm implementation.
20903
- *
20904
- * @param query The query containing the main parameters for the routing.
20905
- * @returns A result object containing data structures allowing to reconstruct routes and .
20906
- */
20907
- route(query) {
20908
- var _a, _b, _c, _d, _e;
20909
- const { from, to, departureTime, options } = query;
20910
- // Consider children or siblings of the "from" stop as potential origins
20911
- const origins = this.stopsIndex.equivalentStops(from);
20912
- // Consider children or siblings of the "to" stop(s) as potential destinations
20913
- const destinations = Array.from(to).flatMap((destination) => this.stopsIndex.equivalentStops(destination));
20914
- const earliestArrivals = new Map();
20915
- const earliestArrivalsWithoutAnyLeg = new Map();
20916
- const earliestArrivalsPerRound = [earliestArrivalsWithoutAnyLeg];
20917
- // Stops that have been improved at round k-1
20918
- const markedStops = new Set();
20919
- for (let i = 0; i < origins.length; i++) {
20920
- const originStop = origins[i];
20921
- markedStops.add(originStop.id);
20922
- earliestArrivals.set(originStop.id, {
20923
- arrival: departureTime,
20924
- legNumber: 0,
20925
- origin: originStop.id,
20926
- });
20927
- earliestArrivalsWithoutAnyLeg.set(originStop.id, {
20928
- arrival: departureTime,
20929
- legNumber: 0,
20930
- origin: originStop.id,
20931
- });
20932
- }
20933
- // on the first round we need to first consider transfers to discover all possible route origins
20934
- this.considerTransfers(query, markedStops, earliestArrivalsWithoutAnyLeg, earliestArrivals, 0);
20935
- for (let round = 1; round <= options.maxTransfers + 1; round++) {
20936
- const arrivalsAtCurrentRound = new Map();
20937
- earliestArrivalsPerRound.push(arrivalsAtCurrentRound);
20938
- const arrivalsAtPreviousRound = earliestArrivalsPerRound[round - 1];
20939
- // Routes that contain at least one stop reached with at least round - 1 legs
20940
- // together with corresponding hop on stop index (earliest marked stop)
20941
- const reachableRoutes = this.timetable.findReachableRoutes(markedStops, options.transportModes);
20942
- markedStops.clear();
20943
- const earliestArrivalAtAnyDestination = this.earliestArrivalAtAnyStop(earliestArrivals, destinations);
20944
- // for each route that can be reached with at least round - 1 trips
20945
- const reachableRoutesArray = Array.from(reachableRoutes.entries());
20946
- for (let i = 0; i < reachableRoutesArray.length; i++) {
20947
- const [route, hopOnStop] = reachableRoutesArray[i];
20948
- let currentTrip = undefined;
20949
- const startIndex = route.stopIndex(hopOnStop);
20950
- for (let j = startIndex; j < route.getNbStops(); j++) {
20951
- const currentStop = route.stops[j];
20952
- // If we're currently on a trip,
20953
- // check if arrival at the stop improves the earliest arrival time
20954
- if (currentTrip !== undefined) {
20955
- const currentArrivalTime = route.arrivalAt(currentStop, currentTrip.tripIndex);
20956
- const currentDropOffType = route.dropOffTypeAt(currentStop, currentTrip.tripIndex);
20957
- const earliestArrivalAtCurrentStop = (_b = (_a = earliestArrivals.get(currentStop)) === null || _a === void 0 ? void 0 : _a.arrival) !== null && _b !== void 0 ? _b : UNREACHED;
20958
- if (currentDropOffType !== 'NOT_AVAILABLE' &&
20959
- currentArrivalTime.isBefore(earliestArrivalAtCurrentStop) &&
20960
- currentArrivalTime.isBefore(earliestArrivalAtAnyDestination)) {
20961
- const bestHopOnDepartureTime = route.departureFrom(currentTrip.bestHopOnStop, currentTrip.tripIndex);
20962
- arrivalsAtCurrentRound.set(currentStop, {
20963
- arrival: currentArrivalTime,
20964
- legNumber: round,
20965
- origin: currentTrip.origin,
20966
- leg: {
20967
- from: this.stopsIndex.findStopById(currentTrip.bestHopOnStop),
20968
- to: this.stopsIndex.findStopById(currentStop),
20969
- departureTime: bestHopOnDepartureTime,
20970
- arrivalTime: currentArrivalTime,
20971
- route: this.timetable.getServiceRouteInfo(route),
20972
- },
20973
- });
20974
- earliestArrivals.set(currentStop, {
20975
- arrival: currentArrivalTime,
20976
- legNumber: round,
20977
- origin: currentTrip.origin,
20978
- });
20979
- markedStops.add(currentStop);
20980
- }
20981
- }
20982
- // check if we can board an earlier trip at the current stop
20983
- // if there was no current trip, find the first one reachable
20984
- const earliestArrivalOnPreviousRound = (_c = arrivalsAtPreviousRound.get(currentStop)) === null || _c === void 0 ? void 0 : _c.arrival;
20985
- if (earliestArrivalOnPreviousRound !== undefined &&
20986
- (currentTrip === undefined ||
20987
- earliestArrivalOnPreviousRound.isBefore(route.departureFrom(currentStop, currentTrip.tripIndex)) ||
20988
- earliestArrivalOnPreviousRound.equals(route.departureFrom(currentStop, currentTrip.tripIndex)))) {
20989
- const earliestTrip = route.findEarliestTrip(currentStop, earliestArrivalOnPreviousRound, currentTrip === null || currentTrip === void 0 ? void 0 : currentTrip.tripIndex);
20990
- if (earliestTrip !== undefined) {
20991
- currentTrip = {
20992
- tripIndex: earliestTrip,
20993
- // we need to keep track of the best hop-on stop to reconstruct the route at the end
20994
- bestHopOnStop: currentStop,
20995
- origin: (_e = (_d = arrivalsAtPreviousRound.get(currentStop)) === null || _d === void 0 ? void 0 : _d.origin) !== null && _e !== void 0 ? _e : currentStop,
20996
- };
20997
- }
20998
- }
20999
- }
21000
- }
21001
- this.considerTransfers(query, markedStops, arrivalsAtCurrentRound, earliestArrivals, round);
21002
- if (markedStops.size === 0)
21003
- break;
21004
- }
21005
- return new Result(query, earliestArrivals, earliestArrivalsPerRound, this.stopsIndex);
21006
- }
21007
21786
  }
21008
21787
 
21009
21788
  /**
@@ -21140,7 +21919,6 @@ const startRepl = (stopsPath, timetablePath) => {
21140
21919
  replServer.defineCommand('route', {
21141
21920
  help: 'Find a route using .route from <stationIdOrName> to <stationIdOrName> at <HH:mm> [with <N> transfers]',
21142
21921
  action(routeQuery) {
21143
- var _a;
21144
21922
  this.clearBufferedCommand();
21145
21923
  const parts = routeQuery.split(' ').filter(Boolean);
21146
21924
  const withTransfersIndex = parts.indexOf('with');
@@ -21196,12 +21974,13 @@ const startRepl = (stopsPath, timetablePath) => {
21196
21974
  console.log(`Destination not reachable`);
21197
21975
  }
21198
21976
  else {
21199
- 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}.`);
21200
21978
  }
21201
21979
  const bestRoute = result.bestRoute(toStop.sourceStopId);
21202
21980
  if (bestRoute) {
21203
21981
  console.log(`Found route from ${fromStop.name} to ${toStop.name}:`);
21204
21982
  console.log(bestRoute.toString());
21983
+ console.log(bestRoute.asJson());
21205
21984
  }
21206
21985
  else {
21207
21986
  console.log('No route found');
@@ -21277,6 +22056,203 @@ const startRepl = (stopsPath, timetablePath) => {
21277
22056
  this.displayPrompt();
21278
22057
  },
21279
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
+ });
21280
22256
  };
21281
22257
 
21282
22258
  const program = new Command();