minotor 11.2.0 → 11.2.2
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/CHANGELOG.md +3 -3
- package/dist/cli.mjs +54 -21
- package/dist/cli.mjs.map +1 -1
- package/dist/router.cjs.js +1 -1
- package/dist/router.cjs.js.map +1 -1
- 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/rangeResult.d.ts +21 -1
- package/dist/routing/rangeState.d.ts +1 -1
- package/package.json +1 -1
- package/src/routing/__tests__/rangeResult.test.ts +86 -1
- package/src/routing/__tests__/rangeRouter.test.ts +129 -0
- package/src/routing/__tests__/rangeState.test.ts +88 -0
- package/src/routing/rangeResult.ts +21 -1
- package/src/routing/rangeRouter.ts +27 -19
- package/src/routing/rangeState.ts +12 -2
|
@@ -28,7 +28,8 @@ export type ArrivalWithDuration = Arrival & {
|
|
|
28
28
|
* The result of a Range RAPTOR query.
|
|
29
29
|
*
|
|
30
30
|
* Contains the complete Pareto-optimal set of journeys for a resolved
|
|
31
|
-
* destination set
|
|
31
|
+
* destination set, **or** the full per-departure-time routing state when no
|
|
32
|
+
* destinations were provided (full-network / isochrone mode).
|
|
32
33
|
*
|
|
33
34
|
* **Pareto dominance**: journey J1 dominates J2 iff
|
|
34
35
|
* `τdep(J1) ≥ τdep(J2) AND τarr(J1) ≤ τarr(J2)`
|
|
@@ -38,6 +39,15 @@ export type ArrivalWithDuration = Arrival & {
|
|
|
38
39
|
* strictly earlier *and* arrives strictly earlier than the previous one,
|
|
39
40
|
* forming the classic staircase Pareto frontier.
|
|
40
41
|
*
|
|
42
|
+
* **Full-network mode** (empty `destinations`): when no destinations are
|
|
43
|
+
* supplied to the range query every departure slot in the window becomes its
|
|
44
|
+
* own run, because destination-based Pareto pruning cannot be applied.
|
|
45
|
+
* In this mode the destination-specific helpers ({@link getRoutes},
|
|
46
|
+
* {@link bestRoute}, {@link latestDepartureRoute}, {@link fastestRoute})
|
|
47
|
+
* return empty results; use {@link allEarliestArrivals},
|
|
48
|
+
* {@link allShortestDurations}, {@link earliestArrivalAt}, or
|
|
49
|
+
* {@link shortestDurationTo} instead.
|
|
50
|
+
*
|
|
41
51
|
* Destination handling is delegated to {@link Result}, which expands
|
|
42
52
|
* equivalent stops when reconstructing routes or looking up arrivals.
|
|
43
53
|
*/
|
|
@@ -54,6 +64,10 @@ export declare class RangeResult {
|
|
|
54
64
|
*
|
|
55
65
|
* Each route in the list departs strictly earlier *and* arrives strictly
|
|
56
66
|
* earlier than its predecessor.
|
|
67
|
+
*
|
|
68
|
+
* Returns an empty array when no destinations were provided (full-network
|
|
69
|
+
* mode). Use {@link allEarliestArrivals} or {@link allShortestDurations}
|
|
70
|
+
* to query individual stops in that case.
|
|
57
71
|
*/
|
|
58
72
|
getRoutes(): Route[];
|
|
59
73
|
/**
|
|
@@ -65,6 +79,8 @@ export declare class RangeResult {
|
|
|
65
79
|
* at a transit stop.
|
|
66
80
|
*
|
|
67
81
|
* Defaults to this result's own destination stop(s) when `to` is omitted.
|
|
82
|
+
* Always pass an explicit `to` stop when operating in full-network mode
|
|
83
|
+
* (no destinations), otherwise `undefined` is returned.
|
|
68
84
|
*
|
|
69
85
|
* @param to Optional destination stop ID or set of stop IDs.
|
|
70
86
|
* @returns The reconstructed {@link Route} with the earliest arrival,
|
|
@@ -81,6 +97,8 @@ export declare class RangeResult {
|
|
|
81
97
|
* {@link fastestRoute}.
|
|
82
98
|
*
|
|
83
99
|
* Defaults to this result's own destination stop(s) when `to` is omitted.
|
|
100
|
+
* Always pass an explicit `to` stop when operating in full-network mode
|
|
101
|
+
* (no destinations), otherwise `undefined` is returned.
|
|
84
102
|
*
|
|
85
103
|
* @param to Optional destination stop ID or set of stop IDs.
|
|
86
104
|
* @returns The reconstructed {@link Route} with the latest departure,
|
|
@@ -97,6 +115,8 @@ export declare class RangeResult {
|
|
|
97
115
|
* spent traveling.
|
|
98
116
|
*
|
|
99
117
|
* Defaults to this result's own destination stop(s) when `to` is omitted.
|
|
118
|
+
* Always pass an explicit `to` stop when operating in full-network mode
|
|
119
|
+
* (no destinations), otherwise `undefined` is returned.
|
|
100
120
|
*
|
|
101
121
|
* @param to Optional destination stop ID or set of stop IDs.
|
|
102
122
|
* @returns The reconstructed fastest {@link Route}, or `undefined` if the
|
|
@@ -69,7 +69,7 @@ export declare class RangeRaptorState implements IRaptorState {
|
|
|
69
69
|
*/
|
|
70
70
|
get destinationBest(): Time;
|
|
71
71
|
isDestination(stop: StopId): boolean;
|
|
72
|
-
/** Updates
|
|
72
|
+
/** Updates the per-run aggregate best when improved, and always considers the cross-run shared label. */
|
|
73
73
|
updateArrival(stop: StopId, time: Time, round: number): void;
|
|
74
74
|
/**
|
|
75
75
|
* initialized round `k` from round `k-1`: τk(p) ← min(τk(p), τk-1(p)).
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@ import assert from 'node:assert';
|
|
|
2
2
|
import { describe, it } from 'node:test';
|
|
3
3
|
|
|
4
4
|
import { Timetable } from '../../router.js';
|
|
5
|
-
import { Stop } from '../../stops/stops.js';
|
|
5
|
+
import { Stop, StopId } from '../../stops/stops.js';
|
|
6
6
|
import { StopsIndex } from '../../stops/stopsIndex.js';
|
|
7
7
|
import { Route } from '../../timetable/route.js';
|
|
8
8
|
import { timeFromHM } from '../../timetable/time.js';
|
|
@@ -270,4 +270,89 @@ describe('RangeResult', () => {
|
|
|
270
270
|
assert.deepStrictEqual(empty.allEarliestArrivals(), new Map());
|
|
271
271
|
});
|
|
272
272
|
});
|
|
273
|
+
|
|
274
|
+
describe('no destinations (full-network / isochrone mode)', () => {
|
|
275
|
+
// Same routing data as runA/runB but Result is built with empty destinations,
|
|
276
|
+
// reflecting how RangeRouter constructs results in full-network mode.
|
|
277
|
+
const emptyDests = new Set<StopId>();
|
|
278
|
+
|
|
279
|
+
const runAFull: ParetoRun = {
|
|
280
|
+
departureTime: timeFromHM(9, 0),
|
|
281
|
+
result: new Result(
|
|
282
|
+
emptyDests,
|
|
283
|
+
RoutingState.fromTestData({
|
|
284
|
+
nbStops: 2,
|
|
285
|
+
origins: [0],
|
|
286
|
+
destinations: [],
|
|
287
|
+
arrivals: [
|
|
288
|
+
[0, timeFromHM(9, 0), 0],
|
|
289
|
+
[DEST, timeFromHM(9, 30), 1],
|
|
290
|
+
],
|
|
291
|
+
graph: [
|
|
292
|
+
[[0, { stopId: 0, arrival: timeFromHM(9, 0) }]],
|
|
293
|
+
[[DEST, edgeA]],
|
|
294
|
+
],
|
|
295
|
+
}),
|
|
296
|
+
stopsIndex,
|
|
297
|
+
timetable,
|
|
298
|
+
),
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const runBFull: ParetoRun = {
|
|
302
|
+
departureTime: timeFromHM(8, 30),
|
|
303
|
+
result: new Result(
|
|
304
|
+
emptyDests,
|
|
305
|
+
RoutingState.fromTestData({
|
|
306
|
+
nbStops: 2,
|
|
307
|
+
origins: [0],
|
|
308
|
+
destinations: [],
|
|
309
|
+
arrivals: [
|
|
310
|
+
[0, timeFromHM(8, 30), 0],
|
|
311
|
+
[DEST, timeFromHM(9, 10), 1],
|
|
312
|
+
],
|
|
313
|
+
graph: [
|
|
314
|
+
[[0, { stopId: 0, arrival: timeFromHM(8, 30) }]],
|
|
315
|
+
[[DEST, edgeB]],
|
|
316
|
+
],
|
|
317
|
+
}),
|
|
318
|
+
stopsIndex,
|
|
319
|
+
timetable,
|
|
320
|
+
),
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const fullNetworkResult = new RangeResult([runAFull, runBFull], emptyDests);
|
|
324
|
+
|
|
325
|
+
it('destinations is empty', () => {
|
|
326
|
+
assert.strictEqual(fullNetworkResult.destinations.size, 0);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('getRoutes returns an empty array', () => {
|
|
330
|
+
// Result was built with empty destinations, so bestRoute() on each inner
|
|
331
|
+
// Result finds no target and getRoutes() collapses to [].
|
|
332
|
+
assert.deepStrictEqual(fullNetworkResult.getRoutes(), []);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('bestRoute without an explicit stop returns undefined', () => {
|
|
336
|
+
assert.strictEqual(fullNetworkResult.bestRoute(), undefined);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('bestRoute with an explicit stop returns the correct route', () => {
|
|
340
|
+
const route = fullNetworkResult.bestRoute(DEST);
|
|
341
|
+
assert(route);
|
|
342
|
+
// Best route to DEST is via runB (earlier arrival at 09:10).
|
|
343
|
+
assert.strictEqual(route.arrivalTime(), timeFromHM(9, 10));
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('allEarliestArrivals covers all reachable stops', () => {
|
|
347
|
+
const arrivals = fullNetworkResult.allEarliestArrivals();
|
|
348
|
+
// DEST earliest arrival is 09:10 (from runB).
|
|
349
|
+
assert.strictEqual(arrivals.get(DEST)?.arrival, timeFromHM(9, 10));
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('allShortestDurations covers all reachable stops', () => {
|
|
353
|
+
const durations = fullNetworkResult.allShortestDurations();
|
|
354
|
+
// runA: 30 min, runB: 40 min → shortest duration to DEST is 30 min.
|
|
355
|
+
assert.strictEqual(durations.get(DEST)?.duration, 30);
|
|
356
|
+
});
|
|
357
|
+
});
|
|
273
358
|
});
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from '../../timetable/timetable.js';
|
|
13
13
|
import { AccessFinder } from '../access.js';
|
|
14
14
|
import { RangeQuery } from '../query.js';
|
|
15
|
+
import { RangeResult } from '../rangeResult.js';
|
|
15
16
|
import { RangeRouter } from '../rangeRouter.js';
|
|
16
17
|
import { Raptor } from '../raptor.js';
|
|
17
18
|
|
|
@@ -469,4 +470,132 @@ describe('RangeRouter', () => {
|
|
|
469
470
|
assert.strictEqual(run.departureTime, timeFromHM(8, 30));
|
|
470
471
|
});
|
|
471
472
|
});
|
|
473
|
+
|
|
474
|
+
describe('with no destinations (full-network / isochrone mode)', () => {
|
|
475
|
+
// Two-stop network, two trips:
|
|
476
|
+
// trip 0: stop 0 departs 08:00 → stop 1 arrives 08:30 (30-min journey)
|
|
477
|
+
// trip 1: stop 0 departs 08:30 → stop 1 arrives 09:00 (30-min journey)
|
|
478
|
+
// The query window covers both departure slots; no destination is specified.
|
|
479
|
+
let result: RangeResult;
|
|
480
|
+
|
|
481
|
+
beforeEach(() => {
|
|
482
|
+
const stopsAdjacency: StopAdjacency[] = [
|
|
483
|
+
{ routes: [0] },
|
|
484
|
+
{ routes: [0] },
|
|
485
|
+
];
|
|
486
|
+
|
|
487
|
+
const routesAdjacency = [
|
|
488
|
+
Route.of({
|
|
489
|
+
id: 0,
|
|
490
|
+
serviceRouteId: 0,
|
|
491
|
+
trips: [
|
|
492
|
+
{
|
|
493
|
+
stops: [
|
|
494
|
+
{
|
|
495
|
+
id: 0,
|
|
496
|
+
arrivalTime: timeFromHM(8, 0),
|
|
497
|
+
departureTime: timeFromHM(8, 0),
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
id: 1,
|
|
501
|
+
arrivalTime: timeFromHM(8, 30),
|
|
502
|
+
departureTime: timeFromHM(8, 30),
|
|
503
|
+
},
|
|
504
|
+
],
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
stops: [
|
|
508
|
+
{
|
|
509
|
+
id: 0,
|
|
510
|
+
arrivalTime: timeFromHM(8, 30),
|
|
511
|
+
departureTime: timeFromHM(8, 30),
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
id: 1,
|
|
515
|
+
arrivalTime: timeFromHM(9, 0),
|
|
516
|
+
departureTime: timeFromHM(9, 0),
|
|
517
|
+
},
|
|
518
|
+
],
|
|
519
|
+
},
|
|
520
|
+
],
|
|
521
|
+
}),
|
|
522
|
+
];
|
|
523
|
+
|
|
524
|
+
const serviceRoutes: ServiceRoute[] = [
|
|
525
|
+
{ type: 'BUS', name: 'Line 1', routes: [0] },
|
|
526
|
+
];
|
|
527
|
+
|
|
528
|
+
const timetable = new Timetable(
|
|
529
|
+
stopsAdjacency,
|
|
530
|
+
routesAdjacency,
|
|
531
|
+
serviceRoutes,
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
const stops: Stop[] = [
|
|
535
|
+
{
|
|
536
|
+
id: 0,
|
|
537
|
+
sourceStopId: 'origin',
|
|
538
|
+
name: 'Origin',
|
|
539
|
+
lat: 0,
|
|
540
|
+
lon: 0,
|
|
541
|
+
children: [],
|
|
542
|
+
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
id: 1,
|
|
546
|
+
sourceStopId: 'dest',
|
|
547
|
+
name: 'Destination',
|
|
548
|
+
lat: 0,
|
|
549
|
+
lon: 0,
|
|
550
|
+
children: [],
|
|
551
|
+
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
552
|
+
},
|
|
553
|
+
];
|
|
554
|
+
|
|
555
|
+
const stopsIndex = new StopsIndex(stops);
|
|
556
|
+
const accessFinder = new AccessFinder(timetable, stopsIndex);
|
|
557
|
+
const raptor = new Raptor(timetable);
|
|
558
|
+
const router = new RangeRouter(
|
|
559
|
+
timetable,
|
|
560
|
+
stopsIndex,
|
|
561
|
+
accessFinder,
|
|
562
|
+
raptor,
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
// Omitting .to() leaves toValue as its default empty Set.
|
|
566
|
+
const query = new RangeQuery.Builder()
|
|
567
|
+
.from(0)
|
|
568
|
+
.departureTime(timeFromHM(8, 0))
|
|
569
|
+
.lastDepartureTime(timeFromHM(8, 30))
|
|
570
|
+
.build();
|
|
571
|
+
|
|
572
|
+
result = router.rangeRoute(query);
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
it('returns a run for every departure slot in the window', () => {
|
|
576
|
+
// Without destinations the trivialDestCovered guard (0 === 0) previously
|
|
577
|
+
// aborted the loop before any run was processed; now both slots are kept.
|
|
578
|
+
assert.strictEqual(result.size, 2);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
it('allEarliestArrivals covers all reachable stops', () => {
|
|
582
|
+
const arrivals = result.allEarliestArrivals();
|
|
583
|
+
// Stop 1 is first reached by trip 0 (depart 08:00, arrive 08:30).
|
|
584
|
+
assert.strictEqual(arrivals.get(1)?.arrival, timeFromHM(8, 30));
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it('allShortestDurations covers all reachable stops', () => {
|
|
588
|
+
const durations = result.allShortestDurations();
|
|
589
|
+
// Both trips take 30 min; shortest duration to stop 1 is 30 min.
|
|
590
|
+
assert.strictEqual(durations.get(1)?.duration, 30);
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
it('bestRoute without an explicit stop returns undefined', () => {
|
|
594
|
+
assert.strictEqual(result.bestRoute(), undefined);
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it('getRoutes returns an empty array', () => {
|
|
598
|
+
assert.deepStrictEqual(result.getRoutes(), []);
|
|
599
|
+
});
|
|
600
|
+
});
|
|
472
601
|
});
|
|
@@ -229,6 +229,94 @@ describe('RangeRaptorState', () => {
|
|
|
229
229
|
});
|
|
230
230
|
});
|
|
231
231
|
|
|
232
|
+
describe('updateArrival aggregate overwrite behavior', () => {
|
|
233
|
+
it('does not overwrite the current run aggregate with a later arrival from a higher round', () => {
|
|
234
|
+
const state = new RangeRaptorState(
|
|
235
|
+
MAX_ROUNDS,
|
|
236
|
+
NB_STOPS,
|
|
237
|
+
timeFromHM(12, 0),
|
|
238
|
+
);
|
|
239
|
+
const run = RoutingState.fromTestData({
|
|
240
|
+
nbStops: NB_STOPS,
|
|
241
|
+
destinations: [2],
|
|
242
|
+
arrivals: [[2, timeFromHM(8, 30), 1]],
|
|
243
|
+
});
|
|
244
|
+
state.setCurrentRun(run);
|
|
245
|
+
|
|
246
|
+
state.updateArrival(2, timeFromHM(8, 45), 2);
|
|
247
|
+
|
|
248
|
+
assert.deepStrictEqual(run.getArrival(2), {
|
|
249
|
+
arrival: timeFromHM(8, 30),
|
|
250
|
+
legNumber: 1,
|
|
251
|
+
});
|
|
252
|
+
assert.strictEqual(state.roundLabels[2]![2], timeFromHM(8, 45));
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('does not overwrite the current run aggregate with an equal-time arrival using more legs', () => {
|
|
256
|
+
const state = new RangeRaptorState(
|
|
257
|
+
MAX_ROUNDS,
|
|
258
|
+
NB_STOPS,
|
|
259
|
+
timeFromHM(12, 0),
|
|
260
|
+
);
|
|
261
|
+
const run = RoutingState.fromTestData({
|
|
262
|
+
nbStops: NB_STOPS,
|
|
263
|
+
destinations: [2],
|
|
264
|
+
arrivals: [[2, timeFromHM(8, 30), 2]],
|
|
265
|
+
});
|
|
266
|
+
state.setCurrentRun(run);
|
|
267
|
+
|
|
268
|
+
state.updateArrival(2, timeFromHM(8, 30), 3);
|
|
269
|
+
|
|
270
|
+
assert.deepStrictEqual(run.getArrival(2), {
|
|
271
|
+
arrival: timeFromHM(8, 30),
|
|
272
|
+
legNumber: 2,
|
|
273
|
+
});
|
|
274
|
+
assert.strictEqual(state.roundLabels[3]![2], timeFromHM(8, 30));
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('prefers fewer legs for the current run aggregate when arrival time is equal', () => {
|
|
278
|
+
const state = new RangeRaptorState(
|
|
279
|
+
MAX_ROUNDS,
|
|
280
|
+
NB_STOPS,
|
|
281
|
+
timeFromHM(12, 0),
|
|
282
|
+
);
|
|
283
|
+
const run = RoutingState.fromTestData({
|
|
284
|
+
nbStops: NB_STOPS,
|
|
285
|
+
destinations: [2],
|
|
286
|
+
arrivals: [[2, timeFromHM(8, 30), 3]],
|
|
287
|
+
});
|
|
288
|
+
state.setCurrentRun(run);
|
|
289
|
+
|
|
290
|
+
state.updateArrival(2, timeFromHM(8, 30), 2);
|
|
291
|
+
|
|
292
|
+
assert.deepStrictEqual(run.getArrival(2), {
|
|
293
|
+
arrival: timeFromHM(8, 30),
|
|
294
|
+
legNumber: 2,
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('still updates the current run aggregate when the new arrival is earlier', () => {
|
|
299
|
+
const state = new RangeRaptorState(
|
|
300
|
+
MAX_ROUNDS,
|
|
301
|
+
NB_STOPS,
|
|
302
|
+
timeFromHM(12, 0),
|
|
303
|
+
);
|
|
304
|
+
const run = RoutingState.fromTestData({
|
|
305
|
+
nbStops: NB_STOPS,
|
|
306
|
+
destinations: [2],
|
|
307
|
+
arrivals: [[2, timeFromHM(8, 45), 1]],
|
|
308
|
+
});
|
|
309
|
+
state.setCurrentRun(run);
|
|
310
|
+
|
|
311
|
+
state.updateArrival(2, timeFromHM(8, 30), 3);
|
|
312
|
+
|
|
313
|
+
assert.deepStrictEqual(run.getArrival(2), {
|
|
314
|
+
arrival: timeFromHM(8, 30),
|
|
315
|
+
legNumber: 3,
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
232
320
|
describe('isDestination', () => {
|
|
233
321
|
it('delegates to the current run', () => {
|
|
234
322
|
const state = new RangeRaptorState(
|
|
@@ -31,7 +31,8 @@ export type ArrivalWithDuration = Arrival & {
|
|
|
31
31
|
* The result of a Range RAPTOR query.
|
|
32
32
|
*
|
|
33
33
|
* Contains the complete Pareto-optimal set of journeys for a resolved
|
|
34
|
-
* destination set
|
|
34
|
+
* destination set, **or** the full per-departure-time routing state when no
|
|
35
|
+
* destinations were provided (full-network / isochrone mode).
|
|
35
36
|
*
|
|
36
37
|
* **Pareto dominance**: journey J1 dominates J2 iff
|
|
37
38
|
* `τdep(J1) ≥ τdep(J2) AND τarr(J1) ≤ τarr(J2)`
|
|
@@ -41,6 +42,15 @@ export type ArrivalWithDuration = Arrival & {
|
|
|
41
42
|
* strictly earlier *and* arrives strictly earlier than the previous one,
|
|
42
43
|
* forming the classic staircase Pareto frontier.
|
|
43
44
|
*
|
|
45
|
+
* **Full-network mode** (empty `destinations`): when no destinations are
|
|
46
|
+
* supplied to the range query every departure slot in the window becomes its
|
|
47
|
+
* own run, because destination-based Pareto pruning cannot be applied.
|
|
48
|
+
* In this mode the destination-specific helpers ({@link getRoutes},
|
|
49
|
+
* {@link bestRoute}, {@link latestDepartureRoute}, {@link fastestRoute})
|
|
50
|
+
* return empty results; use {@link allEarliestArrivals},
|
|
51
|
+
* {@link allShortestDurations}, {@link earliestArrivalAt}, or
|
|
52
|
+
* {@link shortestDurationTo} instead.
|
|
53
|
+
*
|
|
44
54
|
* Destination handling is delegated to {@link Result}, which expands
|
|
45
55
|
* equivalent stops when reconstructing routes or looking up arrivals.
|
|
46
56
|
*/
|
|
@@ -70,6 +80,10 @@ export class RangeResult {
|
|
|
70
80
|
*
|
|
71
81
|
* Each route in the list departs strictly earlier *and* arrives strictly
|
|
72
82
|
* earlier than its predecessor.
|
|
83
|
+
*
|
|
84
|
+
* Returns an empty array when no destinations were provided (full-network
|
|
85
|
+
* mode). Use {@link allEarliestArrivals} or {@link allShortestDurations}
|
|
86
|
+
* to query individual stops in that case.
|
|
73
87
|
*/
|
|
74
88
|
getRoutes(): Route[] {
|
|
75
89
|
const routes: Route[] = [];
|
|
@@ -89,6 +103,8 @@ export class RangeResult {
|
|
|
89
103
|
* at a transit stop.
|
|
90
104
|
*
|
|
91
105
|
* Defaults to this result's own destination stop(s) when `to` is omitted.
|
|
106
|
+
* Always pass an explicit `to` stop when operating in full-network mode
|
|
107
|
+
* (no destinations), otherwise `undefined` is returned.
|
|
92
108
|
*
|
|
93
109
|
* @param to Optional destination stop ID or set of stop IDs.
|
|
94
110
|
* @returns The reconstructed {@link Route} with the earliest arrival,
|
|
@@ -124,6 +140,8 @@ export class RangeResult {
|
|
|
124
140
|
* {@link fastestRoute}.
|
|
125
141
|
*
|
|
126
142
|
* Defaults to this result's own destination stop(s) when `to` is omitted.
|
|
143
|
+
* Always pass an explicit `to` stop when operating in full-network mode
|
|
144
|
+
* (no destinations), otherwise `undefined` is returned.
|
|
127
145
|
*
|
|
128
146
|
* @param to Optional destination stop ID or set of stop IDs.
|
|
129
147
|
* @returns The reconstructed {@link Route} with the latest departure,
|
|
@@ -148,6 +166,8 @@ export class RangeResult {
|
|
|
148
166
|
* spent traveling.
|
|
149
167
|
*
|
|
150
168
|
* Defaults to this result's own destination stop(s) when `to` is omitted.
|
|
169
|
+
* Always pass an explicit `to` stop when operating in full-network mode
|
|
170
|
+
* (no destinations), otherwise `undefined` is returned.
|
|
151
171
|
*
|
|
152
172
|
* @param to Optional destination stop ID or set of stop IDs.
|
|
153
173
|
* @returns The reconstructed fastest {@link Route}, or `undefined` if the
|
|
@@ -45,6 +45,8 @@ export class RangeRouter {
|
|
|
45
45
|
.flatMap((destination) => this.stopsIndex.equivalentStops(destination))
|
|
46
46
|
.map((destination) => destination.id);
|
|
47
47
|
|
|
48
|
+
const noDestinations = destinations.length === 0;
|
|
49
|
+
|
|
48
50
|
const accessLegs = this.accessFinder.collectAccessPaths(
|
|
49
51
|
query.from,
|
|
50
52
|
query.options.minTransferTime,
|
|
@@ -99,15 +101,19 @@ export class RangeRouter {
|
|
|
99
101
|
},
|
|
100
102
|
rangeState,
|
|
101
103
|
);
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
paretoDestBest.
|
|
104
|
+
if (!noDestinations) {
|
|
105
|
+
for (const dest of destinations) {
|
|
106
|
+
const t = routingState.arrivalTime(dest);
|
|
107
|
+
if (t < (paretoDestBest.get(dest) ?? UNREACHED_TIME))
|
|
108
|
+
paretoDestBest.set(dest, t);
|
|
109
|
+
}
|
|
106
110
|
}
|
|
107
111
|
}
|
|
108
112
|
|
|
109
113
|
for (const { depTime, legs } of departureSlots) {
|
|
110
|
-
if (trivialDestCovered.size === destinations.length)
|
|
114
|
+
if (!noDestinations && trivialDestCovered.size === destinations.length) {
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
111
117
|
|
|
112
118
|
if (routingState === null) {
|
|
113
119
|
routingState = new RoutingState(
|
|
@@ -129,23 +135,25 @@ export class RangeRouter {
|
|
|
129
135
|
rangeState,
|
|
130
136
|
);
|
|
131
137
|
|
|
132
|
-
let isParetoOptimal =
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
+
let isParetoOptimal = noDestinations;
|
|
139
|
+
if (!noDestinations) {
|
|
140
|
+
for (const dest of destinations) {
|
|
141
|
+
const arrival = routingState.arrivalTime(dest);
|
|
142
|
+
if (arrival >= (paretoDestBest.get(dest) ?? UNREACHED_TIME)) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
138
145
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
146
|
+
if (trivialDests.has(dest) && trivialDestCovered.has(dest)) {
|
|
147
|
+
paretoDestBest.set(dest, arrival);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
143
150
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
151
|
+
paretoDestBest.set(dest, arrival);
|
|
152
|
+
if (trivialDests.has(dest)) {
|
|
153
|
+
trivialDestCovered.add(dest);
|
|
154
|
+
}
|
|
155
|
+
isParetoOptimal = true;
|
|
147
156
|
}
|
|
148
|
-
isParetoOptimal = true;
|
|
149
157
|
}
|
|
150
158
|
|
|
151
159
|
if (isParetoOptimal) {
|
|
@@ -113,9 +113,19 @@ export class RangeRaptorState implements IRaptorState {
|
|
|
113
113
|
return this.currentRun.isDestination(stop);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
/** Updates
|
|
116
|
+
/** Updates the per-run aggregate best when improved, and always considers the cross-run shared label. */
|
|
117
117
|
updateArrival(stop: StopId, time: Time, round: number): void {
|
|
118
|
-
this.currentRun.
|
|
118
|
+
const currentRunArrival = this.currentRun.getArrival(stop);
|
|
119
|
+
const improvesCurrentRunAggregate =
|
|
120
|
+
currentRunArrival === undefined ||
|
|
121
|
+
time < currentRunArrival.arrival ||
|
|
122
|
+
(time === currentRunArrival.arrival &&
|
|
123
|
+
round < currentRunArrival.legNumber);
|
|
124
|
+
|
|
125
|
+
if (improvesCurrentRunAggregate) {
|
|
126
|
+
this.currentRun.updateArrival(stop, time, round);
|
|
127
|
+
}
|
|
128
|
+
|
|
119
129
|
if (time < this.roundLabels[round]![stop]!) {
|
|
120
130
|
this.roundLabels[round]![stop] = time;
|
|
121
131
|
this.changedInRound[round]!.push(stop);
|