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,416 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
2
|
+
import { StopId } from '../stops/stops.js';
|
|
3
|
+
import {
|
|
4
|
+
NOT_AVAILABLE,
|
|
5
|
+
Route,
|
|
6
|
+
StopRouteIndex,
|
|
7
|
+
TripRouteIndex,
|
|
8
|
+
} from '../timetable/route.js';
|
|
9
|
+
import { Duration, DURATION_ZERO, Time } from '../timetable/time.js';
|
|
10
|
+
import { Timetable, TripStop } from '../timetable/timetable.js';
|
|
11
|
+
import { QueryOptions } from './query.js';
|
|
12
|
+
import { RoutingEdge, TransferEdge, VehicleEdge } from './state.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Common interface for all variants of RAPTOR routing.
|
|
16
|
+
*/
|
|
17
|
+
export interface IRaptorState {
|
|
18
|
+
/** Origin stop IDs for this run. */
|
|
19
|
+
readonly origins: StopId[];
|
|
20
|
+
|
|
21
|
+
/** Per-round routing graph; `graph[round][stop]` is the best edge used to reach `stop`. */
|
|
22
|
+
readonly graph: (RoutingEdge | undefined)[][];
|
|
23
|
+
|
|
24
|
+
/** Per-run earliest arrival at a stop. Used for boarding decisions. */
|
|
25
|
+
arrivalTime(stop: StopId): Time;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Tightest known upper bound on the arrival time at `stop` in `round`.
|
|
29
|
+
*/
|
|
30
|
+
improvementBound(round: number, stop: StopId): Time;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Best known arrival time at any destination.
|
|
34
|
+
*/
|
|
35
|
+
readonly destinationBest: Time;
|
|
36
|
+
|
|
37
|
+
/** Returns `true` if `stop` is one of the query's destination stops. */
|
|
38
|
+
isDestination(stop: StopId): boolean;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Records a new arrival at `stop`, updating all relevant state.
|
|
42
|
+
*
|
|
43
|
+
* In Range RAPTOR mode this also updates the cross-run shared labels.
|
|
44
|
+
*/
|
|
45
|
+
updateArrival(stop: StopId, time: Time, round: number): void;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Propagates labels from round `k-1` into round `k` before routes are scanned.
|
|
49
|
+
* No-op in standard RAPTOR mode.
|
|
50
|
+
*/
|
|
51
|
+
initRound(round: number): void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type TripContinuation = TripStop & {
|
|
55
|
+
previousEdge: VehicleEdge;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
type Round = number;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Encapsulates the core RAPTOR algorithm, operating on a {@link Timetable} and
|
|
62
|
+
* an {@link IRaptorState} provided by the caller.
|
|
63
|
+
*
|
|
64
|
+
* @see https://www.microsoft.com/en-us/research/wp-content/uploads/2012/01/raptor_alenex.pdf
|
|
65
|
+
*/
|
|
66
|
+
export class Raptor {
|
|
67
|
+
private readonly timetable: Timetable;
|
|
68
|
+
|
|
69
|
+
constructor(timetable: Timetable) {
|
|
70
|
+
this.timetable = timetable;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
run(options: QueryOptions, state: IRaptorState): void {
|
|
74
|
+
const markedStops = new Set<StopId>(state.origins);
|
|
75
|
+
|
|
76
|
+
for (let round = 1; round <= options.maxTransfers + 1; round++) {
|
|
77
|
+
state.initRound(round);
|
|
78
|
+
|
|
79
|
+
const edgesAtCurrentRound = state.graph[round]!;
|
|
80
|
+
const reachableRoutes = this.timetable.findReachableRoutes(
|
|
81
|
+
markedStops,
|
|
82
|
+
options.transportModes,
|
|
83
|
+
);
|
|
84
|
+
markedStops.clear();
|
|
85
|
+
|
|
86
|
+
for (const [route, hopOnStopIndex] of reachableRoutes) {
|
|
87
|
+
for (const stop of this.scanRoute(
|
|
88
|
+
route,
|
|
89
|
+
hopOnStopIndex,
|
|
90
|
+
round,
|
|
91
|
+
state,
|
|
92
|
+
options,
|
|
93
|
+
)) {
|
|
94
|
+
markedStops.add(stop);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let continuations = this.findTripContinuations(
|
|
99
|
+
markedStops,
|
|
100
|
+
edgesAtCurrentRound,
|
|
101
|
+
);
|
|
102
|
+
const stopsFromContinuations = new Set<StopId>();
|
|
103
|
+
while (continuations.length > 0) {
|
|
104
|
+
stopsFromContinuations.clear();
|
|
105
|
+
for (const continuation of continuations) {
|
|
106
|
+
const route = this.timetable.getRoute(continuation.routeId)!;
|
|
107
|
+
for (const stop of this.scanRouteContinuation(
|
|
108
|
+
route,
|
|
109
|
+
continuation.stopIndex,
|
|
110
|
+
round,
|
|
111
|
+
state,
|
|
112
|
+
continuation,
|
|
113
|
+
)) {
|
|
114
|
+
stopsFromContinuations.add(stop);
|
|
115
|
+
markedStops.add(stop);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
continuations = this.findTripContinuations(
|
|
119
|
+
stopsFromContinuations,
|
|
120
|
+
edgesAtCurrentRound,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for (const stop of this.considerTransfers(
|
|
125
|
+
options,
|
|
126
|
+
round,
|
|
127
|
+
markedStops,
|
|
128
|
+
state,
|
|
129
|
+
)) {
|
|
130
|
+
markedStops.add(stop);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (markedStops.size === 0) break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Finds trip continuations for the given marked stops and edges at the current round.
|
|
139
|
+
* @param markedStops The set of marked stops.
|
|
140
|
+
* @param edgesAtCurrentRound The array of edges at the current round, indexed by stop ID.
|
|
141
|
+
* @returns An array of trip continuations.
|
|
142
|
+
*/
|
|
143
|
+
private findTripContinuations(
|
|
144
|
+
markedStops: Set<StopId>,
|
|
145
|
+
edgesAtCurrentRound: (RoutingEdge | undefined)[],
|
|
146
|
+
): TripContinuation[] {
|
|
147
|
+
const continuations: TripContinuation[] = [];
|
|
148
|
+
for (const stopId of markedStops) {
|
|
149
|
+
const arrival = edgesAtCurrentRound[stopId];
|
|
150
|
+
if (!arrival || !('routeId' in arrival)) continue;
|
|
151
|
+
|
|
152
|
+
const continuousTrips = this.timetable.getContinuousTrips(
|
|
153
|
+
arrival.hopOffStopIndex,
|
|
154
|
+
arrival.routeId,
|
|
155
|
+
arrival.tripIndex,
|
|
156
|
+
);
|
|
157
|
+
for (const trip of continuousTrips) {
|
|
158
|
+
continuations.push({
|
|
159
|
+
routeId: trip.routeId,
|
|
160
|
+
stopIndex: trip.stopIndex,
|
|
161
|
+
tripIndex: trip.tripIndex,
|
|
162
|
+
previousEdge: arrival,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return continuations;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Scans a route for an in-seat trip continuation.
|
|
171
|
+
*
|
|
172
|
+
* The boarded trip and entry stop are fixed, so there is no need to probe for
|
|
173
|
+
* earlier boardings.
|
|
174
|
+
*
|
|
175
|
+
* @param route The route to scan
|
|
176
|
+
* @param hopOnStopIndex The stop index where the continuation begins
|
|
177
|
+
* @param round The current RAPTOR round
|
|
178
|
+
* @param routingState Current routing state
|
|
179
|
+
* @param tripContinuation The in-seat continuation descriptor
|
|
180
|
+
* @param shared Optional shared state for Range RAPTOR mode
|
|
181
|
+
*/
|
|
182
|
+
private scanRouteContinuation(
|
|
183
|
+
route: Route,
|
|
184
|
+
hopOnStopIndex: StopRouteIndex,
|
|
185
|
+
round: Round,
|
|
186
|
+
state: IRaptorState,
|
|
187
|
+
tripContinuation: TripContinuation,
|
|
188
|
+
): Set<StopId> {
|
|
189
|
+
const newlyMarkedStops = new Set<StopId>();
|
|
190
|
+
const edgesAtCurrentRound = state.graph[round]!;
|
|
191
|
+
|
|
192
|
+
const nbStops = route.getNbStops();
|
|
193
|
+
const routeId = route.id;
|
|
194
|
+
const tripIndex = tripContinuation.tripIndex;
|
|
195
|
+
const tripStopOffset = route.tripStopOffset(tripIndex);
|
|
196
|
+
const previousEdge = tripContinuation.previousEdge;
|
|
197
|
+
|
|
198
|
+
for (
|
|
199
|
+
let currentStopIndex = hopOnStopIndex;
|
|
200
|
+
currentStopIndex < nbStops;
|
|
201
|
+
currentStopIndex++
|
|
202
|
+
) {
|
|
203
|
+
const currentStop: StopId = route.stops[currentStopIndex]!;
|
|
204
|
+
const arrivalTime = route.arrivalAtOffset(
|
|
205
|
+
currentStopIndex,
|
|
206
|
+
tripStopOffset,
|
|
207
|
+
);
|
|
208
|
+
const dropOffType = route.dropOffTypeAtOffset(
|
|
209
|
+
currentStopIndex,
|
|
210
|
+
tripStopOffset,
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
if (
|
|
214
|
+
dropOffType !== NOT_AVAILABLE &&
|
|
215
|
+
arrivalTime < state.improvementBound(round, currentStop) &&
|
|
216
|
+
arrivalTime < state.destinationBest
|
|
217
|
+
) {
|
|
218
|
+
edgesAtCurrentRound[currentStop] = {
|
|
219
|
+
routeId,
|
|
220
|
+
stopIndex: hopOnStopIndex,
|
|
221
|
+
tripIndex,
|
|
222
|
+
arrival: arrivalTime,
|
|
223
|
+
hopOffStopIndex: currentStopIndex,
|
|
224
|
+
continuationOf: previousEdge,
|
|
225
|
+
};
|
|
226
|
+
state.updateArrival(currentStop, arrivalTime, round);
|
|
227
|
+
newlyMarkedStops.add(currentStop);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return newlyMarkedStops;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Scans a route using the standard RAPTOR boarding logic.
|
|
235
|
+
*
|
|
236
|
+
* Iterates through all stops from the hop-on point, maintaining the current
|
|
237
|
+
* best trip and improving arrival times when possible. At each marked stop it
|
|
238
|
+
* also checks whether an earlier (or first) trip can be boarded, upgrading the
|
|
239
|
+
* active trip when one is found.
|
|
240
|
+
*
|
|
241
|
+
* @param route The route to scan
|
|
242
|
+
* @param hopOnStopIndex The stop index where passengers can first board
|
|
243
|
+
* @param round The current RAPTOR round
|
|
244
|
+
* @param state Current routing state
|
|
245
|
+
* @param options Query options (minTransferTime, etc.)
|
|
246
|
+
*/
|
|
247
|
+
private scanRoute(
|
|
248
|
+
route: Route,
|
|
249
|
+
hopOnStopIndex: StopRouteIndex,
|
|
250
|
+
round: Round,
|
|
251
|
+
state: IRaptorState,
|
|
252
|
+
options: QueryOptions,
|
|
253
|
+
): Set<StopId> {
|
|
254
|
+
const newlyMarkedStops = new Set<StopId>();
|
|
255
|
+
const edgesAtCurrentRound = state.graph[round]!;
|
|
256
|
+
const edgesAtPreviousRound = state.graph[round - 1]!;
|
|
257
|
+
|
|
258
|
+
const nbStops = route.getNbStops();
|
|
259
|
+
const routeId = route.id;
|
|
260
|
+
let activeTripIndex: TripRouteIndex | undefined;
|
|
261
|
+
let activeTripBoardStopIndex = hopOnStopIndex;
|
|
262
|
+
// tripStopOffset = activeTripIndex * nbStops, precomputed when the trip changes.
|
|
263
|
+
let activeTripStopOffset = 0;
|
|
264
|
+
|
|
265
|
+
for (
|
|
266
|
+
let currentStopIndex = hopOnStopIndex;
|
|
267
|
+
currentStopIndex < nbStops;
|
|
268
|
+
currentStopIndex++
|
|
269
|
+
) {
|
|
270
|
+
const currentStop: StopId = route.stops[currentStopIndex]!;
|
|
271
|
+
|
|
272
|
+
// If on a trip, check whether alighting here improves the global best.
|
|
273
|
+
if (activeTripIndex !== undefined) {
|
|
274
|
+
const arrivalTime = route.arrivalAtOffset(
|
|
275
|
+
currentStopIndex,
|
|
276
|
+
activeTripStopOffset,
|
|
277
|
+
);
|
|
278
|
+
const dropOffType = route.dropOffTypeAtOffset(
|
|
279
|
+
currentStopIndex,
|
|
280
|
+
activeTripStopOffset,
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
if (
|
|
284
|
+
dropOffType !== NOT_AVAILABLE &&
|
|
285
|
+
arrivalTime < state.improvementBound(round, currentStop) &&
|
|
286
|
+
arrivalTime < state.destinationBest
|
|
287
|
+
) {
|
|
288
|
+
edgesAtCurrentRound[currentStop] = {
|
|
289
|
+
routeId,
|
|
290
|
+
stopIndex: activeTripBoardStopIndex,
|
|
291
|
+
tripIndex: activeTripIndex,
|
|
292
|
+
arrival: arrivalTime,
|
|
293
|
+
hopOffStopIndex: currentStopIndex,
|
|
294
|
+
};
|
|
295
|
+
state.updateArrival(currentStop, arrivalTime, round);
|
|
296
|
+
newlyMarkedStops.add(currentStop);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Check whether we can board an earlier (or first) trip at this stop.
|
|
301
|
+
const previousEdge = edgesAtPreviousRound[currentStop];
|
|
302
|
+
const earliestArrivalOnPreviousRound = previousEdge?.arrival;
|
|
303
|
+
if (
|
|
304
|
+
earliestArrivalOnPreviousRound !== undefined &&
|
|
305
|
+
(activeTripIndex === undefined ||
|
|
306
|
+
earliestArrivalOnPreviousRound <=
|
|
307
|
+
route.departureAtOffset(currentStopIndex, activeTripStopOffset))
|
|
308
|
+
) {
|
|
309
|
+
const earliestTrip = route.findEarliestTrip(
|
|
310
|
+
currentStopIndex,
|
|
311
|
+
earliestArrivalOnPreviousRound,
|
|
312
|
+
activeTripIndex,
|
|
313
|
+
);
|
|
314
|
+
if (earliestTrip === undefined) {
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const fromTripStop =
|
|
319
|
+
previousEdge && 'routeId' in previousEdge
|
|
320
|
+
? {
|
|
321
|
+
stopIndex: previousEdge.hopOffStopIndex,
|
|
322
|
+
routeId: previousEdge.routeId,
|
|
323
|
+
tripIndex: previousEdge.tripIndex,
|
|
324
|
+
}
|
|
325
|
+
: undefined;
|
|
326
|
+
const firstBoardableTrip = this.timetable.findFirstBoardableTrip(
|
|
327
|
+
currentStopIndex,
|
|
328
|
+
route,
|
|
329
|
+
earliestTrip,
|
|
330
|
+
earliestArrivalOnPreviousRound,
|
|
331
|
+
activeTripIndex,
|
|
332
|
+
fromTripStop,
|
|
333
|
+
options.minTransferTime,
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
if (firstBoardableTrip !== undefined) {
|
|
337
|
+
// At round 1, enforce maxInitialWaitingTime: skip boarding if the
|
|
338
|
+
// traveler would have to wait longer than the allowed threshold at
|
|
339
|
+
// the first boarding stop.
|
|
340
|
+
const exceedsInitialWait =
|
|
341
|
+
round === 1 &&
|
|
342
|
+
options.maxInitialWaitingTime !== undefined &&
|
|
343
|
+
route.departureFrom(currentStopIndex, firstBoardableTrip) -
|
|
344
|
+
earliestArrivalOnPreviousRound >
|
|
345
|
+
options.maxInitialWaitingTime;
|
|
346
|
+
|
|
347
|
+
if (!exceedsInitialWait) {
|
|
348
|
+
activeTripIndex = firstBoardableTrip;
|
|
349
|
+
activeTripBoardStopIndex = currentStopIndex;
|
|
350
|
+
activeTripStopOffset = route.tripStopOffset(firstBoardableTrip);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return newlyMarkedStops;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Processes all currently marked stops to find available transfers
|
|
360
|
+
* and determines if using these transfers would result in earlier arrival times
|
|
361
|
+
* at destination stops. It handles different transfer types including in-seat
|
|
362
|
+
* transfers and walking transfers with appropriate minimum transfer times.
|
|
363
|
+
*
|
|
364
|
+
* @param options Query options (minTransferTime, etc.)
|
|
365
|
+
* @param round The current round number in the RAPTOR algorithm
|
|
366
|
+
* @param markedStops The set of currently marked stops
|
|
367
|
+
* @param state Current routing state
|
|
368
|
+
*/
|
|
369
|
+
private considerTransfers(
|
|
370
|
+
options: QueryOptions,
|
|
371
|
+
round: number,
|
|
372
|
+
markedStops: Set<StopId>,
|
|
373
|
+
state: IRaptorState,
|
|
374
|
+
): Set<StopId> {
|
|
375
|
+
const newlyMarkedStops = new Set<StopId>();
|
|
376
|
+
const arrivalsAtCurrentRound = state.graph[round]!;
|
|
377
|
+
for (const stop of markedStops) {
|
|
378
|
+
const currentArrival = arrivalsAtCurrentRound[stop];
|
|
379
|
+
// Skip transfers if the last leg was also a transfer
|
|
380
|
+
if (!currentArrival || 'type' in currentArrival) continue;
|
|
381
|
+
const transfers = this.timetable.getTransfers(stop);
|
|
382
|
+
for (const transfer of transfers) {
|
|
383
|
+
let transferTime: Duration;
|
|
384
|
+
if (transfer.minTransferTime) {
|
|
385
|
+
transferTime = transfer.minTransferTime;
|
|
386
|
+
} else if (transfer.type === 'IN_SEAT') {
|
|
387
|
+
transferTime = DURATION_ZERO;
|
|
388
|
+
} else {
|
|
389
|
+
transferTime = options.minTransferTime;
|
|
390
|
+
}
|
|
391
|
+
const arrivalAfterTransfer = currentArrival.arrival + transferTime;
|
|
392
|
+
|
|
393
|
+
if (
|
|
394
|
+
arrivalAfterTransfer <
|
|
395
|
+
state.improvementBound(round, transfer.destination) &&
|
|
396
|
+
arrivalAfterTransfer < state.destinationBest
|
|
397
|
+
) {
|
|
398
|
+
arrivalsAtCurrentRound[transfer.destination] = {
|
|
399
|
+
arrival: arrivalAfterTransfer,
|
|
400
|
+
from: stop,
|
|
401
|
+
to: transfer.destination,
|
|
402
|
+
minTransferTime: transferTime || undefined,
|
|
403
|
+
type: transfer.type,
|
|
404
|
+
} as TransferEdge;
|
|
405
|
+
state.updateArrival(
|
|
406
|
+
transfer.destination,
|
|
407
|
+
arrivalAfterTransfer,
|
|
408
|
+
round,
|
|
409
|
+
);
|
|
410
|
+
newlyMarkedStops.add(transfer.destination);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return newlyMarkedStops;
|
|
415
|
+
}
|
|
416
|
+
}
|
package/src/routing/result.ts
CHANGED
|
@@ -3,9 +3,9 @@ import { SourceStopId, StopId } from '../stops/stops.js';
|
|
|
3
3
|
import { StopsIndex } from '../stops/stopsIndex.js';
|
|
4
4
|
import { RawPickUpDropOffType } from '../timetable/route.js';
|
|
5
5
|
import { Time } from '../timetable/time.js';
|
|
6
|
-
import {
|
|
7
|
-
import { Leg, Route, Transfer, VehicleLeg } from './route.js';
|
|
6
|
+
import { Access, Leg, Route, Transfer, VehicleLeg } from './route.js';
|
|
8
7
|
import { Arrival, RoutingState, TransferEdge, VehicleEdge } from './router.js';
|
|
8
|
+
import { AccessEdge } from './state.js';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Details about the pickup and drop-off modalities at each stop in each trip of a route.
|
|
@@ -42,26 +42,49 @@ const toPickupDropOffType = (
|
|
|
42
42
|
};
|
|
43
43
|
|
|
44
44
|
export class Result {
|
|
45
|
-
private readonly
|
|
45
|
+
private readonly destinations: ReadonlySet<StopId>;
|
|
46
46
|
public readonly routingState: RoutingState;
|
|
47
47
|
public readonly stopsIndex: StopsIndex;
|
|
48
48
|
public readonly timetable: Timetable;
|
|
49
49
|
|
|
50
50
|
constructor(
|
|
51
|
-
|
|
51
|
+
destinations: ReadonlySet<StopId>,
|
|
52
52
|
routingState: RoutingState,
|
|
53
53
|
stopsIndex: StopsIndex,
|
|
54
54
|
timetable: Timetable,
|
|
55
55
|
) {
|
|
56
|
-
this.
|
|
56
|
+
this.destinations = destinations;
|
|
57
57
|
this.routingState = routingState;
|
|
58
58
|
this.stopsIndex = stopsIndex;
|
|
59
59
|
this.timetable = timetable;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Expands a target stop or stop set to all equivalent concrete stop IDs.
|
|
64
|
+
*
|
|
65
|
+
* When `to` is omitted, defaults to the resolved destinations stored on this
|
|
66
|
+
* result.
|
|
67
|
+
*
|
|
68
|
+
* Equivalent stops are expanded here so destination handling has a single
|
|
69
|
+
* source of truth shared by route reconstruction and arrival lookups.
|
|
70
|
+
*/
|
|
71
|
+
private expandDestinations(to?: StopId | Set<StopId>): Set<StopId> {
|
|
72
|
+
const targets: Iterable<StopId> =
|
|
73
|
+
to instanceof Set ? to : to !== undefined ? [to] : this.destinations;
|
|
74
|
+
|
|
75
|
+
const expanded = new Set<StopId>();
|
|
76
|
+
for (const target of targets) {
|
|
77
|
+
for (const equivalentStop of this.stopsIndex.equivalentStops(target)) {
|
|
78
|
+
expanded.add(equivalentStop.id);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return expanded;
|
|
82
|
+
}
|
|
83
|
+
|
|
62
84
|
/**
|
|
63
85
|
* Reconstructs the best route to a stop by SourceStopId.
|
|
64
|
-
* (to any stop reachable in less time / transfers than
|
|
86
|
+
* (to any stop reachable in less time / transfers than this result's
|
|
87
|
+
* destination set)
|
|
65
88
|
*
|
|
66
89
|
* @param to The destination stop by SourceStopId.
|
|
67
90
|
* @returns a route to the destination stop if it exists.
|
|
@@ -83,33 +106,30 @@ export class Result {
|
|
|
83
106
|
|
|
84
107
|
/**
|
|
85
108
|
* Reconstructs the best route to a stop.
|
|
86
|
-
* (to any stop reachable in less time / transfers than
|
|
109
|
+
* (to any stop reachable in less time / transfers than this result's
|
|
110
|
+
* destination set)
|
|
87
111
|
*
|
|
88
|
-
* @param to The destination stop. Defaults to
|
|
112
|
+
* @param to The destination stop. Defaults to this result's resolved
|
|
113
|
+
* destinations.
|
|
89
114
|
* @returns a route to the destination stop if it exists.
|
|
90
115
|
*/
|
|
91
116
|
bestRoute(to?: StopId | Set<StopId>): Route | undefined {
|
|
92
|
-
const
|
|
93
|
-
to instanceof Set ? to : to ? [to] : this.query.to;
|
|
117
|
+
const destinationStops = this.expandDestinations(to);
|
|
94
118
|
|
|
95
119
|
// Find the fastest-reached destination across all equivalent stops.
|
|
96
120
|
let fastestDestination: StopId | undefined = undefined;
|
|
97
121
|
let fastestArrivalTime: Time | undefined = undefined;
|
|
98
122
|
let fastestLegNumber: number | undefined = undefined;
|
|
99
|
-
for (const
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
fastestDestination = destination.id;
|
|
110
|
-
fastestArrivalTime = arrivalData.arrival;
|
|
111
|
-
fastestLegNumber = arrivalData.legNumber;
|
|
112
|
-
}
|
|
123
|
+
for (const destination of destinationStops) {
|
|
124
|
+
const arrivalData = this.routingState.getArrival(destination);
|
|
125
|
+
if (
|
|
126
|
+
arrivalData !== undefined &&
|
|
127
|
+
(fastestArrivalTime === undefined ||
|
|
128
|
+
arrivalData.arrival < fastestArrivalTime)
|
|
129
|
+
) {
|
|
130
|
+
fastestDestination = destination;
|
|
131
|
+
fastestArrivalTime = arrivalData.arrival;
|
|
132
|
+
fastestLegNumber = arrivalData.legNumber;
|
|
113
133
|
}
|
|
114
134
|
}
|
|
115
135
|
if (fastestDestination === undefined || fastestLegNumber === undefined) {
|
|
@@ -121,9 +141,11 @@ export class Result {
|
|
|
121
141
|
let currentStop = fastestDestination;
|
|
122
142
|
let round = fastestLegNumber;
|
|
123
143
|
let previousVehicleEdge: VehicleEdge | undefined;
|
|
124
|
-
|
|
144
|
+
|
|
145
|
+
while (round >= 0) {
|
|
125
146
|
const edge = this.routingState.graph[round]?.[currentStop];
|
|
126
147
|
if (!edge) {
|
|
148
|
+
if (round === 0) break;
|
|
127
149
|
throw new Error(
|
|
128
150
|
`No edge arriving at stop ${currentStop} at round ${round}`,
|
|
129
151
|
);
|
|
@@ -175,6 +197,9 @@ export class Result {
|
|
|
175
197
|
} else if ('type' in edge) {
|
|
176
198
|
leg = this.buildTransferLeg(edge);
|
|
177
199
|
previousVehicleEdge = undefined;
|
|
200
|
+
} else if ('duration' in edge) {
|
|
201
|
+
leg = this.buildAccessLeg(edge);
|
|
202
|
+
previousVehicleEdge = undefined;
|
|
178
203
|
} else {
|
|
179
204
|
break;
|
|
180
205
|
}
|
|
@@ -250,6 +275,22 @@ export class Result {
|
|
|
250
275
|
};
|
|
251
276
|
}
|
|
252
277
|
|
|
278
|
+
/**
|
|
279
|
+
* Builds a transfer leg from a transfer edge.
|
|
280
|
+
*
|
|
281
|
+
* @param edge Transfer edge representing a walking connection between stops
|
|
282
|
+
* @returns A transfer leg with from/to stops and transfer details
|
|
283
|
+
*/
|
|
284
|
+
private buildAccessLeg(edge: AccessEdge): Access {
|
|
285
|
+
return {
|
|
286
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
287
|
+
from: this.stopsIndex.findStopById(edge.from)!,
|
|
288
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
289
|
+
to: this.stopsIndex.findStopById(edge.to)!,
|
|
290
|
+
duration: edge.duration,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
253
294
|
/**
|
|
254
295
|
* Builds a guaranteed transfer leg between two consecutive vehicle legs.
|
|
255
296
|
*
|
|
@@ -278,7 +319,8 @@ export class Result {
|
|
|
278
319
|
}
|
|
279
320
|
|
|
280
321
|
/**
|
|
281
|
-
* Returns the arrival time at any stop reachable in less time / transfers
|
|
322
|
+
* Returns the arrival time at any stop reachable in less time / transfers
|
|
323
|
+
* than this result's destination set.
|
|
282
324
|
*
|
|
283
325
|
* @param stop The target stop for which to return the arrival time.
|
|
284
326
|
* @param maxTransfers The optional maximum number of transfers allowed.
|
package/src/routing/route.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Stop
|
|
1
|
+
import { Stop } from '../stops/stops.js';
|
|
2
2
|
import {
|
|
3
3
|
Duration,
|
|
4
4
|
DURATION_ZERO,
|
|
@@ -9,21 +9,6 @@ import {
|
|
|
9
9
|
} from '../timetable/time.js';
|
|
10
10
|
import { ServiceRouteInfo, TransferType } from '../timetable/timetable.js';
|
|
11
11
|
|
|
12
|
-
export type JsonLeg = {
|
|
13
|
-
from: StopId;
|
|
14
|
-
to: StopId;
|
|
15
|
-
} & (
|
|
16
|
-
| {
|
|
17
|
-
departure: string;
|
|
18
|
-
arrival: string;
|
|
19
|
-
route: ServiceRouteInfo;
|
|
20
|
-
}
|
|
21
|
-
| {
|
|
22
|
-
type: TransferType;
|
|
23
|
-
minTransferTime?: number;
|
|
24
|
-
}
|
|
25
|
-
);
|
|
26
|
-
|
|
27
12
|
export type PickUpDropOffType =
|
|
28
13
|
| 'REGULAR'
|
|
29
14
|
| 'NOT_AVAILABLE'
|
|
@@ -35,6 +20,10 @@ export type BaseLeg = {
|
|
|
35
20
|
to: Stop;
|
|
36
21
|
};
|
|
37
22
|
|
|
23
|
+
export type Access = BaseLeg & {
|
|
24
|
+
duration: Duration;
|
|
25
|
+
};
|
|
26
|
+
|
|
38
27
|
export type Transfer = BaseLeg & {
|
|
39
28
|
minTransferTime?: Duration;
|
|
40
29
|
type: TransferType;
|
|
@@ -48,7 +37,7 @@ export type VehicleLeg = BaseLeg & {
|
|
|
48
37
|
dropOffType: PickUpDropOffType;
|
|
49
38
|
};
|
|
50
39
|
|
|
51
|
-
export type Leg = Transfer | VehicleLeg;
|
|
40
|
+
export type Leg = Transfer | Access | VehicleLeg;
|
|
52
41
|
|
|
53
42
|
/**
|
|
54
43
|
* Represents a resolved route consisting of multiple legs,
|
|
@@ -68,15 +57,15 @@ export class Route {
|
|
|
68
57
|
* @throws If no vehicle leg is found in the route.
|
|
69
58
|
*/
|
|
70
59
|
departureTime(): Time {
|
|
71
|
-
let
|
|
60
|
+
let cumulativeAccessTime: Duration = DURATION_ZERO;
|
|
72
61
|
for (let i = 0; i < this.legs.length; i++) {
|
|
73
62
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
74
63
|
const leg = this.legs[i]!;
|
|
75
64
|
if ('departureTime' in leg) {
|
|
76
|
-
return leg.departureTime -
|
|
65
|
+
return leg.departureTime - cumulativeAccessTime;
|
|
77
66
|
}
|
|
78
|
-
if ('
|
|
79
|
-
|
|
67
|
+
if ('duration' in leg && leg.duration) {
|
|
68
|
+
cumulativeAccessTime += leg.duration;
|
|
80
69
|
}
|
|
81
70
|
}
|
|
82
71
|
throw new Error('No vehicle leg found in route');
|
|
@@ -132,6 +121,10 @@ export class Route {
|
|
|
132
121
|
'type' in leg && !('route' in leg)
|
|
133
122
|
? `Transfer: ${leg.type}${leg.minTransferTime ? `, Minimum Transfer Time: ${durationToString(leg.minTransferTime)}` : ''}`
|
|
134
123
|
: '';
|
|
124
|
+
const accessDetails =
|
|
125
|
+
'duration' in leg
|
|
126
|
+
? `Access duration: ${durationToString(leg.duration)}`
|
|
127
|
+
: '';
|
|
135
128
|
const travelDetails =
|
|
136
129
|
'route' in leg && 'departureTime' in leg && 'arrivalTime' in leg
|
|
137
130
|
? `Route: ${leg.route.type} ${leg.route.name}, Departure: ${timeToString(leg.departureTime)}, Arrival: ${timeToString(leg.arrivalTime)}`
|
|
@@ -142,6 +135,7 @@ export class Route {
|
|
|
142
135
|
` ${fromStop}`,
|
|
143
136
|
` ${toStop}`,
|
|
144
137
|
transferDetails ? ` ${transferDetails}` : '',
|
|
138
|
+
accessDetails ? ` ${accessDetails}` : '',
|
|
145
139
|
travelDetails ? ` ${travelDetails}` : '',
|
|
146
140
|
]
|
|
147
141
|
.filter((line) => line.trim() !== '')
|
|
@@ -149,36 +143,4 @@ export class Route {
|
|
|
149
143
|
})
|
|
150
144
|
.join('\n');
|
|
151
145
|
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Generates a concise JSON representation of the route.
|
|
155
|
-
* This is particularly useful for generating regression tests
|
|
156
|
-
* to verify the correctness of route calculations.
|
|
157
|
-
*
|
|
158
|
-
* @returns A JSON representation of the route.
|
|
159
|
-
*/
|
|
160
|
-
asJson(): JsonLeg[] {
|
|
161
|
-
const jsonLegs: JsonLeg[] = this.legs.map((leg: Leg) => {
|
|
162
|
-
if ('route' in leg) {
|
|
163
|
-
return {
|
|
164
|
-
from: leg.from.id,
|
|
165
|
-
to: leg.to.id,
|
|
166
|
-
departure: timeToString(leg.departureTime),
|
|
167
|
-
arrival: timeToString(leg.arrivalTime),
|
|
168
|
-
route: leg.route,
|
|
169
|
-
};
|
|
170
|
-
} else {
|
|
171
|
-
return {
|
|
172
|
-
from: leg.from.id,
|
|
173
|
-
to: leg.to.id,
|
|
174
|
-
type: leg.type,
|
|
175
|
-
...(leg.minTransferTime !== undefined && {
|
|
176
|
-
minTransferTime: leg.minTransferTime,
|
|
177
|
-
}),
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
return jsonLegs;
|
|
183
|
-
}
|
|
184
146
|
}
|