minotor 11.1.2 → 11.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/.cspell.json +7 -1
  2. package/CHANGELOG.md +3 -3
  3. package/README.md +111 -86
  4. package/dist/cli/perf.d.ts +57 -18
  5. package/dist/cli.mjs +1371 -342
  6. package/dist/cli.mjs.map +1 -1
  7. package/dist/parser.cjs.js +57 -4
  8. package/dist/parser.cjs.js.map +1 -1
  9. package/dist/parser.esm.js +57 -4
  10. package/dist/parser.esm.js.map +1 -1
  11. package/dist/router.cjs.js +1 -1
  12. package/dist/router.cjs.js.map +1 -1
  13. package/dist/router.d.ts +5 -5
  14. package/dist/router.esm.js +1 -1
  15. package/dist/router.esm.js.map +1 -1
  16. package/dist/router.umd.js +1 -1
  17. package/dist/router.umd.js.map +1 -1
  18. package/dist/routing/__tests__/access.test.d.ts +1 -0
  19. package/dist/routing/__tests__/plainRouter.test.d.ts +1 -0
  20. package/dist/routing/__tests__/rangeResult.test.d.ts +1 -0
  21. package/dist/routing/__tests__/rangeRouter.test.d.ts +1 -0
  22. package/dist/routing/__tests__/rangeState.test.d.ts +1 -0
  23. package/dist/routing/__tests__/raptor.test.d.ts +1 -0
  24. package/dist/routing/__tests__/state.test.d.ts +1 -0
  25. package/dist/routing/access.d.ts +55 -0
  26. package/dist/routing/plainRouter.d.ts +21 -0
  27. package/dist/routing/plotter.d.ts +9 -0
  28. package/dist/routing/query.d.ts +132 -13
  29. package/dist/routing/rangeResult.d.ts +155 -0
  30. package/dist/routing/rangeRouter.d.ts +24 -0
  31. package/dist/routing/rangeState.d.ts +83 -0
  32. package/dist/routing/raptor.d.ts +96 -0
  33. package/dist/routing/result.d.ts +27 -7
  34. package/dist/routing/route.d.ts +5 -21
  35. package/dist/routing/router.d.ts +20 -91
  36. package/dist/routing/state.d.ts +92 -17
  37. package/dist/timetable/route.d.ts +8 -0
  38. package/dist/timetable/timetable.d.ts +17 -1
  39. package/package.json +1 -1
  40. package/src/__e2e__/benchmark.json +18 -0
  41. package/src/__e2e__/router.test.ts +461 -127
  42. package/src/cli/minotor.ts +39 -3
  43. package/src/cli/perf.ts +324 -60
  44. package/src/cli/repl.ts +96 -41
  45. package/src/router.ts +11 -3
  46. package/src/routing/__tests__/access.test.ts +294 -0
  47. package/src/routing/__tests__/plainRouter.test.ts +1633 -0
  48. package/src/routing/__tests__/plotter.test.ts +8 -8
  49. package/src/routing/__tests__/rangeResult.test.ts +273 -0
  50. package/src/routing/__tests__/rangeRouter.test.ts +472 -0
  51. package/src/routing/__tests__/rangeState.test.ts +246 -0
  52. package/src/routing/__tests__/raptor.test.ts +366 -0
  53. package/src/routing/__tests__/result.test.ts +27 -27
  54. package/src/routing/__tests__/route.test.ts +28 -0
  55. package/src/routing/__tests__/router.test.ts +75 -1587
  56. package/src/routing/__tests__/state.test.ts +78 -0
  57. package/src/routing/access.ts +144 -0
  58. package/src/routing/plainRouter.ts +60 -0
  59. package/src/routing/plotter.ts +53 -6
  60. package/src/routing/query.ts +116 -13
  61. package/src/routing/rangeResult.ts +292 -0
  62. package/src/routing/rangeRouter.ts +167 -0
  63. package/src/routing/rangeState.ts +150 -0
  64. package/src/routing/raptor.ts +416 -0
  65. package/src/routing/result.ts +68 -26
  66. package/src/routing/route.ts +15 -53
  67. package/src/routing/router.ts +40 -480
  68. package/src/routing/state.ts +191 -32
  69. package/src/timetable/__tests__/timetable.test.ts +373 -0
  70. package/src/timetable/route.ts +16 -4
  71. package/src/timetable/timetable.ts +54 -1
