minotor 8.0.0 → 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 (50) hide show
  1. package/CHANGELOG.md +4 -4
  2. package/README.md +1 -1
  3. package/dist/cli.mjs +835 -816
  4. package/dist/cli.mjs.map +1 -1
  5. package/dist/gtfs/transfers.d.ts +21 -6
  6. package/dist/gtfs/trips.d.ts +2 -2
  7. package/dist/parser.cjs.js +666 -642
  8. package/dist/parser.cjs.js.map +1 -1
  9. package/dist/parser.esm.js +666 -642
  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.esm.js +1 -1
  14. package/dist/router.esm.js.map +1 -1
  15. package/dist/router.umd.js +1 -1
  16. package/dist/router.umd.js.map +1 -1
  17. package/dist/routing/router.d.ts +4 -4
  18. package/dist/timetable/io.d.ts +3 -3
  19. package/dist/timetable/proto/timetable.d.ts +6 -4
  20. package/dist/timetable/route.d.ts +13 -21
  21. package/dist/timetable/timetable.d.ts +13 -11
  22. package/dist/timetable/tripBoardingId.d.ts +34 -0
  23. package/package.json +1 -1
  24. package/src/__e2e__/timetable/timetable.bin +2 -2
  25. package/src/cli/repl.ts +53 -67
  26. package/src/gtfs/__tests__/parser.test.ts +19 -4
  27. package/src/gtfs/__tests__/transfers.test.ts +598 -318
  28. package/src/gtfs/__tests__/trips.test.ts +3 -44
  29. package/src/gtfs/parser.ts +26 -8
  30. package/src/gtfs/transfers.ts +151 -20
  31. package/src/gtfs/trips.ts +1 -39
  32. package/src/routing/__tests__/result.test.ts +10 -10
  33. package/src/routing/__tests__/router.test.ts +11 -9
  34. package/src/routing/result.ts +2 -2
  35. package/src/routing/router.ts +34 -22
  36. package/src/timetable/__tests__/io.test.ts +8 -7
  37. package/src/timetable/__tests__/route.test.ts +66 -80
  38. package/src/timetable/__tests__/timetable.test.ts +32 -29
  39. package/src/timetable/__tests__/tripBoardingId.test.ts +57 -0
  40. package/src/timetable/io.ts +21 -20
  41. package/src/timetable/proto/timetable.proto +6 -4
  42. package/src/timetable/proto/timetable.ts +84 -48
  43. package/src/timetable/route.ts +39 -56
  44. package/src/timetable/timetable.ts +37 -26
  45. package/src/timetable/tripBoardingId.ts +94 -0
  46. package/tsconfig.json +2 -2
  47. package/dist/timetable/tripId.d.ts +0 -15
  48. package/src/timetable/__tests__/tripId.test.ts +0 -27
  49. package/src/timetable/tripId.ts +0 -29
  50. /package/dist/timetable/__tests__/{tripId.test.d.ts → tripBoardingId.test.d.ts} +0 -0
@@ -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([
@@ -60,6 +69,7 @@ describe('GTFS transfers parser', () => {
60
69
  ]);
61
70
 
62
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,7 +86,7 @@ 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
  ],
@@ -84,10 +94,10 @@ describe('GTFS transfers parser', () => {
84
94
  ]);
85
95
 
86
96
  assert.deepEqual(result.transfers, expectedTransfers);
87
- assert.deepEqual(result.tripContinuations, new Map());
97
+ assert.deepEqual(result.tripContinuations, []);
88
98
  });
89
99
 
