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.
@@ -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: [pickupTypeStop1, dropOffTypeStop1, pickupTypeStop2, dropOffTypeStop2, etc.]
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 binary Uint8Array.
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
- * Format: [pickupTypeStop1, dropOffTypeStop1, pickupTypeStop2, dropOffTypeStop2, etc.]
38
- * TODO: Encode 4 values instead of 1 in 8 bits.
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.3";
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,6 +1,6 @@
1
1
  {
2
2
  "name": "minotor",
3
- "version": "3.0.2",
3
+ "version": "4.0.0",
4
4
  "description": "A lightweight client-side transit routing library.",
5
5
  "keywords": [
6
6
  "minotor",
@@ -1,3 +1,3 @@
1
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:6725771bfd0e287d515b3ddd74202eab8023b39808818e422bd57b564013b37c
3
- size 29833126
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
- new Uint8Array(),
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
- new Uint8Array(),
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
- new Uint8Array([REGULAR, REGULAR, REGULAR, REGULAR]),
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
- new Uint8Array([
296
- REGULAR,
297
- REGULAR,
298
- REGULAR,
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
- new Uint8Array([
375
- REGULAR,
376
- REGULAR,
377
- REGULAR,
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
- new Uint8Array([REGULAR, REGULAR, REGULAR, REGULAR]),
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
- new Uint8Array([REGULAR, REGULAR]),
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
- new Uint8Array([REGULAR, REGULAR]),
508
+ encodePickUpDropOffTypes([REGULAR], [REGULAR]),
520
509
  new Uint32Array([0]),
521
510
  'routeA',
522
511
  ),
@@ -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
- log.info(`${parsedStops.size} parsed stops.`);
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
- log.info(`${validServiceIds.size} valid services.`);
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
- log.info(`${validServiceIds.size} valid services.`);
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
- log.info(`${validGtfsRoutes.size} valid GTFS routes.`);
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
- log.info(`${trips.size} valid trips.`);
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
- log.info(`${transfers.size} valid transfers.`);
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
- log.info(`${routesAdjacency.size} valid unique routes.`);
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(`${stops.size} parsed stops.`);
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
- * Inserts a trip at the right place in the routes adjacency structure.
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
- if (!gtfsRouteId) {
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
- let route = routes.get(routeId);
144
- if (!route) {
145
- const stopsCount = stops.length;
146
- const stopsArray = new Uint32Array(stops);
147
- const stopTimesArray = new Uint16Array(stopsCount * 2);
148
- for (let i = 0; i < stopsCount; i++) {
149
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
150
- stopTimesArray[i * 2] = arrivalTimes[i]!;
151
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
152
- stopTimesArray[i * 2 + 1] = departureTimes[i]!;
153
- }
154
- const pickUpDropOffTypesArray = new Uint8Array(stopsCount * 2);
155
- for (let i = 0; i < stopsCount; i++) {
156
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
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: stopsArray,
164
- stopTimes: stopTimesArray,
165
- pickUpDropOffTypes: pickUpDropOffTypesArray,
275
+ stops: [...stops],
276
+ trips: [],
166
277
  };
167
- routes.set(routeId, route);
278
+ routeBuilders.set(routeId, routeBuilder);
168
279
  for (const stop of stops) {
169
280
  validStopIds.add(stop);
170
281
  }
171
- } else {
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
- newStopTimes.set(route.stopTimes.slice(0, insertPosition * 2), 0);
198
- newPickUpDropOffTypes.set(
199
- route.pickUpDropOffTypes.slice(0, insertPosition * 2),
200
- 0,
201
- );
202
- for (let i = 0; i < stopsCount; i++) {
203
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
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 routes: Map<RouteId, SerializedRoute> = new Map();
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(`Stop sequences not increasing for trip ${line.trip_id}.`);
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
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
262
- stops.push(stopsMap.get(line.stop_id)!.id);
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
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
266
- arrivalTimes.push(toTime(arrival!).toMinutes());
267
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
268
- departureTimes.push(toTime(departure!).toMinutes());
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, routeData] of routes) {
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
  };