minotor 11.1.2 → 11.2.0

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.
Files changed (71) hide show
  1. package/.cspell.json +7 -1
  2. package/CHANGELOG.md +3 -3
  3. package/README.md +111 -86
  4. package/dist/cli/perf.d.ts +57 -18
  5. package/dist/cli.mjs +1371 -342
  6. package/dist/cli.mjs.map +1 -1
  7. package/dist/parser.cjs.js +57 -4
  8. package/dist/parser.cjs.js.map +1 -1
  9. package/dist/parser.esm.js +57 -4
  10. package/dist/parser.esm.js.map +1 -1
  11. package/dist/router.cjs.js +1 -1
  12. package/dist/router.cjs.js.map +1 -1
  13. package/dist/router.d.ts +5 -5
  14. package/dist/router.esm.js +1 -1
  15. package/dist/router.esm.js.map +1 -1
  16. package/dist/router.umd.js +1 -1
  17. package/dist/router.umd.js.map +1 -1
  18. package/dist/routing/__tests__/access.test.d.ts +1 -0
  19. package/dist/routing/__tests__/plainRouter.test.d.ts +1 -0
  20. package/dist/routing/__tests__/rangeResult.test.d.ts +1 -0
  21. package/dist/routing/__tests__/rangeRouter.test.d.ts +1 -0
  22. package/dist/routing/__tests__/rangeState.test.d.ts +1 -0
  23. package/dist/routing/__tests__/raptor.test.d.ts +1 -0
  24. package/dist/routing/__tests__/state.test.d.ts +1 -0
  25. package/dist/routing/access.d.ts +55 -0
  26. package/dist/routing/plainRouter.d.ts +21 -0
  27. package/dist/routing/plotter.d.ts +9 -0
  28. package/dist/routing/query.d.ts +132 -13
  29. package/dist/routing/rangeResult.d.ts +155 -0
  30. package/dist/routing/rangeRouter.d.ts +24 -0
  31. package/dist/routing/rangeState.d.ts +83 -0
  32. package/dist/routing/raptor.d.ts +96 -0
  33. package/dist/routing/result.d.ts +27 -7
  34. package/dist/routing/route.d.ts +5 -21
  35. package/dist/routing/router.d.ts +20 -91
  36. package/dist/routing/state.d.ts +92 -17
  37. package/dist/timetable/route.d.ts +8 -0
  38. package/dist/timetable/timetable.d.ts +17 -1
  39. package/package.json +1 -1
  40. package/src/__e2e__/benchmark.json +18 -0
  41. package/src/__e2e__/router.test.ts +461 -127
  42. package/src/cli/minotor.ts +39 -3
  43. package/src/cli/perf.ts +324 -60
  44. package/src/cli/repl.ts +96 -41
  45. package/src/router.ts +11 -3
  46. package/src/routing/__tests__/access.test.ts +294 -0
  47. package/src/routing/__tests__/plainRouter.test.ts +1633 -0
  48. package/src/routing/__tests__/plotter.test.ts +8 -8
  49. package/src/routing/__tests__/rangeResult.test.ts +273 -0
  50. package/src/routing/__tests__/rangeRouter.test.ts +472 -0
  51. package/src/routing/__tests__/rangeState.test.ts +246 -0
  52. package/src/routing/__tests__/raptor.test.ts +366 -0
  53. package/src/routing/__tests__/result.test.ts +27 -27
  54. package/src/routing/__tests__/route.test.ts +28 -0
  55. package/src/routing/__tests__/router.test.ts +75 -1587
  56. package/src/routing/__tests__/state.test.ts +78 -0
  57. package/src/routing/access.ts +144 -0
  58. package/src/routing/plainRouter.ts +60 -0
  59. package/src/routing/plotter.ts +53 -6
  60. package/src/routing/query.ts +116 -13
  61. package/src/routing/rangeResult.ts +292 -0
  62. package/src/routing/rangeRouter.ts +167 -0
  63. package/src/routing/rangeState.ts +150 -0
  64. package/src/routing/raptor.ts +416 -0
  65. package/src/routing/result.ts +68 -26
  66. package/src/routing/route.ts +15 -53
  67. package/src/routing/router.ts +40 -480
  68. package/src/routing/state.ts +191 -32
  69. package/src/timetable/__tests__/timetable.test.ts +373 -0
  70. package/src/timetable/route.ts +16 -4
  71. package/src/timetable/timetable.ts +54 -1