90
- it('should ignore impossible transfer types', async () => {
100
+ it('should ignore impossible transfer types (3 and 5)', async () => {
91
101
  const mockedStream = new Readable();
92
102
  mockedStream.push(
93
103
  'from_stop_id,to_stop_id,transfer_type,min_transfer_time\n',
@@ -140,35 +150,37 @@ describe('GTFS transfers parser', () => {
140
150
  ]);
141
151
 
142
152
  const result = await parseTransfers(mockedStream, stopsMap);
153
+
143
154
  assert.deepEqual(result.transfers, new Map());
144
- assert.deepEqual(result.tripContinuations, new Map());
155
+ assert.deepEqual(result.tripContinuations, []);
145
156
  });
146
157
 
147
- it('should ignore unsupported transfer types between routes', async () => {
158
+ it('should ignore transfers with missing stop IDs', async () => {
148
159
  const mockedStream = new Readable();
149
160
  mockedStream.push(
150
- 'from_route_id,to_route_id,transfer_type,min_transfer_time\n',
161
+ 'from_stop_id,to_stop_id,transfer_type,min_transfer_time\n',
151
162
  );
152
- mockedStream.push('"1100084","8014440","2","180"\n');
163
+ mockedStream.push(',"8014440:0:1","2","180"\n');
164
+ mockedStream.push('"1100097",,"0","240"\n');
153
165
  mockedStream.push(null);
154
166
 
155
167
  const stopsMap: GtfsStopsMap = new Map([
156
168
  [
157
- '1100084',
169
+ '8014440:0:1',
158
170
  {
159
- id: 0,
160
- sourceStopId: '1100084',
161
- name: 'Test Stop 1',
171
+ id: 1,
172
+ sourceStopId: '8014440:0:1',
173
+ name: 'Test Stop 2',
162
174
  children: [],
163
175
  locationType: 'SIMPLE_STOP_OR_PLATFORM',
164
176
  },
165
177
  ],
166
178
  [
167
- '8014440',
179
+ '1100097',
168
180
  {
169
- id: 1,
170
- sourceStopId: '8014440',
171
- name: 'Test Stop 2',
181
+ id: 2,
182
+ sourceStopId: '1100097',
183
+ name: 'Test Stop 3',
172
184
  children: [],
173
185
  locationType: 'SIMPLE_STOP_OR_PLATFORM',
174
186
  },
@@ -176,16 +188,18 @@ describe('GTFS transfers parser', () => {
176
188
  ]);
177
189
 
178
190
  const result = await parseTransfers(mockedStream, stopsMap);
191
+
179
192
  assert.deepEqual(result.transfers, new Map());
180
- assert.deepEqual(result.tripContinuations, new Map());
193
+ assert.deepEqual(result.tripContinuations, []);
181
194
  });
182
195
 
183
- it('should ignore unsupported transfer types between trips', async () => {
196
+ it('should correctly parse in-seat transfers (type 4)', async () => {
184
197
  const mockedStream = new Readable();
185
198
  mockedStream.push(
186
- 'from_trip_id,to_trip_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',
187
200
  );
188
- 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');
189
203
  mockedStream.push(null);
190
204
 
191
205
  const stopsMap: GtfsStopsMap = new Map([
@@ -200,130 +214,106 @@ describe('GTFS transfers parser', () => {
200
214
  },
201
215
  ],
202
216
  [
203
- '8014440',
217
+ '8014440:0:1',
204
218
  {
205
219
  id: 1,
206
- sourceStopId: '8014440',
220
+ sourceStopId: '8014440:0:1',
207
221
  name: 'Test Stop 2',
208
222
  children: [],
209
223
  locationType: 'SIMPLE_STOP_OR_PLATFORM',
210
224
  },
211
225
  ],
212
- ]);
213
-
214
- const result = await parseTransfers(mockedStream, stopsMap);
215
- assert.deepEqual(result.transfers, new Map());
216
- assert.deepEqual(result.tripContinuations, new Map());
217
- });
218
-
219
- it('should allow missing minimum transfer time', async () => {
220
- const mockedStream = new Readable();
221
- mockedStream.push(
222
- 'from_stop_id,to_stop_id,transfer_type,min_transfer_time\n',
223
- );
224
- mockedStream.push('"1100084","8014440:0:1","2"\n');
225
- mockedStream.push(null);
226
-
227
- const stopsMap: GtfsStopsMap = new Map([
228
226
  [
229
- '1100084',
227
+ '1100097',
230
228
  {
231
- id: 0,
232
- sourceStopId: '1100084',
233
- name: 'Test Stop 1',
229
+ id: 2,
230
+ sourceStopId: '1100097',
231
+ name: 'Test Stop 3',
234
232
  children: [],
235
233
  locationType: 'SIMPLE_STOP_OR_PLATFORM',
236
234
  },
237
235
  ],
238
236
  [
239
- '8014440:0:1',
237
+ '8014447',
240
238
  {
241
- id: 1,
242
- sourceStopId: '8014440:0:1',
243
- name: 'Test Stop 2',
239
+ id: 3,
240
+ sourceStopId: '8014447',
241
+ name: 'Test Stop 4',
244
242
  children: [],
245
243
  locationType: 'SIMPLE_STOP_OR_PLATFORM',
246
244
  },
247
245
  ],
248
246
  ]);
247
+
249
248
  const result = await parseTransfers(mockedStream, stopsMap);
250
- assert.deepEqual(
251
- result.transfers,
252
- new Map([
253
- [
254
- 0, // Internal ID for stop '1100084'
255
- [
256
- {
257
- destination: 1, // Internal ID for stop '8014440:0:1'
258
- type: 'REQUIRES_MINIMAL_TIME',
259
- },
260
- ],
261
- ],
262
- ]),
263
- );
264
- assert.deepEqual(result.tripContinuations, new Map());
265
- });
266
249
 
267
- it('should handle empty transfers', async () => {
268
- const mockedStream = new Readable();
269
- mockedStream.push(
270
- 'from_stop_id,to_stop_id,transfer_type,min_transfer_time\n',
271
- );
272
- mockedStream.push(null);
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
+ ];
273
264
 
274
- const stopsMap: GtfsStopsMap = new Map();
275
-
276
- const result = await parseTransfers(mockedStream, stopsMap);
277
265
  assert.deepEqual(result.transfers, new Map());
278
- assert.deepEqual(result.tripContinuations, new Map());
266
+ assert.deepEqual(result.tripContinuations, expectedTripContinuations);
279
267
  });
280
268
 
281
- it('should correctly parse valid trip continuations (in-seat transfers)', async () => {
269
+ it('should ignore in-seat transfers with missing trip IDs', async () => {
282
270
  const mockedStream = new Readable();
283
271
  mockedStream.push(
284
- 'from_stop_id,to_stop_id,from_trip_id,to_trip_id,transfer_type\n',
272
+ 'from_stop_id,to_stop_id,from_trip_id,to_trip_id,transfer_type,min_transfer_time\n',
285
273
  );
286
- mockedStream.push('"stop1","stop2","trip1","trip2","4"\n');
287
- mockedStream.push('"stop3","stop4","trip3","trip4","4"\n');
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');
288
278
  mockedStream.push(null);
289
279
 
290
280
  const stopsMap: GtfsStopsMap = new Map([
291
281
  [
292
- 'stop1',
282
+ '1100084',
293
283
  {
294
284
  id: 0,
295
- sourceStopId: 'stop1',
296
- name: 'Stop 1',
285
+ sourceStopId: '1100084',
286
+ name: 'Test Stop 1',
297
287
  children: [],
298
288
  locationType: 'SIMPLE_STOP_OR_PLATFORM',
299
289
  },
300
290
  ],
301
291
  [
302
- 'stop2',
292
+ '8014440:0:1',
303
293
  {
304
294
  id: 1,
305
- sourceStopId: 'stop2',
306
- name: 'Stop 2',
295
+ sourceStopId: '8014440:0:1',
296
+ name: 'Test Stop 2',
307
297
  children: [],
308
298
  locationType: 'SIMPLE_STOP_OR_PLATFORM',
309
299
  },
310
300
  ],
311
301
  [
312
- 'stop3',
302
+ '1100097',
313
303
  {
314
304
  id: 2,
315
- sourceStopId: 'stop3',
316
- name: 'Stop 3',
305
+ sourceStopId: '1100097',
306
+ name: 'Test Stop 3',
317
307
  children: [],
318
308
  locationType: 'SIMPLE_STOP_OR_PLATFORM',
319
309
  },
320
310
  ],
321
311
  [
322
- 'stop4',
312
+ '8014447',
323
313
  {
324
314
  id: 3,
325
- sourceStopId: 'stop4',
326
- name: 'Stop 4',
315
+ sourceStopId: '8014447',
316
+ name: 'Test Stop 4',
327
317
  children: [],
328
318
  locationType: 'SIMPLE_STOP_OR_PLATFORM',
329
319
  },
@@ -332,136 +322,130 @@ describe('GTFS transfers parser', () => {
332
322
 
333
323
  const result = await parseTransfers(mockedStream, stopsMap);
334
324
 
335
- const expectedTripContinuations = new Map([
336
- [
337
- 0, // from_stop_id 'stop1' -> internal ID 0
338
- [
339
- {
340
- fromTrip: 'trip1',
341
- toTrip: 'trip2',
342
- hopOnStop: 1, // to_stop_id 'stop2' -> internal ID 1
343
- },
344
- ],
345
- ],
346
- [
347
- 2, // from_stop_id 'stop3' -> internal ID 2
348
- [
349
- {
350
- fromTrip: 'trip3',
351
- toTrip: 'trip4',
352
- hopOnStop: 3, // to_stop_id 'stop4' -> internal ID 3
353
- },
354
- ],
355
- ],
356
- ]);
357
-
358
325
  assert.deepEqual(result.transfers, new Map());
359
- assert.deepEqual(result.tripContinuations, expectedTripContinuations);
326
+ assert.deepEqual(result.tripContinuations, []);
360
327
  });
361
328
 
362
- it('should handle multiple trip continuations from the same stop', async () => {
329
+ it('should ignore unsupported transfer types between trips', async () => {
363
330
  const mockedStream = new Readable();
364
331
  mockedStream.push(
365
- 'from_stop_id,to_stop_id,from_trip_id,to_trip_id,transfer_type\n',
332
+ 'from_stop_id,to_stop_id,from_trip_id,to_trip_id,transfer_type,min_transfer_time\n',
366
333
  );
367
- mockedStream.push('"stop1","stop2","trip1","trip2","4"\n');
368
- mockedStream.push('"stop1","stop3","trip1","trip3","4"\n');
334
+ mockedStream.push('"1100084","8014440:0:1","trip1","trip2","1","0"\n');
369
335
  mockedStream.push(null);
370
336
 
371
337
  const stopsMap: GtfsStopsMap = new Map([
372
338
  [
373
- 'stop1',
339
+ '1100084',
374
340
  {
375
341
  id: 0,
376
- sourceStopId: 'stop1',
377
- name: 'Stop 1',
342
+ sourceStopId: '1100084',
343
+ name: 'Test Stop 1',
378
344
  children: [],
379
345
  locationType: 'SIMPLE_STOP_OR_PLATFORM',
380
346
  },
381
347
  ],
382
348
  [
383
- 'stop2',
349
+ '8014440:0:1',
384
350
  {
385
351
  id: 1,
386
- sourceStopId: 'stop2',
387
- name: 'Stop 2',
352
+ sourceStopId: '8014440:0:1',
353
+ name: 'Test Stop 2',
388
354
  children: [],
389
355
  locationType: 'SIMPLE_STOP_OR_PLATFORM',
390
356
  },
391
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',
370
+ );
371
+ mockedStream.push('"1100084","8014440:0:1","route1","route2","1","0"\n');
372
+ mockedStream.push(null);
373
+
374
+ const stopsMap: GtfsStopsMap = new Map([
392
375
  [
393
- 'stop3',
376
+ '1100084',
394
377
  {
395
- id: 2,
396
- sourceStopId: 'stop3',
397
- name: 'Stop 3',
378
+ id: 0,
379
+ sourceStopId: '1100084',
380
+ name: 'Test Stop 1',
398
381
  children: [],
399
382
  locationType: 'SIMPLE_STOP_OR_PLATFORM',
400
383
  },
401
384
  ],
402
- ]);
403
-
404
- const result = await parseTransfers(mockedStream, stopsMap);
405
-
406
- const expectedTripContinuations = new Map([
407
385
  [
408
- 0, // from_stop_id 'stop1' -> internal ID 0
409
- [
410
- {
411
- fromTrip: 'trip1',
412
- toTrip: 'trip2',
413
- hopOnStop: 1, // to_stop_id 'stop2' -> internal ID 1
414
- },
415
- {
416
- fromTrip: 'trip1',
417
- toTrip: 'trip3',
418
- hopOnStop: 2, // to_stop_id 'stop3' -> internal ID 2
419
- },
420
- ],
386
+ '8014440:0:1',
387
+ {
388
+ id: 1,
389
+ sourceStopId: '8014440:0:1',
390
+ name: 'Test Stop 2',
391
+ children: [],
392
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
393
+ },
421
394
  ],
422
395
  ]);
423
396
 
397
+ const result = await parseTransfers(mockedStream, stopsMap);
398
+
424
399
  assert.deepEqual(result.transfers, new Map());
425
- assert.deepEqual(result.tripContinuations, expectedTripContinuations);
400
+ assert.deepEqual(result.tripContinuations, []);
426
401
  });
427
402
 
428
- it('should mix regular transfers and trip continuations correctly', async () => {
403
+ it('should handle transfers without minimum transfer time', async () => {
429
404
  const mockedStream = new Readable();
430
405
  mockedStream.push(
431
- 'from_stop_id,to_stop_id,from_trip_id,to_trip_id,transfer_type,min_transfer_time\n',
406
+ 'from_stop_id,to_stop_id,transfer_type,min_transfer_time\n',
432
407
  );
433
- mockedStream.push('"stop1","stop2","","","2","120"\n'); // Regular transfer
434
- mockedStream.push('"stop1","stop3","trip1","trip2","4",""\n'); // Trip continuation
435
- mockedStream.push('"stop2","stop3","","","0",""\n'); // Regular transfer
408
+ mockedStream.push('"1100084","8014440:0:1","2"\n');
409
+ mockedStream.push('"1100097","8014447","1","0"\n');
436
410
  mockedStream.push(null);
437
411
 
438
412
  const stopsMap: GtfsStopsMap = new Map([
439
413
  [
440
- 'stop1',
414
+ '1100084',
441
415
  {
442
416
  id: 0,
443
- sourceStopId: 'stop1',
444
- name: 'Stop 1',
417
+ sourceStopId: '1100084',
418
+ name: 'Test Stop 1',
445
419
  children: [],
446
420
  locationType: 'SIMPLE_STOP_OR_PLATFORM',
447
421
  },
448
422
  ],
449
423
  [
450
- 'stop2',
424
+ '8014440:0:1',
451
425
  {
452
426
  id: 1,
453
- sourceStopId: 'stop2',
454
- name: 'Stop 2',
427
+ sourceStopId: '8014440:0:1',
428
+ name: 'Test Stop 2',
455
429
  children: [],
456
430
  locationType: 'SIMPLE_STOP_OR_PLATFORM',
457
431
  },
458
432
  ],
459
433
  [
460
- 'stop3',
434
+ '1100097',
461
435
  {
462
436
  id: 2,
463
- sourceStopId: 'stop3',
464
- name: 'Stop 3',
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',
465
449
  children: [],
466
450
  locationType: 'SIMPLE_STOP_OR_PLATFORM',
467
451
  },
@@ -472,215 +456,152 @@ describe('GTFS transfers parser', () => {
472
456
 
473
457
  const expectedTransfers = new Map([
474
458
  [
475
- 0, // from_stop_id 'stop1' -> internal ID 0
459
+ 0,
476
460
  [
477
461
  {
478
- destination: 1, // to_stop_id 'stop2' -> internal ID 1
462
+ destination: 1,
479
463
  type: 'REQUIRES_MINIMAL_TIME',
480
- minTransferTime: Duration.fromSeconds(120),
481
- },
482
- ],
483
- ],
484
- [
485
- 1, // from_stop_id 'stop2' -> internal ID 1
486
- [
487
- {
488
- destination: 2, // to_stop_id 'stop3' -> internal ID 2
489
- type: 'RECOMMENDED',
490
464
  },
491
465
  ],
492
466
  ],
493
- ]);
494
-
495
- const expectedTripContinuations = new Map([
496
467
  [
497
- 0, // from_stop_id 'stop1' -> internal ID 0
468
+ 2,
498
469
  [
499
470
  {
500
- fromTrip: 'trip1',
501
- toTrip: 'trip2',
502
- hopOnStop: 2, // to_stop_id 'stop3' -> internal ID 2
471
+ destination: 3,
472
+ type: 'GUARANTEED',
473
+ minTransferTime: Duration.fromSeconds(0),
503
474
  },
504
475
  ],
505
476
  ],
506
477
  ]);
507
478
 
508
479
  assert.deepEqual(result.transfers, expectedTransfers);
509
- assert.deepEqual(result.tripContinuations, expectedTripContinuations);
510
- });
511
-
512
- it('should ignore trip continuations with undefined trip IDs', async () => {
513
- const mockedStream = new Readable();
514
- mockedStream.push('from_stop_id,to_stop_id,transfer_type\n');
515
- mockedStream.push('"stop1","stop2","4"\n');
516
- mockedStream.push(null);
517
-
518
- const stopsMap: GtfsStopsMap = new Map([
519
- [
520
- 'stop1',
521
- {
522
- id: 0,
523
- sourceStopId: 'stop1',
524
- name: 'Stop 1',
525
- children: [],
526
- locationType: 'SIMPLE_STOP_OR_PLATFORM',
527
- },
528
- ],
529
- [
530
- 'stop2',
531
- {
532
- id: 1,
533
- sourceStopId: 'stop2',
534
- name: 'Stop 2',
535
- children: [],
536
- locationType: 'SIMPLE_STOP_OR_PLATFORM',
537
- },
538
- ],
539
- ]);
540
-
541
- const result = await parseTransfers(mockedStream, stopsMap);
542
-
543
- assert.deepEqual(result.transfers, new Map());
544
- assert.deepEqual(result.tripContinuations, new Map());
480
+ assert.deepEqual(result.tripContinuations, []);
545
481
  });
546
482
 
547
- it('should ignore trip continuations with empty string trip IDs', async () => {
483
+ it('should handle mixed transfers and trip continuations', async () => {
548
484
  const mockedStream = new Readable();
549
485
  mockedStream.push(
550
- 'from_stop_id,to_stop_id,from_trip_id,to_trip_id,transfer_type\n',
486
+ 'from_stop_id,to_stop_id,from_trip_id,to_trip_id,transfer_type,min_transfer_time\n',
551
487
  );
552
- mockedStream.push('"stop1","stop2","trip1","","4"\n');
553
- mockedStream.push('"stop3","stop4","","trip4","4"\n');
554
- mockedStream.push('"stop5","stop6","","","4"\n');
488
+ mockedStream.push('"1100084","8014440:0:1","","","1","120"\n');
489
+ mockedStream.push('"1100097","8014447","trip1","trip2","4","0"\n');
555
490
  mockedStream.push(null);
556
491
 
557
492
  const stopsMap: GtfsStopsMap = new Map([
558
493
  [
559
- 'stop1',
494
+ '1100084',
560
495
  {
561
496
  id: 0,
562
- sourceStopId: 'stop1',
563
- name: 'Stop 1',
497
+ sourceStopId: '1100084',
498
+ name: 'Test Stop 1',
564
499
  children: [],
565
500
  locationType: 'SIMPLE_STOP_OR_PLATFORM',
566
501
  },
567
502
  ],
568
503
  [
569
- 'stop2',
504
+ '8014440:0:1',
570
505
  {
571
506
  id: 1,
572
- sourceStopId: 'stop2',
573
- name: 'Stop 2',
507
+ sourceStopId: '8014440:0:1',
508
+ name: 'Test Stop 2',
574
509
  children: [],
575
510
  locationType: 'SIMPLE_STOP_OR_PLATFORM',
576
511
  },
577
512
  ],
578
513
  [
579
- 'stop3',
514
+ '1100097',
580
515
  {
581
516
  id: 2,
582
- sourceStopId: 'stop3',
583
- name: 'Stop 3',
517
+ sourceStopId: '1100097',
518
+ name: 'Test Stop 3',
584
519
  children: [],
585
520
  locationType: 'SIMPLE_STOP_OR_PLATFORM',
586
521
  },
587
522
  ],
588
523
  [
589
- 'stop4',
524
+ '8014447',
590
525
  {
591
526
  id: 3,
592
- sourceStopId: 'stop4',
593
- name: 'Stop 4',
594
- children: [],
595
- locationType: 'SIMPLE_STOP_OR_PLATFORM',
596
- },
597
- ],
598
- [
599
- 'stop5',
600
- {
601
- id: 4,
602
- sourceStopId: 'stop5',
603
- name: 'Stop 5',
527
+ sourceStopId: '8014447',
528
+ name: 'Test Stop 4',
604
529
  children: [],
605
530
  locationType: 'SIMPLE_STOP_OR_PLATFORM',
606
531
  },
607
532
  ],
533
+ ]);
534
+
535
+ const result = await parseTransfers(mockedStream, stopsMap);
536
+
537
+ const expectedTransfers = new Map([
608
538
  [
609
- 'stop6',
610
- {
611
- id: 5,
612
- sourceStopId: 'stop6',
613
- name: 'Stop 6',
614
- children: [],
615
- locationType: 'SIMPLE_STOP_OR_PLATFORM',
616
- },
539
+ 0,
540
+ [
541
+ {
542
+ destination: 1,
543
+ type: 'GUARANTEED',
544
+ minTransferTime: Duration.fromSeconds(120),
545
+ },
546
+ ],
617
547
  ],
618
548
  ]);
619
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);
561
+ });
562
+
563
+ it('should handle empty transfers file', async () => {
564
+ const mockedStream = new Readable();
565
+ mockedStream.push(
566
+ 'from_stop_id,to_stop_id,transfer_type,min_transfer_time\n',
567
+ );
568
+ mockedStream.push(null);
569
+
570
+ const stopsMap: GtfsStopsMap = new Map();
571
+
620
572
  const result = await parseTransfers(mockedStream, stopsMap);
621
573
 
622
574
  assert.deepEqual(result.transfers, new Map());
623
- assert.deepEqual(result.tripContinuations, new Map());
575
+ assert.deepEqual(result.tripContinuations, []);
624
576
  });
625
577
 
626
- it('should handle complex scenario with multiple transfer types from same stop', async () => {
578
+ it('should ignore transfers with non-existent stops', async () => {
627
579
  const mockedStream = new Readable();
628
580
  mockedStream.push(
629
- 'from_stop_id,to_stop_id,from_trip_id,to_trip_id,transfer_type,min_transfer_time\n',
581
+ 'from_stop_id,to_stop_id,transfer_type,min_transfer_time\n',
630
582
  );
631
- mockedStream.push('"stop1","stop2","","","2","120"\n'); // Regular transfer to stop2
632
- mockedStream.push('"stop1","stop3","trip1","trip2","4",""\n'); // Trip continuation to stop3
633
- mockedStream.push('"stop1","stop4","","","0",""\n'); // Another regular transfer to stop4
634
- mockedStream.push('"stop1","stop5","trip3","trip4","4",""\n'); // Another trip continuation to stop5
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');
635
586
  mockedStream.push(null);
636
587
 
637
588
  const stopsMap: GtfsStopsMap = new Map([
638
589
  [
639
- 'stop1',
590
+ '1100084',
640
591
  {
641
592
  id: 0,
642
- sourceStopId: 'stop1',
643
- name: 'Stop 1',
593
+ sourceStopId: '1100084',
594
+ name: 'Test Stop 1',
644
595
  children: [],
645
596
  locationType: 'SIMPLE_STOP_OR_PLATFORM',
646
597
  },
647
598
  ],
648
599
  [
649
- 'stop2',
600
+ '8014440:0:1',
650
601
  {
651
602
  id: 1,
652
- sourceStopId: 'stop2',
653
- name: 'Stop 2',
654
- children: [],
655
- locationType: 'SIMPLE_STOP_OR_PLATFORM',
656
- },
657
- ],
658
- [
659
- 'stop3',
660
- {
661
- id: 2,
662
- sourceStopId: 'stop3',
663
- name: 'Stop 3',
664
- children: [],
665
- locationType: 'SIMPLE_STOP_OR_PLATFORM',
666
- },
667
- ],
668
- [
669
- 'stop4',
670
- {
671
- id: 3,
672
- sourceStopId: 'stop4',
673
- name: 'Stop 4',
674
- children: [],
675
- locationType: 'SIMPLE_STOP_OR_PLATFORM',
676
- },
677
- ],
678
- [
679
- 'stop5',
680
- {
681
- id: 4,
682
- sourceStopId: 'stop5',
683
- name: 'Stop 5',
603
+ sourceStopId: '8014440:0:1',
604
+ name: 'Test Stop 2',
684
605
  children: [],
685
606
  locationType: 'SIMPLE_STOP_OR_PLATFORM',
686
607
  },
@@ -691,40 +612,399 @@ describe('GTFS transfers parser', () => {
691
612
 
692
613
  const expectedTransfers = new Map([
693
614
  [
694
- 0, // from_stop_id 'stop1' -> internal ID 0
615
+ 0,
695
616
  [
696
617
  {
697
- destination: 1, // to_stop_id 'stop2' -> internal ID 1
618
+ destination: 1,
698
619
  type: 'REQUIRES_MINIMAL_TIME',
699
- minTransferTime: Duration.fromSeconds(120),
700
- },
701
- {
702
- destination: 3, // to_stop_id 'stop4' -> internal ID 3
703
- type: 'RECOMMENDED',
620
+ minTransferTime: Duration.fromSeconds(180),
704
621
  },
705
622
  ],
706
623
  ],
707
624
  ]);
708
625
 
709
- const expectedTripContinuations = new Map([
710
- [
711
- 0, // from_stop_id 'stop1' -> internal ID 0
712
- [
713
- {
714
- fromTrip: 'trip1',
715
- toTrip: 'trip2',
716
- hopOnStop: 2, // to_stop_id 'stop3' -> internal ID 2
717
- },
718
- {
719
- fromTrip: 'trip3',
720
- toTrip: 'trip4',
721
- hopOnStop: 4, // to_stop_id 'stop5' -> internal ID 4
722
- },
723
- ],
724
- ],
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 }],
725
636
  ]);
726
637
 
727
- assert.deepEqual(result.transfers, expectedTransfers);
728
- assert.deepEqual(result.tripContinuations, expectedTripContinuations);
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);
729
1009
  });
730
1010
  });