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
@@ -1,12 +1,16 @@
1
1
  import assert from 'node:assert';
2
2
  import { describe, it } from 'node:test';
3
3
 
4
+ import { Timetable } from '../../router.js';
4
5
  import { Stop, StopId } from '../../stops/stops.js';
5
6
  import { StopsIndex } from '../../stops/stopsIndex.js';
7
+ import { Duration } from '../../timetable/duration.js';
8
+ import { Route } from '../../timetable/route.js';
6
9
  import { Time } from '../../timetable/time.js';
10
+ import { ServiceRoute, StopAdjacency } from '../../timetable/timetable.js';
7
11
  import { Query } from '../query.js';
8
12
  import { Result } from '../result.js';
9
- import { ReachingTime, TripLeg } from '../router.js';
13
+ import { Arrival, RoutingEdge, TransferEdge, VehicleEdge } from '../router.js';
10
14
 
11
15
  describe('Result', () => {
12
16
  const stop1: Stop = {
@@ -86,7 +90,82 @@ describe('Result', () => {
86
90
  locationType: 'SIMPLE_STOP_OR_PLATFORM',
87
91
  };
88
92
 
89
- const stopsArray = [
93
+ const stopsAdjacency: StopAdjacency[] = [
94
+ { routes: [0] },
95
+ { routes: [0] },
96
+ { routes: [0, 1] },
97
+ { routes: [1] },
98
+ { routes: [1] },
99
+ { routes: [1] },
100
+ { routes: [1] },
101
+ ];
102
+ const routesAdjacency = [
103
+ Route.of({
104
+ id: 0,
105
+ serviceRouteId: 0,
106
+ trips: [
107
+ {
108
+ stops: [
109
+ {
110
+ id: 0,
111
+ arrivalTime: Time.fromString('08:00:00'),
112
+ departureTime: Time.fromString('08:05:00'),
113
+ },
114
+ {
115
+ id: 1,
116
+ arrivalTime: Time.fromString('08:30:00'),
117
+ departureTime: Time.fromString('08:35:00'),
118
+ },
119
+ {
120
+ id: 2,
121
+ arrivalTime: Time.fromString('09:00:00'),
122
+ departureTime: Time.fromString('09:05:00'),
123
+ },
124
+ ],
125
+ },
126
+ ],
127
+ }),
128
+ Route.of({
129
+ id: 1,
130
+ serviceRouteId: 1,
131
+ trips: [
132
+ {
133
+ stops: [
134
+ {
135
+ id: 2,
136
+ arrivalTime: Time.fromString('09:10:00'),
137
+ departureTime: Time.fromString('09:15:00'),
138
+ },
139
+ {
140
+ id: 3,
141
+ arrivalTime: Time.fromString('09:45:00'),
142
+ departureTime: Time.fromString('09:50:00'),
143
+ },
144
+ {
145
+ id: 5,
146
+ arrivalTime: Time.fromString('10:10:00'),
147
+ departureTime: Time.fromString('10:15:00'),
148
+ },
149
+ ],
150
+ },
151
+ ],
152
+ }),
153
+ ];
154
+
155
+ const routes: ServiceRoute[] = [
156
+ {
157
+ type: 'RAIL',
158
+ name: 'Line 1',
159
+ routes: [0],
160
+ },
161
+ {
162
+ type: 'RAIL',
163
+ name: 'Line 2',
164
+ routes: [1],
165
+ },
166
+ ];
167
+
168
+ const mockStopsIndex = new StopsIndex([
90
169
  stop1,
91
170
  stop2,
92
171
  stop3,
@@ -94,9 +173,8 @@ describe('Result', () => {
94
173
  parentStop,
95
174
  childStop1,
96
175
  childStop2,
97
- ];
98
-
99
- const mockStopsIndex = new StopsIndex(stopsArray);
176
+ ]);
177
+ const mockTimetable = new Timetable(stopsAdjacency, routesAdjacency, routes);
100
178
 
101
179
  const mockQuery = new Query.Builder()
102
180
  .from('stop1')
@@ -106,14 +184,18 @@ describe('Result', () => {
106
184
 
107
185
  describe('bestRoute', () => {
108
186
  it('should return undefined when no route exists', () => {
109
- const earliestArrivals = new Map<StopId, ReachingTime>();
110
- const earliestArrivalsPerRound: Map<StopId, TripLeg>[] = [];
187
+ const earliestArrivals = new Map<StopId, Arrival>();
188
+ const graph: Map<StopId, RoutingEdge>[] = [];
111
189
 
112
190
  const result = new Result(
113
191
  mockQuery,
114
- earliestArrivals,
115
- earliestArrivalsPerRound,
192
+ {
193
+ earliestArrivals,
194
+ graph,
195
+ destinations: [2, 3],
196
+ },
116
197
  mockStopsIndex,
198
+ mockTimetable,
117
199
  );
118
200
 
119
201
  const route = result.bestRoute();
@@ -122,15 +204,21 @@ describe('Result', () => {
122
204
 
123
205
  it('should return undefined for unreachable destination', () => {
124
206
  const earliestArrivals = new Map([
125
- [1, { arrival: Time.fromHMS(8, 30, 0), legNumber: 0, origin: 0 }],
207
+ [1, { arrival: Time.fromHMS(8, 30, 0), legNumber: 0 }],
126
208
  ]);
127
- const earliestArrivalsPerRound: Map<StopId, TripLeg>[] = [];
209
+ const graph: Map<StopId, RoutingEdge>[] = [
210
+ new Map<StopId, RoutingEdge>([[0, { arrival: Time.fromHMS(8, 0, 0) }]]), // Round 0 - origins
211
+ ];
128
212
 
129
213
  const result = new Result(
130
214
  mockQuery,
131
- earliestArrivals,
132
- earliestArrivalsPerRound,
215
+ {
216
+ earliestArrivals,
217
+ graph,
218
+ destinations: [2, 3],
219
+ },
133
220
  mockStopsIndex,
221
+ mockTimetable,
134
222
  );
135
223
 
136
224
  const route = result.bestRoute('stop4'); // stop4 not in earliestArrivals
@@ -139,125 +227,375 @@ describe('Result', () => {
139
227
 
140
228
  it('should return route to closest destination when multiple destinations exist', () => {
141
229
  const earliestArrivals = new Map([
142
- [2, { arrival: Time.fromHMS(9, 0, 0), legNumber: 0, origin: 0 }], // faster
143
- [3, { arrival: Time.fromHMS(9, 30, 0), legNumber: 0, origin: 0 }], // slower
230
+ [0, { arrival: Time.fromHMS(8, 0, 0), legNumber: 0 }], // origin
231
+ [2, { arrival: Time.fromHMS(9, 0, 0), legNumber: 1 }], // faster destination
232
+ [3, { arrival: Time.fromHMS(9, 30, 0), legNumber: 1 }], // slower destination
144
233
  ]);
145
234
 
146
- const vehicleLegTo3 = {
147
- from: stop1,
148
- to: stop3,
149
- route: { type: 'BUS', name: 'Bus 101' },
150
- departureTime: Time.fromHMS(8, 0, 0),
151
- arrivalTime: Time.fromHMS(9, 0, 0),
235
+ const vehicleEdge: VehicleEdge = {
236
+ arrival: Time.fromHMS(9, 0, 0),
237
+ from: 0,
238
+ to: 2,
239
+ routeId: 0,
240
+ tripIndex: 0,
152
241
  };
153
242
 
154
- const earliestArrivalsPerRound = [
155
- new Map([
156
- [
157
- 2,
158
- {
159
- arrival: Time.fromHMS(9, 0, 0),
160
- legNumber: 0,
161
- origin: 0,
162
- leg: vehicleLegTo3,
163
- } as TripLeg,
164
- ],
165
- ]),
243
+ const graph: Map<StopId, RoutingEdge>[] = [
244
+ new Map<StopId, RoutingEdge>([[0, { arrival: Time.fromHMS(8, 0, 0) }]]), // Round 0 - origins
245
+ new Map<StopId, RoutingEdge>([[2, vehicleEdge]]), // Round 1
166
246
  ];
167
247
 
168
248
  const result = new Result(
169
249
  mockQuery,
170
- earliestArrivals,
171
- earliestArrivalsPerRound,
250
+ {
251
+ earliestArrivals,
252
+ graph,
253
+ destinations: [2, 3],
254
+ },
172
255
  mockStopsIndex,
256
+ mockTimetable,
173
257
  );
174
258
 
175
259
  const route = result.bestRoute();
176
260
  assert(route);
177
261
  assert.strictEqual(route.legs.length, 1);
178
- assert.deepStrictEqual(route.legs[0], vehicleLegTo3);
262
+ const firstLeg = route.legs[0];
263
+ assert(firstLeg);
264
+ assert.strictEqual(firstLeg.from.id, 0);
265
+ assert.strictEqual(firstLeg.to.id, 2);
179
266
  });
180
267
 
181
268
  it('should return route to fastest child stop when parent stop is queried', () => {
182
- const vehicleLegToChild1 = {
183
- from: stop1,
184
- to: childStop1,
185
- route: { type: 'BUS', name: 'Bus 101' },
186
- departureTime: Time.fromHMS(8, 0, 0),
187
- arrivalTime: Time.fromHMS(9, 0, 0),
188
- };
189
-
190
269
  const earliestArrivals = new Map([
191
- [5, { arrival: Time.fromHMS(9, 0, 0), legNumber: 0, origin: 0 }], // child1 - faster
192
- [6, { arrival: Time.fromHMS(9, 30, 0), legNumber: 0, origin: 0 }], // child2 - slower
270
+ [2, { arrival: Time.fromHMS(9, 10, 0), legNumber: 1 }], // intermediate stop
271
+ [5, { arrival: Time.fromHMS(10, 10, 0), legNumber: 2 }], // child1 - faster
272
+ [6, { arrival: Time.fromHMS(10, 30, 0), legNumber: 2 }], // child2 - slower
193
273
  ]);
194
274
 
195
- const earliestArrivalsPerRound = [
196
- new Map([
275
+ const vehicleEdge: VehicleEdge = {
276
+ arrival: Time.fromHMS(10, 10, 0),
277
+ from: 0,
278
+ to: 2,
279
+ routeId: 1,
280
+ tripIndex: 0,
281
+ };
282
+
283
+ const graph: Map<StopId, RoutingEdge>[] = [
284
+ new Map<StopId, RoutingEdge>([[2, { arrival: Time.fromHMS(8, 0, 0) }]]), // Round 0 - origins
285
+ new Map<StopId, RoutingEdge>([
197
286
  [
198
- 5,
287
+ 2,
199
288
  {
200
- arrival: Time.fromHMS(9, 0, 0),
201
- legNumber: 0,
202
- origin: 0,
203
- leg: vehicleLegToChild1,
204
- } as TripLeg,
289
+ arrival: Time.fromHMS(9, 10, 0),
290
+ from: 0,
291
+ to: 2,
292
+ routeId: 0,
293
+ tripIndex: 0,
294
+ },
205
295
  ],
206
- ]),
296
+ ]), // Round 1
297
+ new Map<StopId, RoutingEdge>([[5, vehicleEdge]]), // Round 2
207
298
  ];
208
299
 
209
300
  const result = new Result(
210
301
  mockQuery,
211
- earliestArrivals,
212
- earliestArrivalsPerRound,
302
+ {
303
+ earliestArrivals,
304
+ graph,
305
+ destinations: [4], // parent stop
306
+ },
213
307
  mockStopsIndex,
308
+ mockTimetable,
214
309
  );
215
310
 
216
311
  const route = result.bestRoute('parent');
217
312
  assert(route);
218
- assert.strictEqual(route.legs.length, 1);
219
- assert.deepStrictEqual(route.legs[0], vehicleLegToChild1);
313
+ assert.strictEqual(route.legs.length, 2);
314
+ const lastLeg = route.legs[route.legs.length - 1];
315
+ assert(lastLeg);
316
+ assert.strictEqual(lastLeg.to.id, 5); // should route to faster child
220
317
  });
221
318
 
222
319
  it('should handle simple single-leg route reconstruction', () => {
223
- const vehicleLeg = {
224
- from: stop1,
225
- to: stop3,
226
- route: { type: 'BUS', name: 'Bus 101' },
227
- departureTime: Time.fromHMS(8, 0, 0),
228
- arrivalTime: Time.fromHMS(9, 0, 0),
320
+ const earliestArrivals = new Map([
321
+ [0, { arrival: Time.fromHMS(8, 0, 0), legNumber: 0 }], // origin
322
+ [2, { arrival: Time.fromHMS(9, 0, 0), legNumber: 1 }], // destination
323
+ ]);
324
+
325
+ const vehicleEdge: VehicleEdge = {
326
+ arrival: Time.fromHMS(9, 0, 0),
327
+ from: 0,
328
+ to: 2,
329
+ routeId: 0,
330
+ tripIndex: 0,
229
331
  };
230
332
 
231
- // Simple case: origin stop 1, destination stop 3, direct connection
333
+ const graph: Map<StopId, RoutingEdge>[] = [
334
+ new Map<StopId, RoutingEdge>([[0, { arrival: Time.fromHMS(8, 0, 0) }]]), // Round 0 - origins
335
+ new Map<StopId, RoutingEdge>([[2, vehicleEdge]]), // Round 1
336
+ ];
337
+
338
+ const result = new Result(
339
+ mockQuery,
340
+ {
341
+ earliestArrivals,
342
+ graph,
343
+ destinations: [2],
344
+ },
345
+ mockStopsIndex,
346
+ mockTimetable,
347
+ );
348
+
349
+ const route = result.bestRoute('stop3');
350
+ assert(route);
351
+ assert.strictEqual(route.legs.length, 1);
352
+ const firstLeg = route.legs[0];
353
+ assert(firstLeg);
354
+ assert.strictEqual(firstLeg.from.id, 0);
355
+ assert.strictEqual(firstLeg.to.id, 2);
356
+ });
357
+
358
+ it('should handle multi-leg route with transfer', () => {
232
359
  const earliestArrivals = new Map([
233
- [2, { arrival: Time.fromHMS(9, 0, 0), legNumber: 0, origin: 0 }],
360
+ [0, { arrival: Time.fromHMS(8, 0, 0), legNumber: 0 }], // origin
361
+ [2, { arrival: Time.fromHMS(9, 0, 0), legNumber: 1 }], // intermediate stop
362
+ [3, { arrival: Time.fromHMS(9, 45, 0), legNumber: 2 }], // final destination
234
363
  ]);
235
364
 
236
- const earliestArrivalsPerRound = [
237
- new Map([
238
- [
239
- 2,
240
- {
241
- arrival: Time.fromHMS(9, 0, 0),
242
- legNumber: 0,
243
- origin: 0,
244
- leg: vehicleLeg,
245
- } as TripLeg,
246
- ],
247
- ]),
365
+ const firstVehicleEdge: VehicleEdge = {
366
+ arrival: Time.fromHMS(9, 0, 0),
367
+ from: 0,
368
+ to: 2,
369
+ routeId: 0,
370
+ tripIndex: 0,
371
+ };
372
+
373
+ const secondVehicleEdge: VehicleEdge = {
374
+ arrival: Time.fromHMS(9, 45, 0),
375
+ from: 0,
376
+ to: 1,
377
+ routeId: 1,
378
+ tripIndex: 0,
379
+ };
380
+
381
+ const graph: Map<StopId, RoutingEdge>[] = [
382
+ new Map<StopId, RoutingEdge>([[0, { arrival: Time.fromHMS(8, 0, 0) }]]), // Round 0 - origins
383
+ new Map<StopId, RoutingEdge>([[2, firstVehicleEdge]]), // Round 1
384
+ new Map<StopId, RoutingEdge>([[3, secondVehicleEdge]]), // Round 2
248
385
  ];
249
386
 
250
387
  const result = new Result(
251
388
  mockQuery,
252
- earliestArrivals,
253
- earliestArrivalsPerRound,
389
+ {
390
+ earliestArrivals,
391
+ graph,
392
+ destinations: [3],
393
+ },
254
394
  mockStopsIndex,
395
+ mockTimetable,
396
+ );
397
+
398
+ const route = result.bestRoute('stop4');
399
+ assert(route);
400
+ assert.strictEqual(route.legs.length, 2); // two vehicle legs (transfer is implicit in route change)
401
+ const firstLeg = route.legs[0];
402
+ const secondLeg = route.legs[1];
403
+ assert(firstLeg);
404
+ assert(secondLeg);
405
+ assert.strictEqual(firstLeg.from.id, 0);
406
+ assert.strictEqual(firstLeg.to.id, 2);
407
+ assert.strictEqual(secondLeg.from.id, 2);
408
+ assert.strictEqual(secondLeg.to.id, 3);
409
+ });
410
+ });
411
+
412
+ describe('continuous trips', () => {
413
+ it('should handle single continuous trip correctly', () => {
414
+ const earliestArrivals = new Map([
415
+ [0, { arrival: Time.fromHMS(8, 0, 0), legNumber: 0 }], // origin
416
+ [1, { arrival: Time.fromHMS(8, 30, 0), legNumber: 1 }], // intermediate stop
417
+ [2, { arrival: Time.fromHMS(9, 0, 0), legNumber: 1 }], // final destination via continuous trip
418
+ ]);
419
+
420
+ const firstVehicleEdge: VehicleEdge = {
421
+ arrival: Time.fromHMS(8, 30, 0),
422
+ from: 0,
423
+ to: 1,
424
+ routeId: 0,
425
+ tripIndex: 0,
426
+ };
427
+
428
+ const continuousVehicleEdge: VehicleEdge = {
429
+ arrival: Time.fromHMS(9, 0, 0),
430
+ from: 1,
431
+ to: 2,
432
+ routeId: 0,
433
+ tripIndex: 0,
434
+ continuationOf: firstVehicleEdge,
435
+ };
436
+
437
+ const graph: Map<StopId, RoutingEdge>[] = [
438
+ new Map<StopId, RoutingEdge>([[0, { arrival: Time.fromHMS(8, 0, 0) }]]), // Round 0 - origins
439
+ new Map<StopId, RoutingEdge>([
440
+ [1, firstVehicleEdge],
441
+ [2, continuousVehicleEdge],
442
+ ]), // Round 1
443
+ ];
444
+
445
+ const result = new Result(
446
+ mockQuery,
447
+ {
448
+ earliestArrivals,
449
+ graph,
450
+ destinations: [2],
451
+ },
452
+ mockStopsIndex,
453
+ mockTimetable,
255
454
  );
256
455
 
257
456
  const route = result.bestRoute('stop3');
258
457
  assert(route);
259
458
  assert.strictEqual(route.legs.length, 1);
260
- assert.deepStrictEqual(route.legs[0], vehicleLeg);
459
+ const leg = route.legs[0];
460
+ assert(leg);
461
+ assert.strictEqual(leg.from.id, 0);
462
+ assert.strictEqual(leg.to.id, 2);
463
+ });
464
+
465
+ it('should handle continuous trips with route change mid-journey', () => {
466
+ const earliestArrivals = new Map([
467
+ [0, { arrival: Time.fromHMS(8, 0, 0), legNumber: 0 }], // origin
468
+ [3, { arrival: Time.fromHMS(9, 45, 0), legNumber: 1 }], // destination via continuous trip to route 1
469
+ ]);
470
+
471
+ const firstVehicleEdge: VehicleEdge = {
472
+ arrival: Time.fromHMS(9, 0, 0),
473
+ from: 0,
474
+ to: 2,
475
+ routeId: 0,
476
+ tripIndex: 0,
477
+ };
478
+
479
+ const continuousVehicleEdge: VehicleEdge = {
480
+ arrival: Time.fromHMS(9, 45, 0),
481
+ from: 0,
482
+ to: 1,
483
+ routeId: 1,
484
+ tripIndex: 0,
485
+ continuationOf: firstVehicleEdge,
486
+ };
487
+
488
+ const graph: Map<StopId, RoutingEdge>[] = [
489
+ new Map<StopId, RoutingEdge>([[0, { arrival: Time.fromHMS(8, 0, 0) }]]), // Round 0 - origins
490
+ new Map<StopId, RoutingEdge>([[3, continuousVehicleEdge]]), // Round 1
491
+ ];
492
+
493
+ const result = new Result(
494
+ mockQuery,
495
+ {
496
+ earliestArrivals,
497
+ graph,
498
+ destinations: [3],
499
+ },
500
+ mockStopsIndex,
501
+ mockTimetable,
502
+ );
503
+
504
+ const route = result.bestRoute('stop4');
505
+ assert(route);
506
+ assert.strictEqual(route.legs.length, 1);
507
+
508
+ const leg = route.legs[0];
509
+ assert(leg);
510
+ assert.strictEqual(leg.from.id, 0);
511
+ assert.strictEqual(leg.to.id, 3);
512
+ });
513
+ it('should handle route reconstruction with actual transfer edges', () => {
514
+ const earliestArrivals = new Map([
515
+ [0, { arrival: Time.fromHMS(8, 0, 0), legNumber: 0 }], // origin
516
+ [1, { arrival: Time.fromHMS(8, 30, 0), legNumber: 1 }], // first vehicle leg destination
517
+ [2, { arrival: Time.fromHMS(8, 35, 0), legNumber: 1 }], // after transfer (same round as transfer doesn't advance round)
518
+ [3, { arrival: Time.fromHMS(9, 15, 0), legNumber: 2 }], // final destination
519
+ ]);
520
+
521
+ const firstVehicleEdge: VehicleEdge = {
522
+ arrival: Time.fromHMS(8, 30, 0),
523
+ from: 0,
524
+ to: 1,
525
+ routeId: 0,
526
+ tripIndex: 0,
527
+ };
528
+ const transferEdge: TransferEdge = {
529
+ arrival: Time.fromHMS(8, 35, 0),
530
+ from: 1,
531
+ to: 2,
532
+ type: 'RECOMMENDED',
533
+ minTransferTime: Duration.fromMinutes(5),
534
+ };
535
+ const secondVehicleEdge: VehicleEdge = {
536
+ arrival: Time.fromHMS(9, 15, 0),
537
+ from: 0,
538
+ to: 1,
539
+ routeId: 1,
540
+ tripIndex: 0,
541
+ };
542
+
543
+ const graph: Map<StopId, RoutingEdge>[] = [
544
+ new Map<StopId, RoutingEdge>([[0, { arrival: Time.fromHMS(8, 0, 0) }]]), // Round 0 - origins
545
+ new Map<StopId, RoutingEdge>([
546
+ [1, firstVehicleEdge], // First vehicle leg
547
+ [2, transferEdge], // Transfer happens in same round as vehicle leg
548
+ ]), // Round 1
549
+ new Map<StopId, RoutingEdge>([[3, secondVehicleEdge]]), // Round 2 - second vehicle leg
550
+ ];
551
+
552
+ const result = new Result(
553
+ mockQuery,
554
+ {
555
+ earliestArrivals,
556
+ graph,
557
+ destinations: [3],
558
+ },
559
+ mockStopsIndex,
560
+ mockTimetable,
561
+ );
562
+
563
+ const route = result.bestRoute('stop4');
564
+ assert(route);
565
+
566
+ assert.strictEqual(
567
+ route.legs.length,
568
+ 3,
569
+ 'Route should have vehicle + transfer + vehicle legs',
570
+ );
571
+
572
+ const firstLeg = route.legs[0];
573
+ const transferLeg = route.legs[1];
574
+ const thirdLeg = route.legs[2];
575
+
576
+ assert(firstLeg);
577
+ assert(transferLeg);
578
+ assert(thirdLeg);
579
+
580
+ assert.strictEqual(firstLeg.from.id, 0);
581
+ assert.strictEqual(firstLeg.to.id, 1);
582
+ assert('departureTime' in firstLeg);
583
+ assert('route' in firstLeg);
584
+
585
+ assert.strictEqual(transferLeg.from.id, 1);
586
+ assert.strictEqual(transferLeg.to.id, 2);
587
+ assert('type' in transferLeg);
588
+ assert('minTransferTime' in transferLeg);
589
+ assert.strictEqual(transferLeg.type, 'RECOMMENDED');
590
+
591
+ assert.strictEqual(thirdLeg.from.id, 2);
592
+ assert.strictEqual(thirdLeg.to.id, 3);
593
+ assert('departureTime' in thirdLeg);
594
+ assert('route' in thirdLeg);
595
+
596
+ assert.strictEqual(earliestArrivals.get(1)?.legNumber, 1);
597
+ assert.strictEqual(earliestArrivals.get(2)?.legNumber, 1);
598
+ assert.strictEqual(earliestArrivals.get(3)?.legNumber, 2);
261
599
  });
262
600
  });
263
601
 
@@ -266,15 +604,19 @@ describe('Result', () => {
266
604
  const arrivalTime = {
267
605
  arrival: Time.fromHMS(9, 0, 0),
268
606
  legNumber: 1,
269
- origin: 0,
270
607
  };
271
608
  const earliestArrivals = new Map([[2, arrivalTime]]);
609
+ const graph: Map<StopId, RoutingEdge>[] = [];
272
610
 
273
611
  const result = new Result(
274
612
  mockQuery,
275
- earliestArrivals,
276
- [],
613
+ {
614
+ earliestArrivals,
615
+ graph,
616
+ destinations: [2],
617
+ },
277
618
  mockStopsIndex,
619
+ mockTimetable,
278
620
  );
279
621
 
280
622
  const arrival = result.arrivalAt('stop3');
@@ -283,14 +625,19 @@ describe('Result', () => {
283
625
 
284
626
  it('should return undefined for unreachable stop', () => {
285
627
  const earliestArrivals = new Map([
286
- [2, { arrival: Time.fromHMS(9, 0, 0), legNumber: 1, origin: 0 }],
628
+ [2, { arrival: Time.fromHMS(9, 0, 0), legNumber: 1 }],
287
629
  ]);
630
+ const graph: Map<StopId, RoutingEdge>[] = [];
288
631
 
289
632
  const result = new Result(
290
633
  mockQuery,
291
- earliestArrivals,
292
- [],
634
+ {
635
+ earliestArrivals,
636
+ graph,
637
+ destinations: [2],
638
+ },
293
639
  mockStopsIndex,
640
+ mockTimetable,
294
641
  );
295
642
 
296
643
  const arrival = result.arrivalAt('stop4');
@@ -301,24 +648,27 @@ describe('Result', () => {
301
648
  const earlierArrival = {
302
649
  arrival: Time.fromHMS(9, 0, 0),
303
650
  legNumber: 1,
304
- origin: 0,
305
651
  };
306
652
  const laterArrival = {
307
653
  arrival: Time.fromHMS(9, 30, 0),
308
654
  legNumber: 1,
309
- origin: 0,
310
655
  };
311
656
 
312
657
  const earliestArrivals = new Map([
313
- [6, earlierArrival], // child1 - faster
314
- [7, laterArrival], // child2 - slower
658
+ [5, earlierArrival], // child1 - faster
659
+ [6, laterArrival], // child2 - slower
315
660
  ]);
661
+ const graph: Map<StopId, RoutingEdge>[] = [];
316
662
 
317
663
  const result = new Result(
318
664
  mockQuery,
319
- earliestArrivals,
320
- [],
665
+ {
666
+ earliestArrivals,
667
+ graph,
668
+ destinations: [4], // parent stop
669
+ },
321
670
  mockStopsIndex,
671
+ mockTimetable,
322
672
  );
323
673
 
324
674
  const arrival = result.arrivalAt('parent');
@@ -326,63 +676,74 @@ describe('Result', () => {
326
676
  });
327
677
 
328
678
  it('should respect maxTransfers constraint', () => {
329
- const tripLeg1 = {
330
- arrival: Time.fromHMS(8, 30, 0),
331
- legNumber: 0,
332
- origin: 0,
333
- };
334
- const tripLeg2 = {
335
- arrival: Time.fromHMS(9, 0, 0),
679
+ const directArrival = {
680
+ arrival: Time.fromHMS(9, 30, 0),
336
681
  legNumber: 1,
337
- origin: 0,
338
682
  };
339
- const tripLeg3 = {
340
- arrival: Time.fromHMS(9, 30, 0),
683
+ const transferArrival = {
684
+ arrival: Time.fromHMS(9, 0, 0),
341
685
  legNumber: 2,
342
- origin: 0,
343
686
  };
344
687
 
345
688
  const earliestArrivals = new Map([
346
- [2, tripLeg2], // 1 transfer
689
+ [2, transferArrival], // Best overall arrival with transfer
347
690
  ]);
348
691
 
349
- const earliestArrivalsPerRound = [
350
- new Map([[2, tripLeg1]]), // Round 0 (start)
351
- new Map([[2, tripLeg3]]), // Round 1 (no transfers)
352
- new Map([[2, tripLeg2]]), // Round 2 (1 transfers)
692
+ const vehicleEdge1: VehicleEdge = {
693
+ arrival: Time.fromHMS(9, 30, 0),
694
+ from: 0,
695
+ to: 2,
696
+ routeId: 0,
697
+ tripIndex: 0,
698
+ };
699
+
700
+ const vehicleEdge2: VehicleEdge = {
701
+ arrival: Time.fromHMS(9, 45, 0),
702
+ from: 0,
703
+ to: 1,
704
+ routeId: 1,
705
+ tripIndex: 0,
706
+ };
707
+
708
+ const graph: Map<StopId, RoutingEdge>[] = [
709
+ new Map<StopId, RoutingEdge>([[0, { arrival: Time.fromHMS(8, 0, 0) }]]), // Round 0 - origins
710
+ new Map<StopId, RoutingEdge>([[2, vehicleEdge1]]), // Round 1 - direct route (no transfers)
711
+ new Map<StopId, RoutingEdge>([[2, vehicleEdge2]]), // Round 2 - route with 1 transfer
353
712
  ];
354
713
 
355
714
  const result = new Result(
356
715
  mockQuery,
357
- earliestArrivals,
358
- earliestArrivalsPerRound,
716
+ {
717
+ earliestArrivals,
718
+ graph,
719
+ destinations: [2],
720
+ },
359
721
  mockStopsIndex,
722
+ mockTimetable,
360
723
  );
361
724
 
362
725
  const arrivalWithLimit = result.arrivalAt('stop3', 0);
363
- assert.deepStrictEqual(arrivalWithLimit, tripLeg3);
726
+ assert.deepStrictEqual(arrivalWithLimit, directArrival);
364
727
 
365
- const arrivalWithoutLimit = result.arrivalAt('stop3', 1);
366
- assert.deepStrictEqual(arrivalWithoutLimit, tripLeg2);
728
+ const arrivalWithoutLimit = result.arrivalAt('stop3');
729
+ assert.deepStrictEqual(arrivalWithoutLimit, transferArrival);
367
730
  });
368
731
 
369
732
  it('should handle non-existent stops', () => {
370
733
  const earliestArrivals = new Map([
371
- [
372
- 2,
373
- {
374
- arrival: Time.fromHMS(9, 0, 0),
375
- legNumber: 1,
376
- origin: 0,
377
- } as ReachingTime,
378
- ],
734
+ [2, { arrival: Time.fromHMS(9, 0, 0), legNumber: 1 }],
379
735
  ]);
736
+ const graph: Map<StopId, RoutingEdge>[] = [];
380
737
 
381
738
  const result = new Result(
382
739
  mockQuery,
383
- earliestArrivals,
384
- [],
740
+ {
741
+ earliestArrivals,
742
+ graph,
743
+ destinations: [2],
744
+ },
385
745
  mockStopsIndex,
746
+ mockTimetable,
386
747
  );
387
748
 
388
749
  const arrival = result.arrivalAt('nonexistent');