minotor 11.1.2 → 11.2.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 (71) hide show
  1. package/.cspell.json +7 -1
  2. package/CHANGELOG.md +3 -3
  3. package/README.md +111 -86
  4. package/dist/cli/perf.d.ts +57 -18
  5. package/dist/cli.mjs +1371 -342
  6. package/dist/cli.mjs.map +1 -1
  7. package/dist/parser.cjs.js +57 -4
  8. package/dist/parser.cjs.js.map +1 -1
  9. package/dist/parser.esm.js +57 -4
  10. package/dist/parser.esm.js.map +1 -1
  11. package/dist/router.cjs.js +1 -1
  12. package/dist/router.cjs.js.map +1 -1
  13. package/dist/router.d.ts +5 -5
  14. package/dist/router.esm.js +1 -1
  15. package/dist/router.esm.js.map +1 -1
  16. package/dist/router.umd.js +1 -1
  17. package/dist/router.umd.js.map +1 -1
  18. package/dist/routing/__tests__/access.test.d.ts +1 -0
  19. package/dist/routing/__tests__/plainRouter.test.d.ts +1 -0
  20. package/dist/routing/__tests__/rangeResult.test.d.ts +1 -0
  21. package/dist/routing/__tests__/rangeRouter.test.d.ts +1 -0
  22. package/dist/routing/__tests__/rangeState.test.d.ts +1 -0
  23. package/dist/routing/__tests__/raptor.test.d.ts +1 -0
  24. package/dist/routing/__tests__/state.test.d.ts +1 -0
  25. package/dist/routing/access.d.ts +55 -0
  26. package/dist/routing/plainRouter.d.ts +21 -0
  27. package/dist/routing/plotter.d.ts +9 -0
  28. package/dist/routing/query.d.ts +132 -13
  29. package/dist/routing/rangeResult.d.ts +155 -0
  30. package/dist/routing/rangeRouter.d.ts +24 -0
  31. package/dist/routing/rangeState.d.ts +83 -0
  32. package/dist/routing/raptor.d.ts +96 -0
  33. package/dist/routing/result.d.ts +27 -7
  34. package/dist/routing/route.d.ts +5 -21
  35. package/dist/routing/router.d.ts +20 -91
  36. package/dist/routing/state.d.ts +92 -17
  37. package/dist/timetable/route.d.ts +8 -0
  38. package/dist/timetable/timetable.d.ts +17 -1
  39. package/package.json +1 -1
  40. package/src/__e2e__/benchmark.json +18 -0
  41. package/src/__e2e__/router.test.ts +461 -127
  42. package/src/cli/minotor.ts +39 -3
  43. package/src/cli/perf.ts +324 -60
  44. package/src/cli/repl.ts +96 -41
  45. package/src/router.ts +11 -3
  46. package/src/routing/__tests__/access.test.ts +294 -0
  47. package/src/routing/__tests__/plainRouter.test.ts +1633 -0
  48. package/src/routing/__tests__/plotter.test.ts +8 -8
  49. package/src/routing/__tests__/rangeResult.test.ts +273 -0
  50. package/src/routing/__tests__/rangeRouter.test.ts +472 -0
  51. package/src/routing/__tests__/rangeState.test.ts +246 -0
  52. package/src/routing/__tests__/raptor.test.ts +366 -0
  53. package/src/routing/__tests__/result.test.ts +27 -27
  54. package/src/routing/__tests__/route.test.ts +28 -0
  55. package/src/routing/__tests__/router.test.ts +75 -1587
  56. package/src/routing/__tests__/state.test.ts +78 -0
  57. package/src/routing/access.ts +144 -0
  58. package/src/routing/plainRouter.ts +60 -0
  59. package/src/routing/plotter.ts +53 -6
  60. package/src/routing/query.ts +116 -13
  61. package/src/routing/rangeResult.ts +292 -0
  62. package/src/routing/rangeRouter.ts +167 -0
  63. package/src/routing/rangeState.ts +150 -0
  64. package/src/routing/raptor.ts +416 -0
  65. package/src/routing/result.ts +68 -26
  66. package/src/routing/route.ts +15 -53
  67. package/src/routing/router.ts +40 -480
  68. package/src/routing/state.ts +191 -32
  69. package/src/timetable/__tests__/timetable.test.ts +373 -0
  70. package/src/timetable/route.ts +16 -4
  71. package/src/timetable/timetable.ts +54 -1
package/dist/cli.mjs CHANGED
@@ -17287,6 +17287,17 @@ let Route$1 = class Route {
17287
17287
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
17288
17288
  return this.stopTimes[(offset + stopIndex) * 2];
17289
17289
  }
17290
+ /**
17291
+ * Hot-path variant of {@link departureFrom} that accepts a precomputed base offset.
17292
+ *
17293
+ * @param stopIndex - The index of the stop in the route.
17294
+ * @param offset - Precomputed value from {@link tripStopOffset}.
17295
+ * @returns The departure time at the specified stop.
17296
+ */
17297
+ departureAtOffset(stopIndex, offset) {
17298
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
17299
+ return this.stopTimes[(offset + stopIndex) * 2 + 1];
17300
+ }
17290
17301
  /**
17291
17302
  * Hot-path variant of {@link dropOffTypeAt} that accepts a precomputed base offset.
17292
17303
  *
@@ -17326,8 +17337,8 @@ let Route$1 = class Route {
17326
17337
  */
