minotor 11.2.1 → 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.1](https://github.com/aubryio/minotor/compare/v11.2.0...v11.2.1) (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
- * range raptor with no destination should cover the full network ([#68](https://github.com/aubryio/minotor/issues/68)) ([8a47577](https://github.com/aubryio/minotor/commit/8a4757739a84db478443ea2dd77bb6867e4b6851))
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,12 +22835,22 @@ 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
  }
22818
- /** Updates both the per-run state and the cross-run shared labels. */
22844
+ /** Updates the per-run aggregate best when improved, and always considers the cross-run shared label. */
22819
22845
  updateArrival(stop, time, round) {
22820
- this.currentRun.updateArrival(stop, time, round);
22846
+ const currentRunArrival = this.currentRun.getArrival(stop);
22847
+ const improvesCurrentRunAggregate = currentRunArrival === undefined ||
22848
+ time < currentRunArrival.arrival ||
22849
+ (time === currentRunArrival.arrival &&
22850
+ round < currentRunArrival.legNumber);
22851
+ if (improvesCurrentRunAggregate) {
22852
+ this.currentRun.updateArrival(stop, time, round);
22853
+ }
22821
22854
  if (time < this.roundLabels[round][stop]) {
22822
22855
  this.roundLabels[round][stop] = time;
22823
22856
  this.changedInRound[round].push(stop);
@@ -22892,7 +22925,7 @@ class RangeRouter {
22892
22925
  const trivialDestCovered = new Set();
22893
22926
  let routingState = null;
22894
22927
  if (query.rangeOptions.optimizeBeyondLatestDeparture) {
22895
- 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);
22896
22929
  rangeState.setCurrentRun(routingState);
22897
22930
  this.raptor.run(Object.assign(Object.assign({}, query.options), { maxInitialWaitingTime: undefined }), rangeState);
22898
22931
  if (!noDestinations) {
@@ -22908,7 +22941,7 @@ class RangeRouter {
22908
22941
  break;
22909
22942
  }
22910
22943
  if (routingState === null) {
22911
- routingState = new RoutingState(depTime, destinations, legs, this.timetable.nbStops(), maxRounds);
22944
+ routingState = new RoutingState(depTime, destinations, legs, this.timetable.nbStops(), maxRounds, query.options.maxDuration);
22912
22945
  }
22913
22946
  else {
22914
22947
  routingState.resetFor(depTime, legs);
@@ -23037,6 +23070,7 @@ class Raptor {
23037
23070
  const arrivalTime = route.arrivalAtOffset(currentStopIndex, tripStopOffset);
23038
23071
  const dropOffType = route.dropOffTypeAtOffset(currentStopIndex, tripStopOffset);
23039
23072
  if (dropOffType !== NOT_AVAILABLE &&
23073
+ arrivalTime <= state.maxArrivalTime &&
23040
23074
  arrivalTime < state.improvementBound(round, currentStop) &&
23041
23075
  arrivalTime < state.destinationBest) {
23042
23076
  edgesAtCurrentRound[currentStop] = {
@@ -23084,6 +23118,7 @@ class Raptor {
23084
23118
  const arrivalTime = route.arrivalAtOffset(currentStopIndex, activeTripStopOffset);
23085
23119
  const dropOffType = route.dropOffTypeAtOffset(currentStopIndex, activeTripStopOffset);
23086
23120
  if (dropOffType !== NOT_AVAILABLE &&
23121
+ arrivalTime <= state.maxArrivalTime &&
23087
23122
  arrivalTime < state.improvementBound(round, currentStop) &&
23088
23123
  arrivalTime < state.destinationBest) {
23089
23124
  edgesAtCurrentRound[currentStop] = {
@@ -23117,15 +23152,16 @@ class Raptor {
23117
23152
  : undefined;
23118
23153
  const firstBoardableTrip = this.timetable.findFirstBoardableTrip(currentStopIndex, route, earliestTrip, earliestArrivalOnPreviousRound, activeTripIndex, fromTripStop, options.minTransferTime);
23119
23154
  if (firstBoardableTrip !== undefined) {
23155
+ const departureTime = route.departureFrom(currentStopIndex, firstBoardableTrip);
23120
23156
  // At round 1, enforce maxInitialWaitingTime: skip boarding if the
23121
23157
  // traveler would have to wait longer than the allowed threshold at
23122
23158
  // the first boarding stop.
23123
23159
  const exceedsInitialWait = round === 1 &&
23124
23160
  options.maxInitialWaitingTime !== undefined &&
23125
- route.departureFrom(currentStopIndex, firstBoardableTrip) -
23126
- earliestArrivalOnPreviousRound >
23161
+ departureTime - earliestArrivalOnPreviousRound >
23127
23162
  options.maxInitialWaitingTime;
23128
- if (!exceedsInitialWait) {
23163
+ const exceedsMaxDuration = departureTime > state.maxArrivalTime;
23164
+ if (!exceedsInitialWait && !exceedsMaxDuration) {
23129
23165
  activeTripIndex = firstBoardableTrip;
23130
23166
  activeTripBoardStopIndex = currentStopIndex;
23131
23167
  activeTripStopOffset = route.tripStopOffset(firstBoardableTrip);
@@ -23167,13 +23203,14 @@ class Raptor {
23167
23203
  transferTime = options.minTransferTime;
23168
23204
  }
23169
23205
  const arrivalAfterTransfer = currentArrival.arrival + transferTime;
23170
- if (arrivalAfterTransfer <
23171
- state.improvementBound(round, transfer.destination) &&
23206
+ if (arrivalAfterTransfer <= state.maxArrivalTime &&
23207
+ arrivalAfterTransfer <
23208
+ state.improvementBound(round, transfer.destination) &&
23172
23209
  arrivalAfterTransfer < state.destinationBest) {
23173
23210
  arrivalsAtCurrentRound[transfer.destination] = {
23174
23211
  arrival: arrivalAfterTransfer,
23175
23212
  from: stop,
23176
- to: transfer.destination,
23213
+ to: transfer.destination, // TODO needed?
23177
23214
  minTransferTime: transferTime || undefined,
23178
23215
  type: transfer.type,
23179
23216
  };
@@ -23287,7 +23324,8 @@ const loadQueriesFromJson = (filePath, stopsIndex) => {
23287
23324
  .from(fromStop.id)
23288
23325
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
23289
23326
  .to(new Set(toStops.map((stop) => stop.id)))
23290
- .departureTime(timeFromString(serializedQuery.departureTime));
23327
+ .departureTime(timeFromString(serializedQuery.departureTime))
23328
+ .maxDuration(6 * 60);
23291
23329
  if (serializedQuery.maxTransfers !== undefined) {
23292
23330
  queryBuilder.maxTransfers(serializedQuery.maxTransfers);
23293
23331
  }
@@ -23326,7 +23364,8 @@ const loadRangeQueriesFromJson = (filePath, stopsIndex) => {
23326
23364
  .to(new Set(toStops.map((stop) => stop.id)))
23327
23365
  .departureTime(timeFromString(serializedQuery.departureTime))
23328
23366
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
23329
- .lastDepartureTime(timeFromString(serializedQuery.lastDepartureTime));
23367
+ .lastDepartureTime(timeFromString(serializedQuery.lastDepartureTime))
23368
+ .maxDuration(6 * 60);
23330
23369
  if (serializedQuery.maxTransfers !== undefined) {
23331
23370
  queryBuilder.maxTransfers(serializedQuery.maxTransfers);
23332
23371
  }
@@ -23588,8 +23627,9 @@ const startRepl = (stopsPath, timetablePath) => {
23588
23627
  this.displayPrompt();
23589
23628
  },
23590
23629
  });
23630
+ const routeSyntax = '.route from <stop> to <stop> at <HH:mm> [before <HH:mm>] [with <N> transfers] [wait max <N> minutes]';
23591
23631
  replServer.defineCommand('route', {
23592
- 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}`,
23593
23633
  action(routeQuery) {
23594
23634
  this.clearBufferedCommand();
23595
23635
  const parts = routeQuery.split(' ').filter(Boolean);
@@ -23598,30 +23638,40 @@ const startRepl = (stopsPath, timetablePath) => {
23598
23638
  const atIndex = parts.indexOf('at');
23599
23639
  const beforeIndex = parts.indexOf('before');
23600
23640
  const withIndex = parts.indexOf('with');
23641
+ const waitIndex = parts.indexOf('wait');
23642
+ const routeClauseIndexes = [beforeIndex, withIndex, waitIndex].filter((index) => index !== -1);
23601
23643
  if (fromIndex === -1 || toIndex === -1 || atIndex === -1) {
23602
- console.log('Usage: .route from <stop> to <stop> at <HH:mm> [before <HH:mm>] [with <N> transfers]');
23644
+ console.log(`Usage: ${routeSyntax}`);
23603
23645
  this.displayPrompt();
23604
23646
  return;
23605
23647
  }
23606
23648
  const fromId = parts.slice(fromIndex + 1, toIndex).join(' ');
23607
23649
  const toId = parts.slice(toIndex + 1, atIndex).join(' ');
23608
- // atTime ends at 'before', 'with', or the end of the input.
23609
- const atTimeEnd = beforeIndex !== -1
23610
- ? beforeIndex
23611
- : withIndex !== -1
23612
- ? withIndex
23613
- : 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);
23614
23652
  const atTime = parts.slice(atIndex + 1, atTimeEnd).join(' ');
23615
23653
  // beforeTime is only present when the 'before' keyword appears.
23616
- const beforeTimeEnd = withIndex !== -1 ? withIndex : parts.length;
23654
+ const beforeTimeEnd = Math.min(...[withIndex, waitIndex].filter((index) => index !== -1 && index > beforeIndex), parts.length);
23617
23655
  const beforeTime = beforeIndex !== -1
23618
23656
  ? parts.slice(beforeIndex + 1, beforeTimeEnd).join(' ')
23619
23657
  : undefined;
23620
23658
  const maxTransfers = withIndex !== -1 && parts[withIndex + 1] !== undefined
23621
23659
  ? parseInt(parts[withIndex + 1])
23622
23660
  : 4;
23623
- if (!fromId || !toId || !atTime) {
23624
- 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}`);
23625
23675
  this.displayPrompt();
23626
23676
  return;
23627
23677
  }
@@ -23650,13 +23700,16 @@ const startRepl = (stopsPath, timetablePath) => {
23650
23700
  const router = new Router(timetable, stopsIndex);
23651
23701
  if (beforeTime !== undefined) {
23652
23702
  const lastDepartureTime = timeFromString(beforeTime);
23653
- const query = new RangeQuery.Builder()
23703
+ const queryBuilder = new RangeQuery.Builder()
23654
23704
  .from(fromStop.id)
23655
23705
  .to(toStop.id)
23656
23706
  .departureTime(departureTime)
23657
23707
  .lastDepartureTime(lastDepartureTime)
23658
- .maxTransfers(maxTransfers)
23659
- .build();
23708
+ .maxTransfers(maxTransfers);
23709
+ if (maxInitialWaitingTime !== undefined) {
23710
+ queryBuilder.maxInitialWaitingTime(maxInitialWaitingTime);
23711
+ }
23712
+ const query = queryBuilder.build();
23660
23713
  const result = router.rangeRoute(query);
23661
23714
  if (result.size === 0) {
23662
23715
  console.log(`No journeys found from ${fromStop.name} to ${toStop.name} ` +
@@ -23675,12 +23728,15 @@ const startRepl = (stopsPath, timetablePath) => {
23675
23728
  }
23676
23729
  }
23677
23730
  else {
23678
- const query = new Query.Builder()
23731
+ const queryBuilder = new Query.Builder()
23679
23732
  .from(fromStop.id)
23680
23733
  .to(toStop.id)
23681
23734
  .departureTime(departureTime)
23682
- .maxTransfers(maxTransfers)
23683
- .build();
23735
+ .maxTransfers(maxTransfers);
23736
+ if (maxInitialWaitingTime !== undefined) {
23737
+ queryBuilder.maxInitialWaitingTime(maxInitialWaitingTime);
23738
+ }
23739
+ const query = queryBuilder.build();
23684
23740
  const result = router.route(query);
23685
23741
  const arrivalTime = result.arrivalAt(toStop.id);
23686
23742
  if (arrivalTime === undefined) {