minotor 11.1.3 → 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 +1349 -345
- 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 +74 -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 +165 -32
- package/src/timetable/__tests__/timetable.test.ts +373 -0
- package/src/timetable/route.ts +16 -4
- package/src/timetable/timetable.ts +54 -1
package/dist/cli.mjs
CHANGED
|
@@ -17287,6 +17287,17 @@ let Route$1 = class Route {
|
|
|
17287
17287
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
17288
17288
|
return this.stopTimes[(offset + stopIndex) * 2];
|
|
17289
17289
|
}
|
|
17290
|
+
/**
|
|
17291
|
+
* Hot-path variant of {@link departureFrom} that accepts a precomputed base offset.
|
|
17292
|
+
*
|
|
17293
|
+
* @param stopIndex - The index of the stop in the route.
|
|
17294
|
+
* @param offset - Precomputed value from {@link tripStopOffset}.
|
|
17295
|
+
* @returns The departure time at the specified stop.
|
|
17296
|
+
*/
|
|
17297
|
+
departureAtOffset(stopIndex, offset) {
|
|
17298
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
17299
|
+
return this.stopTimes[(offset + stopIndex) * 2 + 1];
|
|
17300
|
+
}
|
|
17290
17301
|
/**
|
|
17291
17302
|
* Hot-path variant of {@link dropOffTypeAt} that accepts a precomputed base offset.
|
|
17292
17303
|
*
|
|
@@ -17326,8 +17337,8 @@ let Route$1 = class Route {
|
|
|
17326
17337
|
*/
|
|
17327
17338
|
pickUpTypeFrom(stopIndex, tripIndex) {
|
|
17328
17339
|
const globalIndex = tripIndex * this.stops.length + stopIndex;
|
|
17329
|
-
const byteIndex =
|
|
17330
|
-
const isSecondPair = globalIndex
|
|
17340
|
+
const byteIndex = globalIndex >> 1;
|
|
17341
|
+
const isSecondPair = (globalIndex & 1) === 1;
|
|
17331
17342
|
const byte = this.pickupDropOffTypes[byteIndex];
|
|
17332
17343
|
if (byte === undefined) {
|
|
17333
17344
|
throw new Error(`Pick up type not found for stop ${this.stopId(stopIndex)} (${stopIndex}) at trip index ${tripIndex} in route ${this.serviceRouteId}`);
|
|
@@ -17346,8 +17357,8 @@ let Route$1 = class Route {
|
|
|
17346
17357
|
*/
|
|
17347
17358
|
dropOffTypeAt(stopIndex, tripIndex) {
|
|
17348
17359
|
const globalIndex = tripIndex * this.stops.length + stopIndex;
|
|
17349
|
-
const byteIndex =
|
|
17350
|
-
const isSecondPair = globalIndex
|
|
17360
|
+
const byteIndex = globalIndex >> 1;
|
|
17361
|
+
const isSecondPair = (globalIndex & 1) === 1;
|
|
17351
17362
|
const byte = this.pickupDropOffTypes[byteIndex];
|
|
17352
17363
|
if (byte === undefined) {
|
|
17353
17364
|
throw new Error(`Drop off type not found for stop ${this.stopId(stopIndex)} (${stopIndex}) at trip index ${tripIndex} in route ${this.serviceRouteId}`);
|
|
@@ -17947,6 +17958,47 @@ class Timetable {
|
|
|
17947
17958
|
}
|
|
17948
17959
|
return false;
|
|
17949
17960
|
}
|
|
17961
|
+
/**
|
|
17962
|
+
* Finds the first trip on `route` at `stopIndex` that can be boarded, starting
|
|
17963
|
+
* from `earliestTrip` and respecting pickup availability, transfer guarantees,
|
|
17964
|
+
* and minimum transfer times.
|
|
17965
|
+
*
|
|
17966
|
+
* @param stopIndex Stop at which boarding is attempted.
|
|
17967
|
+
* @param route The route to search.
|
|
17968
|
+
* @param earliestTrip First trip index to consider.
|
|
17969
|
+
* @param after Earliest time after which boarding is allowed.
|
|
17970
|
+
* @param beforeTrip Exclusive upper bound on the trip index (omit to search all).
|
|
17971
|
+
* @param fromTripStop The alighted trip stop when transferring from another trip;
|
|
17972
|
+
* `undefined` when boarding from a walk or origin.
|
|
17973
|
+
* @param transferTime Minimum transfer time required between trips.
|
|
17974
|
+
* @returns The index of the first boardable trip, or `undefined` if none found.
|
|
17975
|
+
*/
|
|
17976
|
+
findFirstBoardableTrip(stopIndex, route, earliestTrip, after = TIME_ORIGIN, beforeTrip, fromTripStop, transferTime = DURATION_ZERO) {
|
|
17977
|
+
const nbTrips = route.getNbTrips();
|
|
17978
|
+
for (let t = earliestTrip; t < (beforeTrip !== null && beforeTrip !== void 0 ? beforeTrip : nbTrips); t++) {
|
|
17979
|
+
const pickup = route.pickUpTypeFrom(stopIndex, t);
|
|
17980
|
+
if (pickup === NOT_AVAILABLE) {
|
|
17981
|
+
continue;
|
|
17982
|
+
}
|
|
17983
|
+
if (fromTripStop === undefined) {
|
|
17984
|
+
return t;
|
|
17985
|
+
}
|
|
17986
|
+
const isGuaranteed = this.isTripTransferGuaranteed(fromTripStop, {
|
|
17987
|
+
stopIndex,
|
|
17988
|
+
routeId: route.id,
|
|
17989
|
+
tripIndex: t,
|
|
17990
|
+
});
|
|
17991
|
+
if (isGuaranteed) {
|
|
17992
|
+
return t;
|
|
17993
|
+
}
|
|
17994
|
+
const departure = route.departureFrom(stopIndex, t);
|
|
17995
|
+
const requiredTime = after + transferTime;
|
|
17996
|
+
if (departure >= requiredTime) {
|
|
17997
|
+
return t;
|
|
17998
|
+
}
|
|
17999
|
+
}
|
|
18000
|
+
return undefined;
|
|
18001
|
+
}
|
|
17950
18002
|
/**
|
|
17951
18003
|
* Retrieves all guaranteed trip transfer options available at the specified stop for a given trip.
|
|
17952
18004
|
*
|
|
@@ -21124,6 +21176,12 @@ function isVehicleEdge(edge) {
|
|
|
21124
21176
|
function isTransferEdge(edge) {
|
|
21125
21177
|
return 'from' in edge && 'to' in edge && 'type' in edge;
|
|
21126
21178
|
}
|
|
21179
|
+
/**
|
|
21180
|
+
* Type guard to check if an edge is an AccessEdge (walking access leg).
|
|
21181
|
+
*/
|
|
21182
|
+
function isAccessEdge(edge) {
|
|
21183
|
+
return 'from' in edge && 'duration' in edge;
|
|
21184
|
+
}
|
|
21127
21185
|
/**
|
|
21128
21186
|
* Helper class for building DOT graph syntax.
|
|
21129
21187
|
*/
|
|
@@ -21219,6 +21277,12 @@ class Plotter {
|
|
|
21219
21277
|
transferEdgeNodeId(fromStopId, toStopId, round) {
|
|
21220
21278
|
return `e_${fromStopId}_${toStopId}_${round}`;
|
|
21221
21279
|
}
|
|
21280
|
+
/**
|
|
21281
|
+
* Generates a unique node ID for a walking access edge oval.
|
|
21282
|
+
*/
|
|
21283
|
+
accessEdgeNodeId(fromStopId, toStopId) {
|
|
21284
|
+
return `access_${fromStopId}_${toStopId}`;
|
|
21285
|
+
}
|
|
21222
21286
|
/**
|
|
21223
21287
|
* Generates a unique node ID for a continuation edge oval.
|
|
21224
21288
|
*/
|
|
@@ -21342,6 +21406,22 @@ class Plotter {
|
|
|
21342
21406
|
` "${routeOvalId}" -> "${toNodeId}" [color="${roundColor}"];`,
|
|
21343
21407
|
];
|
|
21344
21408
|
}
|
|
21409
|
+
/**
|
|
21410
|
+
* Creates a walking access leg as a dashed oval connecting the query origin
|
|
21411
|
+
* to the initial boarding stop.
|
|
21412
|
+
*/
|
|
21413
|
+
createAccessEdge(edge) {
|
|
21414
|
+
const fromNodeId = this.stationNodeId(edge.from);
|
|
21415
|
+
const toNodeId = this.stationNodeId(edge.to);
|
|
21416
|
+
const color = DOT_CONFIG.colors.defaultRound;
|
|
21417
|
+
const ovalId = this.accessEdgeNodeId(edge.from, edge.to);
|
|
21418
|
+
const label = `Walk\\n${durationToString(edge.duration)}`;
|
|
21419
|
+
return [
|
|
21420
|
+
` "${ovalId}" [label="${label}" shape=oval style="dashed,filled" fillcolor="white" color="${color}"];`,
|
|
21421
|
+
` "${fromNodeId}" -> "${ovalId}" [color="${color}" style="dashed"];`,
|
|
21422
|
+
` "${ovalId}" -> "${toNodeId}" [color="${color}" style="dashed"];`,
|
|
21423
|
+
];
|
|
21424
|
+
}
|
|
21345
21425
|
/**
|
|
21346
21426
|
* Creates a transfer edge with transfer information oval in the middle.
|
|
21347
21427
|
*/
|
|
@@ -21426,6 +21506,12 @@ class Plotter {
|
|
|
21426
21506
|
if (toStopId)
|
|
21427
21507
|
stations.add(toStopId);
|
|
21428
21508
|
}
|
|
21509
|
+
else if (isAccessEdge(edge)) {
|
|
21510
|
+
// Ensure the query origin (edge.from) is always collected even when
|
|
21511
|
+
// its own OriginNode hasn't been processed yet in this iteration.
|
|
21512
|
+
stations.add(edge.from);
|
|
21513
|
+
stations.add(edge.to);
|
|
21514
|
+
}
|
|
21429
21515
|
}
|
|
21430
21516
|
}
|
|
21431
21517
|
return stations;
|
|
@@ -21456,14 +21542,18 @@ class Plotter {
|
|
|
21456
21542
|
const roundEdges = graph[round];
|
|
21457
21543
|
if (!roundEdges)
|
|
21458
21544
|
continue;
|
|
21459
|
-
// Skip round 0 as it contains only origin nodes
|
|
21460
|
-
if (round === 0) {
|
|
21461
|
-
continue;
|
|
21462
|
-
}
|
|
21463
21545
|
for (let stopId = 0; stopId < roundEdges.length; stopId++) {
|
|
21464
21546
|
const edge = roundEdges[stopId];
|
|
21465
21547
|
if (edge === undefined)
|
|
21466
21548
|
continue;
|
|
21549
|
+
if (round === 0) {
|
|
21550
|
+
// Round 0 holds OriginNodes (no edge to draw) and AccessEdges
|
|
21551
|
+
// (walking legs from the query origin to the first boarding stop).
|
|
21552
|
+
if (isAccessEdge(edge)) {
|
|
21553
|
+
edges.push(...this.createAccessEdge(edge));
|
|
21554
|
+
}
|
|
21555
|
+
continue;
|
|
21556
|
+
}
|
|
21467
21557
|
if (isVehicleEdge(edge)) {
|
|
21468
21558
|
edges.push(...this.createVehicleEdge(edge, round));
|
|
21469
21559
|
if (edge.continuationOf) {
|
|
@@ -21500,6 +21590,13 @@ class Plotter {
|
|
|
21500
21590
|
}
|
|
21501
21591
|
}
|
|
21502
21592
|
|
|
21593
|
+
/**
|
|
21594
|
+
* A routing query for standard RAPTOR.
|
|
21595
|
+
*
|
|
21596
|
+
* Finds the earliest-arrival journey from `from` to `to` for a single
|
|
21597
|
+
* departure time. Use {@link RangeQuery} (and `router.rangeRoute()`) when
|
|
21598
|
+
* you want all Pareto-optimal journeys within a departure-time window.
|
|
21599
|
+
*/
|
|
21503
21600
|
class Query {
|
|
21504
21601
|
constructor(builder) {
|
|
21505
21602
|
this.from = builder.fromValue;
|
|
@@ -21525,16 +21622,16 @@ Query.Builder = class {
|
|
|
21525
21622
|
return this;
|
|
21526
21623
|
}
|
|
21527
21624
|
/**
|
|
21528
|
-
* Sets the destination
|
|
21625
|
+
* Sets the destination stop(s).
|
|
21626
|
+
* Routing stops as soon as all provided stops have been reached.
|
|
21529
21627
|
*/
|
|
21530
21628
|
to(to) {
|
|
21531
21629
|
this.toValue = to instanceof Set ? to : new Set([to]);
|
|
21532
21630
|
return this;
|
|
21533
21631
|
}
|
|
21534
21632
|
/**
|
|
21535
|
-
* Sets the departure time
|
|
21536
|
-
*
|
|
21537
|
-
* even if a later route might arrive at the same time.
|
|
21633
|
+
* Sets the departure time in minutes from midnight.
|
|
21634
|
+
* The router favours trips departing shortly after this time.
|
|
21538
21635
|
*/
|
|
21539
21636
|
departureTime(departureTime) {
|
|
21540
21637
|
this.departureTimeValue = departureTime;
|
|
@@ -21548,24 +21645,77 @@ Query.Builder = class {
|
|
|
21548
21645
|
return this;
|
|
21549
21646
|
}
|
|
21550
21647
|
/**
|
|
21551
|
-
* Sets the minimum transfer time (in minutes)
|
|
21552
|
-
*
|
|
21648
|
+
* Sets the fallback minimum transfer time (in minutes) used when the
|
|
21649
|
+
* timetable data does not specify one for a particular transfer.
|
|
21553
21650
|
*/
|
|
21554
21651
|
minTransferTime(minTransferTime) {
|
|
21555
21652
|
this.optionsValue.minTransferTime = minTransferTime;
|
|
21556
21653
|
return this;
|
|
21557
21654
|
}
|
|
21558
21655
|
/**
|
|
21559
|
-
*
|
|
21656
|
+
* Restricts routing to the given transport modes.
|
|
21560
21657
|
*/
|
|
21561
21658
|
transportModes(transportModes) {
|
|
21562
21659
|
this.optionsValue.transportModes = transportModes;
|
|
21563
21660
|
return this;
|
|
21564
21661
|
}
|
|
21662
|
+
/**
|
|
21663
|
+
* Sets the maximum time (in minutes) the traveler is willing to wait at
|
|
21664
|
+
* the first boarding stop before the first transit vehicle departs.
|
|
21665
|
+
*
|
|
21666
|
+
* When set, any trip that would require waiting longer than this duration
|
|
21667
|
+
* after arriving at the stop is not considered for the first boarding leg.
|
|
21668
|
+
*/
|
|
21669
|
+
maxInitialWaitingTime(maxInitialWaitingTime) {
|
|
21670
|
+
this.optionsValue.maxInitialWaitingTime = maxInitialWaitingTime;
|
|
21671
|
+
return this;
|
|
21672
|
+
}
|
|
21565
21673
|
build() {
|
|
21566
21674
|
return new Query(this);
|
|
21567
21675
|
}
|
|
21568
21676
|
};
|
|
21677
|
+
/**
|
|
21678
|
+
* A routing query for Range RAPTOR.
|
|
21679
|
+
*
|
|
21680
|
+
* Extends {@link Query} with a required `lastDepartureTime` that defines the
|
|
21681
|
+
* upper bound of the departure-time window. `router.rangeRoute()` returns
|
|
21682
|
+
* all Pareto-optimal journeys departing in
|
|
21683
|
+
* `[departureTime, lastDepartureTime]`.
|
|
21684
|
+
*
|
|
21685
|
+
*/
|
|
21686
|
+
class RangeQuery extends Query {
|
|
21687
|
+
constructor(builder) {
|
|
21688
|
+
super(builder);
|
|
21689
|
+
this.lastDepartureTime = builder.lastDepartureTimeValue;
|
|
21690
|
+
this.rangeOptions = builder.rangeOptionsValue;
|
|
21691
|
+
}
|
|
21692
|
+
}
|
|
21693
|
+
RangeQuery.Builder = class extends Query.Builder {
|
|
21694
|
+
constructor() {
|
|
21695
|
+
super(...arguments);
|
|
21696
|
+
this.rangeOptionsValue = {
|
|
21697
|
+
optimizeBeyondLatestDeparture: true,
|
|
21698
|
+
};
|
|
21699
|
+
}
|
|
21700
|
+
/**
|
|
21701
|
+
* Sets the upper bound of the departure-time window.
|
|
21702
|
+
*/
|
|
21703
|
+
lastDepartureTime(time) {
|
|
21704
|
+
this.lastDepartureTimeValue = time;
|
|
21705
|
+
return this;
|
|
21706
|
+
}
|
|
21707
|
+
/**
|
|
21708
|
+
* Overrides individual Range RAPTOR options.
|
|
21709
|
+
* Unspecified fields keep their defaults.
|
|
21710
|
+
*/
|
|
21711
|
+
rangeOptions(options) {
|
|
21712
|
+
this.rangeOptionsValue = Object.assign(Object.assign({}, this.rangeOptionsValue), options);
|
|
21713
|
+
return this;
|
|
21714
|
+
}
|
|
21715
|
+
build() {
|
|
21716
|
+
return new RangeQuery(this);
|
|
21717
|
+
}
|
|
21718
|
+
};
|
|
21569
21719
|
|
|
21570
21720
|
/**
|
|
21571
21721
|
* Represents a resolved route consisting of multiple legs,
|
|
@@ -21582,15 +21732,15 @@ class Route {
|
|
|
21582
21732
|
* @throws If no vehicle leg is found in the route.
|
|
21583
21733
|
*/
|
|
21584
21734
|
departureTime() {
|
|
21585
|
-
let
|
|
21735
|
+
let cumulativeAccessTime = DURATION_ZERO;
|
|
21586
21736
|
for (let i = 0; i < this.legs.length; i++) {
|
|
21587
21737
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
21588
21738
|
const leg = this.legs[i];
|
|
21589
21739
|
if ('departureTime' in leg) {
|
|
21590
|
-
return leg.departureTime -
|
|
21740
|
+
return leg.departureTime - cumulativeAccessTime;
|
|
21591
21741
|
}
|
|
21592
|
-
if ('
|
|
21593
|
-
|
|
21742
|
+
if ('duration' in leg && leg.duration) {
|
|
21743
|
+
cumulativeAccessTime += leg.duration;
|
|
21594
21744
|
}
|
|
21595
21745
|
}
|
|
21596
21746
|
throw new Error('No vehicle leg found in route');
|
|
@@ -21641,6 +21791,9 @@ class Route {
|
|
|
21641
21791
|
const transferDetails = 'type' in leg && !('route' in leg)
|
|
21642
21792
|
? `Transfer: ${leg.type}${leg.minTransferTime ? `, Minimum Transfer Time: ${durationToString(leg.minTransferTime)}` : ''}`
|
|
21643
21793
|
: '';
|
|
21794
|
+
const accessDetails = 'duration' in leg
|
|
21795
|
+
? `Access duration: ${durationToString(leg.duration)}`
|
|
21796
|
+
: '';
|
|
21644
21797
|
const travelDetails = 'route' in leg && 'departureTime' in leg && 'arrivalTime' in leg
|
|
21645
21798
|
? `Route: ${leg.route.type} ${leg.route.name}, Departure: ${timeToString(leg.departureTime)}, Arrival: ${timeToString(leg.arrivalTime)}`
|
|
21646
21799
|
: '';
|
|
@@ -21649,6 +21802,7 @@ class Route {
|
|
|
21649
21802
|
` ${fromStop}`,
|
|
21650
21803
|
` ${toStop}`,
|
|
21651
21804
|
transferDetails ? ` ${transferDetails}` : '',
|
|
21805
|
+
accessDetails ? ` ${accessDetails}` : '',
|
|
21652
21806
|
travelDetails ? ` ${travelDetails}` : '',
|
|
21653
21807
|
]
|
|
21654
21808
|
.filter((line) => line.trim() !== '')
|
|
@@ -21656,32 +21810,6 @@ class Route {
|
|
|
21656
21810
|
})
|
|
21657
21811
|
.join('\n');
|
|
21658
21812
|
}
|
|
21659
|
-
/**
|
|
21660
|
-
* Generates a concise JSON representation of the route.
|
|
21661
|
-
* This is particularly useful for generating regression tests
|
|
21662
|
-
* to verify the correctness of route calculations.
|
|
21663
|
-
*
|
|
21664
|
-
* @returns A JSON representation of the route.
|
|
21665
|
-
*/
|
|
21666
|
-
asJson() {
|
|
21667
|
-
const jsonLegs = this.legs.map((leg) => {
|
|
21668
|
-
if ('route' in leg) {
|
|
21669
|
-
return {
|
|
21670
|
-
from: leg.from.id,
|
|
21671
|
-
to: leg.to.id,
|
|
21672
|
-
departure: timeToString(leg.departureTime),
|
|
21673
|
-
arrival: timeToString(leg.arrivalTime),
|
|
21674
|
-
route: leg.route,
|
|
21675
|
-
};
|
|
21676
|
-
}
|
|
21677
|
-
else {
|
|
21678
|
-
return Object.assign({ from: leg.from.id, to: leg.to.id, type: leg.type }, (leg.minTransferTime !== undefined && {
|
|
21679
|
-
minTransferTime: leg.minTransferTime,
|
|
21680
|
-
}));
|
|
21681
|
-
}
|
|
21682
|
-
});
|
|
21683
|
-
return jsonLegs;
|
|
21684
|
-
}
|
|
21685
21813
|
}
|
|
21686
21814
|
|
|
21687
21815
|
const pickUpDropOffTypeMap = [
|
|
@@ -21706,15 +21834,35 @@ const toPickupDropOffType = (rawType) => {
|
|
|
21706
21834
|
return type;
|
|
21707
21835
|
};
|
|
21708
21836
|
class Result {
|
|
21709
|
-
constructor(
|
|
21710
|
-
this.
|
|
21837
|
+
constructor(destinations, routingState, stopsIndex, timetable) {
|
|
21838
|
+
this.destinations = destinations;
|
|
21711
21839
|
this.routingState = routingState;
|
|
21712
21840
|
this.stopsIndex = stopsIndex;
|
|
21713
21841
|
this.timetable = timetable;
|
|
21714
21842
|
}
|
|
21843
|
+
/**
|
|
21844
|
+
* Expands a target stop or stop set to all equivalent concrete stop IDs.
|
|
21845
|
+
*
|
|
21846
|
+
* When `to` is omitted, defaults to the resolved destinations stored on this
|
|
21847
|
+
* result.
|
|
21848
|
+
*
|
|
21849
|
+
* Equivalent stops are expanded here so destination handling has a single
|
|
21850
|
+
* source of truth shared by route reconstruction and arrival lookups.
|
|
21851
|
+
*/
|
|
21852
|
+
expandDestinations(to) {
|
|
21853
|
+
const targets = to instanceof Set ? to : to !== undefined ? [to] : this.destinations;
|
|
21854
|
+
const expanded = new Set();
|
|
21855
|
+
for (const target of targets) {
|
|
21856
|
+
for (const equivalentStop of this.stopsIndex.equivalentStops(target)) {
|
|
21857
|
+
expanded.add(equivalentStop.id);
|
|
21858
|
+
}
|
|
21859
|
+
}
|
|
21860
|
+
return expanded;
|
|
21861
|
+
}
|
|
21715
21862
|
/**
|
|
21716
21863
|
* Reconstructs the best route to a stop by SourceStopId.
|
|
21717
|
-
* (to any stop reachable in less time / transfers than
|
|
21864
|
+
* (to any stop reachable in less time / transfers than this result's
|
|
21865
|
+
* destination set)
|
|
21718
21866
|
*
|
|
21719
21867
|
* @param to The destination stop by SourceStopId.
|
|
21720
21868
|
* @returns a route to the destination stop if it exists.
|
|
@@ -21735,29 +21883,28 @@ class Result {
|
|
|
21735
21883
|
}
|
|
21736
21884
|
/**
|
|
21737
21885
|
* Reconstructs the best route to a stop.
|
|
21738
|
-
* (to any stop reachable in less time / transfers than
|
|
21886
|
+
* (to any stop reachable in less time / transfers than this result's
|
|
21887
|
+
* destination set)
|
|
21739
21888
|
*
|
|
21740
|
-
* @param to The destination stop. Defaults to
|
|
21889
|
+
* @param to The destination stop. Defaults to this result's resolved
|
|
21890
|
+
* destinations.
|
|
21741
21891
|
* @returns a route to the destination stop if it exists.
|
|
21742
21892
|
*/
|
|
21743
21893
|
bestRoute(to) {
|
|
21744
21894
|
var _a;
|
|
21745
|
-
const
|
|
21895
|
+
const destinationStops = this.expandDestinations(to);
|
|
21746
21896
|
// Find the fastest-reached destination across all equivalent stops.
|
|
21747
21897
|
let fastestDestination = undefined;
|
|
21748
21898
|
let fastestArrivalTime = undefined;
|
|
21749
21899
|
let fastestLegNumber = undefined;
|
|
21750
|
-
for (const
|
|
21751
|
-
const
|
|
21752
|
-
|
|
21753
|
-
|
|
21754
|
-
|
|
21755
|
-
|
|
21756
|
-
|
|
21757
|
-
|
|
21758
|
-
fastestArrivalTime = arrivalData.arrival;
|
|
21759
|
-
fastestLegNumber = arrivalData.legNumber;
|
|
21760
|
-
}
|
|
21900
|
+
for (const destination of destinationStops) {
|
|
21901
|
+
const arrivalData = this.routingState.getArrival(destination);
|
|
21902
|
+
if (arrivalData !== undefined &&
|
|
21903
|
+
(fastestArrivalTime === undefined ||
|
|
21904
|
+
arrivalData.arrival < fastestArrivalTime)) {
|
|
21905
|
+
fastestDestination = destination;
|
|
21906
|
+
fastestArrivalTime = arrivalData.arrival;
|
|
21907
|
+
fastestLegNumber = arrivalData.legNumber;
|
|
21761
21908
|
}
|
|
21762
21909
|
}
|
|
21763
21910
|
if (fastestDestination === undefined || fastestLegNumber === undefined) {
|
|
@@ -21768,9 +21915,11 @@ class Result {
|
|
|
21768
21915
|
let currentStop = fastestDestination;
|
|
21769
21916
|
let round = fastestLegNumber;
|
|
21770
21917
|
let previousVehicleEdge;
|
|
21771
|
-
while (round
|
|
21918
|
+
while (round >= 0) {
|
|
21772
21919
|
const edge = (_a = this.routingState.graph[round]) === null || _a === void 0 ? void 0 : _a[currentStop];
|
|
21773
21920
|
if (!edge) {
|
|
21921
|
+
if (round === 0)
|
|
21922
|
+
break;
|
|
21774
21923
|
throw new Error(`No edge arriving at stop ${currentStop} at round ${round}`);
|
|
21775
21924
|
}
|
|
21776
21925
|
let leg;
|
|
@@ -21815,6 +21964,10 @@ class Result {
|
|
|
21815
21964
|
leg = this.buildTransferLeg(edge);
|
|
21816
21965
|
previousVehicleEdge = undefined;
|
|
21817
21966
|
}
|
|
21967
|
+
else if ('duration' in edge) {
|
|
21968
|
+
leg = this.buildAccessLeg(edge);
|
|
21969
|
+
previousVehicleEdge = undefined;
|
|
21970
|
+
}
|
|
21818
21971
|
else {
|
|
21819
21972
|
break;
|
|
21820
21973
|
}
|
|
@@ -21876,6 +22029,21 @@ class Result {
|
|
|
21876
22029
|
type: edge.type,
|
|
21877
22030
|
};
|
|
21878
22031
|
}
|
|
22032
|
+
/**
|
|
22033
|
+
* Builds a transfer leg from a transfer edge.
|
|
22034
|
+
*
|
|
22035
|
+
* @param edge Transfer edge representing a walking connection between stops
|
|
22036
|
+
* @returns A transfer leg with from/to stops and transfer details
|
|
22037
|
+
*/
|
|
22038
|
+
buildAccessLeg(edge) {
|
|
22039
|
+
return {
|
|
22040
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
22041
|
+
from: this.stopsIndex.findStopById(edge.from),
|
|
22042
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
22043
|
+
to: this.stopsIndex.findStopById(edge.to),
|
|
22044
|
+
duration: edge.duration,
|
|
22045
|
+
};
|
|
22046
|
+
}
|
|
21879
22047
|
/**
|
|
21880
22048
|
* Builds a guaranteed transfer leg between two consecutive vehicle legs.
|
|
21881
22049
|
*
|
|
@@ -21899,7 +22067,8 @@ class Result {
|
|
|
21899
22067
|
};
|
|
21900
22068
|
}
|
|
21901
22069
|
/**
|
|
21902
|
-
* Returns the arrival time at any stop reachable in less time / transfers
|
|
22070
|
+
* Returns the arrival time at any stop reachable in less time / transfers
|
|
22071
|
+
* than this result's destination set.
|
|
21903
22072
|
*
|
|
21904
22073
|
* @param stop The target stop for which to return the arrival time.
|
|
21905
22074
|
* @param maxTransfers The optional maximum number of transfers allowed.
|
|
@@ -21941,6 +22110,118 @@ class Result {
|
|
|
21941
22110
|
}
|
|
21942
22111
|
}
|
|
21943
22112
|
|
|
22113
|
+
/**
|
|
22114
|
+
* Collects access paths from a query origin and resolves the set of
|
|
22115
|
+
* distinct departure-time slots for Range RAPTOR.
|
|
22116
|
+
*/
|
|
22117
|
+
class AccessFinder {
|
|
22118
|
+
constructor(timetable, stopsIndex) {
|
|
22119
|
+
this.timetable = timetable;
|
|
22120
|
+
this.stopsIndex = stopsIndex;
|
|
22121
|
+
}
|
|
22122
|
+
/**
|
|
22123
|
+
* Returns every initial access path from the query origin: equivalent stops
|
|
22124
|
+
* (no duration) plus every stop reachable via a single timed walking transfer
|
|
22125
|
+
* (REQUIRES_MINIMAL_TIME), keeping the shortest walk when multiple origins
|
|
22126
|
+
* can reach the same stop.
|
|
22127
|
+
*
|
|
22128
|
+
* @param origin Origin stop ID.
|
|
22129
|
+
* @param fallbackMinTransferTime Transfer time used when a walking transfer
|
|
22130
|
+
* has no explicit `minTransferTime` in the timetable data.
|
|
22131
|
+
*/
|
|
22132
|
+
collectAccessPaths(queryOrigin, fallbackMinTransferTime) {
|
|
22133
|
+
var _a;
|
|
22134
|
+
const equivalentOrigins = this.stopsIndex
|
|
22135
|
+
.equivalentStops(queryOrigin)
|
|
22136
|
+
.map((stop) => stop.id);
|
|
22137
|
+
const accessPaths = new Map();
|
|
22138
|
+
for (const origin of equivalentOrigins) {
|
|
22139
|
+
const existingAccess = accessPaths.get(origin);
|
|
22140
|
+
if (existingAccess === undefined || existingAccess.duration > 0) {
|
|
22141
|
+
accessPaths.set(origin, {
|
|
22142
|
+
fromStopId: origin,
|
|
22143
|
+
toStopId: origin,
|
|
22144
|
+
duration: 0,
|
|
22145
|
+
});
|
|
22146
|
+
}
|
|
22147
|
+
for (const transfer of this.timetable.getTransfers(origin)) {
|
|
22148
|
+
if (transfer.type === 'REQUIRES_MINIMAL_TIME') {
|
|
22149
|
+
const duration = (_a = transfer.minTransferTime) !== null && _a !== void 0 ? _a : fallbackMinTransferTime;
|
|
22150
|
+
const existingAccess = accessPaths.get(transfer.destination);
|
|
22151
|
+
// Keep the shortest walk to maximize the set of reachable trips.
|
|
22152
|
+
if (existingAccess === undefined ||
|
|
22153
|
+
(existingAccess.duration && duration < existingAccess.duration)) {
|
|
22154
|
+
accessPaths.set(transfer.destination, {
|
|
22155
|
+
fromStopId: origin,
|
|
22156
|
+
toStopId: transfer.destination,
|
|
22157
|
+
duration,
|
|
22158
|
+
});
|
|
22159
|
+
}
|
|
22160
|
+
}
|
|
22161
|
+
}
|
|
22162
|
+
}
|
|
22163
|
+
return Array.from(accessPaths.values());
|
|
22164
|
+
}
|
|
22165
|
+
/**
|
|
22166
|
+
* Collects all distinct origin departure times within `[from, to]`
|
|
22167
|
+
* (inclusive) and, for each slot, the specific access paths that directly
|
|
22168
|
+
* induce it — i.e. paths whose boarded stop has a boardable trip departing
|
|
22169
|
+
* at exactly `depTime + path.duration`.
|
|
22170
|
+
*
|
|
22171
|
+
* Returned array is sorted **latest-first**. The Range RAPTOR outer loop
|
|
22172
|
+
* seeds only the responsible paths for each slot, avoiding redundant
|
|
22173
|
+
* exploration of access stops whose boarding opportunities belong to a
|
|
22174
|
+
* later slot and whose journeys would therefore be dominated by it.
|
|
22175
|
+
*
|
|
22176
|
+
|
|
22177
|
+
* @param accessPaths Access paths from the origin to initial boarding stops.
|
|
22178
|
+
* @param from Earliest origin departure time (inclusive).
|
|
22179
|
+
* @param to Latest origin departure time (inclusive).
|
|
22180
|
+
*/
|
|
22181
|
+
collectDepartureTimes(accessPaths, from, to) {
|
|
22182
|
+
// Map from origin-departure-time → the set of access paths that induce it.
|
|
22183
|
+
const slotMap = new Map();
|
|
22184
|
+
for (const path of accessPaths) {
|
|
22185
|
+
const { toStopId } = path;
|
|
22186
|
+
// Trips from this stop must depart in [from + duration, to + duration]
|
|
22187
|
+
// so that the corresponding origin departure (dep - duration) falls in
|
|
22188
|
+
// [from, to].
|
|
22189
|
+
const searchFrom = from + path.duration;
|
|
22190
|
+
const searchTo = to + path.duration;
|
|
22191
|
+
for (const route of this.timetable.routesPassingThrough(toStopId)) {
|
|
22192
|
+
for (const stopIndex of route.stopRouteIndices(toStopId)) {
|
|
22193
|
+
let tripIndex = route.findEarliestTrip(stopIndex, searchFrom);
|
|
22194
|
+
if (tripIndex === undefined)
|
|
22195
|
+
continue;
|
|
22196
|
+
const nbTrips = route.getNbTrips();
|
|
22197
|
+
while (tripIndex < nbTrips) {
|
|
22198
|
+
const dep = route.departureFrom(stopIndex, tripIndex);
|
|
22199
|
+
if (dep > searchTo)
|
|
22200
|
+
break;
|
|
22201
|
+
if (route.pickUpTypeFrom(stopIndex, tripIndex) !== NOT_AVAILABLE) {
|
|
22202
|
+
const t = dep - path.duration;
|
|
22203
|
+
let paths = slotMap.get(t);
|
|
22204
|
+
if (paths === undefined) {
|
|
22205
|
+
slotMap.set(t, (paths = new Set()));
|
|
22206
|
+
}
|
|
22207
|
+
paths.add(path);
|
|
22208
|
+
}
|
|
22209
|
+
tripIndex++;
|
|
22210
|
+
}
|
|
22211
|
+
}
|
|
22212
|
+
}
|
|
22213
|
+
}
|
|
22214
|
+
if (slotMap.size === 0)
|
|
22215
|
+
return [];
|
|
22216
|
+
// Sort descending so the outer loop processes latest departures first.
|
|
22217
|
+
const sorted = Array.from(slotMap.entries()).sort(([a], [b]) => b - a);
|
|
22218
|
+
return sorted.map(([depTime, paths]) => ({
|
|
22219
|
+
depTime,
|
|
22220
|
+
legs: Array.from(paths),
|
|
22221
|
+
}));
|
|
22222
|
+
}
|
|
22223
|
+
}
|
|
22224
|
+
|
|
21944
22225
|
/**
|
|
21945
22226
|
* Sentinel value used in the internal arrival-time array to mark stops not yet reached.
|
|
21946
22227
|
* 0xFFFF = 65 535 minutes ≈ 45.5 days, safely beyond any realistic transit arrival time.
|
|
@@ -21950,31 +22231,64 @@ const UNREACHED_TIME = 0xffff;
|
|
|
21950
22231
|
* Encapsulates all mutable state for a single RAPTOR routing query.
|
|
21951
22232
|
*/
|
|
21952
22233
|
class RoutingState {
|
|
22234
|
+
constructor(departureTime, destinations, accessPaths, nbStops, maxRounds = 0) {
|
|
22235
|
+
/**
|
|
22236
|
+
* Cached best arrival time at any destination stop, kept up-to-date by
|
|
22237
|
+
* {@link updateArrival} so that destination pruning is always O(1).
|
|
22238
|
+
*/
|
|
22239
|
+
this._destinationBest = UNREACHED_TIME;
|
|
22240
|
+
/**
|
|
22241
|
+
* Every stop that has received an arrival improvement during the current run,
|
|
22242
|
+
* in the order the improvements occurred. Used by {@link resetFor} to clear
|
|
22243
|
+
* only the touched entries instead of scanning the entire array.
|
|
22244
|
+
*/
|
|
22245
|
+
this.reachedStops = [];
|
|
22246
|
+
this.destinations = destinations;
|
|
22247
|
+
this.destinationSet = new Set(destinations);
|
|
22248
|
+
this.earliestArrivalTimes = new Uint16Array(nbStops).fill(UNREACHED_TIME);
|
|
22249
|
+
this.earliestArrivalLegs = new Uint8Array(nbStops);
|
|
22250
|
+
this.origins = []; // overwritten by seedAccessPaths below
|
|
22251
|
+
this.graph = [new Array(nbStops)];
|
|
22252
|
+
for (let r = 1; r <= maxRounds; r++) {
|
|
22253
|
+
this.graph.push(new Array(nbStops));
|
|
22254
|
+
}
|
|
22255
|
+
this.seedAccessPaths(departureTime, accessPaths);
|
|
22256
|
+
}
|
|
21953
22257
|
/**
|
|
21954
|
-
*
|
|
21955
|
-
*
|
|
21956
|
-
*
|
|
21957
|
-
*
|
|
21958
|
-
*
|
|
21959
|
-
*
|
|
21960
|
-
* @param origins Stop IDs to depart from (may be several equivalent stops).
|
|
21961
|
-
* @param destinations Stop IDs that count as the target of the query.
|
|
21962
|
-
* @param departureTime Earliest departure time in minutes from midnight.
|
|
21963
|
-
* @param nbStops Total number of stops in the timetable (sets array sizes).
|
|
22258
|
+
* Seeds round-0 arrivals and {@link origins} from a set of access paths.
|
|
22259
|
+
* Called by the constructor and by {@link resetFor}.
|
|
22260
|
+
* Assumes {@link earliestArrivalTimes} and {@link graph}[0] are already
|
|
22261
|
+
* allocated and in their "cleared" state (all entries at UNREACHED_TIME /
|
|
22262
|
+
* undefined) before this method runs.
|
|
21964
22263
|
*/
|
|
21965
|
-
|
|
21966
|
-
|
|
21967
|
-
|
|
21968
|
-
|
|
21969
|
-
|
|
21970
|
-
|
|
21971
|
-
|
|
21972
|
-
|
|
21973
|
-
|
|
22264
|
+
seedAccessPaths(depTime, accessPaths) {
|
|
22265
|
+
const seededOrigins = new Set();
|
|
22266
|
+
for (const access of accessPaths) {
|
|
22267
|
+
const arrival = depTime + access.duration;
|
|
22268
|
+
const edge = access.duration === 0
|
|
22269
|
+
? { stopId: access.fromStopId, arrival: depTime }
|
|
22270
|
+
: {
|
|
22271
|
+
arrival,
|
|
22272
|
+
from: access.fromStopId,
|
|
22273
|
+
to: access.toStopId,
|
|
22274
|
+
duration: access.duration,
|
|
22275
|
+
};
|
|
22276
|
+
const stop = access.toStopId;
|
|
22277
|
+
if (arrival < this.earliestArrivalTimes[stop]) {
|
|
22278
|
+
this.earliestArrivalTimes[stop] = arrival;
|
|
22279
|
+
this.graph[0][stop] = edge;
|
|
22280
|
+
}
|
|
22281
|
+
seededOrigins.add(stop);
|
|
22282
|
+
}
|
|
22283
|
+
for (const stop of seededOrigins) {
|
|
22284
|
+
this.reachedStops.push(stop);
|
|
22285
|
+
}
|
|
22286
|
+
this.origins = Array.from(seededOrigins);
|
|
22287
|
+
for (let i = 0; i < this.destinations.length; i++) {
|
|
22288
|
+
const t = this.earliestArrivalTimes[this.destinations[i]];
|
|
22289
|
+
if (t < this._destinationBest)
|
|
22290
|
+
this._destinationBest = t;
|
|
21974
22291
|
}
|
|
21975
|
-
this.earliestArrivalTimes = earliestArrivalTimes;
|
|
21976
|
-
this.earliestArrivalLegs = earliestArrivalLegs;
|
|
21977
|
-
this.graph = [graph0];
|
|
21978
22292
|
}
|
|
21979
22293
|
/** Total number of stops in the timetable */
|
|
21980
22294
|
get nbStops() {
|
|
@@ -21987,17 +22301,62 @@ class RoutingState {
|
|
|
21987
22301
|
arrivalTime(stop) {
|
|
21988
22302
|
return this.earliestArrivalTimes[stop];
|
|
21989
22303
|
}
|
|
22304
|
+
/**
|
|
22305
|
+
* Earliest arrival at any destination stop; {@link UNREACHED_TIME} if none
|
|
22306
|
+
* has been reached yet. Updated automatically by {@link updateArrival}. O(1).
|
|
22307
|
+
*/
|
|
22308
|
+
get destinationBest() {
|
|
22309
|
+
return this._destinationBest;
|
|
22310
|
+
}
|
|
22311
|
+
/**
|
|
22312
|
+
* In standard RAPTOR the improvement bound is simply the per-run earliest
|
|
22313
|
+
* arrival; the `round` argument is ignored.
|
|
22314
|
+
*/
|
|
22315
|
+
improvementBound(_round, stop) {
|
|
22316
|
+
return this.arrivalTime(stop);
|
|
22317
|
+
}
|
|
22318
|
+
/** No-op in standard RAPTOR — there are no shared cross-run labels to propagate. */
|
|
22319
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
22320
|
+
initRound(_round) { }
|
|
21990
22321
|
/**
|
|
21991
22322
|
* Records a new earliest arrival at a stop.
|
|
21992
|
-
|
|
21993
22323
|
*
|
|
21994
22324
|
* @param stop The stop that was reached.
|
|
21995
22325
|
* @param time The arrival time in minutes from midnight.
|
|
21996
22326
|
* @param leg The round number (number of transit legs taken so far).
|
|
21997
22327
|
*/
|
|
21998
22328
|
updateArrival(stop, time, leg) {
|
|
22329
|
+
this.reachedStops.push(stop);
|
|
21999
22330
|
this.earliestArrivalTimes[stop] = time;
|
|
22000
22331
|
this.earliestArrivalLegs[stop] = leg;
|
|
22332
|
+
if (this.destinationSet.has(stop) && time < this._destinationBest) {
|
|
22333
|
+
this._destinationBest = time;
|
|
22334
|
+
}
|
|
22335
|
+
}
|
|
22336
|
+
/**
|
|
22337
|
+
* Resets this state for a new departure-time iteration **without
|
|
22338
|
+
* reallocating** the underlying arrays.
|
|
22339
|
+
*
|
|
22340
|
+
* Only the stops recorded in {@link reachedStops} are touched — all other
|
|
22341
|
+
* entries are already at their initial bound values.
|
|
22342
|
+
*
|
|
22343
|
+
* After this call the state is equivalent to a freshly constructed
|
|
22344
|
+
* {@link RoutingState} for the given `depTime` and `accessPaths`.
|
|
22345
|
+
*
|
|
22346
|
+
* @param depTime New origin departure time.
|
|
22347
|
+
* @param accessPaths Access legs for this departure-time slot.
|
|
22348
|
+
*/
|
|
22349
|
+
resetFor(depTime, accessPaths) {
|
|
22350
|
+
for (const stop of this.reachedStops) {
|
|
22351
|
+
this.earliestArrivalTimes[stop] = UNREACHED_TIME;
|
|
22352
|
+
this.earliestArrivalLegs[stop] = 0;
|
|
22353
|
+
for (let r = 0; r < this.graph.length; r++) {
|
|
22354
|
+
this.graph[r][stop] = undefined;
|
|
22355
|
+
}
|
|
22356
|
+
}
|
|
22357
|
+
this.reachedStops.length = 0;
|
|
22358
|
+
this._destinationBest = UNREACHED_TIME;
|
|
22359
|
+
this.seedAccessPaths(depTime, accessPaths);
|
|
22001
22360
|
}
|
|
22002
22361
|
/**
|
|
22003
22362
|
* Iterates over every stop that has been reached, yielding its stop ID,
|
|
@@ -22024,6 +22383,15 @@ class RoutingState {
|
|
|
22024
22383
|
}
|
|
22025
22384
|
}
|
|
22026
22385
|
}
|
|
22386
|
+
/**
|
|
22387
|
+
* Finds the earliest arrival time at any stop from a given set of destinations.
|
|
22388
|
+
*
|
|
22389
|
+
* @param routingState The routing state containing arrival times and destinations.
|
|
22390
|
+
* @returns The earliest arrival time among the provided destinations.
|
|
22391
|
+
*/
|
|
22392
|
+
earliestArrivalAtAnyDestination() {
|
|
22393
|
+
return this._destinationBest;
|
|
22394
|
+
}
|
|
22027
22395
|
/**
|
|
22028
22396
|
* Returns the earliest arrival at a stop as an {@link Arrival} object,
|
|
22029
22397
|
* or undefined if the stop has not been reached.
|
|
@@ -22034,6 +22402,13 @@ class RoutingState {
|
|
|
22034
22402
|
return undefined;
|
|
22035
22403
|
return { arrival: time, legNumber: this.earliestArrivalLegs[stop] };
|
|
22036
22404
|
}
|
|
22405
|
+
/**
|
|
22406
|
+
* Returns `true` if `stop` is one of the query's destination stops.
|
|
22407
|
+
* O(1) — backed by a `Set` built at construction time.
|
|
22408
|
+
*/
|
|
22409
|
+
isDestination(stop) {
|
|
22410
|
+
return this.destinationSet.has(stop);
|
|
22411
|
+
}
|
|
22037
22412
|
/**
|
|
22038
22413
|
* Creates a {@link RoutingState} from fully-specified raw data.
|
|
22039
22414
|
*
|
|
@@ -22052,7 +22427,11 @@ class RoutingState {
|
|
|
22052
22427
|
* @internal For use in tests only.
|
|
22053
22428
|
*/
|
|
22054
22429
|
static fromTestData({ nbStops, origins = [], destinations = [], arrivals = [], graph = [], }) {
|
|
22055
|
-
const state = new RoutingState(
|
|
22430
|
+
const state = new RoutingState(0, destinations, origins.map((stop) => ({
|
|
22431
|
+
fromStopId: stop,
|
|
22432
|
+
toStopId: stop,
|
|
22433
|
+
duration: 0,
|
|
22434
|
+
})), nbStops);
|
|
22056
22435
|
// Replace the arrival arrays with freshly built ones so the constructor's
|
|
22057
22436
|
// origin-seeding doesn't bleed into the test state.
|
|
22058
22437
|
const earliestArrivalTimes = new Uint16Array(nbStops).fill(UNREACHED_TIME);
|
|
@@ -22063,6 +22442,14 @@ class RoutingState {
|
|
|
22063
22442
|
}
|
|
22064
22443
|
state.earliestArrivalTimes = earliestArrivalTimes;
|
|
22065
22444
|
state.earliestArrivalLegs = earliestArrivalLegs;
|
|
22445
|
+
// Recompute _destinationBest from the test data since we bypassed updateArrival.
|
|
22446
|
+
// fromTestData is a static method of RoutingState, so private access is allowed.
|
|
22447
|
+
state._destinationBest = UNREACHED_TIME;
|
|
22448
|
+
for (const dest of destinations) {
|
|
22449
|
+
const t = earliestArrivalTimes[dest];
|
|
22450
|
+
if (t !== undefined && t < state._destinationBest)
|
|
22451
|
+
state._destinationBest = t;
|
|
22452
|
+
}
|
|
22066
22453
|
// Convert the sparse per-round representation to dense arrays and replace
|
|
22067
22454
|
// the graph in-place.
|
|
22068
22455
|
const denseRounds = graph.map((round) => {
|
|
@@ -22077,120 +22464,531 @@ class RoutingState {
|
|
|
22077
22464
|
}
|
|
22078
22465
|
}
|
|
22079
22466
|
|
|
22080
|
-
|
|
22081
|
-
|
|
22082
|
-
* For more information on the RAPTOR algorithm,
|
|
22083
|
-
* refer to its detailed explanation in the research paper:
|
|
22084
|
-
* https://www.microsoft.com/en-us/research/wp-content/uploads/2012/01/raptor_alenex.pdf
|
|
22085
|
-
*/
|
|
22086
|
-
class Router {
|
|
22087
|
-
constructor(timetable, stopsIndex) {
|
|
22467
|
+
class PlainRouter {
|
|
22468
|
+
constructor(timetable, stopsIndex, accessFinder, raptor) {
|
|
22088
22469
|
this.timetable = timetable;
|
|
22089
22470
|
this.stopsIndex = stopsIndex;
|
|
22471
|
+
this.accessFinder = accessFinder;
|
|
22472
|
+
this.raptor = raptor;
|
|
22090
22473
|
}
|
|
22091
22474
|
/**
|
|
22092
|
-
*
|
|
22475
|
+
* Standard RAPTOR: finds the earliest-arrival journey from `query.from` to
|
|
22476
|
+
* `query.to` for the given departure time.
|
|
22093
22477
|
*
|
|
22094
|
-
* @param query The query
|
|
22095
|
-
* @returns A
|
|
22478
|
+
* @param query The routing query.
|
|
22479
|
+
* @returns A {@link Result} that can reconstruct the best route and arrival times.
|
|
22096
22480
|
*/
|
|
22097
22481
|
route(query) {
|
|
22098
|
-
const
|
|
22099
|
-
const
|
|
22100
|
-
|
|
22101
|
-
|
|
22102
|
-
|
|
22103
|
-
|
|
22104
|
-
|
|
22105
|
-
|
|
22106
|
-
|
|
22107
|
-
|
|
22108
|
-
|
|
22109
|
-
|
|
22110
|
-
|
|
22111
|
-
|
|
22112
|
-
|
|
22113
|
-
|
|
22114
|
-
|
|
22115
|
-
|
|
22116
|
-
|
|
22117
|
-
|
|
22118
|
-
|
|
22119
|
-
|
|
22120
|
-
|
|
22121
|
-
|
|
22122
|
-
|
|
22123
|
-
|
|
22124
|
-
|
|
22125
|
-
|
|
22126
|
-
|
|
22127
|
-
|
|
22128
|
-
|
|
22129
|
-
|
|
22130
|
-
|
|
22131
|
-
|
|
22132
|
-
|
|
22133
|
-
|
|
22134
|
-
|
|
22135
|
-
|
|
22136
|
-
|
|
22137
|
-
|
|
22138
|
-
|
|
22139
|
-
|
|
22140
|
-
}
|
|
22141
|
-
return new Result(query, routingState, this.stopsIndex, this.timetable);
|
|
22482
|
+
const accessLegs = this.accessFinder.collectAccessPaths(query.from, query.options.minTransferTime);
|
|
22483
|
+
const destinations = Array.from(query.to)
|
|
22484
|
+
.flatMap((destination) => this.stopsIndex.equivalentStops(destination))
|
|
22485
|
+
.map((destination) => destination.id);
|
|
22486
|
+
const routingState = new RoutingState(query.departureTime, destinations, accessLegs, this.timetable.nbStops(), query.options.maxTransfers + 1);
|
|
22487
|
+
this.raptor.run(query.options, routingState);
|
|
22488
|
+
return new Result(new Set(destinations), routingState, this.stopsIndex, this.timetable);
|
|
22489
|
+
}
|
|
22490
|
+
}
|
|
22491
|
+
|
|
22492
|
+
/**
|
|
22493
|
+
* The result of a Range RAPTOR query.
|
|
22494
|
+
*
|
|
22495
|
+
* Contains the complete Pareto-optimal set of journeys for a resolved
|
|
22496
|
+
* destination set.
|
|
22497
|
+
*
|
|
22498
|
+
* **Pareto dominance**: journey J1 dominates J2 iff
|
|
22499
|
+
* `τdep(J1) ≥ τdep(J2) AND τarr(J1) ≤ τarr(J2)`
|
|
22500
|
+
* (with at least one strict inequality).
|
|
22501
|
+
*
|
|
22502
|
+
* Runs are ordered **latest-departure-first**: each successive run departs
|
|
22503
|
+
* strictly earlier *and* arrives strictly earlier than the previous one,
|
|
22504
|
+
* forming the classic staircase Pareto frontier.
|
|
22505
|
+
*
|
|
22506
|
+
* Destination handling is delegated to {@link Result}, which expands
|
|
22507
|
+
* equivalent stops when reconstructing routes or looking up arrivals.
|
|
22508
|
+
*/
|
|
22509
|
+
class RangeResult {
|
|
22510
|
+
constructor(runs, destinations) {
|
|
22511
|
+
this._runs = runs;
|
|
22512
|
+
this._destinations = destinations;
|
|
22513
|
+
}
|
|
22514
|
+
/** The resolved destination stop IDs for this result. */
|
|
22515
|
+
get destinations() {
|
|
22516
|
+
return this._destinations;
|
|
22517
|
+
}
|
|
22518
|
+
normalizeTargets(to) {
|
|
22519
|
+
if (to instanceof Set)
|
|
22520
|
+
return new Set(to);
|
|
22521
|
+
if (to !== undefined)
|
|
22522
|
+
return new Set([to]);
|
|
22523
|
+
return new Set(this._destinations);
|
|
22142
22524
|
}
|
|
22143
22525
|
/**
|
|
22144
|
-
*
|
|
22145
|
-
*
|
|
22146
|
-
*
|
|
22147
|
-
*
|
|
22526
|
+
* Returns all non-dominated routes to this result's default destination set,
|
|
22527
|
+
* ordered from the earliest departure to the latest departure.
|
|
22528
|
+
*
|
|
22529
|
+
* Each route in the list departs strictly earlier *and* arrives strictly
|
|
22530
|
+
* earlier than its predecessor.
|
|
22148
22531
|
*/
|
|
22149
|
-
|
|
22150
|
-
const
|
|
22151
|
-
for (const
|
|
22152
|
-
const
|
|
22153
|
-
if (
|
|
22154
|
-
|
|
22155
|
-
const continuousTrips = this.timetable.getContinuousTrips(arrival.hopOffStopIndex, arrival.routeId, arrival.tripIndex);
|
|
22156
|
-
for (let i = 0; i < continuousTrips.length; i++) {
|
|
22157
|
-
const trip = continuousTrips[i];
|
|
22158
|
-
continuations.push({
|
|
22159
|
-
routeId: trip.routeId,
|
|
22160
|
-
stopIndex: trip.stopIndex,
|
|
22161
|
-
tripIndex: trip.tripIndex,
|
|
22162
|
-
previousEdge: arrival,
|
|
22163
|
-
});
|
|
22164
|
-
}
|
|
22532
|
+
getRoutes() {
|
|
22533
|
+
const routes = [];
|
|
22534
|
+
for (const { result } of this._runs) {
|
|
22535
|
+
const route = result.bestRoute();
|
|
22536
|
+
if (route !== undefined)
|
|
22537
|
+
routes.push(route);
|
|
22165
22538
|
}
|
|
22166
|
-
return
|
|
22539
|
+
return routes.reverse();
|
|
22167
22540
|
}
|
|
22168
22541
|
/**
|
|
22169
|
-
*
|
|
22542
|
+
* The route that arrives **earliest** at the given stop(s) across all
|
|
22543
|
+
* Pareto-optimal runs.
|
|
22170
22544
|
*
|
|
22171
|
-
*
|
|
22172
|
-
*
|
|
22173
|
-
*
|
|
22545
|
+
* When two runs achieve the same arrival time at the target, the one with
|
|
22546
|
+
* the **later departure** is preferred — you wait at the origin rather than
|
|
22547
|
+
* at a transit stop.
|
|
22174
22548
|
*
|
|
22175
|
-
*
|
|
22176
|
-
*
|
|
22549
|
+
* Defaults to this result's own destination stop(s) when `to` is omitted.
|
|
22550
|
+
*
|
|
22551
|
+
* @param to Optional destination stop ID or set of stop IDs.
|
|
22552
|
+
* @returns The reconstructed {@link Route} with the earliest arrival,
|
|
22553
|
+
* or `undefined` if the target is unreachable in every run.
|
|
22177
22554
|
*/
|
|
22178
|
-
|
|
22179
|
-
const
|
|
22180
|
-
|
|
22181
|
-
|
|
22182
|
-
|
|
22183
|
-
|
|
22184
|
-
|
|
22185
|
-
|
|
22186
|
-
|
|
22187
|
-
|
|
22188
|
-
|
|
22555
|
+
bestRoute(to) {
|
|
22556
|
+
const targetStops = this.normalizeTargets(to);
|
|
22557
|
+
let bestRun;
|
|
22558
|
+
let bestArrival;
|
|
22559
|
+
for (const run of this._runs) {
|
|
22560
|
+
for (const stopId of targetStops) {
|
|
22561
|
+
const arrival = run.result.arrivalAt(stopId);
|
|
22562
|
+
if (arrival === undefined)
|
|
22563
|
+
continue;
|
|
22564
|
+
if (bestArrival === undefined || arrival.arrival < bestArrival) {
|
|
22565
|
+
bestArrival = arrival.arrival;
|
|
22566
|
+
bestRun = run;
|
|
22567
|
+
}
|
|
22568
|
+
}
|
|
22569
|
+
}
|
|
22570
|
+
return bestRun === null || bestRun === void 0 ? void 0 : bestRun.result.bestRoute(targetStops);
|
|
22189
22571
|
}
|
|
22190
22572
|
/**
|
|
22191
|
-
*
|
|
22573
|
+
* The route with the **latest possible departure** from the origin among all
|
|
22574
|
+
* Pareto-optimal journeys in the window.
|
|
22192
22575
|
*
|
|
22193
|
-
*
|
|
22576
|
+
* This is the journey that lets you leave the origin as late as possible.
|
|
22577
|
+
* It does **not** necessarily achieve the earliest arrival — for that, use
|
|
22578
|
+
* {@link bestRoute}. For the shortest travel duration, use
|
|
22579
|
+
* {@link fastestRoute}.
|
|
22580
|
+
*
|
|
22581
|
+
* Defaults to this result's own destination stop(s) when `to` is omitted.
|
|
22582
|
+
*
|
|
22583
|
+
* @param to Optional destination stop ID or set of stop IDs.
|
|
22584
|
+
* @returns The reconstructed {@link Route} with the latest departure,
|
|
22585
|
+
* or `undefined` if the target is unreachable in every run.
|
|
22586
|
+
*/
|
|
22587
|
+
latestDepartureRoute(to) {
|
|
22588
|
+
const targetStops = this.normalizeTargets(to);
|
|
22589
|
+
for (const { result } of this._runs) {
|
|
22590
|
+
const route = result.bestRoute(targetStops);
|
|
22591
|
+
if (route !== undefined)
|
|
22592
|
+
return route;
|
|
22593
|
+
}
|
|
22594
|
+
return undefined;
|
|
22595
|
+
}
|
|
22596
|
+
/**
|
|
22597
|
+
* Reconstructs the **fastest** route to the given stop(s) — the journey with
|
|
22598
|
+
* the shortest travel duration (arrival time − origin departure time) across
|
|
22599
|
+
* all Pareto-optimal runs.
|
|
22600
|
+
*
|
|
22601
|
+
* Unlike {@link bestRoute}, which returns the route that departs as late as
|
|
22602
|
+
* possible while still arriving early, this method minimizes total time
|
|
22603
|
+
* spent traveling.
|
|
22604
|
+
*
|
|
22605
|
+
* Defaults to this result's own destination stop(s) when `to` is omitted.
|
|
22606
|
+
*
|
|
22607
|
+
* @param to Optional destination stop ID or set of stop IDs.
|
|
22608
|
+
* @returns The reconstructed fastest {@link Route}, or `undefined` if the
|
|
22609
|
+
* target is unreachable in every run.
|
|
22610
|
+
*/
|
|
22611
|
+
fastestRoute(to) {
|
|
22612
|
+
const targetStops = this.normalizeTargets(to);
|
|
22613
|
+
let fastestRun;
|
|
22614
|
+
let shortestDuration = Infinity;
|
|
22615
|
+
for (const run of this._runs) {
|
|
22616
|
+
for (const stopId of targetStops) {
|
|
22617
|
+
const arrival = run.result.arrivalAt(stopId);
|
|
22618
|
+
if (arrival === undefined)
|
|
22619
|
+
continue;
|
|
22620
|
+
const duration = arrival.arrival - run.departureTime;
|
|
22621
|
+
if (duration < shortestDuration) {
|
|
22622
|
+
shortestDuration = duration;
|
|
22623
|
+
fastestRun = run;
|
|
22624
|
+
}
|
|
22625
|
+
}
|
|
22626
|
+
}
|
|
22627
|
+
return fastestRun === null || fastestRun === void 0 ? void 0 : fastestRun.result.bestRoute(targetStops);
|
|
22628
|
+
}
|
|
22629
|
+
/** Number of Pareto-optimal journeys found. */
|
|
22630
|
+
get size() {
|
|
22631
|
+
return this._runs.length;
|
|
22632
|
+
}
|
|
22633
|
+
/**
|
|
22634
|
+
* Earliest achievable arrival at a stop across all Pareto-optimal runs.
|
|
22635
|
+
*
|
|
22636
|
+
* Useful for isochrone / accessibility analysis: given this result's
|
|
22637
|
+
* departure-time frontier, how early can you reach stop `s` regardless of
|
|
22638
|
+
* which specific trip you take?
|
|
22639
|
+
*
|
|
22640
|
+
* Equivalent stops are handled by {@link Result.arrivalAt}.
|
|
22641
|
+
*
|
|
22642
|
+
* @param stop The target stop ID.
|
|
22643
|
+
* @param maxTransfers Optional upper bound on the number of transfers.
|
|
22644
|
+
*/
|
|
22645
|
+
earliestArrivalAt(stop, maxTransfers) {
|
|
22646
|
+
let best;
|
|
22647
|
+
for (const { result } of this._runs) {
|
|
22648
|
+
const arrival = result.arrivalAt(stop, maxTransfers);
|
|
22649
|
+
if (arrival !== undefined &&
|
|
22650
|
+
(best === undefined || arrival.arrival < best.arrival)) {
|
|
22651
|
+
best = arrival;
|
|
22652
|
+
}
|
|
22653
|
+
}
|
|
22654
|
+
return best;
|
|
22655
|
+
}
|
|
22656
|
+
/**
|
|
22657
|
+
* Shortest travel duration to reach a stop across all Pareto-optimal runs.
|
|
22658
|
+
*
|
|
22659
|
+
* For each run, duration is measured from the run's origin departure time to
|
|
22660
|
+
* the earliest arrival at `stop` within that run. The minimum across all
|
|
22661
|
+
* runs is returned.
|
|
22662
|
+
*
|
|
22663
|
+
* Equivalent stops are handled by {@link Result.arrivalAt}.
|
|
22664
|
+
*
|
|
22665
|
+
* Duration is **not** monotone along the Pareto frontier — a run that
|
|
22666
|
+
* departs later may still travel faster — so every run is checked. In
|
|
22667
|
+
* practice the Pareto frontier is small, so this is O(runs).
|
|
22668
|
+
*
|
|
22669
|
+
* Returns `undefined` if `stop` is unreachable in every run.
|
|
22670
|
+
*
|
|
22671
|
+
* @param stop The target stop ID.
|
|
22672
|
+
* @param maxTransfers Optional upper bound on the number of transfers.
|
|
22673
|
+
*/
|
|
22674
|
+
shortestDurationTo(stop, maxTransfers) {
|
|
22675
|
+
let shortest;
|
|
22676
|
+
for (const { departureTime, result } of this._runs) {
|
|
22677
|
+
const arrival = result.arrivalAt(stop, maxTransfers);
|
|
22678
|
+
if (arrival === undefined)
|
|
22679
|
+
continue;
|
|
22680
|
+
const duration = arrival.arrival - departureTime;
|
|
22681
|
+
if (shortest === undefined || duration < shortest.duration) {
|
|
22682
|
+
shortest = Object.assign(Object.assign({}, arrival), { duration });
|
|
22683
|
+
}
|
|
22684
|
+
}
|
|
22685
|
+
return shortest;
|
|
22686
|
+
}
|
|
22687
|
+
/**
|
|
22688
|
+
* Shortest travel duration to **every reachable stop** across all
|
|
22689
|
+
* Pareto-optimal runs, as a single `Map<StopId, DurationArrival>`.
|
|
22690
|
+
*/
|
|
22691
|
+
allShortestDurations() {
|
|
22692
|
+
const durations = new Map();
|
|
22693
|
+
for (const { departureTime, result } of this._runs) {
|
|
22694
|
+
for (const { stop, arrival, legNumber, } of result.routingState.arrivals()) {
|
|
22695
|
+
const duration = arrival - departureTime;
|
|
22696
|
+
const existing = durations.get(stop);
|
|
22697
|
+
if (existing === undefined || duration < existing.duration) {
|
|
22698
|
+
durations.set(stop, { arrival, legNumber, duration });
|
|
22699
|
+
}
|
|
22700
|
+
}
|
|
22701
|
+
}
|
|
22702
|
+
return durations;
|
|
22703
|
+
}
|
|
22704
|
+
/**
|
|
22705
|
+
* Earliest achievable arrival at **every reachable stop** across all
|
|
22706
|
+
* Pareto-optimal runs, as a single `Map<StopId, Arrival>`.
|
|
22707
|
+
*/
|
|
22708
|
+
allEarliestArrivals() {
|
|
22709
|
+
const arrivals = new Map();
|
|
22710
|
+
for (const { result } of this._runs) {
|
|
22711
|
+
for (const { stop, arrival, legNumber, } of result.routingState.arrivals()) {
|
|
22712
|
+
const existing = arrivals.get(stop);
|
|
22713
|
+
if (existing === undefined || arrival < existing.arrival) {
|
|
22714
|
+
arrivals.set(stop, { arrival, legNumber });
|
|
22715
|
+
}
|
|
22716
|
+
}
|
|
22717
|
+
}
|
|
22718
|
+
return arrivals;
|
|
22719
|
+
}
|
|
22720
|
+
/**
|
|
22721
|
+
* Iterates over all Pareto-optimal `(departureTime, result)` pairs,
|
|
22722
|
+
* ordered from the latest departure to the earliest departure.
|
|
22723
|
+
*/
|
|
22724
|
+
[Symbol.iterator]() {
|
|
22725
|
+
return this._runs[Symbol.iterator]();
|
|
22726
|
+
}
|
|
22727
|
+
}
|
|
22728
|
+
|
|
22729
|
+
/**
|
|
22730
|
+
* RAPTOR state for Range RAPTOR mode, implementing {@link IRaptorState}.
|
|
22731
|
+
*
|
|
22732
|
+
* Holds both the cross-run shared labels (carried over from one departure-time
|
|
22733
|
+
* iteration to the next, latest → earliest) and a reference to the current
|
|
22734
|
+
* per-iteration {@link RoutingState} (swapped via {@link setCurrentRun}).
|
|
22735
|
+
*
|
|
22736
|
+
* Concretely, `roundLabels[k][p]` is the best known arrival at stop `p` using
|
|
22737
|
+
* at most `k` transit legs, across **all departure times tried so far**.
|
|
22738
|
+
*
|
|
22739
|
+
* @see https://www.microsoft.com/en-us/research/wp-content/uploads/2012/01/raptor_alenex.pdf
|
|
22740
|
+
*/
|
|
22741
|
+
class RangeRaptorState {
|
|
22742
|
+
constructor(maxRounds, nbStops, latestDeparture) {
|
|
22743
|
+
/**
|
|
22744
|
+
* Global best arrival at any destination stop across all runs and rounds.
|
|
22745
|
+
* Used for destination-pruning inside scan methods so that routes that cannot
|
|
22746
|
+
* beat the already-known best are skipped early.
|
|
22747
|
+
*/
|
|
22748
|
+
this._destinationBest = UNREACHED_TIME;
|
|
22749
|
+
this.latestDeparture = latestDeparture;
|
|
22750
|
+
// maxRounds + 2: index 0 = origin/walk legs, indices 1…maxRounds+1 = transit rounds
|
|
22751
|
+
this.roundLabels = Array.from({ length: maxRounds + 2 }, () => new Uint16Array(nbStops).fill(UNREACHED_TIME));
|
|
22752
|
+
this.changedInRound = Array.from({ length: maxRounds + 2 }, () => []);
|
|
22753
|
+
}
|
|
22754
|
+
/**
|
|
22755
|
+
* Swaps in a fresh {@link RoutingState} for the next departure-time iteration
|
|
22756
|
+
* and seeds the shared round-0 labels from its access arrivals.
|
|
22757
|
+
*
|
|
22758
|
+
* Must be called before every `runRaptor` invocation.
|
|
22759
|
+
*/
|
|
22760
|
+
setCurrentRun(routingState) {
|
|
22761
|
+
this.currentRun = routingState;
|
|
22762
|
+
// Propagate round-0 access arrivals into the shared labels so that
|
|
22763
|
+
// initRound(1) can tighten round-1 pruning bounds correctly.
|
|
22764
|
+
const round0 = routingState.graph[0];
|
|
22765
|
+
for (const stop of routingState.origins) {
|
|
22766
|
+
const edge = round0[stop];
|
|
22767
|
+
if (!edge)
|
|
22768
|
+
continue;
|
|
22769
|
+
this.updateArrival(stop, edge.arrival, 0);
|
|
22770
|
+
}
|
|
22771
|
+
}
|
|
22772
|
+
get origins() {
|
|
22773
|
+
return this.currentRun.origins;
|
|
22774
|
+
}
|
|
22775
|
+
get graph() {
|
|
22776
|
+
return this.currentRun.graph;
|
|
22777
|
+
}
|
|
22778
|
+
arrivalTime(stop) {
|
|
22779
|
+
return this.currentRun.arrivalTime(stop);
|
|
22780
|
+
}
|
|
22781
|
+
/**
|
|
22782
|
+
* Uses the cross-run shared label for `round`, which is always at least as
|
|
22783
|
+
* tight as the per-run arrival and therefore provides stronger pruning.
|
|
22784
|
+
*/
|
|
22785
|
+
improvementBound(round, stop) {
|
|
22786
|
+
return this.roundLabels[round][stop];
|
|
22787
|
+
}
|
|
22788
|
+
/**
|
|
22789
|
+
* Global best arrival at any destination across all departure-time iterations.
|
|
22790
|
+
* Always at least as tight as the per-run `destinationBest`.
|
|
22791
|
+
*/
|
|
22792
|
+
get destinationBest() {
|
|
22793
|
+
return this._destinationBest;
|
|
22794
|
+
}
|
|
22795
|
+
isDestination(stop) {
|
|
22796
|
+
return this.currentRun.isDestination(stop);
|
|
22797
|
+
}
|
|
22798
|
+
/** Updates both the per-run state and the cross-run shared labels. */
|
|
22799
|
+
updateArrival(stop, time, round) {
|
|
22800
|
+
this.currentRun.updateArrival(stop, time, round);
|
|
22801
|
+
if (time < this.roundLabels[round][stop]) {
|
|
22802
|
+
this.roundLabels[round][stop] = time;
|
|
22803
|
+
this.changedInRound[round].push(stop);
|
|
22804
|
+
if (this.currentRun.isDestination(stop) && time < this._destinationBest) {
|
|
22805
|
+
this._destinationBest = time;
|
|
22806
|
+
}
|
|
22807
|
+
}
|
|
22808
|
+
}
|
|
22809
|
+
/**
|
|
22810
|
+
* initialized round `k` from round `k-1`: τk(p) ← min(τk(p), τk-1(p)).
|
|
22811
|
+
*
|
|
22812
|
+
* Must be called at the very start of each RAPTOR round before routes are
|
|
22813
|
+
* scanned. After this call, `roundLabels[k][p]` is the minimum arrival at
|
|
22814
|
+
* stop `p` achievable with **at most** k transit legs from any departure time
|
|
22815
|
+
* tried so far — which is exactly the tightest valid pruning bound for round k.
|
|
22816
|
+
*/
|
|
22817
|
+
initRound(round) {
|
|
22818
|
+
const changed = this.changedInRound[round - 1];
|
|
22819
|
+
if (changed.length === 0)
|
|
22820
|
+
return;
|
|
22821
|
+
const prev = this.roundLabels[round - 1];
|
|
22822
|
+
const curr = this.roundLabels[round];
|
|
22823
|
+
for (let i = 0; i < changed.length; i++) {
|
|
22824
|
+
const stop = changed[i];
|
|
22825
|
+
if (prev[stop] < curr[stop]) {
|
|
22826
|
+
curr[stop] = prev[stop];
|
|
22827
|
+
}
|
|
22828
|
+
}
|
|
22829
|
+
changed.length = 0;
|
|
22830
|
+
}
|
|
22831
|
+
}
|
|
22832
|
+
|
|
22833
|
+
class RangeRouter {
|
|
22834
|
+
constructor(timetable, stopsIndex, accessFinder, raptor) {
|
|
22835
|
+
this.timetable = timetable;
|
|
22836
|
+
this.stopsIndex = stopsIndex;
|
|
22837
|
+
this.accessFinder = accessFinder;
|
|
22838
|
+
this.raptor = raptor;
|
|
22839
|
+
}
|
|
22840
|
+
/**
|
|
22841
|
+
* Range RAPTOR: finds all Pareto-optimal journeys within the departure-time
|
|
22842
|
+
* window `[query.departureTime, query.lastDepartureTime]`.
|
|
22843
|
+
*
|
|
22844
|
+
* A journey is Pareto-optimal iff no journey departing no earlier arrives no
|
|
22845
|
+
* later. Runs are ordered latest-departure-first in the returned result.
|
|
22846
|
+
*
|
|
22847
|
+
* @param query A {@link RangeQuery} with both `departureTime` and `lastDepartureTime` set.
|
|
22848
|
+
* @returns A {@link RangeResult} exposing the full Pareto frontier.
|
|
22849
|
+
*/
|
|
22850
|
+
rangeRoute(query) {
|
|
22851
|
+
var _a, _b;
|
|
22852
|
+
const { departureTime: earliest, lastDepartureTime: latest } = query;
|
|
22853
|
+
const destinations = Array.from(query.to)
|
|
22854
|
+
.flatMap((destination) => this.stopsIndex.equivalentStops(destination))
|
|
22855
|
+
.map((destination) => destination.id);
|
|
22856
|
+
const accessLegs = this.accessFinder.collectAccessPaths(query.from, query.options.minTransferTime);
|
|
22857
|
+
const departureSlots = this.accessFinder.collectDepartureTimes(accessLegs, earliest, latest);
|
|
22858
|
+
if (departureSlots.length === 0) {
|
|
22859
|
+
return new RangeResult([], new Set(destinations));
|
|
22860
|
+
}
|
|
22861
|
+
const maxRounds = query.options.maxTransfers + 1;
|
|
22862
|
+
const rangeState = new RangeRaptorState(maxRounds, this.timetable.nbStops(), latest);
|
|
22863
|
+
const paretoRuns = [];
|
|
22864
|
+
const paretoDestBest = new Map();
|
|
22865
|
+
for (const dest of destinations) {
|
|
22866
|
+
paretoDestBest.set(dest, UNREACHED_TIME);
|
|
22867
|
+
}
|
|
22868
|
+
const trivialDests = new Set(accessLegs
|
|
22869
|
+
.map((leg) => leg.toStopId)
|
|
22870
|
+
.filter((id) => destinations.includes(id)));
|
|
22871
|
+
const trivialDestCovered = new Set();
|
|
22872
|
+
let routingState = null;
|
|
22873
|
+
if (query.rangeOptions.optimizeBeyondLatestDeparture) {
|
|
22874
|
+
routingState = new RoutingState(latest + 1, destinations, accessLegs, this.timetable.nbStops(), maxRounds);
|
|
22875
|
+
rangeState.setCurrentRun(routingState);
|
|
22876
|
+
this.raptor.run(Object.assign(Object.assign({}, query.options), { maxInitialWaitingTime: undefined }), rangeState);
|
|
22877
|
+
for (const dest of destinations) {
|
|
22878
|
+
const t = routingState.arrivalTime(dest);
|
|
22879
|
+
if (t < ((_a = paretoDestBest.get(dest)) !== null && _a !== void 0 ? _a : UNREACHED_TIME))
|
|
22880
|
+
paretoDestBest.set(dest, t);
|
|
22881
|
+
}
|
|
22882
|
+
}
|
|
22883
|
+
for (const { depTime, legs } of departureSlots) {
|
|
22884
|
+
if (trivialDestCovered.size === destinations.length)
|
|
22885
|
+
break;
|
|
22886
|
+
if (routingState === null) {
|
|
22887
|
+
routingState = new RoutingState(depTime, destinations, legs, this.timetable.nbStops(), maxRounds);
|
|
22888
|
+
}
|
|
22889
|
+
else {
|
|
22890
|
+
routingState.resetFor(depTime, legs);
|
|
22891
|
+
}
|
|
22892
|
+
rangeState.setCurrentRun(routingState);
|
|
22893
|
+
this.raptor.run(Object.assign(Object.assign({}, query.options), { maxInitialWaitingTime: 0 }), rangeState);
|
|
22894
|
+
let isParetoOptimal = false;
|
|
22895
|
+
for (const dest of destinations) {
|
|
22896
|
+
const arrival = routingState.arrivalTime(dest);
|
|
22897
|
+
if (arrival >= ((_b = paretoDestBest.get(dest)) !== null && _b !== void 0 ? _b : UNREACHED_TIME)) {
|
|
22898
|
+
continue;
|
|
22899
|
+
}
|
|
22900
|
+
if (trivialDests.has(dest) && trivialDestCovered.has(dest)) {
|
|
22901
|
+
paretoDestBest.set(dest, arrival);
|
|
22902
|
+
continue;
|
|
22903
|
+
}
|
|
22904
|
+
paretoDestBest.set(dest, arrival);
|
|
22905
|
+
if (trivialDests.has(dest)) {
|
|
22906
|
+
trivialDestCovered.add(dest);
|
|
22907
|
+
}
|
|
22908
|
+
isParetoOptimal = true;
|
|
22909
|
+
}
|
|
22910
|
+
if (isParetoOptimal) {
|
|
22911
|
+
paretoRuns.push({
|
|
22912
|
+
departureTime: depTime,
|
|
22913
|
+
result: new Result(new Set(destinations), routingState, this.stopsIndex, this.timetable),
|
|
22914
|
+
});
|
|
22915
|
+
routingState = null;
|
|
22916
|
+
}
|
|
22917
|
+
}
|
|
22918
|
+
return new RangeResult(paretoRuns, new Set(destinations));
|
|
22919
|
+
}
|
|
22920
|
+
}
|
|
22921
|
+
|
|
22922
|
+
/**
|
|
22923
|
+
* Encapsulates the core RAPTOR algorithm, operating on a {@link Timetable} and
|
|
22924
|
+
* an {@link IRaptorState} provided by the caller.
|
|
22925
|
+
*
|
|
22926
|
+
* @see https://www.microsoft.com/en-us/research/wp-content/uploads/2012/01/raptor_alenex.pdf
|
|
22927
|
+
*/
|
|
22928
|
+
class Raptor {
|
|
22929
|
+
constructor(timetable) {
|
|
22930
|
+
this.timetable = timetable;
|
|
22931
|
+
}
|
|
22932
|
+
run(options, state) {
|
|
22933
|
+
const markedStops = new Set(state.origins);
|
|
22934
|
+
for (let round = 1; round <= options.maxTransfers + 1; round++) {
|
|
22935
|
+
state.initRound(round);
|
|
22936
|
+
const edgesAtCurrentRound = state.graph[round];
|
|
22937
|
+
const reachableRoutes = this.timetable.findReachableRoutes(markedStops, options.transportModes);
|
|
22938
|
+
markedStops.clear();
|
|
22939
|
+
for (const [route, hopOnStopIndex] of reachableRoutes) {
|
|
22940
|
+
for (const stop of this.scanRoute(route, hopOnStopIndex, round, state, options)) {
|
|
22941
|
+
markedStops.add(stop);
|
|
22942
|
+
}
|
|
22943
|
+
}
|
|
22944
|
+
let continuations = this.findTripContinuations(markedStops, edgesAtCurrentRound);
|
|
22945
|
+
const stopsFromContinuations = new Set();
|
|
22946
|
+
while (continuations.length > 0) {
|
|
22947
|
+
stopsFromContinuations.clear();
|
|
22948
|
+
for (const continuation of continuations) {
|
|
22949
|
+
const route = this.timetable.getRoute(continuation.routeId);
|
|
22950
|
+
for (const stop of this.scanRouteContinuation(route, continuation.stopIndex, round, state, continuation)) {
|
|
22951
|
+
stopsFromContinuations.add(stop);
|
|
22952
|
+
markedStops.add(stop);
|
|
22953
|
+
}
|
|
22954
|
+
}
|
|
22955
|
+
continuations = this.findTripContinuations(stopsFromContinuations, edgesAtCurrentRound);
|
|
22956
|
+
}
|
|
22957
|
+
for (const stop of this.considerTransfers(options, round, markedStops, state)) {
|
|
22958
|
+
markedStops.add(stop);
|
|
22959
|
+
}
|
|
22960
|
+
if (markedStops.size === 0)
|
|
22961
|
+
break;
|
|
22962
|
+
}
|
|
22963
|
+
}
|
|
22964
|
+
/**
|
|
22965
|
+
* Finds trip continuations for the given marked stops and edges at the current round.
|
|
22966
|
+
* @param markedStops The set of marked stops.
|
|
22967
|
+
* @param edgesAtCurrentRound The array of edges at the current round, indexed by stop ID.
|
|
22968
|
+
* @returns An array of trip continuations.
|
|
22969
|
+
*/
|
|
22970
|
+
findTripContinuations(markedStops, edgesAtCurrentRound) {
|
|
22971
|
+
const continuations = [];
|
|
22972
|
+
for (const stopId of markedStops) {
|
|
22973
|
+
const arrival = edgesAtCurrentRound[stopId];
|
|
22974
|
+
if (!arrival || !('routeId' in arrival))
|
|
22975
|
+
continue;
|
|
22976
|
+
const continuousTrips = this.timetable.getContinuousTrips(arrival.hopOffStopIndex, arrival.routeId, arrival.tripIndex);
|
|
22977
|
+
for (const trip of continuousTrips) {
|
|
22978
|
+
continuations.push({
|
|
22979
|
+
routeId: trip.routeId,
|
|
22980
|
+
stopIndex: trip.stopIndex,
|
|
22981
|
+
tripIndex: trip.tripIndex,
|
|
22982
|
+
previousEdge: arrival,
|
|
22983
|
+
});
|
|
22984
|
+
}
|
|
22985
|
+
}
|
|
22986
|
+
return continuations;
|
|
22987
|
+
}
|
|
22988
|
+
/**
|
|
22989
|
+
* Scans a route for an in-seat trip continuation.
|
|
22990
|
+
*
|
|
22991
|
+
* The boarded trip and entry stop are fixed, so there is no need to probe for
|
|
22194
22992
|
* earlier boardings.
|
|
22195
22993
|
*
|
|
22196
22994
|
* @param route The route to scan
|
|
@@ -22198,11 +22996,11 @@ class Router {
|
|
|
22198
22996
|
* @param round The current RAPTOR round
|
|
22199
22997
|
* @param routingState Current routing state
|
|
22200
22998
|
* @param tripContinuation The in-seat continuation descriptor
|
|
22999
|
+
* @param shared Optional shared state for Range RAPTOR mode
|
|
22201
23000
|
*/
|
|
22202
|
-
scanRouteContinuation(route, hopOnStopIndex, round,
|
|
23001
|
+
scanRouteContinuation(route, hopOnStopIndex, round, state, tripContinuation) {
|
|
22203
23002
|
const newlyMarkedStops = new Set();
|
|
22204
|
-
const edgesAtCurrentRound =
|
|
22205
|
-
const earliestArrivalAtAnyDestination = this.earliestArrivalAtAnyStop(routingState);
|
|
23003
|
+
const edgesAtCurrentRound = state.graph[round];
|
|
22206
23004
|
const nbStops = route.getNbStops();
|
|
22207
23005
|
const routeId = route.id;
|
|
22208
23006
|
const tripIndex = tripContinuation.tripIndex;
|
|
@@ -22212,10 +23010,9 @@ class Router {
|
|
|
22212
23010
|
const currentStop = route.stops[currentStopIndex];
|
|
22213
23011
|
const arrivalTime = route.arrivalAtOffset(currentStopIndex, tripStopOffset);
|
|
22214
23012
|
const dropOffType = route.dropOffTypeAtOffset(currentStopIndex, tripStopOffset);
|
|
22215
|
-
const earliestArrivalAtCurrentStop = routingState.arrivalTime(currentStop);
|
|
22216
23013
|
if (dropOffType !== NOT_AVAILABLE &&
|
|
22217
|
-
arrivalTime <
|
|
22218
|
-
arrivalTime <
|
|
23014
|
+
arrivalTime < state.improvementBound(round, currentStop) &&
|
|
23015
|
+
arrivalTime < state.destinationBest) {
|
|
22219
23016
|
edgesAtCurrentRound[currentStop] = {
|
|
22220
23017
|
routeId,
|
|
22221
23018
|
stopIndex: hopOnStopIndex,
|
|
@@ -22224,7 +23021,7 @@ class Router {
|
|
|
22224
23021
|
hopOffStopIndex: currentStopIndex,
|
|
22225
23022
|
continuationOf: previousEdge,
|
|
22226
23023
|
};
|
|
22227
|
-
|
|
23024
|
+
state.updateArrival(currentStop, arrivalTime, round);
|
|
22228
23025
|
newlyMarkedStops.add(currentStop);
|
|
22229
23026
|
}
|
|
22230
23027
|
}
|
|
@@ -22241,20 +23038,18 @@ class Router {
|
|
|
22241
23038
|
* @param route The route to scan
|
|
22242
23039
|
* @param hopOnStopIndex The stop index where passengers can first board
|
|
22243
23040
|
* @param round The current RAPTOR round
|
|
22244
|
-
* @param
|
|
23041
|
+
* @param state Current routing state
|
|
22245
23042
|
* @param options Query options (minTransferTime, etc.)
|
|
22246
23043
|
*/
|
|
22247
|
-
scanRoute(route, hopOnStopIndex, round,
|
|
23044
|
+
scanRoute(route, hopOnStopIndex, round, state, options) {
|
|
22248
23045
|
const newlyMarkedStops = new Set();
|
|
22249
|
-
const edgesAtCurrentRound =
|
|
22250
|
-
const edgesAtPreviousRound =
|
|
22251
|
-
const earliestArrivalAtAnyDestination = this.earliestArrivalAtAnyStop(routingState);
|
|
23046
|
+
const edgesAtCurrentRound = state.graph[round];
|
|
23047
|
+
const edgesAtPreviousRound = state.graph[round - 1];
|
|
22252
23048
|
const nbStops = route.getNbStops();
|
|
22253
23049
|
const routeId = route.id;
|
|
22254
23050
|
let activeTripIndex;
|
|
22255
23051
|
let activeTripBoardStopIndex = hopOnStopIndex;
|
|
22256
23052
|
// tripStopOffset = activeTripIndex * nbStops, precomputed when the trip changes.
|
|
22257
|
-
// Only valid while activeTripIndex !== undefined.
|
|
22258
23053
|
let activeTripStopOffset = 0;
|
|
22259
23054
|
for (let currentStopIndex = hopOnStopIndex; currentStopIndex < nbStops; currentStopIndex++) {
|
|
22260
23055
|
const currentStop = route.stops[currentStopIndex];
|
|
@@ -22262,10 +23057,9 @@ class Router {
|
|
|
22262
23057
|
if (activeTripIndex !== undefined) {
|
|
22263
23058
|
const arrivalTime = route.arrivalAtOffset(currentStopIndex, activeTripStopOffset);
|
|
22264
23059
|
const dropOffType = route.dropOffTypeAtOffset(currentStopIndex, activeTripStopOffset);
|
|
22265
|
-
const earliestArrivalAtCurrentStop = routingState.arrivalTime(currentStop);
|
|
22266
23060
|
if (dropOffType !== NOT_AVAILABLE &&
|
|
22267
|
-
arrivalTime <
|
|
22268
|
-
arrivalTime <
|
|
23061
|
+
arrivalTime < state.improvementBound(round, currentStop) &&
|
|
23062
|
+
arrivalTime < state.destinationBest) {
|
|
22269
23063
|
edgesAtCurrentRound[currentStop] = {
|
|
22270
23064
|
routeId,
|
|
22271
23065
|
stopIndex: activeTripBoardStopIndex,
|
|
@@ -22273,7 +23067,7 @@ class Router {
|
|
|
22273
23067
|
arrival: arrivalTime,
|
|
22274
23068
|
hopOffStopIndex: currentStopIndex,
|
|
22275
23069
|
};
|
|
22276
|
-
|
|
23070
|
+
state.updateArrival(currentStop, arrivalTime, round);
|
|
22277
23071
|
newlyMarkedStops.add(currentStop);
|
|
22278
23072
|
}
|
|
22279
23073
|
}
|
|
@@ -22283,142 +23077,166 @@ class Router {
|
|
|
22283
23077
|
if (earliestArrivalOnPreviousRound !== undefined &&
|
|
22284
23078
|
(activeTripIndex === undefined ||
|
|
22285
23079
|
earliestArrivalOnPreviousRound <=
|
|
22286
|
-
route.
|
|
23080
|
+
route.departureAtOffset(currentStopIndex, activeTripStopOffset))) {
|
|
22287
23081
|
const earliestTrip = route.findEarliestTrip(currentStopIndex, earliestArrivalOnPreviousRound, activeTripIndex);
|
|
22288
23082
|
if (earliestTrip === undefined) {
|
|
22289
23083
|
continue;
|
|
22290
23084
|
}
|
|
22291
|
-
const
|
|
22292
|
-
|
|
22293
|
-
|
|
23085
|
+
const fromTripStop = previousEdge && 'routeId' in previousEdge
|
|
23086
|
+
? {
|
|
23087
|
+
stopIndex: previousEdge.hopOffStopIndex,
|
|
23088
|
+
routeId: previousEdge.routeId,
|
|
23089
|
+
tripIndex: previousEdge.tripIndex,
|
|
23090
|
+
}
|
|
23091
|
+
: undefined;
|
|
23092
|
+
const firstBoardableTrip = this.timetable.findFirstBoardableTrip(currentStopIndex, route, earliestTrip, earliestArrivalOnPreviousRound, activeTripIndex, fromTripStop, options.minTransferTime);
|
|
22294
23093
|
if (firstBoardableTrip !== undefined) {
|
|
22295
|
-
|
|
22296
|
-
|
|
22297
|
-
|
|
23094
|
+
// At round 1, enforce maxInitialWaitingTime: skip boarding if the
|
|
23095
|
+
// traveler would have to wait longer than the allowed threshold at
|
|
23096
|
+
// the first boarding stop.
|
|
23097
|
+
const exceedsInitialWait = round === 1 &&
|
|
23098
|
+
options.maxInitialWaitingTime !== undefined &&
|
|
23099
|
+
route.departureFrom(currentStopIndex, firstBoardableTrip) -
|
|
23100
|
+
earliestArrivalOnPreviousRound >
|
|
23101
|
+
options.maxInitialWaitingTime;
|
|
23102
|
+
if (!exceedsInitialWait) {
|
|
23103
|
+
activeTripIndex = firstBoardableTrip;
|
|
23104
|
+
activeTripBoardStopIndex = currentStopIndex;
|
|
23105
|
+
activeTripStopOffset = route.tripStopOffset(firstBoardableTrip);
|
|
23106
|
+
}
|
|
22298
23107
|
}
|
|
22299
23108
|
}
|
|
22300
23109
|
}
|
|
22301
23110
|
return newlyMarkedStops;
|
|
22302
23111
|
}
|
|
22303
|
-
/**
|
|
22304
|
-
* Finds the first boardable trip on a route at a given stop that meets transfer requirements.
|
|
22305
|
-
*
|
|
22306
|
-
* This method searches through trips on a route starting from the earliest trip index reachable
|
|
22307
|
-
* from the previous edge to find the first trip that can be effectively boarded,
|
|
22308
|
-
* considering pickup availability, transfer guarantees, and minimum transfer times.
|
|
22309
|
-
*
|
|
22310
|
-
* @param stopIndex The index in the route of the stop where boarding is attempted
|
|
22311
|
-
* @param route The route to search for boardable trips
|
|
22312
|
-
* @param earliestTrip The earliest trip index to start searching from
|
|
22313
|
-
* @param after The earliest time after which boarding can occur
|
|
22314
|
-
* @param beforeTrip Optional upper bound trip index to limit search
|
|
22315
|
-
* @param previousTrip The previous trip taken (for transfer guarantee checks)
|
|
22316
|
-
* @param transferTime Minimum time required for transfers between trips
|
|
22317
|
-
* @returns The trip index of the first boardable trip, or undefined if none found
|
|
22318
|
-
*/
|
|
22319
|
-
findFirstBoardableTrip(stopIndex, route, earliestTrip, after = TIME_ORIGIN, beforeTrip, previousTrip, transferTime = DURATION_ZERO) {
|
|
22320
|
-
const nbTrips = route.getNbTrips();
|
|
22321
|
-
for (let t = earliestTrip; t < (beforeTrip !== null && beforeTrip !== void 0 ? beforeTrip : nbTrips); t++) {
|
|
22322
|
-
const pickup = route.pickUpTypeFrom(stopIndex, t);
|
|
22323
|
-
if (pickup === NOT_AVAILABLE) {
|
|
22324
|
-
continue;
|
|
22325
|
-
}
|
|
22326
|
-
if (previousTrip === undefined) {
|
|
22327
|
-
return t;
|
|
22328
|
-
}
|
|
22329
|
-
const isGuaranteed = this.timetable.isTripTransferGuaranteed({
|
|
22330
|
-
stopIndex: previousTrip.hopOffStopIndex,
|
|
22331
|
-
routeId: previousTrip.routeId,
|
|
22332
|
-
tripIndex: previousTrip.tripIndex,
|
|
22333
|
-
}, { stopIndex, routeId: route.id, tripIndex: t });
|
|
22334
|
-
if (isGuaranteed) {
|
|
22335
|
-
return t;
|
|
22336
|
-
}
|
|
22337
|
-
const departure = route.departureFrom(stopIndex, t);
|
|
22338
|
-
const requiredTime = after + transferTime;
|
|
22339
|
-
if (departure >= requiredTime) {
|
|
22340
|
-
return t;
|
|
22341
|
-
}
|
|
22342
|
-
}
|
|
22343
|
-
return undefined;
|
|
22344
|
-
}
|
|
22345
23112
|
/**
|
|
22346
23113
|
* Processes all currently marked stops to find available transfers
|
|
22347
23114
|
* and determines if using these transfers would result in earlier arrival times
|
|
22348
23115
|
* at destination stops. It handles different transfer types including in-seat
|
|
22349
23116
|
* transfers and walking transfers with appropriate minimum transfer times.
|
|
22350
23117
|
*
|
|
22351
|
-
* @param
|
|
23118
|
+
* @param options Query options (minTransferTime, etc.)
|
|
22352
23119
|
* @param round The current round number in the RAPTOR algorithm
|
|
22353
|
-
* @param
|
|
23120
|
+
* @param markedStops The set of currently marked stops
|
|
23121
|
+
* @param state Current routing state
|
|
22354
23122
|
*/
|
|
22355
|
-
considerTransfers(
|
|
22356
|
-
const { options } = query;
|
|
22357
|
-
const arrivalsAtCurrentRound = routingState.graph[round];
|
|
23123
|
+
considerTransfers(options, round, markedStops, state) {
|
|
22358
23124
|
const newlyMarkedStops = new Set();
|
|
23125
|
+
const arrivalsAtCurrentRound = state.graph[round];
|
|
22359
23126
|
for (const stop of markedStops) {
|
|
22360
23127
|
const currentArrival = arrivalsAtCurrentRound[stop];
|
|
22361
23128
|
// Skip transfers if the last leg was also a transfer
|
|
22362
23129
|
if (!currentArrival || 'type' in currentArrival)
|
|
22363
23130
|
continue;
|
|
22364
23131
|
const transfers = this.timetable.getTransfers(stop);
|
|
22365
|
-
for (
|
|
22366
|
-
const transfer = transfers[j];
|
|
23132
|
+
for (const transfer of transfers) {
|
|
22367
23133
|
let transferTime;
|
|
22368
23134
|
if (transfer.minTransferTime) {
|
|
22369
23135
|
transferTime = transfer.minTransferTime;
|
|
22370
23136
|
}
|
|
22371
23137
|
else if (transfer.type === 'IN_SEAT') {
|
|
22372
|
-
// TODO not needed anymore now that trip continuations are handled separately
|
|
22373
23138
|
transferTime = DURATION_ZERO;
|
|
22374
23139
|
}
|
|
22375
23140
|
else {
|
|
22376
23141
|
transferTime = options.minTransferTime;
|
|
22377
23142
|
}
|
|
22378
23143
|
const arrivalAfterTransfer = currentArrival.arrival + transferTime;
|
|
22379
|
-
|
|
22380
|
-
|
|
23144
|
+
if (arrivalAfterTransfer <
|
|
23145
|
+
state.improvementBound(round, transfer.destination) &&
|
|
23146
|
+
arrivalAfterTransfer < state.destinationBest) {
|
|
22381
23147
|
arrivalsAtCurrentRound[transfer.destination] = {
|
|
22382
23148
|
arrival: arrivalAfterTransfer,
|
|
22383
23149
|
from: stop,
|
|
22384
23150
|
to: transfer.destination,
|
|
22385
|
-
minTransferTime:
|
|
23151
|
+
minTransferTime: transferTime || undefined,
|
|
22386
23152
|
type: transfer.type,
|
|
22387
23153
|
};
|
|
22388
|
-
|
|
23154
|
+
state.updateArrival(transfer.destination, arrivalAfterTransfer, round);
|
|
22389
23155
|
newlyMarkedStops.add(transfer.destination);
|
|
22390
23156
|
}
|
|
22391
23157
|
}
|
|
22392
23158
|
}
|
|
22393
23159
|
return newlyMarkedStops;
|
|
22394
23160
|
}
|
|
23161
|
+
}
|
|
23162
|
+
|
|
23163
|
+
/**
|
|
23164
|
+
* A public transportation router implementing the RAPTOR and Range RAPTOR
|
|
23165
|
+
* algorithms.
|
|
23166
|
+
*
|
|
23167
|
+
* Thin facade over {@link PlainRouter} and {@link RangeRouter}: constructs the
|
|
23168
|
+
* shared {@link Raptor} engine and {@link AccessFinder} once and delegates each
|
|
23169
|
+
* query to the appropriate router.
|
|
23170
|
+
*
|
|
23171
|
+
* @see https://www.microsoft.com/en-us/research/wp-content/uploads/2012/01/raptor_alenex.pdf
|
|
23172
|
+
*/
|
|
23173
|
+
class Router {
|
|
23174
|
+
constructor(timetable, stopsIndex) {
|
|
23175
|
+
const raptor = new Raptor(timetable);
|
|
23176
|
+
const accessFinder = new AccessFinder(timetable, stopsIndex);
|
|
23177
|
+
this.plainRouter = new PlainRouter(timetable, stopsIndex, accessFinder, raptor);
|
|
23178
|
+
this.rangeRouter = new RangeRouter(timetable, stopsIndex, accessFinder, raptor);
|
|
23179
|
+
}
|
|
22395
23180
|
/**
|
|
22396
|
-
*
|
|
22397
|
-
*
|
|
22398
|
-
* @param routingState The routing state containing arrival times and destinations.
|
|
22399
|
-
* @returns The earliest arrival time among the provided destinations.
|
|
23181
|
+
* Standard RAPTOR: finds the earliest-arrival journey from `query.from` to
|
|
23182
|
+
* `query.to` for the given departure time.
|
|
22400
23183
|
*/
|
|
22401
|
-
|
|
22402
|
-
|
|
22403
|
-
|
|
22404
|
-
|
|
22405
|
-
|
|
22406
|
-
|
|
22407
|
-
|
|
22408
|
-
|
|
22409
|
-
return
|
|
23184
|
+
route(query) {
|
|
23185
|
+
return this.plainRouter.route(query);
|
|
23186
|
+
}
|
|
23187
|
+
/**
|
|
23188
|
+
* Range RAPTOR: finds all Pareto-optimal journeys within the departure-time
|
|
23189
|
+
* window `[query.departureTime, query.lastDepartureTime]`.
|
|
23190
|
+
*/
|
|
23191
|
+
rangeRoute(query) {
|
|
23192
|
+
return this.rangeRouter.rangeRoute(query);
|
|
22410
23193
|
}
|
|
22411
23194
|
}
|
|
22412
23195
|
|
|
23196
|
+
const renderTable = (columns, rows, footerRow) => {
|
|
23197
|
+
const bar = (l, m, r) => l + columns.map((c) => '─'.repeat(c.width + 2)).join(m) + r;
|
|
23198
|
+
const renderRow = (cells) => '│' +
|
|
23199
|
+
cells
|
|
23200
|
+
.map((cell, i) => {
|
|
23201
|
+
var _a, _b, _c, _d;
|
|
23202
|
+
const width = (_b = (_a = columns[i]) === null || _a === void 0 ? void 0 : _a.width) !== null && _b !== void 0 ? _b : 0;
|
|
23203
|
+
const align = (_d = (_c = columns[i]) === null || _c === void 0 ? void 0 : _c.align) !== null && _d !== void 0 ? _d : 'left';
|
|
23204
|
+
const padded = align === 'right' ? cell.padStart(width) : cell.padEnd(width);
|
|
23205
|
+
return ` ${padded} `;
|
|
23206
|
+
})
|
|
23207
|
+
.join('│') +
|
|
23208
|
+
'│';
|
|
23209
|
+
return [
|
|
23210
|
+
bar('┌', '┬', '┐'),
|
|
23211
|
+
renderRow(columns.map((c) => c.header)),
|
|
23212
|
+
bar('├', '┼', '┤'),
|
|
23213
|
+
...rows.map(renderRow),
|
|
23214
|
+
bar('├', '┼', '┤'),
|
|
23215
|
+
renderRow(footerRow),
|
|
23216
|
+
bar('└', '┴', '┘'),
|
|
23217
|
+
].join('\n');
|
|
23218
|
+
};
|
|
23219
|
+
// ─── Query label ──────────────────────────────────────────────────────────────
|
|
23220
|
+
const buildQueryLabel = (query, stopsIndex) => {
|
|
23221
|
+
var _a, _b;
|
|
23222
|
+
const fromName = (_b = (_a = stopsIndex.findStopById(query.from)) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : String(query.from);
|
|
23223
|
+
const toNames = [...query.to]
|
|
23224
|
+
.map((id) => { var _a, _b; return (_b = (_a = stopsIndex.findStopById(id)) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : String(id); })
|
|
23225
|
+
.join(' / ');
|
|
23226
|
+
const dep = timeToString(query.departureTime);
|
|
23227
|
+
if (query instanceof RangeQuery) {
|
|
23228
|
+
const lastDep = timeToString(query.lastDepartureTime);
|
|
23229
|
+
return `${fromName} → ${toNames} ${dep}–${lastDep}`;
|
|
23230
|
+
}
|
|
23231
|
+
return `${fromName} → ${toNames} ${dep}`;
|
|
23232
|
+
};
|
|
23233
|
+
// ─── Query loaders ────────────────────────────────────────────────────────────
|
|
22413
23234
|
/**
|
|
22414
23235
|
* Loads a list of routing queries from a JSON file and resolves the
|
|
22415
23236
|
* human-readable stop IDs to the internal numeric IDs used by the router.
|
|
22416
23237
|
*
|
|
22417
|
-
*
|
|
22418
|
-
*
|
|
22419
|
-
* { "from": "STOP_A", "to": ["STOP_B", "STOP_C"], "departureTime": "08:30:00" }
|
|
22420
|
-
* ```
|
|
22421
|
-
* An optional `maxTransfers` integer field is also supported.
|
|
23238
|
+
* Only entries that do **not** carry a `lastDepartureTime` field are loaded —
|
|
23239
|
+
* range-query entries are silently skipped.
|
|
22422
23240
|
*
|
|
22423
23241
|
* @param filePath - Path to the JSON file containing the serialized queries.
|
|
22424
23242
|
* @param stopsIndex - The stops index used to resolve source stop IDs to the
|
|
@@ -22431,7 +23249,9 @@ class Router {
|
|
|
22431
23249
|
const loadQueriesFromJson = (filePath, stopsIndex) => {
|
|
22432
23250
|
const fileContent = fs.readFileSync(filePath, 'utf-8');
|
|
22433
23251
|
const serializedQueries = JSON.parse(fileContent);
|
|
22434
|
-
return serializedQueries
|
|
23252
|
+
return serializedQueries
|
|
23253
|
+
.filter((q) => q.lastDepartureTime === undefined)
|
|
23254
|
+
.map((serializedQuery) => {
|
|
22435
23255
|
const fromStop = stopsIndex.findStopBySourceStopId(serializedQuery.from);
|
|
22436
23256
|
const toStops = Array.from(serializedQuery.to).map((stopId) => stopsIndex.findStopBySourceStopId(stopId));
|
|
22437
23257
|
if (!fromStop || toStops.some((toStop) => !toStop)) {
|
|
@@ -22448,6 +23268,46 @@ const loadQueriesFromJson = (filePath, stopsIndex) => {
|
|
|
22448
23268
|
return queryBuilder.build();
|
|
22449
23269
|
});
|
|
22450
23270
|
};
|
|
23271
|
+
/**
|
|
23272
|
+
* Loads a list of range routing queries from a JSON file and resolves the
|
|
23273
|
+
* human-readable stop IDs to the internal numeric IDs used by the router.
|
|
23274
|
+
*
|
|
23275
|
+
* Only entries that carry a `lastDepartureTime` field are loaded — plain
|
|
23276
|
+
* point-query entries are silently skipped.
|
|
23277
|
+
*
|
|
23278
|
+
* @param filePath - Path to the JSON file containing the serialized queries.
|
|
23279
|
+
* @param stopsIndex - The stops index used to resolve source stop IDs to the
|
|
23280
|
+
* internal numeric IDs expected by the router.
|
|
23281
|
+
* @returns An array of fully constructed {@link RangeQuery} objects ready to
|
|
23282
|
+
* be passed to {@link Router.rangeRoute}.
|
|
23283
|
+
* @throws If the file cannot be read, the JSON is malformed, or any stop ID
|
|
23284
|
+
* referenced in the file cannot be found in the stops index.
|
|
23285
|
+
*/
|
|
23286
|
+
const loadRangeQueriesFromJson = (filePath, stopsIndex) => {
|
|
23287
|
+
const fileContent = fs.readFileSync(filePath, 'utf-8');
|
|
23288
|
+
const serializedQueries = JSON.parse(fileContent);
|
|
23289
|
+
return serializedQueries
|
|
23290
|
+
.filter((q) => q.lastDepartureTime !== undefined)
|
|
23291
|
+
.map((serializedQuery) => {
|
|
23292
|
+
const fromStop = stopsIndex.findStopBySourceStopId(serializedQuery.from);
|
|
23293
|
+
const toStops = Array.from(serializedQuery.to).map((stopId) => stopsIndex.findStopBySourceStopId(stopId));
|
|
23294
|
+
if (!fromStop || toStops.some((toStop) => !toStop)) {
|
|
23295
|
+
throw new Error(`Invalid task: Start or end station not found for task ${JSON.stringify(serializedQuery)}`);
|
|
23296
|
+
}
|
|
23297
|
+
const queryBuilder = new RangeQuery.Builder()
|
|
23298
|
+
.from(fromStop.id)
|
|
23299
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
23300
|
+
.to(new Set(toStops.map((stop) => stop.id)))
|
|
23301
|
+
.departureTime(timeFromString(serializedQuery.departureTime))
|
|
23302
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
23303
|
+
.lastDepartureTime(timeFromString(serializedQuery.lastDepartureTime));
|
|
23304
|
+
if (serializedQuery.maxTransfers !== undefined) {
|
|
23305
|
+
queryBuilder.maxTransfers(serializedQuery.maxTransfers);
|
|
23306
|
+
}
|
|
23307
|
+
return queryBuilder.build();
|
|
23308
|
+
});
|
|
23309
|
+
};
|
|
23310
|
+
// ─── Benchmark runners ────────────────────────────────────────────────────────
|
|
22451
23311
|
/**
|
|
22452
23312
|
* Benchmarks {@link Router.route} across a set of queries.
|
|
22453
23313
|
*
|
|
@@ -22456,10 +23316,11 @@ const loadQueriesFromJson = (filePath, stopsIndex) => {
|
|
|
22456
23316
|
* produced per query.
|
|
22457
23317
|
* @param iterations - Number of times each query is repeated. Higher values
|
|
22458
23318
|
* yield a more stable mean at the cost of longer wall-clock time.
|
|
23319
|
+
* @param stopsIndex - Used to resolve stop names for result labels.
|
|
22459
23320
|
* @returns An array of {@link PerformanceResult} objects, one per query, each
|
|
22460
23321
|
* containing the mean wall-clock time (µs) and mean heap delta (MB).
|
|
22461
23322
|
*/
|
|
22462
|
-
const testRouterPerformance = (router, tasks, iterations) => {
|
|
23323
|
+
const testRouterPerformance = (router, tasks, iterations, stopsIndex) => {
|
|
22463
23324
|
const results = [];
|
|
22464
23325
|
for (const task of tasks) {
|
|
22465
23326
|
let totalTime = 0;
|
|
@@ -22479,7 +23340,7 @@ const testRouterPerformance = (router, tasks, iterations) => {
|
|
|
22479
23340
|
}
|
|
22480
23341
|
}
|
|
22481
23342
|
results.push({
|
|
22482
|
-
task,
|
|
23343
|
+
label: buildQueryLabel(task, stopsIndex),
|
|
22483
23344
|
meanTimeUs: totalTime / iterations,
|
|
22484
23345
|
meanMemoryMb: totalMemory / iterations / (1024 * 1024),
|
|
22485
23346
|
});
|
|
@@ -22495,14 +23356,14 @@ const testRouterPerformance = (router, tasks, iterations) => {
|
|
|
22495
23356
|
* @param tasks - The list of queries to benchmark. One {@link PerformanceResult}
|
|
22496
23357
|
* is produced per query.
|
|
22497
23358
|
* @param iterations - Number of times `bestRoute` is called per query.
|
|
23359
|
+
* @param stopsIndex - Used to resolve stop names for result labels.
|
|
22498
23360
|
* @returns An array of {@link PerformanceResult} objects, one per query, each
|
|
22499
23361
|
* containing the mean wall-clock time (µs) and mean heap delta (MB) for the
|
|
22500
23362
|
* `bestRoute` call alone.
|
|
22501
23363
|
*/
|
|
22502
|
-
const testBestRoutePerformance = (router, tasks, iterations) => {
|
|
23364
|
+
const testBestRoutePerformance = (router, tasks, iterations, stopsIndex) => {
|
|
22503
23365
|
const results = [];
|
|
22504
23366
|
for (const task of tasks) {
|
|
22505
|
-
// Compute the routing result once — this is not part of the benchmark.
|
|
22506
23367
|
const result = router.route(task);
|
|
22507
23368
|
let totalTime = 0;
|
|
22508
23369
|
let totalMemory = 0;
|
|
@@ -22521,7 +23382,45 @@ const testBestRoutePerformance = (router, tasks, iterations) => {
|
|
|
22521
23382
|
}
|
|
22522
23383
|
}
|
|
22523
23384
|
results.push({
|
|
22524
|
-
task,
|
|
23385
|
+
label: buildQueryLabel(task, stopsIndex),
|
|
23386
|
+
meanTimeUs: totalTime / iterations,
|
|
23387
|
+
meanMemoryMb: totalMemory / iterations / (1024 * 1024),
|
|
23388
|
+
});
|
|
23389
|
+
}
|
|
23390
|
+
return results;
|
|
23391
|
+
};
|
|
23392
|
+
/**
|
|
23393
|
+
* Benchmarks {@link Router.rangeRoute} across a set of range queries.
|
|
23394
|
+
*
|
|
23395
|
+
* @param router - The router instance to benchmark.
|
|
23396
|
+
* @param tasks - The list of range queries to run. One {@link PerformanceResult}
|
|
23397
|
+
* is produced per query.
|
|
23398
|
+
* @param iterations - Number of times each query is repeated.
|
|
23399
|
+
* @param stopsIndex - Used to resolve stop names for result labels.
|
|
23400
|
+
* @returns An array of {@link PerformanceResult} objects, one per query, each
|
|
23401
|
+
* containing the mean wall-clock time (µs) and mean heap delta (MB).
|
|
23402
|
+
*/
|
|
23403
|
+
const testRangeRouterPerformance = (router, tasks, iterations, stopsIndex) => {
|
|
23404
|
+
const results = [];
|
|
23405
|
+
for (const task of tasks) {
|
|
23406
|
+
let totalTime = 0;
|
|
23407
|
+
let totalMemory = 0;
|
|
23408
|
+
for (let i = 0; i < iterations; i++) {
|
|
23409
|
+
if (global.gc) {
|
|
23410
|
+
global.gc();
|
|
23411
|
+
}
|
|
23412
|
+
const startMemory = process.memoryUsage().heapUsed;
|
|
23413
|
+
const startTime = performance$1.now();
|
|
23414
|
+
router.rangeRoute(task);
|
|
23415
|
+
const endTime = performance$1.now();
|
|
23416
|
+
const endMemory = process.memoryUsage().heapUsed;
|
|
23417
|
+
totalTime += (endTime - startTime) * 1000;
|
|
23418
|
+
if (endMemory >= startMemory) {
|
|
23419
|
+
totalMemory += endMemory - startMemory;
|
|
23420
|
+
}
|
|
23421
|
+
}
|
|
23422
|
+
results.push({
|
|
23423
|
+
label: buildQueryLabel(task, stopsIndex),
|
|
22525
23424
|
meanTimeUs: totalTime / iterations,
|
|
22526
23425
|
meanMemoryMb: totalMemory / iterations / (1024 * 1024),
|
|
22527
23426
|
});
|
|
@@ -22529,38 +23428,93 @@ const testBestRoutePerformance = (router, tasks, iterations) => {
|
|
|
22529
23428
|
return results;
|
|
22530
23429
|
};
|
|
22531
23430
|
/**
|
|
22532
|
-
*
|
|
23431
|
+
* Benchmarks {@link RangeResult.getRoutes} — the full Pareto-frontier
|
|
23432
|
+
* reconstruction phase — independently of the range routing phase.
|
|
22533
23433
|
*
|
|
22534
|
-
*
|
|
22535
|
-
*
|
|
22536
|
-
*
|
|
22537
|
-
*
|
|
23434
|
+
* @param router - The router instance used to produce the range results that
|
|
23435
|
+
* are then fed into `getRoutes`.
|
|
23436
|
+
* @param tasks - The list of range queries to benchmark. One
|
|
23437
|
+
* {@link PerformanceResult} is produced per query.
|
|
23438
|
+
* @param iterations - Number of times `getRoutes` is called per query.
|
|
23439
|
+
* @param stopsIndex - Used to resolve stop names for result labels.
|
|
23440
|
+
* @returns An array of {@link PerformanceResult} objects, one per query, each
|
|
23441
|
+
* containing the mean wall-clock time (µs) and mean heap delta (MB) for the
|
|
23442
|
+
* `getRoutes` call alone.
|
|
23443
|
+
*/
|
|
23444
|
+
const testRangeResultPerformance = (router, tasks, iterations, stopsIndex) => {
|
|
23445
|
+
const results = [];
|
|
23446
|
+
for (const task of tasks) {
|
|
23447
|
+
const rangeResult = router.rangeRoute(task);
|
|
23448
|
+
let totalTime = 0;
|
|
23449
|
+
let totalMemory = 0;
|
|
23450
|
+
for (let i = 0; i < iterations; i++) {
|
|
23451
|
+
if (global.gc) {
|
|
23452
|
+
global.gc();
|
|
23453
|
+
}
|
|
23454
|
+
const startMemory = process.memoryUsage().heapUsed;
|
|
23455
|
+
const startTime = performance$1.now();
|
|
23456
|
+
rangeResult.getRoutes();
|
|
23457
|
+
const endTime = performance$1.now();
|
|
23458
|
+
const endMemory = process.memoryUsage().heapUsed;
|
|
23459
|
+
totalTime += (endTime - startTime) * 1000;
|
|
23460
|
+
if (endMemory >= startMemory) {
|
|
23461
|
+
totalMemory += endMemory - startMemory;
|
|
23462
|
+
}
|
|
23463
|
+
}
|
|
23464
|
+
results.push({
|
|
23465
|
+
label: buildQueryLabel(task, stopsIndex),
|
|
23466
|
+
meanTimeUs: totalTime / iterations,
|
|
23467
|
+
meanMemoryMb: totalMemory / iterations / (1024 * 1024),
|
|
23468
|
+
});
|
|
23469
|
+
}
|
|
23470
|
+
return results;
|
|
23471
|
+
};
|
|
23472
|
+
// ─── Output ───────────────────────────────────────────────────────────────────
|
|
23473
|
+
/**
|
|
23474
|
+
* Prints a table summary of performance results to stdout.
|
|
23475
|
+
*
|
|
23476
|
+
* Each row corresponds to one task, identified by a human-readable query label
|
|
23477
|
+
* (origin → destination + departure time). A footer row shows the mean across
|
|
23478
|
+
* all tasks. An optional `label` is printed as a section header above the table.
|
|
22538
23479
|
*
|
|
22539
|
-
* @param results - The performance results to display
|
|
22540
|
-
*
|
|
22541
|
-
* @param label - Optional heading printed above the results block.
|
|
22542
|
-
* Defaults to `'Performance Results'`.
|
|
23480
|
+
* @param results - The performance results to display.
|
|
23481
|
+
* @param label - Heading printed above the table. Defaults to `'Performance Results'`.
|
|
22543
23482
|
*/
|
|
22544
23483
|
const prettyPrintPerformanceResults = (results, label = 'Performance Results') => {
|
|
23484
|
+
console.log(`\n${label}`);
|
|
22545
23485
|
if (results.length === 0) {
|
|
22546
|
-
console.log('
|
|
23486
|
+
console.log(' (no results)');
|
|
22547
23487
|
return;
|
|
22548
23488
|
}
|
|
22549
|
-
const
|
|
22550
|
-
|
|
22551
|
-
const
|
|
22552
|
-
|
|
22553
|
-
|
|
22554
|
-
|
|
22555
|
-
|
|
22556
|
-
|
|
22557
|
-
|
|
22558
|
-
|
|
22559
|
-
|
|
22560
|
-
|
|
22561
|
-
|
|
22562
|
-
|
|
23489
|
+
const fmtTime = (n) => Math.round(n).toLocaleString('en-US');
|
|
23490
|
+
const fmtMem = (n) => n.toFixed(2);
|
|
23491
|
+
const meanTime = results.reduce((s, r) => s + r.meanTimeUs, 0) / results.length;
|
|
23492
|
+
const meanMem = results.reduce((s, r) => s + r.meanMemoryMb, 0) / results.length;
|
|
23493
|
+
const queryHeader = 'Query';
|
|
23494
|
+
const timeHeader = 'Time (µs)';
|
|
23495
|
+
const memHeader = 'Mem (MB)';
|
|
23496
|
+
const timeVals = results.map((r) => fmtTime(r.meanTimeUs));
|
|
23497
|
+
const memVals = results.map((r) => fmtMem(r.meanMemoryMb));
|
|
23498
|
+
const meanTimeStr = fmtTime(meanTime);
|
|
23499
|
+
const meanMemStr = fmtMem(meanMem);
|
|
23500
|
+
const queryWidth = Math.max(queryHeader.length, 'mean'.length, ...results.map((r) => r.label.length));
|
|
23501
|
+
const timeWidth = Math.max(timeHeader.length, meanTimeStr.length, ...timeVals.map((v) => v.length));
|
|
23502
|
+
const memWidth = Math.max(memHeader.length, meanMemStr.length, ...memVals.map((v) => v.length));
|
|
23503
|
+
const columns = [
|
|
23504
|
+
{ header: queryHeader, width: queryWidth, align: 'left' },
|
|
23505
|
+
{ header: timeHeader, width: timeWidth, align: 'right' },
|
|
23506
|
+
{ header: memHeader, width: memWidth, align: 'right' },
|
|
23507
|
+
];
|
|
23508
|
+
const rows = results.map((r, i) => {
|
|
23509
|
+
var _a, _b;
|
|
23510
|
+
return [
|
|
23511
|
+
r.label,
|
|
23512
|
+
(_a = timeVals[i]) !== null && _a !== void 0 ? _a : '',
|
|
23513
|
+
(_b = memVals[i]) !== null && _b !== void 0 ? _b : '',
|
|
23514
|
+
];
|
|
22563
23515
|
});
|
|
23516
|
+
const footer = ['mean', meanTimeStr, meanMemStr];
|
|
23517
|
+
console.log(renderTable(columns, rows, footer));
|
|
22564
23518
|
};
|
|
22565
23519
|
|
|
22566
23520
|
/**
|
|
@@ -22609,25 +23563,39 @@ const startRepl = (stopsPath, timetablePath) => {
|
|
|
22609
23563
|
},
|
|
22610
23564
|
});
|
|
22611
23565
|
replServer.defineCommand('route', {
|
|
22612
|
-
help: 'Find a route using .route from <
|
|
23566
|
+
help: 'Find a route using .route from <stop> to <stop> at <HH:mm> [before <HH:mm>] [with <N> transfers]',
|
|
22613
23567
|
action(routeQuery) {
|
|
22614
23568
|
this.clearBufferedCommand();
|
|
22615
23569
|
const parts = routeQuery.split(' ').filter(Boolean);
|
|
22616
|
-
const withTransfersIndex = parts.indexOf('with');
|
|
22617
|
-
const maxTransfers = withTransfersIndex !== -1 && parts[withTransfersIndex + 1] !== undefined
|
|
22618
|
-
? parseInt(parts[withTransfersIndex + 1])
|
|
22619
|
-
: 4;
|
|
22620
|
-
const atTime = parts
|
|
22621
|
-
.slice(withTransfersIndex === -1
|
|
22622
|
-
? parts.indexOf('at') + 1
|
|
22623
|
-
: parts.indexOf('at') + 1, withTransfersIndex === -1 ? parts.length : withTransfersIndex)
|
|
22624
|
-
.join(' ');
|
|
22625
23570
|
const fromIndex = parts.indexOf('from');
|
|
22626
23571
|
const toIndex = parts.indexOf('to');
|
|
23572
|
+
const atIndex = parts.indexOf('at');
|
|
23573
|
+
const beforeIndex = parts.indexOf('before');
|
|
23574
|
+
const withIndex = parts.indexOf('with');
|
|
23575
|
+
if (fromIndex === -1 || toIndex === -1 || atIndex === -1) {
|
|
23576
|
+
console.log('Usage: .route from <stop> to <stop> at <HH:mm> [before <HH:mm>] [with <N> transfers]');
|
|
23577
|
+
this.displayPrompt();
|
|
23578
|
+
return;
|
|
23579
|
+
}
|
|
22627
23580
|
const fromId = parts.slice(fromIndex + 1, toIndex).join(' ');
|
|
22628
|
-
const toId = parts.slice(toIndex + 1,
|
|
23581
|
+
const toId = parts.slice(toIndex + 1, atIndex).join(' ');
|
|
23582
|
+
// atTime ends at 'before', 'with', or the end of the input.
|
|
23583
|
+
const atTimeEnd = beforeIndex !== -1
|
|
23584
|
+
? beforeIndex
|
|
23585
|
+
: withIndex !== -1
|
|
23586
|
+
? withIndex
|
|
23587
|
+
: parts.length;
|
|
23588
|
+
const atTime = parts.slice(atIndex + 1, atTimeEnd).join(' ');
|
|
23589
|
+
// beforeTime is only present when the 'before' keyword appears.
|
|
23590
|
+
const beforeTimeEnd = withIndex !== -1 ? withIndex : parts.length;
|
|
23591
|
+
const beforeTime = beforeIndex !== -1
|
|
23592
|
+
? parts.slice(beforeIndex + 1, beforeTimeEnd).join(' ')
|
|
23593
|
+
: undefined;
|
|
23594
|
+
const maxTransfers = withIndex !== -1 && parts[withIndex + 1] !== undefined
|
|
23595
|
+
? parseInt(parts[withIndex + 1])
|
|
23596
|
+
: 4;
|
|
22629
23597
|
if (!fromId || !toId || !atTime) {
|
|
22630
|
-
console.log('Usage: .route from <
|
|
23598
|
+
console.log('Usage: .route from <stop> to <stop> at <HH:mm> [before <HH:mm>] [with <N> transfers]');
|
|
22631
23599
|
this.displayPrompt();
|
|
22632
23600
|
return;
|
|
22633
23601
|
}
|
|
@@ -22651,30 +23619,61 @@ const startRepl = (stopsPath, timetablePath) => {
|
|
|
22651
23619
|
this.displayPrompt();
|
|
22652
23620
|
return;
|
|
22653
23621
|
}
|
|
22654
|
-
const departureTime = timeFromString(atTime);
|
|
22655
23622
|
try {
|
|
22656
|
-
const
|
|
22657
|
-
.from(fromStop.id)
|
|
22658
|
-
.to(toStop.id)
|
|
22659
|
-
.departureTime(departureTime)
|
|
22660
|
-
.maxTransfers(maxTransfers)
|
|
22661
|
-
.build();
|
|
23623
|
+
const departureTime = timeFromString(atTime);
|
|
22662
23624
|
const router = new Router(timetable, stopsIndex);
|
|
22663
|
-
|
|
22664
|
-
|
|
22665
|
-
|
|
22666
|
-
|
|
22667
|
-
|
|
22668
|
-
|
|
22669
|
-
|
|
22670
|
-
|
|
22671
|
-
|
|
22672
|
-
|
|
22673
|
-
|
|
22674
|
-
|
|
23625
|
+
if (beforeTime !== undefined) {
|
|
23626
|
+
const lastDepartureTime = timeFromString(beforeTime);
|
|
23627
|
+
const query = new RangeQuery.Builder()
|
|
23628
|
+
.from(fromStop.id)
|
|
23629
|
+
.to(toStop.id)
|
|
23630
|
+
.departureTime(departureTime)
|
|
23631
|
+
.lastDepartureTime(lastDepartureTime)
|
|
23632
|
+
.maxTransfers(maxTransfers)
|
|
23633
|
+
.build();
|
|
23634
|
+
const result = router.rangeRoute(query);
|
|
23635
|
+
if (result.size === 0) {
|
|
23636
|
+
console.log(`No journeys found from ${fromStop.name} to ${toStop.name} ` +
|
|
23637
|
+
`between ${atTime} and ${beforeTime}.`);
|
|
23638
|
+
}
|
|
23639
|
+
else {
|
|
23640
|
+
console.log(`Found ${result.size} Pareto-optimal journey${result.size === 1 ? '' : 's'} ` +
|
|
23641
|
+
`from ${fromStop.name} to ${toStop.name} ` +
|
|
23642
|
+
`(window ${atTime}–${beforeTime}):`);
|
|
23643
|
+
const routes = result.getRoutes();
|
|
23644
|
+
routes.forEach((route, index) => {
|
|
23645
|
+
const journeyNumber = index + 1;
|
|
23646
|
+
console.log(`\nJourney ${journeyNumber}:`);
|
|
23647
|
+
console.log(route.toString());
|
|
23648
|
+
});
|
|
23649
|
+
}
|
|
22675
23650
|
}
|
|
22676
23651
|
else {
|
|
22677
|
-
|
|
23652
|
+
const query = new Query.Builder()
|
|
23653
|
+
.from(fromStop.id)
|
|
23654
|
+
.to(toStop.id)
|
|
23655
|
+
.departureTime(departureTime)
|
|
23656
|
+
.maxTransfers(maxTransfers)
|
|
23657
|
+
.build();
|
|
23658
|
+
const result = router.route(query);
|
|
23659
|
+
const arrivalTime = result.arrivalAt(toStop.id);
|
|
23660
|
+
if (arrivalTime === undefined) {
|
|
23661
|
+
console.log(`Destination not reachable`);
|
|
23662
|
+
}
|
|
23663
|
+
else {
|
|
23664
|
+
const transfers = Math.max(0, arrivalTime.legNumber - 1);
|
|
23665
|
+
console.log(`Arriving to ${toStop.name} at ${timeToString(arrivalTime.arrival)} ` +
|
|
23666
|
+
`with ${transfers} transfer${transfers === 1 ? '' : 's'} ` +
|
|
23667
|
+
`from ${fromStop.name}.`);
|
|
23668
|
+
}
|
|
23669
|
+
const bestRoute = result.bestRoute(toStop.id);
|
|
23670
|
+
if (bestRoute) {
|
|
23671
|
+
console.log(`Found route from ${fromStop.name} to ${toStop.name}:`);
|
|
23672
|
+
console.log(bestRoute.toString());
|
|
23673
|
+
}
|
|
23674
|
+
else {
|
|
23675
|
+
console.log('No route found');
|
|
23676
|
+
}
|
|
22678
23677
|
}
|
|
22679
23678
|
}
|
|
22680
23679
|
catch (error) {
|
|
@@ -23034,10 +24033,15 @@ program
|
|
|
23034
24033
|
const router = new Router(timetable, stopsIndex);
|
|
23035
24034
|
const queries = loadQueriesFromJson(routesPath, stopsIndex);
|
|
23036
24035
|
const iterations = parseInt(options.iterations, 10);
|
|
23037
|
-
const routerResults = testRouterPerformance(router, queries, iterations);
|
|
23038
|
-
prettyPrintPerformanceResults(routerResults, '
|
|
23039
|
-
const bestRouteResults = testBestRoutePerformance(router, queries, iterations);
|
|
23040
|
-
prettyPrintPerformanceResults(bestRouteResults, '
|
|
24036
|
+
const routerResults = testRouterPerformance(router, queries, iterations, stopsIndex);
|
|
24037
|
+
prettyPrintPerformanceResults(routerResults, 'Point queries — router.route()');
|
|
24038
|
+
const bestRouteResults = testBestRoutePerformance(router, queries, iterations, stopsIndex);
|
|
24039
|
+
prettyPrintPerformanceResults(bestRouteResults, 'Point queries — result.bestRoute() (reconstruction only)');
|
|
24040
|
+
const rangeQueries = loadRangeQueriesFromJson(routesPath, stopsIndex);
|
|
24041
|
+
const rangeRouterResults = testRangeRouterPerformance(router, rangeQueries, iterations, stopsIndex);
|
|
24042
|
+
prettyPrintPerformanceResults(rangeRouterResults, 'Range queries — router.rangeRoute()');
|
|
24043
|
+
const rangeResultResults = testRangeResultPerformance(router, rangeQueries, iterations, stopsIndex);
|
|
24044
|
+
prettyPrintPerformanceResults(rangeResultResults, 'Range queries — rangeResult.getRoutes() (reconstruction only)');
|
|
23041
24045
|
});
|
|
23042
24046
|
program.parse(process.argv);
|
|
23043
24047
|
//# sourceMappingURL=cli.mjs.map
|