@@ -0,0 +1,78 @@
1
+ /* eslint-disable @typescript-eslint/no-non-null-assertion */
2
+ import assert from 'node:assert';
3
+ import { describe, it } from 'node:test';
4
+
5
+ import { timeFromHM } from '../../timetable/time.js';
6
+ import { RoutingState, UNREACHED_TIME } from '../state.js';
7
+
8
+ describe('RoutingState', () => {
9
+ describe('arrivals', () => {
10
+ it('yields every reached stop with its arrival time and leg number', () => {
11
+ const state = RoutingState.fromTestData({
12
+ nbStops: 4,
13
+ arrivals: [
14
+ [1, timeFromHM(8, 30), 1],
15
+ [3, timeFromHM(9, 0), 2],
16
+ ],
17
+ });
18
+
19
+ const reached = [...state.arrivals()];
20
+ assert.strictEqual(reached.length, 2);
21
+ assert.deepStrictEqual(reached[0], {
22
+ stop: 1,
23
+ arrival: timeFromHM(8, 30),
24
+ legNumber: 1,
25
+ });
26
+ assert.deepStrictEqual(reached[1], {
27
+ stop: 3,
28
+ arrival: timeFromHM(9, 0),
29
+ legNumber: 2,
30
+ });
31
+ });
32
+
33
+ it('skips stops still at UNREACHED_TIME', () => {
34
+ const state = RoutingState.fromTestData({
35
+ nbStops: 3,
36
+ arrivals: [[0, timeFromHM(8, 0), 0]],
37
+ });
38
+
39
+ const reached = [...state.arrivals()];
40
+ assert.strictEqual(reached.length, 1);
41
+ assert.strictEqual(reached[0]!.stop, 0);
42
+ });
43
+
44
+ it('yields nothing when no stop has been reached', () => {
45
+ const state = RoutingState.fromTestData({ nbStops: 3 });
46
+ assert.strictEqual([...state.arrivals()].length, 0);
47
+ });
48
+ });
49
+
50
+ describe('destinationBest', () => {
51
+ it('reflects the earliest arrival at any destination', () => {
52
+ const state = RoutingState.fromTestData({
53
+ nbStops: 3,
54
+ destinations: [1, 2],
55
+ arrivals: [
56
+ [1, timeFromHM(9, 30), 1],
57
+ [2, timeFromHM(9, 0), 1],
58
+ ],
59
+ });
60
+ assert.strictEqual(state.destinationBest, timeFromHM(9, 0));
61
+ });
62
+
63
+ it('is UNREACHED_TIME when no destination has been reached', () => {
64
+ const state = RoutingState.fromTestData({
65
+ nbStops: 3,
66
+ destinations: [2],
67
+ });
68
+ assert.strictEqual(state.destinationBest, UNREACHED_TIME);
69
+ });
70
+ });
71
+
72
+ describe('nbStops', () => {
73
+ it('matches the size passed to fromTestData', () => {
74
+ const state = RoutingState.fromTestData({ nbStops: 7 });
75
+ assert.strictEqual(state.nbStops, 7);
76
+ });
77
+ });
78
+ });
@@ -0,0 +1,144 @@
1
+ import { StopId } from '../stops/stops.js';
2
+ import { StopsIndex } from '../stops/stopsIndex.js';
3
+ import { NOT_AVAILABLE } from '../timetable/route.js';
4
+ import { Duration, Time } from '../timetable/time.js';
5
+ import { Timetable } from '../timetable/timetable.js';
6
+
7
+ /**
8
+ * An access path from the query origin to an initial boarding stop.
9
+ *
10
+ * Equivalent origin stops (reached at zero cost) have no `duration`.
11
+ * Stops reachable via a timed walking transfer carry a `duration` in minutes.
12
+ */
13
+ export type AccessPoint = {
14
+ fromStopId: StopId;
15
+ toStopId: StopId;
16
+ duration: Duration;
17
+ };
18
+
19
+ /**
20
+ * Collects access paths from a query origin and resolves the set of
21
+ * distinct departure-time slots for Range RAPTOR.
22
+ */
23
+ export class AccessFinder {
24
+ private readonly timetable: Timetable;
25
+ private readonly stopsIndex: StopsIndex;
26
+
27
+ constructor(timetable: Timetable, stopsIndex: StopsIndex) {
28
+ this.timetable = timetable;
29
+ this.stopsIndex = stopsIndex;
30
+ }
31
+
32
+ /**
33
+ * Returns every initial access path from the query origin: equivalent stops
34
+ * (no duration) plus every stop reachable via a single timed walking transfer
35
+ * (REQUIRES_MINIMAL_TIME), keeping the shortest walk when multiple origins
36
+ * can reach the same stop.
37
+ *
38
+ * @param origin Origin stop ID.
39
+ * @param fallbackMinTransferTime Transfer time used when a walking transfer
40
+ * has no explicit `minTransferTime` in the timetable data.
41
+ */
42
+ collectAccessPaths(
43
+ queryOrigin: StopId,
44
+ fallbackMinTransferTime: Duration,
45
+ ): AccessPoint[] {
46
+ const equivalentOrigins = this.stopsIndex
47
+ .equivalentStops(queryOrigin)
48
+ .map((stop) => stop.id);
49
+
50
+ const accessPaths = new Map<StopId, AccessPoint>();
51
+ for (const origin of equivalentOrigins) {
52
+ const existingAccess = accessPaths.get(origin);
53
+ if (existingAccess === undefined || existingAccess.duration > 0) {
54
+ accessPaths.set(origin, {
55
+ fromStopId: origin,
56
+ toStopId: origin,
57
+ duration: 0,
58
+ });
59
+ }
60
+ for (const transfer of this.timetable.getTransfers(origin)) {
61
+ if (transfer.type === 'REQUIRES_MINIMAL_TIME') {
62
+ const duration = transfer.minTransferTime ?? fallbackMinTransferTime;
63
+ const existingAccess = accessPaths.get(transfer.destination);
64
+ // Keep the shortest walk to maximize the set of reachable trips.
65
+ if (
66
+ existingAccess === undefined ||
67
+ (existingAccess.duration && duration < existingAccess.duration)
68
+ ) {
69
+ accessPaths.set(transfer.destination, {
70
+ fromStopId: origin,
71
+ toStopId: transfer.destination,
72
+ duration,
73
+ });
74
+ }
75
+ }
76
+ }
77
+ }
78
+ return Array.from(accessPaths.values());
79
+ }
80
+
81
+ /**
82
+ * Collects all distinct origin departure times within `[from, to]`
83
+ * (inclusive) and, for each slot, the specific access paths that directly
84
+ * induce it — i.e. paths whose boarded stop has a boardable trip departing
85
+ * at exactly `depTime + path.duration`.
86
+ *
87
+ * Returned array is sorted **latest-first**. The Range RAPTOR outer loop
88
+ * seeds only the responsible paths for each slot, avoiding redundant
89
+ * exploration of access stops whose boarding opportunities belong to a
90
+ * later slot and whose journeys would therefore be dominated by it.
91
+ *
92
+
93
+ * @param accessPaths Access paths from the origin to initial boarding stops.
94
+ * @param from Earliest origin departure time (inclusive).
95
+ * @param to Latest origin departure time (inclusive).
96
+ */
97
+ collectDepartureTimes(
98
+ accessPaths: AccessPoint[],
99
+ from: Time,
100
+ to: Time,
101
+ ): { depTime: Time; legs: AccessPoint[] }[] {
102
+ // Map from origin-departure-time → the set of access paths that induce it.
103
+ const slotMap = new Map<Time, Set<AccessPoint>>();
104
+
105
+ for (const path of accessPaths) {
106
+ const { toStopId } = path;
107
+ // Trips from this stop must depart in [from + duration, to + duration]
108
+ // so that the corresponding origin departure (dep - duration) falls in
109
+ // [from, to].
110
+ const searchFrom = from + path.duration;
111
+ const searchTo = to + path.duration;
112
+ for (const route of this.timetable.routesPassingThrough(toStopId)) {
113
+ for (const stopIndex of route.stopRouteIndices(toStopId)) {
114
+ let tripIndex = route.findEarliestTrip(stopIndex, searchFrom);
115
+ if (tripIndex === undefined) continue;
116
+ const nbTrips = route.getNbTrips();
117
+ while (tripIndex < nbTrips) {
118
+ const dep = route.departureFrom(stopIndex, tripIndex);
119
+ if (dep > searchTo) break;
120
+ if (route.pickUpTypeFrom(stopIndex, tripIndex) !== NOT_AVAILABLE) {
121
+ const t = dep - path.duration;
122
+ let paths = slotMap.get(t);
123
+ if (paths === undefined) {
124
+ slotMap.set(t, (paths = new Set<AccessPoint>()));
125
+ }
126
+ paths.add(path);
127
+ }
128
+ tripIndex++;
129
+ }
130
+ }
131
+ }
132
+ }
133
+
134
+ if (slotMap.size === 0) return [];
135
+
136
+ // Sort descending so the outer loop processes latest departures first.
137
+ const sorted = Array.from(slotMap.entries()).sort(([a], [b]) => b - a);
138
+
139
+ return sorted.map(([depTime, paths]) => ({
140
+ depTime,
141
+ legs: Array.from(paths),
142
+ }));
143
+ }
144
+ }
@@ -0,0 +1,60 @@
1
+ import { StopsIndex } from '../stops/stopsIndex.js';
2
+ import { Timetable } from '../timetable/timetable.js';
3
+ import { AccessFinder } from './access.js';
4
+ import { Query } from './query.js';
5
+ import { Raptor } from './raptor.js';
6
+ import { Result } from './result.js';
7
+ import { RoutingState } from './state.js';
8
+
9
+ export class PlainRouter {
10
+ private readonly timetable: Timetable;
11
+ private readonly stopsIndex: StopsIndex;
12
+ private readonly accessFinder: AccessFinder;
13
+ private readonly raptor: Raptor;
14
+
15
+ constructor(
16
+ timetable: Timetable,
17
+ stopsIndex: StopsIndex,
18
+ accessFinder: AccessFinder,
19
+ raptor: Raptor,
20
+ ) {
21
+ this.timetable = timetable;
22
+ this.stopsIndex = stopsIndex;
23
+ this.accessFinder = accessFinder;
24
+ this.raptor = raptor;
25
+ }
26
+
27
+ /**
28
+ * Standard RAPTOR: finds the earliest-arrival journey from `query.from` to
29
+ * `query.to` for the given departure time.
30
+ *
31
+ * @param query The routing query.
32
+ * @returns A {@link Result} that can reconstruct the best route and arrival times.
33
+ */
34
+ route(query: Query): Result {
35
+ const accessLegs = this.accessFinder.collectAccessPaths(
36
+ query.from,
37
+ query.options.minTransferTime,
38
+ );
39
+
40
+ const destinations = Array.from(query.to)
41
+ .flatMap((destination) => this.stopsIndex.equivalentStops(destination))
42
+ .map((destination) => destination.id);
43
+
44
+ const routingState = new RoutingState(
45
+ query.departureTime,
46
+ destinations,
47
+ accessLegs,
48
+ this.timetable.nbStops(),
49
+ query.options.maxTransfers + 1,
50
+ );
51
+
52
+ this.raptor.run(query.options, routingState);
53
+ return new Result(
54
+ new Set(destinations),
55
+ routingState,
56
+ this.stopsIndex,
57
+ this.timetable,
58
+ );
59
+ }
60
+ }
@@ -1,7 +1,12 @@
1
1
  import { StopId } from '../stops/stops.js';
