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,292 @@
1
+ import { StopId } from '../stops/stops.js';
2
+ import { Duration, Time } from '../timetable/time.js';
3
+ import { Result } from './result.js';
4
+ import { Route } from './route.js';
5
+ import { Arrival } from './state.js';
6
+
7
+ /**
8
+ * A single departure-time iteration that produced at least one Pareto-optimal
9
+ * journey to this result's destination set.
10
+ */
11
+ export type ParetoRun = {
12
+ /** Departure time from the origin (minutes from midnight) for this run. */
13
+ readonly departureTime: Time;
14
+ /** Full RAPTOR result for this departure time — use it to reconstruct routes. */
15
+ readonly result: Result;
16
+ };
17
+
18
+ /**
19
+ * An {@link Arrival} enriched with the travel duration from the origin.
20
+ *
21
+ * Returned by duration-based methods on {@link RangeResult} so callers
22
+ * receive both the absolute arrival time with transfer count *and* the total
23
+ * travel time that was optimized over.
24
+ */
25
+ export type ArrivalWithDuration = Arrival & {
26
+ /** Total travel time from origin departure to stop arrival (minutes). */
27
+ readonly duration: Duration;
28
+ };
29
+
30
+ /**
31
+ * The result of a Range RAPTOR query.
32
+ *
33
+ * Contains the complete Pareto-optimal set of journeys for a resolved
34
+ * destination set.
35
+ *
36
+ * **Pareto dominance**: journey J1 dominates J2 iff
37
+ * `τdep(J1) ≥ τdep(J2) AND τarr(J1) ≤ τarr(J2)`
38
+ * (with at least one strict inequality).
39
+ *
40
+ * Runs are ordered **latest-departure-first**: each successive run departs
41
+ * strictly earlier *and* arrives strictly earlier than the previous one,
42
+ * forming the classic staircase Pareto frontier.
43
+ *
44
+ * Destination handling is delegated to {@link Result}, which expands
45
+ * equivalent stops when reconstructing routes or looking up arrivals.
46
+ */
47
+ export class RangeResult {
48
+ private readonly _runs: readonly ParetoRun[];
49
+ private readonly _destinations: ReadonlySet<StopId>;
50
+
51
+ constructor(runs: ParetoRun[], destinations: ReadonlySet<StopId>) {
52
+ this._runs = runs;
53
+ this._destinations = destinations;
54
+ }
55
+
56
+ /** The resolved destination stop IDs for this result. */
57
+ get destinations(): ReadonlySet<StopId> {
58
+ return this._destinations;
59
+ }
60
+
61
+ private normalizeTargets(to?: StopId | Set<StopId>): Set<StopId> {
62
+ if (to instanceof Set) return new Set(to);
63
+ if (to !== undefined) return new Set([to]);
64
+ return new Set(this._destinations);
65
+ }
66
+
67
+ /**
68
+ * Returns all non-dominated routes to this result's default destination set,
69
+ * ordered from the earliest departure to the latest departure.
70
+ *
71
+ * Each route in the list departs strictly earlier *and* arrives strictly
72
+ * earlier than its predecessor.
73
+ */
74
+ getRoutes(): Route[] {
75
+ const routes: Route[] = [];
76
+ for (const { result } of this._runs) {
77
+ const route = result.bestRoute();
78
+ if (route !== undefined) routes.push(route);
79
+ }
80
+ return routes.reverse();
81
+ }
82
+
83
+ /**
84
+ * The route that arrives **earliest** at the given stop(s) across all
85
+ * Pareto-optimal runs.
86
+ *
87
+ * When two runs achieve the same arrival time at the target, the one with
88
+ * the **later departure** is preferred — you wait at the origin rather than
89
+ * at a transit stop.
90
+ *
91
+ * Defaults to this result's own destination stop(s) when `to` is omitted.
92
+ *
93
+ * @param to Optional destination stop ID or set of stop IDs.
94
+ * @returns The reconstructed {@link Route} with the earliest arrival,
95
+ * or `undefined` if the target is unreachable in every run.
96
+ */
97
+ bestRoute(to?: StopId | Set<StopId>): Route | undefined {
98
+ const targetStops = this.normalizeTargets(to);
99
+
100
+ let bestRun: ParetoRun | undefined;
101
+ let bestArrival: Time | undefined;
102
+
103
+ for (const run of this._runs) {
104
+ for (const stopId of targetStops) {
105
+ const arrival = run.result.arrivalAt(stopId);
106
+ if (arrival === undefined) continue;
107
+ if (bestArrival === undefined || arrival.arrival < bestArrival) {
108
+ bestArrival = arrival.arrival;
109
+ bestRun = run;
110
+ }
111
+ }
112
+ }
113
+
114
+ return bestRun?.result.bestRoute(targetStops);
115
+ }
116
+
117
+ /**
118
+ * The route with the **latest possible departure** from the origin among all
119
+ * Pareto-optimal journeys in the window.
120
+ *
121
+ * This is the journey that lets you leave the origin as late as possible.
122
+ * It does **not** necessarily achieve the earliest arrival — for that, use
123
+ * {@link bestRoute}. For the shortest travel duration, use
124
+ * {@link fastestRoute}.
125
+ *
126
+ * Defaults to this result's own destination stop(s) when `to` is omitted.
127
+ *
128
+ * @param to Optional destination stop ID or set of stop IDs.
129
+ * @returns The reconstructed {@link Route} with the latest departure,
130
+ * or `undefined` if the target is unreachable in every run.
131
+ */
132
+ latestDepartureRoute(to?: StopId | Set<StopId>): Route | undefined {
133
+ const targetStops = this.normalizeTargets(to);
134
+ for (const { result } of this._runs) {
135
+ const route = result.bestRoute(targetStops);
136
+ if (route !== undefined) return route;
137
+ }
138
+ return undefined;
139
+ }
140
+
141
+ /**
142
+ * Reconstructs the **fastest** route to the given stop(s) — the journey with
143
+ * the shortest travel duration (arrival time − origin departure time) across
144
+ * all Pareto-optimal runs.
145
+ *
146
+ * Unlike {@link bestRoute}, which returns the route that departs as late as
147
+ * possible while still arriving early, this method minimizes total time
148
+ * spent traveling.
149
+ *
150
+ * Defaults to this result's own destination stop(s) when `to` is omitted.
151
+ *
152
+ * @param to Optional destination stop ID or set of stop IDs.
153
+ * @returns The reconstructed fastest {@link Route}, or `undefined` if the
154
+ * target is unreachable in every run.
155
+ */
156
+ fastestRoute(to?: StopId | Set<StopId>): Route | undefined {
157
+ const targetStops = this.normalizeTargets(to);
158
+
159
+ let fastestRun: ParetoRun | undefined;
160
+ let shortestDuration = Infinity;
161
+
162
+ for (const run of this._runs) {
163
+ for (const stopId of targetStops) {
164
+ const arrival = run.result.arrivalAt(stopId);
165
+ if (arrival === undefined) continue;
166
+ const duration = arrival.arrival - run.departureTime;
167
+ if (duration < shortestDuration) {
168
+ shortestDuration = duration;
169
+ fastestRun = run;
170
+ }
171
+ }
172
+ }
173
+
174
+ return fastestRun?.result.bestRoute(targetStops);
175
+ }
176
+
177
+ /** Number of Pareto-optimal journeys found. */
178
+ get size(): number {
179
+ return this._runs.length;
180
+ }
181
+
182
+ /**
183
+ * Earliest achievable arrival at a stop across all Pareto-optimal runs.
184
+ *
185
+ * Useful for isochrone / accessibility analysis: given this result's
186
+ * departure-time frontier, how early can you reach stop `s` regardless of
187
+ * which specific trip you take?
188
+ *
189
+ * Equivalent stops are handled by {@link Result.arrivalAt}.
190
+ *
191
+ * @param stop The target stop ID.
192
+ * @param maxTransfers Optional upper bound on the number of transfers.
193
+ */
194
+ earliestArrivalAt(stop: StopId, maxTransfers?: number): Arrival | undefined {
195
+ let best: Arrival | undefined;
196
+ for (const { result } of this._runs) {
197
+ const arrival = result.arrivalAt(stop, maxTransfers);
198
+ if (
199
+ arrival !== undefined &&
200
+ (best === undefined || arrival.arrival < best.arrival)
201
+ ) {
202
+ best = arrival;
203
+ }
204
+ }
205
+ return best;
206
+ }
207
+
208
+ /**
209
+ * Shortest travel duration to reach a stop across all Pareto-optimal runs.
210
+ *
211
+ * For each run, duration is measured from the run's origin departure time to
212
+ * the earliest arrival at `stop` within that run. The minimum across all
213
+ * runs is returned.
214
+ *
215
+ * Equivalent stops are handled by {@link Result.arrivalAt}.
216
+ *
217
+ * Duration is **not** monotone along the Pareto frontier — a run that
218
+ * departs later may still travel faster — so every run is checked. In
219
+ * practice the Pareto frontier is small, so this is O(runs).
220
+ *
221
+ * Returns `undefined` if `stop` is unreachable in every run.
222
+ *
223
+ * @param stop The target stop ID.
224
+ * @param maxTransfers Optional upper bound on the number of transfers.
225
+ */
226
+ shortestDurationTo(
227
+ stop: StopId,
228
+ maxTransfers?: number,
229
+ ): ArrivalWithDuration | undefined {
230
+ let shortest: ArrivalWithDuration | undefined;
231
+ for (const { departureTime, result } of this._runs) {
232
+ const arrival = result.arrivalAt(stop, maxTransfers);
233
+ if (arrival === undefined) continue;
234
+ const duration = arrival.arrival - departureTime;
235
+ if (shortest === undefined || duration < shortest.duration) {
236
+ shortest = { ...arrival, duration };
237
+ }
238
+ }
239
+ return shortest;
240
+ }
241
+
242
+ /**
243
+ * Shortest travel duration to **every reachable stop** across all
244
+ * Pareto-optimal runs, as a single `Map<StopId, DurationArrival>`.
245
+ */
246
+ allShortestDurations(): Map<StopId, ArrivalWithDuration> {
247
+ const durations = new Map<StopId, ArrivalWithDuration>();
248
+ for (const { departureTime, result } of this._runs) {
249
+ for (const {
250
+ stop,
251
+ arrival,
252
+ legNumber,
253
+ } of result.routingState.arrivals()) {
254
+ const duration = arrival - departureTime;
255
+ const existing = durations.get(stop);
256
+ if (existing === undefined || duration < existing.duration) {
257
+ durations.set(stop, { arrival, legNumber, duration });
258
+ }
259
+ }
260
+ }
261
+ return durations;
262
+ }
263
+
264
+ /**
265
+ * Earliest achievable arrival at **every reachable stop** across all
266
+ * Pareto-optimal runs, as a single `Map<StopId, Arrival>`.
267
+ */
268
+ allEarliestArrivals(): Map<StopId, Arrival> {
269
+ const arrivals = new Map<StopId, Arrival>();
270
+ for (const { result } of this._runs) {
271
+ for (const {
272
+ stop,
273
+ arrival,
274
+ legNumber,
275
+ } of result.routingState.arrivals()) {
276
+ const existing = arrivals.get(stop);
277
+ if (existing === undefined || arrival < existing.arrival) {
278
+ arrivals.set(stop, { arrival, legNumber });
279
+ }
280
+ }
281
+ }
282
+ return arrivals;
283
+ }
284
+
285
+ /**
286
+ * Iterates over all Pareto-optimal `(departureTime, result)` pairs,
287
+ * ordered from the latest departure to the earliest departure.
288
+ */
289
+ [Symbol.iterator](): IterableIterator<ParetoRun> {
290
+ return this._runs[Symbol.iterator]() as IterableIterator<ParetoRun>;
291
+ }
292
+ }
@@ -0,0 +1,167 @@
1
+ import { StopId } from '../stops/stops.js';
2
+ import { StopsIndex } from '../stops/stopsIndex.js';
3
+ import { Time } from '../timetable/time.js';
4
+ import { Timetable } from '../timetable/timetable.js';
5
+ import { AccessFinder } from './access.js';
6
+ import { RangeQuery } from './query.js';
7
+ import { ParetoRun, RangeResult } from './rangeResult.js';
8
+ import { RangeRaptorState } from './rangeState.js';
9
+ import { Raptor } from './raptor.js';
10
+ import { Result } from './result.js';
11
+ import { RoutingState, UNREACHED_TIME } from './state.js';
12
+
13
+ export class RangeRouter {
14
+ private readonly timetable: Timetable;
15
+ private readonly stopsIndex: StopsIndex;
16
+ private readonly accessFinder: AccessFinder;
17
+ private readonly raptor: Raptor;
18
+
19
+ constructor(
20
+ timetable: Timetable,
21
+ stopsIndex: StopsIndex,
22
+ accessFinder: AccessFinder,
23
+ raptor: Raptor,
24
+ ) {
25
+ this.timetable = timetable;
26
+ this.stopsIndex = stopsIndex;
27
+ this.accessFinder = accessFinder;
28
+ this.raptor = raptor;
29
+ }
30
+
31
+ /**
32
+ * Range RAPTOR: finds all Pareto-optimal journeys within the departure-time
33
+ * window `[query.departureTime, query.lastDepartureTime]`.
34
+ *
35
+ * A journey is Pareto-optimal iff no journey departing no earlier arrives no
36
+ * later. Runs are ordered latest-departure-first in the returned result.
37
+ *
38
+ * @param query A {@link RangeQuery} with both `departureTime` and `lastDepartureTime` set.
39
+ * @returns A {@link RangeResult} exposing the full Pareto frontier.
40
+ */
41
+ rangeRoute(query: RangeQuery): RangeResult {
42
+ const { departureTime: earliest, lastDepartureTime: latest } = query;
43
+
44
+ const destinations = Array.from(query.to)
45
+ .flatMap((destination) => this.stopsIndex.equivalentStops(destination))
46
+ .map((destination) => destination.id);
47
+
48
+ const accessLegs = this.accessFinder.collectAccessPaths(
49
+ query.from,
50
+ query.options.minTransferTime,
51
+ );
52
+
53
+ const departureSlots = this.accessFinder.collectDepartureTimes(
54
+ accessLegs,
55
+ earliest,
56
+ latest,
57
+ );
58
+ if (departureSlots.length === 0) {
59
+ return new RangeResult([], new Set(destinations));
60
+ }
61
+
62
+ const maxRounds = query.options.maxTransfers + 1;
63
+
64
+ const rangeState = new RangeRaptorState(
65
+ maxRounds,
66
+ this.timetable.nbStops(),
67
+ latest,
68
+ );
69
+
70
+ const paretoRuns: ParetoRun[] = [];
71
+
72
+ const paretoDestBest = new Map<StopId, Time>();
73
+ for (const dest of destinations) {
74
+ paretoDestBest.set(dest, UNREACHED_TIME);
75
+ }
76
+
77
+ const trivialDests = new Set(
78
+ accessLegs
79
+ .map((leg) => leg.toStopId)
80
+ .filter((id) => destinations.includes(id)),
81
+ );
82
+ const trivialDestCovered = new Set<StopId>();
83
+
84
+ let routingState: RoutingState | null = null;
85
+
86
+ if (query.rangeOptions.optimizeBeyondLatestDeparture) {
87
+ routingState = new RoutingState(
88
+ latest + 1,
89
+ destinations,
90
+ accessLegs,
91
+ this.timetable.nbStops(),
92
+ maxRounds,
93
+ );
94
+ rangeState.setCurrentRun(routingState);
95
+ this.raptor.run(
96
+ {
97
+ ...query.options,
98
+ maxInitialWaitingTime: undefined,
99
+ },
100
+ rangeState,
101
+ );
102
+ for (const dest of destinations) {
103
+ const t = routingState.arrivalTime(dest);
104
+ if (t < (paretoDestBest.get(dest) ?? UNREACHED_TIME))
105
+ paretoDestBest.set(dest, t);
106
+ }
107
+ }
108
+
109
+ for (const { depTime, legs } of departureSlots) {
110
+ if (trivialDestCovered.size === destinations.length) break;
111
+
112
+ if (routingState === null) {
113
+ routingState = new RoutingState(
114
+ depTime,
115
+ destinations,
116
+ legs,
117
+ this.timetable.nbStops(),
118
+ maxRounds,
119
+ );
120
+ } else {
121
+ routingState.resetFor(depTime, legs);
122
+ }
123
+ rangeState.setCurrentRun(routingState);
124
+ this.raptor.run(
125
+ {
126
+ ...query.options,
127
+ maxInitialWaitingTime: 0,
128
+ },
129
+ rangeState,
130
+ );
131
+
132
+ let isParetoOptimal = false;
133
+ for (const dest of destinations) {
134
+ const arrival = routingState.arrivalTime(dest);
135
+ if (arrival >= (paretoDestBest.get(dest) ?? UNREACHED_TIME)) {
136
+ continue;
137
+ }
138
+
139
+ if (trivialDests.has(dest) && trivialDestCovered.has(dest)) {
140
+ paretoDestBest.set(dest, arrival);
141
+ continue;
142
+ }
143
+
144
+ paretoDestBest.set(dest, arrival);
145
+ if (trivialDests.has(dest)) {
146
+ trivialDestCovered.add(dest);
147
+ }
148
+ isParetoOptimal = true;
149
+ }
150
+
151
+ if (isParetoOptimal) {
152
+ paretoRuns.push({
153
+ departureTime: depTime,
154
+ result: new Result(
155
+ new Set(destinations),
156
+ routingState,
157
+ this.stopsIndex,
158
+ this.timetable,
159
+ ),
160
+ });
161
+ routingState = null;
162
+ }
163
+ }
164
+
165
+ return new RangeResult(paretoRuns, new Set(destinations));
166
+ }
167
+ }
@@ -0,0 +1,150 @@
1
+ /* eslint-disable @typescript-eslint/no-non-null-assertion */
2
+ import { StopId } from '../stops/stops.js';
3
+ import { Time } from '../timetable/time.js';
4
+ import type { IRaptorState } from './raptor.js';
5
+ import { RoutingEdge, RoutingState, UNREACHED_TIME } from './state.js';
6
+
7
+ /**
8
+ * RAPTOR state for Range RAPTOR mode, implementing {@link IRaptorState}.
9
+ *
10
+ * Holds both the cross-run shared labels (carried over from one departure-time
11
+ * iteration to the next, latest → earliest) and a reference to the current
12
+ * per-iteration {@link RoutingState} (swapped via {@link setCurrentRun}).
13
+ *
14
+ * Concretely, `roundLabels[k][p]` is the best known arrival at stop `p` using
15
+ * at most `k` transit legs, across **all departure times tried so far**.
16
+ *
17
+ * @see https://www.microsoft.com/en-us/research/wp-content/uploads/2012/01/raptor_alenex.pdf
18
+ */
19
+ export class RangeRaptorState implements IRaptorState {
20
+ /**
21
+ * `roundLabels[k]` is a flat `Uint16Array` of size `nbStops`.
22
+ * `roundLabels[k][p]` = best arrival time (minutes from midnight) at stop `p`
23
+ * in round `k`, across all departure-time iterations processed so far.
24
+ * Pre-filled with `UNREACHED_TIME`; updated in-place as better arrivals are found.
25
+ */
26
+ readonly roundLabels: Uint16Array[];
27
+
28
+ /**
29
+ * The latest departure time of the range query.
30
+ */
31
+ readonly latestDeparture: Time;
32
+
33
+ /**
34
+ * Global best arrival at any destination stop across all runs and rounds.
35
+ * Used for destination-pruning inside scan methods so that routes that cannot
36
+ * beat the already-known best are skipped early.
37
+ */
38
+ private _destinationBest: Time = UNREACHED_TIME;
39
+
40
+ /**
41
+ * Sparse change-tracking for `initRound`.
42
+ *
43
+ * `changedInRound[k]` is the list of stops whose round-k label was improved
44
+ * (via `tryImprove`) since the last call to `initRound(k + 1)`. When
45
+ * `initRound(k + 1)` runs, it only visits these stops instead of scanning
46
+ * all `nbStops` entries, reducing the work from O(nbStops × rounds ×
47
+ * departureTimes) to O(changedStops × rounds × departureTimes).
48
+ *
49
+ * Duplicates are allowed and harmless — a stop that appears twice merely
50
+ * receives a redundant (no-op) min-update on the second visit. The list is
51
+ * cleared inside `initRound` immediately after processing.
52
+ */
53
+ private readonly changedInRound: StopId[][];
54
+
55
+ private currentRun!: RoutingState;
56
+
57
+ constructor(maxRounds: number, nbStops: number, latestDeparture: Time) {
58
+ this.latestDeparture = latestDeparture;
59
+ // maxRounds + 2: index 0 = origin/walk legs, indices 1…maxRounds+1 = transit rounds
60
+ this.roundLabels = Array.from({ length: maxRounds + 2 }, () =>
61
+ new Uint16Array(nbStops).fill(UNREACHED_TIME),
62
+ );
63
+ this.changedInRound = Array.from({ length: maxRounds + 2 }, () => []);
64
+ }
65
+
66
+ /**
67
+ * Swaps in a fresh {@link RoutingState} for the next departure-time iteration
68
+ * and seeds the shared round-0 labels from its access arrivals.
69
+ *
70
+ * Must be called before every `runRaptor` invocation.
71
+ */
72
+ setCurrentRun(routingState: RoutingState): void {
73
+ this.currentRun = routingState;
74
+ // Propagate round-0 access arrivals into the shared labels so that
75
+ // initRound(1) can tighten round-1 pruning bounds correctly.
76
+ const round0 = routingState.graph[0]!;
77
+ for (const stop of routingState.origins) {
78
+ const edge = round0[stop];
79
+ if (!edge) continue;
80
+ this.updateArrival(stop, edge.arrival, 0);
81
+ }
82
+ }
83
+
84
+ get origins(): StopId[] {
85
+ return this.currentRun.origins;
86
+ }
87
+
88
+ get graph(): (RoutingEdge | undefined)[][] {
89
+ return this.currentRun.graph;
90
+ }
91
+
92
+ arrivalTime(stop: StopId): Time {
93
+ return this.currentRun.arrivalTime(stop);
94
+ }
95
+
96
+ /**
97
+ * Uses the cross-run shared label for `round`, which is always at least as
98
+ * tight as the per-run arrival and therefore provides stronger pruning.
99
+ */
100
+ improvementBound(round: number, stop: StopId): Time {
101
+ return this.roundLabels[round]![stop]!;
102
+ }
103
+
104
+ /**
105
+ * Global best arrival at any destination across all departure-time iterations.
106
+ * Always at least as tight as the per-run `destinationBest`.
107
+ */
108
+ get destinationBest(): Time {
109
+ return this._destinationBest;
110
+ }
111
+
112
+ isDestination(stop: StopId): boolean {
113
+ return this.currentRun.isDestination(stop);
114
+ }
115
+
116
+ /** Updates both the per-run state and the cross-run shared labels. */
117
+ updateArrival(stop: StopId, time: Time, round: number): void {
118
+ this.currentRun.updateArrival(stop, time, round);
119
+ if (time < this.roundLabels[round]![stop]!) {
120
+ this.roundLabels[round]![stop] = time;
121
+ this.changedInRound[round]!.push(stop);
122
+ if (this.currentRun.isDestination(stop) && time < this._destinationBest) {
123
+ this._destinationBest = time;
124
+ }
125
+ }
126
+ }
127
+
128
+ /**
129
+ * initialized round `k` from round `k-1`: τk(p) ← min(τk(p), τk-1(p)).
130
+ *
131
+ * Must be called at the very start of each RAPTOR round before routes are
132
+ * scanned. After this call, `roundLabels[k][p]` is the minimum arrival at
133
+ * stop `p` achievable with **at most** k transit legs from any departure time
134
+ * tried so far — which is exactly the tightest valid pruning bound for round k.
135
+ */
136
+ initRound(round: number): void {
137
+ const changed = this.changedInRound[round - 1]!;
138
+ if (changed.length === 0) return;
139
+
140
+ const prev = this.roundLabels[round - 1]!;
141
+ const curr = this.roundLabels[round]!;
142
+ for (let i = 0; i < changed.length; i++) {
143
+ const stop = changed[i]!;
144
+ if (prev[stop]! < curr[stop]!) {
145
+ curr[stop] = prev[stop]!;
146
+ }
147
+ }
148
+ changed.length = 0;
149
+ }
150
+ }