minotor 3.0.2 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -3
- package/README.md +1 -0
- package/dist/cli.mjs +175 -98
- package/dist/cli.mjs.map +1 -1
- package/dist/gtfs/trips.d.ts +6 -1
- package/dist/parser.cjs.js +172 -94
- package/dist/parser.cjs.js.map +1 -1
- package/dist/parser.esm.js +172 -94
- 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.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/timetable/proto/timetable.d.ts +2 -1
- package/dist/timetable/route.d.ts +6 -4
- package/dist/timetable/timetable.d.ts +1 -1
- package/package.json +1 -1
- package/src/__e2e__/timetable/timetable.bin +2 -2
- package/src/cli/repl.ts +0 -1
- package/src/gtfs/__tests__/trips.test.ts +15 -26
- package/src/gtfs/parser.ts +49 -9
- package/src/gtfs/trips.ts +176 -95
- package/src/timetable/__tests__/route.test.ts +25 -17
- package/src/timetable/__tests__/timetable.test.ts +7 -11
- package/src/timetable/proto/timetable.proto +2 -1
- package/src/timetable/proto/timetable.ts +2 -1
- package/src/timetable/route.ts +26 -12
- package/src/timetable/timetable.ts +1 -1
|
@@ -37,7 +37,8 @@ export interface Route {
|
|
|
37
37
|
* 1: NOT_AVAILABLE
|
|
38
38
|
* 2: MUST_PHONE_AGENCY
|
|
39
39
|
* 3: MUST_COORDINATE_WITH_DRIVER
|
|
40
|
-
* Format: [
|
|
40
|
+
* Format: [drop_off_1][pickup_1][drop_off_0][pickup_0]
|
|
41
|
+
* 2 bits per value
|
|
41
42
|
*/
|
|
42
43
|
pickUpDropOffTypes: Uint8Array;
|
|
43
44
|
/**
|
|
@@ -28,14 +28,16 @@ export declare class Route {
|
|
|
28
28
|
*/
|
|
29
29
|
private readonly stopTimes;
|
|
30
30
|
/**
|
|
31
|
-
* PickUp and DropOff types represented as a
|
|
32
|
-
* Values:
|
|
31
|
+
* PickUp and DropOff types represented as a 2-bit encoded Uint8Array.
|
|
32
|
+
* Values (2 bits each):
|
|
33
33
|
* 0: REGULAR
|
|
34
34
|
* 1: NOT_AVAILABLE
|
|
35
35
|
* 2: MUST_PHONE_AGENCY
|
|
36
36
|
* 3: MUST_COORDINATE_WITH_DRIVER
|
|
37
|
-
*
|
|
38
|
-
*
|
|
37
|
+
*
|
|
38
|
+
* Encoding format: Each byte contains 2 pickup/drop-off pairs (4 bits each)
|
|
39
|
+
* Bit layout per byte: [pickup_1 (2 bits)][drop_off_1 (2 bits)][pickup_0 (2 bits)][drop_off_0 (2 bits)]
|
|
40
|
+
* Example: For stops 0 and 1 in a trip, one byte encodes all 4 values
|
|
39
41
|
*/
|
|
40
42
|
private readonly pickUpDropOffTypes;
|
|
41
43
|
/**
|
|
@@ -20,7 +20,7 @@ export type ServiceRoute = {
|
|
|
20
20
|
};
|
|
21
21
|
export type ServiceRoutesMap = Map<ServiceRouteId, ServiceRoute>;
|
|
22
22
|
export declare const ALL_TRANSPORT_MODES: Set<RouteType>;
|
|
23
|
-
export declare const CURRENT_VERSION = "0.0.
|
|
23
|
+
export declare const CURRENT_VERSION = "0.0.4";
|
|
24
24
|
/**
|
|
25
25
|
* The internal transit timetable format.
|
|
26
26
|
*/
|
package/package.json
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
version https://git-lfs.github.com/spec/v1
|
|
2
|
-
oid sha256:
|
|
3
|
-
size
|
|
2
|
+
oid sha256:a88ef7ca621f3c18af125d01518732475e649398d50d28981af134aeee17fa6c
|
|
3
|
+
size 24889916
|
package/src/cli/repl.ts
CHANGED
|
@@ -121,7 +121,6 @@ export const startRepl = (stopsPath: string, timetablePath: string) => {
|
|
|
121
121
|
if (bestRoute) {
|
|
122
122
|
console.log(`Found route from ${fromStop.name} to ${toStop.name}:`);
|
|
123
123
|
console.log(bestRoute.toString());
|
|
124
|
-
console.log(JSON.stringify(bestRoute.asJson(), null, 2));
|
|
125
124
|
} else {
|
|
126
125
|
console.log('No route found');
|
|
127
126
|
}
|
|
@@ -14,6 +14,7 @@ import { ParsedStopsMap } from '../stops.js';
|
|
|
14
14
|
import { TransfersMap } from '../transfers.js';
|
|
15
15
|
import {
|
|
16
16
|
buildStopsAdjacencyStructure,
|
|
17
|
+
encodePickUpDropOffTypes,
|
|
17
18
|
parseStopTimes,
|
|
18
19
|
parseTrips,
|
|
19
20
|
TripIdsMap,
|
|
@@ -27,7 +28,7 @@ describe('buildStopsAdjacencyStructure', () => {
|
|
|
27
28
|
'routeA',
|
|
28
29
|
new Route(
|
|
29
30
|
new Uint16Array(),
|
|
30
|
-
|
|
31
|
+
encodePickUpDropOffTypes([], []),
|
|
31
32
|
new Uint32Array([0, 1]),
|
|
32
33
|
'service1',
|
|
33
34
|
),
|
|
@@ -61,7 +62,7 @@ describe('buildStopsAdjacencyStructure', () => {
|
|
|
61
62
|
'routeA',
|
|
62
63
|
new Route(
|
|
63
64
|
new Uint16Array(),
|
|
64
|
-
|
|
65
|
+
encodePickUpDropOffTypes([], []),
|
|
65
66
|
new Uint32Array([0, 1]),
|
|
66
67
|
'service1',
|
|
67
68
|
),
|
|
@@ -222,7 +223,7 @@ describe('GTFS stop times parser', () => {
|
|
|
222
223
|
Time.fromHMS(8, 10, 0).toMinutes(),
|
|
223
224
|
Time.fromHMS(8, 15, 0).toMinutes(),
|
|
224
225
|
]),
|
|
225
|
-
|
|
226
|
+
encodePickUpDropOffTypes([REGULAR, REGULAR], [REGULAR, REGULAR]),
|
|
226
227
|
new Uint32Array([0, 1]),
|
|
227
228
|
'routeA',
|
|
228
229
|
),
|
|
@@ -292,16 +293,10 @@ describe('GTFS stop times parser', () => {
|
|
|
292
293
|
Time.fromHMS(9, 10, 0).toMinutes(),
|
|
293
294
|
Time.fromHMS(9, 15, 0).toMinutes(),
|
|
294
295
|
]),
|
|
295
|
-
|
|
296
|
-
REGULAR,
|
|
297
|
-
REGULAR,
|
|
298
|
-
|
|
299
|
-
REGULAR,
|
|
300
|
-
REGULAR,
|
|
301
|
-
REGULAR,
|
|
302
|
-
REGULAR,
|
|
303
|
-
REGULAR,
|
|
304
|
-
]),
|
|
296
|
+
encodePickUpDropOffTypes(
|
|
297
|
+
[REGULAR, REGULAR, REGULAR, REGULAR],
|
|
298
|
+
[REGULAR, REGULAR, REGULAR, REGULAR],
|
|
299
|
+
),
|
|
305
300
|
new Uint32Array([0, 1]),
|
|
306
301
|
'routeA',
|
|
307
302
|
),
|
|
@@ -371,16 +366,10 @@ describe('GTFS stop times parser', () => {
|
|
|
371
366
|
Time.fromHMS(9, 10, 0).toMinutes(),
|
|
372
367
|
Time.fromHMS(9, 15, 0).toMinutes(),
|
|
373
368
|
]),
|
|
374
|
-
|
|
375
|
-
REGULAR,
|
|
376
|
-
REGULAR,
|
|
377
|
-
|
|
378
|
-
REGULAR,
|
|
379
|
-
REGULAR,
|
|
380
|
-
REGULAR,
|
|
381
|
-
REGULAR,
|
|
382
|
-
REGULAR,
|
|
383
|
-
]),
|
|
369
|
+
encodePickUpDropOffTypes(
|
|
370
|
+
[REGULAR, REGULAR, REGULAR, REGULAR],
|
|
371
|
+
[REGULAR, REGULAR, REGULAR, REGULAR],
|
|
372
|
+
),
|
|
384
373
|
new Uint32Array([0, 1]),
|
|
385
374
|
'routeA',
|
|
386
375
|
),
|
|
@@ -445,7 +434,7 @@ describe('GTFS stop times parser', () => {
|
|
|
445
434
|
Time.fromHMS(8, 10, 0).toMinutes(),
|
|
446
435
|
Time.fromHMS(8, 15, 0).toMinutes(),
|
|
447
436
|
]),
|
|
448
|
-
|
|
437
|
+
encodePickUpDropOffTypes([REGULAR, REGULAR], [REGULAR, REGULAR]),
|
|
449
438
|
new Uint32Array([0, 1]),
|
|
450
439
|
'routeA',
|
|
451
440
|
),
|
|
@@ -457,7 +446,7 @@ describe('GTFS stop times parser', () => {
|
|
|
457
446
|
Time.fromHMS(9, 0, 0).toMinutes(),
|
|
458
447
|
Time.fromHMS(9, 15, 0).toMinutes(),
|
|
459
448
|
]),
|
|
460
|
-
|
|
449
|
+
encodePickUpDropOffTypes([REGULAR], [REGULAR]),
|
|
461
450
|
new Uint32Array([0]),
|
|
462
451
|
'routeA',
|
|
463
452
|
),
|
|
@@ -516,7 +505,7 @@ describe('GTFS stop times parser', () => {
|
|
|
516
505
|
Time.fromHMS(8, 0, 0).toMinutes(),
|
|
517
506
|
Time.fromHMS(8, 5, 0).toMinutes(),
|
|
518
507
|
]),
|
|
519
|
-
|
|
508
|
+
encodePickUpDropOffTypes([REGULAR], [REGULAR]),
|
|
520
509
|
new Uint32Array([0]),
|
|
521
510
|
'routeA',
|
|
522
511
|
),
|
package/src/gtfs/parser.ts
CHANGED
|
@@ -51,6 +51,7 @@ export class GtfsParser {
|
|
|
51
51
|
async parse(
|
|
52
52
|
date: Date,
|
|
53
53
|
): Promise<{ timetable: Timetable; stopsIndex: StopsIndex }> {
|
|
54
|
+
log.setLevel('INFO');
|
|
54
55
|
const zip = new StreamZip.async({ file: this.path });
|
|
55
56
|
const entries = await zip.entries();
|
|
56
57
|
const datetime = DateTime.fromJSDate(date);
|
|
@@ -59,50 +60,75 @@ export class GtfsParser {
|
|
|
59
60
|
const validStopIds = new Set<StopId>();
|
|
60
61
|
|
|
61
62
|
log.info(`Parsing ${STOPS_FILE}`);
|
|
63
|
+
const stopsStart = performance.now();
|
|
62
64
|
const stopsStream = await zip.stream(STOPS_FILE);
|
|
63
65
|
const parsedStops = await parseStops(
|
|
64
66
|
stopsStream,
|
|
65
67
|
this.profile.platformParser,
|
|
66
68
|
);
|
|
67
|
-
|
|
69
|
+
const stopsEnd = performance.now();
|
|
70
|
+
log.info(
|
|
71
|
+
`${parsedStops.size} parsed stops. (${(stopsEnd - stopsStart).toFixed(2)}ms)`,
|
|
72
|
+
);
|
|
68
73
|
|
|
69
74
|
if (entries[CALENDAR_FILE]) {
|
|
70
75
|
log.info(`Parsing ${CALENDAR_FILE}`);
|
|
76
|
+
const calendarStart = performance.now();
|
|
71
77
|
const calendarStream = await zip.stream(CALENDAR_FILE);
|
|
72
78
|
await parseCalendar(calendarStream, validServiceIds, datetime);
|
|
73
|
-
|
|
79
|
+
const calendarEnd = performance.now();
|
|
80
|
+
log.info(
|
|
81
|
+
`${validServiceIds.size} valid services. (${(calendarEnd - calendarStart).toFixed(2)}ms)`,
|
|
82
|
+
);
|
|
74
83
|
}
|
|
75
84
|
|
|
76
85
|
if (entries[CALENDAR_DATES_FILE]) {
|
|
77
86
|
log.info(`Parsing ${CALENDAR_DATES_FILE}`);
|
|
87
|
+
const calendarDatesStart = performance.now();
|
|
78
88
|
const calendarDatesStream = await zip.stream(CALENDAR_DATES_FILE);
|
|
79
89
|
await parseCalendarDates(calendarDatesStream, validServiceIds, datetime);
|
|
80
|
-
|
|
90
|
+
const calendarDatesEnd = performance.now();
|
|
91
|
+
log.info(
|
|
92
|
+
`${validServiceIds.size} valid services. (${(calendarDatesEnd - calendarDatesStart).toFixed(2)}ms)`,
|
|
93
|
+
);
|
|
81
94
|
}
|
|
82
95
|
|
|
83
96
|
log.info(`Parsing ${ROUTES_FILE}`);
|
|
97
|
+
const routesStart = performance.now();
|
|
84
98
|
const routesStream = await zip.stream(ROUTES_FILE);
|
|
85
99
|
const validGtfsRoutes = await parseRoutes(routesStream, this.profile);
|
|
86
|
-
|
|
100
|
+
const routesEnd = performance.now();
|
|
101
|
+
log.info(
|
|
102
|
+
`${validGtfsRoutes.size} valid GTFS routes. (${(routesEnd - routesStart).toFixed(2)}ms)`,
|
|
103
|
+
);
|
|
87
104
|
|
|
88
105
|
log.info(`Parsing ${TRIPS_FILE}`);
|
|
106
|
+
const tripsStart = performance.now();
|
|
89
107
|
const tripsStream = await zip.stream(TRIPS_FILE);
|
|
90
108
|
const trips = await parseTrips(
|
|
91
109
|
tripsStream,
|
|
92
110
|
validServiceIds,
|
|
93
111
|
validGtfsRoutes,
|
|
94
112
|
);
|
|
95
|
-
|
|
113
|
+
const tripsEnd = performance.now();
|
|
114
|
+
log.info(
|
|
115
|
+
`${trips.size} valid trips. (${(tripsEnd - tripsStart).toFixed(2)}ms)`,
|
|
116
|
+
);
|
|
96
117
|
|
|
97
118
|
let transfers = new Map() as TransfersMap;
|
|
98
119
|
if (entries[TRANSFERS_FILE]) {
|
|
99
120
|
log.info(`Parsing ${TRANSFERS_FILE}`);
|
|
121
|
+
const transfersStart = performance.now();
|
|
100
122
|
const transfersStream = await zip.stream(TRANSFERS_FILE);
|
|
101
123
|
transfers = await parseTransfers(transfersStream, parsedStops);
|
|
102
|
-
|
|
124
|
+
const transfersEnd = performance.now();
|
|
125
|
+
log.info(
|
|
126
|
+
`${transfers.size} valid transfers. (${(transfersEnd - transfersStart).toFixed(2)}ms)`,
|
|
127
|
+
);
|
|
103
128
|
}
|
|
104
129
|
|
|
105
130
|
log.info(`Parsing ${STOP_TIMES_FILE}`);
|
|
131
|
+
const stopTimesStart = performance.now();
|
|
106
132
|
const stopTimesStream = await zip.stream(STOP_TIMES_FILE);
|
|
107
133
|
const routesAdjacency = await parseStopTimes(
|
|
108
134
|
stopTimesStream,
|
|
@@ -115,12 +141,17 @@ export class GtfsParser {
|
|
|
115
141
|
routesAdjacency,
|
|
116
142
|
transfers,
|
|
117
143
|
);
|
|
118
|
-
|
|
144
|
+
const stopTimesEnd = performance.now();
|
|
145
|
+
log.info(
|
|
146
|
+
`${routesAdjacency.size} valid unique routes. (${(stopTimesEnd - stopTimesStart).toFixed(2)}ms)`,
|
|
147
|
+
);
|
|
119
148
|
|
|
120
149
|
log.info(`Removing unused stops.`);
|
|
150
|
+
const indexStopsStart = performance.now();
|
|
121
151
|
const stops = indexStops(parsedStops, validStopIds);
|
|
152
|
+
const indexStopsEnd = performance.now();
|
|
122
153
|
log.info(
|
|
123
|
-
`${stops.size} used stop stops, ${parsedStops.size - stops.size} unused
|
|
154
|
+
`${stops.size} used stop stops, ${parsedStops.size - stops.size} unused. (${(indexStopsEnd - indexStopsStart).toFixed(2)}ms)`,
|
|
124
155
|
);
|
|
125
156
|
|
|
126
157
|
await zip.close();
|
|
@@ -132,7 +163,12 @@ export class GtfsParser {
|
|
|
132
163
|
);
|
|
133
164
|
|
|
134
165
|
log.info(`Building stops index.`);
|
|
166
|
+
const stopsIndexStart = performance.now();
|
|
135
167
|
const stopsIndex = new StopsIndex(stops);
|
|
168
|
+
const stopsIndexEnd = performance.now();
|
|
169
|
+
log.info(
|
|
170
|
+
`Stops index built. (${(stopsIndexEnd - stopsIndexStart).toFixed(2)}ms)`,
|
|
171
|
+
);
|
|
136
172
|
|
|
137
173
|
log.info('Parsing complete.');
|
|
138
174
|
return { timetable, stopsIndex };
|
|
@@ -149,12 +185,16 @@ export class GtfsParser {
|
|
|
149
185
|
const zip = new StreamZip.async({ file: this.path });
|
|
150
186
|
|
|
151
187
|
log.info(`Parsing ${STOPS_FILE}`);
|
|
188
|
+
const stopsStart = performance.now();
|
|
152
189
|
const stopsStream = await zip.stream(STOPS_FILE);
|
|
153
190
|
const stops = indexStops(
|
|
154
191
|
await parseStops(stopsStream, this.profile.platformParser),
|
|
155
192
|
);
|
|
193
|
+
const stopsEnd = performance.now();
|
|
156
194
|
|
|
157
|
-
log.info(
|
|
195
|
+
log.info(
|
|
196
|
+
`${stops.size} parsed stops. (${(stopsEnd - stopsStart).toFixed(2)}ms)`,
|
|
197
|
+
);
|
|
158
198
|
|
|
159
199
|
await zip.close();
|
|
160
200
|
|
package/src/gtfs/trips.ts
CHANGED
|
@@ -32,10 +32,10 @@ type TripEntry = {
|
|
|
32
32
|
|
|
33
33
|
export type GtfsPickupDropOffType =
|
|
34
34
|
| '' // Not specified
|
|
35
|
-
| 0 // Regularly scheduled
|
|
36
|
-
| 1 // Not available
|
|
37
|
-
| 2 // Must phone agency
|
|
38
|
-
| 3; // Must coordinate with driver
|
|
35
|
+
| '0' // Regularly scheduled
|
|
36
|
+
| '1' // Not available
|
|
37
|
+
| '2' // Must phone agency
|
|
38
|
+
| '3'; // Must coordinate with driver
|
|
39
39
|
|
|
40
40
|
type StopTimeEntry = {
|
|
41
41
|
trip_id: TripId;
|
|
@@ -49,6 +49,121 @@ type StopTimeEntry = {
|
|
|
49
49
|
|
|
50
50
|
export type SerializedPickUpDropOffType = 0 | 1 | 2 | 3;
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Intermediate data structure for building routes during parsing
|
|
54
|
+
*/
|
|
55
|
+
type RouteBuilder = {
|
|
56
|
+
serviceRouteId: ServiceRouteId;
|
|
57
|
+
stops: StopId[];
|
|
58
|
+
trips: Array<{
|
|
59
|
+
firstDeparture: number;
|
|
60
|
+
arrivalTimes: number[];
|
|
61
|
+
departureTimes: number[];
|
|
62
|
+
pickUpTypes: SerializedPickUpDropOffType[];
|
|
63
|
+
dropOffTypes: SerializedPickUpDropOffType[];
|
|
64
|
+
}>;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Encodes pickup/drop-off types into a Uint8Array using 2 bits per value.
|
|
69
|
+
* Layout per byte: [drop_off_1][pickup_1][drop_off_0][pickup_0] for stops 0 and 1
|
|
70
|
+
*/
|
|
71
|
+
export const encodePickUpDropOffTypes = (
|
|
72
|
+
pickUpTypes: SerializedPickUpDropOffType[],
|
|
73
|
+
dropOffTypes: SerializedPickUpDropOffType[],
|
|
74
|
+
): Uint8Array => {
|
|
75
|
+
const stopsCount = pickUpTypes.length;
|
|
76
|
+
// Each byte stores 2 pickup/drop-off pairs (4 bits each)
|
|
77
|
+
const arraySize = Math.ceil(stopsCount / 2);
|
|
78
|
+
const encoded = new Uint8Array(arraySize);
|
|
79
|
+
|
|
80
|
+
for (let i = 0; i < stopsCount; i++) {
|
|
81
|
+
const byteIndex = Math.floor(i / 2);
|
|
82
|
+
const isSecondPair = i % 2 === 1;
|
|
83
|
+
const dropOffType = dropOffTypes[i];
|
|
84
|
+
const pickUpType = pickUpTypes[i];
|
|
85
|
+
|
|
86
|
+
if (
|
|
87
|
+
dropOffType !== undefined &&
|
|
88
|
+
pickUpType !== undefined &&
|
|
89
|
+
byteIndex < encoded.length
|
|
90
|
+
) {
|
|
91
|
+
if (isSecondPair) {
|
|
92
|
+
// Second pair: upper 4 bits
|
|
93
|
+
const currentByte = encoded[byteIndex];
|
|
94
|
+
if (currentByte !== undefined) {
|
|
95
|
+
encoded[byteIndex] =
|
|
96
|
+
currentByte | (dropOffType << 4) | (pickUpType << 6);
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
// First pair: lower 4 bits
|
|
100
|
+
const currentByte = encoded[byteIndex];
|
|
101
|
+
if (currentByte !== undefined) {
|
|
102
|
+
encoded[byteIndex] = currentByte | dropOffType | (pickUpType << 2);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return encoded;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Sorts trips by departure time and creates optimized typed arrays
|
|
112
|
+
*/
|
|
113
|
+
const finalizeRouteFromBuilder = (builder: RouteBuilder): SerializedRoute => {
|
|
114
|
+
builder.trips.sort((a, b) => a.firstDeparture - b.firstDeparture);
|
|
115
|
+
|
|
116
|
+
const stopsCount = builder.stops.length;
|
|
117
|
+
const tripsCount = builder.trips.length;
|
|
118
|
+
const stopsArray = new Uint32Array(builder.stops);
|
|
119
|
+
const stopTimesArray = new Uint16Array(stopsCount * tripsCount * 2);
|
|
120
|
+
const allPickUpTypes: SerializedPickUpDropOffType[] = [];
|
|
121
|
+
const allDropOffTypes: SerializedPickUpDropOffType[] = [];
|
|
122
|
+
|
|
123
|
+
for (let tripIndex = 0; tripIndex < tripsCount; tripIndex++) {
|
|
124
|
+
const trip = builder.trips[tripIndex];
|
|
125
|
+
if (!trip) {
|
|
126
|
+
throw new Error(`Missing trip data at index ${tripIndex}`);
|
|
127
|
+
}
|
|
128
|
+
const baseIndex = tripIndex * stopsCount * 2;
|
|
129
|
+
|
|
130
|
+
for (let stopIndex = 0; stopIndex < stopsCount; stopIndex++) {
|
|
131
|
+
const timeIndex = baseIndex + stopIndex * 2;
|
|
132
|
+
const arrivalTime = trip.arrivalTimes[stopIndex];
|
|
133
|
+
const departureTime = trip.departureTimes[stopIndex];
|
|
134
|
+
const pickUpType = trip.pickUpTypes[stopIndex];
|
|
135
|
+
const dropOffType = trip.dropOffTypes[stopIndex];
|
|
136
|
+
|
|
137
|
+
if (
|
|
138
|
+
arrivalTime === undefined ||
|
|
139
|
+
departureTime === undefined ||
|
|
140
|
+
pickUpType === undefined ||
|
|
141
|
+
dropOffType === undefined
|
|
142
|
+
) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
`Missing trip data for trip ${tripIndex} at stop ${stopIndex}`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
stopTimesArray[timeIndex] = arrivalTime;
|
|
149
|
+
stopTimesArray[timeIndex + 1] = departureTime;
|
|
150
|
+
allDropOffTypes.push(dropOffType);
|
|
151
|
+
allPickUpTypes.push(pickUpType);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Use 2-bit encoding for pickup/drop-off types
|
|
155
|
+
const pickUpDropOffTypesArray = encodePickUpDropOffTypes(
|
|
156
|
+
allPickUpTypes,
|
|
157
|
+
allDropOffTypes,
|
|
158
|
+
);
|
|
159
|
+
return {
|
|
160
|
+
serviceRouteId: builder.serviceRouteId,
|
|
161
|
+
stops: stopsArray,
|
|
162
|
+
stopTimes: stopTimesArray,
|
|
163
|
+
pickUpDropOffTypes: pickUpDropOffTypesArray,
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
|
|
52
167
|
/**
|
|
53
168
|
* Parses the trips.txt file from a GTFS feed
|
|
54
169
|
*
|
|
@@ -126,11 +241,12 @@ export const parseStopTimes = async (
|
|
|
126
241
|
validStopIds: Set<StopId>,
|
|
127
242
|
): Promise<RoutesAdjacency> => {
|
|
128
243
|
/**
|
|
129
|
-
*
|
|
244
|
+
* Adds a trip to the appropriate route builder
|
|
130
245
|
*/
|
|
131
246
|
const addTrip = (currentTripId: TripId) => {
|
|
132
247
|
const gtfsRouteId = validTripIds.get(currentTripId);
|
|
133
|
-
|
|
248
|
+
|
|
249
|
+
if (!gtfsRouteId || stops.length === 0) {
|
|
134
250
|
stops = [];
|
|
135
251
|
arrivalTimes = [];
|
|
136
252
|
departureTimes = [];
|
|
@@ -138,90 +254,41 @@ export const parseStopTimes = async (
|
|
|
138
254
|
dropOffTypes = [];
|
|
139
255
|
return;
|
|
140
256
|
}
|
|
257
|
+
|
|
141
258
|
const routeId = `${gtfsRouteId}_${hashIds(stops)}`;
|
|
142
259
|
|
|
143
|
-
|
|
144
|
-
if (
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
pickUpDropOffTypesArray[i * 2] = pickUpTypes[i]!;
|
|
158
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
159
|
-
pickUpDropOffTypesArray[i * 2 + 1] = dropOffTypes[i]!;
|
|
160
|
-
}
|
|
161
|
-
route = {
|
|
260
|
+
const firstDeparture = departureTimes[0];
|
|
261
|
+
if (firstDeparture === undefined) {
|
|
262
|
+
console.warn(`Empty trip ${currentTripId}`);
|
|
263
|
+
stops = [];
|
|
264
|
+
arrivalTimes = [];
|
|
265
|
+
departureTimes = [];
|
|
266
|
+
pickUpTypes = [];
|
|
267
|
+
dropOffTypes = [];
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
let routeBuilder = routeBuilders.get(routeId);
|
|
272
|
+
if (!routeBuilder) {
|
|
273
|
+
routeBuilder = {
|
|
162
274
|
serviceRouteId: gtfsRouteId,
|
|
163
|
-
stops:
|
|
164
|
-
|
|
165
|
-
pickUpDropOffTypes: pickUpDropOffTypesArray,
|
|
275
|
+
stops: [...stops],
|
|
276
|
+
trips: [],
|
|
166
277
|
};
|
|
167
|
-
|
|
278
|
+
routeBuilders.set(routeId, routeBuilder);
|
|
168
279
|
for (const stop of stops) {
|
|
169
280
|
validStopIds.add(stop);
|
|
170
281
|
}
|
|
171
|
-
}
|
|
172
|
-
const tripFirstStopDeparture = departureTimes[0];
|
|
173
|
-
if (tripFirstStopDeparture === undefined) {
|
|
174
|
-
throw new Error(`Empty trip ${currentTripId}`);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Find the correct position to insert the new trip
|
|
178
|
-
const stopsCount = stops.length;
|
|
179
|
-
let insertPosition = 0;
|
|
180
|
-
const existingTripsCount = route.stopTimes.length / (stopsCount * 2);
|
|
181
|
-
|
|
182
|
-
for (let tripIndex = 0; tripIndex < existingTripsCount; tripIndex++) {
|
|
183
|
-
const currentDeparture =
|
|
184
|
-
route.stopTimes[tripIndex * stopsCount * 2 + 1];
|
|
185
|
-
if (currentDeparture && tripFirstStopDeparture > currentDeparture) {
|
|
186
|
-
insertPosition = (tripIndex + 1) * stopsCount;
|
|
187
|
-
} else {
|
|
188
|
-
break;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// insert data for the new trip at the right place
|
|
193
|
-
const newStopTimesLength = route.stopTimes.length + stopsCount * 2;
|
|
194
|
-
const newStopTimes = new Uint16Array(newStopTimesLength);
|
|
195
|
-
const newPickUpDropOffTypes = new Uint8Array(newStopTimesLength);
|
|
282
|
+
}
|
|
196
283
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
newStopTimes[(insertPosition + i) * 2] = arrivalTimes[i]!;
|
|
205
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
206
|
-
newStopTimes[(insertPosition + i) * 2 + 1] = departureTimes[i]!;
|
|
207
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
208
|
-
newPickUpDropOffTypes[(insertPosition + i) * 2] = pickUpTypes[i]!;
|
|
209
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
210
|
-
newPickUpDropOffTypes[(insertPosition + i) * 2 + 1] = dropOffTypes[i]!;
|
|
211
|
-
}
|
|
212
|
-
const afterInsertionSlice = route.stopTimes.slice(insertPosition * 2);
|
|
213
|
-
newStopTimes.set(afterInsertionSlice, (insertPosition + stopsCount) * 2);
|
|
214
|
-
const afterInsertionTypesSlice = route.pickUpDropOffTypes.slice(
|
|
215
|
-
insertPosition * 2,
|
|
216
|
-
);
|
|
217
|
-
newPickUpDropOffTypes.set(
|
|
218
|
-
afterInsertionTypesSlice,
|
|
219
|
-
(insertPosition + stopsCount) * 2,
|
|
220
|
-
);
|
|
284
|
+
routeBuilder.trips.push({
|
|
285
|
+
firstDeparture,
|
|
286
|
+
arrivalTimes: [...arrivalTimes],
|
|
287
|
+
departureTimes: [...departureTimes],
|
|
288
|
+
pickUpTypes: [...pickUpTypes],
|
|
289
|
+
dropOffTypes: [...dropOffTypes],
|
|
290
|
+
});
|
|
221
291
|
|
|
222
|
-
route.stopTimes = newStopTimes;
|
|
223
|
-
route.pickUpDropOffTypes = newPickUpDropOffTypes;
|
|
224
|
-
}
|
|
225
292
|
stops = [];
|
|
226
293
|
arrivalTimes = [];
|
|
227
294
|
departureTimes = [];
|
|
@@ -229,7 +296,7 @@ export const parseStopTimes = async (
|
|
|
229
296
|
dropOffTypes = [];
|
|
230
297
|
};
|
|
231
298
|
|
|
232
|
-
const
|
|
299
|
+
const routeBuilders: Map<RouteId, RouteBuilder> = new Map();
|
|
233
300
|
|
|
234
301
|
let previousSeq = 0;
|
|
235
302
|
let stops: StopId[] = [];
|
|
@@ -242,7 +309,9 @@ export const parseStopTimes = async (
|
|
|
242
309
|
for await (const rawLine of parseCsv(stopTimesStream, ['stop_sequence'])) {
|
|
243
310
|
const line = rawLine as StopTimeEntry;
|
|
244
311
|
if (line.trip_id === currentTripId && line.stop_sequence <= previousSeq) {
|
|
245
|
-
console.warn(
|
|
312
|
+
console.warn(
|
|
313
|
+
`Stop sequences not increasing for trip ${line.trip_id}: ${line.stop_sequence} > ${previousSeq}.`,
|
|
314
|
+
);
|
|
246
315
|
continue;
|
|
247
316
|
}
|
|
248
317
|
if (!line.arrival_time && !line.departure_time) {
|
|
@@ -251,21 +320,32 @@ export const parseStopTimes = async (
|
|
|
251
320
|
);
|
|
252
321
|
continue;
|
|
253
322
|
}
|
|
254
|
-
if (line.pickup_type === 1 && line.drop_off_type === 1) {
|
|
323
|
+
if (line.pickup_type === '1' && line.drop_off_type === '1') {
|
|
255
324
|
continue;
|
|
256
325
|
}
|
|
257
326
|
if (currentTripId && line.trip_id !== currentTripId && stops.length > 0) {
|
|
258
327
|
addTrip(currentTripId);
|
|
259
328
|
}
|
|
260
329
|
currentTripId = line.trip_id;
|
|
261
|
-
|
|
262
|
-
|
|
330
|
+
|
|
331
|
+
const stopData = stopsMap.get(line.stop_id);
|
|
332
|
+
if (!stopData) {
|
|
333
|
+
console.warn(`Unknown stop ID: ${line.stop_id}`);
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
stops.push(stopData.id);
|
|
337
|
+
|
|
263
338
|
const departure = line.departure_time ?? line.arrival_time;
|
|
264
339
|
const arrival = line.arrival_time ?? line.departure_time;
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
340
|
+
|
|
341
|
+
if (!arrival || !departure) {
|
|
342
|
+
console.warn(
|
|
343
|
+
`Missing time data for ${line.trip_id} at stop ${line.stop_id}`,
|
|
344
|
+
);
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
arrivalTimes.push(toTime(arrival).toMinutes());
|
|
348
|
+
departureTimes.push(toTime(departure).toMinutes());
|
|
269
349
|
pickUpTypes.push(parsePickupDropOffType(line.pickup_type));
|
|
270
350
|
dropOffTypes.push(parsePickupDropOffType(line.drop_off_type));
|
|
271
351
|
|
|
@@ -276,7 +356,8 @@ export const parseStopTimes = async (
|
|
|
276
356
|
}
|
|
277
357
|
|
|
278
358
|
const routesAdjacency: RoutesAdjacency = new Map<RouteId, Route>();
|
|
279
|
-
for (const [routeId,
|
|
359
|
+
for (const [routeId, routeBuilder] of routeBuilders) {
|
|
360
|
+
const routeData = finalizeRouteFromBuilder(routeBuilder);
|
|
280
361
|
routesAdjacency.set(
|
|
281
362
|
routeId,
|
|
282
363
|
new Route(
|
|
@@ -296,13 +377,13 @@ const parsePickupDropOffType = (
|
|
|
296
377
|
switch (gtfsType) {
|
|
297
378
|
default:
|
|
298
379
|
return REGULAR;
|
|
299
|
-
case 0:
|
|
380
|
+
case '0':
|
|
300
381
|
return REGULAR;
|
|
301
|
-
case 1:
|
|
382
|
+
case '1':
|
|
302
383
|
return NOT_AVAILABLE;
|
|
303
|
-
case 2:
|
|
384
|
+
case '2':
|
|
304
385
|
return MUST_PHONE_AGENCY;
|
|
305
|
-
case 3:
|
|
386
|
+
case '3':
|
|
306
387
|
return MUST_COORDINATE_WITH_DRIVER;
|
|
307
388
|
}
|
|
308
389
|
};
|