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.
@@ -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;
@@ -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
  /**
@@ -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 = 'RECOMMENDED' | 'GUARANTEED' | 'REQUIRES_MINIMAL_TIME' | 'IN_SEAT';
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minotor",
3
- "version": "11.2.2",
3
+ "version": "11.2.3",
4
4
  "description": "A lightweight client-side transit routing library.",
5
5
  "keywords": [
6
6
  "minotor",
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: 'Find a route using .route from <stop> to <stop> at <HH:mm> [before <HH:mm>] [with <N> transfers]',
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
- beforeIndex !== -1
78
- ? beforeIndex
79
- : withIndex !== -1
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 = withIndex !== -1 ? withIndex : parts.length;
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
- if (!fromId || !toId || !atTime) {
97
- console.log(
98
- 'Usage: .route from <stop> to <stop> at <HH:mm> [before <HH:mm>] [with <N> transfers]',
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 query = new RangeQuery.Builder()
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
- .build();
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 query = new Query.Builder()
186
+ const queryBuilder = new Query.Builder()
164
187
  .from(fromStop.id)
165
188
  .to(toStop.id)
166
189
  .departureTime(departureTime)
167
- .maxTransfers(maxTransfers)
168
- .build();
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,
@@ -47,6 +47,7 @@ export class PlainRouter {
47
47
  accessLegs,
48
48
  this.timetable.nbStops(),
49
49
  query.options.maxTransfers + 1,
50
+ query.options.maxDuration,
50
51
  );
51
52
 
52
53
  this.raptor.run(query.options, routingState);
@@ -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
  }
@@ -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
- route.departureFrom(currentStopIndex, firstBoardableTrip) -
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;