17327
17338
  pickUpTypeFrom(stopIndex, tripIndex) {
17328
17339
  const globalIndex = tripIndex * this.stops.length + stopIndex;
17329
- const byteIndex = Math.floor(globalIndex / 2);
17330
- const isSecondPair = globalIndex % 2 === 1;
17340
+ const byteIndex = globalIndex >> 1;
17341
+ const isSecondPair = (globalIndex & 1) === 1;
17331
17342
  const byte = this.pickupDropOffTypes[byteIndex];
17332
17343
  if (byte === undefined) {
17333
17344
  throw new Error(`Pick up type not found for stop ${this.stopId(stopIndex)} (${stopIndex}) at trip index ${tripIndex} in route ${this.serviceRouteId}`);
@@ -17346,8 +17357,8 @@ let Route$1 = class Route {
17346
17357
  */
17347
17358
  dropOffTypeAt(stopIndex, tripIndex) {
17348
17359
  const globalIndex = tripIndex * this.stops.length + stopIndex;
17349
- const byteIndex = Math.floor(globalIndex / 2);
17350
- const isSecondPair = globalIndex % 2 === 1;
17360
+ const byteIndex = globalIndex >> 1;
17361
+ const isSecondPair = (globalIndex & 1) === 1;
17351
17362
  const byte = this.pickupDropOffTypes[byteIndex];
17352
17363
  if (byte === undefined) {
17353
17364
  throw new Error(`Drop off type not found for stop ${this.stopId(stopIndex)} (${stopIndex}) at trip index ${tripIndex} in route ${this.serviceRouteId}`);
@@ -17947,6 +17958,47 @@ class Timetable {
17947
17958
  }
17948
17959
  return false;
17949
17960
  }
17961
+ /**
17962
+ * Finds the first trip on `route` at `stopIndex` that can be boarded, starting
17963
+ * from `earliestTrip` and respecting pickup availability, transfer guarantees,
17964
+ * and minimum transfer times.
17965
+ *
17966
+ * @param stopIndex Stop at which boarding is attempted.
17967
+ * @param route The route to search.
17968
+ * @param earliestTrip First trip index to consider.
17969
+ * @param after Earliest time after which boarding is allowed.
17970
+ * @param beforeTrip Exclusive upper bound on the trip index (omit to search all).
17971
+ * @param fromTripStop The alighted trip stop when transferring from another trip;
17972
+ * `undefined` when boarding from a walk or origin.
17973
+ * @param transferTime Minimum transfer time required between trips.
17974
+ * @returns The index of the first boardable trip, or `undefined` if none found.
17975
+ */
17976
+ findFirstBoardableTrip(stopIndex, route, earliestTrip, after = TIME_ORIGIN, beforeTrip, fromTripStop, transferTime = DURATION_ZERO) {
17977
+ const nbTrips = route.getNbTrips();
17978
+ for (let t = earliestTrip; t < (beforeTrip !== null && beforeTrip !== void 0 ? beforeTrip : nbTrips); t++) {
17979
+ const pickup = route.pickUpTypeFrom(stopIndex, t);
17980
+ if (pickup === NOT_AVAILABLE) {
17981
+ continue;
17982
+ }
17983
+ if (fromTripStop === undefined) {
17984
+ return t;
17985
+ }
17986
+ const isGuaranteed = this.isTripTransferGuaranteed(fromTripStop, {
17987
+ stopIndex,
17988
+ routeId: route.id,
17989
+ tripIndex: t,
17990
+ });
17991
+ if (isGuaranteed) {
17992
+ return t;
17993
+ }
17994
+ const departure = route.departureFrom(stopIndex, t);
17995
+ const requiredTime = after + transferTime;
17996
+ if (departure >= requiredTime) {
17997
+ return t;
17998
+ }
17999
+ }
18000
+ return undefined;
18001
+ }
17950
18002
  /**
17951
18003
  * Retrieves all guaranteed trip transfer options available at the specified stop for a given trip.
17952
18004
  *
@@ -21124,6 +21176,12 @@ function isVehicleEdge(edge) {
21124
21176
  function isTransferEdge(edge) {
21125
21177
  return 'from' in edge && 'to' in edge && 'type' in edge;
21126
21178
  }
21179
+ /**
21180
+ * Type guard to check if an edge is an AccessEdge (walking access leg).
21181
+ */
21182
+ function isAccessEdge(edge) {
21183
+ return 'from' in edge && 'duration' in edge;
21184
+ }
21127
21185
  /**
21128
21186
  * Helper class for building DOT graph syntax.
21129
21187
  */
@@ -21219,6 +21277,12 @@ class Plotter {
21219
21277
  transferEdgeNodeId(fromStopId, toStopId, round) {
21220
21278
  return `e_${fromStopId}_${toStopId}_${round}`;
21221
21279
  }
21280
+ /**
21281
+ * Generates a unique node ID for a walking access edge oval.
21282
+ */
21283
+ accessEdgeNodeId(fromStopId, toStopId) {
21284
+ return `access_${fromStopId}_${toStopId}`;
21285
+ }
21222
21286
  /**
21223
21287
  * Generates a unique node ID for a continuation edge oval.
21224
21288
  */
@@ -21342,6 +21406,22 @@ class Plotter {
21342
21406
  ` "${routeOvalId}" -> "${toNodeId}" [color="${roundColor}"];`,
21343
21407
  ];
21344
21408
  }
21409
+ /**
21410
+ * Creates a walking access leg as a dashed oval connecting the query origin
21411
+ * to the initial boarding stop.
21412
+ */
21413
+ createAccessEdge(edge) {
21414
+ const fromNodeId = this.stationNodeId(edge.from);
21415
+ const toNodeId = this.stationNodeId(edge.to);
21416
+ const color = DOT_CONFIG.colors.defaultRound;
21417
+ const ovalId = this.accessEdgeNodeId(edge.from, edge.to);
21418
+ const label = `Walk\\n${durationToString(edge.duration)}`;
21419
+ return [
21420
+ ` "${ovalId}" [label="${label}" shape=oval style="dashed,filled" fillcolor="white" color="${color}"];`,
21421
+ ` "${fromNodeId}" -> "${ovalId}" [color="${color}" style="dashed"];`,
21422
+ ` "${ovalId}" -> "${toNodeId}" [color="${color}" style="dashed"];`,
21423
+ ];
21424
+ }
21345
21425
  /**
21346
21426
  * Creates a transfer edge with transfer information oval in the middle.
21347
21427
  */
@@ -21426,6 +21506,12 @@ class Plotter {
21426
21506
  if (toStopId)
21427
21507
  stations.add(toStopId);
21428
21508
  }
21509
+ else if (isAccessEdge(edge)) {
21510
+ // Ensure the query origin (edge.from) is always collected even when
21511
+ // its own OriginNode hasn't been processed yet in this iteration.
21512
+ stations.add(edge.from);
21513
+ stations.add(edge.to);
21514
+ }
21429
21515
  }
21430
21516
  }
21431
21517
  return stations;
@@ -21456,14 +21542,18 @@ class Plotter {
21456
21542
  const roundEdges = graph[round];
21457
21543
  if (!roundEdges)
21458
21544
  continue;
21459
- // Skip round 0 as it contains only origin nodes
21460
- if (round === 0) {
21461
- continue;
21462
- }
21463
21545
  for (let stopId = 0; stopId < roundEdges.length; stopId++) {
21464
21546
  const edge = roundEdges[stopId];
21465
21547
  if (edge === undefined)
21466
21548
  continue;
21549
+ if (round === 0) {
21550
+ // Round 0 holds OriginNodes (no edge to draw) and AccessEdges
21551
+ // (walking legs from the query origin to the first boarding stop).
21552
+ if (isAccessEdge(edge)) {
21553
+ edges.push(...this.createAccessEdge(edge));
21554
+ }
21555
+ continue;
21556
+ }
21467
21557
  if (isVehicleEdge(edge)) {
21468
21558
  edges.push(...this.createVehicleEdge(edge, round));
21469
21559
  if (edge.continuationOf) {
@@ -21500,6 +21590,13 @@ class Plotter {
21500
21590
  }
21501
21591
  }
21502
21592
 
21593
+ /**
21594
+ * A routing query for standard RAPTOR.
21595
+ *
21596
+ * Finds the earliest-arrival journey from `from` to `to` for a single
21597
+ * departure time. Use {@link RangeQuery} (and `router.rangeRoute()`) when
21598
+ * you want all Pareto-optimal journeys within a departure-time window.
21599
+ */
21503
21600
  class Query {
21504
21601
  constructor(builder) {
21505
21602
  this.from = builder.fromValue;
@@ -21525,16 +21622,16 @@ Query.Builder = class {
21525
21622
  return this;
21526
21623
  }
21527
21624
  /**
21528
- * Sets the destination stops(s), routing will stop when all the provided stops are reached.
21625
+ * Sets the destination stop(s).
21626
+ * Routing stops as soon as all provided stops have been reached.
21529
21627
  */
21530
21628
  to(to) {
21531
21629
  this.toValue = to instanceof Set ? to : new Set([to]);
21532
21630
  return this;
21533
21631
  }
21534
21632
  /**
21535
- * Sets the departure time for the query as minutes since midnight.
21536
- * Note that the router will favor routes that depart shortly after the provided departure time,
21537
- * even if a later route might arrive at the same time.
21633
+ * Sets the departure time in minutes from midnight.
21634
+ * The router favours trips departing shortly after this time.
21538
21635
  */
21539
21636
  departureTime(departureTime) {
21540
21637
  this.departureTimeValue = departureTime;
@@ -21548,24 +21645,77 @@ Query.Builder = class {
21548
21645
  return this;
21549
21646
  }
21550
21647
  /**
21551
- * Sets the minimum transfer time (in minutes)
21552
- * to use when no transfer time is provided in the data.
21648
+ * Sets the fallback minimum transfer time (in minutes) used when the
21649
+ * timetable data does not specify one for a particular transfer.
21553
21650
  */
21554
21651
  minTransferTime(minTransferTime) {
21555
21652
  this.optionsValue.minTransferTime = minTransferTime;
21556
21653
  return this;
21557
21654
  }
21558
21655
  /**
21559
- * Sets the transport modes to consider.
21656
+ * Restricts routing to the given transport modes.
21560
21657
  */
21561
21658
  transportModes(transportModes) {
21562
21659
  this.optionsValue.transportModes = transportModes;
21563
21660
  return this;
21564
21661
  }
21662
+ /**
21663
+ * Sets the maximum time (in minutes) the traveler is willing to wait at
21664
+ * the first boarding stop before the first transit vehicle departs.
21665
+ *
21666
+ * When set, any trip that would require waiting longer than this duration
21667
+ * after arriving at the stop is not considered for the first boarding leg.
21668
+ */
21669
+ maxInitialWaitingTime(maxInitialWaitingTime) {
21670
+ this.optionsValue.maxInitialWaitingTime = maxInitialWaitingTime;
21671
+ return this;
21672
+ }
21565
21673
  build() {
21566
21674
  return new Query(this);
21567
21675
  }
21568
21676
  };
21677
+ /**
21678
+ * A routing query for Range RAPTOR.
21679
+ *
21680
+ * Extends {@link Query} with a required `lastDepartureTime` that defines the
21681
+ * upper bound of the departure-time window. `router.rangeRoute()` returns
21682
+ * all Pareto-optimal journeys departing in
21683
+ * `[departureTime, lastDepartureTime]`.
21684
+ *
21685
+ */
21686
+ class RangeQuery extends Query {
21687
+ constructor(builder) {
21688
+ super(builder);
21689
+ this.lastDepartureTime = builder.lastDepartureTimeValue;
21690
+ this.rangeOptions = builder.rangeOptionsValue;
21691
+ }
21692
+ }
21693
+ RangeQuery.Builder = class extends Query.Builder {
21694
+ constructor() {
21695
+ super(...arguments);
21696
+ this.rangeOptionsValue = {
21697
+ optimizeBeyondLatestDeparture: true,
21698
+ };
21699
+ }
21700
+ /**
21701
+ * Sets the upper bound of the departure-time window.
21702
+ */
21703
+ lastDepartureTime(time) {
21704
+ this.lastDepartureTimeValue = time;
21705
+ return this;
21706
+ }
21707
+ /**
21708
+ * Overrides individual Range RAPTOR options.
21709
+ * Unspecified fields keep their defaults.
21710
+ */
21711
+ rangeOptions(options) {
21712
+ this.rangeOptionsValue = Object.assign(Object.assign({}, this.rangeOptionsValue), options);
21713
+ return this;
21714
+ }
21715
+ build() {
21716
+ return new RangeQuery(this);
21717
+ }
21718
+ };
21569
21719
 
21570
21720
  /**
21571
21721
  * Represents a resolved route consisting of multiple legs,
@@ -21582,15 +21732,15 @@ class Route {
21582
21732
  * @throws If no vehicle leg is found in the route.
21583
21733
  */
21584
21734
  departureTime() {
21585
- let cumulativeTransferTime = DURATION_ZERO;
21735
+ let cumulativeAccessTime = DURATION_ZERO;
21586
21736
  for (let i = 0; i < this.legs.length; i++) {
21587
21737
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
21588
21738
  const leg = this.legs[i];
21589
21739
  if ('departureTime' in leg) {
21590
- return leg.departureTime - cumulativeTransferTime;
21740
+ return leg.departureTime - cumulativeAccessTime;
21591
21741
  }
21592
- if ('minTransferTime' in leg && leg.minTransferTime) {
21593
- cumulativeTransferTime += leg.minTransferTime;
21742
+ if ('duration' in leg && leg.duration) {
21743
+ cumulativeAccessTime += leg.duration;
21594
21744
  }
21595
21745
  }
21596
21746
  throw new Error('No vehicle leg found in route');
@@ -21641,6 +21791,9 @@ class Route {
21641
21791
  const transferDetails = 'type' in leg && !('route' in leg)
21642
21792
  ? `Transfer: ${leg.type}${leg.minTransferTime ? `, Minimum Transfer Time: ${durationToString(leg.minTransferTime)}` : ''}`
21643
21793
  : '';
21794
+ const accessDetails = 'duration' in leg
21795
+ ? `Access duration: ${durationToString(leg.duration)}`
21796
+ : '';
21644
21797
  const travelDetails = 'route' in leg && 'departureTime' in leg && 'arrivalTime' in leg
21645
21798
  ? `Route: ${leg.route.type} ${leg.route.name}, Departure: ${timeToString(leg.departureTime)}, Arrival: ${timeToString(leg.arrivalTime)}`
21646
21799
  : '';
@@ -21649,6 +21802,7 @@ class Route {
21649
21802
  ` ${fromStop}`,
21650
21803
  ` ${toStop}`,
21651
21804
  transferDetails ? ` ${transferDetails}` : '',
21805
+ accessDetails ? ` ${accessDetails}` : '',
21652
21806
  travelDetails ? ` ${travelDetails}` : '',
21653
21807
  ]
21654
21808
  .filter((line) => line.trim() !== '')
@@ -21656,32 +21810,6 @@ class Route {
21656
21810
  })
21657
21811
  .join('\n');
21658
21812
  }
21659
- /**
21660
- * Generates a concise JSON representation of the route.
21661
- * This is particularly useful for generating regression tests
21662
- * to verify the correctness of route calculations.
21663
- *
21664
- * @returns A JSON representation of the route.
21665
- */
21666
- asJson() {
21667
- const jsonLegs = this.legs.map((leg) => {
21668
- if ('route' in leg) {
21669
- return {
21670
- from: leg.from.id,
21671
- to: leg.to.id,
21672
- departure: timeToString(leg.departureTime),
21673
- arrival: timeToString(leg.arrivalTime),
21674
- route: leg.route,
21675
- };
21676
- }
21677
- else {
21678
- return Object.assign({ from: leg.from.id, to: leg.to.id, type: leg.type }, (leg.minTransferTime !== undefined && {
21679
- minTransferTime: leg.minTransferTime,
21680
- }));
21681
- }
21682
- });
21683
- return jsonLegs;
21684
- }
21685
21813
  }
21686
21814
 
21687
21815
  const pickUpDropOffTypeMap = [
@@ -21706,15 +21834,35 @@ const toPickupDropOffType = (rawType) => {
21706
21834
  return type;
21707
21835
  };
21708
21836
  class Result {
21709
- constructor(query, routingState, stopsIndex, timetable) {
21710
- this.query = query;
21837
+ constructor(destinations, routingState, stopsIndex, timetable) {
21838
+ this.destinations = destinations;
21711
21839
  this.routingState = routingState;
21712
21840
  this.stopsIndex = stopsIndex;
21713
21841
  this.timetable = timetable;
21714
21842
  }
21843
+ /**
21844
+ * Expands a target stop or stop set to all equivalent concrete stop IDs.
21845
+ *
21846
+ * When `to` is omitted, defaults to the resolved destinations stored on this
21847
+ * result.
21848
+ *
21849
+ * Equivalent stops are expanded here so destination handling has a single
21850
+ * source of truth shared by route reconstruction and arrival lookups.
21851
+ */
21852
+ expandDestinations(to) {
21853
+ const targets = to instanceof Set ? to : to !== undefined ? [to] : this.destinations;
21854
+ const expanded = new Set();
21855
+ for (const target of targets) {
21856
+ for (const equivalentStop of this.stopsIndex.equivalentStops(target)) {
21857
+ expanded.add(equivalentStop.id);
21858
+ }
21859
+ }
21860
+ return expanded;
21861
+ }
21715
21862
  /**
21716
21863
  * Reconstructs the best route to a stop by SourceStopId.
21717
- * (to any stop reachable in less time / transfers than the destination(s) of the query)
21864
+ * (to any stop reachable in less time / transfers than this result's
21865
+ * destination set)
21718
21866
  *
21719
21867
  * @param to The destination stop by SourceStopId.
21720
21868
  * @returns a route to the destination stop if it exists.
@@ -21735,29 +21883,28 @@ class Result {
21735
21883
  }
21736
21884
  /**
21737
21885
  * Reconstructs the best route to a stop.
21738
- * (to any stop reachable in less time / transfers than the destination(s) of the query)
21886
+ * (to any stop reachable in less time / transfers than this result's
21887
+ * destination set)
21739
21888
  *
21740
- * @param to The destination stop. Defaults to the destination of the original query.
21889
+ * @param to The destination stop. Defaults to this result's resolved
21890
+ * destinations.
21741
21891
  * @returns a route to the destination stop if it exists.
21742
21892
  */
21743
21893
  bestRoute(to) {
21744
21894
  var _a;
21745
- const destinationIterable = to instanceof Set ? to : to ? [to] : this.query.to;
21895
+ const destinationStops = this.expandDestinations(to);
21746
21896
  // Find the fastest-reached destination across all equivalent stops.
21747
21897
  let fastestDestination = undefined;
21748
21898
  let fastestArrivalTime = undefined;
21749
21899
  let fastestLegNumber = undefined;
21750
- for (const sourceDestination of destinationIterable) {
21751
- const equivalentStops = this.stopsIndex.equivalentStops(sourceDestination);
21752
- for (const destination of equivalentStops) {
21753
- const arrivalData = this.routingState.getArrival(destination.id);
21754
- if (arrivalData !== undefined &&
21755
- (fastestArrivalTime === undefined ||
21756
- arrivalData.arrival < fastestArrivalTime)) {
21757
- fastestDestination = destination.id;
21758
- fastestArrivalTime = arrivalData.arrival;
21759
- fastestLegNumber = arrivalData.legNumber;
21760
- }
21900
+ for (const destination of destinationStops) {
21901
+ const arrivalData = this.routingState.getArrival(destination);
21902
+ if (arrivalData !== undefined &&
21903
+ (fastestArrivalTime === undefined ||
21904
+ arrivalData.arrival < fastestArrivalTime)) {
21905
+ fastestDestination = destination;
21906
+ fastestArrivalTime = arrivalData.arrival;
21907
+ fastestLegNumber = arrivalData.legNumber;
21761
21908
  }
21762
21909
  }
21763
21910
  if (fastestDestination === undefined || fastestLegNumber === undefined) {
@@ -21768,9 +21915,11 @@ class Result {
21768
21915
  let currentStop = fastestDestination;
21769
21916
  let round = fastestLegNumber;
21770
21917
  let previousVehicleEdge;
21771
- while (round > 0) {
21918
+ while (round >= 0) {
21772
21919
  const edge = (_a = this.routingState.graph[round]) === null || _a === void 0 ? void 0 : _a[currentStop];
21773
21920
  if (!edge) {
21921
+ if (round === 0)
21922
+ break;
21774
21923
  throw new Error(`No edge arriving at stop ${currentStop} at round ${round}`);
21775
21924
  }
21776
21925
  let leg;
@@ -21815,6 +21964,10 @@ class Result {
21815
21964
  leg = this.buildTransferLeg(edge);
21816
21965
  previousVehicleEdge = undefined;
21817
21966
  }
21967
+ else if ('duration' in edge) {
21968
+ leg = this.buildAccessLeg(edge);
21969
+ previousVehicleEdge = undefined;
21970
+ }
21818
21971
  else {
21819
21972
  break;
21820
21973
  }
@@ -21876,6 +22029,21 @@ class Result {
21876
22029
  type: edge.type,
21877
22030
  };
21878
22031
  }
22032
+ /**
22033
+ * Builds a transfer leg from a transfer edge.
22034
+ *
22035
+ * @param edge Transfer edge representing a walking connection between stops
22036
+ * @returns A transfer leg with from/to stops and transfer details
22037
+ */
22038
+ buildAccessLeg(edge) {
22039
+ return {
22040
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
22041
+ from: this.stopsIndex.findStopById(edge.from),
22042
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
22043
+ to: this.stopsIndex.findStopById(edge.to),
22044
+ duration: edge.duration,
22045
+ };
22046
+ }
21879
22047
  /**
21880
22048
  * Builds a guaranteed transfer leg between two consecutive vehicle legs.
21881
22049
  *
@@ -21899,7 +22067,8 @@ class Result {
21899
22067
  };
21900
22068
  }
21901
22069
  /**
21902
- * Returns the arrival time at any stop reachable in less time / transfers than the destination(s) of the query)
22070
+ * Returns the arrival time at any stop reachable in less time / transfers
22071
+ * than this result's destination set.
21903
22072
  *
21904
22073
  * @param stop The target stop for which to return the arrival time.
21905
22074
  * @param maxTransfers The optional maximum number of transfers allowed.
@@ -21941,6 +22110,118 @@ class Result {
21941
22110
  }
21942
22111
  }
21943
22112
 
22113
+ /**
22114
+ * Collects access paths from a query origin and resolves the set of
22115
+ * distinct departure-time slots for Range RAPTOR.
22116
+ */
22117
+ class AccessFinder {
22118
+ constructor(timetable, stopsIndex) {
22119
+ this.timetable = timetable;
22120
+ this.stopsIndex = stopsIndex;
22121
+ }
22122
+ /**
22123
+ * Returns every initial access path from the query origin: equivalent stops
22124
+ * (no duration) plus every stop reachable via a single timed walking transfer
22125
+ * (REQUIRES_MINIMAL_TIME), keeping the shortest walk when multiple origins
22126
+ * can reach the same stop.
22127
+ *
22128
+ * @param origin Origin stop ID.
22129
+ * @param fallbackMinTransferTime Transfer time used when a walking transfer
22130
+ * has no explicit `minTransferTime` in the timetable data.
22131
+ */
22132
+ collectAccessPaths(queryOrigin, fallbackMinTransferTime) {
22133
+ var _a;
22134
+ const equivalentOrigins = this.stopsIndex
22135
+ .equivalentStops(queryOrigin)
22136
+ .map((stop) => stop.id);
22137
+ const accessPaths = new Map();
22138
+ for (const origin of equivalentOrigins) {
22139
+ const existingAccess = accessPaths.get(origin);
22140
+ if (existingAccess === undefined || existingAccess.duration > 0) {
22141
+ accessPaths.set(origin, {
22142
+ fromStopId: origin,
22143
+ toStopId: origin,
22144
+ duration: 0,
22145
+ });
22146
+ }
22147
+ for (const transfer of this.timetable.getTransfers(origin)) {
22148
+ if (transfer.type === 'REQUIRES_MINIMAL_TIME') {
22149
+ const duration = (_a = transfer.minTransferTime) !== null && _a !== void 0 ? _a : fallbackMinTransferTime;
22150
+ const existingAccess = accessPaths.get(transfer.destination);
22151
+ // Keep the shortest walk to maximize the set of reachable trips.
22152
+ if (existingAccess === undefined ||
22153
+ (existingAccess.duration && duration < existingAccess.duration)) {
22154
+ accessPaths.set(transfer.destination, {
22155
+ fromStopId: origin,
22156
+ toStopId: transfer.destination,
22157
+ duration,
22158
+ });
22159
+ }
22160
+ }
22161
+ }
22162
+ }
22163
+ return Array.from(accessPaths.values());
22164
+ }
22165
+ /**
22166
+ * Collects all distinct origin departure times within `[from, to]`
22167
+ * (inclusive) and, for each slot, the specific access paths that directly
22168
+ * induce it — i.e. paths whose boarded stop has a boardable trip departing
22169
+ * at exactly `depTime + path.duration`.
22170
+ *
22171
+ * Returned array is sorted **latest-first**. The Range RAPTOR outer loop
22172
+ * seeds only the responsible paths for each slot, avoiding redundant
22173
+ * exploration of access stops whose boarding opportunities belong to a
22174
+ * later slot and whose journeys would therefore be dominated by it.
22175
+ *
22176
+
22177
+ * @param accessPaths Access paths from the origin to initial boarding stops.
22178
+ * @param from Earliest origin departure time (inclusive).
22179
+ * @param to Latest origin departure time (inclusive).
22180
+ */
22181
+ collectDepartureTimes(accessPaths, from, to) {
22182
+ // Map from origin-departure-time → the set of access paths that induce it.
22183
+ const slotMap = new Map();
22184
+ for (const path of accessPaths) {
22185
+ const { toStopId } = path;
22186
+ // Trips from this stop must depart in [from + duration, to + duration]
22187
+ // so that the corresponding origin departure (dep - duration) falls in
22188
+ // [from, to].
22189
+ const searchFrom = from + path.duration;
22190
+ const searchTo = to + path.duration;
22191
+ for (const route of this.timetable.routesPassingThrough(toStopId)) {
22192
+ for (const stopIndex of route.stopRouteIndices(toStopId)) {
22193
+ let tripIndex = route.findEarliestTrip(stopIndex, searchFrom);
22194
+ if (tripIndex === undefined)
22195
+ continue;
22196
+ const nbTrips = route.getNbTrips();
22197
+ while (tripIndex < nbTrips) {
22198
+ const dep = route.departureFrom(stopIndex, tripIndex);
22199
+ if (dep > searchTo)
22200
+ break;
22201
+ if (route.pickUpTypeFrom(stopIndex, tripIndex) !== NOT_AVAILABLE) {
22202
+ const t = dep - path.duration;
22203
+ let paths = slotMap.get(t);
22204
+ if (paths === undefined) {
22205
+ slotMap.set(t, (paths = new Set()));
22206
+ }
22207
+ paths.add(path);
22208
+ }
22209
+ tripIndex++;
22210
+ }
22211
+ }
22212
+ }
22213
+ }
22214
+ if (slotMap.size === 0)
22215
+ return [];
22216
+ // Sort descending so the outer loop processes latest departures first.
22217
+ const sorted = Array.from(slotMap.entries()).sort(([a], [b]) => b - a);
22218
+ return sorted.map(([depTime, paths]) => ({
22219
+ depTime,
22220
+ legs: Array.from(paths),
22221
+ }));
22222
+ }
22223
+ }
22224
+
21944
22225
  /**
21945
22226
  * Sentinel value used in the internal arrival-time array to mark stops not yet reached.
21946
22227
  * 0xFFFF = 65 535 minutes ≈ 45.5 days, safely beyond any realistic transit arrival time.
@@ -21950,31 +22231,64 @@ const UNREACHED_TIME = 0xffff;
21950
22231
  * Encapsulates all mutable state for a single RAPTOR routing query.
21951
22232
  */
21952
22233
  class RoutingState {
22234
+ constructor(departureTime, destinations, accessPaths, nbStops, maxRounds = 0) {
22235
+ /**
22236
+ * Cached best arrival time at any destination stop, kept up-to-date by
22237
+ * {@link updateArrival} so that destination pruning is always O(1).
22238
+ */
22239
+ this._destinationBest = UNREACHED_TIME;
22240
+ /**
22241
+ * Every stop that has received an arrival improvement during the current run,
22242
+ * in the order the improvements occurred. Used by {@link resetFor} to clear
22243
+ * only the touched entries instead of scanning the entire array.
22244
+ */
22245
+ this.reachedStops = [];
22246
+ this.destinations = destinations;
22247
+ this.destinationSet = new Set(destinations);
22248
+ this.earliestArrivalTimes = new Uint16Array(nbStops).fill(UNREACHED_TIME);
22249
+ this.earliestArrivalLegs = new Uint8Array(nbStops);
22250
+ this.origins = []; // overwritten by seedAccessPaths below
22251
+ this.graph = [new Array(nbStops)];
22252
+ for (let r = 1; r <= maxRounds; r++) {
22253
+ this.graph.push(new Array(nbStops));
22254
+ }
22255
+ this.seedAccessPaths(departureTime, accessPaths);
22256
+ }
21953
22257
  /**
21954
- * Initializes the routing state for a fresh query.
21955
- *
21956
- * All stops start as unreached. Each origin is immediately recorded at the
21957
- * departure time with leg number 0, and a corresponding OriginNode is placed
21958
- * in round 0 of the graph.
21959
- *
21960
- * @param origins Stop IDs to depart from (may be several equivalent stops).
21961
- * @param destinations Stop IDs that count as the target of the query.
21962
- * @param departureTime Earliest departure time in minutes from midnight.
21963
- * @param nbStops Total number of stops in the timetable (sets array sizes).
22258
+ * Seeds round-0 arrivals and {@link origins} from a set of access paths.
22259
+ * Called by the constructor and by {@link resetFor}.
22260
+ * Assumes {@link earliestArrivalTimes} and {@link graph}[0] are already
22261
+ * allocated and in their "cleared" state (all entries at UNREACHED_TIME /
22262
+ * undefined) before this method runs.
21964
22263
  */
21965
- constructor(origins, destinations, departureTime, nbStops) {
21966
- this.origins = origins;
21967
- this.destinations = destinations;
21968
- const earliestArrivalTimes = new Uint16Array(nbStops).fill(UNREACHED_TIME);
21969
- const earliestArrivalLegs = new Uint8Array(nbStops); // zero-initialized = leg 0
21970
- const graph0 = new Array(nbStops);
21971
- for (const stop of origins) {
21972
- earliestArrivalTimes[stop] = departureTime;
21973
- graph0[stop] = { arrival: departureTime };
22264
+ seedAccessPaths(depTime, accessPaths) {
22265
+ const seededOrigins = new Set();
22266
+ for (const access of accessPaths) {
22267
+ const arrival = depTime + access.duration;
22268
+ const edge = access.duration === 0
22269
+ ? { stopId: access.fromStopId, arrival: depTime }
22270
+ : {
22271
+ arrival,
22272
+ from: access.fromStopId,
22273
+ to: access.toStopId,
22274
+ duration: access.duration,
22275
+ };
22276
+ const stop = access.toStopId;
22277
+ if (arrival < this.earliestArrivalTimes[stop]) {
22278
+ this.earliestArrivalTimes[stop] = arrival;
22279
+ this.graph[0][stop] = edge;
22280
+ }
22281
+ seededOrigins.add(stop);
22282
+ }
22283
+ for (const stop of seededOrigins) {
22284
+ this.reachedStops.push(stop);
22285
+ }
22286
+ this.origins = Array.from(seededOrigins);
22287
+ for (let i = 0; i < this.destinations.length; i++) {
22288
+ const t = this.earliestArrivalTimes[this.destinations[i]];
22289
+ if (t < this._destinationBest)
22290
+ this._destinationBest = t;
21974
22291
  }
21975
- this.earliestArrivalTimes = earliestArrivalTimes;
21976
- this.earliestArrivalLegs = earliestArrivalLegs;
21977
- this.graph = [graph0];
21978
22292
  }
21979
22293
  /** Total number of stops in the timetable */
21980
22294
  get nbStops() {
@@ -21987,17 +22301,96 @@ class RoutingState {
21987
22301
  arrivalTime(stop) {
21988
22302
  return this.earliestArrivalTimes[stop];
21989
22303
  }
22304
+ /**
22305
+ * Earliest arrival at any destination stop; {@link UNREACHED_TIME} if none
22306
+ * has been reached yet. Updated automatically by {@link updateArrival}. O(1).
22307
+ */
22308
+ get destinationBest() {
22309
+ return this._destinationBest;
22310
+ }
22311
+ /**
22312
+ * In standard RAPTOR the improvement bound is simply the per-run earliest
22313
+ * arrival; the `round` argument is ignored.
22314
+ */
22315
+ improvementBound(_round, stop) {
22316
+ return this.arrivalTime(stop);
22317
+ }
22318
+ /** No-op in standard RAPTOR — there are no shared cross-run labels to propagate. */
22319
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
22320
+ initRound(_round) { }
21990
22321
  /**
21991
22322
  * Records a new earliest arrival at a stop.
21992
-
21993
22323
  *
21994
22324
  * @param stop The stop that was reached.
21995
22325
  * @param time The arrival time in minutes from midnight.
21996
22326
  * @param leg The round number (number of transit legs taken so far).
21997
22327
  */
21998
22328
  updateArrival(stop, time, leg) {
22329
+ this.reachedStops.push(stop);
21999
22330
  this.earliestArrivalTimes[stop] = time;
22000
22331
  this.earliestArrivalLegs[stop] = leg;
22332
+ if (this.destinationSet.has(stop) && time < this._destinationBest) {
22333
+ this._destinationBest = time;
22334
+ }
22335
+ }
22336
+ /**
22337
+ * Resets this state for a new departure-time iteration **without
22338
+ * reallocating** the underlying arrays.
22339
+ *
22340
+ * Only the stops recorded in {@link reachedStops} are touched — all other
22341
+ * entries are already at their initial bound values.
22342
+ *
22343
+ * After this call the state is equivalent to a freshly constructed
22344
+ * {@link RoutingState} for the given `depTime` and `accessPaths`.
22345
+ *
22346
+ * @param depTime New origin departure time.
22347
+ * @param accessPaths Access legs for this departure-time slot.
22348
+ */
22349
+ resetFor(depTime, accessPaths) {
22350
+ for (const stop of this.reachedStops) {
22351
+ this.earliestArrivalTimes[stop] = UNREACHED_TIME;
22352
+ this.earliestArrivalLegs[stop] = 0;
22353
+ for (let r = 0; r < this.graph.length; r++) {
22354
+ this.graph[r][stop] = undefined;
22355
+ }
22356
+ }
22357
+ this.reachedStops.length = 0;
22358
+ this._destinationBest = UNREACHED_TIME;
22359
+ this.seedAccessPaths(depTime, accessPaths);
22360
+ }
22361
+ /**
22362
+ * Iterates over every stop that has been reached, yielding its stop ID,
22363
+ * earliest arrival time, and the number of legs taken to reach it.
22364
+ *
22365
+ * Unreached stops (those still at UNREACHED_TIME) are skipped entirely.
22366
+ *
22367
+ * @example
22368
+ * ```ts
22369
+ * for (const { stop, arrival, legNumber } of routingState.arrivals()) {
22370
+ * console.log(`Stop ${stop}: arrived at ${arrival} after ${legNumber} leg(s)`);
22371
+ * }
22372
+ * ```
22373
+ */
22374
+ *arrivals() {
22375
+ for (let stop = 0; stop < this.earliestArrivalTimes.length; stop++) {
22376
+ const time = this.earliestArrivalTimes[stop];
22377
+ if (time < UNREACHED_TIME) {
22378
+ yield {
22379
+ stop,
22380
+ arrival: time,
22381
+ legNumber: this.earliestArrivalLegs[stop],
22382
+ };
22383
+ }
22384
+ }
22385
+ }
22386
+ /**
22387
+ * Finds the earliest arrival time at any stop from a given set of destinations.
22388
+ *
22389
+ * @param routingState The routing state containing arrival times and destinations.
22390
+ * @returns The earliest arrival time among the provided destinations.
22391
+ */
22392
+ earliestArrivalAtAnyDestination() {
22393
+ return this._destinationBest;
22001
22394
  }
22002
22395
  /**
22003
22396
  * Returns the earliest arrival at a stop as an {@link Arrival} object,
@@ -22009,6 +22402,13 @@ class RoutingState {
22009
22402
  return undefined;
22010
22403
  return { arrival: time, legNumber: this.earliestArrivalLegs[stop] };
22011
22404
  }
22405
+ /**
22406
+ * Returns `true` if `stop` is one of the query's destination stops.
22407
+ * O(1) — backed by a `Set` built at construction time.
22408
+ */
22409
+ isDestination(stop) {
22410
+ return this.destinationSet.has(stop);
22411
+ }
22012
22412
  /**
22013
22413
  * Creates a {@link RoutingState} from fully-specified raw data.
22014
22414
  *
@@ -22027,7 +22427,11 @@ class RoutingState {
22027
22427
  * @internal For use in tests only.
22028
22428
  */
22029
22429
  static fromTestData({ nbStops, origins = [], destinations = [], arrivals = [], graph = [], }) {
22030
- const state = new RoutingState(origins, destinations, 0, nbStops);
22430
+ const state = new RoutingState(0, destinations, origins.map((stop) => ({
22431
+ fromStopId: stop,
22432
+ toStopId: stop,
22433
+ duration: 0,
22434
+ })), nbStops);
22031
22435
  // Replace the arrival arrays with freshly built ones so the constructor's
22032
22436
  // origin-seeding doesn't bleed into the test state.
22033
22437
  const earliestArrivalTimes = new Uint16Array(nbStops).fill(UNREACHED_TIME);
@@ -22038,6 +22442,14 @@ class RoutingState {
22038
22442
  }
22039
22443
  state.earliestArrivalTimes = earliestArrivalTimes;
22040
22444
  state.earliestArrivalLegs = earliestArrivalLegs;
22445
+ // Recompute _destinationBest from the test data since we bypassed updateArrival.
22446
+ // fromTestData is a static method of RoutingState, so private access is allowed.
22447
+ state._destinationBest = UNREACHED_TIME;
22448
+ for (const dest of destinations) {
22449
+ const t = earliestArrivalTimes[dest];
22450
+ if (t !== undefined && t < state._destinationBest)
22451
+ state._destinationBest = t;
22452
+ }
22041
22453
  // Convert the sparse per-round representation to dense arrays and replace
22042
22454
  // the graph in-place.
22043
22455
  const denseRounds = graph.map((round) => {
@@ -22052,115 +22464,526 @@ class RoutingState {
22052
22464
  }
22053
22465
  }
22054
22466
 
22055
- /**
22056
- * A public transportation router implementing the RAPTOR algorithm.
22057
- * For more information on the RAPTOR algorithm,
22058
- * refer to its detailed explanation in the research paper:
22059
- * https://www.microsoft.com/en-us/research/wp-content/uploads/2012/01/raptor_alenex.pdf
22060
- */
22061
- class Router {
22062
- constructor(timetable, stopsIndex) {
22467
+ class PlainRouter {
22468
+ constructor(timetable, stopsIndex, accessFinder, raptor) {
22063
22469
  this.timetable = timetable;
22064
22470
  this.stopsIndex = stopsIndex;
22471
+ this.accessFinder = accessFinder;
22472
+ this.raptor = raptor;
22065
22473
  }
22066
22474
  /**
22067
- * The main Raptor algorithm implementation.
22475
+ * Standard RAPTOR: finds the earliest-arrival journey from `query.from` to
22476
+ * `query.to` for the given departure time.
22068
22477
  *
22069
- * @param query The query containing the main parameters for the routing.
22070
- * @returns A result object containing data structures allowing to reconstruct routes and .
22478
+ * @param query The routing query.
22479
+ * @returns A {@link Result} that can reconstruct the best route and arrival times.
22071
22480
  */
22072
22481
  route(query) {
22073
- const routingState = this.initRoutingState(query);
22074
- const markedStops = new Set(routingState.origins);
22075
- // Initial transfer consideration for origins
22076
- const newlyMarkedStops = this.considerTransfers(query, 0, markedStops, routingState);
22077
- for (const newStop of newlyMarkedStops) {
22078
- markedStops.add(newStop);
22079
- }
22080
- for (let round = 1; round <= query.options.maxTransfers + 1; round++) {
22081
- const edgesAtCurrentRound = new Array(routingState.nbStops);
22082
- routingState.graph.push(edgesAtCurrentRound);
22083
- const reachableRoutes = this.timetable.findReachableRoutes(markedStops, query.options.transportModes);
22084
- markedStops.clear();
22085
- // for each route that can be reached with at least round - 1 trips
22086
- for (const [route, hopOnStopIndex] of reachableRoutes) {
22087
- const newlyMarkedStops = this.scanRoute(route, hopOnStopIndex, round, routingState, query.options);
22088
- for (const newStop of newlyMarkedStops) {
22089
- markedStops.add(newStop);
22090
- }
22091
- }
22092
- // process in-seat trip continuations
22093
- let continuations = this.findTripContinuations(markedStops, edgesAtCurrentRound);
22094
- const stopsFromContinuations = new Set();
22095
- while (continuations.length > 0) {
22096
- stopsFromContinuations.clear();
22097
- for (const continuation of continuations) {
22098
- const route = this.timetable.getRoute(continuation.routeId);
22099
- const routeScanResults = this.scanRouteContinuation(route, continuation.stopIndex, round, routingState, continuation);
22100
- for (const newStop of routeScanResults) {
22101
- stopsFromContinuations.add(newStop);
22102
- }
22103
- }
22104
- for (const newStop of stopsFromContinuations) {
22105
- markedStops.add(newStop);
22106
- }
22107
- continuations = this.findTripContinuations(stopsFromContinuations, edgesAtCurrentRound);
22108
- }
22109
- const newlyMarkedStops = this.considerTransfers(query, round, markedStops, routingState);
22110
- for (const newStop of newlyMarkedStops) {
22111
- markedStops.add(newStop);
22112
- }
22113
- if (markedStops.size === 0)
22114
- break;
22115
- }
22116
- return new Result(query, routingState, this.stopsIndex, this.timetable);
22482
+ const accessLegs = this.accessFinder.collectAccessPaths(query.from, query.options.minTransferTime);
22483
+ const destinations = Array.from(query.to)
22484
+ .flatMap((destination) => this.stopsIndex.equivalentStops(destination))
22485
+ .map((destination) => destination.id);
22486
+ const routingState = new RoutingState(query.departureTime, destinations, accessLegs, this.timetable.nbStops(), query.options.maxTransfers + 1);
22487
+ this.raptor.run(query.options, routingState);
22488
+ return new Result(new Set(destinations), routingState, this.stopsIndex, this.timetable);
22489
+ }
22490
+ }
22491
+
22492
+ /**
22493
+ * The result of a Range RAPTOR query.
22494
+ *
22495
+ * Contains the complete Pareto-optimal set of journeys for a resolved
22496
+ * destination set.
22497
+ *
22498
+ * **Pareto dominance**: journey J1 dominates J2 iff
22499
+ * `τdep(J1) ≥ τdep(J2) AND τarr(J1) ≤ τarr(J2)`
22500
+ * (with at least one strict inequality).
22501
+ *
22502
+ * Runs are ordered **latest-departure-first**: each successive run departs
22503
+ * strictly earlier *and* arrives strictly earlier than the previous one,
22504
+ * forming the classic staircase Pareto frontier.
22505
+ *
22506
+ * Destination handling is delegated to {@link Result}, which expands
22507
+ * equivalent stops when reconstructing routes or looking up arrivals.
22508
+ */
22509
+ class RangeResult {
22510
+ constructor(runs, destinations) {
22511
+ this._runs = runs;
22512
+ this._destinations = destinations;
22513
+ }
22514
+ /** The resolved destination stop IDs for this result. */
22515
+ get destinations() {
22516
+ return this._destinations;
22517
+ }
22518
+ normalizeTargets(to) {
22519
+ if (to instanceof Set)
22520
+ return new Set(to);
22521
+ if (to !== undefined)
22522
+ return new Set([to]);
22523
+ return new Set(this._destinations);
22117
22524
  }
22118
22525
  /**
22119
- * Finds trip continuations for the given marked stops and edges at the current round.
22120
- * @param markedStops The set of marked stops.
22121
- * @param edgesAtCurrentRound The array of edges at the current round, indexed by stop ID.
22122
- * @returns An array of trip continuations.
22526
+ * Returns all non-dominated routes to this result's default destination set,
22527
+ * ordered from the earliest departure to the latest departure.
22528
+ *
22529
+ * Each route in the list departs strictly earlier *and* arrives strictly
22530
+ * earlier than its predecessor.
22123
22531
  */
22124
- findTripContinuations(markedStops, edgesAtCurrentRound) {
22125
- const continuations = [];
22126
- for (const stopId of markedStops) {
22127
- const arrival = edgesAtCurrentRound[stopId];
22128
- if (!arrival || !('routeId' in arrival))
22129
- continue;
22130
- const continuousTrips = this.timetable.getContinuousTrips(arrival.hopOffStopIndex, arrival.routeId, arrival.tripIndex);
22131
- for (let i = 0; i < continuousTrips.length; i++) {
22132
- const trip = continuousTrips[i];
22133
- continuations.push({
22134
- routeId: trip.routeId,
22135
- stopIndex: trip.stopIndex,
22136
- tripIndex: trip.tripIndex,
22137
- previousEdge: arrival,
22138
- });
22139
- }
22532
+ getRoutes() {
22533
+ const routes = [];
22534
+ for (const { result } of this._runs) {
22535
+ const route = result.bestRoute();
22536
+ if (route !== undefined)
22537
+ routes.push(route);
22140
22538
  }
22141
- return continuations;
22539
+ return routes.reverse();
22142
22540
  }
22143
22541
  /**
22144
- * Initializes the routing state for the RAPTOR algorithm.
22542
+ * The route that arrives **earliest** at the given stop(s) across all
22543
+ * Pareto-optimal runs.
22145
22544
  *
22146
- * This method sets up the initial data structures needed for route planning,
22147
- * including origin and destination stops (considering equivalent stops),
22148
- * earliest arrival times, and marked stops for processing.
22545
+ * When two runs achieve the same arrival time at the target, the one with
22546
+ * the **later departure** is preferred you wait at the origin rather than
22547
+ * at a transit stop.
22149
22548
  *
22150
- * @param query The routing query containing origin, destination, and departure time
22151
- * @returns The initialized routing state with all necessary data structures
22549
+ * Defaults to this result's own destination stop(s) when `to` is omitted.
22550
+ *
22551
+ * @param to Optional destination stop ID or set of stop IDs.
22552
+ * @returns The reconstructed {@link Route} with the earliest arrival,
22553
+ * or `undefined` if the target is unreachable in every run.
22152
22554
  */
22153
- initRoutingState(query) {
22154
- const { from, to, departureTime } = query;
22155
- // Consider children or siblings of the "from" stop as potential origins
22156
- const origins = this.stopsIndex
22157
- .equivalentStops(from)
22158
- .map((origin) => origin.id);
22159
- // Consider children or siblings of the "to" stop(s) as potential destinations
22160
- const destinations = Array.from(to)
22161
- .flatMap((destination) => this.stopsIndex.equivalentStops(destination))
22555
+ bestRoute(to) {
22556
+ const targetStops = this.normalizeTargets(to);
22557
+ let bestRun;
22558
+ let bestArrival;
22559
+ for (const run of this._runs) {
22560
+ for (const stopId of targetStops) {
22561
+ const arrival = run.result.arrivalAt(stopId);
22562
+ if (arrival === undefined)
22563
+ continue;
22564
+ if (bestArrival === undefined || arrival.arrival < bestArrival) {
22565
+ bestArrival = arrival.arrival;
22566
+ bestRun = run;
22567
+ }
22568
+ }
22569
+ }
22570
+ return bestRun === null || bestRun === void 0 ? void 0 : bestRun.result.bestRoute(targetStops);
22571
+ }
22572
+ /**
22573
+ * The route with the **latest possible departure** from the origin among all
22574
+ * Pareto-optimal journeys in the window.
22575
+ *
22576
+ * This is the journey that lets you leave the origin as late as possible.
22577
+ * It does **not** necessarily achieve the earliest arrival — for that, use
22578
+ * {@link bestRoute}. For the shortest travel duration, use
22579
+ * {@link fastestRoute}.
22580
+ *
22581
+ * Defaults to this result's own destination stop(s) when `to` is omitted.
22582
+ *
22583
+ * @param to Optional destination stop ID or set of stop IDs.
22584
+ * @returns The reconstructed {@link Route} with the latest departure,
22585
+ * or `undefined` if the target is unreachable in every run.
22586
+ */
22587
+ latestDepartureRoute(to) {
22588
+ const targetStops = this.normalizeTargets(to);
22589
+ for (const { result } of this._runs) {
22590
+ const route = result.bestRoute(targetStops);
22591
+ if (route !== undefined)
22592
+ return route;
22593
+ }
22594
+ return undefined;
22595
+ }
22596
+ /**
22597
+ * Reconstructs the **fastest** route to the given stop(s) — the journey with
22598
+ * the shortest travel duration (arrival time − origin departure time) across
22599
+ * all Pareto-optimal runs.
22600
+ *
22601
+ * Unlike {@link bestRoute}, which returns the route that departs as late as
22602
+ * possible while still arriving early, this method minimizes total time
22603
+ * spent traveling.
22604
+ *
22605
+ * Defaults to this result's own destination stop(s) when `to` is omitted.
22606
+ *
22607
+ * @param to Optional destination stop ID or set of stop IDs.
22608
+ * @returns The reconstructed fastest {@link Route}, or `undefined` if the
22609
+ * target is unreachable in every run.
22610
+ */
22611
+ fastestRoute(to) {
22612
+ const targetStops = this.normalizeTargets(to);
22613
+ let fastestRun;
22614
+ let shortestDuration = Infinity;
22615
+ for (const run of this._runs) {
22616
+ for (const stopId of targetStops) {
22617
+ const arrival = run.result.arrivalAt(stopId);
22618
+ if (arrival === undefined)
22619
+ continue;
22620
+ const duration = arrival.arrival - run.departureTime;
22621
+ if (duration < shortestDuration) {
22622
+ shortestDuration = duration;
22623
+ fastestRun = run;
22624
+ }
22625
+ }
22626
+ }
22627
+ return fastestRun === null || fastestRun === void 0 ? void 0 : fastestRun.result.bestRoute(targetStops);
22628
+ }
22629
+ /** Number of Pareto-optimal journeys found. */
22630
+ get size() {
22631
+ return this._runs.length;
22632
+ }
22633
+ /**
22634
+ * Earliest achievable arrival at a stop across all Pareto-optimal runs.
22635
+ *
22636
+ * Useful for isochrone / accessibility analysis: given this result's
22637
+ * departure-time frontier, how early can you reach stop `s` regardless of
22638
+ * which specific trip you take?
22639
+ *
22640
+ * Equivalent stops are handled by {@link Result.arrivalAt}.
22641
+ *
22642
+ * @param stop The target stop ID.
22643
+ * @param maxTransfers Optional upper bound on the number of transfers.
22644
+ */
22645
+ earliestArrivalAt(stop, maxTransfers) {
22646
+ let best;
22647
+ for (const { result } of this._runs) {
22648
+ const arrival = result.arrivalAt(stop, maxTransfers);
22649
+ if (arrival !== undefined &&
22650
+ (best === undefined || arrival.arrival < best.arrival)) {
22651
+ best = arrival;
22652
+ }
22653
+ }
22654
+ return best;
22655
+ }
22656
+ /**
22657
+ * Shortest travel duration to reach a stop across all Pareto-optimal runs.
22658
+ *
22659
+ * For each run, duration is measured from the run's origin departure time to
22660
+ * the earliest arrival at `stop` within that run. The minimum across all
22661
+ * runs is returned.
22662
+ *
22663
+ * Equivalent stops are handled by {@link Result.arrivalAt}.
22664
+ *
22665
+ * Duration is **not** monotone along the Pareto frontier — a run that
22666
+ * departs later may still travel faster — so every run is checked. In
22667
+ * practice the Pareto frontier is small, so this is O(runs).
22668
+ *
22669
+ * Returns `undefined` if `stop` is unreachable in every run.
22670
+ *
22671
+ * @param stop The target stop ID.
22672
+ * @param maxTransfers Optional upper bound on the number of transfers.
22673
+ */
22674
+ shortestDurationTo(stop, maxTransfers) {
22675
+ let shortest;
22676
+ for (const { departureTime, result } of this._runs) {
22677
+ const arrival = result.arrivalAt(stop, maxTransfers);
22678
+ if (arrival === undefined)
22679
+ continue;
22680
+ const duration = arrival.arrival - departureTime;
22681
+ if (shortest === undefined || duration < shortest.duration) {
22682
+ shortest = Object.assign(Object.assign({}, arrival), { duration });
22683
+ }
22684
+ }
22685
+ return shortest;
22686
+ }
22687
+ /**
22688
+ * Shortest travel duration to **every reachable stop** across all
22689
+ * Pareto-optimal runs, as a single `Map<StopId, DurationArrival>`.
22690
+ */
22691
+ allShortestDurations() {
22692
+ const durations = new Map();
22693
+ for (const { departureTime, result } of this._runs) {
22694
+ for (const { stop, arrival, legNumber, } of result.routingState.arrivals()) {
22695
+ const duration = arrival - departureTime;
22696
+ const existing = durations.get(stop);
22697
+ if (existing === undefined || duration < existing.duration) {
22698
+ durations.set(stop, { arrival, legNumber, duration });
22699
+ }
22700
+ }
22701
+ }
22702
+ return durations;
22703
+ }
22704
+ /**
22705
+ * Earliest achievable arrival at **every reachable stop** across all
22706
+ * Pareto-optimal runs, as a single `Map<StopId, Arrival>`.
22707
+ */
22708
+ allEarliestArrivals() {
22709
+ const arrivals = new Map();
22710
+ for (const { result } of this._runs) {
22711
+ for (const { stop, arrival, legNumber, } of result.routingState.arrivals()) {
22712
+ const existing = arrivals.get(stop);
22713
+ if (existing === undefined || arrival < existing.arrival) {
22714
+ arrivals.set(stop, { arrival, legNumber });
22715
+ }
22716
+ }
22717
+ }
22718
+ return arrivals;
22719
+ }
22720
+ /**
22721
+ * Iterates over all Pareto-optimal `(departureTime, result)` pairs,
22722
+ * ordered from the latest departure to the earliest departure.
22723
+ */
22724
+ [Symbol.iterator]() {
22725
+ return this._runs[Symbol.iterator]();
22726
+ }
22727
+ }
22728
+
22729
+ /**
22730
+ * RAPTOR state for Range RAPTOR mode, implementing {@link IRaptorState}.
22731
+ *
22732
+ * Holds both the cross-run shared labels (carried over from one departure-time
22733
+ * iteration to the next, latest → earliest) and a reference to the current
22734
+ * per-iteration {@link RoutingState} (swapped via {@link setCurrentRun}).
22735
+ *
22736
+ * Concretely, `roundLabels[k][p]` is the best known arrival at stop `p` using
22737
+ * at most `k` transit legs, across **all departure times tried so far**.
22738
+ *
22739
+ * @see https://www.microsoft.com/en-us/research/wp-content/uploads/2012/01/raptor_alenex.pdf
22740
+ */
22741
+ class RangeRaptorState {
22742
+ constructor(maxRounds, nbStops, latestDeparture) {
22743
+ /**
22744
+ * Global best arrival at any destination stop across all runs and rounds.
22745
+ * Used for destination-pruning inside scan methods so that routes that cannot
22746
+ * beat the already-known best are skipped early.
22747
+ */
22748
+ this._destinationBest = UNREACHED_TIME;
22749
+ this.latestDeparture = latestDeparture;
22750
+ // maxRounds + 2: index 0 = origin/walk legs, indices 1…maxRounds+1 = transit rounds
22751
+ this.roundLabels = Array.from({ length: maxRounds + 2 }, () => new Uint16Array(nbStops).fill(UNREACHED_TIME));
22752
+ this.changedInRound = Array.from({ length: maxRounds + 2 }, () => []);
22753
+ }
22754
+ /**
22755
+ * Swaps in a fresh {@link RoutingState} for the next departure-time iteration
22756
+ * and seeds the shared round-0 labels from its access arrivals.
22757
+ *
22758
+ * Must be called before every `runRaptor` invocation.
22759
+ */
22760
+ setCurrentRun(routingState) {
22761
+ this.currentRun = routingState;
22762
+ // Propagate round-0 access arrivals into the shared labels so that
22763
+ // initRound(1) can tighten round-1 pruning bounds correctly.
22764
+ const round0 = routingState.graph[0];
22765
+ for (const stop of routingState.origins) {
22766
+ const edge = round0[stop];
22767
+ if (!edge)
22768
+ continue;
22769
+ this.updateArrival(stop, edge.arrival, 0);
22770
+ }
22771
+ }
22772
+ get origins() {
22773
+ return this.currentRun.origins;
22774
+ }
22775
+ get graph() {
22776
+ return this.currentRun.graph;
22777
+ }
22778
+ arrivalTime(stop) {
22779
+ return this.currentRun.arrivalTime(stop);
22780
+ }
22781
+ /**
22782
+ * Uses the cross-run shared label for `round`, which is always at least as
22783
+ * tight as the per-run arrival and therefore provides stronger pruning.
22784
+ */
22785
+ improvementBound(round, stop) {
22786
+ return this.roundLabels[round][stop];
22787
+ }
22788
+ /**
22789
+ * Global best arrival at any destination across all departure-time iterations.
22790
+ * Always at least as tight as the per-run `destinationBest`.
22791
+ */
22792
+ get destinationBest() {
22793
+ return this._destinationBest;
22794
+ }
22795
+ isDestination(stop) {
22796
+ return this.currentRun.isDestination(stop);
22797
+ }
22798
+ /** Updates both the per-run state and the cross-run shared labels. */
22799
+ updateArrival(stop, time, round) {
22800
+ this.currentRun.updateArrival(stop, time, round);
22801
+ if (time < this.roundLabels[round][stop]) {
22802
+ this.roundLabels[round][stop] = time;
22803
+ this.changedInRound[round].push(stop);
22804
+ if (this.currentRun.isDestination(stop) && time < this._destinationBest) {
22805
+ this._destinationBest = time;
22806
+ }
22807
+ }
22808
+ }
22809
+ /**
22810
+ * initialized round `k` from round `k-1`: τk(p) ← min(τk(p), τk-1(p)).
22811
+ *
22812
+ * Must be called at the very start of each RAPTOR round before routes are
22813
+ * scanned. After this call, `roundLabels[k][p]` is the minimum arrival at
22814
+ * stop `p` achievable with **at most** k transit legs from any departure time
22815
+ * tried so far — which is exactly the tightest valid pruning bound for round k.
22816
+ */
22817
+ initRound(round) {
22818
+ const changed = this.changedInRound[round - 1];
22819
+ if (changed.length === 0)
22820
+ return;
22821
+ const prev = this.roundLabels[round - 1];
22822
+ const curr = this.roundLabels[round];
22823
+ for (let i = 0; i < changed.length; i++) {
22824
+ const stop = changed[i];
22825
+ if (prev[stop] < curr[stop]) {
22826
+ curr[stop] = prev[stop];
22827
+ }
22828
+ }
22829
+ changed.length = 0;
22830
+ }
22831
+ }
22832
+
22833
+ class RangeRouter {
22834
+ constructor(timetable, stopsIndex, accessFinder, raptor) {
22835
+ this.timetable = timetable;
22836
+ this.stopsIndex = stopsIndex;
22837
+ this.accessFinder = accessFinder;
22838
+ this.raptor = raptor;
22839
+ }
22840
+ /**
22841
+ * Range RAPTOR: finds all Pareto-optimal journeys within the departure-time
22842
+ * window `[query.departureTime, query.lastDepartureTime]`.
22843
+ *
22844
+ * A journey is Pareto-optimal iff no journey departing no earlier arrives no
22845
+ * later. Runs are ordered latest-departure-first in the returned result.
22846
+ *
22847
+ * @param query A {@link RangeQuery} with both `departureTime` and `lastDepartureTime` set.
22848
+ * @returns A {@link RangeResult} exposing the full Pareto frontier.
22849
+ */
22850
+ rangeRoute(query) {
22851
+ var _a, _b;
22852
+ const { departureTime: earliest, lastDepartureTime: latest } = query;
22853
+ const destinations = Array.from(query.to)
22854
+ .flatMap((destination) => this.stopsIndex.equivalentStops(destination))
22162
22855
  .map((destination) => destination.id);
22163
- return new RoutingState(origins, destinations, departureTime, this.timetable.nbStops());
22856
+ const accessLegs = this.accessFinder.collectAccessPaths(query.from, query.options.minTransferTime);
22857
+ const departureSlots = this.accessFinder.collectDepartureTimes(accessLegs, earliest, latest);
22858
+ if (departureSlots.length === 0) {
22859
+ return new RangeResult([], new Set(destinations));
22860
+ }
22861
+ const maxRounds = query.options.maxTransfers + 1;
22862
+ const rangeState = new RangeRaptorState(maxRounds, this.timetable.nbStops(), latest);
22863
+ const paretoRuns = [];
22864
+ const paretoDestBest = new Map();
22865
+ for (const dest of destinations) {
22866
+ paretoDestBest.set(dest, UNREACHED_TIME);
22867
+ }
22868
+ const trivialDests = new Set(accessLegs
22869
+ .map((leg) => leg.toStopId)
22870
+ .filter((id) => destinations.includes(id)));
22871
+ const trivialDestCovered = new Set();
22872
+ let routingState = null;
22873
+ if (query.rangeOptions.optimizeBeyondLatestDeparture) {
22874
+ routingState = new RoutingState(latest + 1, destinations, accessLegs, this.timetable.nbStops(), maxRounds);
22875
+ rangeState.setCurrentRun(routingState);
22876
+ this.raptor.run(Object.assign(Object.assign({}, query.options), { maxInitialWaitingTime: undefined }), rangeState);
22877
+ for (const dest of destinations) {
22878
+ const t = routingState.arrivalTime(dest);
22879
+ if (t < ((_a = paretoDestBest.get(dest)) !== null && _a !== void 0 ? _a : UNREACHED_TIME))
22880
+ paretoDestBest.set(dest, t);
22881
+ }
22882
+ }
22883
+ for (const { depTime, legs } of departureSlots) {
22884
+ if (trivialDestCovered.size === destinations.length)
22885
+ break;
22886
+ if (routingState === null) {
22887
+ routingState = new RoutingState(depTime, destinations, legs, this.timetable.nbStops(), maxRounds);
22888
+ }
22889
+ else {
22890
+ routingState.resetFor(depTime, legs);
22891
+ }
22892
+ rangeState.setCurrentRun(routingState);
22893
+ this.raptor.run(Object.assign(Object.assign({}, query.options), { maxInitialWaitingTime: 0 }), rangeState);
22894
+ let isParetoOptimal = false;
22895
+ for (const dest of destinations) {
22896
+ const arrival = routingState.arrivalTime(dest);
22897
+ if (arrival >= ((_b = paretoDestBest.get(dest)) !== null && _b !== void 0 ? _b : UNREACHED_TIME)) {
22898
+ continue;
22899
+ }
22900
+ if (trivialDests.has(dest) && trivialDestCovered.has(dest)) {
22901
+ paretoDestBest.set(dest, arrival);
22902
+ continue;
22903
+ }
22904
+ paretoDestBest.set(dest, arrival);
22905
+ if (trivialDests.has(dest)) {
22906
+ trivialDestCovered.add(dest);
22907
+ }
22908
+ isParetoOptimal = true;
22909
+ }
22910
+ if (isParetoOptimal) {
22911
+ paretoRuns.push({
22912
+ departureTime: depTime,
22913
+ result: new Result(new Set(destinations), routingState, this.stopsIndex, this.timetable),
22914
+ });
22915
+ routingState = null;
22916
+ }
22917
+ }
22918
+ return new RangeResult(paretoRuns, new Set(destinations));
22919
+ }
22920
+ }
22921
+
22922
+ /**
22923
+ * Encapsulates the core RAPTOR algorithm, operating on a {@link Timetable} and
22924
+ * an {@link IRaptorState} provided by the caller.
22925
+ *
22926
+ * @see https://www.microsoft.com/en-us/research/wp-content/uploads/2012/01/raptor_alenex.pdf
22927
+ */
22928
+ class Raptor {
22929
+ constructor(timetable) {
22930
+ this.timetable = timetable;
22931
+ }
22932
+ run(options, state) {
22933
+ const markedStops = new Set(state.origins);
22934
+ for (let round = 1; round <= options.maxTransfers + 1; round++) {
22935
+ state.initRound(round);
22936
+ const edgesAtCurrentRound = state.graph[round];
22937
+ const reachableRoutes = this.timetable.findReachableRoutes(markedStops, options.transportModes);
22938
+ markedStops.clear();
22939
+ for (const [route, hopOnStopIndex] of reachableRoutes) {
22940
+ for (const stop of this.scanRoute(route, hopOnStopIndex, round, state, options)) {
22941
+ markedStops.add(stop);
22942
+ }
22943
+ }
22944
+ let continuations = this.findTripContinuations(markedStops, edgesAtCurrentRound);
22945
+ const stopsFromContinuations = new Set();
22946
+ while (continuations.length > 0) {
22947
+ stopsFromContinuations.clear();
22948
+ for (const continuation of continuations) {
22949
+ const route = this.timetable.getRoute(continuation.routeId);
22950
+ for (const stop of this.scanRouteContinuation(route, continuation.stopIndex, round, state, continuation)) {
22951
+ stopsFromContinuations.add(stop);
22952
+ markedStops.add(stop);
22953
+ }
22954
+ }
22955
+ continuations = this.findTripContinuations(stopsFromContinuations, edgesAtCurrentRound);
22956
+ }
22957
+ for (const stop of this.considerTransfers(options, round, markedStops, state)) {
22958
+ markedStops.add(stop);
22959
+ }
22960
+ if (markedStops.size === 0)
22961
+ break;
22962
+ }
22963
+ }
22964
+ /**
22965
+ * Finds trip continuations for the given marked stops and edges at the current round.
22966
+ * @param markedStops The set of marked stops.
22967
+ * @param edgesAtCurrentRound The array of edges at the current round, indexed by stop ID.
22968
+ * @returns An array of trip continuations.
22969
+ */
22970
+ findTripContinuations(markedStops, edgesAtCurrentRound) {
22971
+ const continuations = [];
22972
+ for (const stopId of markedStops) {
22973
+ const arrival = edgesAtCurrentRound[stopId];
22974
+ if (!arrival || !('routeId' in arrival))
22975
+ continue;
22976
+ const continuousTrips = this.timetable.getContinuousTrips(arrival.hopOffStopIndex, arrival.routeId, arrival.tripIndex);
22977
+ for (const trip of continuousTrips) {
22978
+ continuations.push({
22979
+ routeId: trip.routeId,
22980
+ stopIndex: trip.stopIndex,
22981
+ tripIndex: trip.tripIndex,
22982
+ previousEdge: arrival,
22983
+ });
22984
+ }
22985
+ }
22986
+ return continuations;
22164
22987
  }
22165
22988
  /**
22166
22989
  * Scans a route for an in-seat trip continuation.
@@ -22173,11 +22996,11 @@ class Router {
22173
22996
  * @param round The current RAPTOR round
22174
22997
  * @param routingState Current routing state
22175
22998
  * @param tripContinuation The in-seat continuation descriptor
22999
+ * @param shared Optional shared state for Range RAPTOR mode
22176
23000
  */
22177
- scanRouteContinuation(route, hopOnStopIndex, round, routingState, tripContinuation) {
23001
+ scanRouteContinuation(route, hopOnStopIndex, round, state, tripContinuation) {
22178
23002
  const newlyMarkedStops = new Set();
22179
- const edgesAtCurrentRound = routingState.graph[round];
22180
- const earliestArrivalAtAnyDestination = this.earliestArrivalAtAnyStop(routingState);
23003
+ const edgesAtCurrentRound = state.graph[round];
22181
23004
  const nbStops = route.getNbStops();
22182
23005
  const routeId = route.id;
22183
23006
  const tripIndex = tripContinuation.tripIndex;
@@ -22187,10 +23010,9 @@ class Router {
22187
23010
  const currentStop = route.stops[currentStopIndex];
22188
23011
  const arrivalTime = route.arrivalAtOffset(currentStopIndex, tripStopOffset);
22189
23012
  const dropOffType = route.dropOffTypeAtOffset(currentStopIndex, tripStopOffset);
22190
- const earliestArrivalAtCurrentStop = routingState.arrivalTime(currentStop);
22191
23013
  if (dropOffType !== NOT_AVAILABLE &&
22192
- arrivalTime < earliestArrivalAtCurrentStop &&
22193
- arrivalTime < earliestArrivalAtAnyDestination) {
23014
+ arrivalTime < state.improvementBound(round, currentStop) &&
23015
+ arrivalTime < state.destinationBest) {
22194
23016
  edgesAtCurrentRound[currentStop] = {
22195
23017
  routeId,
22196
23018
  stopIndex: hopOnStopIndex,
@@ -22199,7 +23021,7 @@ class Router {
22199
23021
  hopOffStopIndex: currentStopIndex,
22200
23022
  continuationOf: previousEdge,
22201
23023
  };
22202
- routingState.updateArrival(currentStop, arrivalTime, round);
23024
+ state.updateArrival(currentStop, arrivalTime, round);
22203
23025
  newlyMarkedStops.add(currentStop);
22204
23026
  }
22205
23027
  }
@@ -22216,20 +23038,18 @@ class Router {
22216
23038
  * @param route The route to scan
22217
23039
  * @param hopOnStopIndex The stop index where passengers can first board
22218
23040
  * @param round The current RAPTOR round
22219
- * @param routingState Current routing state
23041
+ * @param state Current routing state
22220
23042
  * @param options Query options (minTransferTime, etc.)
22221
23043
  */
22222
- scanRoute(route, hopOnStopIndex, round, routingState, options) {
23044
+ scanRoute(route, hopOnStopIndex, round, state, options) {
22223
23045
  const newlyMarkedStops = new Set();
22224
- const edgesAtCurrentRound = routingState.graph[round];
22225
- const edgesAtPreviousRound = routingState.graph[round - 1];
22226
- const earliestArrivalAtAnyDestination = this.earliestArrivalAtAnyStop(routingState);
23046
+ const edgesAtCurrentRound = state.graph[round];
23047
+ const edgesAtPreviousRound = state.graph[round - 1];
22227
23048
  const nbStops = route.getNbStops();
22228
23049
  const routeId = route.id;
22229
23050
  let activeTripIndex;
22230
23051
  let activeTripBoardStopIndex = hopOnStopIndex;
22231
23052
  // tripStopOffset = activeTripIndex * nbStops, precomputed when the trip changes.
22232
- // Only valid while activeTripIndex !== undefined.
22233
23053
  let activeTripStopOffset = 0;
22234
23054
  for (let currentStopIndex = hopOnStopIndex; currentStopIndex < nbStops; currentStopIndex++) {
22235
23055
  const currentStop = route.stops[currentStopIndex];
@@ -22237,10 +23057,9 @@ class Router {
22237
23057
  if (activeTripIndex !== undefined) {
22238
23058
  const arrivalTime = route.arrivalAtOffset(currentStopIndex, activeTripStopOffset);
22239
23059
  const dropOffType = route.dropOffTypeAtOffset(currentStopIndex, activeTripStopOffset);
22240
- const earliestArrivalAtCurrentStop = routingState.arrivalTime(currentStop);
22241
23060
  if (dropOffType !== NOT_AVAILABLE &&
22242
- arrivalTime < earliestArrivalAtCurrentStop &&
22243
- arrivalTime < earliestArrivalAtAnyDestination) {
23061
+ arrivalTime < state.improvementBound(round, currentStop) &&
23062
+ arrivalTime < state.destinationBest) {
22244
23063
  edgesAtCurrentRound[currentStop] = {
22245
23064
  routeId,
22246
23065
  stopIndex: activeTripBoardStopIndex,
@@ -22248,7 +23067,7 @@ class Router {
22248
23067
  arrival: arrivalTime,
22249
23068
  hopOffStopIndex: currentStopIndex,
22250
23069
  };
22251
- routingState.updateArrival(currentStop, arrivalTime, round);
23070
+ state.updateArrival(currentStop, arrivalTime, round);
22252
23071
  newlyMarkedStops.add(currentStop);
22253
23072
  }
22254
23073
  }
@@ -22258,142 +23077,166 @@ class Router {
22258
23077
  if (earliestArrivalOnPreviousRound !== undefined &&
22259
23078
  (activeTripIndex === undefined ||
22260
23079
  earliestArrivalOnPreviousRound <=
22261
- route.departureFrom(currentStopIndex, activeTripIndex))) {
23080
+ route.departureAtOffset(currentStopIndex, activeTripStopOffset))) {
22262
23081
  const earliestTrip = route.findEarliestTrip(currentStopIndex, earliestArrivalOnPreviousRound, activeTripIndex);
22263
23082
  if (earliestTrip === undefined) {
22264
23083
  continue;
22265
23084
  }
22266
- const firstBoardableTrip = this.findFirstBoardableTrip(currentStopIndex, route, earliestTrip, earliestArrivalOnPreviousRound, activeTripIndex,
22267
- // provide the previous edge only if it was a vehicle leg
22268
- previousEdge && 'routeId' in previousEdge ? previousEdge : undefined, options.minTransferTime);
23085
+ const fromTripStop = previousEdge && 'routeId' in previousEdge
23086
+ ? {
23087
+ stopIndex: previousEdge.hopOffStopIndex,
23088
+ routeId: previousEdge.routeId,
23089
+ tripIndex: previousEdge.tripIndex,
23090
+ }
23091
+ : undefined;
23092
+ const firstBoardableTrip = this.timetable.findFirstBoardableTrip(currentStopIndex, route, earliestTrip, earliestArrivalOnPreviousRound, activeTripIndex, fromTripStop, options.minTransferTime);
22269
23093
  if (firstBoardableTrip !== undefined) {
22270
- activeTripIndex = firstBoardableTrip;
22271
- activeTripBoardStopIndex = currentStopIndex;
22272
- activeTripStopOffset = route.tripStopOffset(firstBoardableTrip);
23094
+ // At round 1, enforce maxInitialWaitingTime: skip boarding if the
23095
+ // traveler would have to wait longer than the allowed threshold at
23096
+ // the first boarding stop.
23097
+ const exceedsInitialWait = round === 1 &&
23098
+ options.maxInitialWaitingTime !== undefined &&
23099
+ route.departureFrom(currentStopIndex, firstBoardableTrip) -
23100
+ earliestArrivalOnPreviousRound >
23101
+ options.maxInitialWaitingTime;
23102
+ if (!exceedsInitialWait) {
23103
+ activeTripIndex = firstBoardableTrip;
23104
+ activeTripBoardStopIndex = currentStopIndex;
23105
+ activeTripStopOffset = route.tripStopOffset(firstBoardableTrip);
23106
+ }
22273
23107
  }
22274
23108
  }
22275
23109
  }
22276
23110
  return newlyMarkedStops;
22277
23111
  }
22278
- /**
22279
- * Finds the first boardable trip on a route at a given stop that meets transfer requirements.
22280
- *
22281
- * This method searches through trips on a route starting from the earliest trip index reachable
22282
- * from the previous edge to find the first trip that can be effectively boarded,
22283
- * considering pickup availability, transfer guarantees, and minimum transfer times.
22284
- *
22285
- * @param stopIndex The index in the route of the stop where boarding is attempted
22286
- * @param route The route to search for boardable trips
22287
- * @param earliestTrip The earliest trip index to start searching from
22288
- * @param after The earliest time after which boarding can occur
22289
- * @param beforeTrip Optional upper bound trip index to limit search
22290
- * @param previousTrip The previous trip taken (for transfer guarantee checks)
22291
- * @param transferTime Minimum time required for transfers between trips
22292
- * @returns The trip index of the first boardable trip, or undefined if none found
22293
- */
22294
- findFirstBoardableTrip(stopIndex, route, earliestTrip, after = TIME_ORIGIN, beforeTrip, previousTrip, transferTime = DURATION_ZERO) {
22295
- const nbTrips = route.getNbTrips();
22296
- for (let t = earliestTrip; t < (beforeTrip !== null && beforeTrip !== void 0 ? beforeTrip : nbTrips); t++) {
22297
- const pickup = route.pickUpTypeFrom(stopIndex, t);
22298
- if (pickup === NOT_AVAILABLE) {
22299
- continue;
22300
- }
22301
- if (previousTrip === undefined) {
22302
- return t;
22303
- }
22304
- const isGuaranteed = this.timetable.isTripTransferGuaranteed({
22305
- stopIndex: previousTrip.hopOffStopIndex,
22306
- routeId: previousTrip.routeId,
22307
- tripIndex: previousTrip.tripIndex,
22308
- }, { stopIndex, routeId: route.id, tripIndex: t });
22309
- if (isGuaranteed) {
22310
- return t;
22311
- }
22312
- const departure = route.departureFrom(stopIndex, t);
22313
- const requiredTime = after + transferTime;
22314
- if (departure >= requiredTime) {
22315
- return t;
22316
- }
22317
- }
22318
- return undefined;
22319
- }
22320
23112
  /**
22321
23113
  * Processes all currently marked stops to find available transfers
22322
23114
  * and determines if using these transfers would result in earlier arrival times
22323
23115
  * at destination stops. It handles different transfer types including in-seat
22324
23116
  * transfers and walking transfers with appropriate minimum transfer times.
22325
23117
  *
22326
- * @param query The routing query containing transfer options and constraints
23118
+ * @param options Query options (minTransferTime, etc.)
22327
23119
  * @param round The current round number in the RAPTOR algorithm
22328
- * @param routingState The current routing state containing arrival times and marked stops
23120
+ * @param markedStops The set of currently marked stops
23121
+ * @param state Current routing state
22329
23122
  */
22330
- considerTransfers(query, round, markedStops, routingState) {
22331
- const { options } = query;
22332
- const arrivalsAtCurrentRound = routingState.graph[round];
23123
+ considerTransfers(options, round, markedStops, state) {
22333
23124
  const newlyMarkedStops = new Set();
23125
+ const arrivalsAtCurrentRound = state.graph[round];
22334
23126
  for (const stop of markedStops) {
22335
23127
  const currentArrival = arrivalsAtCurrentRound[stop];
22336
23128
  // Skip transfers if the last leg was also a transfer
22337
23129
  if (!currentArrival || 'type' in currentArrival)
22338
23130
  continue;
22339
23131
  const transfers = this.timetable.getTransfers(stop);
22340
- for (let j = 0; j < transfers.length; j++) {
22341
- const transfer = transfers[j];
23132
+ for (const transfer of transfers) {
22342
23133
  let transferTime;
22343
23134
  if (transfer.minTransferTime) {
22344
23135
  transferTime = transfer.minTransferTime;
22345
23136
  }
22346
23137
  else if (transfer.type === 'IN_SEAT') {
22347
- // TODO not needed anymore now that trip continuations are handled separately
22348
23138
  transferTime = DURATION_ZERO;
22349
23139
  }
22350
23140
  else {
22351
23141
  transferTime = options.minTransferTime;
22352
23142
  }
22353
23143
  const arrivalAfterTransfer = currentArrival.arrival + transferTime;
22354
- const originalArrival = routingState.arrivalTime(transfer.destination);
22355
- if (arrivalAfterTransfer < originalArrival) {
23144
+ if (arrivalAfterTransfer <
23145
+ state.improvementBound(round, transfer.destination) &&
23146
+ arrivalAfterTransfer < state.destinationBest) {
22356
23147
  arrivalsAtCurrentRound[transfer.destination] = {
22357
23148
  arrival: arrivalAfterTransfer,
22358
23149
  from: stop,
22359
23150
  to: transfer.destination,
22360
- minTransferTime: transfer.minTransferTime,
23151
+ minTransferTime: transferTime || undefined,
22361
23152
  type: transfer.type,
22362
23153
  };
22363
- routingState.updateArrival(transfer.destination, arrivalAfterTransfer, round);
23154
+ state.updateArrival(transfer.destination, arrivalAfterTransfer, round);
22364
23155
  newlyMarkedStops.add(transfer.destination);
22365
23156
  }
22366
23157
  }
22367
23158
  }
22368
23159
  return newlyMarkedStops;
22369
23160
  }
23161
+ }
23162
+
23163
+ /**
23164
+ * A public transportation router implementing the RAPTOR and Range RAPTOR
23165
+ * algorithms.
23166
+ *
23167
+ * Thin facade over {@link PlainRouter} and {@link RangeRouter}: constructs the
23168
+ * shared {@link Raptor} engine and {@link AccessFinder} once and delegates each
23169
+ * query to the appropriate router.
23170
+ *
23171
+ * @see https://www.microsoft.com/en-us/research/wp-content/uploads/2012/01/raptor_alenex.pdf
23172
+ */
23173
+ class Router {
23174
+ constructor(timetable, stopsIndex) {
23175
+ const raptor = new Raptor(timetable);
23176
+ const accessFinder = new AccessFinder(timetable, stopsIndex);
23177
+ this.plainRouter = new PlainRouter(timetable, stopsIndex, accessFinder, raptor);
23178
+ this.rangeRouter = new RangeRouter(timetable, stopsIndex, accessFinder, raptor);
23179
+ }
22370
23180
  /**
22371
- * Finds the earliest arrival time at any stop from a given set of destinations.
22372
- *
22373
- * @param routingState The routing state containing arrival times and destinations.
22374
- * @returns The earliest arrival time among the provided destinations.
23181
+ * Standard RAPTOR: finds the earliest-arrival journey from `query.from` to
23182
+ * `query.to` for the given departure time.
22375
23183
  */
22376
- earliestArrivalAtAnyStop(routingState) {
22377
- let earliestArrivalAtAnyDestination = UNREACHED_TIME;
22378
- for (let i = 0; i < routingState.destinations.length; i++) {
22379
- const arrival = routingState.arrivalTime(routingState.destinations[i]);
22380
- if (arrival < earliestArrivalAtAnyDestination) {
22381
- earliestArrivalAtAnyDestination = arrival;
22382
- }
22383
- }
22384
- return earliestArrivalAtAnyDestination;
23184
+ route(query) {
23185
+ return this.plainRouter.route(query);
23186
+ }
23187
+ /**
23188
+ * Range RAPTOR: finds all Pareto-optimal journeys within the departure-time
23189
+ * window `[query.departureTime, query.lastDepartureTime]`.
23190
+ */
23191
+ rangeRoute(query) {
23192
+ return this.rangeRouter.rangeRoute(query);
22385
23193
  }
22386
23194
  }
22387
23195
 
23196
+ const renderTable = (columns, rows, footerRow) => {
23197
+ const bar = (l, m, r) => l + columns.map((c) => '─'.repeat(c.width + 2)).join(m) + r;
23198
+ const renderRow = (cells) => '│' +
23199
+ cells
23200
+ .map((cell, i) => {
23201
+ var _a, _b, _c, _d;
23202
+ const width = (_b = (_a = columns[i]) === null || _a === void 0 ? void 0 : _a.width) !== null && _b !== void 0 ? _b : 0;
23203
+ const align = (_d = (_c = columns[i]) === null || _c === void 0 ? void 0 : _c.align) !== null && _d !== void 0 ? _d : 'left';
23204
+ const padded = align === 'right' ? cell.padStart(width) : cell.padEnd(width);
23205
+ return ` ${padded} `;
23206
+ })
23207
+ .join('│') +
23208
+ '│';
23209
+ return [
23210
+ bar('┌', '┬', '┐'),
23211
+ renderRow(columns.map((c) => c.header)),
23212
+ bar('├', '┼', '┤'),
23213
+ ...rows.map(renderRow),
23214
+ bar('├', '┼', '┤'),
23215
+ renderRow(footerRow),
23216
+ bar('└', '┴', '┘'),
23217
+ ].join('\n');
23218
+ };
23219
+ // ─── Query label ──────────────────────────────────────────────────────────────
23220
+ const buildQueryLabel = (query, stopsIndex) => {
23221
+ var _a, _b;
23222
+ const fromName = (_b = (_a = stopsIndex.findStopById(query.from)) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : String(query.from);
23223
+ const toNames = [...query.to]
23224
+ .map((id) => { var _a, _b; return (_b = (_a = stopsIndex.findStopById(id)) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : String(id); })
23225
+ .join(' / ');
23226
+ const dep = timeToString(query.departureTime);
23227
+ if (query instanceof RangeQuery) {
23228
+ const lastDep = timeToString(query.lastDepartureTime);
23229
+ return `${fromName} → ${toNames} ${dep}–${lastDep}`;
23230
+ }
23231
+ return `${fromName} → ${toNames} ${dep}`;
23232
+ };
23233
+ // ─── Query loaders ────────────────────────────────────────────────────────────
22388
23234
  /**
22389
23235
  * Loads a list of routing queries from a JSON file and resolves the
22390
23236
  * human-readable stop IDs to the internal numeric IDs used by the router.
22391
23237
  *
22392
- * The file must contain a JSON array whose elements each have the shape:
22393
- * ```json
22394
- * { "from": "STOP_A", "to": ["STOP_B", "STOP_C"], "departureTime": "08:30:00" }
22395
- * ```
22396
- * An optional `maxTransfers` integer field is also supported.
23238
+ * Only entries that do **not** carry a `lastDepartureTime` field are loaded
23239
+ * range-query entries are silently skipped.
22397
23240
  *
22398
23241
  * @param filePath - Path to the JSON file containing the serialized queries.
22399
23242
  * @param stopsIndex - The stops index used to resolve source stop IDs to the
@@ -22406,7 +23249,9 @@ class Router {
22406
23249
  const loadQueriesFromJson = (filePath, stopsIndex) => {
22407
23250
  const fileContent = fs.readFileSync(filePath, 'utf-8');
22408
23251
  const serializedQueries = JSON.parse(fileContent);
22409
- return serializedQueries.map((serializedQuery) => {
23252
+ return serializedQueries
23253
+ .filter((q) => q.lastDepartureTime === undefined)
23254
+ .map((serializedQuery) => {
22410
23255
  const fromStop = stopsIndex.findStopBySourceStopId(serializedQuery.from);
22411
23256
  const toStops = Array.from(serializedQuery.to).map((stopId) => stopsIndex.findStopBySourceStopId(stopId));
22412
23257
  if (!fromStop || toStops.some((toStop) => !toStop)) {
@@ -22423,6 +23268,46 @@ const loadQueriesFromJson = (filePath, stopsIndex) => {
22423
23268
  return queryBuilder.build();
22424
23269
  });
22425
23270
  };
23271
+ /**
23272
+ * Loads a list of range routing queries from a JSON file and resolves the
23273
+ * human-readable stop IDs to the internal numeric IDs used by the router.
23274
+ *
23275
+ * Only entries that carry a `lastDepartureTime` field are loaded — plain
23276
+ * point-query entries are silently skipped.
23277
+ *
23278
+ * @param filePath - Path to the JSON file containing the serialized queries.
23279
+ * @param stopsIndex - The stops index used to resolve source stop IDs to the
23280
+ * internal numeric IDs expected by the router.
23281
+ * @returns An array of fully constructed {@link RangeQuery} objects ready to
23282
+ * be passed to {@link Router.rangeRoute}.
23283
+ * @throws If the file cannot be read, the JSON is malformed, or any stop ID
23284
+ * referenced in the file cannot be found in the stops index.
23285
+ */
23286
+ const loadRangeQueriesFromJson = (filePath, stopsIndex) => {
23287
+ const fileContent = fs.readFileSync(filePath, 'utf-8');
23288
+ const serializedQueries = JSON.parse(fileContent);
23289
+ return serializedQueries
23290
+ .filter((q) => q.lastDepartureTime !== undefined)
23291
+ .map((serializedQuery) => {
23292
+ const fromStop = stopsIndex.findStopBySourceStopId(serializedQuery.from);
23293
+ const toStops = Array.from(serializedQuery.to).map((stopId) => stopsIndex.findStopBySourceStopId(stopId));
23294
+ if (!fromStop || toStops.some((toStop) => !toStop)) {
23295
+ throw new Error(`Invalid task: Start or end station not found for task ${JSON.stringify(serializedQuery)}`);
23296
+ }
23297
+ const queryBuilder = new RangeQuery.Builder()
23298
+ .from(fromStop.id)
23299
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
23300
+ .to(new Set(toStops.map((stop) => stop.id)))
23301
+ .departureTime(timeFromString(serializedQuery.departureTime))
23302
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
23303
+ .lastDepartureTime(timeFromString(serializedQuery.lastDepartureTime));
23304
+ if (serializedQuery.maxTransfers !== undefined) {
23305
+ queryBuilder.maxTransfers(serializedQuery.maxTransfers);
23306
+ }
23307
+ return queryBuilder.build();
23308
+ });
23309
+ };
23310
+ // ─── Benchmark runners ────────────────────────────────────────────────────────
22426
23311
  /**
22427
23312
  * Benchmarks {@link Router.route} across a set of queries.
22428
23313
  *
@@ -22431,10 +23316,11 @@ const loadQueriesFromJson = (filePath, stopsIndex) => {
22431
23316
  * produced per query.
22432
23317
  * @param iterations - Number of times each query is repeated. Higher values
22433
23318
  * yield a more stable mean at the cost of longer wall-clock time.
23319
+ * @param stopsIndex - Used to resolve stop names for result labels.
22434
23320
  * @returns An array of {@link PerformanceResult} objects, one per query, each
22435
23321
  * containing the mean wall-clock time (µs) and mean heap delta (MB).
22436
23322
  */
22437
- const testRouterPerformance = (router, tasks, iterations) => {
23323
+ const testRouterPerformance = (router, tasks, iterations, stopsIndex) => {
22438
23324
  const results = [];
22439
23325
  for (const task of tasks) {
22440
23326
  let totalTime = 0;
@@ -22454,7 +23340,7 @@ const testRouterPerformance = (router, tasks, iterations) => {
22454
23340
  }
22455
23341
  }
22456
23342
  results.push({
22457
- task,
23343
+ label: buildQueryLabel(task, stopsIndex),
22458
23344
  meanTimeUs: totalTime / iterations,
22459
23345
  meanMemoryMb: totalMemory / iterations / (1024 * 1024),
22460
23346
  });
@@ -22470,14 +23356,14 @@ const testRouterPerformance = (router, tasks, iterations) => {
22470
23356
  * @param tasks - The list of queries to benchmark. One {@link PerformanceResult}
22471
23357
  * is produced per query.
22472
23358
  * @param iterations - Number of times `bestRoute` is called per query.
23359
+ * @param stopsIndex - Used to resolve stop names for result labels.
22473
23360
  * @returns An array of {@link PerformanceResult} objects, one per query, each
22474
23361
  * containing the mean wall-clock time (µs) and mean heap delta (MB) for the
22475
23362
  * `bestRoute` call alone.
22476
23363
  */
22477
- const testBestRoutePerformance = (router, tasks, iterations) => {
23364
+ const testBestRoutePerformance = (router, tasks, iterations, stopsIndex) => {
22478
23365
  const results = [];
22479
23366
  for (const task of tasks) {
22480
- // Compute the routing result once — this is not part of the benchmark.
22481
23367
  const result = router.route(task);
22482
23368
  let totalTime = 0;
22483
23369
  let totalMemory = 0;
@@ -22496,7 +23382,7 @@ const testBestRoutePerformance = (router, tasks, iterations) => {
22496
23382
  }
22497
23383
  }
22498
23384
  results.push({
22499
- task,
23385
+ label: buildQueryLabel(task, stopsIndex),
22500
23386
  meanTimeUs: totalTime / iterations,
22501
23387
  meanMemoryMb: totalMemory / iterations / (1024 * 1024),
22502
23388
  });
@@ -22504,38 +23390,131 @@ const testBestRoutePerformance = (router, tasks, iterations) => {
22504
23390
  return results;
22505
23391
  };
22506
23392
  /**
22507
- * Prints a human-readable summary of performance results to stdout.
23393
+ * Benchmarks {@link Router.rangeRoute} across a set of range queries.
22508
23394
  *
22509
- * Displays an overall mean across all tasks followed by a per-task breakdown.
22510
- * An optional {@link label} is printed as a section header so that results
22511
- * from different benchmark phases (e.g. routing vs. reconstruction) can be
22512
- * told apart when several calls appear in the same run.
23395
+ * @param router - The router instance to benchmark.
23396
+ * @param tasks - The list of range queries to run. One {@link PerformanceResult}
23397
+ * is produced per query.
23398
+ * @param iterations - Number of times each query is repeated.
23399
+ * @param stopsIndex - Used to resolve stop names for result labels.
23400
+ * @returns An array of {@link PerformanceResult} objects, one per query, each
23401
+ * containing the mean wall-clock time (µs) and mean heap delta (MB).
23402
+ */
23403
+ const testRangeRouterPerformance = (router, tasks, iterations, stopsIndex) => {
23404
+ const results = [];
23405
+ for (const task of tasks) {
23406
+ let totalTime = 0;
23407
+ let totalMemory = 0;
23408
+ for (let i = 0; i < iterations; i++) {
23409
+ if (global.gc) {
23410
+ global.gc();
23411
+ }
23412
+ const startMemory = process.memoryUsage().heapUsed;
23413
+ const startTime = performance$1.now();
23414
+ router.rangeRoute(task);
23415
+ const endTime = performance$1.now();
23416
+ const endMemory = process.memoryUsage().heapUsed;
23417
+ totalTime += (endTime - startTime) * 1000;
23418
+ if (endMemory >= startMemory) {
23419
+ totalMemory += endMemory - startMemory;
23420
+ }
23421
+ }
23422
+ results.push({
23423
+ label: buildQueryLabel(task, stopsIndex),
23424
+ meanTimeUs: totalTime / iterations,
23425
+ meanMemoryMb: totalMemory / iterations / (1024 * 1024),
23426
+ });
23427
+ }
23428
+ return results;
23429
+ };
23430
+ /**
23431
+ * Benchmarks {@link RangeResult.getRoutes} — the full Pareto-frontier
23432
+ * reconstruction phase — independently of the range routing phase.
22513
23433
  *
22514
- * @param results - The performance results to display, as returned by
22515
- * {@link testRouterPerformance} or {@link testBestRoutePerformance}.
22516
- * @param label - Optional heading printed above the results block.
22517
- * Defaults to `'Performance Results'`.
23434
+ * @param router - The router instance used to produce the range results that
23435
+ * are then fed into `getRoutes`.
23436
+ * @param tasks - The list of range queries to benchmark. One
23437
+ * {@link PerformanceResult} is produced per query.
23438
+ * @param iterations - Number of times `getRoutes` is called per query.
23439
+ * @param stopsIndex - Used to resolve stop names for result labels.
23440
+ * @returns An array of {@link PerformanceResult} objects, one per query, each
23441
+ * containing the mean wall-clock time (µs) and mean heap delta (MB) for the
23442
+ * `getRoutes` call alone.
23443
+ */
23444
+ const testRangeResultPerformance = (router, tasks, iterations, stopsIndex) => {
23445
+ const results = [];
23446
+ for (const task of tasks) {
23447
+ const rangeResult = router.rangeRoute(task);
23448
+ let totalTime = 0;
23449
+ let totalMemory = 0;
23450
+ for (let i = 0; i < iterations; i++) {
23451
+ if (global.gc) {
23452
+ global.gc();
23453
+ }
23454
+ const startMemory = process.memoryUsage().heapUsed;
23455
+ const startTime = performance$1.now();
23456
+ rangeResult.getRoutes();
23457
+ const endTime = performance$1.now();
23458
+ const endMemory = process.memoryUsage().heapUsed;
23459
+ totalTime += (endTime - startTime) * 1000;
23460
+ if (endMemory >= startMemory) {
23461
+ totalMemory += endMemory - startMemory;
23462
+ }
23463
+ }
23464
+ results.push({
23465
+ label: buildQueryLabel(task, stopsIndex),
23466
+ meanTimeUs: totalTime / iterations,
23467
+ meanMemoryMb: totalMemory / iterations / (1024 * 1024),
23468
+ });
23469
+ }
23470
+ return results;
23471
+ };
23472
+ // ─── Output ───────────────────────────────────────────────────────────────────
23473
+ /**
23474
+ * Prints a table summary of performance results to stdout.
23475
+ *
23476
+ * Each row corresponds to one task, identified by a human-readable query label
23477
+ * (origin → destination + departure time). A footer row shows the mean across
23478
+ * all tasks. An optional `label` is printed as a section header above the table.
23479
+ *
23480
+ * @param results - The performance results to display.
23481
+ * @param label - Heading printed above the table. Defaults to `'Performance Results'`.
22518
23482
  */
22519
23483
  const prettyPrintPerformanceResults = (results, label = 'Performance Results') => {
23484
+ console.log(`\n${label}`);
22520
23485
  if (results.length === 0) {
22521
- console.log('No performance results to display.');
23486
+ console.log(' (no results)');
22522
23487
  return;
22523
23488
  }
22524
- const overallMeanTimeNs = results.reduce((sum, result) => sum + result.meanTimeUs, 0) /
22525
- results.length;
22526
- const overallMeanMemoryMb = results.reduce((sum, result) => sum + result.meanMemoryMb, 0) /
22527
- results.length;
22528
- console.log(`${label}:`);
22529
- console.log(` Mean Time (µs): ${overallMeanTimeNs.toFixed(0)}`);
22530
- console.log(` Mean Memory (MB): ${overallMeanMemoryMb.toFixed(2)}`);
22531
- console.log('');
22532
- console.log('Individual Task Results:');
22533
- results.forEach((result, index) => {
22534
- console.log(`Task ${index + 1}:`);
22535
- console.log(` Mean Time (µs): ${result.meanTimeUs.toFixed(0)}`);
22536
- console.log(` Mean Memory (MB): ${result.meanMemoryMb.toFixed(2)}`);
22537
- console.log('');
23489
+ const fmtTime = (n) => Math.round(n).toLocaleString('en-US');
23490
+ const fmtMem = (n) => n.toFixed(2);
23491
+ const meanTime = results.reduce((s, r) => s + r.meanTimeUs, 0) / results.length;
23492
+ const meanMem = results.reduce((s, r) => s + r.meanMemoryMb, 0) / results.length;
23493
+ const queryHeader = 'Query';
23494
+ const timeHeader = 'Time (µs)';
23495
+ const memHeader = 'Mem (MB)';
23496
+ const timeVals = results.map((r) => fmtTime(r.meanTimeUs));
23497
+ const memVals = results.map((r) => fmtMem(r.meanMemoryMb));
23498
+ const meanTimeStr = fmtTime(meanTime);
23499
+ const meanMemStr = fmtMem(meanMem);
23500
+ const queryWidth = Math.max(queryHeader.length, 'mean'.length, ...results.map((r) => r.label.length));
23501
+ const timeWidth = Math.max(timeHeader.length, meanTimeStr.length, ...timeVals.map((v) => v.length));
23502
+ const memWidth = Math.max(memHeader.length, meanMemStr.length, ...memVals.map((v) => v.length));
23503
+ const columns = [
23504
+ { header: queryHeader, width: queryWidth, align: 'left' },
23505
+ { header: timeHeader, width: timeWidth, align: 'right' },
23506
+ { header: memHeader, width: memWidth, align: 'right' },
23507
+ ];
23508
+ const rows = results.map((r, i) => {
23509
+ var _a, _b;
23510
+ return [
23511
+ r.label,
23512
+ (_a = timeVals[i]) !== null && _a !== void 0 ? _a : '',
23513
+ (_b = memVals[i]) !== null && _b !== void 0 ? _b : '',
23514
+ ];
22538
23515
  });
23516
+ const footer = ['mean', meanTimeStr, meanMemStr];
23517
+ console.log(renderTable(columns, rows, footer));
22539
23518
  };
22540
23519
 
22541
23520
  /**
@@ -22584,25 +23563,39 @@ const startRepl = (stopsPath, timetablePath) => {
22584
23563
  },
22585
23564
  });
22586
23565
  replServer.defineCommand('route', {
22587
- help: 'Find a route using .route from <stationIdOrName> to <stationIdOrName> at <HH:mm> [with <N> transfers]',
23566
+ help: 'Find a route using .route from <stop> to <stop> at <HH:mm> [before <HH:mm>] [with <N> transfers]',
22588
23567
  action(routeQuery) {
22589
23568
  this.clearBufferedCommand();
22590
23569
  const parts = routeQuery.split(' ').filter(Boolean);
22591
- const withTransfersIndex = parts.indexOf('with');
22592
- const maxTransfers = withTransfersIndex !== -1 && parts[withTransfersIndex + 1] !== undefined
22593
- ? parseInt(parts[withTransfersIndex + 1])
22594
- : 4;
22595
- const atTime = parts
22596
- .slice(withTransfersIndex === -1
22597
- ? parts.indexOf('at') + 1
22598
- : parts.indexOf('at') + 1, withTransfersIndex === -1 ? parts.length : withTransfersIndex)
22599
- .join(' ');
22600
23570
  const fromIndex = parts.indexOf('from');
22601
23571
  const toIndex = parts.indexOf('to');
23572
+ const atIndex = parts.indexOf('at');
23573
+ const beforeIndex = parts.indexOf('before');
23574
+ const withIndex = parts.indexOf('with');
23575
+ if (fromIndex === -1 || toIndex === -1 || atIndex === -1) {
23576
+ console.log('Usage: .route from <stop> to <stop> at <HH:mm> [before <HH:mm>] [with <N> transfers]');
23577
+ this.displayPrompt();
23578
+ return;
23579
+ }
22602
23580
  const fromId = parts.slice(fromIndex + 1, toIndex).join(' ');
22603
- const toId = parts.slice(toIndex + 1, parts.indexOf('at')).join(' ');
23581
+ const toId = parts.slice(toIndex + 1, atIndex).join(' ');
23582
+ // atTime ends at 'before', 'with', or the end of the input.
23583
+ const atTimeEnd = beforeIndex !== -1
23584
+ ? beforeIndex
23585
+ : withIndex !== -1
23586
+ ? withIndex
23587
+ : parts.length;
23588
+ const atTime = parts.slice(atIndex + 1, atTimeEnd).join(' ');
23589
+ // beforeTime is only present when the 'before' keyword appears.
23590
+ const beforeTimeEnd = withIndex !== -1 ? withIndex : parts.length;
23591
+ const beforeTime = beforeIndex !== -1
23592
+ ? parts.slice(beforeIndex + 1, beforeTimeEnd).join(' ')
23593
+ : undefined;
23594
+ const maxTransfers = withIndex !== -1 && parts[withIndex + 1] !== undefined
23595
+ ? parseInt(parts[withIndex + 1])
23596
+ : 4;
22604
23597
  if (!fromId || !toId || !atTime) {
22605
- console.log('Usage: .route from <stationIdOrName> to <stationIdOrName> at <HH:mm> [with <N> transfers]');
23598
+ console.log('Usage: .route from <stop> to <stop> at <HH:mm> [before <HH:mm>] [with <N> transfers]');
22606
23599
  this.displayPrompt();
22607
23600
  return;
22608
23601
  }
@@ -22626,30 +23619,61 @@ const startRepl = (stopsPath, timetablePath) => {
22626
23619
  this.displayPrompt();
22627
23620
  return;
22628
23621
  }
22629
- const departureTime = timeFromString(atTime);
22630
23622
  try {
22631
- const query = new Query.Builder()
22632
- .from(fromStop.id)
22633
- .to(toStop.id)
22634
- .departureTime(departureTime)
22635
- .maxTransfers(maxTransfers)
22636
- .build();
23623
+ const departureTime = timeFromString(atTime);
22637
23624
  const router = new Router(timetable, stopsIndex);
22638
- const result = router.route(query);
22639
- const arrivalTime = result.arrivalAt(toStop.id);
22640
- if (arrivalTime === undefined) {
22641
- console.log(`Destination not reachable`);
22642
- }
22643
- else {
22644
- console.log(`Arriving to ${toStop.name} at ${timeToString(arrivalTime.arrival)} with ${arrivalTime.legNumber - 1} transfers from ${fromStop.name}.`);
22645
- }
22646
- const bestRoute = result.bestRoute(toStop.id);
22647
- if (bestRoute) {
22648
- console.log(`Found route from ${fromStop.name} to ${toStop.name}:`);
22649
- console.log(bestRoute.toString());
23625
+ if (beforeTime !== undefined) {
23626
+ const lastDepartureTime = timeFromString(beforeTime);
23627
+ const query = new RangeQuery.Builder()
23628
+ .from(fromStop.id)
23629
+ .to(toStop.id)
23630
+ .departureTime(departureTime)
23631
+ .lastDepartureTime(lastDepartureTime)
23632
+ .maxTransfers(maxTransfers)
23633
+ .build();
23634
+ const result = router.rangeRoute(query);
23635
+ if (result.size === 0) {
23636
+ console.log(`No journeys found from ${fromStop.name} to ${toStop.name} ` +
23637
+ `between ${atTime} and ${beforeTime}.`);
23638
+ }
23639
+ else {
23640
+ console.log(`Found ${result.size} Pareto-optimal journey${result.size === 1 ? '' : 's'} ` +
23641
+ `from ${fromStop.name} to ${toStop.name} ` +
23642
+ `(window ${atTime}–${beforeTime}):`);
23643
+ const routes = result.getRoutes();
23644
+ routes.forEach((route, index) => {
23645
+ const journeyNumber = index + 1;
23646
+ console.log(`\nJourney ${journeyNumber}:`);
23647
+ console.log(route.toString());
23648
+ });
23649
+ }
22650
23650
  }
22651
23651
  else {
22652
- console.log('No route found');
23652
+ const query = new Query.Builder()
23653
+ .from(fromStop.id)
23654
+ .to(toStop.id)
23655
+ .departureTime(departureTime)
23656
+ .maxTransfers(maxTransfers)
23657
+ .build();
23658
+ const result = router.route(query);
23659
+ const arrivalTime = result.arrivalAt(toStop.id);
23660
+ if (arrivalTime === undefined) {
23661
+ console.log(`Destination not reachable`);
23662
+ }
23663
+ else {
23664
+ const transfers = Math.max(0, arrivalTime.legNumber - 1);
23665
+ console.log(`Arriving to ${toStop.name} at ${timeToString(arrivalTime.arrival)} ` +
23666
+ `with ${transfers} transfer${transfers === 1 ? '' : 's'} ` +
23667
+ `from ${fromStop.name}.`);
23668
+ }
23669
+ const bestRoute = result.bestRoute(toStop.id);
23670
+ if (bestRoute) {
23671
+ console.log(`Found route from ${fromStop.name} to ${toStop.name}:`);
23672
+ console.log(bestRoute.toString());
23673
+ }
23674
+ else {
23675
+ console.log('No route found');
23676
+ }
22653
23677
  }
22654
23678
  }
22655
23679
  catch (error) {
@@ -23009,10 +24033,15 @@ program
23009
24033
  const router = new Router(timetable, stopsIndex);
23010
24034
  const queries = loadQueriesFromJson(routesPath, stopsIndex);
23011
24035
  const iterations = parseInt(options.iterations, 10);
23012
- const routerResults = testRouterPerformance(router, queries, iterations);
23013
- prettyPrintPerformanceResults(routerResults, 'Router Performance');
23014
- const bestRouteResults = testBestRoutePerformance(router, queries, iterations);
23015
- prettyPrintPerformanceResults(bestRouteResults, 'bestRoute Performance (reconstruction only)');
24036
+ const routerResults = testRouterPerformance(router, queries, iterations, stopsIndex);
24037
+ prettyPrintPerformanceResults(routerResults, 'Point queries — router.route()');
24038
+ const bestRouteResults = testBestRoutePerformance(router, queries, iterations, stopsIndex);
24039
+ prettyPrintPerformanceResults(bestRouteResults, 'Point queries — result.bestRoute() (reconstruction only)');
24040
+ const rangeQueries = loadRangeQueriesFromJson(routesPath, stopsIndex);
24041
+ const rangeRouterResults = testRangeRouterPerformance(router, rangeQueries, iterations, stopsIndex);
24042
+ prettyPrintPerformanceResults(rangeRouterResults, 'Range queries — router.rangeRoute()');
24043
+ const rangeResultResults = testRangeResultPerformance(router, rangeQueries, iterations, stopsIndex);
24044
+ prettyPrintPerformanceResults(rangeResultResults, 'Range queries — rangeResult.getRoutes() (reconstruction only)');
23016
24045
  });
23017
24046
  program.parse(process.argv);
23018
24047
  //# sourceMappingURL=cli.mjs.map