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.
Files changed (71) hide show
  1. package/.cspell.json +7 -1
  2. package/CHANGELOG.md +3 -3
  3. package/README.md +111 -86
  4. package/dist/cli/perf.d.ts +57 -18
  5. package/dist/cli.mjs +1371 -342
  6. package/dist/cli.mjs.map +1 -1
  7. package/dist/parser.cjs.js +57 -4
  8. package/dist/parser.cjs.js.map +1 -1
  9. package/dist/parser.esm.js +57 -4
  10. package/dist/parser.esm.js.map +1 -1
  11. package/dist/router.cjs.js +1 -1
  12. package/dist/router.cjs.js.map +1 -1
  13. package/dist/router.d.ts +5 -5
  14. package/dist/router.esm.js +1 -1
  15. package/dist/router.esm.js.map +1 -1
  16. package/dist/router.umd.js +1 -1
  17. package/dist/router.umd.js.map +1 -1
  18. package/dist/routing/__tests__/access.test.d.ts +1 -0
  19. package/dist/routing/__tests__/plainRouter.test.d.ts +1 -0
  20. package/dist/routing/__tests__/rangeResult.test.d.ts +1 -0
  21. package/dist/routing/__tests__/rangeRouter.test.d.ts +1 -0
  22. package/dist/routing/__tests__/rangeState.test.d.ts +1 -0
  23. package/dist/routing/__tests__/raptor.test.d.ts +1 -0
  24. package/dist/routing/__tests__/state.test.d.ts +1 -0
  25. package/dist/routing/access.d.ts +55 -0
  26. package/dist/routing/plainRouter.d.ts +21 -0
  27. package/dist/routing/plotter.d.ts +9 -0
  28. package/dist/routing/query.d.ts +132 -13
  29. package/dist/routing/rangeResult.d.ts +155 -0
  30. package/dist/routing/rangeRouter.d.ts +24 -0
  31. package/dist/routing/rangeState.d.ts +83 -0
  32. package/dist/routing/raptor.d.ts +96 -0
  33. package/dist/routing/result.d.ts +27 -7
  34. package/dist/routing/route.d.ts +5 -21
  35. package/dist/routing/router.d.ts +20 -91
  36. package/dist/routing/state.d.ts +92 -17
  37. package/dist/timetable/route.d.ts +8 -0
  38. package/dist/timetable/timetable.d.ts +17 -1
  39. package/package.json +1 -1
  40. package/src/__e2e__/benchmark.json +18 -0
  41. package/src/__e2e__/router.test.ts +461 -127
  42. package/src/cli/minotor.ts +39 -3
  43. package/src/cli/perf.ts +324 -60
  44. package/src/cli/repl.ts +96 -41
  45. package/src/router.ts +11 -3
  46. package/src/routing/__tests__/access.test.ts +294 -0
  47. package/src/routing/__tests__/plainRouter.test.ts +1633 -0
  48. package/src/routing/__tests__/plotter.test.ts +8 -8
  49. package/src/routing/__tests__/rangeResult.test.ts +273 -0
  50. package/src/routing/__tests__/rangeRouter.test.ts +472 -0
  51. package/src/routing/__tests__/rangeState.test.ts +246 -0
  52. package/src/routing/__tests__/raptor.test.ts +366 -0
  53. package/src/routing/__tests__/result.test.ts +27 -27
  54. package/src/routing/__tests__/route.test.ts +28 -0
  55. package/src/routing/__tests__/router.test.ts +75 -1587
  56. package/src/routing/__tests__/state.test.ts +78 -0
  57. package/src/routing/access.ts +144 -0
  58. package/src/routing/plainRouter.ts +60 -0
  59. package/src/routing/plotter.ts +53 -6
  60. package/src/routing/query.ts +116 -13
  61. package/src/routing/rangeResult.ts +292 -0
  62. package/src/routing/rangeRouter.ts +167 -0
  63. package/src/routing/rangeState.ts +150 -0
  64. package/src/routing/raptor.ts +416 -0
  65. package/src/routing/result.ts +68 -26
  66. package/src/routing/route.ts +15 -53
  67. package/src/routing/router.ts +40 -480
  68. package/src/routing/state.ts +191 -32
  69. package/src/timetable/__tests__/timetable.test.ts +373 -0
  70. package/src/timetable/route.ts +16 -4
  71. package/src/timetable/timetable.ts +54 -1
@@ -1,1605 +1,93 @@
1
1
  import assert from 'node:assert';
2
- import { beforeEach, describe, it } from 'node:test';
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 { durationFromSeconds, timeFromHM } from '../../timetable/time.js';
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 { encode } from '../../timetable/tripStopId.js';
15
- import { Query } from '../query.js';
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
- describe('Router', () => {
20
- describe('with a single route', () => {
21
- let router: Router;
22
- let timetable: Timetable;
23
-
24
- beforeEach(() => {
25
- // Setup: A single route (Line 1) serving 3 stops in sequence
26
- // Route 0: stop1 (depart 08:10) -> stop2 (08:15-08:25) -> stop3 (arrive 08:35)
27
- const stopsAdjacency: StopAdjacency[] = [
28
- { routes: [0] }, // stop 0 (stop1)
29
- { routes: [0] }, // stop 1 (stop2)
30
- { routes: [0] }, // stop 2 (stop3)
31
- ];
32
-
33
- const routesAdjacency = [
34
- // Route 0: stops 0 -> 1 -> 2
35
- Route.of({
36
- id: 0,
37
- serviceRouteId: 0,
38
- trips: [
39
- {
40
- stops: [
41
- {
42
- id: 0,
43
- arrivalTime: timeFromHM(8, 0),
44
- departureTime: timeFromHM(8, 10),
45
- },
46
- {
47
- id: 1,
48
- arrivalTime: timeFromHM(8, 15),
49
- departureTime: timeFromHM(8, 25),
50
- },
51
- {
52
- id: 2,
53
- arrivalTime: timeFromHM(8, 35),
54
- departureTime: timeFromHM(8, 45),
55
- },
56
- ],
57
- },
58
- ],
59
- }),
60
- ];
61
-
62
- const routes: ServiceRoute[] = [
63
- {
64
- type: 'BUS',
65
- name: 'Line 1',
66
- routes: [0],
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
- const stops: Stop[] = [
942
- {
943
- id: 0,
944
- sourceStopId: 'stop1',
945
- name: 'Stop 1',
946
- lat: 1.0,
947
- lon: 1.0,
948
- children: [],
949
- locationType: 'SIMPLE_STOP_OR_PLATFORM',
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
- describe('with timing edge cases', () => {
1472
- let router: Router;
1473
- let timetable: Timetable;
1474
-
1475
- beforeEach(() => {
1476
- // Setup: A single route with multiple trips at different times
1477
- // Trip 0: departs 08:10, arrives 08:30
1478
- // Trip 1: departs 09:10, arrives 09:30
1479
- const stopsAdjacency: StopAdjacency[] = [
1480
- { routes: [0] },
1481
- { routes: [0] },
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
  });