2
2
  import { durationToString, timeToString } from '../timetable/time.js';
3
3
  import { Result } from './result.js';
4
- import { RoutingEdge, TransferEdge, VehicleEdge } from './router.js';
4
+ import {
5
+ AccessEdge,
6
+ RoutingEdge,
7
+ TransferEdge,
8
+ VehicleEdge,
9
+ } from './router.js';
5
10
 
6
11
  /**
7
12
  * Configuration for DOT graph styling.
@@ -44,6 +49,13 @@ function isTransferEdge(edge: RoutingEdge): edge is TransferEdge {
44
49
  return 'from' in edge && 'to' in edge && 'type' in edge;
45
50
  }
46
51
 
52
+ /**
53
+ * Type guard to check if an edge is an AccessEdge (walking access leg).
54
+ */
55
+ function isAccessEdge(edge: RoutingEdge): edge is AccessEdge {
56
+ return 'from' in edge && 'duration' in edge;
57
+ }
58
+
47
59
  /**
48
60
  * Helper class for building DOT graph syntax.
49
61
  */
@@ -164,6 +176,13 @@ export class Plotter {
164
176
  return `e_${fromStopId}_${toStopId}_${round}`;
165
177
  }
166
178
 
179
+ /**
180
+ * Generates a unique node ID for a walking access edge oval.
181
+ */
182
+ private accessEdgeNodeId(fromStopId: StopId, toStopId: StopId): string {
183
+ return `access_${fromStopId}_${toStopId}`;
184
+ }
185
+
167
186
  /**
168
187
  * Generates a unique node ID for a continuation edge oval.
169
188
  */
