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.
- package/.cspell.json +7 -1
- package/CHANGELOG.md +3 -3
- package/README.md +111 -86
- package/dist/cli/perf.d.ts +57 -18
- package/dist/cli.mjs +1371 -342
- package/dist/cli.mjs.map +1 -1
- package/dist/parser.cjs.js +57 -4
- package/dist/parser.cjs.js.map +1 -1
- package/dist/parser.esm.js +57 -4
- 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 +5 -5
- 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/__tests__/access.test.d.ts +1 -0
- package/dist/routing/__tests__/plainRouter.test.d.ts +1 -0
- package/dist/routing/__tests__/rangeResult.test.d.ts +1 -0
- package/dist/routing/__tests__/rangeRouter.test.d.ts +1 -0
- package/dist/routing/__tests__/rangeState.test.d.ts +1 -0
- package/dist/routing/__tests__/raptor.test.d.ts +1 -0
- package/dist/routing/__tests__/state.test.d.ts +1 -0
- package/dist/routing/access.d.ts +55 -0
- package/dist/routing/plainRouter.d.ts +21 -0
- package/dist/routing/plotter.d.ts +9 -0
- package/dist/routing/query.d.ts +132 -13
- package/dist/routing/rangeResult.d.ts +155 -0
- package/dist/routing/rangeRouter.d.ts +24 -0
- package/dist/routing/rangeState.d.ts +83 -0
- package/dist/routing/raptor.d.ts +96 -0
- package/dist/routing/result.d.ts +27 -7
- package/dist/routing/route.d.ts +5 -21
- package/dist/routing/router.d.ts +20 -91
- package/dist/routing/state.d.ts +92 -17
- package/dist/timetable/route.d.ts +8 -0
- package/dist/timetable/timetable.d.ts +17 -1
- package/package.json +1 -1
- package/src/__e2e__/benchmark.json +18 -0
- package/src/__e2e__/router.test.ts +461 -127
- package/src/cli/minotor.ts +39 -3
- package/src/cli/perf.ts +324 -60
- package/src/cli/repl.ts +96 -41
- package/src/router.ts +11 -3
- package/src/routing/__tests__/access.test.ts +294 -0
- package/src/routing/__tests__/plainRouter.test.ts +1633 -0
- package/src/routing/__tests__/plotter.test.ts +8 -8
- package/src/routing/__tests__/rangeResult.test.ts +273 -0
- package/src/routing/__tests__/rangeRouter.test.ts +472 -0
- package/src/routing/__tests__/rangeState.test.ts +246 -0
- package/src/routing/__tests__/raptor.test.ts +366 -0
- package/src/routing/__tests__/result.test.ts +27 -27
- package/src/routing/__tests__/route.test.ts +28 -0
- package/src/routing/__tests__/router.test.ts +75 -1587
- package/src/routing/__tests__/state.test.ts +78 -0
- package/src/routing/access.ts +144 -0
- package/src/routing/plainRouter.ts +60 -0
- package/src/routing/plotter.ts +53 -6
- package/src/routing/query.ts +116 -13
- package/src/routing/rangeResult.ts +292 -0
- package/src/routing/rangeRouter.ts +167 -0
- package/src/routing/rangeState.ts +150 -0
- package/src/routing/raptor.ts +416 -0
- package/src/routing/result.ts +68 -26
- package/src/routing/route.ts +15 -53
- package/src/routing/router.ts +40 -480
- package/src/routing/state.ts +191 -32
- package/src/timetable/__tests__/timetable.test.ts +373 -0
- package/src/timetable/route.ts +16 -4
- 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
|
+
}
|