minotor 3.0.2 → 5.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +9 -3
  2. package/README.md +1 -0
  3. package/dist/cli.mjs +294 -307
  4. package/dist/cli.mjs.map +1 -1
  5. package/dist/gtfs/trips.d.ts +12 -6
  6. package/dist/parser.cjs.js +290 -302
  7. package/dist/parser.cjs.js.map +1 -1
  8. package/dist/parser.esm.js +290 -302
  9. package/dist/parser.esm.js.map +1 -1
  10. package/dist/router.cjs.js +1 -1
  11. package/dist/router.cjs.js.map +1 -1
  12. package/dist/router.d.ts +2 -2
  13. package/dist/router.esm.js +1 -1
  14. package/dist/router.esm.js.map +1 -1
  15. package/dist/router.umd.js +1 -1
  16. package/dist/router.umd.js.map +1 -1
  17. package/dist/routing/route.d.ts +3 -3
  18. package/dist/timetable/io.d.ts +5 -4
  19. package/dist/timetable/proto/timetable.d.ts +7 -16
  20. package/dist/timetable/route.d.ts +7 -5
  21. package/dist/timetable/timetable.d.ts +7 -5
  22. package/package.json +1 -1
  23. package/src/__e2e__/timetable/timetable.bin +2 -2
  24. package/src/cli/repl.ts +0 -1
  25. package/src/gtfs/__tests__/parser.test.ts +2 -2
  26. package/src/gtfs/__tests__/routes.test.ts +3 -0
  27. package/src/gtfs/__tests__/trips.test.ts +123 -166
  28. package/src/gtfs/parser.ts +50 -9
  29. package/src/gtfs/routes.ts +1 -0
  30. package/src/gtfs/trips.ts +195 -112
  31. package/src/router.ts +2 -2
  32. package/src/routing/__tests__/route.test.ts +3 -3
  33. package/src/routing/__tests__/router.test.ts +186 -203
  34. package/src/routing/route.ts +3 -3
  35. package/src/routing/router.ts +1 -1
  36. package/src/timetable/__tests__/io.test.ts +52 -64
  37. package/src/timetable/__tests__/route.test.ts +25 -17
  38. package/src/timetable/__tests__/timetable.test.ts +16 -24
  39. package/src/timetable/io.ts +20 -19
  40. package/src/timetable/proto/timetable.proto +7 -9
  41. package/src/timetable/proto/timetable.ts +80 -202
  42. package/src/timetable/route.ts +27 -13
  43. package/src/timetable/timetable.ts +20 -16
@@ -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,
@@ -112,15 +138,21 @@ export class GtfsParser {
112
138
  );
113
139
  const stopsAdjacency = buildStopsAdjacencyStructure(
114
140
  validStopIds,
141
+ validGtfsRoutes,
115
142
  routesAdjacency,
116
143
  transfers,
117
144
  );
118
- log.info(`${routesAdjacency.size} valid unique routes.`);
145
+ const stopTimesEnd = performance.now();
146
+ log.info(
147
+ `${routesAdjacency.length} valid unique routes. (${(stopTimesEnd - stopTimesStart).toFixed(2)}ms)`,
148
+ );
119
149
 
120
150
  log.info(`Removing unused stops.`);
151
+ const indexStopsStart = performance.now();
121
152
  const stops = indexStops(parsedStops, validStopIds);
153
+ const indexStopsEnd = performance.now();
122
154
  log.info(
123
- `${stops.size} used stop stops, ${parsedStops.size - stops.size} unused.`,
155
+ `${stops.size} used stop stops, ${parsedStops.size - stops.size} unused. (${(indexStopsEnd - indexStopsStart).toFixed(2)}ms)`,
124
156
  );
125
157
 
126
158
  await zip.close();
@@ -132,7 +164,12 @@ export class GtfsParser {
132
164
  );
133
165
 
134
166
  log.info(`Building stops index.`);
