minotor 11.2.2 → 11.2.3

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.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,6 @@
1
- ## [11.2.2](https://github.com/aubryio/minotor/compare/v11.2.1...v11.2.2) (2026-04-27)
1
+ ## [11.2.3](https://github.com/aubryio/minotor/compare/v11.2.2...v11.2.3) (2026-05-04)
2
2
 
3
3
 
4
- ### Bug Fixes
4
+ ### Performance Improvements
5
5
 
6
- * prevent higher-round rRAPTOR labels from overwriting equal or better aggregate arrivals ([#69](https://github.com/aubryio/minotor/issues/69)) ([a35c4e2](https://github.com/aubryio/minotor/commit/a35c4e23caab026972fbab78e54c0de2a905feda))
6
+ * max duration query option ([#70](https://github.com/aubryio/minotor/issues/70)) ([4d7fce3](https://github.com/aubryio/minotor/commit/4d7fce31f5af80d97ac095be0b91e8daec10eb68))
package/README.md CHANGED
@@ -111,6 +111,7 @@ Query options:
111
111
  | ----------------------- | --------- | ---------------------------------------------------------------------- |
112
112
  | `maxTransfers` | `5` | Maximum number of transfers |
113
113
  | `minTransferTime` | `2 min` | Fallback minimum transfer time |
114
+ | `maxDuration` | unlimited | Maximum total journey duration from the query departure time |
114
115
  | `maxInitialWaitingTime` | unlimited | Maximum wait for the first vehicle after arriving at the boarding stop |
115
116
  | `transportModes` | all | Restrict to a subset of GTFS route types |
116
117
 
package/dist/cli.mjs CHANGED
@@ -21659,6 +21659,15 @@ Query.Builder = class {
21659
21659
  this.optionsValue.transportModes = transportModes;
21660
21660
  return this;
21661
21661
  }
21662
+ /**
21663
+ * Sets the maximum total journey duration (in minutes) from the query
21664
+ * departure time. The limit includes initial access, waiting time, transit
21665
+ * legs, and transfers.
21666
+ */
21667
+ maxDuration(maxDuration) {
21668
+ this.optionsValue.maxDuration = maxDuration;
21669
+ return this;
21670
+ }
21662
21671
  /**
21663
21672
  * Sets the maximum time (in minutes) the traveler is willing to wait at
21664
21673
  * the first boarding stop before the first transit vehicle departs.
@@ -22231,12 +22240,17 @@ const UNREACHED_TIME = 0xffff;
22231
22240
  * Encapsulates all mutable state for a single RAPTOR routing query.
22232
22241
  */
22233
22242
  class RoutingState {
22234
- constructor(departureTime, destinations, accessPaths, nbStops, maxRounds = 0) {
22243
+ constructor(departureTime, destinations, accessPaths, nbStops, maxRounds = 0, maxDuration) {
22235
22244
  /**
22236
22245
  * Cached best arrival time at any destination stop, kept up-to-date by
22237
22246
  * {@link updateArrival} so that destination pruning is always O(1).
22238
22247
  */
22239
22248
  this._destinationBest = UNREACHED_TIME;
22249
+ /**
22250
+ * Maximum arrival time allowed for this run. Defaults to UNREACHED_TIME when
22251
+ * the query has no maxDuration limit.
22252
+ */
22253
+ this.maxArrivalTime = UNREACHED_TIME;
22240
22254
  /**
22241
22255
  * Every stop that has received an arrival improvement during the current run,
22242
22256
  * in the order the improvements occurred. Used by {@link resetFor} to clear
@@ -22244,6 +22258,9 @@ class RoutingState {
22244
22258
  */
22245
22259
  this.reachedStops = [];
22246
22260
  this.destinations = destinations;
22261
+ this.maxDuration = maxDuration;
22262
+ this.maxArrivalTime =
22263
+ maxDuration === undefined ? UNREACHED_TIME : departureTime + maxDuration;
22247
22264
  this.destinationSet = new Set(destinations);
22248
22265
  this.earliestArrivalTimes = new Uint16Array(nbStops).fill(UNREACHED_TIME);
22249
22266
  this.earliestArrivalLegs = new Uint8Array(nbStops);
@@ -22265,6 +22282,8 @@ class RoutingState {
22265
22282
  const seededOrigins = new Set();
22266
22283
  for (const access of accessPaths) {
22267
22284
  const arrival = depTime + access.duration;
22285
+ if (arrival > this.maxArrivalTime)
22286
+ continue;
22268
22287
  const edge = access.duration === 0
22269
22288
  ? { stopId: access.fromStopId, arrival: depTime }
22270
22289
  : {
@@ -22356,6 +22375,10 @@ class RoutingState {
22356
22375
  }
22357
22376
  this.reachedStops.length = 0;
22358
22377
  this._destinationBest = UNREACHED_TIME;
22378
+ this.maxArrivalTime =
22379
+ this.maxDuration === undefined
22380
+ ? UNREACHED_TIME
22381
+ : depTime + this.maxDuration;
22359
22382
  this.seedAccessPaths(depTime, accessPaths);
22360
22383
  }
22361
22384
  /**
@@ -22483,7 +22506,7 @@ class PlainRouter {
22483
22506
  const destinations = Array.from(query.to)
22484
22507
  .flatMap((destination) => this.stopsIndex.equivalentStops(destination))
22485
22508
  .map((destination) => destination.id);
22486
- const routingState = new RoutingState(query.departureTime, destinations, accessLegs, this.timetable.nbStops(), query.options.maxTransfers + 1);
22509
+ const routingState = new RoutingState(query.departureTime, destinations, accessLegs, this.timetable.nbStops(), query.options.maxTransfers + 1, query.options.maxDuration);
22487
22510
  this.raptor.run(query.options, routingState);
22488
22511
  return new Result(new Set(destinations), routingState, this.stopsIndex, this.timetable);
22489
22512
  }
@@ -22812,6 +22835,9 @@ class RangeRaptorState {
22812
22835
  get destinationBest() {
22813
22836
  return this._destinationBest;
22814
22837
  }
22838
+ get maxArrivalTime() {
22839
+ return this.currentRun.maxArrivalTime;
22840
+ }
22815
22841
  isDestination(stop) {
22816
22842
  return this.currentRun.isDestination(stop);
22817
22843
  }
@@ -22899,7 +22925,7 @@ class RangeRouter {
22899
22925
  const trivialDestCovered = new Set();
22900
22926
  let routingState = null;
22901
22927
  if (query.rangeOptions.optimizeBeyondLatestDeparture) {
22902
- routingState = new RoutingState(latest + 1, destinations, accessLegs, this.timetable.nbStops(), maxRounds);
22928
+ routingState = new RoutingState(latest + 1, destinations, accessLegs, this.timetable.nbStops(), maxRounds, query.options.maxDuration);
22903
22929
  rangeState.setCurrentRun(routingState);
22904
22930
  this.raptor.run(Object.assign(Object.assign({}, query.options), { maxInitialWaitingTime: undefined }), rangeState);
22905
22931
  if (!noDestinations) {
@@ -22915,7 +22941,7 @@ class RangeRouter {
22915
22941
  break;
22916
22942
  }
22917
22943
  if (routingState === null) {
22918
- routingState = new RoutingState(depTime, destinations, legs, this.timetable.nbStops(), maxRounds);
22944
+ routingState = new RoutingState(depTime, destinations, legs, this.timetable.nbStops(), maxRounds, query.options.maxDuration);
22919
22945
  }
22920
22946
  else {
22921
22947
  routingState.resetFor(depTime, legs);
@@ -23044,6 +23070,7 @@ class Raptor {
23044
23070
  const arrivalTime = route.arrivalAtOffset(currentStopIndex, tripStopOffset);
23045
23071
  const dropOffType = route.dropOffTypeAtOffset(currentStopIndex, tripStopOffset);
23046
23072
  if (dropOffType !== NOT_AVAILABLE &&
23073
+ arrivalTime <= state.maxArrivalTime &&
23047
23074
  arrivalTime < state.improvementBound(round, currentStop) &&
23048
23075
  arrivalTime < state.destinationBest) {
23049
23076
  edgesAtCurrentRound[currentStop] = {
@@ -23091,6 +23118,7 @@ class Raptor {
23091
23118
  const arrivalTime = route.arrivalAtOffset(currentStopIndex, activeTripStopOffset);
23092
23119
  const dropOffType = route.dropOffTypeAtOffset(currentStopIndex, activeTripStopOffset);
23093
23120
  if (dropOffType !== NOT_AVAILABLE &&
23121
+ arrivalTime <= state.maxArrivalTime &&
23094
23122
  arrivalTime < state.improvementBound(round, currentStop) &&
23095
23123
  arrivalTime < state.destinationBest) {
23096
23124
  edgesAtCurrentRound[currentStop] = {
@@ -23124,15 +23152,16 @@ class Raptor {
23124
23152
  : undefined;
23125
23153
  const firstBoardableTrip = this.timetable.findFirstBoardableTrip(currentStopIndex, route, earliestTrip, earliestArrivalOnPreviousRound, activeTripIndex, fromTripStop, options.minTransferTime);
23126
23154
  if (firstBoardableTrip !== undefined) {
23155
+ const departureTime = route.departureFrom(currentStopIndex, firstBoardableTrip);
23127
23156
  // At round 1, enforce maxInitialWaitingTime: skip boarding if the
23128
23157
  // traveler would have to wait longer than the allowed threshold at
23129
23158
  // the first boarding stop.
23130
23159
  const exceedsInitialWait = round === 1 &&
23131
23160
  options.maxInitialWaitingTime !== undefined &&
23132
- route.departureFrom(currentStopIndex, firstBoardableTrip) -
23133
- earliestArrivalOnPreviousRound >
23161
+ departureTime - earliestArrivalOnPreviousRound >
23134
23162
  options.maxInitialWaitingTime;
23135
- if (!exceedsInitialWait) {
23163
+ const exceedsMaxDuration = departureTime > state.maxArrivalTime;
23164
+ if (!exceedsInitialWait && !exceedsMaxDuration) {
23136
23165
  activeTripIndex = firstBoardableTrip;
23137
23166
  activeTripBoardStopIndex = currentStopIndex;
23138
23167
  activeTripStopOffset = route.tripStopOffset(firstBoardableTrip);
@@ -23174,13 +23203,14 @@ class Raptor {
23174
23203
  transferTime = options.minTransferTime;
23175
23204
  }
23176
23205
  const arrivalAfterTransfer = currentArrival.arrival + transferTime;
23177
- if (arrivalAfterTransfer <
23178
- state.improvementBound(round, transfer.destination) &&
23206
+ if (arrivalAfterTransfer <= state.maxArrivalTime &&
23207
+ arrivalAfterTransfer <
23208
+ state.improvementBound(round, transfer.destination) &&
23179
23209
  arrivalAfterTransfer < state.destinationBest) {
23180
23210
  arrivalsAtCurrentRound[transfer.destination] = {
23181
23211
  arrival: arrivalAfterTransfer,
23182
23212
  from: stop,
23183
- to: transfer.destination,
23213
+ to: transfer.destination, // TODO needed?
23184
23214
  minTransferTime: transferTime || undefined,
23185
23215
  type: transfer.type,
23186
23216
  };
@@ -23294,7 +23324,8 @@ const loadQueriesFromJson = (filePath, stopsIndex) => {
23294
23324
  .from(fromStop.id)
23295
23325
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
23296
23326
  .to(new Set(toStops.map((stop) => stop.id)))
23297
- .departureTime(timeFromString(serializedQuery.departureTime));
23327
+ .departureTime(timeFromString(serializedQuery.departureTime))
23328
+ .maxDuration(6 * 60);
23298
23329
  if (serializedQuery.maxTransfers !== undefined) {
23299
23330
  queryBuilder.maxTransfers(serializedQuery.maxTransfers);
23300
23331
  }
@@ -23333,7 +23364,8 @@ const loadRangeQueriesFromJson = (filePath, stopsIndex) => {
23333
23364
  .to(new Set(toStops.map((stop) => stop.id)))
23334
23365
  .departureTime(timeFromString(serializedQuery.departureTime))
23335
23366
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
23336
- .lastDepartureTime(timeFromString(serializedQuery.lastDepartureTime));
23367
+ .lastDepartureTime(timeFromString(serializedQuery.lastDepartureTime))
23368
+ .maxDuration(6 * 60);
23337
23369
  if (serializedQuery.maxTransfers !== undefined) {
23338
23370
  queryBuilder.maxTransfers(serializedQuery.maxTransfers);
23339
23371
  }
@@ -23595,8 +23627,9 @@ const startRepl = (stopsPath, timetablePath) => {
23595
23627
  this.displayPrompt();
23596
23628
  },
23597
23629
  });
23630
+ const routeSyntax = '.route from <stop> to <stop> at <HH:mm> [before <HH:mm>] [with <N> transfers] [wait max <N> minutes]';
23598
23631
  replServer.defineCommand('route', {
23599
- help: 'Find a route using .route from <stop> to <stop> at <HH:mm> [before <HH:mm>] [with <N> transfers]',
23632
+ help: `Find a route using ${routeSyntax}`,
23600
23633
  action(routeQuery) {
23601
23634
  this.clearBufferedCommand();
23602
23635
  const parts = routeQuery.split(' ').filter(Boolean);
@@ -23605,30 +23638,40 @@ const startRepl = (stopsPath, timetablePath) => {
23605
23638
  const atIndex = parts.indexOf('at');
23606
23639
  const beforeIndex = parts.indexOf('before');
23607
23640
  const withIndex = parts.indexOf('with');
23641
+ const waitIndex = parts.indexOf('wait');
23642
+ const routeClauseIndexes = [beforeIndex, withIndex, waitIndex].filter((index) => index !== -1);
23608
23643
  if (fromIndex === -1 || toIndex === -1 || atIndex === -1) {
23609
- console.log('Usage: .route from <stop> to <stop> at <HH:mm> [before <HH:mm>] [with <N> transfers]');
23644
+ console.log(`Usage: ${routeSyntax}`);
23610
23645
  this.displayPrompt();
23611
23646
  return;
23612
23647
  }
23613
23648
  const fromId = parts.slice(fromIndex + 1, toIndex).join(' ');
23614
23649
  const toId = parts.slice(toIndex + 1, atIndex).join(' ');
23615
- // atTime ends at 'before', 'with', or the end of the input.
23616
- const atTimeEnd = beforeIndex !== -1
23617
- ? beforeIndex
23618
- : withIndex !== -1
23619
- ? withIndex
23620
- : parts.length;
23650
+ // atTime ends at 'before', 'with', 'wait', or the end of the input.
23651
+ const atTimeEnd = Math.min(...routeClauseIndexes.filter((index) => index > atIndex), parts.length);
23621
23652
  const atTime = parts.slice(atIndex + 1, atTimeEnd).join(' ');
23622
23653
  // beforeTime is only present when the 'before' keyword appears.
23623
- const beforeTimeEnd = withIndex !== -1 ? withIndex : parts.length;
23654
+ const beforeTimeEnd = Math.min(...[withIndex, waitIndex].filter((index) => index !== -1 && index > beforeIndex), parts.length);
23624
23655
  const beforeTime = beforeIndex !== -1
23625
23656
  ? parts.slice(beforeIndex + 1, beforeTimeEnd).join(' ')
23626
23657
  : undefined;
23627
23658
  const maxTransfers = withIndex !== -1 && parts[withIndex + 1] !== undefined
23628
23659
  ? parseInt(parts[withIndex + 1])
23629
23660
  : 4;
23630
- if (!fromId || !toId || !atTime) {
23631
- console.log('Usage: .route from <stop> to <stop> at <HH:mm> [before <HH:mm>] [with <N> transfers]');
23661
+ const maxInitialWaitingTime = waitIndex !== -1 &&
23662
+ parts[waitIndex + 1] === 'max' &&
23663
+ parts[waitIndex + 2] !== undefined
23664
+ ? Number(parts[waitIndex + 2])
23665
+ : undefined;
23666
+ const waitUnit = parts[waitIndex + 3];
23667
+ const hasInvalidWaitClause = waitIndex !== -1 &&
23668
+ (parts[waitIndex + 1] !== 'max' ||
23669
+ maxInitialWaitingTime === undefined ||
23670
+ !Number.isFinite(maxInitialWaitingTime) ||
23671
+ maxInitialWaitingTime < 0 ||
23672
+ (waitUnit !== 'minute' && waitUnit !== 'minutes'));
23673
+ if (!fromId || !toId || !atTime || hasInvalidWaitClause) {
23674
+ console.log(`Usage: ${routeSyntax}`);
23632
23675
  this.displayPrompt();
23633
23676
  return;
23634
23677
  }
@@ -23657,13 +23700,16 @@ const startRepl = (stopsPath, timetablePath) => {
23657
23700
  const router = new Router(timetable, stopsIndex);
23658
23701
  if (beforeTime !== undefined) {
23659
23702
  const lastDepartureTime = timeFromString(beforeTime);
23660
- const query = new RangeQuery.Builder()
23703
+ const queryBuilder = new RangeQuery.Builder()
23661
23704
  .from(fromStop.id)
23662
23705
  .to(toStop.id)
23663
23706
  .departureTime(departureTime)
23664
23707
  .lastDepartureTime(lastDepartureTime)
23665
- .maxTransfers(maxTransfers)
23666
- .build();
23708
+ .maxTransfers(maxTransfers);
23709
+ if (maxInitialWaitingTime !== undefined) {
23710
+ queryBuilder.maxInitialWaitingTime(maxInitialWaitingTime);
23711
+ }
23712
+ const query = queryBuilder.build();
23667
23713
  const result = router.rangeRoute(query);
23668
23714
  if (result.size === 0) {
23669
23715
  console.log(`No journeys found from ${fromStop.name} to ${toStop.name} ` +
@@ -23682,12 +23728,15 @@ const startRepl = (stopsPath, timetablePath) => {
23682
23728
  }
23683
23729
  }
23684
23730
  else {
23685
- const query = new Query.Builder()
23731
+ const queryBuilder = new Query.Builder()
23686
23732
  .from(fromStop.id)
23687
23733
  .to(toStop.id)
23688
23734
  .departureTime(departureTime)
23689
- .maxTransfers(maxTransfers)
23690
- .build();
23735
+ .maxTransfers(maxTransfers);
23736
+ if (maxInitialWaitingTime !== undefined) {
23737
+ queryBuilder.maxInitialWaitingTime(maxInitialWaitingTime);
23738
+ }
23739
+ const query = queryBuilder.build();
23691
23740
  const result = router.route(query);
23692
23741
  const arrivalTime = result.arrivalAt(toStop.id);
23693
23742
  if (arrivalTime === undefined) {