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 +3 -3
- package/README.md +1 -0
- package/dist/cli.mjs +87 -31
- package/dist/cli.mjs.map +1 -1
- package/dist/parser.cjs.js.map +1 -1
- package/dist/parser.esm.js.map +1 -1
- package/dist/router.cjs.js +1 -1
- package/dist/router.cjs.js.map +1 -1
- package/dist/router.d.ts +2 -1
- package/dist/router.esm.js +1 -1
- package/dist/router.esm.js.map +1 -1
- package/dist/router.umd.js +1 -1
- package/dist/router.umd.js.map +1 -1
- package/dist/routing/query.d.ts +20 -0
- package/dist/routing/rangeState.d.ts +2 -1
- package/dist/routing/raptor.d.ts +2 -0
- package/dist/routing/state.d.ts +11 -1
- package/dist/timetable/timetable.d.ts +2 -1
- package/package.json +1 -1
- package/src/cli/perf.ts +4 -2
- package/src/cli/repl.ts +49 -23
- package/src/router.ts +3 -0
- package/src/routing/__tests__/plainRouter.test.ts +22 -0
- package/src/routing/__tests__/rangeRouter.test.ts +22 -0
- package/src/routing/__tests__/rangeState.test.ts +88 -0
- package/src/routing/__tests__/raptor.test.ts +142 -0
- package/src/routing/plainRouter.ts +1 -0
- package/src/routing/query.ts +18 -0
- package/src/routing/rangeRouter.ts +2 -0
- package/src/routing/rangeState.ts +16 -2
- package/src/routing/raptor.ts +14 -4
- package/src/routing/state.ts +31 -1
- package/src/timetable/timetable.ts +2 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
## [11.2.
|
|
1
|
+
## [11.2.3](https://github.com/aubryio/minotor/compare/v11.2.2...v11.2.3) (2026-05-04)
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
###
|
|
4
|
+
### Performance Improvements
|
|
5
5
|
|
|
6
|
-
*
|
|
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
|
|
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.
|
|
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
|
-
|
|
23126
|
-
earliestArrivalOnPreviousRound >
|
|
23161
|
+
departureTime - earliestArrivalOnPreviousRound >
|
|
23127
23162
|
options.maxInitialWaitingTime;
|
|
23128
|
-
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
-
|
|
23624
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
23731
|
+
const queryBuilder = new Query.Builder()
|
|
23679
23732
|
.from(fromStop.id)
|
|
23680
23733
|
.to(toStop.id)
|
|
23681
23734
|
.departureTime(departureTime)
|
|
23682
|
-
.maxTransfers(maxTransfers)
|
|
23683
|
-
|
|
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) {
|