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
@@ -7,16 +7,24 @@ import {
7
7
  Transfer as ProtoTransfer,
8
8
  TransferType as ProtoTransferType,
9
9
  } from './proto/timetable.js';
10
+ import { Route } from './route.js';
10
11
  import {
11
- Route,
12
12
  RoutesAdjacency,
13
13
  RouteType,
14
+ ServiceRouteId,
14
15
  ServiceRoutesMap,
15
16
  StopsAdjacency,
16
17
  Transfer,
17
18
  TransferType,
18
19
  } from './timetable.js';
19
20
 
21
+ export type SerializedRoute = {
22
+ stopTimes: Uint16Array;
23
+ pickUpDropOffTypes: Uint8Array;
24
+ stops: Uint32Array;
25
+ serviceRouteId: ServiceRouteId;
26
+ };
27
+
20
28
  const isLittleEndian = (() => {
21
29
  const buffer = new ArrayBuffer(4);
22
30
  const view = new DataView(buffer);
@@ -26,7 +34,7 @@ const isLittleEndian = (() => {
26
34
 
27
35
  const STANDARD_ENDIANNESS = true; // true = little-endian
28
36
 
29
- function uint32ArrayToBytes(array: Uint32Array): Uint8Array {
37
+ const uint32ArrayToBytes = (array: Uint32Array): Uint8Array => {
30
38
  if (isLittleEndian === STANDARD_ENDIANNESS) {
31
39
  return new Uint8Array(array.buffer, array.byteOffset, array.byteLength);
32
40
  }
@@ -41,9 +49,9 @@ function uint32ArrayToBytes(array: Uint32Array): Uint8Array {
41
49
  }
42
50
 
43
51
  return result;
44
- }
52
+ };
45
53
 
46
- function bytesToUint32Array(bytes: Uint8Array): Uint32Array {
54
+ const bytesToUint32Array = (bytes: Uint8Array): Uint32Array => {
47
55
  if (bytes.byteLength % 4 !== 0) {
48
56
  throw new Error(
49
57
  'Byte array length must be a multiple of 4 to convert to Uint32Array',
@@ -68,9 +76,9 @@ function bytesToUint32Array(bytes: Uint8Array): Uint32Array {
68
76
  }
69
77
 
70
78
  return result;
71
- }
79
+ };
72
80
 
73
- function uint16ArrayToBytes(array: Uint16Array): Uint8Array {
81
+ const uint16ArrayToBytes = (array: Uint16Array): Uint8Array => {
74
82
  if (isLittleEndian === STANDARD_ENDIANNESS) {
75
83
  return new Uint8Array(array.buffer, array.byteOffset, array.byteLength);
76
84
  }
@@ -85,9 +93,9 @@ function uint16ArrayToBytes(array: Uint16Array): Uint8Array {
85
93
  }
86
94
 
87
95
  return result;
88
- }
96
+ };
89
97
 
90
- function bytesToUint16Array(bytes: Uint8Array): Uint16Array {
98
+ const bytesToUint16Array = (bytes: Uint8Array): Uint16Array => {
91
99
  if (bytes.byteLength % 2 !== 0) {
92
100
  throw new Error(
93
101
  'Byte array length must be a multiple of 2 to convert to Uint16Array',
@@ -112,7 +120,7 @@ function bytesToUint16Array(bytes: Uint8Array): Uint16Array {
112
120
  }
113
121
 
114
122
  return result;
115
- }
123
+ };
116
124
 
117
125
  export const serializeStopsAdjacency = (
118
126
  stopsAdjacency: StopsAdjacency,
@@ -146,12 +154,13 @@ export const serializeRoutesAdjacency = (
146
154
  routes: {},
147
155
  };
148
156
 
149
- routesAdjacency.forEach((value: Route, key: string) => {
157
+ routesAdjacency.forEach((route: Route, key: string) => {
158
+ const routeData = route.serialize();
150
159
  protoRoutesAdjacency.routes[key] = {
151
- stopTimes: uint16ArrayToBytes(value.stopTimes),
152
- pickUpDropOffTypes: value.pickUpDropOffTypes,
153
- stops: uint32ArrayToBytes(value.stops),
154
- serviceRouteId: value.serviceRouteId,
160
+ stopTimes: uint16ArrayToBytes(routeData.stopTimes),
161
+ pickUpDropOffTypes: routeData.pickUpDropOffTypes,
162
+ stops: uint32ArrayToBytes(routeData.stops),
163
+ serviceRouteId: routeData.serviceRouteId,
155
164
  };
156
165
  });
157
166
 
@@ -208,18 +217,15 @@ export const deserializeRoutesAdjacency = (
208
217
 
209
218
  Object.entries(protoRoutesAdjacency.routes).forEach(([key, value]) => {
210
219
  const stops = bytesToUint32Array(value.stops);
211
- const indices = new Map<number, number>();
212
- for (let i = 0; i < stops.length; i++) {
213
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
214
- indices.set(stops[i]!, i);
215
- }
216
- routesAdjacency.set(key, {
217
- stopTimes: bytesToUint16Array(value.stopTimes),
218
- pickUpDropOffTypes: value.pickUpDropOffTypes,
219
- stops: stops,
220
- stopIndices: indices,
221
- serviceRouteId: value.serviceRouteId,
222
- });
220
+ routesAdjacency.set(
221
+ key,
222
+ new Route(
223
+ bytesToUint16Array(value.stopTimes),
224
+ value.pickUpDropOffTypes,
225
+ stops,
226
+ value.serviceRouteId,
227
+ ),
228
+ );
223
229
  });
224
230
 
225
231
  return routesAdjacency;
@@ -4,7 +4,7 @@ package minotor.timetable;
4
4
 
5
5
  message Route {
6
6
  /**
7
- * Arrivals and departures encoded as a 32 bit uint array.
7
+ * Arrivals and departures encoded as a 16 bit uint array.
8
8
  * Format: [arrival1, departure1, arrival2, departure2, etc.]
9
9
  */
10
10
  bytes stopTimes = 1;
@@ -1,6 +1,6 @@
1
1
  // Code generated by protoc-gen-ts_proto. DO NOT EDIT.
2
2
  // versions:
3
- // protoc-gen-ts_proto v2.6.1
3
+ // protoc-gen-ts_proto v2.7.7
4
4
  // protoc v4.23.4
5
5
  // source: src/timetable/proto/timetable.proto
6
6
 
@@ -137,7 +137,7 @@ export function routeTypeToJSON(object: RouteType): string {
137
137
 
138
138
  export interface Route {
139
139
  /**
140
- * Arrivals and departures encoded as a 32 bit uint array.
140
+ * Arrivals and departures encoded as a 16 bit uint array.
141
141
  * Format: [arrival1, departure1, arrival2, departure2, etc.]
142
142
  */
143
143
  stopTimes: Uint8Array;
@@ -237,7 +237,7 @@ export const Route: MessageFns<Route> = {
237
237
 
238
238
  decode(input: BinaryReader | Uint8Array, length?: number): Route {
239
239
  const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
240
- let end = length === undefined ? reader.len : reader.pos + length;
240
+ const end = length === undefined ? reader.len : reader.pos + length;
241
241
  const message = createBaseRoute();
242
242
  while (reader.pos < end) {
243
243
  const tag = reader.uint32();
@@ -338,7 +338,7 @@ export const RoutesAdjacency: MessageFns<RoutesAdjacency> = {
338
338
 
339
339
  decode(input: BinaryReader | Uint8Array, length?: number): RoutesAdjacency {
340
340
  const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
341
- let end = length === undefined ? reader.len : reader.pos + length;
341
+ const end = length === undefined ? reader.len : reader.pos + length;
342
342
  const message = createBaseRoutesAdjacency();
343
343
  while (reader.pos < end) {
344
344
  const tag = reader.uint32();
@@ -420,7 +420,7 @@ export const RoutesAdjacency_RoutesEntry: MessageFns<RoutesAdjacency_RoutesEntry
420
420
 
421
421
  decode(input: BinaryReader | Uint8Array, length?: number): RoutesAdjacency_RoutesEntry {
422
422
  const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
423
- let end = length === undefined ? reader.len : reader.pos + length;
423
+ const end = length === undefined ? reader.len : reader.pos + length;
424
424
  const message = createBaseRoutesAdjacency_RoutesEntry();
425
425
  while (reader.pos < end) {
426
426
  const tag = reader.uint32();
@@ -499,7 +499,7 @@ export const Transfer: MessageFns<Transfer> = {
499
499
 
500
500
  decode(input: BinaryReader | Uint8Array, length?: number): Transfer {
501
501
  const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
502
- let end = length === undefined ? reader.len : reader.pos + length;
502
+ const end = length === undefined ? reader.len : reader.pos + length;
503
503
  const message = createBaseTransfer();
504
504
  while (reader.pos < end) {
505
505
  const tag = reader.uint32();
@@ -585,7 +585,7 @@ export const StopsAdjacency: MessageFns<StopsAdjacency> = {
585
585
 
586
586
  decode(input: BinaryReader | Uint8Array, length?: number): StopsAdjacency {
587
587
  const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
588
- let end = length === undefined ? reader.len : reader.pos + length;
588
+ const end = length === undefined ? reader.len : reader.pos + length;
589
589
  const message = createBaseStopsAdjacency();
590
590
  while (reader.pos < end) {
591
591
  const tag = reader.uint32();
@@ -670,7 +670,7 @@ export const StopsAdjacency_StopAdjacency: MessageFns<StopsAdjacency_StopAdjacen
670
670
 
671
671
  decode(input: BinaryReader | Uint8Array, length?: number): StopsAdjacency_StopAdjacency {
672
672
  const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
673
- let end = length === undefined ? reader.len : reader.pos + length;
673
+ const end = length === undefined ? reader.len : reader.pos + length;
674
674
  const message = createBaseStopsAdjacency_StopAdjacency();
675
675
  while (reader.pos < end) {
676
676
  const tag = reader.uint32();
@@ -748,7 +748,7 @@ export const StopsAdjacency_StopsEntry: MessageFns<StopsAdjacency_StopsEntry> =
748
748
 
749
749
  decode(input: BinaryReader | Uint8Array, length?: number): StopsAdjacency_StopsEntry {
750
750
  const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
751
- let end = length === undefined ? reader.len : reader.pos + length;
751
+ const end = length === undefined ? reader.len : reader.pos + length;
752
752
  const message = createBaseStopsAdjacency_StopsEntry();
753
753
  while (reader.pos < end) {
754
754
  const tag = reader.uint32();
@@ -826,7 +826,7 @@ export const ServiceRoute: MessageFns<ServiceRoute> = {
826
826
 
827
827
  decode(input: BinaryReader | Uint8Array, length?: number): ServiceRoute {
828
828
  const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
829
- let end = length === undefined ? reader.len : reader.pos + length;
829
+ const end = length === undefined ? reader.len : reader.pos + length;
830
830
  const message = createBaseServiceRoute();
831
831
  while (reader.pos < end) {
832
832
  const tag = reader.uint32();
@@ -899,7 +899,7 @@ export const ServiceRoutesMap: MessageFns<ServiceRoutesMap> = {
899
899
 
900
900
  decode(input: BinaryReader | Uint8Array, length?: number): ServiceRoutesMap {
901
901
  const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
902
- let end = length === undefined ? reader.len : reader.pos + length;
902
+ const end = length === undefined ? reader.len : reader.pos + length;
903
903
  const message = createBaseServiceRoutesMap();
904
904
  while (reader.pos < end) {
905
905
  const tag = reader.uint32();
@@ -984,7 +984,7 @@ export const ServiceRoutesMap_RoutesEntry: MessageFns<ServiceRoutesMap_RoutesEnt
984
984
 
985
985
  decode(input: BinaryReader | Uint8Array, length?: number): ServiceRoutesMap_RoutesEntry {
986
986
  const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
987
- let end = length === undefined ? reader.len : reader.pos + length;
987
+ const end = length === undefined ? reader.len : reader.pos + length;
988
988
  const message = createBaseServiceRoutesMap_RoutesEntry();
989
989
  while (reader.pos < end) {
990
990
  const tag = reader.uint32();
@@ -1068,7 +1068,7 @@ export const Timetable: MessageFns<Timetable> = {
1068
1068
 
1069
1069
  decode(input: BinaryReader | Uint8Array, length?: number): Timetable {
1070
1070
  const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
1071
- let end = length === undefined ? reader.len : reader.pos + length;
1071
+ const end = length === undefined ? reader.len : reader.pos + length;
1072
1072
  const message = createBaseTimetable();
1073
1073
  while (reader.pos < end) {
1074
1074
  const tag = reader.uint32();
@@ -0,0 +1,347 @@
1
+ import { StopId } from '../stops/stops.js';
2
+ import { SerializedRoute } from './io.js';
3
+ import { Time } from './time.js';
4
+ import { ServiceRouteId } from './timetable.js';
5
+
6
+ /**
7
+ * An internal identifier for routes.
8
+ * Not to mix with the ServiceRouteId which corresponds to the GTFS RouteId.
9
+ * This one is used for identifying groups of trips
10
+ * from a service route sharing the same list of stops.
11
+ */
12
+ export type RouteId = string;
13
+
14
+ /**
15
+ * Details about the pickup and drop-off modalities at each stop in each trip of a route.
16
+ */
17
+ export type PickUpDropOffType =
18
+ | 'REGULAR'
19
+ | 'NOT_AVAILABLE'
20
+ | 'MUST_PHONE_AGENCY'
21
+ | 'MUST_COORDINATE_WITH_DRIVER';
22
+
23
+ export const REGULAR = 0;
24
+ export const NOT_AVAILABLE = 1;
25
+ export const MUST_PHONE_AGENCY = 2;
26
+ export const MUST_COORDINATE_WITH_DRIVER = 3;
27
+
28
+ /*
29
+ * A trip index corresponds to the index of the
30
+ * first stop time in the trip divided by the number of stops
31
+ * in the given route
32
+ */
33
+ export type TripIndex = number;
34
+
35
+ const pickUpDropOffTypeMap: PickUpDropOffType[] = [
36
+ 'REGULAR',
37
+ 'NOT_AVAILABLE',
38
+ 'MUST_PHONE_AGENCY',
39
+ 'MUST_COORDINATE_WITH_DRIVER',
40
+ ];
41
+
42
+ /**
43
+ * Converts a numerical representation of a pick-up/drop-off type
44
+ * into its corresponding string representation.
45
+ *
46
+ * @param numericalType - The numerical value representing the pick-up/drop-off type.
47
+ * @returns The corresponding PickUpDropOffType as a string.
48
+ * @throws An error if the numerical type is invalid.
49
+ */
50
+ const toPickupDropOffType = (numericalType: number): PickUpDropOffType => {
51
+ const type = pickUpDropOffTypeMap[numericalType];
52
+ if (!type) {
53
+ throw new Error(`Invalid pickup/drop-off type ${numericalType}`);
54
+ }
55
+ return type;
56
+ };
57
+
58
+ /**
59
+ * A route identifies all trips of a given service route sharing the same list of stops.
60
+ */
61
+ export class Route {
62
+ /**
63
+ * Arrivals and departures encoded as minutes from midnight.
64
+ * Format: [arrival1, departure1, arrival2, departure2, etc.]
65
+ */
66
+ private readonly stopTimes: Uint16Array;
67
+ /**
68
+ * PickUp and DropOff types represented as a binary Uint8Array.
69
+ * Values:
70
+ * 0: REGULAR
71
+ * 1: NOT_AVAILABLE
72
+ * 2: MUST_PHONE_AGENCY
73
+ * 3: MUST_COORDINATE_WITH_DRIVER
74
+ * Format: [pickupTypeStop1, dropOffTypeStop1, pickupTypeStop2, dropOffTypeStop2, etc.]
75
+ * TODO: Encode 4 values instead of 1 in 8 bits.
76
+ */
77
+ private readonly pickUpDropOffTypes: Uint8Array;
78
+ /**
79
+ * A binary array of stopIds in the route.
80
+ * [stop1, stop2, stop3,...]
81
+ */
82
+ private readonly stops: Uint32Array;
83
+ /**
84
+ * A reverse mapping of each stop with their index in the route:
85
+ * {
86
+ * 4: 0,
87
+ * 5: 1,
88
+ * ...
89
+ * }
90
+ */
91
+ private readonly stopIndices: Map<StopId, number>;
92
+ /**
93
+ * The identifier of the route as a service shown to users.
94
+ */
95
+ private readonly serviceRouteId: ServiceRouteId;
96
+
97
+ /**
98
+ * The total number of stops in the route.
99
+ */
100
+ private readonly nbStops: number;
101
+
102
+ /**
103
+ * The total number of trips in the route.
104
+ */
105
+ private readonly nbTrips: number;
106
+
107
+ constructor(
108
+ stopTimes: Uint16Array,
109
+ pickUpDropOffTypes: Uint8Array,
110
+ stops: Uint32Array,
111
+ serviceRouteId: ServiceRouteId,
112
+ ) {
113
+ this.stopTimes = stopTimes;
114
+ this.pickUpDropOffTypes = pickUpDropOffTypes;
115
+ this.stops = stops;
116
+ this.serviceRouteId = serviceRouteId;
117
+ this.nbStops = stops.length;
118
+ this.nbTrips = this.stopTimes.length / (this.stops.length * 2);
119
+ this.stopIndices = new Map<number, number>();
120
+ for (let i = 0; i < stops.length; i++) {
121
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
122
+ this.stopIndices.set(stops[i]!, i);
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Serializes the Route into binary arrays.
128
+ *
129
+ * @returns The serialized binary data.
130
+ */
131
+ serialize(): SerializedRoute {
132
+ return {
133
+ stopTimes: this.stopTimes,
134
+ pickUpDropOffTypes: this.pickUpDropOffTypes,
135
+ stops: this.stops,
136
+ serviceRouteId: this.serviceRouteId,
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Checks if stop A is before stop B in the route.
142
+ *
143
+ * @param stopA - The StopId of the first stop.
144
+ * @param stopB - The StopId of the second stop.
145
+ * @returns True if stop A is before stop B, false otherwise.
146
+ */
147
+ isBefore(stopA: StopId, stopB: StopId): boolean {
148
+ const stopAIndex = this.stopIndices.get(stopA);
149
+ if (stopAIndex === undefined) {
150
+ throw new Error(
151
+ `Stop index ${stopAIndex} not found in route ${this.serviceRouteId}`,
152
+ );
153
+ }
154
+ const stopBIndex = this.stopIndices.get(stopB);
155
+ if (stopBIndex === undefined) {
156
+ throw new Error(
157
+ `Stop index ${stopBIndex} not found in route ${this.serviceRouteId}`,
158
+ );
159
+ }
160
+ return stopAIndex < stopBIndex;
161
+ }
162
+
163
+ /**
164
+ * Retrieves the number of stops in the route.
165
+ *
166
+ * @returns The total number of stops in the route.
167
+ */
168
+ getNbStops(): number {
169
+ return this.nbStops;
170
+ }
171
+
172
+ /**
173
+ * Finds the ServiceRouteId of the route. It corresponds the identifier
174
+ * of the service shown to the end user as a route.
175
+ *
176
+ * @returns The ServiceRouteId of the route.
177
+ */
178
+ serviceRoute(): ServiceRouteId {
179
+ return this.serviceRouteId;
180
+ }
181
+
182
+ /**
183
+ * Retrieves the arrival time at a specific stop for a given trip.
184
+ *
185
+ * @param stopId - The identifier of the stop.
186
+ * @param tripIndex - The index of the trip.
187
+ * @returns The arrival time at the specified stop and trip as a Time object.
188
+ */
189
+ arrivalAt(stopId: StopId, tripIndex: TripIndex): Time {
190
+ const arrivalIndex =
191
+ (tripIndex * this.stops.length + this.stopIndex(stopId)) * 2;
192
+ const arrival = this.stopTimes[arrivalIndex];
193
+ if (arrival === undefined) {
194
+ throw new Error(
195
+ `Arrival time not found for stop ${stopId} at trip index ${tripIndex} in route ${this.serviceRouteId}`,
196
+ );
197
+ }
198
+ return Time.fromMinutes(arrival);
199
+ }
200
+
201
+ /**
202
+ * Retrieves the departure time at a specific stop for a given trip.
203
+ *
204
+ * @param stopId - The identifier of the stop.
205
+ * @param tripIndex - The index of the trip.
206
+ * @returns The departure time at the specified stop and trip as a Time object.
207
+ */
208
+ departureFrom(stopId: StopId, tripIndex: TripIndex): Time {
209
+ const departureIndex =
210
+ (tripIndex * this.stops.length + this.stopIndex(stopId)) * 2 + 1;
211
+ const departure = this.stopTimes[departureIndex];
212
+ if (departure === undefined) {
213
+ throw new Error(
214
+ `Departure time not found for stop ${stopId} at trip index ${tripIndex} in route ${this.serviceRouteId}`,
215
+ );
216
+ }
217
+ return Time.fromMinutes(departure);
218
+ }
219
+
220
+ /**
221
+ * Retrieves the pick-up type for a specific stop and trip.
222
+ *
223
+ * @param stopId - The identifier of the stop.
224
+ * @param tripIndex - The index of the trip.
225
+ * @returns The pick-up type at the specified stop and trip.
226
+ */
227
+ pickUpTypeFrom(stopId: StopId, tripIndex: TripIndex): PickUpDropOffType {
228
+ const pickUpIndex =
229
+ (tripIndex * this.stops.length + this.stopIndex(stopId)) * 2;
230
+ const pickUpValue = this.pickUpDropOffTypes[pickUpIndex];
231
+ if (pickUpValue === undefined) {
232
+ throw new Error(
233
+ `Pick up type not found for stop ${stopId} at trip index ${tripIndex} in route ${this.serviceRouteId}`,
234
+ );
235
+ }
236
+ return toPickupDropOffType(pickUpValue);
237
+ }
238
+
239
+ /**
240
+ * Retrieves the drop-off type for a specific stop and trip.
241
+ *
242
+ * @param stopId - The identifier of the stop.
243
+ * @param tripIndex - The index of the trip.
244
+ * @returns The drop-off type at the specified stop and trip.
245
+ */
246
+ dropOffTypeAt(stopId: StopId, tripIndex: TripIndex): PickUpDropOffType {
247
+ const dropOffIndex =
248
+ (tripIndex * this.stops.length + this.stopIndex(stopId)) * 2 + 1;
249
+ const dropOffValue = this.pickUpDropOffTypes[dropOffIndex];
250
+ if (dropOffValue === undefined) {
251
+ throw new Error(
252
+ `Drop off type not found for stop ${stopId} at trip index ${tripIndex} in route ${this.serviceRouteId}`,
253
+ );
254
+ }
255
+ return toPickupDropOffType(dropOffValue);
256
+ }
257
+
258
+ /**
259
+ * Iterates over the stops in the route, starting from an optional specified stop.
260
+ * If no start stop is provided, the iteration begins from the first stop in the route.
261
+ *
262
+ * @param [startStopId] - (Optional) The StopId of the stop to start the iteration from.
263
+ * @returns An IterableIterator of StopIds, starting from the specified stop or the first stop.
264
+ * @throws An error if the specified start stop is not found in the route.
265
+ */
266
+ stopsIterator(startStopId?: StopId): IterableIterator<StopId> {
267
+ const startIndex =
268
+ startStopId !== undefined ? this.stopIndices.get(startStopId) : 0;
269
+ if (startIndex === undefined) {
270
+ throw new Error(
271
+ `Start stop ${startStopId} not found in route ${this.serviceRouteId}`,
272
+ );
273
+ }
274
+
275
+ function* generator(
276
+ stops: Uint32Array,
277
+ startIndex: number,
278
+ ): IterableIterator<StopId> {
279
+ for (let i = startIndex; i < stops.length; i++) {
280
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
281
+ yield stops[i]!;
282
+ }
283
+ }
284
+
285
+ return generator(this.stops, startIndex);
286
+ }
287
+
288
+ /**
289
+ * Finds the earliest trip that can be taken from a specific stop on a given route,
290
+ * optionally constrained by a latest trip index and a time before which the trip
291
+ * should not depart.
292
+ * *
293
+ * @param stopId - The StopId of the stop where the trip should be found.
294
+ * @param [after=Time.origin()] - The earliest time after which the trip should depart.
295
+ * If not provided, searches all available trips.
296
+ * @param [beforeTrip] - (Optional) The index of the trip before which the search should be constrained.
297
+ * If not provided, searches all available trips.
298
+ * @returns The index of the earliest trip meeting the criteria, or undefined if no such trip is found.
299
+ */
300
+ findEarliestTrip(
301
+ stopId: StopId,
302
+ after: Time = Time.origin(),
303
+ beforeTrip?: TripIndex,
304
+ ): TripIndex | undefined {
305
+ const maxTripIndex =
306
+ beforeTrip !== undefined
307
+ ? Math.min(beforeTrip - 1, this.nbTrips - 1)
308
+ : this.nbTrips - 1;
309
+ if (maxTripIndex < 0) {
310
+ return undefined;
311
+ }
312
+ let earliestTripIndex: TripIndex | undefined;
313
+ let lowTrip = 0;
314
+ let highTrip = maxTripIndex;
315
+
316
+ while (lowTrip <= highTrip) {
317
+ const midTrip = Math.floor((lowTrip + highTrip) / 2);
318
+ const departure = this.departureFrom(stopId, midTrip);
319
+ const pickUpType = this.pickUpTypeFrom(stopId, midTrip);
320
+ if (
321
+ (departure.isAfter(after) || departure.equals(after)) &&
322
+ pickUpType !== 'NOT_AVAILABLE'
323
+ ) {
324
+ earliestTripIndex = midTrip;
325
+ highTrip = midTrip - 1;
326
+ } else {
327
+ lowTrip = midTrip + 1;
328
+ }
329
+ }
330
+ return earliestTripIndex;
331
+ }
332
+
333
+ /**
334
+ * Retrieves the index of a stop within the route.
335
+ * @param stopId The StopId of the stop to locate in the route.
336
+ * @returns The index of the stop in the route.
337
+ */
338
+ private stopIndex(stopId: StopId): number {
339
+ const stopIndex = this.stopIndices.get(stopId);
340
+ if (stopIndex === undefined) {
341
+ throw new Error(
342
+ `Stop index for ${stopId} not found in route ${this.serviceRouteId}`,
343
+ );
344
+ }
345
+ return stopIndex;
346
+ }
347
+ }
@@ -107,9 +107,12 @@ export class Time {
107
107
  if (
108
108
  hoursStr === undefined ||
109
109
  minutesStr === undefined ||
110
+ hoursStr.trim() === '' ||
111
+ minutesStr.trim() === '' ||
110
112
  isNaN(Number(hoursStr)) ||
111
113
  isNaN(Number(minutesStr)) ||
112
- (secondsStr !== undefined && isNaN(Number(secondsStr)))
114
+ (secondsStr !== undefined &&
115
+ (secondsStr.trim() === '' || isNaN(Number(secondsStr))))
113
116
  ) {
114
117
  throw new Error(
115
118
  'Input string must be in the format "HH:MM:SS" or "HH:MM".',
@@ -127,8 +130,11 @@ export class Time {
127
130
  * @returns A string representing the time.
128
131
  */
129
132
  toString(): string {
130
- const hours = Math.floor(this.minutesSinceMidnight / 60);
133
+ let hours = Math.floor(this.minutesSinceMidnight / 60);
131
134
  const minutes = Math.floor(this.minutesSinceMidnight % 60);
135
+ if (hours >= 24) {
136
+ hours = hours % 24;
137
+ }
132
138
  return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
133
139
  }
134
140
 
@@ -188,9 +194,7 @@ export class Time {
188
194
  throw new Error('At least one Time instance is required.');
189
195
  }
190
196
  return times.reduce((maxTime, currentTime) => {
191
- return currentTime.toMinutes() > maxTime.toMinutes()
192
- ? currentTime
193
- : maxTime;
197
+ return currentTime.isAfter(maxTime) ? currentTime : maxTime;
194
198
  });
195
199
  }
196
200
 
@@ -205,9 +209,37 @@ export class Time {
205
209
  throw new Error('At least one Time instance is required.');
206
210
  }
207
211
  return times.reduce((minTime, currentTime) => {
208
- return currentTime.toMinutes() < minTime.toMinutes()
209
- ? currentTime
210
- : minTime;
212
+ return currentTime.isBefore(minTime) ? currentTime : minTime;
211
213
  });
212
214
  }
215
+
216
+ /**
217
+ * Determines if the current Time instance is after another Time instance.
218
+ *
219
+ * @param otherTime - A Time instance to compare against.
220
+ * @returns True if the current Time instance is after the other Time instance, otherwise false.
221
+ */
222
+ isAfter(otherTime: Time): boolean {
223
+ return this.minutesSinceMidnight > otherTime.toMinutes();
224
+ }
225
+
226
+ /**
227
+ * Determines if the current Time instance is before another Time instance.
228
+ *
229
+ * @param otherTime - A Time instance to compare against.
230
+ * @returns True if the current Time instance is before the other Time instance, otherwise false.
231
+ */
232
+ isBefore(otherTime: Time): boolean {
233
+ return this.minutesSinceMidnight < otherTime.toMinutes();
234
+ }
235
+
236
+ /**
237
+ * Determines if the current Time instance is equal to another Time instance.
238
+ *
239
+ * @param otherTime - A Time instance to compare against.
240
+ * @returns True if the current Time instance is equal to the other Time instance, otherwise false.
241
+ */
242
+ equals(otherTime: Time): boolean {
243
+ return this.minutesSinceMidnight === otherTime.toMinutes();
244
+ }
213
245
  }