minotor 11.1.2 → 11.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cspell.json +7 -1
- package/CHANGELOG.md +3 -3
- package/README.md +111 -86
- package/dist/cli/perf.d.ts +57 -18
- package/dist/cli.mjs +1371 -342
- package/dist/cli.mjs.map +1 -1
- package/dist/parser.cjs.js +57 -4
- package/dist/parser.cjs.js.map +1 -1
- package/dist/parser.esm.js +57 -4
- package/dist/parser.esm.js.map +1 -1
- package/dist/router.cjs.js +1 -1
- package/dist/router.cjs.js.map +1 -1
- package/dist/router.d.ts +5 -5
- package/dist/router.esm.js +1 -1
- package/dist/router.esm.js.map +1 -1
- package/dist/router.umd.js +1 -1
- package/dist/router.umd.js.map +1 -1
- package/dist/routing/__tests__/access.test.d.ts +1 -0
- package/dist/routing/__tests__/plainRouter.test.d.ts +1 -0
- package/dist/routing/__tests__/rangeResult.test.d.ts +1 -0
- package/dist/routing/__tests__/rangeRouter.test.d.ts +1 -0
- package/dist/routing/__tests__/rangeState.test.d.ts +1 -0
- package/dist/routing/__tests__/raptor.test.d.ts +1 -0
- package/dist/routing/__tests__/state.test.d.ts +1 -0
- package/dist/routing/access.d.ts +55 -0
- package/dist/routing/plainRouter.d.ts +21 -0
- package/dist/routing/plotter.d.ts +9 -0
- package/dist/routing/query.d.ts +132 -13
- package/dist/routing/rangeResult.d.ts +155 -0
- package/dist/routing/rangeRouter.d.ts +24 -0
- package/dist/routing/rangeState.d.ts +83 -0
- package/dist/routing/raptor.d.ts +96 -0
- package/dist/routing/result.d.ts +27 -7
- package/dist/routing/route.d.ts +5 -21
- package/dist/routing/router.d.ts +20 -91
- package/dist/routing/state.d.ts +92 -17
- package/dist/timetable/route.d.ts +8 -0
- package/dist/timetable/timetable.d.ts +17 -1
- package/package.json +1 -1
- package/src/__e2e__/benchmark.json +18 -0
- package/src/__e2e__/router.test.ts +461 -127
- package/src/cli/minotor.ts +39 -3
- package/src/cli/perf.ts +324 -60
- package/src/cli/repl.ts +96 -41
- package/src/router.ts +11 -3
- package/src/routing/__tests__/access.test.ts +294 -0
- package/src/routing/__tests__/plainRouter.test.ts +1633 -0
- package/src/routing/__tests__/plotter.test.ts +8 -8
- package/src/routing/__tests__/rangeResult.test.ts +273 -0
- package/src/routing/__tests__/rangeRouter.test.ts +472 -0
- package/src/routing/__tests__/rangeState.test.ts +246 -0
- package/src/routing/__tests__/raptor.test.ts +366 -0
- package/src/routing/__tests__/result.test.ts +27 -27
- package/src/routing/__tests__/route.test.ts +28 -0
- package/src/routing/__tests__/router.test.ts +75 -1587
- package/src/routing/__tests__/state.test.ts +78 -0
- package/src/routing/access.ts +144 -0
- package/src/routing/plainRouter.ts +60 -0
- package/src/routing/plotter.ts +53 -6
- package/src/routing/query.ts +116 -13
- package/src/routing/rangeResult.ts +292 -0
- package/src/routing/rangeRouter.ts +167 -0
- package/src/routing/rangeState.ts +150 -0
- package/src/routing/raptor.ts +416 -0
- package/src/routing/result.ts +68 -26
- package/src/routing/route.ts +15 -53
- package/src/routing/router.ts +40 -480
- package/src/routing/state.ts +191 -32
- package/src/timetable/__tests__/timetable.test.ts +373 -0
- package/src/timetable/route.ts +16 -4
- package/src/timetable/timetable.ts +54 -1
package/src/routing/state.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { StopId } from '../stops/stops.js';
|
|
|
3
3
|
import { StopRouteIndex } from '../timetable/route.js';
|
|
4
4
|
import { Duration, Time } from '../timetable/time.js';
|
|
5
5
|
import { TransferType, TripStop } from '../timetable/timetable.js';
|
|
6
|
+
import { AccessPoint } from './access.js';
|
|
7
|
+
import type { IRaptorState } from './raptor.js';
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Sentinel value used in the internal arrival-time array to mark stops not yet reached.
|
|
@@ -10,14 +12,20 @@ import { TransferType, TripStop } from '../timetable/timetable.js';
|
|
|
10
12
|
*/
|
|
11
13
|
export const UNREACHED_TIME: Time = 0xffff;
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
export type OriginNode = { stopId: StopId; arrival: Time };
|
|
16
|
+
|
|
17
|
+
export type AccessEdge = {
|
|
18
|
+
arrival: Time;
|
|
19
|
+
from: StopId;
|
|
20
|
+
to: StopId;
|
|
21
|
+
duration: Duration;
|
|
22
|
+
};
|
|
15
23
|
|
|
16
24
|
/** A boarded transit trip that carries the passenger from one stop to another. */
|
|
17
25
|
export type VehicleEdge = TripStop & {
|
|
18
26
|
arrival: Time;
|
|
19
27
|
hopOffStopIndex: StopRouteIndex;
|
|
20
|
-
/**
|
|
28
|
+
/** modeling in-seat transfer */
|
|
21
29
|
continuationOf?: VehicleEdge;
|
|
22
30
|
};
|
|
23
31
|
|
|
@@ -30,7 +38,7 @@ export type TransferEdge = {
|
|
|
30
38
|
minTransferTime?: Duration;
|
|
31
39
|
};
|
|
32
40
|
|
|
33
|
-
export type RoutingEdge = OriginNode | VehicleEdge | TransferEdge;
|
|
41
|
+
export type RoutingEdge = OriginNode | AccessEdge | VehicleEdge | TransferEdge;
|
|
34
42
|
|
|
35
43
|
/** The earliest arrival at a stop together with how many legs were needed to reach it. */
|
|
36
44
|
export type Arrival = {
|
|
@@ -41,9 +49,9 @@ export type Arrival = {
|
|
|
41
49
|
/**
|
|
42
50
|
* Encapsulates all mutable state for a single RAPTOR routing query.
|
|
43
51
|
*/
|
|
44
|
-
export class RoutingState {
|
|
52
|
+
export class RoutingState implements IRaptorState {
|
|
45
53
|
/** Origin stop IDs for this query. */
|
|
46
|
-
|
|
54
|
+
origins: StopId[];
|
|
47
55
|
|
|
48
56
|
/** Destination stop IDs for this query. */
|
|
49
57
|
readonly destinations: StopId[];
|
|
@@ -70,38 +78,78 @@ export class RoutingState {
|
|
|
70
78
|
private earliestArrivalLegs: Uint8Array;
|
|
71
79
|
|
|
72
80
|
/**
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* All stops start as unreached. Each origin is immediately recorded at the
|
|
76
|
-
* departure time with leg number 0, and a corresponding OriginNode is placed
|
|
77
|
-
* in round 0 of the graph.
|
|
78
|
-
*
|
|
79
|
-
* @param origins Stop IDs to depart from (may be several equivalent stops).
|
|
80
|
-
* @param destinations Stop IDs that count as the target of the query.
|
|
81
|
-
* @param departureTime Earliest departure time in minutes from midnight.
|
|
82
|
-
* @param nbStops Total number of stops in the timetable (sets array sizes).
|
|
81
|
+
* Fast O(1) membership test for destination stops.
|
|
82
|
+
* Built once at construction time from the `destinations` array.
|
|
83
83
|
*/
|
|
84
|
+
private readonly destinationSet: Set<StopId>;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Cached best arrival time at any destination stop, kept up-to-date by
|
|
88
|
+
* {@link updateArrival} so that destination pruning is always O(1).
|
|
89
|
+
*/
|
|
90
|
+
private _destinationBest: Time = UNREACHED_TIME;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Every stop that has received an arrival improvement during the current run,
|
|
94
|
+
* in the order the improvements occurred. Used by {@link resetFor} to clear
|
|
95
|
+
* only the touched entries instead of scanning the entire array.
|
|
96
|
+
*/
|
|
97
|
+
private readonly reachedStops: StopId[] = [];
|
|
98
|
+
|
|
84
99
|
constructor(
|
|
85
|
-
origins: StopId[],
|
|
86
|
-
destinations: StopId[],
|
|
87
100
|
departureTime: Time,
|
|
101
|
+
destinations: StopId[],
|
|
102
|
+
accessPaths: AccessPoint[],
|
|
88
103
|
nbStops: number,
|
|
104
|
+
maxRounds: number = 0,
|
|
89
105
|
) {
|
|
90
|
-
this.origins = origins;
|
|
91
106
|
this.destinations = destinations;
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
for (
|
|
98
|
-
|
|
99
|
-
graph0[stop] = { arrival: departureTime };
|
|
107
|
+
this.destinationSet = new Set(destinations);
|
|
108
|
+
this.earliestArrivalTimes = new Uint16Array(nbStops).fill(UNREACHED_TIME);
|
|
109
|
+
this.earliestArrivalLegs = new Uint8Array(nbStops);
|
|
110
|
+
this.origins = []; // overwritten by seedAccessPaths below
|
|
111
|
+
this.graph = [new Array<RoutingEdge | undefined>(nbStops)];
|
|
112
|
+
for (let r = 1; r <= maxRounds; r++) {
|
|
113
|
+
this.graph.push(new Array<RoutingEdge | undefined>(nbStops));
|
|
100
114
|
}
|
|
115
|
+
this.seedAccessPaths(departureTime, accessPaths);
|
|
116
|
+
}
|
|
101
117
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
118
|
+
/**
|
|
119
|
+
* Seeds round-0 arrivals and {@link origins} from a set of access paths.
|
|
120
|
+
* Called by the constructor and by {@link resetFor}.
|
|
121
|
+
* Assumes {@link earliestArrivalTimes} and {@link graph}[0] are already
|
|
122
|
+
* allocated and in their "cleared" state (all entries at UNREACHED_TIME /
|
|
123
|
+
* undefined) before this method runs.
|
|
124
|
+
*/
|
|
125
|
+
private seedAccessPaths(depTime: Time, accessPaths: AccessPoint[]): void {
|
|
126
|
+
const seededOrigins = new Set<StopId>();
|
|
127
|
+
for (const access of accessPaths) {
|
|
128
|
+
const arrival = depTime + access.duration;
|
|
129
|
+
const edge: OriginNode | AccessEdge =
|
|
130
|
+
access.duration === 0
|
|
131
|
+
? { stopId: access.fromStopId, arrival: depTime }
|
|
132
|
+
: {
|
|
133
|
+
arrival,
|
|
134
|
+
from: access.fromStopId,
|
|
135
|
+
to: access.toStopId,
|
|
136
|
+
duration: access.duration,
|
|
137
|
+
};
|
|
138
|
+
const stop = access.toStopId;
|
|
139
|
+
if (arrival < this.earliestArrivalTimes[stop]!) {
|
|
140
|
+
this.earliestArrivalTimes[stop] = arrival;
|
|
141
|
+
this.graph[0]![stop] = edge;
|
|
142
|
+
}
|
|
143
|
+
seededOrigins.add(stop);
|
|
144
|
+
}
|
|
145
|
+
for (const stop of seededOrigins) {
|
|
146
|
+
this.reachedStops.push(stop);
|
|
147
|
+
}
|
|
148
|
+
this.origins = Array.from(seededOrigins);
|
|
149
|
+
for (let i = 0; i < this.destinations.length; i++) {
|
|
150
|
+
const t = this.earliestArrivalTimes[this.destinations[i]!]!;
|
|
151
|
+
if (t < this._destinationBest) this._destinationBest = t;
|
|
152
|
+
}
|
|
105
153
|
}
|
|
106
154
|
|
|
107
155
|
/** Total number of stops in the timetable */
|
|
@@ -118,16 +166,101 @@ export class RoutingState {
|
|
|
118
166
|
}
|
|
119
167
|
|
|
120
168
|
/**
|
|
121
|
-
*
|
|
169
|
+
* Earliest arrival at any destination stop; {@link UNREACHED_TIME} if none
|
|
170
|
+
* has been reached yet. Updated automatically by {@link updateArrival}. O(1).
|
|
171
|
+
*/
|
|
172
|
+
get destinationBest(): Time {
|
|
173
|
+
return this._destinationBest;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* In standard RAPTOR the improvement bound is simply the per-run earliest
|
|
178
|
+
* arrival; the `round` argument is ignored.
|
|
179
|
+
*/
|
|
180
|
+
improvementBound(_round: number, stop: StopId): Time {
|
|
181
|
+
return this.arrivalTime(stop);
|
|
182
|
+
}
|
|
122
183
|
|
|
184
|
+
/** No-op in standard RAPTOR — there are no shared cross-run labels to propagate. */
|
|
185
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
186
|
+
initRound(_round: number): void {}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Records a new earliest arrival at a stop.
|
|
123
190
|
*
|
|
124
191
|
* @param stop The stop that was reached.
|
|
125
192
|
* @param time The arrival time in minutes from midnight.
|
|
126
193
|
* @param leg The round number (number of transit legs taken so far).
|
|
127
194
|
*/
|
|
128
195
|
updateArrival(stop: StopId, time: Time, leg: number): void {
|
|
196
|
+
this.reachedStops.push(stop);
|
|
129
197
|
this.earliestArrivalTimes[stop] = time;
|
|
130
198
|
this.earliestArrivalLegs[stop] = leg;
|
|
199
|
+
if (this.destinationSet.has(stop) && time < this._destinationBest) {
|
|
200
|
+
this._destinationBest = time;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Resets this state for a new departure-time iteration **without
|
|
206
|
+
* reallocating** the underlying arrays.
|
|
207
|
+
*
|
|
208
|
+
* Only the stops recorded in {@link reachedStops} are touched — all other
|
|
209
|
+
* entries are already at their initial bound values.
|
|
210
|
+
*
|
|
211
|
+
* After this call the state is equivalent to a freshly constructed
|
|
212
|
+
* {@link RoutingState} for the given `depTime` and `accessPaths`.
|
|
213
|
+
*
|
|
214
|
+
* @param depTime New origin departure time.
|
|
215
|
+
* @param accessPaths Access legs for this departure-time slot.
|
|
216
|
+
*/
|
|
217
|
+
resetFor(depTime: Time, accessPaths: AccessPoint[]): void {
|
|
218
|
+
for (const stop of this.reachedStops) {
|
|
219
|
+
this.earliestArrivalTimes[stop] = UNREACHED_TIME;
|
|
220
|
+
this.earliestArrivalLegs[stop] = 0;
|
|
221
|
+
for (let r = 0; r < this.graph.length; r++) {
|
|
222
|
+
this.graph[r]![stop] = undefined;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
this.reachedStops.length = 0;
|
|
226
|
+
this._destinationBest = UNREACHED_TIME;
|
|
227
|
+
this.seedAccessPaths(depTime, accessPaths);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Iterates over every stop that has been reached, yielding its stop ID,
|
|
232
|
+
* earliest arrival time, and the number of legs taken to reach it.
|
|
233
|
+
*
|
|
234
|
+
* Unreached stops (those still at UNREACHED_TIME) are skipped entirely.
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* ```ts
|
|
238
|
+
* for (const { stop, arrival, legNumber } of routingState.arrivals()) {
|
|
239
|
+
* console.log(`Stop ${stop}: arrived at ${arrival} after ${legNumber} leg(s)`);
|
|
240
|
+
* }
|
|
241
|
+
* ```
|
|
242
|
+
*/
|
|
243
|
+
*arrivals(): Generator<{ stop: StopId; arrival: Time; legNumber: number }> {
|
|
244
|
+
for (let stop = 0; stop < this.earliestArrivalTimes.length; stop++) {
|
|
245
|
+
const time = this.earliestArrivalTimes[stop]!;
|
|
246
|
+
if (time < UNREACHED_TIME) {
|
|
247
|
+
yield {
|
|
248
|
+
stop,
|
|
249
|
+
arrival: time,
|
|
250
|
+
legNumber: this.earliestArrivalLegs[stop]!,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Finds the earliest arrival time at any stop from a given set of destinations.
|
|
258
|
+
*
|
|
259
|
+
* @param routingState The routing state containing arrival times and destinations.
|
|
260
|
+
* @returns The earliest arrival time among the provided destinations.
|
|
261
|
+
*/
|
|
262
|
+
earliestArrivalAtAnyDestination(): Time {
|
|
263
|
+
return this._destinationBest;
|
|
131
264
|
}
|
|
132
265
|
|
|
133
266
|
/**
|
|
@@ -140,6 +273,14 @@ export class RoutingState {
|
|
|
140
273
|
return { arrival: time, legNumber: this.earliestArrivalLegs[stop]! };
|
|
141
274
|
}
|
|
142
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Returns `true` if `stop` is one of the query's destination stops.
|
|
278
|
+
* O(1) — backed by a `Set` built at construction time.
|
|
279
|
+
*/
|
|
280
|
+
isDestination(stop: StopId): boolean {
|
|
281
|
+
return this.destinationSet.has(stop);
|
|
282
|
+
}
|
|
283
|
+
|
|
143
284
|
/**
|
|
144
285
|
* Creates a {@link RoutingState} from fully-specified raw data.
|
|
145
286
|
*
|
|
@@ -170,7 +311,16 @@ export class RoutingState {
|
|
|
170
311
|
arrivals?: [stop: StopId, time: Time, leg: number][];
|
|
171
312
|
graph?: [stop: StopId, edge: RoutingEdge][][];
|
|
172
313
|
}): RoutingState {
|
|
173
|
-
const state = new RoutingState(
|
|
314
|
+
const state = new RoutingState(
|
|
315
|
+
0,
|
|
316
|
+
destinations,
|
|
317
|
+
origins.map((stop) => ({
|
|
318
|
+
fromStopId: stop,
|
|
319
|
+
toStopId: stop,
|
|
320
|
+
duration: 0,
|
|
321
|
+
})),
|
|
322
|
+
nbStops,
|
|
323
|
+
);
|
|
174
324
|
|
|
175
325
|
// Replace the arrival arrays with freshly built ones so the constructor's
|
|
176
326
|
// origin-seeding doesn't bleed into the test state.
|
|
@@ -183,6 +333,15 @@ export class RoutingState {
|
|
|
183
333
|
state.earliestArrivalTimes = earliestArrivalTimes;
|
|
184
334
|
state.earliestArrivalLegs = earliestArrivalLegs;
|
|
185
335
|
|
|
336
|
+
// Recompute _destinationBest from the test data since we bypassed updateArrival.
|
|
337
|
+
// fromTestData is a static method of RoutingState, so private access is allowed.
|
|
338
|
+
state._destinationBest = UNREACHED_TIME;
|
|
339
|
+
for (const dest of destinations) {
|
|
340
|
+
const t = earliestArrivalTimes[dest];
|
|
341
|
+
if (t !== undefined && t < state._destinationBest)
|
|
342
|
+
state._destinationBest = t;
|
|
343
|
+
}
|
|
344
|
+
|
|
186
345
|
// Convert the sparse per-round representation to dense arrays and replace
|
|
187
346
|
// the graph in-place.
|
|
188
347
|
const denseRounds = graph.map((round) => {
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
StopAdjacency,
|
|
10
10
|
Timetable,
|
|
11
11
|
TripStop,
|
|
12
|
+
TripTransfers,
|
|
12
13
|
} from '../timetable.js';
|
|
13
14
|
import { encode } from '../tripStopId.js';
|
|
14
15
|
|
|
@@ -655,5 +656,377 @@ describe('Timetable', () => {
|
|
|
655
656
|
);
|
|
656
657
|
});
|
|
657
658
|
});
|
|
659
|
+
|
|
660
|
+
describe('findFirstBoardableTrip', () => {
|
|
661
|
+
const BOARDING_STOP_INDEX = 0;
|
|
662
|
+
const FFBT_ROUTE_ID = 2;
|
|
663
|
+
|
|
664
|
+
const ffbtRoute = Route.of({
|
|
665
|
+
id: FFBT_ROUTE_ID,
|
|
666
|
+
serviceRouteId: 2,
|
|
667
|
+
trips: [
|
|
668
|
+
{
|
|
669
|
+
stops: [
|
|
670
|
+
{
|
|
671
|
+
id: 0,
|
|
672
|
+
arrivalTime: timeFromHMS(8, 0, 0),
|
|
673
|
+
departureTime: timeFromHMS(8, 0, 0),
|
|
674
|
+
},
|
|
675
|
+
{
|
|
676
|
+
id: 1,
|
|
677
|
+
arrivalTime: timeFromHMS(8, 30, 0),
|
|
678
|
+
departureTime: timeFromHMS(8, 30, 0),
|
|
679
|
+
},
|
|
680
|
+
],
|
|
681
|
+
},
|
|
682
|
+
{
|
|
683
|
+
stops: [
|
|
684
|
+
{
|
|
685
|
+
id: 0,
|
|
686
|
+
arrivalTime: timeFromHMS(9, 0, 0),
|
|
687
|
+
departureTime: timeFromHMS(9, 0, 0),
|
|
688
|
+
pickUpType: NOT_AVAILABLE,
|
|
689
|
+
},
|
|
690
|
+
{
|
|
691
|
+
id: 1,
|
|
692
|
+
arrivalTime: timeFromHMS(9, 30, 0),
|
|
693
|
+
departureTime: timeFromHMS(9, 30, 0),
|
|
694
|
+
},
|
|
695
|
+
],
|
|
696
|
+
},
|
|
697
|
+
{
|
|
698
|
+
stops: [
|
|
699
|
+
{
|
|
700
|
+
id: 0,
|
|
701
|
+
arrivalTime: timeFromHMS(10, 0, 0),
|
|
702
|
+
departureTime: timeFromHMS(10, 0, 0),
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
id: 1,
|
|
706
|
+
arrivalTime: timeFromHMS(10, 30, 0),
|
|
707
|
+
departureTime: timeFromHMS(10, 30, 0),
|
|
708
|
+
},
|
|
709
|
+
],
|
|
710
|
+
},
|
|
711
|
+
{
|
|
712
|
+
stops: [
|
|
713
|
+
{
|
|
714
|
+
id: 0,
|
|
715
|
+
arrivalTime: timeFromHMS(11, 0, 0),
|
|
716
|
+
departureTime: timeFromHMS(11, 0, 0),
|
|
717
|
+
},
|
|
718
|
+
{
|
|
719
|
+
id: 1,
|
|
720
|
+
arrivalTime: timeFromHMS(11, 30, 0),
|
|
721
|
+
departureTime: timeFromHMS(11, 30, 0),
|
|
722
|
+
},
|
|
723
|
+
],
|
|
724
|
+
},
|
|
725
|
+
],
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
const ffbtStopsAdjacency: StopAdjacency[] = [
|
|
729
|
+
{ routes: [FFBT_ROUTE_ID] },
|
|
730
|
+
{ routes: [FFBT_ROUTE_ID] },
|
|
731
|
+
];
|
|
732
|
+
|
|
733
|
+
const ffbtServiceRoutes = [
|
|
734
|
+
{ type: 'BUS' as const, name: 'Route 2', routes: [FFBT_ROUTE_ID] },
|
|
735
|
+
];
|
|
736
|
+
|
|
737
|
+
const ffbtTimetable = new Timetable(
|
|
738
|
+
ffbtStopsAdjacency,
|
|
739
|
+
[ffbtRoute],
|
|
740
|
+
ffbtServiceRoutes,
|
|
741
|
+
);
|
|
742
|
+
|
|
743
|
+
describe('without fromTripStop', () => {
|
|
744
|
+
it('returns the first trip with an available pickup', () => {
|
|
745
|
+
const result = ffbtTimetable.findFirstBoardableTrip(
|
|
746
|
+
BOARDING_STOP_INDEX,
|
|
747
|
+
ffbtRoute,
|
|
748
|
+
0,
|
|
749
|
+
);
|
|
750
|
+
assert.strictEqual(result, 0);
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
it('skips trips with NOT_AVAILABLE pickup', () => {
|
|
754
|
+
// Start scanning from trip 1 (NOT_AVAILABLE) → must return trip 2.
|
|
755
|
+
const result = ffbtTimetable.findFirstBoardableTrip(
|
|
756
|
+
BOARDING_STOP_INDEX,
|
|
757
|
+
ffbtRoute,
|
|
758
|
+
1,
|
|
759
|
+
);
|
|
760
|
+
assert.strictEqual(result, 2);
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
it('starts scanning at earliestTrip', () => {
|
|
764
|
+
// Even though trip 0 is valid, scanning starts at trip 2.
|
|
765
|
+
const result = ffbtTimetable.findFirstBoardableTrip(
|
|
766
|
+
BOARDING_STOP_INDEX,
|
|
767
|
+
ffbtRoute,
|
|
768
|
+
2,
|
|
769
|
+
);
|
|
770
|
+
assert.strictEqual(result, 2);
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
it('respects the beforeTrip exclusive upper bound', () => {
|
|
774
|
+
// beforeTrip=1 means only trip 0 is in scope.
|
|
775
|
+
const result = ffbtTimetable.findFirstBoardableTrip(
|
|
776
|
+
BOARDING_STOP_INDEX,
|
|
777
|
+
ffbtRoute,
|
|
778
|
+
0,
|
|
779
|
+
0,
|
|
780
|
+
1,
|
|
781
|
+
);
|
|
782
|
+
assert.strictEqual(result, 0);
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
it('returns undefined when the only trip in range has a NOT_AVAILABLE pickup', () => {
|
|
786
|
+
// [1, 2) contains only trip 1, which is NOT_AVAILABLE.
|
|
787
|
+
const result = ffbtTimetable.findFirstBoardableTrip(
|
|
788
|
+
BOARDING_STOP_INDEX,
|
|
789
|
+
ffbtRoute,
|
|
790
|
+
1,
|
|
791
|
+
0,
|
|
792
|
+
2,
|
|
793
|
+
);
|
|
794
|
+
assert.strictEqual(result, undefined);
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
it('returns undefined when earliestTrip is past all trips', () => {
|
|
798
|
+
const result = ffbtTimetable.findFirstBoardableTrip(
|
|
799
|
+
BOARDING_STOP_INDEX,
|
|
800
|
+
ffbtRoute,
|
|
801
|
+
99,
|
|
802
|
+
);
|
|
803
|
+
assert.strictEqual(result, undefined);
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
it('returns undefined when beforeTrip equals earliestTrip (empty range)', () => {
|
|
807
|
+
const result = ffbtTimetable.findFirstBoardableTrip(
|
|
808
|
+
BOARDING_STOP_INDEX,
|
|
809
|
+
ffbtRoute,
|
|
810
|
+
2,
|
|
811
|
+
0,
|
|
812
|
+
2,
|
|
813
|
+
);
|
|
814
|
+
assert.strictEqual(result, undefined);
|
|
815
|
+
});
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
describe('with fromTripStop', () => {
|
|
819
|
+
const fromTripStop: TripStop = {
|
|
820
|
+
routeId: 99,
|
|
821
|
+
stopIndex: 0,
|
|
822
|
+
tripIndex: 0,
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
it('returns the first trip whose departure satisfies after + transferTime', () => {
|
|
826
|
+
// after=09:30, transferTime=30 → requiredTime=10:00.
|
|
827
|
+
// trip 0 (08:00) < 10:00 → skip.
|
|
828
|
+
// trip 1 (09:00) NOT_AVAILABLE → skip.
|
|
829
|
+
// trip 2 (10:00) ≥ 10:00 → returned.
|
|
830
|
+
const result = ffbtTimetable.findFirstBoardableTrip(
|
|
831
|
+
BOARDING_STOP_INDEX,
|
|
832
|
+
ffbtRoute,
|
|
833
|
+
0,
|
|
834
|
+
timeFromHMS(9, 30, 0),
|
|
835
|
+
undefined,
|
|
836
|
+
fromTripStop,
|
|
837
|
+
30,
|
|
838
|
+
);
|
|
839
|
+
assert.strictEqual(result, 2);
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
it('treats departure exactly equal to after as satisfying (transferTime=0)', () => {
|
|
843
|
+
// after=10:00, transferTime=0 → requiredTime=10:00.
|
|
844
|
+
// trip 0 (08:00) < 10:00 → skip.
|
|
845
|
+
// trip 1 (09:00) NOT_AVAILABLE → skip.
|
|
846
|
+
// trip 2 (10:00) ≥ 10:00 → returned.
|
|
847
|
+
const result = ffbtTimetable.findFirstBoardableTrip(
|
|
848
|
+
BOARDING_STOP_INDEX,
|
|
849
|
+
ffbtRoute,
|
|
850
|
+
0,
|
|
851
|
+
timeFromHMS(10, 0, 0),
|
|
852
|
+
undefined,
|
|
853
|
+
fromTripStop,
|
|
854
|
+
0,
|
|
855
|
+
);
|
|
856
|
+
assert.strictEqual(result, 2);
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
it('returns undefined when all trips depart before after + transferTime', () => {
|
|
860
|
+
// after=11:00, transferTime=30 → requiredTime=11:30.
|
|
861
|
+
// trip 3 (11:00) < 11:30 → skip. No further trips.
|
|
862
|
+
const result = ffbtTimetable.findFirstBoardableTrip(
|
|
863
|
+
BOARDING_STOP_INDEX,
|
|
864
|
+
ffbtRoute,
|
|
865
|
+
0,
|
|
866
|
+
timeFromHMS(11, 0, 0),
|
|
867
|
+
undefined,
|
|
868
|
+
fromTripStop,
|
|
869
|
+
30,
|
|
870
|
+
);
|
|
871
|
+
assert.strictEqual(result, undefined);
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
it('returns undefined when beforeTrip excludes all trips that would satisfy the requirement', () => {
|
|
875
|
+
// Only trips 0 and 1 are in scope (beforeTrip=2).
|
|
876
|
+
// after=09:00, transferTime=30 → requiredTime=09:30.
|
|
877
|
+
// trip 0 (08:00) < 09:30 → skip.
|
|
878
|
+
// trip 1 (09:00) NOT_AVAILABLE → skip.
|
|
879
|
+
// beforeTrip reached → undefined.
|
|
880
|
+
const result = ffbtTimetable.findFirstBoardableTrip(
|
|
881
|
+
BOARDING_STOP_INDEX,
|
|
882
|
+
ffbtRoute,
|
|
883
|
+
0,
|
|
884
|
+
timeFromHMS(9, 0, 0),
|
|
885
|
+
2,
|
|
886
|
+
fromTripStop,
|
|
887
|
+
30,
|
|
888
|
+
);
|
|
889
|
+
assert.strictEqual(result, undefined);
|
|
890
|
+
});
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
describe('guaranteed connection', () => {
|
|
894
|
+
it('bypasses the after + transferTime departure check', () => {
|
|
895
|
+
const fromTripStop: TripStop = {
|
|
896
|
+
routeId: 99,
|
|
897
|
+
stopIndex: 0,
|
|
898
|
+
tripIndex: 0,
|
|
899
|
+
};
|
|
900
|
+
const guaranteedTransfers: TripTransfers = new Map([
|
|
901
|
+
[
|
|
902
|
+
encode(
|
|
903
|
+
fromTripStop.stopIndex,
|
|
904
|
+
fromTripStop.routeId,
|
|
905
|
+
fromTripStop.tripIndex,
|
|
906
|
+
),
|
|
907
|
+
[
|
|
908
|
+
{
|
|
909
|
+
stopIndex: BOARDING_STOP_INDEX,
|
|
910
|
+
routeId: FFBT_ROUTE_ID,
|
|
911
|
+
tripIndex: 0,
|
|
912
|
+
},
|
|
913
|
+
],
|
|
914
|
+
],
|
|
915
|
+
]);
|
|
916
|
+
const timetableWithGuarantee = new Timetable(
|
|
917
|
+
ffbtStopsAdjacency,
|
|
918
|
+
[ffbtRoute],
|
|
919
|
+
ffbtServiceRoutes,
|
|
920
|
+
new Map(),
|
|
921
|
+
guaranteedTransfers,
|
|
922
|
+
);
|
|
923
|
+
|
|
924
|
+
// after=07:00, transferTime=120 → requiredTime=09:00; trip 0 (08:00) is below
|
|
925
|
+
// that threshold but the guarantee returns it immediately.
|
|
926
|
+
const result = timetableWithGuarantee.findFirstBoardableTrip(
|
|
927
|
+
BOARDING_STOP_INDEX,
|
|
928
|
+
ffbtRoute,
|
|
929
|
+
0,
|
|
930
|
+
timeFromHMS(7, 0, 0),
|
|
931
|
+
undefined,
|
|
932
|
+
fromTripStop,
|
|
933
|
+
120,
|
|
934
|
+
);
|
|
935
|
+
assert.strictEqual(result, 0);
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
it('does not bypass a NOT_AVAILABLE pickup', () => {
|
|
939
|
+
const fromTripStop: TripStop = {
|
|
940
|
+
routeId: 99,
|
|
941
|
+
stopIndex: 0,
|
|
942
|
+
tripIndex: 0,
|
|
943
|
+
};
|
|
944
|
+
const guaranteedTransfers: TripTransfers = new Map([
|
|
945
|
+
[
|
|
946
|
+
encode(
|
|
947
|
+
fromTripStop.stopIndex,
|
|
948
|
+
fromTripStop.routeId,
|
|
949
|
+
fromTripStop.tripIndex,
|
|
950
|
+
),
|
|
951
|
+
[
|
|
952
|
+
{
|
|
953
|
+
stopIndex: BOARDING_STOP_INDEX,
|
|
954
|
+
routeId: FFBT_ROUTE_ID,
|
|
955
|
+
tripIndex: 1,
|
|
956
|
+
},
|
|
957
|
+
],
|
|
958
|
+
],
|
|
959
|
+
]);
|
|
960
|
+
const timetableWithGuarantee = new Timetable(
|
|
961
|
+
ffbtStopsAdjacency,
|
|
962
|
+
[ffbtRoute],
|
|
963
|
+
ffbtServiceRoutes,
|
|
964
|
+
new Map(),
|
|
965
|
+
guaranteedTransfers,
|
|
966
|
+
);
|
|
967
|
+
|
|
968
|
+
// after=08:30, transferTime=60 → requiredTime=09:30.
|
|
969
|
+
// trip 0 (08:00): not guaranteed for trip 0, 08:00 < 09:30 → skip.
|
|
970
|
+
// trip 1 (09:00): NOT_AVAILABLE → skip (guarantee is irrelevant).
|
|
971
|
+
// trip 2 (10:00): no guarantee, but 10:00 ≥ 09:30 → first boardable.
|
|
972
|
+
const result = timetableWithGuarantee.findFirstBoardableTrip(
|
|
973
|
+
BOARDING_STOP_INDEX,
|
|
974
|
+
ffbtRoute,
|
|
975
|
+
0,
|
|
976
|
+
timeFromHMS(8, 30, 0),
|
|
977
|
+
undefined,
|
|
978
|
+
fromTripStop,
|
|
979
|
+
60,
|
|
980
|
+
);
|
|
981
|
+
assert.strictEqual(result, 2);
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
it('applies only to its declared target trip index', () => {
|
|
985
|
+
const fromTripStop: TripStop = {
|
|
986
|
+
routeId: 99,
|
|
987
|
+
stopIndex: 0,
|
|
988
|
+
tripIndex: 0,
|
|
989
|
+
};
|
|
990
|
+
const guaranteedTransfers: TripTransfers = new Map([
|
|
991
|
+
[
|
|
992
|
+
encode(
|
|
993
|
+
fromTripStop.stopIndex,
|
|
994
|
+
fromTripStop.routeId,
|
|
995
|
+
fromTripStop.tripIndex,
|
|
996
|
+
),
|
|
997
|
+
[
|
|
998
|
+
{
|
|
999
|
+
stopIndex: BOARDING_STOP_INDEX,
|
|
1000
|
+
routeId: FFBT_ROUTE_ID,
|
|
1001
|
+
tripIndex: 2,
|
|
1002
|
+
},
|
|
1003
|
+
],
|
|
1004
|
+
],
|
|
1005
|
+
]);
|
|
1006
|
+
const timetableWithGuarantee = new Timetable(
|
|
1007
|
+
ffbtStopsAdjacency,
|
|
1008
|
+
[ffbtRoute],
|
|
1009
|
+
ffbtServiceRoutes,
|
|
1010
|
+
new Map(),
|
|
1011
|
+
guaranteedTransfers,
|
|
1012
|
+
);
|
|
1013
|
+
|
|
1014
|
+
// after=09:30, transferTime=120 → requiredTime=11:30.
|
|
1015
|
+
// trip 0 (08:00): not guaranteed, 08:00 < 11:30 → skip.
|
|
1016
|
+
// trip 1 (09:00): NOT_AVAILABLE → skip.
|
|
1017
|
+
// trip 2 (10:00): guaranteed → returned immediately despite 10:00 < 11:30.
|
|
1018
|
+
const result = timetableWithGuarantee.findFirstBoardableTrip(
|
|
1019
|
+
BOARDING_STOP_INDEX,
|
|
1020
|
+
ffbtRoute,
|
|
1021
|
+
0,
|
|
1022
|
+
timeFromHMS(9, 30, 0),
|
|
1023
|
+
undefined,
|
|
1024
|
+
fromTripStop,
|
|
1025
|
+
120,
|
|
1026
|
+
);
|
|
1027
|
+
assert.strictEqual(result, 2);
|
|
1028
|
+
});
|
|
1029
|
+
});
|
|
1030
|
+
});
|
|
658
1031
|
});
|
|
659
1032
|
});
|