@@ -0,0 +1,366 @@
1
+ /* eslint-disable @typescript-eslint/no-non-null-assertion */
2
+ import assert from 'node:assert';
3
+ import { describe, it } from 'node:test';
4
+
5
+ import { Route } from '../../timetable/route.js';
6
+ import { timeFromHM } from '../../timetable/time.js';
7
+ import {
8
+ ALL_TRANSPORT_MODES,
9
+ ServiceRoute,
10
+ StopAdjacency,
11
+ Timetable,
12
+ TripTransfers,
13
+ } from '../../timetable/timetable.js';
14
+ import { encode } from '../../timetable/tripStopId.js';
15
+ import { QueryOptions } from '../query.js';
16
+ import { Raptor } from '../raptor.js';
17
+ import { RoutingState } from '../state.js';
18
+
19
+ // ─── Base fixture ─────────────────────────────────────────────────────────────
20
+ // 3 stops: 0 (origin), 1 (transfer stop), 2 (destination)
21
+ // Route 0 (BUS): 0→1 depart 08:10 / arrive 08:30
22
+ // Route 1 (BUS): 1→2 depart 08:35 / arrive 08:50
23
+ // Stop adjacency: stop 0 → [0], stop 1 → [0, 1], stop 2 → [1]
24
+
25
+ const NB_STOPS = 3;
26
+
27
+ const route0 = Route.of({
28
+ id: 0,
29
+ serviceRouteId: 0,
30
+ trips: [
31
+ {
32
+ stops: [
33
+ {
34
+ id: 0,
35
+ arrivalTime: timeFromHM(8, 10),
36
+ departureTime: timeFromHM(8, 10),
37
+ },
38
+ {
39
+ id: 1,
40
+ arrivalTime: timeFromHM(8, 30),
41
+ departureTime: timeFromHM(8, 30),
42
+ },
43
+ ],
44
+ },
45
+ ],
46
+ });
47
+
48
+ const route1 = Route.of({
49
+ id: 1,
50
+ serviceRouteId: 1,
51
+ trips: [
52
+ {
53
+ stops: [
54
+ {
55
+ id: 1,
56
+ arrivalTime: timeFromHM(8, 35),
57
+ departureTime: timeFromHM(8, 35),
58
+ },
59
+ {
60
+ id: 2,
61
+ arrivalTime: timeFromHM(8, 50),
62
+ departureTime: timeFromHM(8, 50),
63
+ },
64
+ ],
65
+ },
66
+ ],
67
+ });
68
+
69
+ const stopsAdjacency: StopAdjacency[] = [
70
+ { routes: [0] }, // stop 0: origin
71
+ { routes: [0, 1] }, // stop 1: transfer stop
72
+ { routes: [1] }, // stop 2: destination
73
+ ];
74
+
75
+ const serviceRoutes: ServiceRoute[] = [
76
+ { type: 'BUS', name: 'Route 0', routes: [0] },
77
+ { type: 'BUS', name: 'Route 1', routes: [1] },
78
+ ];
79
+
80
+ // ─── Extended fixture: slower direct route 0→2 ────────────────────────────────
81
+ // Route 2 (BUS): 0→2 depart 08:10 / arrive 09:00
82
+
83
+ const route2 = Route.of({
84
+ id: 2,
85
+ serviceRouteId: 2,
86
+ trips: [
87
+ {
88
+ stops: [
89
+ {
90
+ id: 0,
91
+ arrivalTime: timeFromHM(8, 10),
92
+ departureTime: timeFromHM(8, 10),
93
+ },
94
+ {
95
+ id: 2,
96
+ arrivalTime: timeFromHM(9, 0),
97
+ departureTime: timeFromHM(9, 0),
98
+ },
99
+ ],
100
+ },
101
+ ],
102
+ });
103
+
104
+ const stopsAdjacencyWithDirectRoute: StopAdjacency[] = [
105
+ { routes: [0, 2] }, // stop 0
106
+ { routes: [0, 1] }, // stop 1
107
+ { routes: [1, 2] }, // stop 2
108
+ ];
109
+
110
+ const serviceRoutesWithDirectRoute: ServiceRoute[] = [
111
+ { type: 'BUS', name: 'Route 0', routes: [0] },
112
+ { type: 'BUS', name: 'Route 1', routes: [1] },
113
+ { type: 'BUS', name: 'Route 2', routes: [2] },
114
+ ];
115
+
116
+ // ─── Extended fixture: walking transfer from stop 1 to stop 2 ─────────────────
117
+ // Stop 2 has no routes; can only be reached via the 5-minute walk from stop 1.
118
+
119
+ const stopsAdjacencyWithTransfer: StopAdjacency[] = [
120
+ { routes: [0] },
121
+ {
122
+ routes: [0],
123
+ transfers: [
124
+ { destination: 2, type: 'REQUIRES_MINIMAL_TIME', minTransferTime: 5 },
125
+ ],
126
+ },
127
+ { routes: [] },
128
+ ];
129
+
130
+ // ─── Extended fixture: mixed transport modes ──────────────────────────────────
131
+ // Route 0 is BUS, route 1 is RAIL — used to verify mode filtering.
132
+
133
+ const mixedModeServiceRoutes: ServiceRoute[] = [
134
+ { type: 'BUS', name: 'Route 0', routes: [0] },
135
+ { type: 'RAIL', name: 'Route 1', routes: [1] },
136
+ ];
137
+
138
+ // ─── Extended fixture: in-seat trip continuation ──────────────────────────────
139
+ // Encodes: at stop-index 1 of route 0, trip 0 → continue as route 1, trip 0,
140
+ // boarding at stop-index 0.
141
+
142
+ const tripContinuations: TripTransfers = new Map([
143
+ [encode(1, 0, 0), [{ stopIndex: 0, routeId: 1, tripIndex: 0 }]],
144
+ ]);
145
+
146
+ // ─────────────────────────────────────────────────────────────────────────────
147
+
148
+ describe('Raptor', () => {
149
+ describe('route scanning', () => {
150
+ it('marks the destination with the correct arrival time', () => {
151
+ const timetable = new Timetable(
152
+ stopsAdjacency,
153
+ [route0, route1],
154
+ serviceRoutes,
155
+ );
156
+ const state = new RoutingState(
157
+ timeFromHM(8, 0),
158
+ [1],
159
+ [{ fromStopId: 0, toStopId: 0, duration: 0 }],
160
+ NB_STOPS,
161
+ 6,
162
+ );
163
+ const raptor = new Raptor(timetable);
164
+ const options: QueryOptions = {
165
+ maxTransfers: 5,
166
+ minTransferTime: 2,
167
+ transportModes: ALL_TRANSPORT_MODES,
168
+ };
169
+ raptor.run(options, state);
170
+ assert.strictEqual(state.getArrival(1)?.arrival, timeFromHM(8, 30));
171
+ });
172
+
173
+ it('finds a two-leg journey via a route change', () => {
174
+ const timetable = new Timetable(
175
+ stopsAdjacency,
176
+ [route0, route1],
177
+ serviceRoutes,
178
+ );
179
+ const state = new RoutingState(
180
+ timeFromHM(8, 0),
181
+ [2],
182
+ [{ fromStopId: 0, toStopId: 0, duration: 0 }],
183
+ NB_STOPS,
184
+ 6,
185
+ );
186
+ const raptor = new Raptor(timetable);
187
+ const options: QueryOptions = {
188
+ maxTransfers: 5,
189
+ minTransferTime: 2,
190
+ transportModes: ALL_TRANSPORT_MODES,
191
+ };
192
+ raptor.run(options, state);
193
+ assert.strictEqual(state.getArrival(1)?.arrival, timeFromHM(8, 30));
194
+ assert.strictEqual(state.getArrival(1)?.legNumber, 1);
195
+ assert.strictEqual(state.getArrival(2)?.arrival, timeFromHM(8, 50));
196
+ assert.strictEqual(state.getArrival(2)?.legNumber, 2);
197
+ });
198
+
199
+ it('prefers the faster two-leg route over a slower direct route', () => {
200
+ const timetable = new Timetable(
201
+ stopsAdjacencyWithDirectRoute,
202
+ [route0, route1, route2],
203
+ serviceRoutesWithDirectRoute,
204
+ );
205
+ const state = new RoutingState(
206
+ timeFromHM(8, 0),
207
+ [2],
208
+ [{ fromStopId: 0, toStopId: 0, duration: 0 }],
209
+ NB_STOPS,
210
+ 6,
211
+ );
212
+ const raptor = new Raptor(timetable);
213
+ const options: QueryOptions = {
214
+ maxTransfers: 5,
215
+ minTransferTime: 2,
216
+ transportModes: ALL_TRANSPORT_MODES,
217
+ };
218
+ raptor.run(options, state);
219
+ // The 0→1→2 journey arrives at 08:50; the direct 0→2 route arrives at 09:00.
220
+ assert.strictEqual(state.getArrival(2)?.arrival, timeFromHM(8, 50));
221
+ });
222
+ });
223
+
224
+ describe('maxTransfers', () => {
225
+ it('stops after maxTransfers+1 rounds', () => {
226
+ // maxTransfers=0 means only round 1 runs: stop 1 is reachable (1 leg),
227
+ // but stop 2 requires a second round and must remain unreached.
228
+ const timetable = new Timetable(
229
+ stopsAdjacency,
230
+ [route0, route1],
231
+ serviceRoutes,
232
+ );
233
+ const state = new RoutingState(
234
+ timeFromHM(8, 0),
235
+ [2],
236
+ [{ fromStopId: 0, toStopId: 0, duration: 0 }],
237
+ NB_STOPS,
238
+ 1, // maxRounds = maxTransfers + 1
239
+ );
240
+ const raptor = new Raptor(timetable);
241
+ const options: QueryOptions = {
242
+ maxTransfers: 0,
243
+ minTransferTime: 2,
244
+ transportModes: ALL_TRANSPORT_MODES,
245
+ };
246
+ raptor.run(options, state);
247
+ assert.strictEqual(state.getArrival(1)?.arrival, timeFromHM(8, 30));
248
+ assert.strictEqual(state.getArrival(2), undefined);
249
+ });
250
+ });
251
+
252
+ describe('early termination', () => {
253
+ it('exits when no trips are catchable', () => {
254
+ // Departing at 09:00 — all trips have already left (route 0 at 08:10,
255
+ // route 1 at 08:35). No stop should be marked.
256
+ const timetable = new Timetable(
257
+ stopsAdjacency,
258
+ [route0, route1],
259
+ serviceRoutes,
260
+ );
261
+ const state = new RoutingState(
262
+ timeFromHM(9, 0),
263
+ [2],
264
+ [{ fromStopId: 0, toStopId: 0, duration: 0 }],
265
+ NB_STOPS,
266
+ 6,
267
+ );
268
+ const raptor = new Raptor(timetable);
269
+ const options: QueryOptions = {
270
+ maxTransfers: 5,
271
+ minTransferTime: 2,
272
+ transportModes: ALL_TRANSPORT_MODES,
273
+ };
274
+ raptor.run(options, state);
275
+ assert.strictEqual(state.getArrival(1), undefined);
276
+ assert.strictEqual(state.getArrival(2), undefined);
277
+ });
278
+ });
279
+
280
+ describe('walking transfers', () => {
281
+ it('marks stops reached only via a timed walk', () => {
282
+ // Only route 0 exists (0→1). Stop 2 has no routes but is reachable from
283
+ // stop 1 via a 5-minute REQUIRES_MINIMAL_TIME transfer.
284
+ // Expected: stop 2 arrival = 08:30 + 5 = 08:35.
285
+ const timetable = new Timetable(
286
+ stopsAdjacencyWithTransfer,
287
+ [route0],
288
+ [serviceRoutes[0]!],
289
+ );
290
+ const state = new RoutingState(
291
+ timeFromHM(8, 0),
292
+ [2],
293
+ [{ fromStopId: 0, toStopId: 0, duration: 0 }],
294
+ NB_STOPS,
295
+ 6,
296
+ );
297
+ const raptor = new Raptor(timetable);
298
+ const options: QueryOptions = {
299
+ maxTransfers: 5,
300
+ minTransferTime: 2,
301
+ transportModes: ALL_TRANSPORT_MODES,
302
+ };
303
+ raptor.run(options, state);
304
+ assert.strictEqual(state.getArrival(2)?.arrival, timeFromHM(8, 35));
305
+ });
306
+ });
307
+
308
+ describe('transport mode filtering', () => {
309
+ it('skips routes of excluded mode', () => {
310
+ // Route 0 is BUS; route 1 is RAIL. When only RAIL is allowed, route 0
311
+ // is filtered out, stop 0 has no eligible route, and stop 1 is never
312
+ // reached — so neither stop 1 nor stop 2 should be marked.
313
+ const timetable = new Timetable(
314
+ stopsAdjacency,
315
+ [route0, route1],
316
+ mixedModeServiceRoutes,
317
+ );
318
+ const state = new RoutingState(
319
+ timeFromHM(8, 0),
320
+ [2],
321
+ [{ fromStopId: 0, toStopId: 0, duration: 0 }],
322
+ NB_STOPS,
323
+ 6,
324
+ );
325
+ const raptor = new Raptor(timetable);
326
+ const options: QueryOptions = {
327
+ maxTransfers: 5,
328
+ minTransferTime: 2,
329
+ transportModes: new Set(['RAIL']),
330
+ };
331
+ raptor.run(options, state);
332
+ assert.strictEqual(state.getArrival(1), undefined);
333
+ });
334
+ });
335
+
336
+ describe('in-seat transfer continuations', () => {
337
+ it('reaches a stop in the same round via a continuation', () => {
338
+ // Continuation: route 0, trip 0, hop-off stop-index 1 → route 1, trip 0,
339
+ // boarding at stop-index 0. Because the continuation is processed within
340
+ // round 1 (no extra round needed), stop 2 should have legNumber === 1
341
+ // instead of the legNumber === 2 that a normal two-round journey produces.
342
+ const timetable = new Timetable(
343
+ stopsAdjacency,
344
+ [route0, route1],
345
+ serviceRoutes,
346
+ tripContinuations,
347
+ );
348
+ const state = new RoutingState(
349
+ timeFromHM(8, 0),
350
+ [2],
351
+ [{ fromStopId: 0, toStopId: 0, duration: 0 }],
352
+ NB_STOPS,
353
+ 6,
354
+ );
355
+ const raptor = new Raptor(timetable);
356
+ const options: QueryOptions = {
357
+ maxTransfers: 5,
358
+ minTransferTime: 2,
359
+ transportModes: ALL_TRANSPORT_MODES,
360
+ };
361
+ raptor.run(options, state);
362
+ assert.strictEqual(state.getArrival(2)?.arrival, timeFromHM(8, 50));
363
+ assert.strictEqual(state.getArrival(2)?.legNumber, 1);
364
+ });
365
+ });
366
+ });
@@ -179,7 +179,7 @@ describe('Result', () => {
179
179
  describe('bestRoute', () => {
180
180
  it('should return undefined when no route exists', () => {
181
181
  const result = new Result(
182
- mockQuery,
182
+ mockQuery.to,
183
183
  RoutingState.fromTestData({ nbStops: NB_STOPS, destinations: [2, 3] }),
184
184
  mockStopsIndex,
185
185
  mockTimetable,
@@ -191,13 +191,13 @@ describe('Result', () => {
191
191
 
192
192
  it('should return undefined for unreachable destination', () => {
193
193
  const result = new Result(
194
- mockQuery,
194
+ mockQuery.to,
195
195
  RoutingState.fromTestData({
196
196
  nbStops: NB_STOPS,
197
197
  origins: [0],
198
198
  destinations: [2, 3],
199
199
  arrivals: [[1, timeFromHMS(8, 30, 0), 0]],
200
- graph: [[[0, { arrival: timeFromHMS(8, 0, 0) }]]],
200
+ graph: [[[0, { stopId: 0, arrival: timeFromHMS(8, 0, 0) }]]],
201
201
  }),
202
202
  mockStopsIndex,
203
203
  mockTimetable,
@@ -217,7 +217,7 @@ describe('Result', () => {
217
217
  };
218
218
 
219
219
  const result = new Result(
220
- mockQuery,
220
+ mockQuery.to,
221
221
  RoutingState.fromTestData({
222
222
  nbStops: NB_STOPS,
223
223
  origins: [0],
@@ -228,7 +228,7 @@ describe('Result', () => {
228
228
  [3, timeFromHMS(9, 30, 0), 1],
229
229
  ],
230
230
  graph: [
231
- [[0, { arrival: timeFromHMS(8, 0, 0) }]], // round 0 – origins
231
+ [[0, { stopId: 0, arrival: timeFromHMS(8, 0, 0) }]], // round 0 – origins
232
232
  [[2, vehicleEdge]], // round 1
233
233
  ],
234
234
  }),
@@ -255,7 +255,7 @@ describe('Result', () => {
255
255
  };
256
256
 
257
257
  const result = new Result(
258
- mockQuery,
258
+ mockQuery.to,
259
259
  RoutingState.fromTestData({
260
260
  nbStops: NB_STOPS,
261
261
  origins: [2],
@@ -266,7 +266,7 @@ describe('Result', () => {
266
266
  [6, timeFromHMS(10, 30, 0), 2],
267
267
  ],
268
268
  graph: [
269
- [[2, { arrival: timeFromHMS(8, 0, 0) }]], // round 0 – origins
269
+ [[2, { stopId: 2, arrival: timeFromHMS(8, 0, 0) }]], // round 0 – origins
270
270
  [
271
271
  [
272
272
  2,
@@ -304,7 +304,7 @@ describe('Result', () => {
304
304
  };
305
305
 
306
306
  const result = new Result(
307
- mockQuery,
307
+ mockQuery.to,
308
308
  RoutingState.fromTestData({
309
309
  nbStops: NB_STOPS,
310
310
  origins: [0],
@@ -314,7 +314,7 @@ describe('Result', () => {
314
314
  [2, timeFromHMS(9, 0, 0), 1],
315
315
  ],
316
316
  graph: [
317
- [[0, { arrival: timeFromHMS(8, 0, 0) }]], // round 0 – origins
317
+ [[0, { stopId: 0, arrival: timeFromHMS(8, 0, 0) }]], // round 0 – origins
318
318
  [[2, vehicleEdge]], // round 1
319
319
  ],
320
320
  }),
@@ -349,7 +349,7 @@ describe('Result', () => {
349
349
  };
350
350
 
351
351
  const result = new Result(
352
- mockQuery,
352
+ mockQuery.to,
353
353
  RoutingState.fromTestData({
354
354
  nbStops: NB_STOPS,
355
355
  origins: [0],
@@ -360,7 +360,7 @@ describe('Result', () => {
360
360
  [3, timeFromHMS(9, 45, 0), 2],
361
361
  ],
362
362
  graph: [
363
- [[0, { arrival: timeFromHMS(8, 0, 0) }]], // round 0 – origins
363
+ [[0, { stopId: 0, arrival: timeFromHMS(8, 0, 0) }]], // round 0 – origins
364
364
  [[2, firstVehicleEdge]], // round 1
365
365
  [[3, secondVehicleEdge]], // round 2
366
366
  ],
@@ -394,7 +394,7 @@ describe('Result', () => {
394
394
  };
395
395
 
396
396
  const result = new Result(
397
- mockQuery,
397
+ mockQuery.to,
398
398
  RoutingState.fromTestData({
399
399
  nbStops: NB_STOPS,
400
400
  origins: [0],
@@ -404,7 +404,7 @@ describe('Result', () => {
404
404
  [2, timeFromHMS(9, 0, 0), 1],
405
405
  ],
406
406
  graph: [
407
- [[0, { arrival: timeFromHMS(8, 0, 0) }]], // round 0 – origins
407
+ [[0, { stopId: 0, arrival: timeFromHMS(8, 0, 0) }]], // round 0 – origins
408
408
  [[2, vehicleEdge]], // round 1
409
409
  ],
410
410
  }),
@@ -431,7 +431,7 @@ describe('Result', () => {
431
431
  };
432
432
 
433
433
  const result = new Result(
434
- mockQuery,
434
+ mockQuery.to,
435
435
  RoutingState.fromTestData({
436
436
  nbStops: NB_STOPS,
437
437
  origins: [0],
@@ -442,7 +442,7 @@ describe('Result', () => {
442
442
  [3, timeFromHMS(9, 45, 0), 1],
443
443
  ],
444
444
  graph: [
445
- [[0, { arrival: timeFromHMS(8, 0, 0) }]], // round 0 – origins
445
+ [[0, { stopId: 0, arrival: timeFromHMS(8, 0, 0) }]], // round 0 – origins
446
446
  [[2, vehicleEdge]], // round 1
447
447
  ],
448
448
  }),
@@ -480,7 +480,7 @@ describe('Result', () => {
480
480
  };
481
481
 
482
482
  const result = new Result(
483
- mockQuery,
483
+ mockQuery.to,
484
484
  RoutingState.fromTestData({
485
485
  nbStops: NB_STOPS,
486
486
  origins: [0],
@@ -491,7 +491,7 @@ describe('Result', () => {
491
491
  [2, timeFromHMS(9, 0, 0), 1],
492
492
  ],
493
493
  graph: [
494
- [[0, { arrival: timeFromHMS(8, 0, 0) }]], // round 0 – origins
494
+ [[0, { stopId: 0, arrival: timeFromHMS(8, 0, 0) }]], // round 0 – origins
495
495
  [
496
496
  [1, firstVehicleEdge],
497
497
  [2, continuousVehicleEdge],
@@ -530,7 +530,7 @@ describe('Result', () => {
530
530
  };
531
531
 
532
532
  const result = new Result(
533
- mockQuery,
533
+ mockQuery.to,
534
534
  RoutingState.fromTestData({
535
535
  nbStops: NB_STOPS,
536
536
  origins: [0],
@@ -540,7 +540,7 @@ describe('Result', () => {
540
540
  [3, timeFromHMS(9, 45, 0), 1],
541
541
  ],
542
542
  graph: [
543
- [[0, { arrival: timeFromHMS(8, 0, 0) }]], // round 0 – origins
543
+ [[0, { stopId: 0, arrival: timeFromHMS(8, 0, 0) }]], // round 0 – origins
544
544
  [[3, continuousVehicleEdge]], // round 1
545
545
  ],
546
546
  }),
@@ -591,7 +591,7 @@ describe('Result', () => {
591
591
  [3, timeFromHMS(9, 15, 0), 2],
592
592
  ],
593
593
  graph: [
594
- [[0, { arrival: timeFromHMS(8, 0, 0) }]], // round 0 – origins
594
+ [[0, { stopId: 0, arrival: timeFromHMS(8, 0, 0) }]], // round 0 – origins
595
595
  [
596
596
  [1, firstVehicleEdge],
597
597
  [2, transferEdge],
@@ -601,7 +601,7 @@ describe('Result', () => {
601
601
  });
602
602
 
603
603
  const result = new Result(
604
- mockQuery,
604
+ mockQuery.to,
605
605
  state,
606
606
  mockStopsIndex,
607
607
  mockTimetable,
@@ -649,7 +649,7 @@ describe('Result', () => {
649
649
  const arrivalTime = { arrival: timeFromHMS(9, 0, 0), legNumber: 1 };
650
650
 
651
651
  const result = new Result(
652
- mockQuery,
652
+ mockQuery.to,
653
653
  RoutingState.fromTestData({
654
654
  nbStops: NB_STOPS,
655
655
  destinations: [2],
@@ -665,7 +665,7 @@ describe('Result', () => {
665
665
 
666
666
  it('should return undefined for unreachable stop', () => {
667
667
  const result = new Result(
668
- mockQuery,
668
+ mockQuery.to,
669
669
  RoutingState.fromTestData({
670
670
  nbStops: NB_STOPS,
671
671
  destinations: [2],
@@ -683,7 +683,7 @@ describe('Result', () => {
683
683
  const earlierArrival = { arrival: timeFromHMS(9, 0, 0), legNumber: 1 };
684
684
 
685
685
  const result = new Result(
686
- mockQuery,
686
+ mockQuery.to,
687
687
  RoutingState.fromTestData({
688
688
  nbStops: NB_STOPS,
689
689
  destinations: [4],
@@ -721,7 +721,7 @@ describe('Result', () => {
721
721
  };
722
722
 
723
723
  const result = new Result(
724
- mockQuery,
724
+ mockQuery.to,
725
725
  RoutingState.fromTestData({
726
726
  nbStops: NB_STOPS,
727
727
  origins: [0],
@@ -732,7 +732,7 @@ describe('Result', () => {
732
732
  // falling back to the graph rather than the global best.
733
733
  arrivals: [[2, timeFromHMS(9, 0, 0), 2]],
734
734
  graph: [
735
- [[0, { arrival: timeFromHMS(8, 0, 0) }]], // round 0 – origins
735
+ [[0, { stopId: 0, arrival: timeFromHMS(8, 0, 0) }]], // round 0 – origins
736
736
  [[2, vehicleEdge1]], // round 1 – direct (9:30)
737
737
  [[2, vehicleEdge2]], // round 2 – with transfer
738
738
  ],
@@ -752,7 +752,7 @@ describe('Result', () => {
752
752
 
753
753
  it('should handle non-existent stops', () => {
754
754
  const result = new Result(
755
- mockQuery,
755
+ mockQuery.to,
756
756
  RoutingState.fromTestData({
757
757
  nbStops: NB_STOPS,
758
758
  destinations: [2],
@@ -103,4 +103,32 @@ describe('Route', () => {
103
103
  const route = new Route([transferLeg]);
104
104
  assert.throws(() => route.arrivalTime(), /No vehicle leg found in route/);
105
105
  });
106
+
107
+ describe('toString', () => {
108
+ it('includes leg numbers, stop names, and travel details for each leg', () => {
109
+ const route = new Route([vehicleLeg, transferLeg, secondVehicleLeg]);
110
+ const str = route.toString();
111
+ assert(str.includes('Leg 1:'));
112
+ assert(str.includes('Leg 2:'));
113
+ assert(str.includes('Leg 3:'));
114
+ assert(str.includes('Stop A'));
115
+ assert(str.includes('Stop D'));
116
+ assert(str.includes('BUS Route 1'));
117
+ assert(str.includes('RAIL Route 2'));
118
+ assert(str.includes('Transfer: RECOMMENDED'));
119
+ });
120
+
121
+ it('includes platform info when present', () => {
122
+ const stopWithPlatform: Stop = {
123
+ ...stopA,
124
+ platform: '3A',
125
+ };
126
+ const legWithPlatform: VehicleLeg = {
127
+ ...vehicleLeg,
128
+ from: stopWithPlatform,
129
+ };
130
+ const route = new Route([legWithPlatform]);
131
+ assert(route.toString().includes('Pl. 3A'));
132
+ });
133
+ });
106
134
  });