minotor 7.0.2 → 9.0.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 (60) hide show
  1. package/.cspell.json +11 -1
  2. package/CHANGELOG.md +8 -3
  3. package/README.md +26 -24
  4. package/dist/cli.mjs +1786 -791
  5. package/dist/cli.mjs.map +1 -1
  6. package/dist/gtfs/transfers.d.ts +29 -5
  7. package/dist/gtfs/trips.d.ts +10 -5
  8. package/dist/parser.cjs.js +972 -525
  9. package/dist/parser.cjs.js.map +1 -1
  10. package/dist/parser.esm.js +972 -525
  11. package/dist/parser.esm.js.map +1 -1
  12. package/dist/router.cjs.js +1 -1
  13. package/dist/router.cjs.js.map +1 -1
  14. package/dist/router.d.ts +2 -2
  15. package/dist/router.esm.js +1 -1
  16. package/dist/router.esm.js.map +1 -1
  17. package/dist/router.umd.js +1 -1
  18. package/dist/router.umd.js.map +1 -1
  19. package/dist/routing/__tests__/plotter.test.d.ts +1 -0
  20. package/dist/routing/plotter.d.ts +42 -3
  21. package/dist/routing/result.d.ts +23 -7
  22. package/dist/routing/route.d.ts +2 -0
  23. package/dist/routing/router.d.ts +78 -19
  24. package/dist/timetable/__tests__/tripBoardingId.test.d.ts +1 -0
  25. package/dist/timetable/io.d.ts +4 -2
  26. package/dist/timetable/proto/timetable.d.ts +15 -1
  27. package/dist/timetable/route.d.ts +48 -23
  28. package/dist/timetable/timetable.d.ts +24 -7
  29. package/dist/timetable/tripBoardingId.d.ts +34 -0
  30. package/package.json +1 -1
  31. package/src/__e2e__/router.test.ts +114 -105
  32. package/src/__e2e__/timetable/stops.bin +2 -2
  33. package/src/__e2e__/timetable/timetable.bin +2 -2
  34. package/src/cli/repl.ts +245 -1
  35. package/src/gtfs/__tests__/parser.test.ts +19 -4
  36. package/src/gtfs/__tests__/transfers.test.ts +773 -37
  37. package/src/gtfs/__tests__/trips.test.ts +308 -27
  38. package/src/gtfs/parser.ts +36 -6
  39. package/src/gtfs/transfers.ts +193 -19
  40. package/src/gtfs/trips.ts +58 -21
  41. package/src/router.ts +2 -2
  42. package/src/routing/__tests__/plotter.test.ts +230 -0
  43. package/src/routing/__tests__/result.test.ts +486 -125
  44. package/src/routing/__tests__/route.test.ts +7 -3
  45. package/src/routing/__tests__/router.test.ts +380 -172
  46. package/src/routing/plotter.ts +279 -48
  47. package/src/routing/result.ts +114 -34
  48. package/src/routing/route.ts +0 -3
  49. package/src/routing/router.ts +344 -211
  50. package/src/timetable/__tests__/io.test.ts +34 -1
  51. package/src/timetable/__tests__/route.test.ts +74 -81
  52. package/src/timetable/__tests__/timetable.test.ts +232 -61
  53. package/src/timetable/__tests__/tripBoardingId.test.ts +57 -0
  54. package/src/timetable/io.ts +72 -10
  55. package/src/timetable/proto/timetable.proto +16 -2
  56. package/src/timetable/proto/timetable.ts +256 -22
  57. package/src/timetable/route.ts +174 -58
  58. package/src/timetable/timetable.ts +66 -16
  59. package/src/timetable/tripBoardingId.ts +94 -0
  60. package/tsconfig.json +2 -2
@@ -3,8 +3,17 @@ import { Readable } from 'node:stream';
3
3
  import { describe, it } from 'node:test';
4
4
 
5
5
  import { Duration } from '../../timetable/duration.js';
6
+ import { Route } from '../../timetable/route.js';
7
+ import { Time } from '../../timetable/time.js';
8
+ import { Timetable } from '../../timetable/timetable.js';
9
+ import { encode } from '../../timetable/tripBoardingId.js';
6
10
  import { GtfsStopsMap } from '../stops.js';
7
- import { parseTransfers } from '../transfers.js';
11
+ import {
12
+ buildTripContinuations,
13
+ GtfsTripContinuation,
14
+ parseTransfers,
15
+ } from '../transfers.js';
16
+ import { TripsMapping } from '../trips.js';
8
17
 
