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/dist/routing/query.d.ts
CHANGED
|
@@ -5,6 +5,14 @@ export type QueryOptions = {
|
|
|
5
5
|
maxTransfers: number;
|
|
6
6
|
minTransferTime: Duration;
|
|
7
7
|
transportModes: Set<RouteType>;
|
|
8
|
+
/**
|
|
9
|
+
* Maximum total journey duration (in minutes) from the query departure time.
|
|
10
|
+
*
|
|
11
|
+
* When set, arrivals after `departureTime + maxDuration` are skipped. The
|
|
12
|
+
* duration includes initial access, waiting time, transit legs, and transfers.
|
|
13
|
+
* Undefined means no limit.
|
|
14
|
+
*/
|
|
15
|
+
maxDuration?: Duration;
|
|
8
16
|
/**
|
|
9
17
|
* Maximum time (in minutes) the traveler is willing to wait at the first
|
|
10
18
|
* boarding stop before the first transit vehicle departs.
|
|
@@ -61,6 +69,12 @@ export declare class Query {
|
|
|
61
69
|
* Restricts routing to the given transport modes.
|
|
62
70
|
*/
|
|
63
71
|
transportModes(transportModes: Set<RouteType>): /*elided*/ any;
|
|
72
|
+
/**
|
|
73
|
+
* Sets the maximum total journey duration (in minutes) from the query
|
|
74
|
+
* departure time. The limit includes initial access, waiting time, transit
|
|
75
|
+
* legs, and transfers.
|
|
76
|
+
*/
|
|
77
|
+
maxDuration(maxDuration: Duration): /*elided*/ any;
|
|
64
78
|
/**
|
|
65
79
|
* Sets the maximum time (in minutes) the traveler is willing to wait at
|
|
66
80
|
* the first boarding stop before the first transit vehicle departs.
|
|
@@ -162,6 +176,12 @@ export declare class RangeQuery extends Query {
|
|
|
162
176
|
* Restricts routing to the given transport modes.
|
|
163
177
|
*/
|
|
164
178
|
transportModes(transportModes: Set<RouteType>): /*elided*/ any;
|
|
179
|
+
/**
|
|
180
|
+
* Sets the maximum total journey duration (in minutes) from the query
|
|
181
|
+
* departure time. The limit includes initial access, waiting time, transit
|
|
182
|
+
* legs, and transfers.
|
|
183
|
+
*/
|
|
184
|
+
maxDuration(maxDuration: Duration): /*elided*/ any;
|
|
165
185
|
/**
|
|
166
186
|
* Sets the maximum time (in minutes) the traveler is willing to wait at
|
|
167
187
|
* the first boarding stop before the first transit vehicle departs.
|
|
@@ -68,6 +68,7 @@ export declare class RangeRaptorState implements IRaptorState {
|
|
|
68
68
|
* Always at least as tight as the per-run `destinationBest`.
|
|
69
69
|
*/
|
|
70
70
|
get destinationBest(): Time;
|
|
71
|
+
get maxArrivalTime(): Time;
|
|
71
72
|
isDestination(stop: StopId): boolean;
|
|
72
73
|
/** Updates the per-run aggregate best when improved, and always considers the cross-run shared label. */
|
|
73
74
|
updateArrival(stop: StopId, time: Time, round: number): void;
|
package/dist/routing/raptor.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ export interface IRaptorState {
|
|
|
21
21
|
* Best known arrival time at any destination.
|
|
22
22
|
*/
|
|
23
23
|
readonly destinationBest: Time;
|
|
24
|
+
/** Latest arrival time allowed by the current query/run. */
|
|
25
|
+
readonly maxArrivalTime: Time;
|
|
24
26
|
/** Returns `true` if `stop` is one of the query's destination stops. */
|
|
25
27
|
isDestination(stop: StopId): boolean;
|
|
26
28
|
/**
|
package/dist/routing/state.d.ts
CHANGED
|
@@ -76,13 +76,23 @@ export declare class RoutingState implements IRaptorState {
|
|
|
76
76
|
* {@link updateArrival} so that destination pruning is always O(1).
|
|
77
77
|
*/
|
|
78
78
|
private _destinationBest;
|
|
79
|
+
/**
|
|
80
|
+
* Maximum arrival time allowed for this run. Defaults to UNREACHED_TIME when
|
|
81
|
+
* the query has no maxDuration limit.
|
|
82
|
+
*/
|
|
83
|
+
maxArrivalTime: Time;
|
|
84
|
+
/**
|
|
85
|
+
* Query-level maximum duration, retained so resetFor() can recompute the
|
|
86
|
+
* absolute max arrival time for each departure-time iteration.
|
|
87
|
+
*/
|
|
88
|
+
private readonly maxDuration?;
|
|
79
89
|
/**
|
|
80
90
|
* Every stop that has received an arrival improvement during the current run,
|
|
81
91
|
* in the order the improvements occurred. Used by {@link resetFor} to clear
|
|
82
92
|
* only the touched entries instead of scanning the entire array.
|
|
83
93
|
*/
|
|
84
94
|
private readonly reachedStops;
|
|
85
|
-
constructor(departureTime: Time, destinations: StopId[], accessPaths: AccessPoint[], nbStops: number, maxRounds?: number);
|
|
95
|
+
constructor(departureTime: Time, destinations: StopId[], accessPaths: AccessPoint[], nbStops: number, maxRounds?: number, maxDuration?: Duration);
|
|
86
96
|
/**
|
|
87
97
|
* Seeds round-0 arrivals and {@link origins} from a set of access paths.
|
|
88
98
|
* Called by the constructor and by {@link resetFor}.
|
|
@@ -2,7 +2,8 @@ import { StopId } from '../stops/stops.js';
|
|
|
2
2
|
import { Route, RouteId, StopRouteIndex, TripRouteIndex } from './route.js';
|
|
3
3
|
import { Duration, Time } from './time.js';
|
|
4
4
|
import { TripStopId } from './tripStopId.js';
|
|
5
|
-
export type TransferType =
|
|
5
|
+
export type TransferType = // TODO use number to represent that.
|
|
6
|
+
'RECOMMENDED' | 'GUARANTEED' | 'REQUIRES_MINIMAL_TIME' | 'IN_SEAT';
|
|
6
7
|
export type Transfer = {
|
|
7
8
|
destination: StopId;
|
|
8
9
|
type: TransferType;
|
package/package.json
CHANGED
package/src/cli/perf.ts
CHANGED
|
@@ -127,7 +127,8 @@ export const loadQueriesFromJson = (
|
|
|
127
127
|
.from(fromStop.id)
|
|
128
128
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
129
129
|
.to(new Set(toStops.map((stop) => stop!.id)))
|
|
130
|
-
.departureTime(timeFromString(serializedQuery.departureTime))
|
|
130
|
+
.departureTime(timeFromString(serializedQuery.departureTime))
|
|
131
|
+
.maxDuration(6 * 60);
|
|
131
132
|
|
|
132
133
|
if (serializedQuery.maxTransfers !== undefined) {
|
|
133
134
|
queryBuilder.maxTransfers(serializedQuery.maxTransfers);
|
|
@@ -180,7 +181,8 @@ export const loadRangeQueriesFromJson = (
|
|
|
180
181
|
.to(new Set(toStops.map((stop) => stop!.id)))
|
|
181
182
|
.departureTime(timeFromString(serializedQuery.departureTime))
|
|
182
183
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
183
|
-
.lastDepartureTime(timeFromString(serializedQuery.lastDepartureTime!))
|
|
184
|
+
.lastDepartureTime(timeFromString(serializedQuery.lastDepartureTime!))
|
|
185
|
+
.maxDuration(6 * 60);
|
|
184
186
|
|
|
185
187
|
if (serializedQuery.maxTransfers !== undefined) {
|
|
186
188
|
queryBuilder.maxTransfers(serializedQuery.maxTransfers);
|
package/src/cli/repl.ts
CHANGED
|
@@ -49,8 +49,11 @@ export const startRepl = (stopsPath: string, timetablePath: string) => {
|
|
|
49
49
|
this.displayPrompt();
|
|
50
50
|
},
|
|
51
51
|
});
|
|
52
|
+
const routeSyntax =
|
|
53
|
+
'.route from <stop> to <stop> at <HH:mm> [before <HH:mm>] [with <N> transfers] [wait max <N> minutes]';
|
|
54
|
+
|
|
52
55
|
replServer.defineCommand('route', {
|
|
53
|
-
help:
|
|
56
|
+
help: `Find a route using ${routeSyntax}`,
|
|
54
57
|
action(routeQuery: string) {
|
|
55
58
|
this.clearBufferedCommand();
|
|
56
59
|
const parts = routeQuery.split(' ').filter(Boolean);
|
|
@@ -60,11 +63,13 @@ export const startRepl = (stopsPath: string, timetablePath: string) => {
|
|
|
60
63
|
const atIndex = parts.indexOf('at');
|
|
61
64
|
const beforeIndex = parts.indexOf('before');
|
|
62
65
|
const withIndex = parts.indexOf('with');
|
|
66
|
+
const waitIndex = parts.indexOf('wait');
|
|
67
|
+
const routeClauseIndexes = [beforeIndex, withIndex, waitIndex].filter(
|
|
68
|
+
(index) => index !== -1,
|
|
69
|
+
);
|
|
63
70
|
|
|
64
71
|
if (fromIndex === -1 || toIndex === -1 || atIndex === -1) {
|
|
65
|
-
console.log(
|
|
66
|
-
'Usage: .route from <stop> to <stop> at <HH:mm> [before <HH:mm>] [with <N> transfers]',
|
|
67
|
-
);
|
|
72
|
+
console.log(`Usage: ${routeSyntax}`);
|
|
68
73
|
this.displayPrompt();
|
|
69
74
|
return;
|
|
70
75
|
}
|
|
@@ -72,17 +77,20 @@ export const startRepl = (stopsPath: string, timetablePath: string) => {
|
|
|
72
77
|
const fromId = parts.slice(fromIndex + 1, toIndex).join(' ');
|
|
73
78
|
const toId = parts.slice(toIndex + 1, atIndex).join(' ');
|
|
74
79
|
|
|
75
|
-
// atTime ends at 'before', 'with', or the end of the input.
|
|
76
|
-
const atTimeEnd =
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
? withIndex
|
|
81
|
-
: parts.length;
|
|
80
|
+
// atTime ends at 'before', 'with', 'wait', or the end of the input.
|
|
81
|
+
const atTimeEnd = Math.min(
|
|
82
|
+
...routeClauseIndexes.filter((index) => index > atIndex),
|
|
83
|
+
parts.length,
|
|
84
|
+
);
|
|
82
85
|
const atTime = parts.slice(atIndex + 1, atTimeEnd).join(' ');
|
|
83
86
|
|
|
84
87
|
// beforeTime is only present when the 'before' keyword appears.
|
|
85
|
-
const beforeTimeEnd =
|
|
88
|
+
const beforeTimeEnd = Math.min(
|
|
89
|
+
...[withIndex, waitIndex].filter(
|
|
90
|
+
(index) => index !== -1 && index > beforeIndex,
|
|
91
|
+
),
|
|
92
|
+
parts.length,
|
|
93
|
+
);
|
|
86
94
|
const beforeTime =
|
|
87
95
|
beforeIndex !== -1
|
|
88
96
|
? parts.slice(beforeIndex + 1, beforeTimeEnd).join(' ')
|
|
@@ -92,11 +100,23 @@ export const startRepl = (stopsPath: string, timetablePath: string) => {
|
|
|
92
100
|
withIndex !== -1 && parts[withIndex + 1] !== undefined
|
|
93
101
|
? parseInt(parts[withIndex + 1] as string)
|
|
94
102
|
: 4;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
103
|
+
const maxInitialWaitingTime =
|
|
104
|
+
waitIndex !== -1 &&
|
|
105
|
+
parts[waitIndex + 1] === 'max' &&
|
|
106
|
+
parts[waitIndex + 2] !== undefined
|
|
107
|
+
? Number(parts[waitIndex + 2])
|
|
108
|
+
: undefined;
|
|
109
|
+
const waitUnit = parts[waitIndex + 3];
|
|
110
|
+
const hasInvalidWaitClause =
|
|
111
|
+
waitIndex !== -1 &&
|
|
112
|
+
(parts[waitIndex + 1] !== 'max' ||
|
|
113
|
+
maxInitialWaitingTime === undefined ||
|
|
114
|
+
!Number.isFinite(maxInitialWaitingTime) ||
|
|
115
|
+
maxInitialWaitingTime < 0 ||
|
|
116
|
+
(waitUnit !== 'minute' && waitUnit !== 'minutes'));
|
|
117
|
+
|
|
118
|
+
if (!fromId || !toId || !atTime || hasInvalidWaitClause) {
|
|
119
|
+
console.log(`Usage: ${routeSyntax}`);
|
|
100
120
|
this.displayPrompt();
|
|
101
121
|
return;
|
|
102
122
|
}
|
|
@@ -131,13 +151,16 @@ export const startRepl = (stopsPath: string, timetablePath: string) => {
|
|
|
131
151
|
const router = new Router(timetable, stopsIndex);
|
|
132
152
|
if (beforeTime !== undefined) {
|
|
133
153
|
const lastDepartureTime = timeFromString(beforeTime);
|
|
134
|
-
const
|
|
154
|
+
const queryBuilder = new RangeQuery.Builder()
|
|
135
155
|
.from(fromStop.id)
|
|
136
156
|
.to(toStop.id)
|
|
137
157
|
.departureTime(departureTime)
|
|
138
158
|
.lastDepartureTime(lastDepartureTime)
|
|
139
|
-
.maxTransfers(maxTransfers)
|
|
140
|
-
|
|
159
|
+
.maxTransfers(maxTransfers);
|
|
160
|
+
if (maxInitialWaitingTime !== undefined) {
|
|
161
|
+
queryBuilder.maxInitialWaitingTime(maxInitialWaitingTime);
|
|
162
|
+
}
|
|
163
|
+
const query = queryBuilder.build();
|
|
141
164
|
|
|
142
165
|
const result = router.rangeRoute(query);
|
|
143
166
|
|
|
@@ -160,12 +183,15 @@ export const startRepl = (stopsPath: string, timetablePath: string) => {
|
|
|
160
183
|
});
|
|
161
184
|
}
|
|
162
185
|
} else {
|
|
163
|
-
const
|
|
186
|
+
const queryBuilder = new Query.Builder()
|
|
164
187
|
.from(fromStop.id)
|
|
165
188
|
.to(toStop.id)
|
|
166
189
|
.departureTime(departureTime)
|
|
167
|
-
.maxTransfers(maxTransfers)
|
|
168
|
-
|
|
190
|
+
.maxTransfers(maxTransfers);
|
|
191
|
+
if (maxInitialWaitingTime !== undefined) {
|
|
192
|
+
queryBuilder.maxInitialWaitingTime(maxInitialWaitingTime);
|
|
193
|
+
}
|
|
194
|
+
const query = queryBuilder.build();
|
|
169
195
|
|
|
170
196
|
const result = router.route(query);
|
|
171
197
|
const arrivalTime = result.arrivalAt(toStop.id);
|
package/src/router.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Plotter } from './routing/plotter.js';
|
|
2
|
+
import type { QueryOptions, RangeQueryOptions } from './routing/query.js';
|
|
2
3
|
import { Query, RangeQuery } from './routing/query.js';
|
|
3
4
|
import { Result } from './routing/result.js';
|
|
4
5
|
import type { Leg, Transfer, VehicleLeg } from './routing/route.js';
|
|
@@ -40,6 +41,8 @@ export type {
|
|
|
40
41
|
Leg,
|
|
41
42
|
LocationType,
|
|
42
43
|
ParetoRun,
|
|
44
|
+
QueryOptions,
|
|
45
|
+
RangeQueryOptions,
|
|
43
46
|
RouteType,
|
|
44
47
|
ServiceRouteInfo,
|
|
45
48
|
SourceStopId,
|
|
@@ -148,6 +148,28 @@ describe('PlainRouter', () => {
|
|
|
148
148
|
// Route 0 arrives at stop3 at 08:35
|
|
149
149
|
assert.strictEqual(timeToStop3?.arrival, timeFromHM(8, 35));
|
|
150
150
|
});
|
|
151
|
+
|
|
152
|
+
it('should not return journeys arriving after maxDuration', () => {
|
|
153
|
+
const tooShort = new Query.Builder()
|
|
154
|
+
.from(0)
|
|
155
|
+
.to(2)
|
|
156
|
+
.departureTime(timeFromHM(8, 0))
|
|
157
|
+
.maxDuration(34)
|
|
158
|
+
.build();
|
|
159
|
+
|
|
160
|
+
assert.strictEqual(router.route(tooShort).bestRoute(), undefined);
|
|
161
|
+
|
|
162
|
+
const justEnough = new Query.Builder()
|
|
163
|
+
.from(0)
|
|
164
|
+
.to(2)
|
|
165
|
+
.departureTime(timeFromHM(8, 0))
|
|
166
|
+
.maxDuration(35)
|
|
167
|
+
.build();
|
|
168
|
+
|
|
169
|
+
const route = router.route(justEnough).bestRoute();
|
|
170
|
+
assert(route);
|
|
171
|
+
assert.strictEqual(route.arrivalTime(), timeFromHM(8, 35));
|
|
172
|
+
});
|
|
151
173
|
});
|
|
152
174
|
|
|
153
175
|
describe('with a route change', () => {
|
|
@@ -347,6 +347,28 @@ describe('RangeRouter', () => {
|
|
|
347
347
|
assert.strictEqual(result.size, 0);
|
|
348
348
|
assert.strictEqual(result.bestRoute(), undefined);
|
|
349
349
|
});
|
|
350
|
+
|
|
351
|
+
it('filters runs whose arrivals exceed maxDuration for their departure slot', () => {
|
|
352
|
+
const tooShort = new RangeQuery.Builder()
|
|
353
|
+
.from(0)
|
|
354
|
+
.to(1)
|
|
355
|
+
.departureTime(timeFromHM(8, 0))
|
|
356
|
+
.lastDepartureTime(timeFromHM(8, 30))
|
|
357
|
+
.maxDuration(29)
|
|
358
|
+
.build();
|
|
359
|
+
|
|
360
|
+
assert.strictEqual(router.rangeRoute(tooShort).size, 0);
|
|
361
|
+
|
|
362
|
+
const justEnough = new RangeQuery.Builder()
|
|
363
|
+
.from(0)
|
|
364
|
+
.to(1)
|
|
365
|
+
.departureTime(timeFromHM(8, 0))
|
|
366
|
+
.lastDepartureTime(timeFromHM(8, 30))
|
|
367
|
+
.maxDuration(30)
|
|
368
|
+
.build();
|
|
369
|
+
|
|
370
|
+
assert.strictEqual(router.rangeRoute(justEnough).size, 2);
|
|
371
|
+
});
|
|
350
372
|
});
|
|
351
373
|
|
|
352
374
|
describe('same-stop query (origin equals destination)', () => {
|
|
@@ -249,6 +249,148 @@ describe('Raptor', () => {
|
|
|
249
249
|
});
|
|
250
250
|
});
|
|
251
251
|
|
|
252
|
+
describe('maxDuration', () => {
|
|
253
|
+
it('filters vehicle arrivals after the maxDuration cutoff and allows the exact boundary', () => {
|
|
254
|
+
const timetable = new Timetable(
|
|
255
|
+
stopsAdjacency,
|
|
256
|
+
[route0, route1],
|
|
257
|
+
serviceRoutes,
|
|
258
|
+
);
|
|
259
|
+
const raptor = new Raptor(timetable);
|
|
260
|
+
const tooShortOptions: QueryOptions = {
|
|
261
|
+
maxTransfers: 5,
|
|
262
|
+
minTransferTime: 2,
|
|
263
|
+
transportModes: ALL_TRANSPORT_MODES,
|
|
264
|
+
maxDuration: 29,
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const tooShort = new RoutingState(
|
|
268
|
+
timeFromHM(8, 0),
|
|
269
|
+
[1],
|
|
270
|
+
[{ fromStopId: 0, toStopId: 0, duration: 0 }],
|
|
271
|
+
NB_STOPS,
|
|
272
|
+
tooShortOptions.maxTransfers + 1,
|
|
273
|
+
tooShortOptions.maxDuration,
|
|
274
|
+
);
|
|
275
|
+
raptor.run(tooShortOptions, tooShort);
|
|
276
|
+
assert.strictEqual(tooShort.getArrival(1), undefined);
|
|
277
|
+
|
|
278
|
+
const justEnoughOptions: QueryOptions = {
|
|
279
|
+
maxTransfers: 5,
|
|
280
|
+
minTransferTime: 2,
|
|
281
|
+
transportModes: ALL_TRANSPORT_MODES,
|
|
282
|
+
maxDuration: 30,
|
|
283
|
+
};
|
|
284
|
+
const justEnough = new RoutingState(
|
|
285
|
+
timeFromHM(8, 0),
|
|
286
|
+
[1],
|
|
287
|
+
[{ fromStopId: 0, toStopId: 0, duration: 0 }],
|
|
288
|
+
NB_STOPS,
|
|
289
|
+
justEnoughOptions.maxTransfers + 1,
|
|
290
|
+
justEnoughOptions.maxDuration,
|
|
291
|
+
);
|
|
292
|
+
raptor.run(justEnoughOptions, justEnough);
|
|
293
|
+
assert.strictEqual(justEnough.getArrival(1)?.arrival, timeFromHM(8, 30));
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('keeps intermediate stops reachable while filtering later vehicle arrivals', () => {
|
|
297
|
+
const timetable = new Timetable(
|
|
298
|
+
stopsAdjacency,
|
|
299
|
+
[route0, route1],
|
|
300
|
+
serviceRoutes,
|
|
301
|
+
);
|
|
302
|
+
const options: QueryOptions = {
|
|
303
|
+
maxTransfers: 5,
|
|
304
|
+
minTransferTime: 2,
|
|
305
|
+
transportModes: ALL_TRANSPORT_MODES,
|
|
306
|
+
maxDuration: 45,
|
|
307
|
+
};
|
|
308
|
+
const state = new RoutingState(
|
|
309
|
+
timeFromHM(8, 0),
|
|
310
|
+
[2],
|
|
311
|
+
[{ fromStopId: 0, toStopId: 0, duration: 0 }],
|
|
312
|
+
NB_STOPS,
|
|
313
|
+
options.maxTransfers + 1,
|
|
314
|
+
options.maxDuration,
|
|
315
|
+
);
|
|
316
|
+
const raptor = new Raptor(timetable);
|
|
317
|
+
raptor.run(options, state);
|
|
318
|
+
assert.strictEqual(state.getArrival(1)?.arrival, timeFromHM(8, 30));
|
|
319
|
+
assert.strictEqual(state.getArrival(2), undefined);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('filters timed walking transfers after the maxDuration cutoff', () => {
|
|
323
|
+
const timetable = new Timetable(
|
|
324
|
+
stopsAdjacencyWithTransfer,
|
|
325
|
+
[route0],
|
|
326
|
+
[serviceRoutes[0]!],
|
|
327
|
+
);
|
|
328
|
+
const raptor = new Raptor(timetable);
|
|
329
|
+
const tooShortOptions: QueryOptions = {
|
|
330
|
+
maxTransfers: 5,
|
|
331
|
+
minTransferTime: 2,
|
|
332
|
+
transportModes: ALL_TRANSPORT_MODES,
|
|
333
|
+
maxDuration: 34,
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const tooShort = new RoutingState(
|
|
337
|
+
timeFromHM(8, 0),
|
|
338
|
+
[2],
|
|
339
|
+
[{ fromStopId: 0, toStopId: 0, duration: 0 }],
|
|
340
|
+
NB_STOPS,
|
|
341
|
+
tooShortOptions.maxTransfers + 1,
|
|
342
|
+
tooShortOptions.maxDuration,
|
|
343
|
+
);
|
|
344
|
+
raptor.run(tooShortOptions, tooShort);
|
|
345
|
+
assert.strictEqual(tooShort.getArrival(1)?.arrival, timeFromHM(8, 30));
|
|
346
|
+
assert.strictEqual(tooShort.getArrival(2), undefined);
|
|
347
|
+
|
|
348
|
+
const justEnoughOptions: QueryOptions = {
|
|
349
|
+
maxTransfers: 5,
|
|
350
|
+
minTransferTime: 2,
|
|
351
|
+
transportModes: ALL_TRANSPORT_MODES,
|
|
352
|
+
maxDuration: 35,
|
|
353
|
+
};
|
|
354
|
+
const justEnough = new RoutingState(
|
|
355
|
+
timeFromHM(8, 0),
|
|
356
|
+
[2],
|
|
357
|
+
[{ fromStopId: 0, toStopId: 0, duration: 0 }],
|
|
358
|
+
NB_STOPS,
|
|
359
|
+
justEnoughOptions.maxTransfers + 1,
|
|
360
|
+
justEnoughOptions.maxDuration,
|
|
361
|
+
);
|
|
362
|
+
raptor.run(justEnoughOptions, justEnough);
|
|
363
|
+
assert.strictEqual(justEnough.getArrival(2)?.arrival, timeFromHM(8, 35));
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('filters in-seat continuation arrivals after the maxDuration cutoff', () => {
|
|
367
|
+
const timetable = new Timetable(
|
|
368
|
+
stopsAdjacency,
|
|
369
|
+
[route0, route1],
|
|
370
|
+
serviceRoutes,
|
|
371
|
+
tripContinuations,
|
|
372
|
+
);
|
|
373
|
+
const options: QueryOptions = {
|
|
374
|
+
maxTransfers: 5,
|
|
375
|
+
minTransferTime: 2,
|
|
376
|
+
transportModes: ALL_TRANSPORT_MODES,
|
|
377
|
+
maxDuration: 45,
|
|
378
|
+
};
|
|
379
|
+
const state = new RoutingState(
|
|
380
|
+
timeFromHM(8, 0),
|
|
381
|
+
[2],
|
|
382
|
+
[{ fromStopId: 0, toStopId: 0, duration: 0 }],
|
|
383
|
+
NB_STOPS,
|
|
384
|
+
options.maxTransfers + 1,
|
|
385
|
+
options.maxDuration,
|
|
386
|
+
);
|
|
387
|
+
const raptor = new Raptor(timetable);
|
|
388
|
+
raptor.run(options, state);
|
|
389
|
+
assert.strictEqual(state.getArrival(1)?.arrival, timeFromHM(8, 30));
|
|
390
|
+
assert.strictEqual(state.getArrival(2), undefined);
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
|
|
252
394
|
describe('early termination', () => {
|
|
253
395
|
it('exits when no trips are catchable', () => {
|
|
254
396
|
// Departing at 09:00 — all trips have already left (route 0 at 08:10,
|
package/src/routing/query.ts
CHANGED
|
@@ -6,6 +6,14 @@ export type QueryOptions = {
|
|
|
6
6
|
maxTransfers: number;
|
|
7
7
|
minTransferTime: Duration;
|
|
8
8
|
transportModes: Set<RouteType>;
|
|
9
|
+
/**
|
|
10
|
+
* Maximum total journey duration (in minutes) from the query departure time.
|
|
11
|
+
*
|
|
12
|
+
* When set, arrivals after `departureTime + maxDuration` are skipped. The
|
|
13
|
+
* duration includes initial access, waiting time, transit legs, and transfers.
|
|
14
|
+
* Undefined means no limit.
|
|
15
|
+
*/
|
|
16
|
+
maxDuration?: Duration;
|
|
9
17
|
/**
|
|
10
18
|
* Maximum time (in minutes) the traveler is willing to wait at the first
|
|
11
19
|
* boarding stop before the first transit vehicle departs.
|
|
@@ -98,6 +106,16 @@ export class Query {
|
|
|
98
106
|
return this;
|
|
99
107
|
}
|
|
100
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Sets the maximum total journey duration (in minutes) from the query
|
|
111
|
+
* departure time. The limit includes initial access, waiting time, transit
|
|
112
|
+
* legs, and transfers.
|
|
113
|
+
*/
|
|
114
|
+
maxDuration(maxDuration: Duration): this {
|
|
115
|
+
this.optionsValue.maxDuration = maxDuration;
|
|
116
|
+
return this;
|
|
117
|
+
}
|
|
118
|
+
|
|
101
119
|
/**
|
|
102
120
|
* Sets the maximum time (in minutes) the traveler is willing to wait at
|
|
103
121
|
* the first boarding stop before the first transit vehicle departs.
|
|
@@ -92,6 +92,7 @@ export class RangeRouter {
|
|
|
92
92
|
accessLegs,
|
|
93
93
|
this.timetable.nbStops(),
|
|
94
94
|
maxRounds,
|
|
95
|
+
query.options.maxDuration,
|
|
95
96
|
);
|
|
96
97
|
rangeState.setCurrentRun(routingState);
|
|
97
98
|
this.raptor.run(
|
|
@@ -122,6 +123,7 @@ export class RangeRouter {
|
|
|
122
123
|
legs,
|
|
123
124
|
this.timetable.nbStops(),
|
|
124
125
|
maxRounds,
|
|
126
|
+
query.options.maxDuration,
|
|
125
127
|
);
|
|
126
128
|
} else {
|
|
127
129
|
routingState.resetFor(depTime, legs);
|
|
@@ -109,6 +109,10 @@ export class RangeRaptorState implements IRaptorState {
|
|
|
109
109
|
return this._destinationBest;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
get maxArrivalTime(): Time {
|
|
113
|
+
return this.currentRun.maxArrivalTime;
|
|
114
|
+
}
|
|
115
|
+
|
|
112
116
|
isDestination(stop: StopId): boolean {
|
|
113
117
|
return this.currentRun.isDestination(stop);
|
|
114
118
|
}
|
package/src/routing/raptor.ts
CHANGED
|
@@ -34,6 +34,9 @@ export interface IRaptorState {
|
|
|
34
34
|
*/
|
|
35
35
|
readonly destinationBest: Time;
|
|
36
36
|
|
|
37
|
+
/** Latest arrival time allowed by the current query/run. */
|
|
38
|
+
readonly maxArrivalTime: Time;
|
|
39
|
+
|
|
37
40
|
/** Returns `true` if `stop` is one of the query's destination stops. */
|
|
38
41
|
isDestination(stop: StopId): boolean;
|
|
39
42
|
|
|
@@ -212,6 +215,7 @@ export class Raptor {
|
|
|
212
215
|
|
|
213
216
|
if (
|
|
214
217
|
dropOffType !== NOT_AVAILABLE &&
|
|
218
|
+
arrivalTime <= state.maxArrivalTime &&
|
|
215
219
|
arrivalTime < state.improvementBound(round, currentStop) &&
|
|
216
220
|
arrivalTime < state.destinationBest
|
|
217
221
|
) {
|
|
@@ -282,6 +286,7 @@ export class Raptor {
|
|
|
282
286
|
|
|
283
287
|
if (
|
|
284
288
|
dropOffType !== NOT_AVAILABLE &&
|
|
289
|
+
arrivalTime <= state.maxArrivalTime &&
|
|
285
290
|
arrivalTime < state.improvementBound(round, currentStop) &&
|
|
286
291
|
arrivalTime < state.destinationBest
|
|
287
292
|
) {
|
|
@@ -334,17 +339,21 @@ export class Raptor {
|
|
|
334
339
|
);
|
|
335
340
|
|
|
336
341
|
if (firstBoardableTrip !== undefined) {
|
|
342
|
+
const departureTime = route.departureFrom(
|
|
343
|
+
currentStopIndex,
|
|
344
|
+
firstBoardableTrip,
|
|
345
|
+
);
|
|
337
346
|
// At round 1, enforce maxInitialWaitingTime: skip boarding if the
|
|
338
347
|
// traveler would have to wait longer than the allowed threshold at
|
|
339
348
|
// the first boarding stop.
|
|
340
349
|
const exceedsInitialWait =
|
|
341
350
|
round === 1 &&
|
|
342
351
|
options.maxInitialWaitingTime !== undefined &&
|
|
343
|
-
|
|
344
|
-
earliestArrivalOnPreviousRound >
|
|
352
|
+
departureTime - earliestArrivalOnPreviousRound >
|
|
345
353
|
options.maxInitialWaitingTime;
|
|
354
|
+
const exceedsMaxDuration = departureTime > state.maxArrivalTime;
|
|
346
355
|
|
|
347
|
-
if (!exceedsInitialWait) {
|
|
356
|
+
if (!exceedsInitialWait && !exceedsMaxDuration) {
|
|
348
357
|
activeTripIndex = firstBoardableTrip;
|
|
349
358
|
activeTripBoardStopIndex = currentStopIndex;
|
|
350
359
|
activeTripStopOffset = route.tripStopOffset(firstBoardableTrip);
|
|
@@ -391,6 +400,7 @@ export class Raptor {
|
|
|
391
400
|
const arrivalAfterTransfer = currentArrival.arrival + transferTime;
|
|
392
401
|
|
|
393
402
|
if (
|
|
403
|
+
arrivalAfterTransfer <= state.maxArrivalTime &&
|
|
394
404
|
arrivalAfterTransfer <
|
|
395
405
|
state.improvementBound(round, transfer.destination) &&
|
|
396
406
|
arrivalAfterTransfer < state.destinationBest
|
|
@@ -398,7 +408,7 @@ export class Raptor {
|
|
|
398
408
|
arrivalsAtCurrentRound[transfer.destination] = {
|
|
399
409
|
arrival: arrivalAfterTransfer,
|
|
400
410
|
from: stop,
|
|
401
|
-
to: transfer.destination,
|
|
411
|
+
to: transfer.destination, // TODO needed?
|
|
402
412
|
minTransferTime: transferTime || undefined,
|
|
403
413
|
type: transfer.type,
|
|
404
414
|
} as TransferEdge;
|