minotor 3.0.1 → 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.
- package/.cspell.json +12 -1
- package/.gitattributes +3 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +3 -0
- package/.github/workflows/minotor.yml +17 -1
- package/CHANGELOG.md +2 -2
- package/README.md +34 -14
- package/dist/__e2e__/router.test.d.ts +1 -0
- package/dist/cli/perf.d.ts +28 -0
- package/dist/cli/utils.d.ts +6 -2
- package/dist/cli.mjs +1967 -823
- package/dist/cli.mjs.map +1 -1
- package/dist/gtfs/trips.d.ts +1 -0
- package/dist/gtfs/utils.d.ts +1 -1
- package/dist/parser.cjs.js +1030 -627
- package/dist/parser.cjs.js.map +1 -1
- package/dist/parser.d.ts +4 -2
- package/dist/parser.esm.js +1030 -627
- package/dist/parser.esm.js.map +1 -1
- package/dist/router.cjs.js +1 -1
- package/dist/router.cjs.js.map +1 -1
- package/dist/router.d.ts +10 -5
- package/dist/router.esm.js +1 -1
- package/dist/router.esm.js.map +1 -1
- package/dist/router.umd.js +1 -1
- package/dist/router.umd.js.map +1 -1
- package/dist/routing/__tests__/result.test.d.ts +1 -0
- package/dist/routing/query.d.ts +27 -6
- package/dist/routing/result.d.ts +1 -1
- package/dist/routing/route.d.ts +47 -2
- package/dist/routing/router.d.ts +15 -1
- package/dist/stops/stopsIndex.d.ts +3 -3
- package/dist/timetable/__tests__/route.test.d.ts +1 -0
- package/dist/timetable/__tests__/time.test.d.ts +1 -0
- package/dist/timetable/io.d.ts +7 -1
- package/dist/timetable/proto/timetable.d.ts +1 -1
- package/dist/timetable/route.d.ts +155 -0
- package/dist/timetable/time.d.ts +21 -0
- package/dist/timetable/timetable.d.ts +41 -61
- package/package.json +36 -34
- package/src/__e2e__/benchmark.json +22 -0
- package/src/__e2e__/router.test.ts +209 -0
- package/src/__e2e__/timetable/stops.bin +3 -0
- package/src/__e2e__/timetable/timetable.bin +3 -0
- package/src/cli/minotor.ts +51 -1
- package/src/cli/perf.ts +136 -0
- package/src/cli/repl.ts +26 -13
- package/src/cli/utils.ts +6 -28
- package/src/gtfs/__tests__/parser.test.ts +12 -15
- package/src/gtfs/__tests__/services.test.ts +1 -0
- package/src/gtfs/__tests__/transfers.test.ts +0 -1
- package/src/gtfs/__tests__/trips.test.ts +67 -74
- package/src/gtfs/profiles/ch.ts +1 -1
- package/src/gtfs/routes.ts +4 -4
- package/src/gtfs/services.ts +15 -2
- package/src/gtfs/stops.ts +7 -3
- package/src/gtfs/transfers.ts +6 -3
- package/src/gtfs/trips.ts +33 -16
- package/src/gtfs/utils.ts +13 -2
- package/src/parser.ts +4 -2
- package/src/router.ts +17 -11
- package/src/routing/__tests__/result.test.ts +392 -0
- package/src/routing/__tests__/router.test.ts +94 -137
- package/src/routing/query.ts +28 -7
- package/src/routing/result.ts +10 -5
- package/src/routing/route.ts +95 -9
- package/src/routing/router.ts +82 -66
- package/src/stops/__tests__/io.test.ts +1 -1
- package/src/stops/__tests__/stopFinder.test.ts +1 -1
- package/src/stops/proto/stops.ts +4 -4
- package/src/stops/stopsIndex.ts +3 -3
- package/src/timetable/__tests__/io.test.ts +16 -23
- package/src/timetable/__tests__/route.test.ts +317 -0
- package/src/timetable/__tests__/time.test.ts +494 -0
- package/src/timetable/__tests__/timetable.test.ts +64 -75
- package/src/timetable/io.ts +32 -26
- package/src/timetable/proto/timetable.proto +1 -1
- package/src/timetable/proto/timetable.ts +13 -13
- package/src/timetable/route.ts +347 -0
- package/src/timetable/time.ts +40 -8
- package/src/timetable/timetable.ts +74 -165
- package/tsconfig.build.json +1 -1
package/src/timetable/io.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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((
|
|
157
|
+
routesAdjacency.forEach((route: Route, key: string) => {
|
|
158
|
+
const routeData = route.serialize();
|
|
150
159
|
protoRoutesAdjacency.routes[key] = {
|
|
151
|
-
stopTimes: uint16ArrayToBytes(
|
|
152
|
-
pickUpDropOffTypes:
|
|
153
|
-
stops: uint32ArrayToBytes(
|
|
154
|
-
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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;
|
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/timetable/time.ts
CHANGED
|
@@ -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 &&
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
}
|