9
18
  describe('GTFS transfers parser', () => {
10
19
  it('should correctly parse valid transfers', async () => {
@@ -13,7 +22,7 @@ describe('GTFS transfers parser', () => {
13
22
  'from_stop_id,to_stop_id,transfer_type,min_transfer_time\n',
14
23
  );
15
24
  mockedStream.push('"1100084","8014440:0:1","2","180"\n');
16
- mockedStream.push('"1100097","8014447","2","240"\n');
25
+ mockedStream.push('"1100097","8014447","0","240"\n');
17
26
  mockedStream.push(null);
18
27
 
19
28
  const stopsMap: GtfsStopsMap = new Map([
@@ -59,7 +68,8 @@ describe('GTFS transfers parser', () => {
59
68
  ],
60
69
  ]);
61
70
 
62
- const transfers = await parseTransfers(mockedStream, stopsMap);
71
+ const result = await parseTransfers(mockedStream, stopsMap);
72
+
63
73
  const expectedTransfers = new Map([
64
74
  [
65
75
  0, // Internal ID for stop '1100084'
@@ -76,17 +86,18 @@ describe('GTFS transfers parser', () => {
76
86
  [
77
87
  {
78
88
  destination: 3, // Internal ID for stop '8014447'
79
- type: 'REQUIRES_MINIMAL_TIME',
89
+ type: 'RECOMMENDED',
80
90
  minTransferTime: Duration.fromSeconds(240),
81
91
  },
82
92
  ],
83
93
  ],
84
94
  ]);
85
95
 
86
- assert.deepEqual(transfers, expectedTransfers);
96
+ assert.deepEqual(result.transfers, expectedTransfers);
97
+ assert.deepEqual(result.tripContinuations, []);
87
98
  });
88
99
 
89
- it('should ignore impossible transfer types', async () => {
100
+ it('should ignore impossible transfer types (3 and 5)', async () => {
90
101
  const mockedStream = new Readable();
91
102
  mockedStream.push(
92
103
  'from_stop_id,to_stop_id,transfer_type,min_transfer_time\n',
@@ -138,16 +149,57 @@ describe('GTFS transfers parser', () => {
138
149
  ],
139
150
  ]);
140
151
 
141
- const transfers = await parseTransfers(mockedStream, stopsMap);
142
- assert.deepEqual(transfers, new Map());
152
+ const result = await parseTransfers(mockedStream, stopsMap);
153
+
154
+ assert.deepEqual(result.transfers, new Map());
155
+ assert.deepEqual(result.tripContinuations, []);
143
156
  });
144
157
 
145
- it('should ignore unsupported transfer types between routes', async () => {
158
+ it('should ignore transfers with missing stop IDs', async () => {
159
+ const mockedStream = new Readable();
160
+ mockedStream.push(
161
+ 'from_stop_id,to_stop_id,transfer_type,min_transfer_time\n',
162
+ );
163
+ mockedStream.push(',"8014440:0:1","2","180"\n');
164
+ mockedStream.push('"1100097",,"0","240"\n');
165
+ mockedStream.push(null);
166
+
167
+ const stopsMap: GtfsStopsMap = new Map([
168
+ [
169
+ '8014440:0:1',
170
+ {
171
+ id: 1,
172
+ sourceStopId: '8014440:0:1',
173
+ name: 'Test Stop 2',
174
+ children: [],
175
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
176
+ },
177
+ ],
178
+ [
179
+ '1100097',
180
+ {
181
+ id: 2,
182
+ sourceStopId: '1100097',
183
+ name: 'Test Stop 3',
184
+ children: [],
185
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
186
+ },
187
+ ],
188
+ ]);
189
+
190
+ const result = await parseTransfers(mockedStream, stopsMap);
191
+
192
+ assert.deepEqual(result.transfers, new Map());
193
+ assert.deepEqual(result.tripContinuations, []);
194
+ });
195
+
196
+ it('should correctly parse in-seat transfers (type 4)', async () => {
146
197
  const mockedStream = new Readable();
147
198
  mockedStream.push(
148
- 'from_route_id,to_route_id,transfer_type,min_transfer_time\n',
199
+ 'from_stop_id,to_stop_id,from_trip_id,to_trip_id,transfer_type,min_transfer_time\n',
149
200
  );
150
- mockedStream.push('"1100084","8014440","2","180"\n');
201
+ mockedStream.push('"1100084","8014440:0:1","trip1","trip2","4","0"\n');
202
+ mockedStream.push('"1100097","8014447","trip3","trip4","4","0"\n');
151
203
  mockedStream.push(null);
152
204
 
153
205
  const stopsMap: GtfsStopsMap = new Map([
@@ -162,27 +214,161 @@ describe('GTFS transfers parser', () => {
162
214
  },
163
215
  ],
164
216
  [
165
- '8014440',
217
+ '8014440:0:1',
166
218
  {
167
219
  id: 1,
168
- sourceStopId: '8014440',
220
+ sourceStopId: '8014440:0:1',
169
221
  name: 'Test Stop 2',
170
222
  children: [],
171
223
  locationType: 'SIMPLE_STOP_OR_PLATFORM',
172
224
  },
173
225
  ],
226
+ [
227
+ '1100097',
228
+ {
229
+ id: 2,
230
+ sourceStopId: '1100097',
231
+ name: 'Test Stop 3',
232
+ children: [],
233
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
234
+ },
235
+ ],
236
+ [
237
+ '8014447',
238
+ {
239
+ id: 3,
240
+ sourceStopId: '8014447',
241
+ name: 'Test Stop 4',
242
+ children: [],
243
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
244
+ },
245
+ ],
174
246
  ]);
175
247
 
176
- const transfers = await parseTransfers(mockedStream, stopsMap);
177
- assert.deepEqual(transfers, new Map());
248
+ const result = await parseTransfers(mockedStream, stopsMap);
249
+
250
+ const expectedTripContinuations = [
251
+ {
252
+ fromStop: 0,
253
+ fromTrip: 'trip1',
254
+ toStop: 1,
255
+ toTrip: 'trip2',
256
+ },
257
+ {
258
+ fromStop: 2,
259
+ fromTrip: 'trip3',
260
+ toStop: 3,
261
+ toTrip: 'trip4',
262
+ },
263
+ ];
264
+
265
+ assert.deepEqual(result.transfers, new Map());
266
+ assert.deepEqual(result.tripContinuations, expectedTripContinuations);
267
+ });
268
+
269
+ it('should ignore in-seat transfers with missing trip IDs', async () => {
270
+ const mockedStream = new Readable();
271
+ mockedStream.push(
272
+ 'from_stop_id,to_stop_id,from_trip_id,to_trip_id,transfer_type,min_transfer_time\n',
273
+ );
274
+ mockedStream.push('"1100084","8014440:0:1",,"trip2","4","0"\n');
275
+ mockedStream.push('"1100097","8014447","trip3",,"4","0"\n');
276
+ mockedStream.push('"1100098","8014448","","trip5","4","0"\n');
277
+ mockedStream.push('"1100099","8014449","trip6","","4","0"\n');
278
+ mockedStream.push(null);
279
+
280
+ const stopsMap: GtfsStopsMap = new Map([
281
+ [
282
+ '1100084',
283
+ {
284
+ id: 0,
285
+ sourceStopId: '1100084',
286
+ name: 'Test Stop 1',
287
+ children: [],
288
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
289
+ },
290
+ ],
291
+ [
292
+ '8014440:0:1',
293
+ {
294
+ id: 1,
295
+ sourceStopId: '8014440:0:1',
296
+ name: 'Test Stop 2',
297
+ children: [],
298
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
299
+ },
300
+ ],
301
+ [
302
+ '1100097',
303
+ {
304
+ id: 2,
305
+ sourceStopId: '1100097',
306
+ name: 'Test Stop 3',
307
+ children: [],
308
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
309
+ },
310
+ ],
311
+ [
312
+ '8014447',
313
+ {
314
+ id: 3,
315
+ sourceStopId: '8014447',
316
+ name: 'Test Stop 4',
317
+ children: [],
318
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
319
+ },
320
+ ],
321
+ ]);
322
+
323
+ const result = await parseTransfers(mockedStream, stopsMap);
324
+
325
+ assert.deepEqual(result.transfers, new Map());
326
+ assert.deepEqual(result.tripContinuations, []);
178
327
  });
179
328
 
180
329
  it('should ignore unsupported transfer types between trips', async () => {
181
330
  const mockedStream = new Readable();
182
331
  mockedStream.push(
183
- 'from_trip_id,to_trip_id,transfer_type,min_transfer_time\n',
332
+ 'from_stop_id,to_stop_id,from_trip_id,to_trip_id,transfer_type,min_transfer_time\n',
333
+ );
334
+ mockedStream.push('"1100084","8014440:0:1","trip1","trip2","1","0"\n');
335
+ mockedStream.push(null);
336
+
337
+ const stopsMap: GtfsStopsMap = new Map([
338
+ [
339
+ '1100084',
340
+ {
341
+ id: 0,
342
+ sourceStopId: '1100084',
343
+ name: 'Test Stop 1',
344
+ children: [],
345
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
346
+ },
347
+ ],
348
+ [
349
+ '8014440:0:1',
350
+ {
351
+ id: 1,
352
+ sourceStopId: '8014440:0:1',
353
+ name: 'Test Stop 2',
354
+ children: [],
355
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
356
+ },
357
+ ],
358
+ ]);
359
+
360
+ const result = await parseTransfers(mockedStream, stopsMap);
361
+
362
+ assert.deepEqual(result.transfers, new Map());
363
+ assert.deepEqual(result.tripContinuations, []);
364
+ });
365
+
366
+ it('should ignore unsupported transfer types between routes', async () => {
367
+ const mockedStream = new Readable();
368
+ mockedStream.push(
369
+ 'from_stop_id,to_stop_id,from_route_id,to_route_id,transfer_type,min_transfer_time\n',
184
370
  );
185
- mockedStream.push('"1100084","8014440","2","180"\n');
371
+ mockedStream.push('"1100084","8014440:0:1","route1","route2","1","0"\n');
186
372
  mockedStream.push(null);
187
373
 
188
374
  const stopsMap: GtfsStopsMap = new Map([
@@ -197,10 +383,10 @@ describe('GTFS transfers parser', () => {
197
383
  },
198
384
  ],
199
385
  [
200
- '8014440',
386
+ '8014440:0:1',
201
387
  {
202
388
  id: 1,
203
- sourceStopId: '8014440',
389
+ sourceStopId: '8014440:0:1',
204
390
  name: 'Test Stop 2',
205
391
  children: [],
206
392
  locationType: 'SIMPLE_STOP_OR_PLATFORM',
@@ -208,16 +394,19 @@ describe('GTFS transfers parser', () => {
208
394
  ],
209
395
  ]);
210
396
 
211
- const transfers = await parseTransfers(mockedStream, stopsMap);
212
- assert.deepEqual(transfers, new Map());
397
+ const result = await parseTransfers(mockedStream, stopsMap);
398
+
399
+ assert.deepEqual(result.transfers, new Map());
400
+ assert.deepEqual(result.tripContinuations, []);
213
401
  });
214
402
 
215
- it('should allow missing minimum transfer time', async () => {
403
+ it('should handle transfers without minimum transfer time', async () => {
216
404
  const mockedStream = new Readable();
217
405
  mockedStream.push(
218
406
  'from_stop_id,to_stop_id,transfer_type,min_transfer_time\n',
219
407
  );
220
408
  mockedStream.push('"1100084","8014440:0:1","2"\n');
409
+ mockedStream.push('"1100097","8014447","1","0"\n');
221
410
  mockedStream.push(null);
222
411
 
223
412
  const stopsMap: GtfsStopsMap = new Map([
@@ -241,25 +430,137 @@ describe('GTFS transfers parser', () => {
241
430
  locationType: 'SIMPLE_STOP_OR_PLATFORM',
242
431
  },
243
432
  ],
433
+ [
434
+ '1100097',
435
+ {
436
+ id: 2,
437
+ sourceStopId: '1100097',
438
+ name: 'Test Stop 3',
439
+ children: [],
440
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
441
+ },
442
+ ],
443
+ [
444
+ '8014447',
445
+ {
446
+ id: 3,
447
+ sourceStopId: '8014447',
448
+ name: 'Test Stop 4',
449
+ children: [],
450
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
451
+ },
452
+ ],
244
453
  ]);
245
- const transfers = await parseTransfers(mockedStream, stopsMap);
246
- assert.deepEqual(
247
- transfers,
248
- new Map([
454
+
455
+ const result = await parseTransfers(mockedStream, stopsMap);
456
+
457
+ const expectedTransfers = new Map([
458
+ [
459
+ 0,
249
460
  [
250
- 0, // Internal ID for stop '1100084'
251
- [
252
- {
253
- destination: 1, // Internal ID for stop '8014440:0:1'
254
- type: 'REQUIRES_MINIMAL_TIME',
255
- },
256
- ],
461
+ {
462
+ destination: 1,
463
+ type: 'REQUIRES_MINIMAL_TIME',
464
+ },
257
465
  ],
258
- ]),
466
+ ],
467
+ [
468
+ 2,
469
+ [
470
+ {
471
+ destination: 3,
472
+ type: 'GUARANTEED',
473
+ minTransferTime: Duration.fromSeconds(0),
474
+ },
475
+ ],
476
+ ],
477
+ ]);
478
+
479
+ assert.deepEqual(result.transfers, expectedTransfers);
480
+ assert.deepEqual(result.tripContinuations, []);
481
+ });
482
+
483
+ it('should handle mixed transfers and trip continuations', async () => {
484
+ const mockedStream = new Readable();
485
+ mockedStream.push(
486
+ 'from_stop_id,to_stop_id,from_trip_id,to_trip_id,transfer_type,min_transfer_time\n',
259
487
  );
488
+ mockedStream.push('"1100084","8014440:0:1","","","1","120"\n');
489
+ mockedStream.push('"1100097","8014447","trip1","trip2","4","0"\n');
490
+ mockedStream.push(null);
491
+
492
+ const stopsMap: GtfsStopsMap = new Map([
493
+ [
494
+ '1100084',
495
+ {
496
+ id: 0,
497
+ sourceStopId: '1100084',
498
+ name: 'Test Stop 1',
499
+ children: [],
500
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
501
+ },
502
+ ],
503
+ [
504
+ '8014440:0:1',
505
+ {
506
+ id: 1,
507
+ sourceStopId: '8014440:0:1',
508
+ name: 'Test Stop 2',
509
+ children: [],
510
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
511
+ },
512
+ ],
513
+ [
514
+ '1100097',
515
+ {
516
+ id: 2,
517
+ sourceStopId: '1100097',
518
+ name: 'Test Stop 3',
519
+ children: [],
520
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
521
+ },
522
+ ],
523
+ [
524
+ '8014447',
525
+ {
526
+ id: 3,
527
+ sourceStopId: '8014447',
528
+ name: 'Test Stop 4',
529
+ children: [],
530
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
531
+ },
532
+ ],
533
+ ]);
534
+
535
+ const result = await parseTransfers(mockedStream, stopsMap);
536
+
537
+ const expectedTransfers = new Map([
538
+ [
539
+ 0,
540
+ [
541
+ {
542
+ destination: 1,
543
+ type: 'GUARANTEED',
544
+ minTransferTime: Duration.fromSeconds(120),
545
+ },
546
+ ],
547
+ ],
548
+ ]);
549
+
550
+ const expectedTripContinuations = [
551
+ {
552
+ fromStop: 2,
553
+ fromTrip: 'trip1',
554
+ toStop: 3,
555
+ toTrip: 'trip2',
556
+ },
557
+ ];
558
+
559
+ assert.deepEqual(result.transfers, expectedTransfers);
560
+ assert.deepEqual(result.tripContinuations, expectedTripContinuations);
260
561
  });
261
562
 
262
- it('should handle empty transfers', async () => {
563
+ it('should handle empty transfers file', async () => {
263
564
  const mockedStream = new Readable();
264
565
  mockedStream.push(
265
566
  'from_stop_id,to_stop_id,transfer_type,min_transfer_time\n',
@@ -268,7 +569,442 @@ describe('GTFS transfers parser', () => {
268
569
 
269
570
  const stopsMap: GtfsStopsMap = new Map();
270
571
 
271
- const transfers = await parseTransfers(mockedStream, stopsMap);
272
- assert.deepEqual(transfers, new Map());
572
+ const result = await parseTransfers(mockedStream, stopsMap);
573
+
574
+ assert.deepEqual(result.transfers, new Map());
575
+ assert.deepEqual(result.tripContinuations, []);
576
+ });
577
+
578
+ it('should ignore transfers with non-existent stops', async () => {
579
+ const mockedStream = new Readable();
580
+ mockedStream.push(
581
+ 'from_stop_id,to_stop_id,transfer_type,min_transfer_time\n',
582
+ );
583
+ mockedStream.push('"unknown_stop","8014440:0:1","0","120"\n');
584
+ mockedStream.push('"1100084","unknown_stop","1","60"\n');
585
+ mockedStream.push('"1100084","8014440:0:1","2","180"\n');
586
+ mockedStream.push(null);
587
+
588
+ const stopsMap: GtfsStopsMap = new Map([
589
+ [
590
+ '1100084',
591
+ {
592
+ id: 0,
593
+ sourceStopId: '1100084',
594
+ name: 'Test Stop 1',
595
+ children: [],
596
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
597
+ },
598
+ ],
599
+ [
600
+ '8014440:0:1',
601
+ {
602
+ id: 1,
603
+ sourceStopId: '8014440:0:1',
604
+ name: 'Test Stop 2',
605
+ children: [],
606
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
607
+ },
608
+ ],
609
+ ]);
610
+
611
+ const result = await parseTransfers(mockedStream, stopsMap);
612
+
613
+ const expectedTransfers = new Map([
614
+ [
615
+ 0,
616
+ [
617
+ {
618
+ destination: 1,
619
+ type: 'REQUIRES_MINIMAL_TIME',
620
+ minTransferTime: Duration.fromSeconds(180),
621
+ },
622
+ ],
623
+ ],
624
+ ]);
625
+
626
+ assert.deepEqual(result.transfers, expectedTransfers);
627
+ assert.deepEqual(result.tripContinuations, []);
628
+ });
629
+ });
630
+
631
+ describe('buildTripContinuations', () => {
632
+ it('should build trip continuations for valid data', () => {
633
+ const tripsMapping: TripsMapping = new Map([
634
+ ['trip1', { routeId: 0, tripRouteIndex: 0 }],
635
+ ['trip2', { routeId: 1, tripRouteIndex: 0 }],
636
+ ]);
637
+
638
+ const tripContinuations = [
639
+ {
640
+ fromStop: 100,
641
+ fromTrip: 'trip1',
642
+ toStop: 200,
643
+ toTrip: 'trip2',
644
+ },
645
+ ];
646
+
647
+ // Mock route with simple stops and timing
648
+ const mockFromRoute = {
649
+ stopRouteIndices: () => [0],
650
+ arrivalAt: () => Time.fromMinutes(60), // 1:00
651
+ } as unknown as Route;
652
+
653
+ const mockToRoute = {
654
+ stopRouteIndices: () => [1],
655
+ departureFrom: () => Time.fromMinutes(75), // 1:15
656
+ } as unknown as Route;
657
+
658
+ const mockTimetable = {
659
+ getRoute: (routeId: number) =>
660
+ routeId === 0 ? mockFromRoute : mockToRoute,
661
+ } as unknown as Timetable;
662
+
663
+ const activeStopIds = new Set([100, 200]);
664
+
665
+ const result = buildTripContinuations(
666
+ tripsMapping,
667
+ tripContinuations,
668
+ mockTimetable,
669
+ activeStopIds,
670
+ );
671
+
672
+ const expectedTripBoardingId = encode(0, 0, 0);
673
+ const continuations = result.get(expectedTripBoardingId);
674
+
675
+ assert(continuations);
676
+ assert.strictEqual(continuations.length, 1);
677
+ assert.deepEqual(continuations[0], {
678
+ hopOnStopIndex: 1,
679
+ routeId: 1,
680
+ tripIndex: 0,
681
+ });
682
+ });
683
+
684
+ it('should ignore trip continuations with inactive stops', () => {
685
+ const tripsMapping: TripsMapping = new Map([
686
+ ['trip1', { routeId: 0, tripRouteIndex: 0 }],
687
+ ['trip2', { routeId: 1, tripRouteIndex: 0 }],
688
+ ]);
689
+
690
+ const tripContinuations = [
691
+ {
692
+ fromStop: 100, // inactive stop
693
+ fromTrip: 'trip1',
694
+ toStop: 200,
695
+ toTrip: 'trip2',
696
+ },
697
+ ];
698
+
699
+ const mockTimetable = {} as unknown as Timetable;
700
+ const activeStopIds = new Set([200]); // only toStop is active
701
+
702
+ const result = buildTripContinuations(
703
+ tripsMapping,
704
+ tripContinuations,
705
+ mockTimetable,
706
+ activeStopIds,
707
+ );
708
+
709
+ assert.strictEqual(result.size, 0);
710
+ });
711
+
712
+ it('should ignore trip continuations with unknown trip IDs', () => {
713
+ const tripsMapping: TripsMapping = new Map([
714
+ ['trip1', { routeId: 0, tripRouteIndex: 0 }],
715
+ ]);
716
+
717
+ const tripContinuations = [
718
+ {
719
+ fromStop: 100,
720
+ fromTrip: 'unknown_trip', // not in tripsMapping
721
+ toStop: 200,
722
+ toTrip: 'trip1',
723
+ },
724
+ ];
725
+
726
+ const mockTimetable = {} as unknown as Timetable;
727
+ const activeStopIds = new Set([100, 200]);
728
+
729
+ const result = buildTripContinuations(
730
+ tripsMapping,
731
+ tripContinuations,
732
+ mockTimetable,
733
+ activeStopIds,
734
+ );
735
+
736
+ assert.strictEqual(result.size, 0);
737
+ });
738
+
739
+ it('should ignore trip continuations with unknown routes', () => {
740
+ const tripsMapping: TripsMapping = new Map([
741
+ ['trip1', { routeId: 0, tripRouteIndex: 0 }],
742
+ ['trip2', { routeId: 1, tripRouteIndex: 0 }],
743
+ ]);
744
+
745
+ const tripContinuations = [
746
+ {
747
+ fromStop: 100,
748
+ fromTrip: 'trip1',
749
+ toStop: 200,
750
+ toTrip: 'trip2',
751
+ },
752
+ ];
753
+
754
+ const mockTimetable = {
755
+ getRoute: () => undefined, // no routes found
756
+ } as unknown as Timetable;
757
+
758
+ const activeStopIds = new Set([100, 200]);
759
+
760
+ const result = buildTripContinuations(
761
+ tripsMapping,
762
+ tripContinuations,
763
+ mockTimetable,
764
+ activeStopIds,
765
+ );
766
+
767
+ assert.strictEqual(result.size, 0);
768
+ });
769
+
770
+ it('should ignore trip continuations with no valid timing', () => {
771
+ const tripsMapping: TripsMapping = new Map([
772
+ ['trip1', { routeId: 0, tripRouteIndex: 0 }],
773
+ ['trip2', { routeId: 1, tripRouteIndex: 0 }],
774
+ ]);
775
+
776
+ const tripContinuations = [
777
+ {
778
+ fromStop: 100,
779
+ fromTrip: 'trip1',
780
+ toStop: 200,
781
+ toTrip: 'trip2',
782
+ },
783
+ ];
784
+
785
+ const mockFromRoute = {
786
+ stopRouteIndices: () => [0],
787
+ arrivalAt: () => Time.fromMinutes(75), // 1:15 - arrives AFTER departure
788
+ } as unknown as Route;
789
+
790
+ const mockToRoute = {
791
+ stopRouteIndices: () => [1],
792
+ departureFrom: () => Time.fromMinutes(60), // 1:00 - departs BEFORE arrival
793
+ } as unknown as Route;
794
+
795
+ const mockTimetable = {
796
+ getRoute: (routeId: number) =>
797
+ routeId === 0 ? mockFromRoute : mockToRoute,
798
+ } as unknown as Timetable;
799
+
800
+ const activeStopIds = new Set([100, 200]);
801
+
802
+ const result = buildTripContinuations(
803
+ tripsMapping,
804
+ tripContinuations,
805
+ mockTimetable,
806
+ activeStopIds,
807
+ );
808
+
809
+ assert.strictEqual(result.size, 0);
810
+ });
811
+
812
+ it('should handle multiple continuations from same trip boarding', () => {
813
+ const tripsMapping: TripsMapping = new Map([
814
+ ['trip1', { routeId: 0, tripRouteIndex: 0 }],
815
+ ['trip2', { routeId: 1, tripRouteIndex: 0 }],
816
+ ['trip3', { routeId: 2, tripRouteIndex: 0 }],
817
+ ]);
818
+
819
+ const tripContinuations = [
820
+ {
821
+ fromStop: 100,
822
+ fromTrip: 'trip1',
823
+ toStop: 200,
824
+ toTrip: 'trip2',
825
+ },
826
+ {
827
+ fromStop: 100,
828
+ fromTrip: 'trip1',
829
+ toStop: 300,
830
+ toTrip: 'trip3',
831
+ },
832
+ ];
833
+
834
+ const mockFromRoute = {
835
+ stopRouteIndices: () => [0],
836
+ arrivalAt: () => Time.fromMinutes(60),
837
+ } as unknown as Route;
838
+
839
+ const mockToRoute1 = {
840
+ stopRouteIndices: () => [1],
841
+ departureFrom: () => Time.fromMinutes(70),
842
+ } as unknown as Route;
843
+
844
+ const mockToRoute2 = {
845
+ stopRouteIndices: () => [2],
846
+ departureFrom: () => Time.fromMinutes(80),
847
+ } as unknown as Route;
848
+
849
+ const mockTimetable = {
850
+ getRoute: (routeId: number) => {
851
+ if (routeId === 0) return mockFromRoute;
852
+ if (routeId === 1) return mockToRoute1;
853
+ if (routeId === 2) return mockToRoute2;
854
+ return undefined;
855
+ },
856
+ } as unknown as Timetable;
857
+
858
+ const activeStopIds = new Set([100, 200, 300]);
859
+
860
+ const result = buildTripContinuations(
861
+ tripsMapping,
862
+ tripContinuations,
863
+ mockTimetable,
864
+ activeStopIds,
865
+ );
866
+
867
+ const expectedTripBoardingId = encode(0, 0, 0);
868
+ const continuations = result.get(expectedTripBoardingId);
869
+
870
+ assert(continuations);
871
+ assert.strictEqual(continuations.length, 2);
872
+ assert.deepEqual(continuations[0], {
873
+ hopOnStopIndex: 1,
874
+ routeId: 1,
875
+ tripIndex: 0,
876
+ });
877
+ assert.deepEqual(continuations[1], {
878
+ hopOnStopIndex: 2,
879
+ routeId: 2,
880
+ tripIndex: 0,
881
+ });
882
+ });
883
+
884
+ it('should handle empty input gracefully', () => {
885
+ const tripsMapping: TripsMapping = new Map();
886
+ const tripContinuations: GtfsTripContinuation[] = [];
887
+ const mockTimetable = {} as unknown as Timetable;
888
+ const activeStopIds = new Set<number>();
889
+
890
+ const result = buildTripContinuations(
891
+ tripsMapping,
892
+ tripContinuations,
893
+ mockTimetable,
894
+ activeStopIds,
895
+ );
896
+
897
+ assert.strictEqual(result.size, 0);
898
+ });
899
+
900
+ it('should disambiguate transfers when routes visit same stop multiple times', () => {
901
+ const tripsMapping: TripsMapping = new Map([
902
+ ['trip1', { routeId: 0, tripRouteIndex: 0 }],
903
+ ['trip2', { routeId: 1, tripRouteIndex: 0 }],
904
+ ]);
905
+
906
+ const tripContinuations = [
907
+ {
908
+ fromStop: 100, // This stop appears multiple times in the route
909
+ fromTrip: 'trip1',
910
+ toStop: 200, // This stop also appears multiple times
911
+ toTrip: 'trip2',
912
+ },
913
+ ];
914
+
915
+ // Mock route that visits stop 100 at indices 0 and 3 (circular route)
916
+ const mockFromRoute = {
917
+ stopRouteIndices: () => [0, 3], // Stop 100 appears twice
918
+ arrivalAt: (stopIndex: number) => {
919
+ // First visit at 1:00, second visit at 2:00
920
+ return stopIndex === 0 ? Time.fromMinutes(60) : Time.fromMinutes(120);
921
+ },
922
+ } as unknown as Route;
923
+
924
+ // Mock route that visits stop 200 at indices 1 and 4
925
+ const mockToRoute = {
926
+ stopRouteIndices: () => [1, 4], // Stop 200 appears twice
927
+ departureFrom: (stopIndex: number) => {
928
+ // First departure at 1:10, second departure at 2:30
929
+ return stopIndex === 1 ? Time.fromMinutes(70) : Time.fromMinutes(150);
930
+ },
931
+ } as unknown as Route;
932
+
933
+ const mockTimetable = {
934
+ getRoute: (routeId: number) =>
935
+ routeId === 0 ? mockFromRoute : mockToRoute,
936
+ } as unknown as Timetable;
937
+
938
+ const activeStopIds = new Set([100, 200]);
939
+
940
+ const result = buildTripContinuations(
941
+ tripsMapping,
942
+ tripContinuations,
943
+ mockTimetable,
944
+ activeStopIds,
945
+ );
946
+
947
+ // Should pick the best timing: arrive at stop 0 (1:00) -> depart from stop 1 (1:10)
948
+ // This is better than arrive at stop 3 (2:00) -> depart from stop 4 (2:30)
949
+ const expectedTripBoardingId = encode(0, 0, 0); // stopIndex=0, routeId=0, tripIndex=0
950
+ const continuations = result.get(expectedTripBoardingId);
951
+
952
+ assert(continuations);
953
+ assert.strictEqual(continuations.length, 1);
954
+ assert.deepEqual(continuations[0], {
955
+ hopOnStopIndex: 1, // Best to-stop index
956
+ routeId: 1,
957
+ tripIndex: 0,
958
+ });
959
+ });
960
+
961
+ it('should handle case where no valid transfer timing exists between duplicate stops', () => {
962
+ const tripsMapping: TripsMapping = new Map([
963
+ ['trip1', { routeId: 0, tripRouteIndex: 0 }],
964
+ ['trip2', { routeId: 1, tripRouteIndex: 0 }],
965
+ ]);
966
+
967
+ const tripContinuations = [
968
+ {
969
+ fromStop: 100,
970
+ fromTrip: 'trip1',
971
+ toStop: 200,
972
+ toTrip: 'trip2',
973
+ },
974
+ ];
975
+
976
+ // Mock route where all arrivals are AFTER all departures (impossible transfer)
977
+ const mockFromRoute = {
978
+ stopRouteIndices: () => [0, 3], // Stop 100 appears twice
979
+ arrivalAt: (stopIndex: number) => {
980
+ // Both arrivals are late: 2:00 and 3:00
981
+ return stopIndex === 0 ? Time.fromMinutes(120) : Time.fromMinutes(180);
982
+ },
983
+ } as unknown as Route;
984
+
985
+ const mockToRoute = {
986
+ stopRouteIndices: () => [1, 4], // Stop 200 appears twice
987
+ departureFrom: (stopIndex: number) => {
988
+ // Both departures are early: 1:00 and 1:30
989
+ return stopIndex === 1 ? Time.fromMinutes(60) : Time.fromMinutes(90);
990
+ },
991
+ } as unknown as Route;
992
+
993
+ const mockTimetable = {
994
+ getRoute: (routeId: number) =>
995
+ routeId === 0 ? mockFromRoute : mockToRoute,
996
+ } as unknown as Timetable;
997
+
998
+ const activeStopIds = new Set([100, 200]);
999
+
1000
+ const result = buildTripContinuations(
1001
+ tripsMapping,
1002
+ tripContinuations,
1003
+ mockTimetable,
1004
+ activeStopIds,
1005
+ );
1006
+
1007
+ // Should find no valid continuations since all departures are before arrivals
1008
+ assert.strictEqual(result.size, 0);
273
1009
  });
274
1010
  });