@@ -326,6 +345,24 @@ export class Plotter {
326
345
  ];
327
346
  }
328
347
 
348
+ /**
349
+ * Creates a walking access leg as a dashed oval connecting the query origin
350
+ * to the initial boarding stop.
351
+ */
352
+ private createAccessEdge(edge: AccessEdge): string[] {
353
+ const fromNodeId = this.stationNodeId(edge.from);
354
+ const toNodeId = this.stationNodeId(edge.to);
355
+ const color = DOT_CONFIG.colors.defaultRound;
356
+ const ovalId = this.accessEdgeNodeId(edge.from, edge.to);
357
+ const label = `Walk\\n${durationToString(edge.duration)}`;
358
+
359
+ return [
360
+ ` "${ovalId}" [label="${label}" shape=oval style="dashed,filled" fillcolor="white" color="${color}"];`,
361
+ ` "${fromNodeId}" -> "${ovalId}" [color="${color}" style="dashed"];`,
362
+ ` "${ovalId}" -> "${toNodeId}" [color="${color}" style="dashed"];`,
363
+ ];
364
+ }
365
+
329
366
  /**
330
367
  * Creates a transfer edge with transfer information oval in the middle.
331
368
  */
@@ -433,6 +470,11 @@ export class Plotter {
433
470
  const toStopId = this.getVehicleEdgeToStopId(edge);
434
471
  if (fromStopId) stations.add(fromStopId);
435
472
  if (toStopId) stations.add(toStopId);
473
+ } else if (isAccessEdge(edge)) {
474
+ // Ensure the query origin (edge.from) is always collected even when
475
+ // its own OriginNode hasn't been processed yet in this iteration.
476
+ stations.add(edge.from);
477
+ stations.add(edge.to);
436
478
  }
