minotor 11.1.3 → 11.2.1
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 +2 -2
- package/README.md +111 -86
- package/dist/cli/perf.d.ts +57 -18
- package/dist/cli.mjs +1373 -343
- 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 +175 -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 +358 -0
- package/src/routing/__tests__/rangeRouter.test.ts +601 -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 +312 -0
- package/src/routing/rangeRouter.ts +175 -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,115 +22464,552 @@ 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
|
-
|
|
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, **or** the full per-departure-time routing state when no
|
|
22497
|
+
* destinations were provided (full-network / isochrone mode).
|
|
22498
|
+
*
|
|
22499
|
+
* **Pareto dominance**: journey J1 dominates J2 iff
|
|
22500
|
+
* `τdep(J1) ≥ τdep(J2) AND τarr(J1) ≤ τarr(J2)`
|
|
22501
|
+
* (with at least one strict inequality).
|
|
22502
|
+
*
|
|
22503
|
+
* Runs are ordered **latest-departure-first**: each successive run departs
|
|
22504
|
+
* strictly earlier *and* arrives strictly earlier than the previous one,
|
|
22505
|
+
* forming the classic staircase Pareto frontier.
|
|
22506
|
+
*
|
|
22507
|
+
* **Full-network mode** (empty `destinations`): when no destinations are
|
|
22508
|
+
* supplied to the range query every departure slot in the window becomes its
|
|
22509
|
+
* own run, because destination-based Pareto pruning cannot be applied.
|
|
22510
|
+
* In this mode the destination-specific helpers ({@link getRoutes},
|
|
22511
|
+
* {@link bestRoute}, {@link latestDepartureRoute}, {@link fastestRoute})
|
|
22512
|
+
* return empty results; use {@link allEarliestArrivals},
|
|
22513
|
+
* {@link allShortestDurations}, {@link earliestArrivalAt}, or
|
|
22514
|
+
* {@link shortestDurationTo} instead.
|
|
22515
|
+
*
|
|
22516
|
+
* Destination handling is delegated to {@link Result}, which expands
|
|
22517
|
+
* equivalent stops when reconstructing routes or looking up arrivals.
|
|
22518
|
+
*/
|
|
22519
|
+
class RangeResult {
|
|
22520
|
+
constructor(runs, destinations) {
|
|
22521
|
+
this._runs = runs;
|
|
22522
|
+
this._destinations = destinations;
|
|
22523
|
+
}
|
|
22524
|
+
/** The resolved destination stop IDs for this result. */
|
|
22525
|
+
get destinations() {
|
|
22526
|
+
return this._destinations;
|
|
22527
|
+
}
|
|
22528
|
+
normalizeTargets(to) {
|
|
22529
|
+
if (to instanceof Set)
|
|
22530
|
+
return new Set(to);
|
|
22531
|
+
if (to !== undefined)
|
|
22532
|
+
return new Set([to]);
|
|
22533
|
+
return new Set(this._destinations);
|
|
22142
22534
|
}
|
|
22143
22535
|
/**
|
|
22144
|
-
*
|
|
22145
|
-
*
|
|
22146
|
-
*
|
|
22147
|
-
*
|
|
22536
|
+
* Returns all non-dominated routes to this result's default destination set,
|
|
22537
|
+
* ordered from the earliest departure to the latest departure.
|
|
22538
|
+
*
|
|
22539
|
+
* Each route in the list departs strictly earlier *and* arrives strictly
|
|
22540
|
+
* earlier than its predecessor.
|
|
22541
|
+
*
|
|
22542
|
+
* Returns an empty array when no destinations were provided (full-network
|
|
22543
|
+
* mode). Use {@link allEarliestArrivals} or {@link allShortestDurations}
|
|
22544
|
+
* to query individual stops in that case.
|
|
22148
22545
|
*/
|
|
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
|
-
}
|
|
22546
|
+
getRoutes() {
|
|
22547
|
+
const routes = [];
|
|
22548
|
+
for (const { result } of this._runs) {
|
|
22549
|
+
const route = result.bestRoute();
|
|
22550
|
+
if (route !== undefined)
|
|
22551
|
+
routes.push(route);
|
|
22165
22552
|
}
|
|
22166
|
-
return
|
|
22553
|
+
return routes.reverse();
|
|
22167
22554
|
}
|
|
22168
22555
|
/**
|
|
22169
|
-
*
|
|
22556
|
+
* The route that arrives **earliest** at the given stop(s) across all
|
|
22557
|
+
* Pareto-optimal runs.
|
|
22170
22558
|
*
|
|
22171
|
-
*
|
|
22172
|
-
*
|
|
22173
|
-
*
|
|
22559
|
+
* When two runs achieve the same arrival time at the target, the one with
|
|
22560
|
+
* the **later departure** is preferred — you wait at the origin rather than
|
|
22561
|
+
* at a transit stop.
|
|
22174
22562
|
*
|
|
22175
|
-
*
|
|
22176
|
-
*
|
|
22563
|
+
* Defaults to this result's own destination stop(s) when `to` is omitted.
|
|
22564
|
+
* Always pass an explicit `to` stop when operating in full-network mode
|
|
22565
|
+
* (no destinations), otherwise `undefined` is returned.
|
|
22566
|
+
*
|
|
22567
|
+
* @param to Optional destination stop ID or set of stop IDs.
|
|
22568
|
+
* @returns The reconstructed {@link Route} with the earliest arrival,
|
|
22569
|
+
* or `undefined` if the target is unreachable in every run.
|
|
22177
22570
|
*/
|
|
22178
|
-
|
|
22179
|
-
const
|
|
22180
|
-
|
|
22181
|
-
|
|
22182
|
-
|
|
22183
|
-
|
|
22184
|
-
|
|
22185
|
-
|
|
22186
|
-
|
|
22187
|
-
|
|
22188
|
-
|
|
22571
|
+
bestRoute(to) {
|
|
22572
|
+
const targetStops = this.normalizeTargets(to);
|
|
22573
|
+
let bestRun;
|
|
22574
|
+
let bestArrival;
|
|
22575
|
+
for (const run of this._runs) {
|
|
22576
|
+
for (const stopId of targetStops) {
|
|
22577
|
+
const arrival = run.result.arrivalAt(stopId);
|
|
22578
|
+
if (arrival === undefined)
|
|
22579
|
+
continue;
|
|
22580
|
+
if (bestArrival === undefined || arrival.arrival < bestArrival) {
|
|
22581
|
+
bestArrival = arrival.arrival;
|
|
22582
|
+
bestRun = run;
|
|
22583
|
+
}
|
|
22584
|
+
}
|
|
22585
|
+
}
|
|
22586
|
+
return bestRun === null || bestRun === void 0 ? void 0 : bestRun.result.bestRoute(targetStops);
|
|
22587
|
+
}
|
|
22588
|
+
/**
|
|
22589
|
+
* The route with the **latest possible departure** from the origin among all
|
|
22590
|
+
* Pareto-optimal journeys in the window.
|
|
22591
|
+
*
|
|
22592
|
+
* This is the journey that lets you leave the origin as late as possible.
|
|
22593
|
+
* It does **not** necessarily achieve the earliest arrival — for that, use
|
|
22594
|
+
* {@link bestRoute}. For the shortest travel duration, use
|
|
22595
|
+
* {@link fastestRoute}.
|
|
22596
|
+
*
|
|
22597
|
+
* Defaults to this result's own destination stop(s) when `to` is omitted.
|
|
22598
|
+
* Always pass an explicit `to` stop when operating in full-network mode
|
|
22599
|
+
* (no destinations), otherwise `undefined` is returned.
|
|
22600
|
+
*
|
|
22601
|
+
* @param to Optional destination stop ID or set of stop IDs.
|
|
22602
|
+
* @returns The reconstructed {@link Route} with the latest departure,
|
|
22603
|
+
* or `undefined` if the target is unreachable in every run.
|
|
22604
|
+
*/
|
|
22605
|
+
latestDepartureRoute(to) {
|
|
22606
|
+
const targetStops = this.normalizeTargets(to);
|
|
22607
|
+
for (const { result } of this._runs) {
|
|
22608
|
+
const route = result.bestRoute(targetStops);
|
|
22609
|
+
if (route !== undefined)
|
|
22610
|
+
return route;
|
|
22611
|
+
}
|
|
22612
|
+
return undefined;
|
|
22613
|
+
}
|
|
22614
|
+
/**
|
|
22615
|
+
* Reconstructs the **fastest** route to the given stop(s) — the journey with
|
|
22616
|
+
* the shortest travel duration (arrival time − origin departure time) across
|
|
22617
|
+
* all Pareto-optimal runs.
|
|
22618
|
+
*
|
|
22619
|
+
* Unlike {@link bestRoute}, which returns the route that departs as late as
|
|
22620
|
+
* possible while still arriving early, this method minimizes total time
|
|
22621
|
+
* spent traveling.
|
|
22622
|
+
*
|
|
22623
|
+
* Defaults to this result's own destination stop(s) when `to` is omitted.
|
|
22624
|
+
* Always pass an explicit `to` stop when operating in full-network mode
|
|
22625
|
+
* (no destinations), otherwise `undefined` is returned.
|
|
22626
|
+
*
|
|
22627
|
+
* @param to Optional destination stop ID or set of stop IDs.
|
|
22628
|
+
* @returns The reconstructed fastest {@link Route}, or `undefined` if the
|
|
22629
|
+
* target is unreachable in every run.
|
|
22630
|
+
*/
|
|
22631
|
+
fastestRoute(to) {
|
|
22632
|
+
const targetStops = this.normalizeTargets(to);
|
|
22633
|
+
let fastestRun;
|
|
22634
|
+
let shortestDuration = Infinity;
|
|
22635
|
+
for (const run of this._runs) {
|
|
22636
|
+
for (const stopId of targetStops) {
|
|
22637
|
+
const arrival = run.result.arrivalAt(stopId);
|
|
22638
|
+
if (arrival === undefined)
|
|
22639
|
+
continue;
|
|
22640
|
+
const duration = arrival.arrival - run.departureTime;
|
|
22641
|
+
if (duration < shortestDuration) {
|
|
22642
|
+
shortestDuration = duration;
|
|
22643
|
+
fastestRun = run;
|
|
22644
|
+
}
|
|
22645
|
+
}
|
|
22646
|
+
}
|
|
22647
|
+
return fastestRun === null || fastestRun === void 0 ? void 0 : fastestRun.result.bestRoute(targetStops);
|
|
22648
|
+
}
|
|
22649
|
+
/** Number of Pareto-optimal journeys found. */
|
|
22650
|
+
get size() {
|
|
22651
|
+
return this._runs.length;
|
|
22652
|
+
}
|
|
22653
|
+
/**
|
|
22654
|
+
* Earliest achievable arrival at a stop across all Pareto-optimal runs.
|
|
22655
|
+
*
|
|
22656
|
+
* Useful for isochrone / accessibility analysis: given this result's
|
|
22657
|
+
* departure-time frontier, how early can you reach stop `s` regardless of
|
|
22658
|
+
* which specific trip you take?
|
|
22659
|
+
*
|
|
22660
|
+
* Equivalent stops are handled by {@link Result.arrivalAt}.
|
|
22661
|
+
*
|
|
22662
|
+
* @param stop The target stop ID.
|
|
22663
|
+
* @param maxTransfers Optional upper bound on the number of transfers.
|
|
22664
|
+
*/
|
|
22665
|
+
earliestArrivalAt(stop, maxTransfers) {
|
|
22666
|
+
let best;
|
|
22667
|
+
for (const { result } of this._runs) {
|
|
22668
|
+
const arrival = result.arrivalAt(stop, maxTransfers);
|
|
22669
|
+
if (arrival !== undefined &&
|
|
22670
|
+
(best === undefined || arrival.arrival < best.arrival)) {
|
|
22671
|
+
best = arrival;
|
|
22672
|
+
}
|
|
22673
|
+
}
|
|
22674
|
+
return best;
|
|
22675
|
+
}
|
|
22676
|
+
/**
|
|
22677
|
+
* Shortest travel duration to reach a stop across all Pareto-optimal runs.
|
|
22678
|
+
*
|
|
22679
|
+
* For each run, duration is measured from the run's origin departure time to
|
|
22680
|
+
* the earliest arrival at `stop` within that run. The minimum across all
|
|
22681
|
+
* runs is returned.
|
|
22682
|
+
*
|
|
22683
|
+
* Equivalent stops are handled by {@link Result.arrivalAt}.
|
|
22684
|
+
*
|
|
22685
|
+
* Duration is **not** monotone along the Pareto frontier — a run that
|
|
22686
|
+
* departs later may still travel faster — so every run is checked. In
|
|
22687
|
+
* practice the Pareto frontier is small, so this is O(runs).
|
|
22688
|
+
*
|
|
22689
|
+
* Returns `undefined` if `stop` is unreachable in every run.
|
|
22690
|
+
*
|
|
22691
|
+
* @param stop The target stop ID.
|
|
22692
|
+
* @param maxTransfers Optional upper bound on the number of transfers.
|
|
22693
|
+
*/
|
|
22694
|
+
shortestDurationTo(stop, maxTransfers) {
|
|
22695
|
+
let shortest;
|
|
22696
|
+
for (const { departureTime, result } of this._runs) {
|
|
22697
|
+
const arrival = result.arrivalAt(stop, maxTransfers);
|
|
22698
|
+
if (arrival === undefined)
|
|
22699
|
+
continue;
|
|
22700
|
+
const duration = arrival.arrival - departureTime;
|
|
22701
|
+
if (shortest === undefined || duration < shortest.duration) {
|
|
22702
|
+
shortest = Object.assign(Object.assign({}, arrival), { duration });
|
|
22703
|
+
}
|
|
22704
|
+
}
|
|
22705
|
+
return shortest;
|
|
22706
|
+
}
|
|
22707
|
+
/**
|
|
22708
|
+
* Shortest travel duration to **every reachable stop** across all
|
|
22709
|
+
* Pareto-optimal runs, as a single `Map<StopId, DurationArrival>`.
|
|
22710
|
+
*/
|
|
22711
|
+
allShortestDurations() {
|
|
22712
|
+
const durations = new Map();
|
|
22713
|
+
for (const { departureTime, result } of this._runs) {
|
|
22714
|
+
for (const { stop, arrival, legNumber, } of result.routingState.arrivals()) {
|
|
22715
|
+
const duration = arrival - departureTime;
|
|
22716
|
+
const existing = durations.get(stop);
|
|
22717
|
+
if (existing === undefined || duration < existing.duration) {
|
|
22718
|
+
durations.set(stop, { arrival, legNumber, duration });
|
|
22719
|
+
}
|
|
22720
|
+
}
|
|
22721
|
+
}
|
|
22722
|
+
return durations;
|
|
22723
|
+
}
|
|
22724
|
+
/**
|
|
22725
|
+
* Earliest achievable arrival at **every reachable stop** across all
|
|
22726
|
+
* Pareto-optimal runs, as a single `Map<StopId, Arrival>`.
|
|
22727
|
+
*/
|
|
22728
|
+
allEarliestArrivals() {
|
|
22729
|
+
const arrivals = new Map();
|
|
22730
|
+
for (const { result } of this._runs) {
|
|
22731
|
+
for (const { stop, arrival, legNumber, } of result.routingState.arrivals()) {
|
|
22732
|
+
const existing = arrivals.get(stop);
|
|
22733
|
+
if (existing === undefined || arrival < existing.arrival) {
|
|
22734
|
+
arrivals.set(stop, { arrival, legNumber });
|
|
22735
|
+
}
|
|
22736
|
+
}
|
|
22737
|
+
}
|
|
22738
|
+
return arrivals;
|
|
22739
|
+
}
|
|
22740
|
+
/**
|
|
22741
|
+
* Iterates over all Pareto-optimal `(departureTime, result)` pairs,
|
|
22742
|
+
* ordered from the latest departure to the earliest departure.
|
|
22743
|
+
*/
|
|
22744
|
+
[Symbol.iterator]() {
|
|
22745
|
+
return this._runs[Symbol.iterator]();
|
|
22746
|
+
}
|
|
22747
|
+
}
|
|
22748
|
+
|
|
22749
|
+
/**
|
|
22750
|
+
* RAPTOR state for Range RAPTOR mode, implementing {@link IRaptorState}.
|
|
22751
|
+
*
|
|
22752
|
+
* Holds both the cross-run shared labels (carried over from one departure-time
|
|
22753
|
+
* iteration to the next, latest → earliest) and a reference to the current
|
|
22754
|
+
* per-iteration {@link RoutingState} (swapped via {@link setCurrentRun}).
|
|
22755
|
+
*
|
|
22756
|
+
* Concretely, `roundLabels[k][p]` is the best known arrival at stop `p` using
|
|
22757
|
+
* at most `k` transit legs, across **all departure times tried so far**.
|
|
22758
|
+
*
|
|
22759
|
+
* @see https://www.microsoft.com/en-us/research/wp-content/uploads/2012/01/raptor_alenex.pdf
|
|
22760
|
+
*/
|
|
22761
|
+
class RangeRaptorState {
|
|
22762
|
+
constructor(maxRounds, nbStops, latestDeparture) {
|
|
22763
|
+
/**
|
|
22764
|
+
* Global best arrival at any destination stop across all runs and rounds.
|
|
22765
|
+
* Used for destination-pruning inside scan methods so that routes that cannot
|
|
22766
|
+
* beat the already-known best are skipped early.
|
|
22767
|
+
*/
|
|
22768
|
+
this._destinationBest = UNREACHED_TIME;
|
|
22769
|
+
this.latestDeparture = latestDeparture;
|
|
22770
|
+
// maxRounds + 2: index 0 = origin/walk legs, indices 1…maxRounds+1 = transit rounds
|
|
22771
|
+
this.roundLabels = Array.from({ length: maxRounds + 2 }, () => new Uint16Array(nbStops).fill(UNREACHED_TIME));
|
|
22772
|
+
this.changedInRound = Array.from({ length: maxRounds + 2 }, () => []);
|
|
22773
|
+
}
|
|
22774
|
+
/**
|
|
22775
|
+
* Swaps in a fresh {@link RoutingState} for the next departure-time iteration
|
|
22776
|
+
* and seeds the shared round-0 labels from its access arrivals.
|
|
22777
|
+
*
|
|
22778
|
+
* Must be called before every `runRaptor` invocation.
|
|
22779
|
+
*/
|
|
22780
|
+
setCurrentRun(routingState) {
|
|
22781
|
+
this.currentRun = routingState;
|
|
22782
|
+
// Propagate round-0 access arrivals into the shared labels so that
|
|
22783
|
+
// initRound(1) can tighten round-1 pruning bounds correctly.
|
|
22784
|
+
const round0 = routingState.graph[0];
|
|
22785
|
+
for (const stop of routingState.origins) {
|
|
22786
|
+
const edge = round0[stop];
|
|
22787
|
+
if (!edge)
|
|
22788
|
+
continue;
|
|
22789
|
+
this.updateArrival(stop, edge.arrival, 0);
|
|
22790
|
+
}
|
|
22791
|
+
}
|
|
22792
|
+
get origins() {
|
|
22793
|
+
return this.currentRun.origins;
|
|
22794
|
+
}
|
|
22795
|
+
get graph() {
|
|
22796
|
+
return this.currentRun.graph;
|
|
22797
|
+
}
|
|
22798
|
+
arrivalTime(stop) {
|
|
22799
|
+
return this.currentRun.arrivalTime(stop);
|
|
22800
|
+
}
|
|
22801
|
+
/**
|
|
22802
|
+
* Uses the cross-run shared label for `round`, which is always at least as
|
|
22803
|
+
* tight as the per-run arrival and therefore provides stronger pruning.
|
|
22804
|
+
*/
|
|
22805
|
+
improvementBound(round, stop) {
|
|
22806
|
+
return this.roundLabels[round][stop];
|
|
22807
|
+
}
|
|
22808
|
+
/**
|
|
22809
|
+
* Global best arrival at any destination across all departure-time iterations.
|
|
22810
|
+
* Always at least as tight as the per-run `destinationBest`.
|
|
22811
|
+
*/
|
|
22812
|
+
get destinationBest() {
|
|
22813
|
+
return this._destinationBest;
|
|
22814
|
+
}
|
|
22815
|
+
isDestination(stop) {
|
|
22816
|
+
return this.currentRun.isDestination(stop);
|
|
22817
|
+
}
|
|
22818
|
+
/** Updates both the per-run state and the cross-run shared labels. */
|
|
22819
|
+
updateArrival(stop, time, round) {
|
|
22820
|
+
this.currentRun.updateArrival(stop, time, round);
|
|
22821
|
+
if (time < this.roundLabels[round][stop]) {
|
|
22822
|
+
this.roundLabels[round][stop] = time;
|
|
22823
|
+
this.changedInRound[round].push(stop);
|
|
22824
|
+
if (this.currentRun.isDestination(stop) && time < this._destinationBest) {
|
|
22825
|
+
this._destinationBest = time;
|
|
22826
|
+
}
|
|
22827
|
+
}
|
|
22828
|
+
}
|
|
22829
|
+
/**
|
|
22830
|
+
* initialized round `k` from round `k-1`: τk(p) ← min(τk(p), τk-1(p)).
|
|
22831
|
+
*
|
|
22832
|
+
* Must be called at the very start of each RAPTOR round before routes are
|
|
22833
|
+
* scanned. After this call, `roundLabels[k][p]` is the minimum arrival at
|
|
22834
|
+
* stop `p` achievable with **at most** k transit legs from any departure time
|
|
22835
|
+
* tried so far — which is exactly the tightest valid pruning bound for round k.
|
|
22836
|
+
*/
|
|
22837
|
+
initRound(round) {
|
|
22838
|
+
const changed = this.changedInRound[round - 1];
|
|
22839
|
+
if (changed.length === 0)
|
|
22840
|
+
return;
|
|
22841
|
+
const prev = this.roundLabels[round - 1];
|
|
22842
|
+
const curr = this.roundLabels[round];
|
|
22843
|
+
for (let i = 0; i < changed.length; i++) {
|
|
22844
|
+
const stop = changed[i];
|
|
22845
|
+
if (prev[stop] < curr[stop]) {
|
|
22846
|
+
curr[stop] = prev[stop];
|
|
22847
|
+
}
|
|
22848
|
+
}
|
|
22849
|
+
changed.length = 0;
|
|
22850
|
+
}
|
|
22851
|
+
}
|
|
22852
|
+
|
|
22853
|
+
class RangeRouter {
|
|
22854
|
+
constructor(timetable, stopsIndex, accessFinder, raptor) {
|
|
22855
|
+
this.timetable = timetable;
|
|
22856
|
+
this.stopsIndex = stopsIndex;
|
|
22857
|
+
this.accessFinder = accessFinder;
|
|
22858
|
+
this.raptor = raptor;
|
|
22859
|
+
}
|
|
22860
|
+
/**
|
|
22861
|
+
* Range RAPTOR: finds all Pareto-optimal journeys within the departure-time
|
|
22862
|
+
* window `[query.departureTime, query.lastDepartureTime]`.
|
|
22863
|
+
*
|
|
22864
|
+
* A journey is Pareto-optimal iff no journey departing no earlier arrives no
|
|
22865
|
+
* later. Runs are ordered latest-departure-first in the returned result.
|
|
22866
|
+
*
|
|
22867
|
+
* @param query A {@link RangeQuery} with both `departureTime` and `lastDepartureTime` set.
|
|
22868
|
+
* @returns A {@link RangeResult} exposing the full Pareto frontier.
|
|
22869
|
+
*/
|
|
22870
|
+
rangeRoute(query) {
|
|
22871
|
+
var _a, _b;
|
|
22872
|
+
const { departureTime: earliest, lastDepartureTime: latest } = query;
|
|
22873
|
+
const destinations = Array.from(query.to)
|
|
22874
|
+
.flatMap((destination) => this.stopsIndex.equivalentStops(destination))
|
|
22875
|
+
.map((destination) => destination.id);
|
|
22876
|
+
const noDestinations = destinations.length === 0;
|
|
22877
|
+
const accessLegs = this.accessFinder.collectAccessPaths(query.from, query.options.minTransferTime);
|
|
22878
|
+
const departureSlots = this.accessFinder.collectDepartureTimes(accessLegs, earliest, latest);
|
|
22879
|
+
if (departureSlots.length === 0) {
|
|
22880
|
+
return new RangeResult([], new Set(destinations));
|
|
22881
|
+
}
|
|
22882
|
+
const maxRounds = query.options.maxTransfers + 1;
|
|
22883
|
+
const rangeState = new RangeRaptorState(maxRounds, this.timetable.nbStops(), latest);
|
|
22884
|
+
const paretoRuns = [];
|
|
22885
|
+
const paretoDestBest = new Map();
|
|
22886
|
+
for (const dest of destinations) {
|
|
22887
|
+
paretoDestBest.set(dest, UNREACHED_TIME);
|
|
22888
|
+
}
|
|
22889
|
+
const trivialDests = new Set(accessLegs
|
|
22890
|
+
.map((leg) => leg.toStopId)
|
|
22891
|
+
.filter((id) => destinations.includes(id)));
|
|
22892
|
+
const trivialDestCovered = new Set();
|
|
22893
|
+
let routingState = null;
|
|
22894
|
+
if (query.rangeOptions.optimizeBeyondLatestDeparture) {
|
|
22895
|
+
routingState = new RoutingState(latest + 1, destinations, accessLegs, this.timetable.nbStops(), maxRounds);
|
|
22896
|
+
rangeState.setCurrentRun(routingState);
|
|
22897
|
+
this.raptor.run(Object.assign(Object.assign({}, query.options), { maxInitialWaitingTime: undefined }), rangeState);
|
|
22898
|
+
if (!noDestinations) {
|
|
22899
|
+
for (const dest of destinations) {
|
|
22900
|
+
const t = routingState.arrivalTime(dest);
|
|
22901
|
+
if (t < ((_a = paretoDestBest.get(dest)) !== null && _a !== void 0 ? _a : UNREACHED_TIME))
|
|
22902
|
+
paretoDestBest.set(dest, t);
|
|
22903
|
+
}
|
|
22904
|
+
}
|
|
22905
|
+
}
|
|
22906
|
+
for (const { depTime, legs } of departureSlots) {
|
|
22907
|
+
if (!noDestinations && trivialDestCovered.size === destinations.length) {
|
|
22908
|
+
break;
|
|
22909
|
+
}
|
|
22910
|
+
if (routingState === null) {
|
|
22911
|
+
routingState = new RoutingState(depTime, destinations, legs, this.timetable.nbStops(), maxRounds);
|
|
22912
|
+
}
|
|
22913
|
+
else {
|
|
22914
|
+
routingState.resetFor(depTime, legs);
|
|
22915
|
+
}
|
|
22916
|
+
rangeState.setCurrentRun(routingState);
|
|
22917
|
+
this.raptor.run(Object.assign(Object.assign({}, query.options), { maxInitialWaitingTime: 0 }), rangeState);
|
|
22918
|
+
let isParetoOptimal = noDestinations;
|
|
22919
|
+
if (!noDestinations) {
|
|
22920
|
+
for (const dest of destinations) {
|
|
22921
|
+
const arrival = routingState.arrivalTime(dest);
|
|
22922
|
+
if (arrival >= ((_b = paretoDestBest.get(dest)) !== null && _b !== void 0 ? _b : UNREACHED_TIME)) {
|
|
22923
|
+
continue;
|
|
22924
|
+
}
|
|
22925
|
+
if (trivialDests.has(dest) && trivialDestCovered.has(dest)) {
|
|
22926
|
+
paretoDestBest.set(dest, arrival);
|
|
22927
|
+
continue;
|
|
22928
|
+
}
|
|
22929
|
+
paretoDestBest.set(dest, arrival);
|
|
22930
|
+
if (trivialDests.has(dest)) {
|
|
22931
|
+
trivialDestCovered.add(dest);
|
|
22932
|
+
}
|
|
22933
|
+
isParetoOptimal = true;
|
|
22934
|
+
}
|
|
22935
|
+
}
|
|
22936
|
+
if (isParetoOptimal) {
|
|
22937
|
+
paretoRuns.push({
|
|
22938
|
+
departureTime: depTime,
|
|
22939
|
+
result: new Result(new Set(destinations), routingState, this.stopsIndex, this.timetable),
|
|
22940
|
+
});
|
|
22941
|
+
routingState = null;
|
|
22942
|
+
}
|
|
22943
|
+
}
|
|
22944
|
+
return new RangeResult(paretoRuns, new Set(destinations));
|
|
22945
|
+
}
|
|
22946
|
+
}
|
|
22947
|
+
|
|
22948
|
+
/**
|
|
22949
|
+
* Encapsulates the core RAPTOR algorithm, operating on a {@link Timetable} and
|
|
22950
|
+
* an {@link IRaptorState} provided by the caller.
|
|
22951
|
+
*
|
|
22952
|
+
* @see https://www.microsoft.com/en-us/research/wp-content/uploads/2012/01/raptor_alenex.pdf
|
|
22953
|
+
*/
|
|
22954
|
+
class Raptor {
|
|
22955
|
+
constructor(timetable) {
|
|
22956
|
+
this.timetable = timetable;
|
|
22957
|
+
}
|
|
22958
|
+
run(options, state) {
|
|
22959
|
+
const markedStops = new Set(state.origins);
|
|
22960
|
+
for (let round = 1; round <= options.maxTransfers + 1; round++) {
|
|
22961
|
+
state.initRound(round);
|
|
22962
|
+
const edgesAtCurrentRound = state.graph[round];
|
|
22963
|
+
const reachableRoutes = this.timetable.findReachableRoutes(markedStops, options.transportModes);
|
|
22964
|
+
markedStops.clear();
|
|
22965
|
+
for (const [route, hopOnStopIndex] of reachableRoutes) {
|
|
22966
|
+
for (const stop of this.scanRoute(route, hopOnStopIndex, round, state, options)) {
|
|
22967
|
+
markedStops.add(stop);
|
|
22968
|
+
}
|
|
22969
|
+
}
|
|
22970
|
+
let continuations = this.findTripContinuations(markedStops, edgesAtCurrentRound);
|
|
22971
|
+
const stopsFromContinuations = new Set();
|
|
22972
|
+
while (continuations.length > 0) {
|
|
22973
|
+
stopsFromContinuations.clear();
|
|
22974
|
+
for (const continuation of continuations) {
|
|
22975
|
+
const route = this.timetable.getRoute(continuation.routeId);
|
|
22976
|
+
for (const stop of this.scanRouteContinuation(route, continuation.stopIndex, round, state, continuation)) {
|
|
22977
|
+
stopsFromContinuations.add(stop);
|
|
22978
|
+
markedStops.add(stop);
|
|
22979
|
+
}
|
|
22980
|
+
}
|
|
22981
|
+
continuations = this.findTripContinuations(stopsFromContinuations, edgesAtCurrentRound);
|
|
22982
|
+
}
|
|
22983
|
+
for (const stop of this.considerTransfers(options, round, markedStops, state)) {
|
|
22984
|
+
markedStops.add(stop);
|
|
22985
|
+
}
|
|
22986
|
+
if (markedStops.size === 0)
|
|
22987
|
+
break;
|
|
22988
|
+
}
|
|
22989
|
+
}
|
|
22990
|
+
/**
|
|
22991
|
+
* Finds trip continuations for the given marked stops and edges at the current round.
|
|
22992
|
+
* @param markedStops The set of marked stops.
|
|
22993
|
+
* @param edgesAtCurrentRound The array of edges at the current round, indexed by stop ID.
|
|
22994
|
+
* @returns An array of trip continuations.
|
|
22995
|
+
*/
|
|
22996
|
+
findTripContinuations(markedStops, edgesAtCurrentRound) {
|
|
22997
|
+
const continuations = [];
|
|
22998
|
+
for (const stopId of markedStops) {
|
|
22999
|
+
const arrival = edgesAtCurrentRound[stopId];
|
|
23000
|
+
if (!arrival || !('routeId' in arrival))
|
|
23001
|
+
continue;
|
|
23002
|
+
const continuousTrips = this.timetable.getContinuousTrips(arrival.hopOffStopIndex, arrival.routeId, arrival.tripIndex);
|
|
23003
|
+
for (const trip of continuousTrips) {
|
|
23004
|
+
continuations.push({
|
|
23005
|
+
routeId: trip.routeId,
|
|
23006
|
+
stopIndex: trip.stopIndex,
|
|
23007
|
+
tripIndex: trip.tripIndex,
|
|
23008
|
+
previousEdge: arrival,
|
|
23009
|
+
});
|
|
23010
|
+
}
|
|
23011
|
+
}
|
|
23012
|
+
return continuations;
|
|
22189
23013
|
}
|
|
22190
23014
|
/**
|
|
22191
23015
|
* Scans a route for an in-seat trip continuation.
|
|
@@ -22198,11 +23022,11 @@ class Router {
|
|
|
22198
23022
|
* @param round The current RAPTOR round
|
|
22199
23023
|
* @param routingState Current routing state
|
|
22200
23024
|
* @param tripContinuation The in-seat continuation descriptor
|
|
23025
|
+
* @param shared Optional shared state for Range RAPTOR mode
|
|
22201
23026
|
*/
|
|
22202
|
-
scanRouteContinuation(route, hopOnStopIndex, round,
|
|
23027
|
+
scanRouteContinuation(route, hopOnStopIndex, round, state, tripContinuation) {
|
|
22203
23028
|
const newlyMarkedStops = new Set();
|
|
22204
|
-
const edgesAtCurrentRound =
|
|
22205
|
-
const earliestArrivalAtAnyDestination = this.earliestArrivalAtAnyStop(routingState);
|
|
23029
|
+
const edgesAtCurrentRound = state.graph[round];
|
|
22206
23030
|
const nbStops = route.getNbStops();
|
|
22207
23031
|
const routeId = route.id;
|
|
22208
23032
|
const tripIndex = tripContinuation.tripIndex;
|
|
@@ -22212,10 +23036,9 @@ class Router {
|
|
|
22212
23036
|
const currentStop = route.stops[currentStopIndex];
|
|
22213
23037
|
const arrivalTime = route.arrivalAtOffset(currentStopIndex, tripStopOffset);
|
|
22214
23038
|
const dropOffType = route.dropOffTypeAtOffset(currentStopIndex, tripStopOffset);
|
|
22215
|
-
const earliestArrivalAtCurrentStop = routingState.arrivalTime(currentStop);
|
|
22216
23039
|
if (dropOffType !== NOT_AVAILABLE &&
|
|
22217
|
-
arrivalTime <
|
|
22218
|
-
arrivalTime <
|
|
23040
|
+
arrivalTime < state.improvementBound(round, currentStop) &&
|
|
23041
|
+
arrivalTime < state.destinationBest) {
|
|
22219
23042
|
edgesAtCurrentRound[currentStop] = {
|
|
22220
23043
|
routeId,
|
|
22221
23044
|
stopIndex: hopOnStopIndex,
|
|
@@ -22224,7 +23047,7 @@ class Router {
|
|
|
22224
23047
|
hopOffStopIndex: currentStopIndex,
|
|
22225
23048
|
continuationOf: previousEdge,
|
|
22226
23049
|
};
|
|
22227
|
-
|
|
23050
|
+
state.updateArrival(currentStop, arrivalTime, round);
|
|
22228
23051
|
newlyMarkedStops.add(currentStop);
|
|
22229
23052
|
}
|
|
22230
23053
|
}
|
|
@@ -22241,20 +23064,18 @@ class Router {
|
|
|
22241
23064
|
* @param route The route to scan
|
|
22242
23065
|
* @param hopOnStopIndex The stop index where passengers can first board
|
|
22243
23066
|
* @param round The current RAPTOR round
|
|
22244
|
-
* @param
|
|
23067
|
+
* @param state Current routing state
|
|
22245
23068
|
* @param options Query options (minTransferTime, etc.)
|
|
22246
23069
|
*/
|
|
22247
|
-
scanRoute(route, hopOnStopIndex, round,
|
|
23070
|
+
scanRoute(route, hopOnStopIndex, round, state, options) {
|
|
22248
23071
|
const newlyMarkedStops = new Set();
|
|
22249
|
-
const edgesAtCurrentRound =
|
|
22250
|
-
const edgesAtPreviousRound =
|
|
22251
|
-
const earliestArrivalAtAnyDestination = this.earliestArrivalAtAnyStop(routingState);
|
|
23072
|
+
const edgesAtCurrentRound = state.graph[round];
|
|
23073
|
+
const edgesAtPreviousRound = state.graph[round - 1];
|
|
22252
23074
|
const nbStops = route.getNbStops();
|
|
22253
23075
|
const routeId = route.id;
|
|
22254
23076
|
let activeTripIndex;
|
|
22255
23077
|
let activeTripBoardStopIndex = hopOnStopIndex;
|
|
22256
23078
|
// tripStopOffset = activeTripIndex * nbStops, precomputed when the trip changes.
|
|
22257
|
-
// Only valid while activeTripIndex !== undefined.
|
|
22258
23079
|
let activeTripStopOffset = 0;
|
|
22259
23080
|
for (let currentStopIndex = hopOnStopIndex; currentStopIndex < nbStops; currentStopIndex++) {
|
|
22260
23081
|
const currentStop = route.stops[currentStopIndex];
|
|
@@ -22262,10 +23083,9 @@ class Router {
|
|
|
22262
23083
|
if (activeTripIndex !== undefined) {
|
|
22263
23084
|
const arrivalTime = route.arrivalAtOffset(currentStopIndex, activeTripStopOffset);
|
|
22264
23085
|
const dropOffType = route.dropOffTypeAtOffset(currentStopIndex, activeTripStopOffset);
|
|
22265
|
-
const earliestArrivalAtCurrentStop = routingState.arrivalTime(currentStop);
|
|
22266
23086
|
if (dropOffType !== NOT_AVAILABLE &&
|
|
22267
|
-
arrivalTime <
|
|
22268
|
-
arrivalTime <
|
|
23087
|
+
arrivalTime < state.improvementBound(round, currentStop) &&
|
|
23088
|
+
arrivalTime < state.destinationBest) {
|
|
22269
23089
|
edgesAtCurrentRound[currentStop] = {
|
|
22270
23090
|
routeId,
|
|
22271
23091
|
stopIndex: activeTripBoardStopIndex,
|
|
@@ -22273,7 +23093,7 @@ class Router {
|
|
|
22273
23093
|
arrival: arrivalTime,
|
|
22274
23094
|
hopOffStopIndex: currentStopIndex,
|
|
22275
23095
|
};
|
|
22276
|
-
|
|
23096
|
+
state.updateArrival(currentStop, arrivalTime, round);
|
|
22277
23097
|
newlyMarkedStops.add(currentStop);
|
|
22278
23098
|
}
|
|
22279
23099
|
}
|
|
@@ -22283,142 +23103,166 @@ class Router {
|
|
|
22283
23103
|
if (earliestArrivalOnPreviousRound !== undefined &&
|
|
22284
23104
|
(activeTripIndex === undefined ||
|
|
22285
23105
|
earliestArrivalOnPreviousRound <=
|
|
22286
|
-
route.
|
|
23106
|
+
route.departureAtOffset(currentStopIndex, activeTripStopOffset))) {
|
|
22287
23107
|
const earliestTrip = route.findEarliestTrip(currentStopIndex, earliestArrivalOnPreviousRound, activeTripIndex);
|
|
22288
23108
|
if (earliestTrip === undefined) {
|
|
22289
23109
|
continue;
|
|
22290
23110
|
}
|
|
22291
|
-
const
|
|
22292
|
-
|
|
22293
|
-
|
|
23111
|
+
const fromTripStop = previousEdge && 'routeId' in previousEdge
|
|
23112
|
+
? {
|
|
23113
|
+
stopIndex: previousEdge.hopOffStopIndex,
|
|
23114
|
+
routeId: previousEdge.routeId,
|
|
23115
|
+
tripIndex: previousEdge.tripIndex,
|
|
23116
|
+
}
|
|
23117
|
+
: undefined;
|
|
23118
|
+
const firstBoardableTrip = this.timetable.findFirstBoardableTrip(currentStopIndex, route, earliestTrip, earliestArrivalOnPreviousRound, activeTripIndex, fromTripStop, options.minTransferTime);
|
|
22294
23119
|
if (firstBoardableTrip !== undefined) {
|
|
22295
|
-
|
|
22296
|
-
|
|
22297
|
-
|
|
23120
|
+
// At round 1, enforce maxInitialWaitingTime: skip boarding if the
|
|
23121
|
+
// traveler would have to wait longer than the allowed threshold at
|
|
23122
|
+
// the first boarding stop.
|
|
23123
|
+
const exceedsInitialWait = round === 1 &&
|
|
23124
|
+
options.maxInitialWaitingTime !== undefined &&
|
|
23125
|
+
route.departureFrom(currentStopIndex, firstBoardableTrip) -
|
|
23126
|
+
earliestArrivalOnPreviousRound >
|
|
23127
|
+
options.maxInitialWaitingTime;
|
|
23128
|
+
if (!exceedsInitialWait) {
|
|
23129
|
+
activeTripIndex = firstBoardableTrip;
|
|
23130
|
+
activeTripBoardStopIndex = currentStopIndex;
|
|
23131
|
+
activeTripStopOffset = route.tripStopOffset(firstBoardableTrip);
|
|
23132
|
+
}
|
|
22298
23133
|
}
|
|
22299
23134
|
}
|
|
22300
23135
|
}
|
|
22301
23136
|
return newlyMarkedStops;
|
|
22302
23137
|
}
|
|
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
23138
|
/**
|
|
22346
23139
|
* Processes all currently marked stops to find available transfers
|
|
22347
23140
|
* and determines if using these transfers would result in earlier arrival times
|
|
22348
23141
|
* at destination stops. It handles different transfer types including in-seat
|
|
22349
23142
|
* transfers and walking transfers with appropriate minimum transfer times.
|
|
22350
23143
|
*
|
|
22351
|
-
* @param
|
|
23144
|
+
* @param options Query options (minTransferTime, etc.)
|
|
22352
23145
|
* @param round The current round number in the RAPTOR algorithm
|
|
22353
|
-
* @param
|
|
23146
|
+
* @param markedStops The set of currently marked stops
|
|
23147
|
+
* @param state Current routing state
|
|
22354
23148
|
*/
|
|
22355
|
-
considerTransfers(
|
|
22356
|
-
const { options } = query;
|
|
22357
|
-
const arrivalsAtCurrentRound = routingState.graph[round];
|
|
23149
|
+
considerTransfers(options, round, markedStops, state) {
|
|
22358
23150
|
const newlyMarkedStops = new Set();
|
|
23151
|
+
const arrivalsAtCurrentRound = state.graph[round];
|
|
22359
23152
|
for (const stop of markedStops) {
|
|
22360
23153
|
const currentArrival = arrivalsAtCurrentRound[stop];
|
|
22361
23154
|
// Skip transfers if the last leg was also a transfer
|
|
22362
23155
|
if (!currentArrival || 'type' in currentArrival)
|
|
22363
23156
|
continue;
|
|
22364
23157
|
const transfers = this.timetable.getTransfers(stop);
|
|
22365
|
-
for (
|
|
22366
|
-
const transfer = transfers[j];
|
|
23158
|
+
for (const transfer of transfers) {
|
|
22367
23159
|
let transferTime;
|
|
22368
23160
|
if (transfer.minTransferTime) {
|
|
22369
23161
|
transferTime = transfer.minTransferTime;
|
|
22370
23162
|
}
|
|
22371
23163
|
else if (transfer.type === 'IN_SEAT') {
|
|
22372
|
-
// TODO not needed anymore now that trip continuations are handled separately
|
|
22373
23164
|
transferTime = DURATION_ZERO;
|
|
22374
23165
|
}
|
|
22375
23166
|
else {
|
|
22376
23167
|
transferTime = options.minTransferTime;
|
|
22377
23168
|
}
|
|
22378
23169
|
const arrivalAfterTransfer = currentArrival.arrival + transferTime;
|
|
22379
|
-
|
|
22380
|
-
|
|
23170
|
+
if (arrivalAfterTransfer <
|
|
23171
|
+
state.improvementBound(round, transfer.destination) &&
|
|
23172
|
+
arrivalAfterTransfer < state.destinationBest) {
|
|
22381
23173
|
arrivalsAtCurrentRound[transfer.destination] = {
|
|
22382
23174
|
arrival: arrivalAfterTransfer,
|
|
22383
23175
|
from: stop,
|
|
22384
23176
|
to: transfer.destination,
|
|
22385
|
-
minTransferTime:
|
|
23177
|
+
minTransferTime: transferTime || undefined,
|
|
22386
23178
|
type: transfer.type,
|
|
22387
23179
|
};
|
|
22388
|
-
|
|
23180
|
+
state.updateArrival(transfer.destination, arrivalAfterTransfer, round);
|
|
22389
23181
|
newlyMarkedStops.add(transfer.destination);
|
|
22390
23182
|
}
|
|
22391
23183
|
}
|
|
22392
23184
|
}
|
|
22393
23185
|
return newlyMarkedStops;
|
|
22394
23186
|
}
|
|
23187
|
+
}
|
|
23188
|
+
|
|
23189
|
+
/**
|
|
23190
|
+
* A public transportation router implementing the RAPTOR and Range RAPTOR
|
|
23191
|
+
* algorithms.
|
|
23192
|
+
*
|
|
23193
|
+
* Thin facade over {@link PlainRouter} and {@link RangeRouter}: constructs the
|
|
23194
|
+
* shared {@link Raptor} engine and {@link AccessFinder} once and delegates each
|
|
23195
|
+
* query to the appropriate router.
|
|
23196
|
+
*
|
|
23197
|
+
* @see https://www.microsoft.com/en-us/research/wp-content/uploads/2012/01/raptor_alenex.pdf
|
|
23198
|
+
*/
|
|
23199
|
+
class Router {
|
|
23200
|
+
constructor(timetable, stopsIndex) {
|
|
23201
|
+
const raptor = new Raptor(timetable);
|
|
23202
|
+
const accessFinder = new AccessFinder(timetable, stopsIndex);
|
|
23203
|
+
this.plainRouter = new PlainRouter(timetable, stopsIndex, accessFinder, raptor);
|
|
23204
|
+
this.rangeRouter = new RangeRouter(timetable, stopsIndex, accessFinder, raptor);
|
|
23205
|
+
}
|
|
22395
23206
|
/**
|
|
22396
|
-
*
|
|
22397
|
-
*
|
|
22398
|
-
* @param routingState The routing state containing arrival times and destinations.
|
|
22399
|
-
* @returns The earliest arrival time among the provided destinations.
|
|
23207
|
+
* Standard RAPTOR: finds the earliest-arrival journey from `query.from` to
|
|
23208
|
+
* `query.to` for the given departure time.
|
|
22400
23209
|
*/
|
|
22401
|
-
|
|
22402
|
-
|
|
22403
|
-
|
|
22404
|
-
|
|
22405
|
-
|
|
22406
|
-
|
|
22407
|
-
|
|
22408
|
-
|
|
22409
|
-
return
|
|
23210
|
+
route(query) {
|
|
23211
|
+
return this.plainRouter.route(query);
|
|
23212
|
+
}
|
|
23213
|
+
/**
|
|
23214
|
+
* Range RAPTOR: finds all Pareto-optimal journeys within the departure-time
|
|
23215
|
+
* window `[query.departureTime, query.lastDepartureTime]`.
|
|
23216
|
+
*/
|
|
23217
|
+
rangeRoute(query) {
|
|
23218
|
+
return this.rangeRouter.rangeRoute(query);
|
|
22410
23219
|
}
|
|
22411
23220
|
}
|
|
22412
23221
|
|
|
23222
|
+
const renderTable = (columns, rows, footerRow) => {
|
|
23223
|
+
const bar = (l, m, r) => l + columns.map((c) => '─'.repeat(c.width + 2)).join(m) + r;
|
|
23224
|
+
const renderRow = (cells) => '│' +
|
|
23225
|
+
cells
|
|
23226
|
+
.map((cell, i) => {
|
|
23227
|
+
var _a, _b, _c, _d;
|
|
23228
|
+
const width = (_b = (_a = columns[i]) === null || _a === void 0 ? void 0 : _a.width) !== null && _b !== void 0 ? _b : 0;
|
|
23229
|
+
const align = (_d = (_c = columns[i]) === null || _c === void 0 ? void 0 : _c.align) !== null && _d !== void 0 ? _d : 'left';
|
|
23230
|
+
const padded = align === 'right' ? cell.padStart(width) : cell.padEnd(width);
|
|
23231
|
+
return ` ${padded} `;
|
|
23232
|
+
})
|
|
23233
|
+
.join('│') +
|
|
23234
|
+
'│';
|
|
23235
|
+
return [
|
|
23236
|
+
bar('┌', '┬', '┐'),
|
|
23237
|
+
renderRow(columns.map((c) => c.header)),
|
|
23238
|
+
bar('├', '┼', '┤'),
|
|
23239
|
+
...rows.map(renderRow),
|
|
23240
|
+
bar('├', '┼', '┤'),
|
|
23241
|
+
renderRow(footerRow),
|
|
23242
|
+
bar('└', '┴', '┘'),
|
|
23243
|
+
].join('\n');
|
|
23244
|
+
};
|
|
23245
|
+
// ─── Query label ──────────────────────────────────────────────────────────────
|
|
23246
|
+
const buildQueryLabel = (query, stopsIndex) => {
|
|
23247
|
+
var _a, _b;
|
|
23248
|
+
const fromName = (_b = (_a = stopsIndex.findStopById(query.from)) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : String(query.from);
|
|
23249
|
+
const toNames = [...query.to]
|
|
23250
|
+
.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); })
|
|
23251
|
+
.join(' / ');
|
|
23252
|
+
const dep = timeToString(query.departureTime);
|
|
23253
|
+
if (query instanceof RangeQuery) {
|
|
23254
|
+
const lastDep = timeToString(query.lastDepartureTime);
|
|
23255
|
+
return `${fromName} → ${toNames} ${dep}–${lastDep}`;
|
|
23256
|
+
}
|
|
23257
|
+
return `${fromName} → ${toNames} ${dep}`;
|
|
23258
|
+
};
|
|
23259
|
+
// ─── Query loaders ────────────────────────────────────────────────────────────
|
|
22413
23260
|
/**
|
|
22414
23261
|
* Loads a list of routing queries from a JSON file and resolves the
|
|
22415
23262
|
* human-readable stop IDs to the internal numeric IDs used by the router.
|
|
22416
23263
|
*
|
|
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.
|
|
23264
|
+
* Only entries that do **not** carry a `lastDepartureTime` field are loaded —
|
|
23265
|
+
* range-query entries are silently skipped.
|
|
22422
23266
|
*
|
|
22423
23267
|
* @param filePath - Path to the JSON file containing the serialized queries.
|
|
22424
23268
|
* @param stopsIndex - The stops index used to resolve source stop IDs to the
|
|
@@ -22431,7 +23275,9 @@ class Router {
|
|
|
22431
23275
|
const loadQueriesFromJson = (filePath, stopsIndex) => {
|
|
22432
23276
|
const fileContent = fs.readFileSync(filePath, 'utf-8');
|
|
22433
23277
|
const serializedQueries = JSON.parse(fileContent);
|
|
22434
|
-
return serializedQueries
|
|
23278
|
+
return serializedQueries
|
|
23279
|
+
.filter((q) => q.lastDepartureTime === undefined)
|
|
23280
|
+
.map((serializedQuery) => {
|
|
22435
23281
|
const fromStop = stopsIndex.findStopBySourceStopId(serializedQuery.from);
|
|
22436
23282
|
const toStops = Array.from(serializedQuery.to).map((stopId) => stopsIndex.findStopBySourceStopId(stopId));
|
|
22437
23283
|
if (!fromStop || toStops.some((toStop) => !toStop)) {
|
|
@@ -22448,6 +23294,46 @@ const loadQueriesFromJson = (filePath, stopsIndex) => {
|
|
|
22448
23294
|
return queryBuilder.build();
|
|
22449
23295
|
});
|
|
22450
23296
|
};
|
|
23297
|
+
/**
|
|
23298
|
+
* Loads a list of range routing queries from a JSON file and resolves the
|
|
23299
|
+
* human-readable stop IDs to the internal numeric IDs used by the router.
|
|
23300
|
+
*
|
|
23301
|
+
* Only entries that carry a `lastDepartureTime` field are loaded — plain
|
|
23302
|
+
* point-query entries are silently skipped.
|
|
23303
|
+
*
|
|
23304
|
+
* @param filePath - Path to the JSON file containing the serialized queries.
|
|
23305
|
+
* @param stopsIndex - The stops index used to resolve source stop IDs to the
|
|
23306
|
+
* internal numeric IDs expected by the router.
|
|
23307
|
+
* @returns An array of fully constructed {@link RangeQuery} objects ready to
|
|
23308
|
+
* be passed to {@link Router.rangeRoute}.
|
|
23309
|
+
* @throws If the file cannot be read, the JSON is malformed, or any stop ID
|
|
23310
|
+
* referenced in the file cannot be found in the stops index.
|
|
23311
|
+
*/
|
|
23312
|
+
const loadRangeQueriesFromJson = (filePath, stopsIndex) => {
|
|
23313
|
+
const fileContent = fs.readFileSync(filePath, 'utf-8');
|
|
23314
|
+
const serializedQueries = JSON.parse(fileContent);
|
|
23315
|
+
return serializedQueries
|
|
23316
|
+
.filter((q) => q.lastDepartureTime !== undefined)
|
|
23317
|
+
.map((serializedQuery) => {
|
|
23318
|
+
const fromStop = stopsIndex.findStopBySourceStopId(serializedQuery.from);
|
|
23319
|
+
const toStops = Array.from(serializedQuery.to).map((stopId) => stopsIndex.findStopBySourceStopId(stopId));
|
|
23320
|
+
if (!fromStop || toStops.some((toStop) => !toStop)) {
|
|
23321
|
+
throw new Error(`Invalid task: Start or end station not found for task ${JSON.stringify(serializedQuery)}`);
|
|
23322
|
+
}
|
|
23323
|
+
const queryBuilder = new RangeQuery.Builder()
|
|
23324
|
+
.from(fromStop.id)
|
|
23325
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
23326
|
+
.to(new Set(toStops.map((stop) => stop.id)))
|
|
23327
|
+
.departureTime(timeFromString(serializedQuery.departureTime))
|
|
23328
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
23329
|
+
.lastDepartureTime(timeFromString(serializedQuery.lastDepartureTime));
|
|
23330
|
+
if (serializedQuery.maxTransfers !== undefined) {
|
|
23331
|
+
queryBuilder.maxTransfers(serializedQuery.maxTransfers);
|
|
23332
|
+
}
|
|
23333
|
+
return queryBuilder.build();
|
|
23334
|
+
});
|
|
23335
|
+
};
|
|
23336
|
+
// ─── Benchmark runners ────────────────────────────────────────────────────────
|
|
22451
23337
|
/**
|
|
22452
23338
|
* Benchmarks {@link Router.route} across a set of queries.
|
|
22453
23339
|
*
|
|
@@ -22456,10 +23342,11 @@ const loadQueriesFromJson = (filePath, stopsIndex) => {
|
|
|
22456
23342
|
* produced per query.
|
|
22457
23343
|
* @param iterations - Number of times each query is repeated. Higher values
|
|
22458
23344
|
* yield a more stable mean at the cost of longer wall-clock time.
|
|
23345
|
+
* @param stopsIndex - Used to resolve stop names for result labels.
|
|
22459
23346
|
* @returns An array of {@link PerformanceResult} objects, one per query, each
|
|
22460
23347
|
* containing the mean wall-clock time (µs) and mean heap delta (MB).
|
|
22461
23348
|
*/
|
|
22462
|
-
const testRouterPerformance = (router, tasks, iterations) => {
|
|
23349
|
+
const testRouterPerformance = (router, tasks, iterations, stopsIndex) => {
|
|
22463
23350
|
const results = [];
|
|
22464
23351
|
for (const task of tasks) {
|
|
22465
23352
|
let totalTime = 0;
|
|
@@ -22479,7 +23366,7 @@ const testRouterPerformance = (router, tasks, iterations) => {
|
|
|
22479
23366
|
}
|
|
22480
23367
|
}
|
|
22481
23368
|
results.push({
|
|
22482
|
-
task,
|
|
23369
|
+
label: buildQueryLabel(task, stopsIndex),
|
|
22483
23370
|
meanTimeUs: totalTime / iterations,
|
|
22484
23371
|
meanMemoryMb: totalMemory / iterations / (1024 * 1024),
|
|
22485
23372
|
});
|
|
@@ -22495,14 +23382,14 @@ const testRouterPerformance = (router, tasks, iterations) => {
|
|
|
22495
23382
|
* @param tasks - The list of queries to benchmark. One {@link PerformanceResult}
|
|
22496
23383
|
* is produced per query.
|
|
22497
23384
|
* @param iterations - Number of times `bestRoute` is called per query.
|
|
23385
|
+
* @param stopsIndex - Used to resolve stop names for result labels.
|
|
22498
23386
|
* @returns An array of {@link PerformanceResult} objects, one per query, each
|
|
22499
23387
|
* containing the mean wall-clock time (µs) and mean heap delta (MB) for the
|
|
22500
23388
|
* `bestRoute` call alone.
|
|
22501
23389
|
*/
|
|
22502
|
-
const testBestRoutePerformance = (router, tasks, iterations) => {
|
|
23390
|
+
const testBestRoutePerformance = (router, tasks, iterations, stopsIndex) => {
|
|
22503
23391
|
const results = [];
|
|
22504
23392
|
for (const task of tasks) {
|
|
22505
|
-
// Compute the routing result once — this is not part of the benchmark.
|
|
22506
23393
|
const result = router.route(task);
|
|
22507
23394
|
let totalTime = 0;
|
|
22508
23395
|
let totalMemory = 0;
|
|
@@ -22521,7 +23408,45 @@ const testBestRoutePerformance = (router, tasks, iterations) => {
|
|
|
22521
23408
|
}
|
|
22522
23409
|
}
|
|
22523
23410
|
results.push({
|
|
22524
|
-
task,
|
|
23411
|
+
label: buildQueryLabel(task, stopsIndex),
|
|
23412
|
+
meanTimeUs: totalTime / iterations,
|
|
23413
|
+
meanMemoryMb: totalMemory / iterations / (1024 * 1024),
|
|
23414
|
+
});
|
|
23415
|
+
}
|
|
23416
|
+
return results;
|
|
23417
|
+
};
|
|
23418
|
+
/**
|
|
23419
|
+
* Benchmarks {@link Router.rangeRoute} across a set of range queries.
|
|
23420
|
+
*
|
|
23421
|
+
* @param router - The router instance to benchmark.
|
|
23422
|
+
* @param tasks - The list of range queries to run. One {@link PerformanceResult}
|
|
23423
|
+
* is produced per query.
|
|
23424
|
+
* @param iterations - Number of times each query is repeated.
|
|
23425
|
+
* @param stopsIndex - Used to resolve stop names for result labels.
|
|
23426
|
+
* @returns An array of {@link PerformanceResult} objects, one per query, each
|
|
23427
|
+
* containing the mean wall-clock time (µs) and mean heap delta (MB).
|
|
23428
|
+
*/
|
|
23429
|
+
const testRangeRouterPerformance = (router, tasks, iterations, stopsIndex) => {
|
|
23430
|
+
const results = [];
|
|
23431
|
+
for (const task of tasks) {
|
|
23432
|
+
let totalTime = 0;
|
|
23433
|
+
let totalMemory = 0;
|
|
23434
|
+
for (let i = 0; i < iterations; i++) {
|
|
23435
|
+
if (global.gc) {
|
|
23436
|
+
global.gc();
|
|
23437
|
+
}
|
|
23438
|
+
const startMemory = process.memoryUsage().heapUsed;
|
|
23439
|
+
const startTime = performance$1.now();
|
|
23440
|
+
router.rangeRoute(task);
|
|
23441
|
+
const endTime = performance$1.now();
|
|
23442
|
+
const endMemory = process.memoryUsage().heapUsed;
|
|
23443
|
+
totalTime += (endTime - startTime) * 1000;
|
|
23444
|
+
if (endMemory >= startMemory) {
|
|
23445
|
+
totalMemory += endMemory - startMemory;
|
|
23446
|
+
}
|
|
23447
|
+
}
|
|
23448
|
+
results.push({
|
|
23449
|
+
label: buildQueryLabel(task, stopsIndex),
|
|
22525
23450
|
meanTimeUs: totalTime / iterations,
|
|
22526
23451
|
meanMemoryMb: totalMemory / iterations / (1024 * 1024),
|
|
22527
23452
|
});
|
|
@@ -22529,38 +23454,93 @@ const testBestRoutePerformance = (router, tasks, iterations) => {
|
|
|
22529
23454
|
return results;
|
|
22530
23455
|
};
|
|
22531
23456
|
/**
|
|
22532
|
-
*
|
|
23457
|
+
* Benchmarks {@link RangeResult.getRoutes} — the full Pareto-frontier
|
|
23458
|
+
* reconstruction phase — independently of the range routing phase.
|
|
22533
23459
|
*
|
|
22534
|
-
*
|
|
22535
|
-
*
|
|
22536
|
-
*
|
|
22537
|
-
*
|
|
23460
|
+
* @param router - The router instance used to produce the range results that
|
|
23461
|
+
* are then fed into `getRoutes`.
|
|
23462
|
+
* @param tasks - The list of range queries to benchmark. One
|
|
23463
|
+
* {@link PerformanceResult} is produced per query.
|
|
23464
|
+
* @param iterations - Number of times `getRoutes` is called per query.
|
|
23465
|
+
* @param stopsIndex - Used to resolve stop names for result labels.
|
|
23466
|
+
* @returns An array of {@link PerformanceResult} objects, one per query, each
|
|
23467
|
+
* containing the mean wall-clock time (µs) and mean heap delta (MB) for the
|
|
23468
|
+
* `getRoutes` call alone.
|
|
23469
|
+
*/
|
|
23470
|
+
const testRangeResultPerformance = (router, tasks, iterations, stopsIndex) => {
|
|
23471
|
+
const results = [];
|
|
23472
|
+
for (const task of tasks) {
|
|
23473
|
+
const rangeResult = router.rangeRoute(task);
|
|
23474
|
+
let totalTime = 0;
|
|
23475
|
+
let totalMemory = 0;
|
|
23476
|
+
for (let i = 0; i < iterations; i++) {
|
|
23477
|
+
if (global.gc) {
|
|
23478
|
+
global.gc();
|
|
23479
|
+
}
|
|
23480
|
+
const startMemory = process.memoryUsage().heapUsed;
|
|
23481
|
+
const startTime = performance$1.now();
|
|
23482
|
+
rangeResult.getRoutes();
|
|
23483
|
+
const endTime = performance$1.now();
|
|
23484
|
+
const endMemory = process.memoryUsage().heapUsed;
|
|
23485
|
+
totalTime += (endTime - startTime) * 1000;
|
|
23486
|
+
if (endMemory >= startMemory) {
|
|
23487
|
+
totalMemory += endMemory - startMemory;
|
|
23488
|
+
}
|
|
23489
|
+
}
|
|
23490
|
+
results.push({
|
|
23491
|
+
label: buildQueryLabel(task, stopsIndex),
|
|
23492
|
+
meanTimeUs: totalTime / iterations,
|
|
23493
|
+
meanMemoryMb: totalMemory / iterations / (1024 * 1024),
|
|
23494
|
+
});
|
|
23495
|
+
}
|
|
23496
|
+
return results;
|
|
23497
|
+
};
|
|
23498
|
+
// ─── Output ───────────────────────────────────────────────────────────────────
|
|
23499
|
+
/**
|
|
23500
|
+
* Prints a table summary of performance results to stdout.
|
|
23501
|
+
*
|
|
23502
|
+
* Each row corresponds to one task, identified by a human-readable query label
|
|
23503
|
+
* (origin → destination + departure time). A footer row shows the mean across
|
|
23504
|
+
* all tasks. An optional `label` is printed as a section header above the table.
|
|
22538
23505
|
*
|
|
22539
|
-
* @param results - The performance results to display
|
|
22540
|
-
*
|
|
22541
|
-
* @param label - Optional heading printed above the results block.
|
|
22542
|
-
* Defaults to `'Performance Results'`.
|
|
23506
|
+
* @param results - The performance results to display.
|
|
23507
|
+
* @param label - Heading printed above the table. Defaults to `'Performance Results'`.
|
|
22543
23508
|
*/
|
|
22544
23509
|
const prettyPrintPerformanceResults = (results, label = 'Performance Results') => {
|
|
23510
|
+
console.log(`\n${label}`);
|
|
22545
23511
|
if (results.length === 0) {
|
|
22546
|
-
console.log('
|
|
23512
|
+
console.log(' (no results)');
|
|
22547
23513
|
return;
|
|
22548
23514
|
}
|
|
22549
|
-
const
|
|
22550
|
-
|
|
22551
|
-
const
|
|
22552
|
-
|
|
22553
|
-
|
|
22554
|
-
|
|
22555
|
-
|
|
22556
|
-
|
|
22557
|
-
|
|
22558
|
-
|
|
22559
|
-
|
|
22560
|
-
|
|
22561
|
-
|
|
22562
|
-
|
|
23515
|
+
const fmtTime = (n) => Math.round(n).toLocaleString('en-US');
|
|
23516
|
+
const fmtMem = (n) => n.toFixed(2);
|
|
23517
|
+
const meanTime = results.reduce((s, r) => s + r.meanTimeUs, 0) / results.length;
|
|
23518
|
+
const meanMem = results.reduce((s, r) => s + r.meanMemoryMb, 0) / results.length;
|
|
23519
|
+
const queryHeader = 'Query';
|
|
23520
|
+
const timeHeader = 'Time (µs)';
|
|
23521
|
+
const memHeader = 'Mem (MB)';
|
|
23522
|
+
const timeVals = results.map((r) => fmtTime(r.meanTimeUs));
|
|
23523
|
+
const memVals = results.map((r) => fmtMem(r.meanMemoryMb));
|
|
23524
|
+
const meanTimeStr = fmtTime(meanTime);
|
|
23525
|
+
const meanMemStr = fmtMem(meanMem);
|
|
23526
|
+
const queryWidth = Math.max(queryHeader.length, 'mean'.length, ...results.map((r) => r.label.length));
|
|
23527
|
+
const timeWidth = Math.max(timeHeader.length, meanTimeStr.length, ...timeVals.map((v) => v.length));
|
|
23528
|
+
const memWidth = Math.max(memHeader.length, meanMemStr.length, ...memVals.map((v) => v.length));
|
|
23529
|
+
const columns = [
|
|
23530
|
+
{ header: queryHeader, width: queryWidth, align: 'left' },
|
|
23531
|
+
{ header: timeHeader, width: timeWidth, align: 'right' },
|
|
23532
|
+
{ header: memHeader, width: memWidth, align: 'right' },
|
|
23533
|
+
];
|
|
23534
|
+
const rows = results.map((r, i) => {
|
|
23535
|
+
var _a, _b;
|
|
23536
|
+
return [
|
|
23537
|
+
r.label,
|
|
23538
|
+
(_a = timeVals[i]) !== null && _a !== void 0 ? _a : '',
|
|
23539
|
+
(_b = memVals[i]) !== null && _b !== void 0 ? _b : '',
|
|
23540
|
+
];
|
|
22563
23541
|
});
|
|
23542
|
+
const footer = ['mean', meanTimeStr, meanMemStr];
|
|
23543
|
+
console.log(renderTable(columns, rows, footer));
|
|
22564
23544
|
};
|
|
22565
23545
|
|
|
22566
23546
|
/**
|
|
@@ -22609,25 +23589,39 @@ const startRepl = (stopsPath, timetablePath) => {
|
|
|
22609
23589
|
},
|
|
22610
23590
|
});
|
|
22611
23591
|
replServer.defineCommand('route', {
|
|
22612
|
-
help: 'Find a route using .route from <
|
|
23592
|
+
help: 'Find a route using .route from <stop> to <stop> at <HH:mm> [before <HH:mm>] [with <N> transfers]',
|
|
22613
23593
|
action(routeQuery) {
|
|
22614
23594
|
this.clearBufferedCommand();
|
|
22615
23595
|
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
23596
|
const fromIndex = parts.indexOf('from');
|
|
22626
23597
|
const toIndex = parts.indexOf('to');
|
|
23598
|
+
const atIndex = parts.indexOf('at');
|
|
23599
|
+
const beforeIndex = parts.indexOf('before');
|
|
23600
|
+
const withIndex = parts.indexOf('with');
|
|
23601
|
+
if (fromIndex === -1 || toIndex === -1 || atIndex === -1) {
|
|
23602
|
+
console.log('Usage: .route from <stop> to <stop> at <HH:mm> [before <HH:mm>] [with <N> transfers]');
|
|
23603
|
+
this.displayPrompt();
|
|
23604
|
+
return;
|
|
23605
|
+
}
|
|
22627
23606
|
const fromId = parts.slice(fromIndex + 1, toIndex).join(' ');
|
|
22628
|
-
const toId = parts.slice(toIndex + 1,
|
|
23607
|
+
const toId = parts.slice(toIndex + 1, atIndex).join(' ');
|
|
23608
|
+
// atTime ends at 'before', 'with', or the end of the input.
|
|
23609
|
+
const atTimeEnd = beforeIndex !== -1
|
|
23610
|
+
? beforeIndex
|
|
23611
|
+
: withIndex !== -1
|
|
23612
|
+
? withIndex
|
|
23613
|
+
: parts.length;
|
|
23614
|
+
const atTime = parts.slice(atIndex + 1, atTimeEnd).join(' ');
|
|
23615
|
+
// beforeTime is only present when the 'before' keyword appears.
|
|
23616
|
+
const beforeTimeEnd = withIndex !== -1 ? withIndex : parts.length;
|
|
23617
|
+
const beforeTime = beforeIndex !== -1
|
|
23618
|
+
? parts.slice(beforeIndex + 1, beforeTimeEnd).join(' ')
|
|
23619
|
+
: undefined;
|
|
23620
|
+
const maxTransfers = withIndex !== -1 && parts[withIndex + 1] !== undefined
|
|
23621
|
+
? parseInt(parts[withIndex + 1])
|
|
23622
|
+
: 4;
|
|
22629
23623
|
if (!fromId || !toId || !atTime) {
|
|
22630
|
-
console.log('Usage: .route from <
|
|
23624
|
+
console.log('Usage: .route from <stop> to <stop> at <HH:mm> [before <HH:mm>] [with <N> transfers]');
|
|
22631
23625
|
this.displayPrompt();
|
|
22632
23626
|
return;
|
|
22633
23627
|
}
|
|
@@ -22651,30 +23645,61 @@ const startRepl = (stopsPath, timetablePath) => {
|
|
|
22651
23645
|
this.displayPrompt();
|
|
22652
23646
|
return;
|
|
22653
23647
|
}
|
|
22654
|
-
const departureTime = timeFromString(atTime);
|
|
22655
23648
|
try {
|
|
22656
|
-
const
|
|
22657
|
-
.from(fromStop.id)
|
|
22658
|
-
.to(toStop.id)
|
|
22659
|
-
.departureTime(departureTime)
|
|
22660
|
-
.maxTransfers(maxTransfers)
|
|
22661
|
-
.build();
|
|
23649
|
+
const departureTime = timeFromString(atTime);
|
|
22662
23650
|
const router = new Router(timetable, stopsIndex);
|
|
22663
|
-
|
|
22664
|
-
|
|
22665
|
-
|
|
22666
|
-
|
|
22667
|
-
|
|
22668
|
-
|
|
22669
|
-
|
|
22670
|
-
|
|
22671
|
-
|
|
22672
|
-
|
|
22673
|
-
|
|
22674
|
-
|
|
23651
|
+
if (beforeTime !== undefined) {
|
|
23652
|
+
const lastDepartureTime = timeFromString(beforeTime);
|
|
23653
|
+
const query = new RangeQuery.Builder()
|
|
23654
|
+
.from(fromStop.id)
|
|
23655
|
+
.to(toStop.id)
|
|
23656
|
+
.departureTime(departureTime)
|
|
23657
|
+
.lastDepartureTime(lastDepartureTime)
|
|
23658
|
+
.maxTransfers(maxTransfers)
|
|
23659
|
+
.build();
|
|
23660
|
+
const result = router.rangeRoute(query);
|
|
23661
|
+
if (result.size === 0) {
|
|
23662
|
+
console.log(`No journeys found from ${fromStop.name} to ${toStop.name} ` +
|
|
23663
|
+
`between ${atTime} and ${beforeTime}.`);
|
|
23664
|
+
}
|
|
23665
|
+
else {
|
|
23666
|
+
console.log(`Found ${result.size} Pareto-optimal journey${result.size === 1 ? '' : 's'} ` +
|
|
23667
|
+
`from ${fromStop.name} to ${toStop.name} ` +
|
|
23668
|
+
`(window ${atTime}–${beforeTime}):`);
|
|
23669
|
+
const routes = result.getRoutes();
|
|
23670
|
+
routes.forEach((route, index) => {
|
|
23671
|
+
const journeyNumber = index + 1;
|
|
23672
|
+
console.log(`\nJourney ${journeyNumber}:`);
|
|
23673
|
+
console.log(route.toString());
|
|
23674
|
+
});
|
|
23675
|
+
}
|
|
22675
23676
|
}
|
|
22676
23677
|
else {
|
|
22677
|
-
|
|
23678
|
+
const query = new Query.Builder()
|
|
23679
|
+
.from(fromStop.id)
|
|
23680
|
+
.to(toStop.id)
|
|
23681
|
+
.departureTime(departureTime)
|
|
23682
|
+
.maxTransfers(maxTransfers)
|
|
23683
|
+
.build();
|
|
23684
|
+
const result = router.route(query);
|
|
23685
|
+
const arrivalTime = result.arrivalAt(toStop.id);
|
|
23686
|
+
if (arrivalTime === undefined) {
|
|
23687
|
+
console.log(`Destination not reachable`);
|
|
23688
|
+
}
|
|
23689
|
+
else {
|
|
23690
|
+
const transfers = Math.max(0, arrivalTime.legNumber - 1);
|
|
23691
|
+
console.log(`Arriving to ${toStop.name} at ${timeToString(arrivalTime.arrival)} ` +
|
|
23692
|
+
`with ${transfers} transfer${transfers === 1 ? '' : 's'} ` +
|
|
23693
|
+
`from ${fromStop.name}.`);
|
|
23694
|
+
}
|
|
23695
|
+
const bestRoute = result.bestRoute(toStop.id);
|
|
23696
|
+
if (bestRoute) {
|
|
23697
|
+
console.log(`Found route from ${fromStop.name} to ${toStop.name}:`);
|
|
23698
|
+
console.log(bestRoute.toString());
|
|
23699
|
+
}
|
|
23700
|
+
else {
|
|
23701
|
+
console.log('No route found');
|
|
23702
|
+
}
|
|
22678
23703
|
}
|
|
22679
23704
|
}
|
|
22680
23705
|
catch (error) {
|
|
@@ -23034,10 +24059,15 @@ program
|
|
|
23034
24059
|
const router = new Router(timetable, stopsIndex);
|
|
23035
24060
|
const queries = loadQueriesFromJson(routesPath, stopsIndex);
|
|
23036
24061
|
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, '
|
|
24062
|
+
const routerResults = testRouterPerformance(router, queries, iterations, stopsIndex);
|
|
24063
|
+
prettyPrintPerformanceResults(routerResults, 'Point queries — router.route()');
|
|
24064
|
+
const bestRouteResults = testBestRoutePerformance(router, queries, iterations, stopsIndex);
|
|
24065
|
+
prettyPrintPerformanceResults(bestRouteResults, 'Point queries — result.bestRoute() (reconstruction only)');
|
|
24066
|
+
const rangeQueries = loadRangeQueriesFromJson(routesPath, stopsIndex);
|
|
24067
|
+
const rangeRouterResults = testRangeRouterPerformance(router, rangeQueries, iterations, stopsIndex);
|
|
24068
|
+
prettyPrintPerformanceResults(rangeRouterResults, 'Range queries — router.rangeRoute()');
|
|
24069
|
+
const rangeResultResults = testRangeResultPerformance(router, rangeQueries, iterations, stopsIndex);
|
|
24070
|
+
prettyPrintPerformanceResults(rangeResultResults, 'Range queries — rangeResult.getRoutes() (reconstruction only)');
|
|
23041
24071
|
});
|
|
23042
24072
|
program.parse(process.argv);
|
|
23043
24073
|
//# sourceMappingURL=cli.mjs.map
|