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
|
@@ -1,1605 +1,93 @@
|
|
|
1
1
|
import assert from 'node:assert';
|
|
2
|
-
import {
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
3
|
|
|
4
4
|
import { Stop } from '../../stops/stops.js';
|
|
5
5
|
import { StopsIndex } from '../../stops/stopsIndex.js';
|
|
6
6
|
import { Route } from '../../timetable/route.js';
|
|
7
|
-
import {
|
|
7
|
+
import { timeFromHM } from '../../timetable/time.js';
|
|
8
8
|
import {
|
|
9
9
|
ServiceRoute,
|
|
10
10
|
StopAdjacency,
|
|
11
11
|
Timetable,
|
|
12
|
-
TripTransfers,
|
|
13
12
|
} from '../../timetable/timetable.js';
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
13
|
+
import { Query, RangeQuery } from '../query.js';
|
|
14
|
+
import { RangeResult } from '../rangeResult.js';
|
|
16
15
|
import { Result } from '../result.js';
|
|
17
16
|
import { Router } from '../router.js';
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
timetable = new Timetable(stopsAdjacency, routesAdjacency, routes);
|
|
71
|
-
const stops: Stop[] = [
|
|
72
|
-
{
|
|
73
|
-
id: 0,
|
|
74
|
-
sourceStopId: 'stop1',
|
|
75
|
-
name: 'Stop 1',
|
|
76
|
-
lat: 1.0,
|
|
77
|
-
lon: 1.0,
|
|
78
|
-
children: [],
|
|
79
|
-
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
80
|
-
},
|
|
81
|
-
{
|
|
82
|
-
id: 1,
|
|
83
|
-
sourceStopId: 'stop2',
|
|
84
|
-
name: 'Stop 2',
|
|
85
|
-
lat: 2.0,
|
|
86
|
-
lon: 2.0,
|
|
87
|
-
children: [],
|
|
88
|
-
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
id: 2,
|
|
92
|
-
sourceStopId: 'stop3',
|
|
93
|
-
name: 'Stop 3',
|
|
94
|
-
lat: 3.0,
|
|
95
|
-
lon: 3.0,
|
|
96
|
-
children: [],
|
|
97
|
-
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
98
|
-
},
|
|
99
|
-
];
|
|
100
|
-
const stopsIndex = new StopsIndex(stops);
|
|
101
|
-
router = new Router(timetable, stopsIndex);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('should find a direct route', () => {
|
|
105
|
-
const query = new Query.Builder()
|
|
106
|
-
.from(0)
|
|
107
|
-
.to(2)
|
|
108
|
-
.departureTime(timeFromHM(8, 0))
|
|
109
|
-
.build();
|
|
110
|
-
|
|
111
|
-
const result: Result = router.route(query);
|
|
112
|
-
const bestRoute = result.bestRoute();
|
|
113
|
-
|
|
114
|
-
// Should find a single-leg direct route on Line 1: stop1 -> stop3
|
|
115
|
-
assert.strictEqual(bestRoute?.legs.length, 1);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('should return an empty result when no route is possible', () => {
|
|
119
|
-
const query = new Query.Builder()
|
|
120
|
-
.from(0)
|
|
121
|
-
.to(12)
|
|
122
|
-
.departureTime(timeFromHM(8, 0))
|
|
123
|
-
.build();
|
|
124
|
-
|
|
125
|
-
const result: Result = router.route(query);
|
|
126
|
-
const bestRoute = result.bestRoute();
|
|
127
|
-
|
|
128
|
-
// No route exists to a non-existent stop
|
|
129
|
-
assert.strictEqual(bestRoute, undefined);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('should correctly calculate the arrival time to a stop', () => {
|
|
133
|
-
const query = new Query.Builder()
|
|
134
|
-
.from(0)
|
|
135
|
-
.to(2)
|
|
136
|
-
.departureTime(timeFromHM(8, 0))
|
|
137
|
-
.build();
|
|
138
|
-
|
|
139
|
-
const result: Result = router.route(query);
|
|
140
|
-
const timeToStop3 = result.arrivalAt(2);
|
|
141
|
-
|
|
142
|
-
// Route 0 arrives at stop3 at 08:35
|
|
143
|
-
assert.strictEqual(timeToStop3?.arrival, timeFromHM(8, 35));
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
describe('with a route change', () => {
|
|
148
|
-
let router: Router;
|
|
149
|
-
let timetable: Timetable;
|
|
150
|
-
|
|
151
|
-
beforeEach(() => {
|
|
152
|
-
// Setup: Two routes that share stop2, enabling a same-stop transfer (route change)
|
|
153
|
-
// Route 0 (Line 1): stop1 (depart 08:15) -> stop2 (08:30-08:45) -> stop3 (09:00)
|
|
154
|
-
// Route 1 (Line 2): stop4 (depart 08:20) -> stop2 (09:00-09:15) -> stop5 (09:20)
|
|
155
|
-
// Both routes serve stop2, allowing transfer without walking
|
|
156
|
-
const stopsAdjacency: StopAdjacency[] = [
|
|
157
|
-
{ routes: [0] }, // stop 0 (stop1)
|
|
158
|
-
{ routes: [0, 1] }, // stop 1 (stop2) - shared by both routes
|
|
159
|
-
{ routes: [0] }, // stop 2 (stop3)
|
|
160
|
-
{ routes: [1] }, // stop 3 (stop4)
|
|
161
|
-
{ routes: [1] }, // stop 4 (stop5)
|
|
162
|
-
];
|
|
163
|
-
|
|
164
|
-
const routesAdjacency = [
|
|
165
|
-
// Route 0: stops 0 -> 1 -> 2
|
|
166
|
-
Route.of({
|
|
167
|
-
id: 0,
|
|
168
|
-
serviceRouteId: 0,
|
|
169
|
-
trips: [
|
|
170
|
-
{
|
|
171
|
-
stops: [
|
|
172
|
-
{
|
|
173
|
-
id: 0,
|
|
174
|
-
arrivalTime: timeFromHM(8, 0),
|
|
175
|
-
departureTime: timeFromHM(8, 15),
|
|
176
|
-
},
|
|
177
|
-
{
|
|
178
|
-
id: 1,
|
|
179
|
-
arrivalTime: timeFromHM(8, 30),
|
|
180
|
-
departureTime: timeFromHM(8, 45),
|
|
181
|
-
},
|
|
182
|
-
{
|
|
183
|
-
id: 2,
|
|
184
|
-
arrivalTime: timeFromHM(9, 0),
|
|
185
|
-
departureTime: timeFromHM(9, 10),
|
|
186
|
-
},
|
|
187
|
-
],
|
|
188
|
-
},
|
|
189
|
-
],
|
|
190
|
-
}),
|
|
191
|
-
// Route 1: stops 3 -> 1 -> 4
|
|
192
|
-
Route.of({
|
|
193
|
-
id: 1,
|
|
194
|
-
serviceRouteId: 1,
|
|
195
|
-
trips: [
|
|
196
|
-
{
|
|
197
|
-
stops: [
|
|
198
|
-
{
|
|
199
|
-
id: 3,
|
|
200
|
-
arrivalTime: timeFromHM(8, 5),
|
|
201
|
-
departureTime: timeFromHM(8, 20),
|
|
202
|
-
},
|
|
203
|
-
{
|
|
204
|
-
id: 1,
|
|
205
|
-
arrivalTime: timeFromHM(9, 0),
|
|
206
|
-
departureTime: timeFromHM(9, 15),
|
|
207
|
-
},
|
|
208
|
-
{
|
|
209
|
-
id: 4,
|
|
210
|
-
arrivalTime: timeFromHM(9, 20),
|
|
211
|
-
departureTime: timeFromHM(9, 35),
|
|
212
|
-
},
|
|
213
|
-
],
|
|
214
|
-
},
|
|
215
|
-
],
|
|
216
|
-
}),
|
|
217
|
-
];
|
|
218
|
-
|
|
219
|
-
const routes: ServiceRoute[] = [
|
|
220
|
-
{
|
|
221
|
-
type: 'BUS',
|
|
222
|
-
name: 'Line 1',
|
|
223
|
-
routes: [0],
|
|
224
|
-
},
|
|
225
|
-
{
|
|
226
|
-
type: 'RAIL',
|
|
227
|
-
name: 'Line 2',
|
|
228
|
-
routes: [1],
|
|
229
|
-
},
|
|
230
|
-
];
|
|
231
|
-
|
|
232
|
-
timetable = new Timetable(stopsAdjacency, routesAdjacency, routes);
|
|
233
|
-
|
|
234
|
-
const stops: Stop[] = [
|
|
235
|
-
{
|
|
236
|
-
id: 0,
|
|
237
|
-
sourceStopId: 'stop1',
|
|
238
|
-
name: 'Stop 1',
|
|
239
|
-
lat: 1.0,
|
|
240
|
-
lon: 1.0,
|
|
241
|
-
children: [],
|
|
242
|
-
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
243
|
-
},
|
|
244
|
-
{
|
|
245
|
-
id: 1,
|
|
246
|
-
sourceStopId: 'stop2',
|
|
247
|
-
name: 'Stop 2',
|
|
248
|
-
lat: 2.0,
|
|
249
|
-
lon: 2.0,
|
|
250
|
-
children: [],
|
|
251
|
-
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
252
|
-
},
|
|
253
|
-
{
|
|
254
|
-
id: 2,
|
|
255
|
-
sourceStopId: 'stop3',
|
|
256
|
-
name: 'Stop 3',
|
|
257
|
-
lat: 3.0,
|
|
258
|
-
lon: 3.0,
|
|
259
|
-
children: [],
|
|
260
|
-
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
261
|
-
},
|
|
262
|
-
{
|
|
263
|
-
id: 3,
|
|
264
|
-
sourceStopId: 'stop4',
|
|
265
|
-
name: 'Stop 4',
|
|
266
|
-
lat: 4.0,
|
|
267
|
-
lon: 4.0,
|
|
268
|
-
children: [],
|
|
269
|
-
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
270
|
-
},
|
|
271
|
-
{
|
|
272
|
-
id: 4,
|
|
273
|
-
sourceStopId: 'stop5',
|
|
274
|
-
name: 'Stop 5',
|
|
275
|
-
lat: 5.0,
|
|
276
|
-
lon: 5.0,
|
|
277
|
-
children: [],
|
|
278
|
-
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
279
|
-
},
|
|
280
|
-
];
|
|
281
|
-
|
|
282
|
-
const stopsIndex = new StopsIndex(stops);
|
|
283
|
-
router = new Router(timetable, stopsIndex);
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
it('should find a route with a change', () => {
|
|
287
|
-
const query = new Query.Builder()
|
|
288
|
-
.from(0)
|
|
289
|
-
.to(4)
|
|
290
|
-
.departureTime(timeFromHM(8, 0))
|
|
291
|
-
.build();
|
|
292
|
-
|
|
293
|
-
const result: Result = router.route(query);
|
|
294
|
-
const bestRoute = result.bestRoute();
|
|
295
|
-
|
|
296
|
-
// Should find a route with 2 legs:
|
|
297
|
-
// 1. Line 1: stop1 -> stop2 (arrive 08:30)
|
|
298
|
-
// 2. Line 2: stop2 -> stop5 (depart 09:15, arrive 09:20)
|
|
299
|
-
// Transfer at stop2 with 30 minutes to spare (default minTransferTime is 2 min)
|
|
300
|
-
assert.strictEqual(bestRoute?.legs.length, 2);
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
it('should correctly calculate the arrival time to a stop', () => {
|
|
304
|
-
const query = new Query.Builder()
|
|
305
|
-
.from(0)
|
|
306
|
-
.to(4)
|
|
307
|
-
.departureTime(timeFromHM(8, 0))
|
|
308
|
-
.build();
|
|
309
|
-
|
|
310
|
-
const result: Result = router.route(query);
|
|
311
|
-
const timeToStop5 = result.arrivalAt(4);
|
|
312
|
-
|
|
313
|
-
// Line 2 arrives at stop5 at 09:20
|
|
314
|
-
assert.strictEqual(timeToStop5?.arrival, timeFromHM(9, 20));
|
|
315
|
-
});
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
describe('with a walking transfer', () => {
|
|
319
|
-
let router: Router;
|
|
320
|
-
let timetable: Timetable;
|
|
321
|
-
|
|
322
|
-
beforeEach(() => {
|
|
323
|
-
// Setup: Two routes that don't share any stops, connected by a walking transfer
|
|
324
|
-
// Route 0 (Line 1): stop1 (depart 08:15) -> stop2 (08:25-08:35) -> stop3 (08:45)
|
|
325
|
-
// Route 1 (Line 2): stop4 (depart 08:20) -> stop5 (08:40-08:50) -> stop6 (09:10)
|
|
326
|
-
// Walking transfer from stop2 to stop5 with 5 minute minTransferTime
|
|
327
|
-
const stopsAdjacency: StopAdjacency[] = [
|
|
328
|
-
{ routes: [0] }, // stop 0 (stop1)
|
|
329
|
-
{
|
|
330
|
-
transfers: [
|
|
331
|
-
{
|
|
332
|
-
destination: 4,
|
|
333
|
-
type: 'REQUIRES_MINIMAL_TIME',
|
|
334
|
-
minTransferTime: durationFromSeconds(300), // 5 minutes walking
|
|
335
|
-
},
|
|
336
|
-
],
|
|
337
|
-
routes: [0],
|
|
338
|
-
}, // stop 1 (stop2) - has walking transfer to stop5
|
|
339
|
-
{ routes: [0] }, // stop 2 (stop3)
|
|
340
|
-
{ routes: [1] }, // stop 3 (stop4)
|
|
341
|
-
{ routes: [1] }, // stop 4 (stop5)
|
|
342
|
-
{ routes: [1] }, // stop 5 (stop6)
|
|
343
|
-
];
|
|
344
|
-
|
|
345
|
-
const routesAdjacency = [
|
|
346
|
-
// Route 0: stops 0 -> 1 -> 2
|
|
347
|
-
Route.of({
|
|
348
|
-
id: 0,
|
|
349
|
-
serviceRouteId: 0,
|
|
350
|
-
trips: [
|
|
351
|
-
{
|
|
352
|
-
stops: [
|
|
353
|
-
{
|
|
354
|
-
id: 0,
|
|
355
|
-
arrivalTime: timeFromHM(8, 0),
|
|
356
|
-
departureTime: timeFromHM(8, 15),
|
|
357
|
-
},
|
|
358
|
-
{
|
|
359
|
-
id: 1,
|
|
360
|
-
arrivalTime: timeFromHM(8, 25),
|
|
361
|
-
departureTime: timeFromHM(8, 35),
|
|
362
|
-
},
|
|
363
|
-
{
|
|
364
|
-
id: 2,
|
|
365
|
-
arrivalTime: timeFromHM(8, 45),
|
|
366
|
-
departureTime: timeFromHM(8, 55),
|
|
367
|
-
},
|
|
368
|
-
],
|
|
369
|
-
},
|
|
370
|
-
],
|
|
371
|
-
}),
|
|
372
|
-
// Route 1: stops 3 -> 4 -> 5
|
|
373
|
-
Route.of({
|
|
374
|
-
id: 1,
|
|
375
|
-
serviceRouteId: 1,
|
|
376
|
-
trips: [
|
|
377
|
-
{
|
|
378
|
-
stops: [
|
|
379
|
-
{
|
|
380
|
-
id: 3,
|
|
381
|
-
arrivalTime: timeFromHM(8, 10),
|
|
382
|
-
departureTime: timeFromHM(8, 20),
|
|
383
|
-
},
|
|
384
|
-
{
|
|
385
|
-
id: 4,
|
|
386
|
-
arrivalTime: timeFromHM(8, 40),
|
|
387
|
-
departureTime: timeFromHM(8, 50),
|
|
388
|
-
},
|
|
389
|
-
{
|
|
390
|
-
id: 5,
|
|
391
|
-
arrivalTime: timeFromHM(9, 10),
|
|
392
|
-
departureTime: timeFromHM(9, 10),
|
|
393
|
-
},
|
|
394
|
-
],
|
|
395
|
-
},
|
|
396
|
-
],
|
|
397
|
-
}),
|
|
398
|
-
];
|
|
399
|
-
|
|
400
|
-
const routes: ServiceRoute[] = [
|
|
401
|
-
{
|
|
402
|
-
type: 'BUS',
|
|
403
|
-
name: 'Line 1',
|
|
404
|
-
routes: [0],
|
|
405
|
-
},
|
|
406
|
-
{
|
|
407
|
-
type: 'RAIL',
|
|
408
|
-
name: 'Line 2',
|
|
409
|
-
routes: [1],
|
|
410
|
-
},
|
|
411
|
-
];
|
|
412
|
-
|
|
413
|
-
timetable = new Timetable(stopsAdjacency, routesAdjacency, routes);
|
|
414
|
-
const stops: Stop[] = [
|
|
415
|
-
{
|
|
416
|
-
id: 0,
|
|
417
|
-
sourceStopId: 'stop1',
|
|
418
|
-
name: 'Stop 1',
|
|
419
|
-
lat: 1.0,
|
|
420
|
-
lon: 1.0,
|
|
421
|
-
children: [],
|
|
422
|
-
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
423
|
-
},
|
|
424
|
-
{
|
|
425
|
-
id: 1,
|
|
426
|
-
sourceStopId: 'stop2',
|
|
427
|
-
name: 'Stop 2',
|
|
428
|
-
lat: 2.0,
|
|
429
|
-
lon: 2.0,
|
|
430
|
-
children: [],
|
|
431
|
-
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
432
|
-
},
|
|
433
|
-
{
|
|
434
|
-
id: 2,
|
|
435
|
-
sourceStopId: 'stop3',
|
|
436
|
-
name: 'Stop 3',
|
|
437
|
-
lat: 3.0,
|
|
438
|
-
lon: 3.0,
|
|
439
|
-
children: [],
|
|
440
|
-
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
441
|
-
},
|
|
442
|
-
{
|
|
443
|
-
id: 3,
|
|
444
|
-
sourceStopId: 'stop4',
|
|
445
|
-
name: 'Stop 4',
|
|
446
|
-
lat: 4.0,
|
|
447
|
-
lon: 4.0,
|
|
448
|
-
children: [],
|
|
449
|
-
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
450
|
-
},
|
|
451
|
-
{
|
|
452
|
-
id: 4,
|
|
453
|
-
sourceStopId: 'stop5',
|
|
454
|
-
name: 'Stop 5',
|
|
455
|
-
lat: 5.0,
|
|
456
|
-
lon: 5.0,
|
|
457
|
-
children: [],
|
|
458
|
-
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
459
|
-
},
|
|
460
|
-
{
|
|
461
|
-
id: 5,
|
|
462
|
-
sourceStopId: 'stop6',
|
|
463
|
-
name: 'Stop 6',
|
|
464
|
-
lat: 6.0,
|
|
465
|
-
lon: 6.0,
|
|
466
|
-
children: [],
|
|
467
|
-
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
468
|
-
},
|
|
469
|
-
];
|
|
470
|
-
const stopsIndex = new StopsIndex(stops);
|
|
471
|
-
router = new Router(timetable, stopsIndex);
|
|
472
|
-
});
|
|
473
|
-
|
|
474
|
-
it('should find a route with a walking transfer', () => {
|
|
475
|
-
const query = new Query.Builder()
|
|
476
|
-
.from(0)
|
|
477
|
-
.to(5)
|
|
478
|
-
.departureTime(timeFromHM(8, 0))
|
|
479
|
-
.build();
|
|
480
|
-
|
|
481
|
-
const result: Result = router.route(query);
|
|
482
|
-
const bestRoute = result.bestRoute();
|
|
483
|
-
|
|
484
|
-
// Should find a route with 3 legs:
|
|
485
|
-
// 1. Line 1: stop1 -> stop2 (arrive 08:25)
|
|
486
|
-
// 2. Walking transfer: stop2 -> stop5 (5 min walk, arrive 08:30)
|
|
487
|
-
// 3. Line 2: stop5 -> stop6 (depart 08:50, arrive 09:10)
|
|
488
|
-
assert.strictEqual(bestRoute?.legs.length, 3);
|
|
489
|
-
});
|
|
490
|
-
|
|
491
|
-
it('should correctly calculate the arrival time at intermediate stop', () => {
|
|
492
|
-
const query = new Query.Builder()
|
|
493
|
-
.from(0)
|
|
494
|
-
.to(5)
|
|
495
|
-
.departureTime(timeFromHM(8, 0))
|
|
496
|
-
.build();
|
|
497
|
-
|
|
498
|
-
const result: Result = router.route(query);
|
|
499
|
-
const timeToStop5 = result.arrivalAt(4);
|
|
500
|
-
|
|
501
|
-
// Arrive at stop2 at 08:25, walk 5 min to stop5, arrive at 08:30
|
|
502
|
-
assert.strictEqual(timeToStop5?.arrival, timeFromHM(8, 30));
|
|
503
|
-
});
|
|
504
|
-
});
|
|
505
|
-
|
|
506
|
-
describe('with a faster change', () => {
|
|
507
|
-
let router: Router;
|
|
508
|
-
let timetable: Timetable;
|
|
509
|
-
|
|
510
|
-
beforeEach(() => {
|
|
511
|
-
// Setup: Three routes where Route 2 is direct but slower than Route 0 + Route 1 with a change
|
|
512
|
-
// Route 0 (Line 1): stop1 (08:15) -> stop2 (08:30-08:45) -> stop3 (09:00)
|
|
513
|
-
// Route 1 (Line 2): stop4 (08:25) -> stop2 (08:50-09:05) -> stop5 (09:10)
|
|
514
|
-
// Route 2 (Line 3): stop1 (08:15) -> stop5 (09:45) - direct but slower
|
|
515
|
-
// The router should prefer Route 0 + Route 1 (arrive 09:10) over Route 2 (arrive 09:45)
|
|
516
|
-
const stopsAdjacency: StopAdjacency[] = [
|
|
517
|
-
{ routes: [0, 2] }, // stop 0 (stop1) - served by Line 1 and Line 3
|
|
518
|
-
{ routes: [0, 1] }, // stop 1 (stop2) - transfer point
|
|
519
|
-
{ routes: [0] }, // stop 2 (stop3)
|
|
520
|
-
{ routes: [1] }, // stop 3 (stop4)
|
|
521
|
-
{ routes: [1, 2] }, // stop 4 (stop5) - destination
|
|
522
|
-
];
|
|
523
|
-
|
|
524
|
-
const routesAdjacency = [
|
|
525
|
-
// Route 0: stops 0 -> 1 -> 2
|
|
526
|
-
Route.of({
|
|
527
|
-
id: 0,
|
|
528
|
-
serviceRouteId: 0,
|
|
529
|
-
trips: [
|
|
530
|
-
{
|
|
531
|
-
stops: [
|
|
532
|
-
{
|
|
533
|
-
id: 0,
|
|
534
|
-
arrivalTime: timeFromHM(8, 0),
|
|
535
|
-
departureTime: timeFromHM(8, 15),
|
|
536
|
-
},
|
|
537
|
-
{
|
|
538
|
-
id: 1,
|
|
539
|
-
arrivalTime: timeFromHM(8, 30),
|
|
540
|
-
departureTime: timeFromHM(8, 45),
|
|
541
|
-
},
|
|
542
|
-
{
|
|
543
|
-
id: 2,
|
|
544
|
-
arrivalTime: timeFromHM(9, 0),
|
|
545
|
-
departureTime: timeFromHM(9, 15),
|
|
546
|
-
},
|
|
547
|
-
],
|
|
548
|
-
},
|
|
549
|
-
],
|
|
550
|
-
}),
|
|
551
|
-
// Route 1: stops 3 -> 1 -> 4
|
|
552
|
-
Route.of({
|
|
553
|
-
id: 1,
|
|
554
|
-
serviceRouteId: 1,
|
|
555
|
-
trips: [
|
|
556
|
-
{
|
|
557
|
-
stops: [
|
|
558
|
-
{
|
|
559
|
-
id: 3,
|
|
560
|
-
arrivalTime: timeFromHM(8, 10),
|
|
561
|
-
departureTime: timeFromHM(8, 25),
|
|
562
|
-
},
|
|
563
|
-
{
|
|
564
|
-
id: 1,
|
|
565
|
-
arrivalTime: timeFromHM(8, 50),
|
|
566
|
-
departureTime: timeFromHM(9, 5),
|
|
567
|
-
},
|
|
568
|
-
{
|
|
569
|
-
id: 4,
|
|
570
|
-
arrivalTime: timeFromHM(9, 10),
|
|
571
|
-
departureTime: timeFromHM(9, 25),
|
|
572
|
-
},
|
|
573
|
-
],
|
|
574
|
-
},
|
|
575
|
-
],
|
|
576
|
-
}),
|
|
577
|
-
// Route 2: stops 0 -> 4 (direct but slow)
|
|
578
|
-
Route.of({
|
|
579
|
-
id: 2,
|
|
580
|
-
serviceRouteId: 2,
|
|
581
|
-
trips: [
|
|
582
|
-
{
|
|
583
|
-
stops: [
|
|
584
|
-
{
|
|
585
|
-
id: 0,
|
|
586
|
-
arrivalTime: timeFromHM(8, 0),
|
|
587
|
-
departureTime: timeFromHM(8, 15),
|
|
588
|
-
},
|
|
589
|
-
{
|
|
590
|
-
id: 4,
|
|
591
|
-
arrivalTime: timeFromHM(9, 45),
|
|
592
|
-
departureTime: timeFromHM(10, 0),
|
|
593
|
-
},
|
|
594
|
-
],
|
|
595
|
-
},
|
|
596
|
-
],
|
|
597
|
-
}),
|
|
598
|
-
];
|
|
599
|
-
|
|
600
|
-
const routes: ServiceRoute[] = [
|
|
601
|
-
{
|
|
602
|
-
type: 'BUS',
|
|
603
|
-
name: 'Line 1',
|
|
604
|
-
routes: [0],
|
|
605
|
-
},
|
|
606
|
-
{
|
|
607
|
-
type: 'RAIL',
|
|
608
|
-
name: 'Line 2',
|
|
609
|
-
routes: [1],
|
|
610
|
-
},
|
|
611
|
-
{
|
|
612
|
-
type: 'FERRY',
|
|
613
|
-
name: 'Line 3',
|
|
614
|
-
routes: [2],
|
|
615
|
-
},
|
|
616
|
-
];
|
|
617
|
-
|
|
618
|
-
timetable = new Timetable(stopsAdjacency, routesAdjacency, routes);
|
|
619
|
-
const stops: Stop[] = [
|
|
620
|
-
{
|
|
621
|
-
id: 0,
|
|
622
|
-
sourceStopId: 'stop1',
|
|
623
|
-
name: 'Stop 1',
|
|
624
|
-
lat: 1.0,
|
|
625
|
-
lon: 1.0,
|
|
626
|
-
children: [],
|
|
627
|
-
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
628
|
-
},
|
|
629
|
-
{
|
|
630
|
-
id: 1,
|
|
631
|
-
sourceStopId: 'stop2',
|
|
632
|
-
name: 'Stop 2',
|
|
633
|
-
lat: 2.0,
|
|
634
|
-
lon: 2.0,
|
|
635
|
-
children: [],
|
|
636
|
-
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
637
|
-
},
|
|
638
|
-
{
|
|
639
|
-
id: 2,
|
|
640
|
-
sourceStopId: 'stop3',
|
|
641
|
-
name: 'Stop 3',
|
|
642
|
-
lat: 3.0,
|
|
643
|
-
lon: 3.0,
|
|
644
|
-
children: [],
|
|
645
|
-
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
646
|
-
},
|
|
647
|
-
{
|
|
648
|
-
id: 3,
|
|
649
|
-
sourceStopId: 'stop4',
|
|
650
|
-
name: 'Stop 4',
|
|
651
|
-
lat: 4.0,
|
|
652
|
-
lon: 4.0,
|
|
653
|
-
children: [],
|
|
654
|
-
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
655
|
-
},
|
|
656
|
-
{
|
|
657
|
-
id: 4,
|
|
658
|
-
sourceStopId: 'stop5',
|
|
659
|
-
name: 'Stop 5',
|
|
660
|
-
lat: 5.0,
|
|
661
|
-
lon: 5.0,
|
|
662
|
-
children: [],
|
|
663
|
-
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
664
|
-
},
|
|
665
|
-
];
|
|
666
|
-
const stopsIndex = new StopsIndex(stops);
|
|
667
|
-
router = new Router(timetable, stopsIndex);
|
|
668
|
-
});
|
|
669
|
-
|
|
670
|
-
it('should prefer a faster route with a change over a slower direct route', () => {
|
|
671
|
-
const query = new Query.Builder()
|
|
672
|
-
.from(0)
|
|
673
|
-
.to(4)
|
|
674
|
-
.departureTime(timeFromHM(8, 0))
|
|
675
|
-
.build();
|
|
676
|
-
|
|
677
|
-
const result: Result = router.route(query);
|
|
678
|
-
const bestRoute = result.bestRoute();
|
|
679
|
-
|
|
680
|
-
// Should prefer the 2-leg route (Line 1 + Line 2, arrive 09:10)
|
|
681
|
-
// over the 1-leg direct route (Line 3, arrive 09:45)
|
|
682
|
-
assert.strictEqual(bestRoute?.legs.length, 2);
|
|
683
|
-
});
|
|
684
|
-
});
|
|
685
|
-
|
|
686
|
-
describe('with route continuation (in-seat transfer)', () => {
|
|
687
|
-
let router: Router;
|
|
688
|
-
let timetable: Timetable;
|
|
689
|
-
|
|
690
|
-
beforeEach(() => {
|
|
691
|
-
// Setup: Route 0 continues as Route 1 at stop2 (same vehicle, different route number)
|
|
692
|
-
// This is an "in-seat transfer" where passengers can stay on the vehicle
|
|
693
|
-
// Route 0 (Line 1): stop1 (depart 08:10) -> stop2 (08:15-08:25)
|
|
694
|
-
// Route 1 (Line 2): stop2 (08:15-08:25) -> stop3 (08:35) -> stop4 (08:55)
|
|
695
|
-
// Trip continuation: Route 0 trip 0 at stop2 continues as Route 1 trip 0
|
|
696
|
-
// encode(1, 0, 0) = stop index 1 on route 0, trip 0 (where the continuation starts)
|
|
697
|
-
const tripContinuations: TripTransfers = new Map([
|
|
698
|
-
[encode(1, 0, 0), [{ stopIndex: 0, routeId: 1, tripIndex: 0 }]],
|
|
699
|
-
]);
|
|
700
|
-
|
|
701
|
-
const stopsAdjacency: StopAdjacency[] = [
|
|
702
|
-
{ routes: [0] }, // stop 0 (stop1)
|
|
703
|
-
{ routes: [0, 1] }, // stop 1 (stop2) - continuation point
|
|
704
|
-
{ routes: [1] }, // stop 2 (stop3)
|
|
705
|
-
{ routes: [1] }, // stop 3 (stop4)
|
|
706
|
-
];
|
|
707
|
-
|
|
708
|
-
const routesAdjacency = [
|
|
709
|
-
// Route 0: stops 0 -> 1
|
|
710
|
-
Route.of({
|
|
711
|
-
id: 0,
|
|
712
|
-
serviceRouteId: 0,
|
|
713
|
-
trips: [
|
|
714
|
-
{
|
|
715
|
-
stops: [
|
|
716
|
-
{
|
|
717
|
-
id: 0,
|
|
718
|
-
arrivalTime: timeFromHM(8, 0),
|
|
719
|
-
departureTime: timeFromHM(8, 10),
|
|
720
|
-
},
|
|
721
|
-
{
|
|
722
|
-
id: 1,
|
|
723
|
-
arrivalTime: timeFromHM(8, 15),
|
|
724
|
-
departureTime: timeFromHM(8, 25),
|
|
725
|
-
},
|
|
726
|
-
],
|
|
727
|
-
},
|
|
728
|
-
],
|
|
729
|
-
}),
|
|
730
|
-
// Route 1: stops 1 -> 2 -> 3 (continuation from route 0)
|
|
731
|
-
Route.of({
|
|
732
|
-
id: 1,
|
|
733
|
-
serviceRouteId: 1,
|
|
734
|
-
trips: [
|
|
735
|
-
{
|
|
736
|
-
stops: [
|
|
737
|
-
{
|
|
738
|
-
id: 1,
|
|
739
|
-
arrivalTime: timeFromHM(8, 15),
|
|
740
|
-
departureTime: timeFromHM(8, 25),
|
|
741
|
-
},
|
|
742
|
-
{
|
|
743
|
-
id: 2,
|
|
744
|
-
arrivalTime: timeFromHM(8, 35),
|
|
745
|
-
departureTime: timeFromHM(8, 45),
|
|
746
|
-
},
|
|
747
|
-
{
|
|
748
|
-
id: 3,
|
|
749
|
-
arrivalTime: timeFromHM(8, 55),
|
|
750
|
-
departureTime: timeFromHM(9, 5),
|
|
751
|
-
},
|
|
752
|
-
],
|
|
753
|
-
},
|
|
754
|
-
],
|
|
755
|
-
}),
|
|
756
|
-
];
|
|
757
|
-
|
|
758
|
-
const routes: ServiceRoute[] = [
|
|
759
|
-
{
|
|
760
|
-
type: 'BUS',
|
|
761
|
-
name: 'Line 1',
|
|
762
|
-
routes: [0],
|
|
763
|
-
},
|
|
764
|
-
{
|
|
765
|
-
type: 'BUS',
|
|
766
|
-
name: 'Line 2',
|
|
767
|
-
routes: [1],
|
|
768
|
-
},
|
|
769
|
-
];
|
|
770
|
-
|
|
771
|
-
timetable = new Timetable(
|
|
772
|
-
stopsAdjacency,
|
|
773
|
-
routesAdjacency,
|
|
774
|
-
routes,
|
|
775
|
-
tripContinuations,
|
|
776
|
-
);
|
|
777
|
-
|
|
778
|
-
const stops: Stop[] = [
|
|
779
|
-
{
|
|
780
|
-
id: 0,
|
|
781
|
-
sourceStopId: 'stop1',
|
|
782
|
-
name: 'Stop 1',
|
|
783
|
-
lat: 1.0,
|
|
784
|
-
lon: 1.0,
|
|
785
|
-
children: [],
|
|
786
|
-
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
787
|
-
},
|
|
788
|
-
{
|
|
789
|
-
id: 1,
|
|
790
|
-
sourceStopId: 'stop2',
|
|
791
|
-
name: 'Stop 2',
|
|
792
|
-
lat: 2.0,
|
|
793
|
-
lon: 2.0,
|
|
794
|
-
children: [],
|
|
795
|
-
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
796
|
-
},
|
|
797
|
-
{
|
|
798
|
-
id: 2,
|
|
799
|
-
sourceStopId: 'stop3',
|
|
800
|
-
name: 'Stop 3',
|
|
801
|
-
lat: 3.0,
|
|
802
|
-
lon: 3.0,
|
|
803
|
-
children: [],
|
|
804
|
-
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
805
|
-
},
|
|
806
|
-
{
|
|
807
|
-
id: 3,
|
|
808
|
-
sourceStopId: 'stop4',
|
|
809
|
-
name: 'Stop 4',
|
|
810
|
-
lat: 4.0,
|
|
811
|
-
lon: 4.0,
|
|
812
|
-
children: [],
|
|
813
|
-
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
814
|
-
},
|
|
815
|
-
];
|
|
816
|
-
|
|
817
|
-
const stopsIndex = new StopsIndex(stops);
|
|
818
|
-
router = new Router(timetable, stopsIndex);
|
|
819
|
-
});
|
|
820
|
-
|
|
821
|
-
it('should find a route using continuation (in-seat transfer)', () => {
|
|
822
|
-
const query = new Query.Builder()
|
|
823
|
-
.from(0)
|
|
824
|
-
.to(3)
|
|
825
|
-
.departureTime(timeFromHM(8, 0))
|
|
826
|
-
.build();
|
|
827
|
-
|
|
828
|
-
const result: Result = router.route(query);
|
|
829
|
-
const bestRoute = result.bestRoute();
|
|
830
|
-
|
|
831
|
-
// Should find a route with only 1 leg because the continuation allows
|
|
832
|
-
// staying on the same vehicle when it changes route numbers
|
|
833
|
-
assert.strictEqual(bestRoute?.legs.length, 1);
|
|
834
|
-
});
|
|
835
|
-
|
|
836
|
-
it('should correctly calculate arrival time with continuation', () => {
|
|
837
|
-
const query = new Query.Builder()
|
|
838
|
-
.from(0)
|
|
839
|
-
.to(3)
|
|
840
|
-
.departureTime(timeFromHM(8, 0))
|
|
841
|
-
.build();
|
|
842
|
-
|
|
843
|
-
const result: Result = router.route(query);
|
|
844
|
-
const timeToStop4 = result.arrivalAt(3);
|
|
845
|
-
|
|
846
|
-
// Route 1 (continuation of Route 0) arrives at stop4 at 08:55
|
|
847
|
-
assert.strictEqual(timeToStop4?.arrival, timeFromHM(8, 55));
|
|
848
|
-
});
|
|
849
|
-
});
|
|
850
|
-
|
|
851
|
-
describe('with guaranteed trip transfers', () => {
|
|
852
|
-
let router: Router;
|
|
853
|
-
let timetable: Timetable;
|
|
854
|
-
|
|
855
|
-
beforeEach(() => {
|
|
856
|
-
// Setup: Route 0 trip 0 has a guaranteed transfer to Route 1 trip 0 at stop2
|
|
857
|
-
// The transfer time is only 1 minute (60 seconds), less than the 5-minute minTransferTime
|
|
858
|
-
// But since it's guaranteed, it should still be considered
|
|
859
|
-
// Route 0: stop1 (depart 08:10) -> stop2 (arrive 08:20)
|
|
860
|
-
// Route 1: stop2 (depart 08:21) -> stop3 (arrive 08:40)
|
|
861
|
-
// encode(1, 0, 0) = stop index 1 on route 0, trip 0 (where we alight)
|
|
862
|
-
// destination { stopIndex: 0, routeId: 1, tripIndex: 0 } = stop index 0 on route 1, trip 0 (where we board)
|
|
863
|
-
const guaranteedTripTransfers: TripTransfers = new Map([
|
|
864
|
-
[encode(1, 0, 0), [{ stopIndex: 0, routeId: 1, tripIndex: 0 }]],
|
|
865
|
-
]);
|
|
866
|
-
|
|
867
|
-
const stopsAdjacency: StopAdjacency[] = [
|
|
868
|
-
{ routes: [0] }, // stop 0 (stop1)
|
|
869
|
-
{ routes: [0, 1] }, // stop 1 (stop2) - both routes serve this stop
|
|
870
|
-
{ routes: [1] }, // stop 2 (stop3)
|
|
871
|
-
];
|
|
872
|
-
|
|
873
|
-
const routesAdjacency = [
|
|
874
|
-
// Route 0: stops 0 -> 1
|
|
875
|
-
Route.of({
|
|
876
|
-
id: 0,
|
|
877
|
-
serviceRouteId: 0,
|
|
878
|
-
trips: [
|
|
879
|
-
{
|
|
880
|
-
stops: [
|
|
881
|
-
{
|
|
882
|
-
id: 0,
|
|
883
|
-
arrivalTime: timeFromHM(8, 0),
|
|
884
|
-
departureTime: timeFromHM(8, 10),
|
|
885
|
-
},
|
|
886
|
-
{
|
|
887
|
-
id: 1,
|
|
888
|
-
arrivalTime: timeFromHM(8, 20),
|
|
889
|
-
departureTime: timeFromHM(8, 30),
|
|
890
|
-
},
|
|
891
|
-
],
|
|
892
|
-
},
|
|
893
|
-
],
|
|
894
|
-
}),
|
|
895
|
-
// Route 1: stops 1 -> 2
|
|
896
|
-
// Departure at 08:21, only 1 minute after arrival from route 0
|
|
897
|
-
// Without the guaranteed transfer, this would be missed with a 5-minute minTransferTime
|
|
898
|
-
Route.of({
|
|
899
|
-
id: 1,
|
|
900
|
-
serviceRouteId: 1,
|
|
901
|
-
trips: [
|
|
902
|
-
{
|
|
903
|
-
stops: [
|
|
904
|
-
{
|
|
905
|
-
id: 1,
|
|
906
|
-
arrivalTime: timeFromHM(8, 15),
|
|
907
|
-
departureTime: timeFromHM(8, 21),
|
|
908
|
-
},
|
|
909
|
-
{
|
|
910
|
-
id: 2,
|
|
911
|
-
arrivalTime: timeFromHM(8, 40),
|
|
912
|
-
departureTime: timeFromHM(8, 50),
|
|
913
|
-
},
|
|
914
|
-
],
|
|
915
|
-
},
|
|
916
|
-
],
|
|
917
|
-
}),
|
|
918
|
-
];
|
|
919
|
-
|
|
920
|
-
const routes: ServiceRoute[] = [
|
|
921
|
-
{
|
|
922
|
-
type: 'BUS',
|
|
923
|
-
name: 'Line 1',
|
|
924
|
-
routes: [0],
|
|
925
|
-
},
|
|
926
|
-
{
|
|
927
|
-
type: 'BUS',
|
|
928
|
-
name: 'Line 2',
|
|
929
|
-
routes: [1],
|
|
930
|
-
},
|
|
931
|
-
];
|
|
932
|
-
|
|
933
|
-
timetable = new Timetable(
|
|
934
|
-
stopsAdjacency,
|
|
935
|
-
routesAdjacency,
|
|
936
|
-
routes,
|
|
937
|
-
undefined, // no trip continuations
|
|
938
|
-
guaranteedTripTransfers,
|
|
939
|
-
);
|
|
18
|
+
// Minimal two-stop timetable used by all tests in this file.
|
|
19
|
+
const stopsAdjacency: StopAdjacency[] = [{ routes: [0] }, { routes: [0] }];
|
|
20
|
+
const routesAdjacency = [
|
|
21
|
+
Route.of({
|
|
22
|
+
id: 0,
|
|
23
|
+
serviceRouteId: 0,
|
|
24
|
+
trips: [
|
|
25
|
+
{
|
|
26
|
+
stops: [
|
|
27
|
+
{
|
|
28
|
+
id: 0,
|
|
29
|
+
arrivalTime: timeFromHM(8, 0),
|
|
30
|
+
departureTime: timeFromHM(8, 10),
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 1,
|
|
34
|
+
arrivalTime: timeFromHM(8, 30),
|
|
35
|
+
departureTime: timeFromHM(8, 30),
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
}),
|
|
41
|
+
];
|
|
42
|
+
const serviceRoutes: ServiceRoute[] = [
|
|
43
|
+
{ type: 'BUS', name: 'Line 1', routes: [0] },
|
|
44
|
+
];
|
|
45
|
+
const stops: Stop[] = [
|
|
46
|
+
{
|
|
47
|
+
id: 0,
|
|
48
|
+
sourceStopId: 'A',
|
|
49
|
+
name: 'Stop A',
|
|
50
|
+
lat: 0,
|
|
51
|
+
lon: 0,
|
|
52
|
+
children: [],
|
|
53
|
+
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 1,
|
|
57
|
+
sourceStopId: 'B',
|
|
58
|
+
name: 'Stop B',
|
|
59
|
+
lat: 0,
|
|
60
|
+
lon: 0,
|
|
61
|
+
children: [],
|
|
62
|
+
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
const timetable = new Timetable(stopsAdjacency, routesAdjacency, serviceRoutes);
|
|
66
|
+
const stopsIndex = new StopsIndex(stops);
|
|
67
|
+
const router = new Router(timetable, stopsIndex);
|
|
940
68
|
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
{
|
|
952
|
-
id: 1,
|
|
953
|
-
sourceStopId: 'stop2',
|
|
954
|
-
name: 'Stop 2',
|
|
955
|
-
lat: 2.0,
|
|
956
|
-
lon: 2.0,
|
|
957
|
-
children: [],
|
|
958
|
-
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
959
|
-
},
|
|
960
|
-
{
|
|
961
|
-
id: 2,
|
|
962
|
-
sourceStopId: 'stop3',
|
|
963
|
-
name: 'Stop 3',
|
|
964
|
-
lat: 3.0,
|
|
965
|
-
lon: 3.0,
|
|
966
|
-
children: [],
|
|
967
|
-
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
968
|
-
},
|
|
969
|
-
];
|
|
970
|
-
|
|
971
|
-
const stopsIndex = new StopsIndex(stops);
|
|
972
|
-
router = new Router(timetable, stopsIndex);
|
|
973
|
-
});
|
|
974
|
-
|
|
975
|
-
it('should consider guaranteed transfer even with less time than minTransferTime', () => {
|
|
976
|
-
const query = new Query.Builder()
|
|
977
|
-
.from(0)
|
|
978
|
-
.to(2)
|
|
979
|
-
.departureTime(timeFromHM(8, 0))
|
|
980
|
-
.minTransferTime(durationFromSeconds(300)) // 5 minutes, but transfer only has 1 minute
|
|
981
|
-
.build();
|
|
982
|
-
|
|
983
|
-
const result: Result = router.route(query);
|
|
984
|
-
const bestRoute = result.bestRoute();
|
|
985
|
-
|
|
986
|
-
// Should find a route with 3 legs:
|
|
987
|
-
// 1. Vehicle leg (route 0)
|
|
988
|
-
// 2. Guaranteed transfer leg (shown as type: 'GUARANTEED')
|
|
989
|
-
// 3. Vehicle leg (route 1)
|
|
990
|
-
assert.ok(bestRoute);
|
|
991
|
-
assert.strictEqual(bestRoute.legs.length, 3);
|
|
992
|
-
|
|
993
|
-
// The middle leg should be the guaranteed transfer
|
|
994
|
-
const transferLeg = bestRoute.legs[1];
|
|
995
|
-
assert.ok(transferLeg && 'type' in transferLeg);
|
|
996
|
-
assert.strictEqual(transferLeg.type, 'GUARANTEED');
|
|
997
|
-
|
|
998
|
-
// Should arrive at 08:40 because the guaranteed transfer allows catching
|
|
999
|
-
// the 08:21 departure despite only having 1 minute of transfer time
|
|
1000
|
-
assert.strictEqual(result.arrivalAt(2)?.arrival, timeFromHM(8, 40));
|
|
1001
|
-
});
|
|
1002
|
-
});
|
|
1003
|
-
|
|
1004
|
-
describe('with non-guaranteed transfers and minTransferTime', () => {
|
|
1005
|
-
let router: Router;
|
|
1006
|
-
let timetable: Timetable;
|
|
1007
|
-
|
|
1008
|
-
beforeEach(() => {
|
|
1009
|
-
// Setup: Same-stop transfer (route change) without guaranteed transfer
|
|
1010
|
-
// Both routes serve stop2, so this is a route change at the same stop
|
|
1011
|
-
// The query's minTransferTime should be respected
|
|
1012
|
-
// Route 0: stop1 (depart 08:10) -> stop2 (arrive 08:20)
|
|
1013
|
-
// Route 1 trip 0: stop2 (depart 08:21) -> stop3 (arrive 08:35) - NOT catchable with 5 min minTransferTime
|
|
1014
|
-
// Route 1 trip 1: stop2 (depart 08:26) -> stop3 (arrive 08:45) - catchable with 5 min minTransferTime
|
|
1015
|
-
const stopsAdjacency: StopAdjacency[] = [
|
|
1016
|
-
{ routes: [0] }, // stop 0 (stop1)
|
|
1017
|
-
{ routes: [0, 1] }, // stop 1 (stop2) - both routes serve this stop
|
|
1018
|
-
{ routes: [1] }, // stop 2 (stop3)
|
|
1019
|
-
];
|
|
1020
|
-
|
|
1021
|
-
const routesAdjacency = [
|
|
1022
|
-
// Route 0: stops 0 -> 1
|
|
1023
|
-
Route.of({
|
|
1024
|
-
id: 0,
|
|
1025
|
-
serviceRouteId: 0,
|
|
1026
|
-
trips: [
|
|
1027
|
-
{
|
|
1028
|
-
stops: [
|
|
1029
|
-
{
|
|
1030
|
-
id: 0,
|
|
1031
|
-
arrivalTime: timeFromHM(8, 0),
|
|
1032
|
-
departureTime: timeFromHM(8, 10),
|
|
1033
|
-
},
|
|
1034
|
-
{
|
|
1035
|
-
id: 1,
|
|
1036
|
-
arrivalTime: timeFromHM(8, 20),
|
|
1037
|
-
departureTime: timeFromHM(8, 30),
|
|
1038
|
-
},
|
|
1039
|
-
],
|
|
1040
|
-
},
|
|
1041
|
-
],
|
|
1042
|
-
}),
|
|
1043
|
-
// Route 1: stops 1 -> 2
|
|
1044
|
-
// Trip 0: Departure at 08:21, only 1 minute after arrival - should NOT be catchable with 5 min minTransferTime
|
|
1045
|
-
// Trip 1: Departure at 08:26, 6 minutes after arrival - should be catchable with 5 min minTransferTime
|
|
1046
|
-
Route.of({
|
|
1047
|
-
id: 1,
|
|
1048
|
-
serviceRouteId: 1,
|
|
1049
|
-
trips: [
|
|
1050
|
-
{
|
|
1051
|
-
stops: [
|
|
1052
|
-
{
|
|
1053
|
-
id: 1,
|
|
1054
|
-
arrivalTime: timeFromHM(8, 15),
|
|
1055
|
-
departureTime: timeFromHM(8, 21),
|
|
1056
|
-
},
|
|
1057
|
-
{
|
|
1058
|
-
id: 2,
|
|
1059
|
-
arrivalTime: timeFromHM(8, 35),
|
|
1060
|
-
departureTime: timeFromHM(8, 45),
|
|
1061
|
-
},
|
|
1062
|
-
],
|
|
1063
|
-
},
|
|
1064
|
-
{
|
|
1065
|
-
stops: [
|
|
1066
|
-
{
|
|
1067
|
-
id: 1,
|
|
1068
|
-
arrivalTime: timeFromHM(8, 20),
|
|
1069
|
-
departureTime: timeFromHM(8, 26),
|
|
1070
|
-
},
|
|
1071
|
-
{
|
|
1072
|
-
id: 2,
|
|
1073
|
-
arrivalTime: timeFromHM(8, 45),
|
|
1074
|
-
departureTime: timeFromHM(8, 55),
|
|
1075
|
-
},
|
|
1076
|
-
],
|
|
1077
|
-
},
|
|
1078
|
-
],
|
|
1079
|
-
}),
|
|
1080
|
-
];
|
|
1081
|
-
|
|
1082
|
-
const routes: ServiceRoute[] = [
|
|
1083
|
-
{
|
|
1084
|
-
type: 'BUS',
|
|
1085
|
-
name: 'Line 1',
|
|
1086
|
-
routes: [0],
|
|
1087
|
-
},
|
|
1088
|
-
{
|
|
1089
|
-
type: 'BUS',
|
|
1090
|
-
name: 'Line 2',
|
|
1091
|
-
routes: [1],
|
|
1092
|
-
},
|
|
1093
|
-
];
|
|
1094
|
-
|
|
1095
|
-
timetable = new Timetable(stopsAdjacency, routesAdjacency, routes);
|
|
1096
|
-
|
|
1097
|
-
const stops: Stop[] = [
|
|
1098
|
-
{
|
|
1099
|
-
id: 0,
|
|
1100
|
-
sourceStopId: 'stop1',
|
|
1101
|
-
name: 'Stop 1',
|
|
1102
|
-
lat: 1.0,
|
|
1103
|
-
lon: 1.0,
|
|
1104
|
-
children: [],
|
|
1105
|
-
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
1106
|
-
},
|
|
1107
|
-
{
|
|
1108
|
-
id: 1,
|
|
1109
|
-
sourceStopId: 'stop2',
|
|
1110
|
-
name: 'Stop 2',
|
|
1111
|
-
lat: 2.0,
|
|
1112
|
-
lon: 2.0,
|
|
1113
|
-
children: [],
|
|
1114
|
-
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
1115
|
-
},
|
|
1116
|
-
{
|
|
1117
|
-
id: 2,
|
|
1118
|
-
sourceStopId: 'stop3',
|
|
1119
|
-
name: 'Stop 3',
|
|
1120
|
-
lat: 3.0,
|
|
1121
|
-
lon: 3.0,
|
|
1122
|
-
children: [],
|
|
1123
|
-
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
1124
|
-
},
|
|
1125
|
-
];
|
|
1126
|
-
|
|
1127
|
-
const stopsIndex = new StopsIndex(stops);
|
|
1128
|
-
router = new Router(timetable, stopsIndex);
|
|
1129
|
-
});
|
|
1130
|
-
|
|
1131
|
-
it('should not consider transfer with less time than minTransferTime', () => {
|
|
1132
|
-
const query = new Query.Builder()
|
|
1133
|
-
.from(0)
|
|
1134
|
-
.to(2)
|
|
1135
|
-
.departureTime(timeFromHM(8, 0))
|
|
1136
|
-
.minTransferTime(durationFromSeconds(300)) // 5 minutes required for transfer
|
|
1137
|
-
.build();
|
|
1138
|
-
|
|
1139
|
-
const result: Result = router.route(query);
|
|
1140
|
-
const bestRoute = result.bestRoute();
|
|
1141
|
-
|
|
1142
|
-
// Should find a route with 2 legs (same-stop transfer, no walking):
|
|
1143
|
-
// 1. Vehicle leg on route 0 (stop1 -> stop2)
|
|
1144
|
-
// 2. Vehicle leg on route 1 (stop2 -> stop3)
|
|
1145
|
-
assert.strictEqual(bestRoute?.legs.length, 2);
|
|
1146
|
-
|
|
1147
|
-
// Arrival at stop2 is 08:20, with 5 min minTransferTime we need departure >= 08:25
|
|
1148
|
-
// Trip 0 of route 1 departs at 08:21 - NOT catchable (only 1 minute transfer time)
|
|
1149
|
-
// Trip 1 of route 1 departs at 08:26 - catchable (6 minutes transfer time)
|
|
1150
|
-
// So we should arrive at stop3 at 08:45 (trip 1 arrival), not 08:35 (trip 0 arrival)
|
|
1151
|
-
assert.strictEqual(result.arrivalAt(2)?.arrival, timeFromHM(8, 45));
|
|
1152
|
-
});
|
|
1153
|
-
});
|
|
1154
|
-
|
|
1155
|
-
describe('with maxTransfers constraint', () => {
|
|
1156
|
-
let router: Router;
|
|
1157
|
-
let timetable: Timetable;
|
|
1158
|
-
|
|
1159
|
-
beforeEach(() => {
|
|
1160
|
-
// Setup: Three routes where reaching stop4 requires 2 transfers
|
|
1161
|
-
// Route 0: stop1 -> stop2
|
|
1162
|
-
// Route 1: stop2 -> stop3
|
|
1163
|
-
// Route 2: stop3 -> stop4
|
|
1164
|
-
// With maxTransfers=1, stop4 should not be reachable
|
|
1165
|
-
// With maxTransfers=2, stop4 should be reachable
|
|
1166
|
-
const stopsAdjacency: StopAdjacency[] = [
|
|
1167
|
-
{ routes: [0] }, // stop 0 (stop1)
|
|
1168
|
-
{ routes: [0, 1] }, // stop 1 (stop2)
|
|
1169
|
-
{ routes: [1, 2] }, // stop 2 (stop3)
|
|
1170
|
-
{ routes: [2] }, // stop 3 (stop4)
|
|
1171
|
-
];
|
|
1172
|
-
|
|
1173
|
-
const routesAdjacency = [
|
|
1174
|
-
// Route 0: stops 0 -> 1
|
|
1175
|
-
Route.of({
|
|
1176
|
-
id: 0,
|
|
1177
|
-
serviceRouteId: 0,
|
|
1178
|
-
trips: [
|
|
1179
|
-
{
|
|
1180
|
-
stops: [
|
|
1181
|
-
{
|
|
1182
|
-
id: 0,
|
|
1183
|
-
arrivalTime: timeFromHM(8, 0),
|
|
1184
|
-
departureTime: timeFromHM(8, 10),
|
|
1185
|
-
},
|
|
1186
|
-
{
|
|
1187
|
-
id: 1,
|
|
1188
|
-
arrivalTime: timeFromHM(8, 20),
|
|
1189
|
-
departureTime: timeFromHM(8, 30),
|
|
1190
|
-
},
|
|
1191
|
-
],
|
|
1192
|
-
},
|
|
1193
|
-
],
|
|
1194
|
-
}),
|
|
1195
|
-
// Route 1: stops 1 -> 2
|
|
1196
|
-
Route.of({
|
|
1197
|
-
id: 1,
|
|
1198
|
-
serviceRouteId: 1,
|
|
1199
|
-
trips: [
|
|
1200
|
-
{
|
|
1201
|
-
stops: [
|
|
1202
|
-
{
|
|
1203
|
-
id: 1,
|
|
1204
|
-
arrivalTime: timeFromHM(8, 25),
|
|
1205
|
-
departureTime: timeFromHM(8, 35),
|
|
1206
|
-
},
|
|
1207
|
-
{
|
|
1208
|
-
id: 2,
|
|
1209
|
-
arrivalTime: timeFromHM(8, 45),
|
|
1210
|
-
departureTime: timeFromHM(8, 55),
|
|
1211
|
-
},
|
|
1212
|
-
],
|
|
1213
|
-
},
|
|
1214
|
-
],
|
|
1215
|
-
}),
|
|
1216
|
-
// Route 2: stops 2 -> 3
|
|
1217
|
-
Route.of({
|
|
1218
|
-
id: 2,
|
|
1219
|
-
serviceRouteId: 2,
|
|
1220
|
-
trips: [
|
|
1221
|
-
{
|
|
1222
|
-
stops: [
|
|
1223
|
-
{
|
|
1224
|
-
id: 2,
|
|
1225
|
-
arrivalTime: timeFromHM(8, 50),
|
|
1226
|
-
departureTime: timeFromHM(9, 0),
|
|
1227
|
-
},
|
|
1228
|
-
{
|
|
1229
|
-
id: 3,
|
|
1230
|
-
arrivalTime: timeFromHM(9, 10),
|
|
1231
|
-
departureTime: timeFromHM(9, 20),
|
|
1232
|
-
},
|
|
1233
|
-
],
|
|
1234
|
-
},
|
|
1235
|
-
],
|
|
1236
|
-
}),
|
|
1237
|
-
];
|
|
1238
|
-
|
|
1239
|
-
const routes: ServiceRoute[] = [
|
|
1240
|
-
{ type: 'BUS', name: 'Line 1', routes: [0] },
|
|
1241
|
-
{ type: 'BUS', name: 'Line 2', routes: [1] },
|
|
1242
|
-
{ type: 'BUS', name: 'Line 3', routes: [2] },
|
|
1243
|
-
];
|
|
1244
|
-
|
|
1245
|
-
timetable = new Timetable(stopsAdjacency, routesAdjacency, routes);
|
|
1246
|
-
|
|
1247
|
-
const stops: Stop[] = [
|
|
1248
|
-
{
|
|
1249
|
-
id: 0,
|
|
1250
|
-
sourceStopId: 'stop1',
|
|
1251
|
-
name: 'Stop 1',
|
|
1252
|
-
lat: 1.0,
|
|
1253
|
-
lon: 1.0,
|
|
1254
|
-
children: [],
|
|
1255
|
-
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
1256
|
-
},
|
|
1257
|
-
{
|
|
1258
|
-
id: 1,
|
|
1259
|
-
sourceStopId: 'stop2',
|
|
1260
|
-
name: 'Stop 2',
|
|
1261
|
-
lat: 2.0,
|
|
1262
|
-
lon: 2.0,
|
|
1263
|
-
children: [],
|
|
1264
|
-
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
1265
|
-
},
|
|
1266
|
-
{
|
|
1267
|
-
id: 2,
|
|
1268
|
-
sourceStopId: 'stop3',
|
|
1269
|
-
name: 'Stop 3',
|
|
1270
|
-
lat: 3.0,
|
|
1271
|
-
lon: 3.0,
|
|
1272
|
-
children: [],
|
|
1273
|
-
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
1274
|
-
},
|
|
1275
|
-
{
|
|
1276
|
-
id: 3,
|
|
1277
|
-
sourceStopId: 'stop4',
|
|
1278
|
-
name: 'Stop 4',
|
|
1279
|
-
lat: 4.0,
|
|
1280
|
-
lon: 4.0,
|
|
1281
|
-
children: [],
|
|
1282
|
-
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
1283
|
-
},
|
|
1284
|
-
];
|
|
1285
|
-
|
|
1286
|
-
const stopsIndex = new StopsIndex(stops);
|
|
1287
|
-
router = new Router(timetable, stopsIndex);
|
|
1288
|
-
});
|
|
1289
|
-
|
|
1290
|
-
it('should not find route when maxTransfers is too low', () => {
|
|
1291
|
-
const query = new Query.Builder()
|
|
1292
|
-
.from(0)
|
|
1293
|
-
.to(3)
|
|
1294
|
-
.departureTime(timeFromHM(8, 0))
|
|
1295
|
-
.maxTransfers(1) // Only allows 1 transfer, but we need 2
|
|
1296
|
-
.build();
|
|
1297
|
-
|
|
1298
|
-
const result: Result = router.route(query);
|
|
1299
|
-
const bestRoute = result.bestRoute();
|
|
1300
|
-
|
|
1301
|
-
// Should not find a route because reaching stop4 requires 2 transfers
|
|
1302
|
-
assert.strictEqual(bestRoute, undefined);
|
|
1303
|
-
});
|
|
1304
|
-
|
|
1305
|
-
it('should find route when maxTransfers is sufficient', () => {
|
|
1306
|
-
const query = new Query.Builder()
|
|
1307
|
-
.from(0)
|
|
1308
|
-
.to(3)
|
|
1309
|
-
.departureTime(timeFromHM(8, 0))
|
|
1310
|
-
.maxTransfers(2) // Allows 2 transfers, which is exactly what we need
|
|
1311
|
-
.build();
|
|
1312
|
-
|
|
1313
|
-
const result: Result = router.route(query);
|
|
1314
|
-
const bestRoute = result.bestRoute();
|
|
1315
|
-
|
|
1316
|
-
// Should find a route with 3 legs (2 transfers)
|
|
1317
|
-
assert.strictEqual(bestRoute?.legs.length, 3);
|
|
1318
|
-
});
|
|
1319
|
-
|
|
1320
|
-
it('should find intermediate stops even with low maxTransfers', () => {
|
|
1321
|
-
const query = new Query.Builder()
|
|
1322
|
-
.from(0)
|
|
1323
|
-
.to(3)
|
|
1324
|
-
.departureTime(timeFromHM(8, 0))
|
|
1325
|
-
.maxTransfers(1)
|
|
1326
|
-
.build();
|
|
1327
|
-
|
|
1328
|
-
const result: Result = router.route(query);
|
|
1329
|
-
|
|
1330
|
-
// stop3 should still be reachable with 1 transfer (stop1 -> stop2 -> stop3)
|
|
1331
|
-
const arrivalAtStop3 = result.arrivalAt(2);
|
|
1332
|
-
assert.ok(arrivalAtStop3);
|
|
1333
|
-
assert.strictEqual(arrivalAtStop3.arrival, timeFromHM(8, 45));
|
|
1334
|
-
});
|
|
1335
|
-
});
|
|
1336
|
-
|
|
1337
|
-
describe('with transport mode filtering', () => {
|
|
1338
|
-
let router: Router;
|
|
1339
|
-
let timetable: Timetable;
|
|
1340
|
-
|
|
1341
|
-
beforeEach(() => {
|
|
1342
|
-
// Setup: Two routes to the same destination with different transport modes
|
|
1343
|
-
// Route 0 (BUS): stop1 -> stop2, arrives 08:30
|
|
1344
|
-
// Route 1 (RAIL): stop1 -> stop2, arrives 08:20 (faster)
|
|
1345
|
-
// When filtering to BUS only, should use the slower bus route
|
|
1346
|
-
const stopsAdjacency: StopAdjacency[] = [
|
|
1347
|
-
{ routes: [0, 1] }, // stop 0 (stop1) - served by both routes
|
|
1348
|
-
{ routes: [0, 1] }, // stop 1 (stop2) - served by both routes
|
|
1349
|
-
];
|
|
1350
|
-
|
|
1351
|
-
const routesAdjacency = [
|
|
1352
|
-
// Route 0 (BUS): stops 0 -> 1, slower
|
|
1353
|
-
Route.of({
|
|
1354
|
-
id: 0,
|
|
1355
|
-
serviceRouteId: 0,
|
|
1356
|
-
trips: [
|
|
1357
|
-
{
|
|
1358
|
-
stops: [
|
|
1359
|
-
{
|
|
1360
|
-
id: 0,
|
|
1361
|
-
arrivalTime: timeFromHM(8, 0),
|
|
1362
|
-
departureTime: timeFromHM(8, 10),
|
|
1363
|
-
},
|
|
1364
|
-
{
|
|
1365
|
-
id: 1,
|
|
1366
|
-
arrivalTime: timeFromHM(8, 30),
|
|
1367
|
-
departureTime: timeFromHM(8, 40),
|
|
1368
|
-
},
|
|
1369
|
-
],
|
|
1370
|
-
},
|
|
1371
|
-
],
|
|
1372
|
-
}),
|
|
1373
|
-
// Route 1 (RAIL): stops 0 -> 1, faster
|
|
1374
|
-
Route.of({
|
|
1375
|
-
id: 1,
|
|
1376
|
-
serviceRouteId: 1,
|
|
1377
|
-
trips: [
|
|
1378
|
-
{
|
|
1379
|
-
stops: [
|
|
1380
|
-
{
|
|
1381
|
-
id: 0,
|
|
1382
|
-
arrivalTime: timeFromHM(8, 0),
|
|
1383
|
-
departureTime: timeFromHM(8, 10),
|
|
1384
|
-
},
|
|
1385
|
-
{
|
|
1386
|
-
id: 1,
|
|
1387
|
-
arrivalTime: timeFromHM(8, 20),
|
|
1388
|
-
departureTime: timeFromHM(8, 30),
|
|
1389
|
-
},
|
|
1390
|
-
],
|
|
1391
|
-
},
|
|
1392
|
-
],
|
|
1393
|
-
}),
|
|
1394
|
-
];
|
|
1395
|
-
|
|
1396
|
-
const routes: ServiceRoute[] = [
|
|
1397
|
-
{ type: 'BUS', name: 'Bus Line', routes: [0] },
|
|
1398
|
-
{ type: 'RAIL', name: 'Rail Line', routes: [1] },
|
|
1399
|
-
];
|
|
1400
|
-
|
|
1401
|
-
timetable = new Timetable(stopsAdjacency, routesAdjacency, routes);
|
|
1402
|
-
|
|
1403
|
-
const stops: Stop[] = [
|
|
1404
|
-
{
|
|
1405
|
-
id: 0,
|
|
1406
|
-
sourceStopId: 'stop1',
|
|
1407
|
-
name: 'Stop 1',
|
|
1408
|
-
lat: 1.0,
|
|
1409
|
-
lon: 1.0,
|
|
1410
|
-
children: [],
|
|
1411
|
-
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
1412
|
-
},
|
|
1413
|
-
{
|
|
1414
|
-
id: 1,
|
|
1415
|
-
sourceStopId: 'stop2',
|
|
1416
|
-
name: 'Stop 2',
|
|
1417
|
-
lat: 2.0,
|
|
1418
|
-
lon: 2.0,
|
|
1419
|
-
children: [],
|
|
1420
|
-
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
1421
|
-
},
|
|
1422
|
-
];
|
|
1423
|
-
|
|
1424
|
-
const stopsIndex = new StopsIndex(stops);
|
|
1425
|
-
router = new Router(timetable, stopsIndex);
|
|
1426
|
-
});
|
|
1427
|
-
|
|
1428
|
-
it('should use fastest route when all modes allowed', () => {
|
|
1429
|
-
const query = new Query.Builder()
|
|
1430
|
-
.from(0)
|
|
1431
|
-
.to(1)
|
|
1432
|
-
.departureTime(timeFromHM(8, 0))
|
|
1433
|
-
.build();
|
|
1434
|
-
|
|
1435
|
-
const result: Result = router.route(query);
|
|
1436
|
-
|
|
1437
|
-
// Should use the faster RAIL route, arriving at 08:20
|
|
1438
|
-
assert.strictEqual(result.arrivalAt(1)?.arrival, timeFromHM(8, 20));
|
|
1439
|
-
});
|
|
1440
|
-
|
|
1441
|
-
it('should only use allowed transport modes', () => {
|
|
1442
|
-
const query = new Query.Builder()
|
|
1443
|
-
.from(0)
|
|
1444
|
-
.to(1)
|
|
1445
|
-
.departureTime(timeFromHM(8, 0))
|
|
1446
|
-
.transportModes(new Set(['BUS'] as const))
|
|
1447
|
-
.build();
|
|
1448
|
-
|
|
1449
|
-
const result: Result = router.route(query);
|
|
1450
|
-
|
|
1451
|
-
// Should use the slower BUS route since RAIL is excluded, arriving at 08:30
|
|
1452
|
-
assert.strictEqual(result.arrivalAt(1)?.arrival, timeFromHM(8, 30));
|
|
1453
|
-
});
|
|
1454
|
-
|
|
1455
|
-
it('should return no route when no matching transport mode', () => {
|
|
1456
|
-
const query = new Query.Builder()
|
|
1457
|
-
.from(0)
|
|
1458
|
-
.to(1)
|
|
1459
|
-
.departureTime(timeFromHM(8, 0))
|
|
1460
|
-
.transportModes(new Set(['FERRY'] as const)) // Neither route is a ferry
|
|
1461
|
-
.build();
|
|
1462
|
-
|
|
1463
|
-
const result: Result = router.route(query);
|
|
1464
|
-
const bestRoute = result.bestRoute();
|
|
1465
|
-
|
|
1466
|
-
// No route should be found since neither BUS nor RAIL is allowed
|
|
1467
|
-
assert.strictEqual(bestRoute, undefined);
|
|
1468
|
-
});
|
|
69
|
+
describe('Router', () => {
|
|
70
|
+
it('route() returns a Result with the correct earliest arrival', () => {
|
|
71
|
+
const query = new Query.Builder()
|
|
72
|
+
.from(0)
|
|
73
|
+
.to(1)
|
|
74
|
+
.departureTime(timeFromHM(8, 0))
|
|
75
|
+
.build();
|
|
76
|
+
const result = router.route(query);
|
|
77
|
+
assert(result instanceof Result);
|
|
78
|
+
assert.strictEqual(result.arrivalAt(1)?.arrival, timeFromHM(8, 30));
|
|
1469
79
|
});
|
|
1470
80
|
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
];
|
|
1483
|
-
|
|
1484
|
-
const routesAdjacency = [
|
|
1485
|
-
Route.of({
|
|
1486
|
-
id: 0,
|
|
1487
|
-
serviceRouteId: 0,
|
|
1488
|
-
trips: [
|
|
1489
|
-
{
|
|
1490
|
-
stops: [
|
|
1491
|
-
{
|
|
1492
|
-
id: 0,
|
|
1493
|
-
arrivalTime: timeFromHM(8, 0),
|
|
1494
|
-
departureTime: timeFromHM(8, 10),
|
|
1495
|
-
},
|
|
1496
|
-
{
|
|
1497
|
-
id: 1,
|
|
1498
|
-
arrivalTime: timeFromHM(8, 30),
|
|
1499
|
-
departureTime: timeFromHM(8, 40),
|
|
1500
|
-
},
|
|
1501
|
-
],
|
|
1502
|
-
},
|
|
1503
|
-
{
|
|
1504
|
-
stops: [
|
|
1505
|
-
{
|
|
1506
|
-
id: 0,
|
|
1507
|
-
arrivalTime: timeFromHM(9, 0),
|
|
1508
|
-
departureTime: timeFromHM(9, 10),
|
|
1509
|
-
},
|
|
1510
|
-
{
|
|
1511
|
-
id: 1,
|
|
1512
|
-
arrivalTime: timeFromHM(9, 30),
|
|
1513
|
-
departureTime: timeFromHM(9, 40),
|
|
1514
|
-
},
|
|
1515
|
-
],
|
|
1516
|
-
},
|
|
1517
|
-
],
|
|
1518
|
-
}),
|
|
1519
|
-
];
|
|
1520
|
-
|
|
1521
|
-
const routes: ServiceRoute[] = [
|
|
1522
|
-
{ type: 'BUS', name: 'Line 1', routes: [0] },
|
|
1523
|
-
];
|
|
1524
|
-
|
|
1525
|
-
timetable = new Timetable(stopsAdjacency, routesAdjacency, routes);
|
|
1526
|
-
|
|
1527
|
-
const stops: Stop[] = [
|
|
1528
|
-
{
|
|
1529
|
-
id: 0,
|
|
1530
|
-
sourceStopId: 'stop1',
|
|
1531
|
-
name: 'Stop 1',
|
|
1532
|
-
lat: 1.0,
|
|
1533
|
-
lon: 1.0,
|
|
1534
|
-
children: [],
|
|
1535
|
-
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
1536
|
-
},
|
|
1537
|
-
{
|
|
1538
|
-
id: 1,
|
|
1539
|
-
sourceStopId: 'stop2',
|
|
1540
|
-
name: 'Stop 2',
|
|
1541
|
-
lat: 2.0,
|
|
1542
|
-
lon: 2.0,
|
|
1543
|
-
children: [],
|
|
1544
|
-
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
1545
|
-
},
|
|
1546
|
-
];
|
|
1547
|
-
|
|
1548
|
-
const stopsIndex = new StopsIndex(stops);
|
|
1549
|
-
router = new Router(timetable, stopsIndex);
|
|
1550
|
-
});
|
|
1551
|
-
|
|
1552
|
-
it('should find first available trip after departure time', () => {
|
|
1553
|
-
const query = new Query.Builder()
|
|
1554
|
-
.from(0)
|
|
1555
|
-
.to(1)
|
|
1556
|
-
.departureTime(timeFromHM(8, 0))
|
|
1557
|
-
.build();
|
|
1558
|
-
|
|
1559
|
-
const result: Result = router.route(query);
|
|
1560
|
-
|
|
1561
|
-
// Should catch the first trip (08:10), arriving at 08:30
|
|
1562
|
-
assert.strictEqual(result.arrivalAt(1)?.arrival, timeFromHM(8, 30));
|
|
1563
|
-
});
|
|
1564
|
-
|
|
1565
|
-
it('should skip trips that have already departed', () => {
|
|
1566
|
-
const query = new Query.Builder()
|
|
1567
|
-
.from(0)
|
|
1568
|
-
.to(1)
|
|
1569
|
-
.departureTime(timeFromHM(8, 15)) // After first trip departs
|
|
1570
|
-
.build();
|
|
1571
|
-
|
|
1572
|
-
const result: Result = router.route(query);
|
|
1573
|
-
|
|
1574
|
-
// Should catch the second trip (09:10), arriving at 09:30
|
|
1575
|
-
assert.strictEqual(result.arrivalAt(1)?.arrival, timeFromHM(9, 30));
|
|
1576
|
-
});
|
|
1577
|
-
|
|
1578
|
-
it('should return no route when departing after all trips', () => {
|
|
1579
|
-
const query = new Query.Builder()
|
|
1580
|
-
.from(0)
|
|
1581
|
-
.to(1)
|
|
1582
|
-
.departureTime(timeFromHM(10, 0)) // After all trips have departed
|
|
1583
|
-
.build();
|
|
1584
|
-
|
|
1585
|
-
const result: Result = router.route(query);
|
|
1586
|
-
const bestRoute = result.bestRoute();
|
|
1587
|
-
|
|
1588
|
-
// No route should be found since all trips have departed
|
|
1589
|
-
assert.strictEqual(bestRoute, undefined);
|
|
1590
|
-
});
|
|
1591
|
-
|
|
1592
|
-
it('should catch trip when departure time exactly matches', () => {
|
|
1593
|
-
const query = new Query.Builder()
|
|
1594
|
-
.from(0)
|
|
1595
|
-
.to(1)
|
|
1596
|
-
.departureTime(timeFromHM(8, 10)) // Exactly when first trip departs
|
|
1597
|
-
.build();
|
|
1598
|
-
|
|
1599
|
-
const result: Result = router.route(query);
|
|
1600
|
-
|
|
1601
|
-
// Should still catch the first trip
|
|
1602
|
-
assert.strictEqual(result.arrivalAt(1)?.arrival, timeFromHM(8, 30));
|
|
1603
|
-
});
|
|
81
|
+
it('rangeRoute() returns a RangeResult covering all Pareto-optimal departures in the window', () => {
|
|
82
|
+
const query = new RangeQuery.Builder()
|
|
83
|
+
.from(0)
|
|
84
|
+
.to(1)
|
|
85
|
+
.departureTime(timeFromHM(8, 0))
|
|
86
|
+
.lastDepartureTime(timeFromHM(9, 0))
|
|
87
|
+
.build();
|
|
88
|
+
const result = router.rangeRoute(query);
|
|
89
|
+
assert(result instanceof RangeResult);
|
|
90
|
+
assert.strictEqual(result.size, 1);
|
|
91
|
+
assert.strictEqual(result.bestRoute()?.arrivalTime(), timeFromHM(8, 30));
|
|
1604
92
|
});
|
|
1605
93
|
});
|