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 +3 -3
- package/README.md +1 -0
- package/dist/cli.mjs +78 -29
- 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 +1 -0
- 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__/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 +4 -0
- 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,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
|
-
|
|
23133
|
-
earliestArrivalOnPreviousRound >
|
|
23161
|
+
departureTime - earliestArrivalOnPreviousRound >
|
|
23134
23162
|
options.maxInitialWaitingTime;
|
|
23135
|
-
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
-
|
|
23631
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
23731
|
+
const queryBuilder = new Query.Builder()
|
|
23686
23732
|
.from(fromStop.id)
|
|
23687
23733
|
.to(toStop.id)
|
|
23688
23734
|
.departureTime(departureTime)
|
|
23689
|
-
.maxTransfers(maxTransfers)
|
|
23690
|
-
|
|
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) {
|