437
479
  }
438
480
  }
@@ -475,14 +517,19 @@ export class Plotter {
475
517
  const roundEdges = graph[round];
476
518
  if (!roundEdges) continue;
477
519
 
478
- // Skip round 0 as it contains only origin nodes
479
- if (round === 0) {
480
- continue;
481
- }
482
-
483
520
  for (let stopId = 0; stopId < roundEdges.length; stopId++) {
484
521
  const edge = roundEdges[stopId];
485
522
  if (edge === undefined) continue;
523
+
524
+ if (round === 0) {
525
+ // Round 0 holds OriginNodes (no edge to draw) and AccessEdges
526
+ // (walking legs from the query origin to the first boarding stop).
527
+ if (isAccessEdge(edge)) {
528
+ edges.push(...this.createAccessEdge(edge));
529
+ }
530
+ continue;
531
+ }
532
+
486
533
  if (isVehicleEdge(edge)) {
487
534
  edges.push(...this.createVehicleEdge(edge, round));
488
535
 
@@ -6,13 +6,28 @@ export type QueryOptions = {
6
6
  maxTransfers: number;
7
7
  minTransferTime: Duration;
8
8
  transportModes: Set<RouteType>;
9
+ /**
10
+ * Maximum time (in minutes) the traveler is willing to wait at the first
11
+ * boarding stop before the first transit vehicle departs.
12
+ *
13
+ * When set, any trip that would require waiting longer than this duration
14
+ * after arriving at the stop is skipped for the first boarding leg.
15
+ * Undefined means no limit.
16
+ */
17
+ maxInitialWaitingTime?: Duration;
9
18
  };
10
19
 
20
+ /**
21
+ * A routing query for standard RAPTOR.
22
+ *
23
+ * Finds the earliest-arrival journey from `from` to `to` for a single
24
+ * departure time. Use {@link RangeQuery} (and `router.rangeRoute()`) when
25
+ * you want all Pareto-optimal journeys within a departure-time window.
26
+ */
11
27
  export class Query {
12
28
  from: StopId;
13
29
  to: Set<StopId>;
14
30
  departureTime: Time;
15
- lastDepartureTime?: Time;
16
31
  options: QueryOptions;
17
32
 
18
33
  constructor(builder: typeof Query.Builder.prototype) {
@@ -26,11 +41,7 @@ export class Query {
26
41
  fromValue!: StopId;
27
42
  toValue: Set<StopId> = new Set();
28
43
  departureTimeValue!: Time;
29
- optionsValue: {
30
- maxTransfers: number;
31
- minTransferTime: Duration;
32
- transportModes: Set<RouteType>;
33
- } = {
44
+ optionsValue: QueryOptions = {
34
45
  maxTransfers: 5,
35
46
  minTransferTime: durationFromSeconds(120),
36
47
  transportModes: ALL_TRANSPORT_MODES,
@@ -45,7 +56,8 @@ export class Query {
45
56
  }
46
57
 
47
58
  /**
48
- * Sets the destination stops(s), routing will stop when all the provided stops are reached.
59
+ * Sets the destination stop(s).
60
+ * Routing stops as soon as all provided stops have been reached.
49
61
  */
50
62
  to(to: StopId | Set<StopId>): this {
51
63
  this.toValue = to instanceof Set ? to : new Set([to]);
@@ -53,9 +65,8 @@ export class Query {
53
65
  }
54
66
 
55
67
  /**
56
- * Sets the departure time for the query as minutes since midnight.
57
- * Note that the router will favor routes that depart shortly after the provided departure time,
58
- * even if a later route might arrive at the same time.
68
+ * Sets the departure time in minutes from midnight.
69
+ * The router favours trips departing shortly after this time.
59
70
  */
60
71
  departureTime(departureTime: Time): this {
61
72
  this.departureTimeValue = departureTime;
@@ -71,8 +82,8 @@ export class Query {
71
82
  }
72
83
 
73
84
  /**
74
- * Sets the minimum transfer time (in minutes)
75
- * to use when no transfer time is provided in the data.
85
+ * Sets the fallback minimum transfer time (in minutes) used when the
86
+ * timetable data does not specify one for a particular transfer.
76
87
  */
77
88
  minTransferTime(minTransferTime: Duration): this {
78
89
  this.optionsValue.minTransferTime = minTransferTime;
@@ -80,15 +91,107 @@ export class Query {
80
91
  }
81
92
 
82
93
  /**
83
- * Sets the transport modes to consider.
94
+ * Restricts routing to the given transport modes.
84
95
  */
85
96
  transportModes(transportModes: Set<RouteType>): this {
86
97
  this.optionsValue.transportModes = transportModes;
87
98
  return this;
88
99
  }
89
100
 
101
+ /**
102
+ * Sets the maximum time (in minutes) the traveler is willing to wait at
103
+ * the first boarding stop before the first transit vehicle departs.
104
+ *
105
+ * When set, any trip that would require waiting longer than this duration
106
+ * after arriving at the stop is not considered for the first boarding leg.
107
+ */
108
+ maxInitialWaitingTime(maxInitialWaitingTime: Duration): this {
109
+ this.optionsValue.maxInitialWaitingTime = maxInitialWaitingTime;
110
+ return this;
111
+ }
112
+
90
113
  build(): Query {
91
114
  return new Query(this);
92
115
  }
93
116
  };
94
117
  }
118
+
119
+ /**
120
+ * Options specific to a {@link RangeQuery}.
121
+ */
122
+ export type RangeQueryOptions = {
123
+ /**
124
+ * When `true`, a full RAPTOR pass is run at `lastDepartureTime + 1` before
125
+ * the main departure-time loop (the *boundary run*).
126
+ *
127
+ * The boundary run seeds the shared Pareto labels with the best arrival
128
+ * achievable by departing just after the window closes. Any in-window
129
+ * journey whose arrival is no better than what that post-window departure
130
+ * achieves is therefore suppressed — you only see journeys that are still
131
+ * worth taking given that a later departure exists.
132
+ *
133
+ * **Timetable use-case** (`boundaryRun: true`): the window is a *display
134
+ * filter*. A journey at 10:55 that arrives at 12:30 is hidden when an
135
+ * 11:05 departure arrives at 12:00 — the router pre-empts the dominated
136
+ * option on the caller's behalf.
137
+ *
138
+ * **Isochrone / accessibility use-case** (`boundaryRun: false`, the
139
+ * default): the window is a hard constraint. Every Pareto-optimal journey
140
+ * whose departure falls strictly within `[departureTime, lastDepartureTime]`
141
+ * is returned, regardless of what might be available just outside the
142
+ * window.
143
+ *
144
+ * @default false
145
+ */
146
+ optimizeBeyondLatestDeparture: boolean;
147
+ };
148
+
149
+ /**
150
+ * A routing query for Range RAPTOR.
151
+ *
152
+ * Extends {@link Query} with a required `lastDepartureTime` that defines the
153
+ * upper bound of the departure-time window. `router.rangeRoute()` returns
154
+ * all Pareto-optimal journeys departing in
155
+ * `[departureTime, lastDepartureTime]`.
156
+ *
157
+ */
158
+ export class RangeQuery extends Query {
159
+ /** Upper bound of the departure-time window (minutes from midnight). */
160
+ readonly lastDepartureTime: Time;
161
+ /** Options specific to Range RAPTOR behavior. */
162
+ readonly rangeOptions: RangeQueryOptions;
163
+
164
+ constructor(builder: typeof RangeQuery.Builder.prototype) {
165
+ super(builder);
166
+ this.lastDepartureTime = builder.lastDepartureTimeValue;
167
+ this.rangeOptions = builder.rangeOptionsValue;
168
+ }
169
+
170
+ static Builder = class extends Query.Builder {
171
+ lastDepartureTimeValue!: Time;
172
+ rangeOptionsValue: RangeQueryOptions = {
173
+ optimizeBeyondLatestDeparture: true,
174
+ };
175
+
176
+ /**
177
+ * Sets the upper bound of the departure-time window.
178
+ */
179
+ lastDepartureTime(time: Time): this {
180
+ this.lastDepartureTimeValue = time;
181
+ return this;
182
+ }
183
+
184
+ /**
185
+ * Overrides individual Range RAPTOR options.
186
+ * Unspecified fields keep their defaults.
187
+ */
188
+ rangeOptions(options: Partial<RangeQueryOptions>): this {
189
+ this.rangeOptionsValue = { ...this.rangeOptionsValue, ...options };
190
+ return this;
191
+ }
192
+
193
+ build(): RangeQuery {
194
+ return new RangeQuery(this);
195
+ }
196
+ };
197
+ }