minotor 3.0.0 → 3.0.2

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 (81) hide show
  1. package/.cspell.json +14 -1
  2. package/.gitattributes +3 -0
  3. package/.github/PULL_REQUEST_TEMPLATE.md +3 -0
  4. package/.github/workflows/minotor.yml +17 -1
  5. package/CHANGELOG.md +3 -9
  6. package/README.md +47 -17
  7. package/dist/__e2e__/router.test.d.ts +1 -0
  8. package/dist/cli/perf.d.ts +28 -0
  9. package/dist/cli/utils.d.ts +6 -2
  10. package/dist/cli.mjs +1967 -823
  11. package/dist/cli.mjs.map +1 -1
  12. package/dist/gtfs/trips.d.ts +1 -0
  13. package/dist/gtfs/utils.d.ts +1 -1
  14. package/dist/parser.cjs.js +1030 -627
  15. package/dist/parser.cjs.js.map +1 -1
  16. package/dist/parser.d.ts +4 -2
  17. package/dist/parser.esm.js +1030 -627
  18. package/dist/parser.esm.js.map +1 -1
  19. package/dist/router.cjs.js +1 -1
  20. package/dist/router.cjs.js.map +1 -1
  21. package/dist/router.d.ts +10 -5
  22. package/dist/router.esm.js +1 -1
  23. package/dist/router.esm.js.map +1 -1
  24. package/dist/router.umd.js +1 -1
  25. package/dist/router.umd.js.map +1 -1
  26. package/dist/routing/__tests__/result.test.d.ts +1 -0
  27. package/dist/routing/query.d.ts +27 -6
  28. package/dist/routing/result.d.ts +1 -1
  29. package/dist/routing/route.d.ts +47 -2
  30. package/dist/routing/router.d.ts +15 -1
  31. package/dist/stops/stopsIndex.d.ts +3 -3
  32. package/dist/timetable/__tests__/route.test.d.ts +1 -0
  33. package/dist/timetable/__tests__/time.test.d.ts +1 -0
  34. package/dist/timetable/io.d.ts +7 -1
  35. package/dist/timetable/proto/timetable.d.ts +1 -1
  36. package/dist/timetable/route.d.ts +155 -0
  37. package/dist/timetable/time.d.ts +21 -0
  38. package/dist/timetable/timetable.d.ts +41 -61
  39. package/package.json +36 -35
  40. package/src/__e2e__/benchmark.json +22 -0
  41. package/src/__e2e__/router.test.ts +209 -0
  42. package/src/__e2e__/timetable/stops.bin +3 -0
  43. package/src/__e2e__/timetable/timetable.bin +3 -0
  44. package/src/cli/minotor.ts +51 -1
  45. package/src/cli/perf.ts +136 -0
  46. package/src/cli/repl.ts +26 -13
  47. package/src/cli/utils.ts +6 -28
  48. package/src/gtfs/__tests__/parser.test.ts +12 -15
  49. package/src/gtfs/__tests__/services.test.ts +1 -0
  50. package/src/gtfs/__tests__/transfers.test.ts +0 -1
  51. package/src/gtfs/__tests__/trips.test.ts +67 -74
  52. package/src/gtfs/profiles/ch.ts +1 -1
  53. package/src/gtfs/routes.ts +4 -4
  54. package/src/gtfs/services.ts +15 -2
  55. package/src/gtfs/stops.ts +7 -3
  56. package/src/gtfs/transfers.ts +6 -3
  57. package/src/gtfs/trips.ts +33 -16
  58. package/src/gtfs/utils.ts +13 -2
  59. package/src/parser.ts +4 -2
  60. package/src/router.ts +17 -11
  61. package/src/routing/__tests__/result.test.ts +392 -0
  62. package/src/routing/__tests__/router.test.ts +94 -137
  63. package/src/routing/query.ts +28 -7
  64. package/src/routing/result.ts +10 -5
  65. package/src/routing/route.ts +95 -9
  66. package/src/routing/router.ts +82 -66
  67. package/src/stops/__tests__/io.test.ts +1 -1
  68. package/src/stops/__tests__/stopFinder.test.ts +1 -1
  69. package/src/stops/proto/stops.ts +4 -4
  70. package/src/stops/stopsIndex.ts +3 -3
  71. package/src/timetable/__tests__/io.test.ts +16 -23
  72. package/src/timetable/__tests__/route.test.ts +317 -0
  73. package/src/timetable/__tests__/time.test.ts +494 -0
  74. package/src/timetable/__tests__/timetable.test.ts +64 -75
  75. package/src/timetable/io.ts +32 -26
  76. package/src/timetable/proto/timetable.proto +1 -1
  77. package/src/timetable/proto/timetable.ts +13 -13
  78. package/src/timetable/route.ts +347 -0
  79. package/src/timetable/time.ts +40 -8
  80. package/src/timetable/timetable.ts +74 -165
  81. package/tsconfig.build.json +1 -1