167
+ const stopsIndexStart = performance.now();
135
168
  const stopsIndex = new StopsIndex(stops);
169
+ const stopsIndexEnd = performance.now();
170
+ log.info(
171
+ `Stops index built. (${(stopsIndexEnd - stopsIndexStart).toFixed(2)}ms)`,
172
+ );
136
173
 
137
174
  log.info('Parsing complete.');
138
175
  return { timetable, stopsIndex };
@@ -149,12 +186,16 @@ export class GtfsParser {
149
186
  const zip = new StreamZip.async({ file: this.path });
150
187
 
151
188
  log.info(`Parsing ${STOPS_FILE}`);
189
+ const stopsStart = performance.now();
152
190
  const stopsStream = await zip.stream(STOPS_FILE);
153
191
  const stops = indexStops(
154
192
  await parseStops(stopsStream, this.profile.platformParser),
155
193
  );
194
+ const stopsEnd = performance.now();
156
195
 
157
- log.info(`${stops.size} parsed stops.`);
196
+ log.info(
197
+ `${stops.size} parsed stops. (${(stopsEnd - stopsStart).toFixed(2)}ms)`,
198
+ );
158
199
 
159
200
  await zip.close();
160
201
 
@@ -42,6 +42,7 @@ export const parseRoutes = async (
42
42
  routes.set(line.route_id, {
43
43
  name: line.route_short_name,
44
44
  type: routeType,
45
+ routes: [],
45
46
  });
46
47
  }
47
48
  return routes;
package/src/gtfs/trips.ts CHANGED
@@ -6,10 +6,8 @@ import {
6
6
  NOT_AVAILABLE,
7
7
  REGULAR,
8
8
  Route,
9
- RouteId,
10
9
  } from '../timetable/route.js';
11
10
  import {
12
- RoutesAdjacency,
13
11
  ServiceRouteId,
14
12
  ServiceRoutesMap,
15
13
  StopsAdjacency,
@@ -32,10 +30,10 @@ type TripEntry = {
32
30
 
33
31
  export type GtfsPickupDropOffType =
34
32
  | '' // Not specified
35
- | 0 // Regularly scheduled
36
- | 1 // Not available
37
- | 2 // Must phone agency
38
- | 3; // Must coordinate with driver
33
+ | '0' // Regularly scheduled
34
+ | '1' // Not available
35
+ | '2' // Must phone agency
36
+ | '3'; // Must coordinate with driver
39
37
 
40
38
  type StopTimeEntry = {
41
39
  trip_id: TripId;
@@ -49,18 +47,133 @@ type StopTimeEntry = {
49
47
 
50
48
  export type SerializedPickUpDropOffType = 0 | 1 | 2 | 3;
51
49
 
50
+ /**
51
+ * Intermediate data structure for building routes during parsing
52
+ */
53
+ type RouteBuilder = {
54
+ serviceRouteId: ServiceRouteId;
55
+ stops: StopId[];
56
+ trips: Array<{
57
+ firstDeparture: number;
58
+ arrivalTimes: number[];
59
+ departureTimes: number[];
60
+ pickUpTypes: SerializedPickUpDropOffType[];
61
+ dropOffTypes: SerializedPickUpDropOffType[];
62
+ }>;
63
+ };
64
+
65
+ /**
66
+ * Encodes pickup/drop-off types into a Uint8Array using 2 bits per value.
67
+ * Layout per byte: [drop_off_1][pickup_1][drop_off_0][pickup_0] for stops 0 and 1
68
+ */
69
+ export const encodePickUpDropOffTypes = (
70
+ pickUpTypes: SerializedPickUpDropOffType[],
71
+ dropOffTypes: SerializedPickUpDropOffType[],
72
+ ): Uint8Array => {
73
+ const stopsCount = pickUpTypes.length;
74
+ // Each byte stores 2 pickup/drop-off pairs (4 bits each)
75
+ const arraySize = Math.ceil(stopsCount / 2);
76
+ const encoded = new Uint8Array(arraySize);
77
+
78
+ for (let i = 0; i < stopsCount; i++) {
79
+ const byteIndex = Math.floor(i / 2);
80
+ const isSecondPair = i % 2 === 1;
81
+ const dropOffType = dropOffTypes[i];
82
+ const pickUpType = pickUpTypes[i];
83
+
84
+ if (
85
+ dropOffType !== undefined &&
86
+ pickUpType !== undefined &&
87
+ byteIndex < encoded.length
88
+ ) {
89
+ if (isSecondPair) {
90
+ // Second pair: upper 4 bits
91
+ const currentByte = encoded[byteIndex];
92
+ if (currentByte !== undefined) {
93
+ encoded[byteIndex] =
94
+ currentByte | (dropOffType << 4) | (pickUpType << 6);
95
+ }
96
+ } else {
97
+ // First pair: lower 4 bits
98
+ const currentByte = encoded[byteIndex];
99
+ if (currentByte !== undefined) {
100
+ encoded[byteIndex] = currentByte | dropOffType | (pickUpType << 2);
101
+ }
102
+ }
103
+ }
104
+ }
105
+ return encoded;
106
+ };
107
+
108
+ /**
109
+ * Sorts trips by departure time and creates optimized typed arrays
110
+ */
111
+ const finalizeRouteFromBuilder = (builder: RouteBuilder): SerializedRoute => {
112
+ builder.trips.sort((a, b) => a.firstDeparture - b.firstDeparture);
113
+
114
+ const stopsCount = builder.stops.length;
115
+ const tripsCount = builder.trips.length;
116
+ const stopsArray = new Uint32Array(builder.stops);
117
+ const stopTimesArray = new Uint16Array(stopsCount * tripsCount * 2);
118
+ const allPickUpTypes: SerializedPickUpDropOffType[] = [];
119
+ const allDropOffTypes: SerializedPickUpDropOffType[] = [];
120
+
121
+ for (let tripIndex = 0; tripIndex < tripsCount; tripIndex++) {
122
+ const trip = builder.trips[tripIndex];
123
+ if (!trip) {
124
+ throw new Error(`Missing trip data at index ${tripIndex}`);
125
+ }
126
+ const baseIndex = tripIndex * stopsCount * 2;
127
+
128
+ for (let stopIndex = 0; stopIndex < stopsCount; stopIndex++) {
129
+ const timeIndex = baseIndex + stopIndex * 2;
130
+ const arrivalTime = trip.arrivalTimes[stopIndex];
131
+ const departureTime = trip.departureTimes[stopIndex];
132
+ const pickUpType = trip.pickUpTypes[stopIndex];
133
+ const dropOffType = trip.dropOffTypes[stopIndex];
134
+
135
+ if (
136
+ arrivalTime === undefined ||
137
+ departureTime === undefined ||
138
+ pickUpType === undefined ||
139
+ dropOffType === undefined
140
+ ) {
141
+ throw new Error(
142
+ `Missing trip data for trip ${tripIndex} at stop ${stopIndex}`,
143
+ );
144
+ }
145
+
146
+ stopTimesArray[timeIndex] = arrivalTime;
147
+ stopTimesArray[timeIndex + 1] = departureTime;
148
+ allDropOffTypes.push(dropOffType);
149
+ allPickUpTypes.push(pickUpType);
150
+ }
151
+ }
152
+ // Use 2-bit encoding for pickup/drop-off types
153
+ const pickUpDropOffTypesArray = encodePickUpDropOffTypes(
154
+ allPickUpTypes,
155
+ allDropOffTypes,
156
+ );
157
+ return {
158
+ serviceRouteId: builder.serviceRouteId,
159
+ stops: stopsArray,
160
+ stopTimes: stopTimesArray,
161
+ pickUpDropOffTypes: pickUpDropOffTypesArray,
162
+ };
163
+ };
164
+
52
165
  /**
53
166
  * Parses the trips.txt file from a GTFS feed
54
167
  *
55
168
  * @param tripsStream The readable stream containing the trips data.
56
169
  * @param serviceIds A mapping of service IDs to corresponding route IDs.
57
- * @param routeIds A mapping of route IDs to route details.
170
+ * @param serviceRoutes A mapping of route IDs to route details.
58
171
  * @returns A mapping of trip IDs to corresponding route IDs.
59
172
  */
60
173
  export const parseTrips = async (
61
174
  tripsStream: NodeJS.ReadableStream,
62
175
  serviceIds: ServiceIds,
63
- routeIds: ServiceRoutesMap,
176
+ serviceRoutes: ServiceRoutesMap,
64
177
  ): Promise<TripIdsMap> => {
65
178
  const trips: TripIdsMap = new Map();
66
179
  for await (const rawLine of parseCsv(tripsStream, ['stop_sequence'])) {
@@ -69,7 +182,7 @@ export const parseTrips = async (
69
182
  // The trip doesn't correspond to an active service
70
183
  continue;
71
184
  }
72
- if (!routeIds.get(line.route_id)) {
185
+ if (!serviceRoutes.get(line.route_id)) {
73
186
  // The trip doesn't correspond to a supported route
74
187
  continue;
75
188
  }
@@ -80,23 +193,27 @@ export const parseTrips = async (
80
193
 
81
194
  export const buildStopsAdjacencyStructure = (
82
195
  validStops: Set<StopId>,
83
- routes: RoutesAdjacency,
196
+ serviceRoutes: ServiceRoutesMap,
197
+ routes: Route[],
84
198
  transfersMap: TransfersMap,
85
199
  ): StopsAdjacency => {
86
200
  const stopsAdjacency: StopsAdjacency = new Map();
87
- for (const routeId of routes.keys()) {
88
- const route = routes.get(routeId);
89
- if (!route) {
90
- throw new Error(`Route ${routeId} not found`);
91
- }
201
+ routes.forEach((route, index) => {
92
202
  for (const stop of route.stopsIterator()) {
93
203
  if (!stopsAdjacency.get(stop) && validStops.has(stop)) {
94
204
  stopsAdjacency.set(stop, { routes: [], transfers: [] });
95
205
  }
96
206
 
97
- stopsAdjacency.get(stop)?.routes.push(routeId);
207
+ stopsAdjacency.get(stop)?.routes.push(index);
98
208
  }
99
- }
209
+ const serviceRoute = serviceRoutes.get(route.serviceRoute());
210
+ if (!serviceRoute) {
211
+ throw new Error(
212
+ `Service route ${route.serviceRoute()} not found for route ${index}.`,
213
+ );
214
+ }
215
+ serviceRoute.routes.push(index);
216
+ });
100
217
  for (const [stop, transfers] of transfersMap) {
101
218
  const s = stopsAdjacency.get(stop);
102
219
  if (s) {
@@ -124,13 +241,14 @@ export const parseStopTimes = async (
124
241
  stopsMap: ParsedStopsMap,
125
242
  validTripIds: TripIdsMap,
126
243
  validStopIds: Set<StopId>,
127
- ): Promise<RoutesAdjacency> => {
244
+ ): Promise<Route[]> => {
128
245
  /**
129
- * Inserts a trip at the right place in the routes adjacency structure.
246
+ * Adds a trip to the appropriate route builder
130
247
  */
131
248
  const addTrip = (currentTripId: TripId) => {
132
249
  const gtfsRouteId = validTripIds.get(currentTripId);
133
- if (!gtfsRouteId) {
250
+
251
+ if (!gtfsRouteId || stops.length === 0) {
134
252
  stops = [];
135
253
  arrivalTimes = [];
136
254
  departureTimes = [];
@@ -138,90 +256,41 @@ export const parseStopTimes = async (
138
256
  dropOffTypes = [];
139
257
  return;
140
258
  }
259
+
141
260
  const routeId = `${gtfsRouteId}_${hashIds(stops)}`;
142
261
 
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 = {
262
+ const firstDeparture = departureTimes[0];
263
+ if (firstDeparture === undefined) {
264
+ console.warn(`Empty trip ${currentTripId}`);
265
+ stops = [];
266
+ arrivalTimes = [];
267
+ departureTimes = [];
268
+ pickUpTypes = [];
269
+ dropOffTypes = [];
270
+ return;
271
+ }
272
+
273
+ let routeBuilder = routeBuilders.get(routeId);
274
+ if (!routeBuilder) {
275
+ routeBuilder = {
162
276
  serviceRouteId: gtfsRouteId,
163
- stops: stopsArray,
164
- stopTimes: stopTimesArray,
165
- pickUpDropOffTypes: pickUpDropOffTypesArray,
277
+ stops: [...stops],
278
+ trips: [],
166
279
  };
167
- routes.set(routeId, route);
280
+ routeBuilders.set(routeId, routeBuilder);
168
281
  for (const stop of stops) {
169
282
  validStopIds.add(stop);
170
283
  }
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
- }
284
+ }
191
285
 
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);
286
+ routeBuilder.trips.push({
287
+ firstDeparture,
288
+ arrivalTimes: [...arrivalTimes],
289
+ departureTimes: [...departureTimes],
290
+ pickUpTypes: [...pickUpTypes],
291
+ dropOffTypes: [...dropOffTypes],
292
+ });
196
293
 
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
- );
221
-
222
- route.stopTimes = newStopTimes;
223
- route.pickUpDropOffTypes = newPickUpDropOffTypes;
224
- }
225
294
  stops = [];
226
295
  arrivalTimes = [];
227
296
  departureTimes = [];
@@ -229,7 +298,8 @@ export const parseStopTimes = async (
229
298
  dropOffTypes = [];
230
299
  };
231
300
 
232
- const routes: Map<RouteId, SerializedRoute> = new Map();
301
+ type BuilderRouteId = string;
302
+ const routeBuilders: Map<BuilderRouteId, RouteBuilder> = new Map();
233
303
 
234
304
  let previousSeq = 0;
235
305
  let stops: StopId[] = [];
@@ -242,7 +312,9 @@ export const parseStopTimes = async (
242
312
  for await (const rawLine of parseCsv(stopTimesStream, ['stop_sequence'])) {
243
313
  const line = rawLine as StopTimeEntry;
244
314
  if (line.trip_id === currentTripId && line.stop_sequence <= previousSeq) {
245
- console.warn(`Stop sequences not increasing for trip ${line.trip_id}.`);
315
+ console.warn(
316
+ `Stop sequences not increasing for trip ${line.trip_id}: ${line.stop_sequence} > ${previousSeq}.`,
317
+ );
246
318
  continue;
247
319
  }
248
320
  if (!line.arrival_time && !line.departure_time) {
@@ -251,21 +323,32 @@ export const parseStopTimes = async (
251
323
  );
252
324
  continue;
253
325
  }
254
- if (line.pickup_type === 1 && line.drop_off_type === 1) {
326
+ if (line.pickup_type === '1' && line.drop_off_type === '1') {
255
327
  continue;
256
328
  }
257
329
  if (currentTripId && line.trip_id !== currentTripId && stops.length > 0) {
258
330
  addTrip(currentTripId);
259
331
  }
260
332
  currentTripId = line.trip_id;
261
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
262
- stops.push(stopsMap.get(line.stop_id)!.id);
333
+
334
+ const stopData = stopsMap.get(line.stop_id);
335
+ if (!stopData) {
336
+ console.warn(`Unknown stop ID: ${line.stop_id}`);
337
+ continue;
338
+ }
339
+ stops.push(stopData.id);
340
+
263
341
  const departure = line.departure_time ?? line.arrival_time;
264
342
  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());
343
+
344
+ if (!arrival || !departure) {
345
+ console.warn(
346
+ `Missing time data for ${line.trip_id} at stop ${line.stop_id}`,
347
+ );
348
+ continue;
349
+ }
350
+ arrivalTimes.push(toTime(arrival).toMinutes());
351
+ departureTimes.push(toTime(departure).toMinutes());
269
352
  pickUpTypes.push(parsePickupDropOffType(line.pickup_type));
270
353
  dropOffTypes.push(parsePickupDropOffType(line.drop_off_type));
271
354
 
@@ -275,10 +358,10 @@ export const parseStopTimes = async (
275
358
  addTrip(currentTripId);
276
359
  }
277
360
 
278
- const routesAdjacency: RoutesAdjacency = new Map<RouteId, Route>();
279
- for (const [routeId, routeData] of routes) {
280
- routesAdjacency.set(
281
- routeId,
361
+ const routesAdjacency: Route[] = [];
362
+ for (const [, routeBuilder] of routeBuilders) {
363
+ const routeData = finalizeRouteFromBuilder(routeBuilder);
364
+ routesAdjacency.push(
282
365
  new Route(
283
366
  routeData.stopTimes,
284
367
  routeData.pickUpDropOffTypes,
@@ -296,13 +379,13 @@ const parsePickupDropOffType = (
296
379
  switch (gtfsType) {
297
380
  default:
298
381
  return REGULAR;
299
- case 0:
382
+ case '0':
300
383
  return REGULAR;
301
- case 1:
384
+ case '1':
302
385
  return NOT_AVAILABLE;
303
- case 2:
386
+ case '2':
304
387
  return MUST_PHONE_AGENCY;
305
- case 3:
388
+ case '3':
306
389
  return MUST_COORDINATE_WITH_DRIVER;
307
390
  }
308
391
  };
package/src/router.ts CHANGED
@@ -12,7 +12,7 @@ import { Duration } from './timetable/duration.js';
12
12
  import { Time } from './timetable/time.js';
13
13
  import type {
14
14
  RouteType,
15
- ServiceRoute,
15
+ ServiceRouteInfo,
16
16
  TransferType,
17
17
  } from './timetable/timetable.js';
18
18
  import { Timetable } from './timetable/timetable.js';
@@ -34,7 +34,7 @@ export type {
34
34
  LocationType,
35
35
  ReachingTime,
36
36
  RouteType,
37
- ServiceRoute,
37
+ ServiceRouteInfo,
38
38
  SourceStopId,
39
39
  Stop,
40
40
  StopId,
@@ -4,7 +4,7 @@ import { describe, it } from 'node:test';
4
4
  import { Stop } from '../../stops/stops.js';
5
5
  import { Duration } from '../../timetable/duration.js';
6
6
  import { Time } from '../../timetable/time.js';
7
- import { ServiceRoute, TransferType } from '../../timetable/timetable.js';
7
+ import { ServiceRouteInfo, TransferType } from '../../timetable/timetable.js';
8
8
  import { Route } from '../route.js';
9
9
 
10
10
  describe('Route', () => {
@@ -40,12 +40,12 @@ describe('Route', () => {
40
40
  children: [],
41
41
  };
42
42
 
43
- const serviceRoute: ServiceRoute = {
43
+ const serviceRoute: ServiceRouteInfo = {
44
44
  type: 'BUS',
45
45
  name: 'Route 1',
46
46
  };
47
47
 
48
- const serviceRoute2: ServiceRoute = {
48
+ const serviceRoute2: ServiceRouteInfo = {
49
49
  type: 'RAIL',
50
50
  name: 'Route 2',
51
51
  };