@@ -4,6 +4,7 @@ import { beforeEach, describe, it } from 'node:test';
4
4
  import { StopsMap } from '../../stops/stops.js';
5
5
  import { StopsIndex } from '../../stops/stopsIndex.js';
6
6
  import { Duration } from '../../timetable/duration.js';
7
+ import { REGULAR, Route } from '../../timetable/route.js';
7
8
  import { Time } from '../../timetable/time.js';
8
9
  import {
9
10
  RoutesAdjacency,
@@ -30,8 +31,8 @@ describe('Router', () => {
30
31
  const routesAdjacency: RoutesAdjacency = new Map([
31
32
  [
32
33
  'route1',
33
- {
34
- stopTimes: new Uint16Array([
34
+ new Route(
35
+ new Uint16Array([
35
36
  Time.fromString('08:00:00').toMinutes(),
36
37
  Time.fromString('08:10:00').toMinutes(),
37
38
  Time.fromString('08:15:00').toMinutes(),
@@ -39,22 +40,17 @@ describe('Router', () => {
39
40
  Time.fromString('08:35:00').toMinutes(),
40
41
  Time.fromString('08:45:00').toMinutes(),
41
42
  ]),
42
- pickUpDropOffTypes: new Uint8Array([
43
- 0,
44
- 0, // REGULAR
45
- 0,
46
- 0, // REGULAR
47
- 0,
48
- 0, // REGULAR
49
- ]),
50
- stops: new Uint32Array([0, 1, 2]),
51
- stopIndices: new Map([
52
- [0, 0],
53
- [1, 1],
54
- [2, 2],
43
+ new Uint8Array([
44
+ REGULAR,
45
+ REGULAR,
46
+ REGULAR,
47
+ REGULAR,
48
+ REGULAR,
49
+ REGULAR,
55
50
  ]),
56
- serviceRouteId: 'service_route1',
57
- },
51
+ new Uint32Array([0, 1, 2]),
52
+ 'service_route1',
53
+ ),
58
54
  ],
59
55
  ]);
60
56
 
@@ -147,7 +143,7 @@ describe('Router', () => {
147
143
 
148
144
  const timeToStop3 = result.arrivalAt('stop3');
149
145
  assert.strictEqual(
150
- timeToStop3?.time.toMinutes(),
146
+ timeToStop3?.arrival.toMinutes(),
151
147
  Time.fromString('08:35:00').toMinutes(),
152
148
  );
153
149
  });
@@ -167,8 +163,8 @@ describe('Router', () => {
167
163
  const routesAdjacency: RoutesAdjacency = new Map([
168
164
  [
169
165
  'route1',
170
- {
171
- stopTimes: new Uint16Array([
166
+ new Route(
167
+ new Uint16Array([
172
168
  Time.fromString('08:00:00').toMinutes(),
173
169
  Time.fromString('08:15:00').toMinutes(),
174
170
  Time.fromString('08:30:00').toMinutes(),
@@ -176,27 +172,22 @@ describe('Router', () => {
176
172
  Time.fromString('09:00:00').toMinutes(),
177
173
  Time.fromString('09:10:00').toMinutes(),
178
174
  ]),
179
- pickUpDropOffTypes: new Uint8Array([
180
- 0, // REGULAR
181
- 0, // REGULAR
182
- 0, // REGULAR
183
- 0, // REGULAR
184
- 0, // REGULAR
185
- 0, // REGULAR
175
+ new Uint8Array([
176
+ REGULAR,
177
+ REGULAR,
178
+ REGULAR,
179
+ REGULAR,
180
+ REGULAR,
181
+ REGULAR,
186
182
  ]),
187
- stops: new Uint32Array([0, 1, 2]),
188
- stopIndices: new Map([
189
- [0, 0],
190
- [1, 1],
191
- [2, 2],
192
- ]),
193
- serviceRouteId: 'service_route1',
194
- },
183
+ new Uint32Array([0, 1, 2]),
184
+ 'service_route1',
185
+ ),
195
186
  ],
196
187
  [
197
188
  'route2',
198
- {
199
- stopTimes: new Uint16Array([
189
+ new Route(
190
+ new Uint16Array([
200
191
  Time.fromString('08:05:00').toMinutes(),
201
192
  Time.fromString('08:20:00').toMinutes(),
202
193
  Time.fromString('09:00:00').toMinutes(),
@@ -204,22 +195,17 @@ describe('Router', () => {
204
195
  Time.fromString('09:20:00').toMinutes(),
205
196
  Time.fromString('09:35:00').toMinutes(),
206
197
  ]),
207
- pickUpDropOffTypes: new Uint8Array([
208
- 0, // REGULAR
209
- 0, // REGULAR
210
- 0, // REGULAR
211
- 0, // REGULAR
212
- 0, // REGULAR
213
- 0, // REGULAR
198
+ new Uint8Array([
199
+ REGULAR,
200
+ REGULAR,
201
+ REGULAR,
202
+ REGULAR,
203
+ REGULAR,
204
+ REGULAR,
214
205
  ]),
215
- stops: new Uint32Array([3, 1, 4]),
216
- stopIndices: new Map([
217
- [3, 0],
218
- [1, 1],
219
- [4, 2],
220
- ]),
221
- serviceRouteId: 'service_route2',
222
- },
206
+ new Uint32Array([3, 1, 4]),
207
+ 'service_route2',
208
+ ),
223
209
  ],
224
210
  ]);
225
211
 
@@ -333,7 +319,7 @@ describe('Router', () => {
333
319
 
334
320
  const timeToStop5 = result.arrivalAt('stop5');
335
321
  assert.strictEqual(
336
- timeToStop5?.time.toMinutes(),
322
+ timeToStop5?.arrival.toMinutes(),
337
323
  Time.fromString('09:20:00').toMinutes(),
338
324
  );
339
325
  });
@@ -367,8 +353,8 @@ describe('Router', () => {
367
353
  const routesAdjacency: RoutesAdjacency = new Map([
368
354
  [
369
355
  'route1',
370
- {
371
- stopTimes: new Uint16Array([
356
+ new Route(
357
+ new Uint16Array([
372
358
  Time.fromString('08:00:00').toMinutes(),
373
359
  Time.fromString('08:15:00').toMinutes(),
374
360
  Time.fromString('08:25:00').toMinutes(),
@@ -376,27 +362,22 @@ describe('Router', () => {
376
362
  Time.fromString('08:45:00').toMinutes(),
377
363
  Time.fromString('08:55:00').toMinutes(),
378
364
  ]),
379
- pickUpDropOffTypes: new Uint8Array([
380
- 0, // REGULAR
381
- 0, // REGULAR
382
- 0, // REGULAR
383
- 0, // REGULAR
384
- 0, // REGULAR
385
- 0, // REGULAR
365
+ new Uint8Array([
366
+ REGULAR,
367
+ REGULAR,
368
+ REGULAR,
369
+ REGULAR,
370
+ REGULAR,
371
+ REGULAR,
386
372
  ]),
387
- stops: new Uint32Array([0, 1, 2]),
388
- stopIndices: new Map([
389
- [0, 0],
390
- [1, 1],
391
- [2, 2],
392
- ]),
393
- serviceRouteId: 'service_route1',
394
- },
373
+ new Uint32Array([0, 1, 2]),
374
+ 'service_route1',
375
+ ),
395
376
  ],
396
377
  [
397
378
  'route2',
398
- {
399
- stopTimes: new Uint16Array([
379
+ new Route(
380
+ new Uint16Array([
400
381
  Time.fromString('08:10:00').toMinutes(),
401
382
  Time.fromString('08:20:00').toMinutes(),
402
383
  Time.fromString('08:40:00').toMinutes(),
@@ -404,22 +385,17 @@ describe('Router', () => {
404
385
  Time.fromString('09:00:00').toMinutes(),
405
386
  Time.fromString('09:10:00').toMinutes(),
406
387
  ]),
407
- pickUpDropOffTypes: new Uint8Array([
408
- 0, // REGULAR
409
- 0, // REGULAR
410
- 0, // REGULAR
411
- 0, // REGULAR
412
- 0, // REGULAR
413
- 0, // REGULAR
388
+ new Uint8Array([
389
+ REGULAR,
390
+ REGULAR,
391
+ REGULAR,
392
+ REGULAR,
393
+ REGULAR,
394
+ REGULAR,
414
395
  ]),
415
- stops: new Uint32Array([3, 4, 5]),
416
- stopIndices: new Map([
417
- [3, 0],
418
- [4, 1],
419
- [5, 2],
420
- ]),
421
- serviceRouteId: 'service_route2',
422
- },
396
+ new Uint32Array([3, 4, 5]),
397
+ 'service_route2',
398
+ ),
423
399
  ],
424
400
  ]);
425
401
 
@@ -543,7 +519,7 @@ describe('Router', () => {
543
519
 
544
520
  const timeToStop5 = result.arrivalAt('stop5');
545
521
  assert.strictEqual(
546
- timeToStop5?.time.toMinutes(),
522
+ timeToStop5?.arrival.toMinutes(),
547
523
  Time.fromString('08:30:00').toMinutes(),
548
524
  );
549
525
  });
@@ -564,8 +540,8 @@ describe('Router', () => {
564
540
  const routesAdjacency: RoutesAdjacency = new Map([
565
541
  [
566
542
  'route1',
567
- {
568
- stopTimes: new Uint16Array([
543
+ new Route(
544
+ new Uint16Array([
569
545
  Time.fromString('08:00:00').toMinutes(),
570
546
  Time.fromString('08:15:00').toMinutes(),
571
547
  Time.fromString('08:30:00').toMinutes(),
@@ -573,27 +549,22 @@ describe('Router', () => {
573
549
  Time.fromString('09:00:00').toMinutes(),
574
550
  Time.fromString('09:15:00').toMinutes(),
575
551
  ]),
576
- pickUpDropOffTypes: new Uint8Array([
577
- 0, // REGULAR
578
- 0, // REGULAR
579
- 0, // REGULAR
580
- 0, // REGULAR
581
- 0, // REGULAR
582
- 0, // REGULAR
552
+ new Uint8Array([
553
+ REGULAR,
554
+ REGULAR,
555
+ REGULAR,
556
+ REGULAR,
557
+ REGULAR,
558
+ REGULAR,
583
559
  ]),
584
- stops: new Uint32Array([0, 1, 2]),
585
- stopIndices: new Map([
586
- [0, 0],
587
- [1, 1],
588
- [2, 2],
589
- ]),
590
- serviceRouteId: 'service_route1',
591
- },
560
+ new Uint32Array([0, 1, 2]),
561
+ 'service_route1',
562
+ ),
592
563
  ],
593
564
  [
594
565
  'route2',
595
- {
596
- stopTimes: new Uint16Array([
566
+ new Route(
567
+ new Uint16Array([
597
568
  Time.fromString('08:10:00').toMinutes(),
598
569
  Time.fromString('08:25:00').toMinutes(),
599
570
  Time.fromString('08:50:00').toMinutes(),
@@ -601,45 +572,31 @@ describe('Router', () => {
601
572
  Time.fromString('09:10:00').toMinutes(),
602
573
  Time.fromString('09:25:00').toMinutes(),
603
574
  ]),
604
- pickUpDropOffTypes: new Uint8Array([
605
- 0, // REGULAR
606
- 0, // REGULAR
607
- 0, // REGULAR
608
- 0, // REGULAR
609
- 0, // REGULAR
610
- 0, // REGULAR
575
+ new Uint8Array([
576
+ REGULAR,
577
+ REGULAR,
578
+ REGULAR,
579
+ REGULAR,
580
+ REGULAR,
581
+ REGULAR,
611
582
  ]),
612
- stops: new Uint32Array([3, 1, 4]),
613
- stopIndices: new Map([
614
- [3, 0],
615
- [1, 1],
616
- [4, 2],
617
- ]),
618
- serviceRouteId: 'service_route2',
619
- },
583
+ new Uint32Array([3, 1, 4]),
584
+ 'service_route2',
585
+ ),
620
586
  ],
621
587
  [
622
588
  'route3',
623
- {
624
- stopTimes: new Uint16Array([
589
+ new Route(
590
+ new Uint16Array([
625
591
  Time.fromString('08:00:00').toMinutes(),
626
592
  Time.fromString('08:15:00').toMinutes(),
627
593
  Time.fromString('09:45:00').toMinutes(),
628
594
  Time.fromString('10:00:00').toMinutes(),
629
595
  ]),
630
- pickUpDropOffTypes: new Uint8Array([
631
- 0, // REGULAR
632
- 0, // REGULAR
633
- 0, // REGULAR
634
- 0, // REGULAR
635
- ]),
636
- stops: new Uint32Array([0, 4]),
637
- stopIndices: new Map([
638
- [0, 0],
639
- [4, 1],
640
- ]),
641
- serviceRouteId: 'service_route3',
642
- },
596
+ new Uint8Array([REGULAR, REGULAR, REGULAR, REGULAR]),
597
+ new Uint32Array([0, 4]),
598
+ 'service_route3',
599
+ ),
643
600
  ],
644
601
  ]);
645
602
 
@@ -5,13 +5,13 @@ import { ALL_TRANSPORT_MODES, RouteType } from '../timetable/timetable.js';
5
5
 
6
6
  export class Query {
7
7
  from: SourceStopId;
8
- to: SourceStopId[];
8
+ to: Set<SourceStopId>;
9
9
  departureTime: Time;
10
10
  lastDepartureTime?: Time;
11
11
  options: {
12
12
  maxTransfers: number;
13
13
  minTransferTime: Duration;
14
- transportModes: RouteType[];
14
+ transportModes: Set<RouteType>;
15
15
  };
16
16
 
17
17
  constructor(builder: typeof Query.Builder.prototype) {
@@ -23,46 +23,67 @@ export class Query {
23
23
 
24
24
  static Builder = class {
25
25
  fromValue!: SourceStopId;
26
- toValue: SourceStopId[] = [];
26
+ toValue: Set<SourceStopId> = new Set();
27
27
  departureTimeValue!: Time;
28
28
  // lastDepartureTimeValue?: Date;
29
29
  // via: StopId[] = [];
30
30
  optionsValue: {
31
31
  maxTransfers: number;
32
32
  minTransferTime: Duration;
33
- transportModes: RouteType[];
33
+ transportModes: Set<RouteType>;
34
34
  } = {
35
35
  maxTransfers: 5,
36
36
  minTransferTime: Duration.fromSeconds(120),
37
37
  transportModes: ALL_TRANSPORT_MODES,
38
38
  };
39
39
 
40
+ /**
41
+ * Sets the starting stop.
42
+ */
40
43
  from(from: SourceStopId): this {
41
44
  this.fromValue = from;
42
45
  return this;
43
46
  }
44
47
 
45
- to(to: SourceStopId | SourceStopId[]): this {
46
- this.toValue = Array.isArray(to) ? to : [to];
48
+ /**
49
+ * Sets the destination stops(s), routing will stop when all the provided stops are reached.
50
+ */
51
+ to(to: SourceStopId | Set<SourceStopId>): this {
52
+ this.toValue = to instanceof Set ? to : new Set([to]);
47
53
  return this;
48
54
  }
49
55
 
56
+ /**
57
+ * Sets the departure time for the query.
58
+ * Note that the router will favor routes that depart shortly after the provided departure time,
59
+ * even if a later route might arrive at the same time.
60
+ * Range queries will allow to specify a range of departure times in the future.
61
+ */
50
62
  departureTime(departureTime: Time): this {
51
63
  this.departureTimeValue = departureTime;
52
64
  return this;
53
65
  }
54
66
 
67
+ /**
68
+ * Sets the maximum number of transfers allowed.
69
+ */
55
70
  maxTransfers(maxTransfers: number): this {
56
71
  this.optionsValue.maxTransfers = maxTransfers;
57
72
  return this;
58
73
  }
59
74
 
75
+ /**
76
+ * Sets the minimum transfer time to use when no transfer time is provided in the data.
77
+ */
60
78
  minTransferTime(minTransferTime: Duration): this {
61
79
  this.optionsValue.minTransferTime = minTransferTime;
62
80
  return this;
63
81
  }
64
82
 
65
- transportModes(transportModes: RouteType[]): this {
83
+ /**
84
+ * Sets the transport modes to consider.
85
+ */
86
+ transportModes(transportModes: Set<RouteType>): this {
66
87
  this.optionsValue.transportModes = transportModes;
67
88
  return this;
68
89
  }
@@ -29,8 +29,13 @@ export class Result {
29
29
  * @param to The destination stop. Defaults to the destination of the original query.
30
30
  * @returns a route to the destination stop if it exists.
31
31
  */
32
- bestRoute(to?: SourceStopId | SourceStopId[]): Route | undefined {
33
- const destinationList = Array.isArray(to) ? to : to ? [to] : this.query.to;
32
+ bestRoute(to?: SourceStopId | Set<SourceStopId>): Route | undefined {
33
+ const destinationList =
34
+ to instanceof Set
35
+ ? Array.from(to)
36
+ : to
37
+ ? [to]
38
+ : Array.from(this.query.to);
34
39
  const destinations = destinationList.flatMap((destination) =>
35
40
  this.stopsIndex.equivalentStops(destination),
36
41
  );
@@ -42,7 +47,7 @@ export class Result {
42
47
  if (arrivalTime !== undefined) {
43
48
  if (
44
49
  fastestTime === undefined ||
45
- arrivalTime.time.toMinutes() < fastestTime.time.toMinutes()
50
+ arrivalTime.arrival.isBefore(fastestTime.arrival)
46
51
  ) {
47
52
  fastestDestination = destination.id;
48
53
  fastestTime = arrivalTime;
@@ -91,7 +96,7 @@ export class Result {
91
96
  const relevantArrivals =
92
97
  maxTransfers !== undefined
93
98
  ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
94
- this.earliestArrivalsPerRound[maxTransfers - 1]!
99
+ this.earliestArrivalsPerRound[maxTransfers + 1]!
95
100
  : this.earliestArrivals;
96
101
 
97
102
  for (const equivalentStop of equivalentStops) {
@@ -99,7 +104,7 @@ export class Result {
99
104
  if (arrivalTime !== undefined) {
100
105
  if (
101
106
  earliestArrival === undefined ||
102
- arrivalTime.time.toMinutes() < earliestArrival.time.toMinutes()
107
+ arrivalTime.arrival.isBefore(earliestArrival.arrival)
103
108
  ) {
104
109
  earliestArrival = arrivalTime;
105
110
  }
@@ -1,8 +1,23 @@
1
- import { Stop } from '../stops/stops.js';
1
+ import { SourceStopId, Stop } from '../stops/stops.js';
2
2
  import { Duration } from '../timetable/duration.js';
3
3
  import { Time } from '../timetable/time.js';
4
4
  import { ServiceRoute, TransferType } from '../timetable/timetable.js';
5
5
 
6
+ export type JsonLeg = {
7
+ from: SourceStopId;
8
+ to: SourceStopId;
9
+ } & (
10
+ | {
11
+ departure: string;
12
+ arrival: string;
13
+ route: ServiceRoute;
14
+ }
15
+ | {
16
+ type: TransferType;
17
+ minTransferTime?: string;
18
+ }
19
+ );
20
+
6
21
  export type PickUpDropOffType =
7
22
  | 'REGULAR'
8
23
  | 'NOT_AVAILABLE'
@@ -32,6 +47,10 @@ export type VehicleLeg = BaseLeg & {
32
47
 
33
48
  export type Leg = Transfer | VehicleLeg;
34
49
 
50
+ /**
51
+ * Represents a resolved route consisting of multiple legs,
52
+ * which can be either vehicle legs or transfer legs.
53
+ */
35
54
  export class Route {
36
55
  legs: Leg[];
37
56
 
@@ -39,6 +58,12 @@ export class Route {
39
58
  this.legs = legs;
40
59
  }
41
60
 
61
+ /**
62
+ * Calculates the departure time of the route.
63
+ *
64
+ * @returns The departure time of the route.
65
+ * @throws If no vehicle leg is found in the route.
66
+ */
42
67
  departureTime(): Time {
43
68
  const cumulativeTransferTime: Duration = Duration.zero();
44
69
  for (let i = 0; i < this.legs.length; i++) {
@@ -54,6 +79,12 @@ export class Route {
54
79
  throw new Error('No vehicle leg found in route');
55
80
  }
56
81
 
82
+ /**
83
+ * Calculates the arrival time of the route.
84
+ *
85
+ * @returns The arrival time of the route.
86
+ * @throws If no vehicle leg is found in the route.
87
+ */
57
88
  arrivalTime(): Time {
58
89
  let lastVehicleArrivalTime: Time = Time.origin();
59
90
  const totalTransferTime: Duration = Duration.zero();
@@ -82,22 +113,77 @@ export class Route {
82
113
  return lastVehicleArrivalTime.plus(totalTransferTime);
83
114
  }
84
115
 
116
+ /**
117
+ * Calculates the total duration of the route.
118
+ *
119
+ * @returns The total duration of the route.
120
+ */
85
121
  totalDuration(): Duration {
86
122
  if (this.legs.length === 0) return Duration.zero();
87
123
  return this.arrivalTime().diff(this.departureTime());
88
124
  }
89
125
 
90
- print(): string {
126
+ /**
127
+ * Generates a human-readable string representation of the route.
128
+ *
129
+ * @returns A formatted string describing each leg of the route.
130
+ */
131
+ toString(): string {
91
132
  return this.legs
92
133
  .map((leg, index) => {
93
- if ('route' in leg) {
94
- return `Leg ${index + 1}: ${leg.from.name} to ${leg.to.name}
95
- via route ${leg.route.type} ${leg.route.name},
96
- departs at ${leg.departureTime.toString()}, arrives at ${leg.arrivalTime.toString()}`;
97
- }
98
- return `Leg ${index + 1}: Transfer from ${leg.from.name} to ${leg.to.name},
99
- minimum transfer time: ${leg.minTransferTime?.toString() ?? 'not specified'}`;
134
+ const fromStop = `From: ${leg.from.name}${leg.from.platform ? ` (Pl. ${leg.from.platform})` : ''}`;
135
+ const toStop = `To: ${leg.to.name}${leg.to.platform ? ` (Pl. ${leg.to.platform})` : ''}`;
136
+ const transferDetails =
137
+ 'minTransferTime' in leg
138
+ ? `Minimum Transfer Time: ${leg.minTransferTime?.toString()}`
139
+ : '';
140
+ const travelDetails =
141
+ 'route' in leg && 'departureTime' in leg && 'arrivalTime' in leg
142
+ ? `Route: ${leg.route.type} ${leg.route.name}, Departure: ${leg.departureTime.toString()}, Arrival: ${leg.arrivalTime.toString()}`
143
+ : '';
144
+
145
+ return [
146
+ `Leg ${index + 1}:`,
147
+ ` ${fromStop}`,
148
+ ` ${toStop}`,
149
+ transferDetails ? ` ${transferDetails}` : '',
150
+ travelDetails ? ` ${travelDetails}` : '',
151
+ ]
152
+ .filter((line) => line.trim() !== '')
153
+ .join('\n');
100
154
  })
101
155
  .join('\n');
102
156
  }
157
+
158
+ /**
159
+ * Generates a concise JSON representation of the route.
160
+ * This is particularly useful for generating regression tests
161
+ * to verify the correctness of route calculations.
162
+ *
163
+ * @returns A JSON representation of the route.
164
+ */
165
+ asJson(): JsonLeg[] {
166
+ const jsonLegs: JsonLeg[] = this.legs.map((leg: Leg) => {
167
+ if ('route' in leg) {
168
+ return {
169
+ from: leg.from.sourceStopId,
170
+ to: leg.to.sourceStopId,
171
+ departure: leg.departureTime.toString(),
172
+ arrival: leg.arrivalTime.toString(),
173
+ route: leg.route,
174
+ };
175
+ } else {
176
+ return {
177
+ from: leg.from.sourceStopId,
178
+ to: leg.to.sourceStopId,
179
+ type: leg.type,
180
+ ...(leg.minTransferTime !== undefined && {
181
+ minTransferTime: leg.minTransferTime.toString(),
182
+ }),
183
+ };
184
+ }
185
+ });
186
+
187
+ return jsonLegs;
188
+ }
103
189
  }