minotor 1.0.7 → 2.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 (60) hide show
  1. package/CHANGELOG.md +9 -3
  2. package/README.md +3 -2
  3. package/dist/cli.mjs +604 -531
  4. package/dist/cli.mjs.map +1 -1
  5. package/dist/gtfs/stops.d.ts +19 -5
  6. package/dist/gtfs/transfers.d.ts +5 -4
  7. package/dist/gtfs/trips.d.ts +7 -5
  8. package/dist/gtfs/utils.d.ts +7 -8
  9. package/dist/parser.cjs.js +569 -501
  10. package/dist/parser.cjs.js.map +1 -1
  11. package/dist/parser.esm.js +569 -501
  12. package/dist/parser.esm.js.map +1 -1
  13. package/dist/router.cjs.js +1 -1
  14. package/dist/router.cjs.js.map +1 -1
  15. package/dist/router.d.ts +3 -3
  16. package/dist/router.esm.js +1 -1
  17. package/dist/router.esm.js.map +1 -1
  18. package/dist/router.umd.js +1 -1
  19. package/dist/router.umd.js.map +1 -1
  20. package/dist/routing/__tests__/route.test.d.ts +1 -0
  21. package/dist/routing/query.d.ts +7 -7
  22. package/dist/routing/result.d.ts +3 -3
  23. package/dist/routing/route.d.ts +1 -0
  24. package/dist/stops/proto/stops.d.ts +5 -4
  25. package/dist/stops/stops.d.ts +10 -1
  26. package/dist/stops/stopsIndex.d.ts +21 -4
  27. package/dist/timetable/proto/timetable.d.ts +21 -18
  28. package/dist/timetable/timetable.d.ts +38 -14
  29. package/package.json +4 -3
  30. package/src/cli/repl.ts +13 -10
  31. package/src/gtfs/__tests__/parser.test.ts +50 -579
  32. package/src/gtfs/__tests__/stops.test.ts +181 -112
  33. package/src/gtfs/__tests__/transfers.test.ts +170 -12
  34. package/src/gtfs/__tests__/trips.test.ts +212 -141
  35. package/src/gtfs/__tests__/utils.test.ts +4 -4
  36. package/src/gtfs/parser.ts +22 -13
  37. package/src/gtfs/stops.ts +63 -28
  38. package/src/gtfs/transfers.ts +14 -6
  39. package/src/gtfs/trips.ts +110 -47
  40. package/src/gtfs/utils.ts +11 -11
  41. package/src/router.ts +2 -4
  42. package/src/routing/__tests__/route.test.ts +112 -0
  43. package/src/routing/__tests__/router.test.ts +234 -244
  44. package/src/routing/query.ts +7 -7
  45. package/src/routing/result.ts +9 -6
  46. package/src/routing/route.ts +11 -0
  47. package/src/routing/router.ts +26 -24
  48. package/src/stops/__tests__/io.test.ts +9 -8
  49. package/src/stops/__tests__/stopFinder.test.ts +45 -36
  50. package/src/stops/io.ts +8 -5
  51. package/src/stops/proto/stops.proto +8 -7
  52. package/src/stops/proto/stops.ts +68 -38
  53. package/src/stops/stops.ts +13 -1
  54. package/src/stops/stopsIndex.ts +50 -7
  55. package/src/timetable/__tests__/io.test.ts +40 -49
  56. package/src/timetable/__tests__/timetable.test.ts +50 -58
  57. package/src/timetable/io.ts +69 -56
  58. package/src/timetable/proto/timetable.proto +22 -17
  59. package/src/timetable/proto/timetable.ts +94 -184
  60. package/src/timetable/timetable.ts +62 -29
package/src/gtfs/stops.ts CHANGED
@@ -3,14 +3,13 @@ import {
3
3
  LocationType,
4
4
  Longitude,
5
5
  Platform,
6
+ SourceStopId,
6
7
  Stop,
7
8
  StopId,
8
9
  StopsMap,
9
10
  } from '../stops/stops.js';
10
11
  import { Maybe, parseCsv } from './utils.js';
11
12
 
12
- export type StopIds = Set<StopId>;
13
-
14
13
  export type GtfsLocationType =
15
14
  | 0 // simple stop or platform (can also be empty)
16
15
  | 1 // station
@@ -19,15 +18,20 @@ export type GtfsLocationType =
19
18
  | 4; // boarding area
20
19
 
21
20
  export type StopEntry = {
22
- stop_id: StopId;
21
+ stop_id: SourceStopId;
23
22
  stop_name: string;
24
23
  stop_lat?: Latitude;
25
24
  stop_lon?: Longitude;
26
25
  location_type?: GtfsLocationType;
27
- parent_station?: StopId;
26
+ parent_station?: SourceStopId;
28
27
  platform_code?: Platform;
29
28
  };
30
29
 
30
+ type ParsedStop = Stop & {
31
+ parentSourceId?: SourceStopId;
32
+ };
33
+
34
+ export type ParsedStopsMap = Map<SourceStopId, ParsedStop>;
31
35
  /**
32
36
  * Parses the stops.txt file from a GTFS feed.
33
37
  *
@@ -37,14 +41,14 @@ export type StopEntry = {
37
41
  export const parseStops = async (
38
42
  stopsStream: NodeJS.ReadableStream,
39
43
  platformParser?: (stopEntry: StopEntry) => Maybe<Platform>,
40
- validStops?: StopIds,
41
- ): Promise<StopsMap> => {
42
- const stops: StopsMap = new Map();
43
-
44
+ ): Promise<ParsedStopsMap> => {
45
+ const parsedStops = new Map<SourceStopId, ParsedStop>();
46
+ let i = 0;
44
47
  for await (const rawLine of parseCsv(stopsStream)) {
45
48
  const line = rawLine as StopEntry;
46
- const stop: Stop = {
47
- id: line.stop_id,
49
+ const stop: ParsedStop = {
50
+ id: i,
51
+ sourceStopId: line.stop_id + '',
48
52
  name: line.stop_name,
49
53
  lat: line.stop_lat,
50
54
  lon: line.stop_lon,
@@ -52,7 +56,7 @@ export const parseStops = async (
52
56
  ? parseGtfsLocationType(line.location_type)
53
57
  : 'SIMPLE_STOP_OR_PLATFORM',
54
58
  children: [],
55
- ...(line.parent_station && { parent: line.parent_station }),
59
+ ...(line.parent_station && { parentSourceId: line.parent_station }),
56
60
  };
57
61
  if (platformParser) {
58
62
  try {
@@ -64,30 +68,61 @@ export const parseStops = async (
64
68
  console.info(`Could not parse platform for stop ${line.stop_id}.`);
65
69
  }
66
70
  }
67
- stops.set(line.stop_id, stop);
71
+ parsedStops.set(line.stop_id + '', stop);
72
+ i = i + 1;
68
73
  }
69
74
 
70
- for (const [stopId, stop] of stops) {
71
- if (stop.parent) {
72
- const parentStop = stops.get(stop.parent);
75
+ for (const [sourceStopId, stop] of parsedStops) {
76
+ if (stop.parentSourceId) {
77
+ const parentStop = parsedStops.get(stop.parentSourceId);
73
78
  if (!parentStop) {
74
- console.warn(`Cannot find parent stop ${stop.parent} of ${stopId}`);
79
+ console.warn(
80
+ `Cannot find parent stop ${stop.parentSourceId} of ${sourceStopId}`,
81
+ );
75
82
  continue;
76
83
  }
77
- parentStop.children.push(stopId);
84
+ stop.parent = parentStop.id;
85
+ parentStop.children.push(stop.id);
78
86
  }
79
87
  }
80
- if (validStops) {
81
- // Remove all stops which don't have at least one valid stopId as a child,
82
- // a parent or as its own.
83
- for (const [stopId, stop] of stops) {
84
- if (
85
- !validStops.has(stopId) &&
86
- (!stop.parent || !validStops.has(stop.parent)) &&
87
- !stop.children.some((childId) => validStops.has(childId))
88
- ) {
89
- stops.delete(stopId);
90
- }
88
+ return parsedStops;
89
+ };
90
+
91
+ /**
92
+ * Builds the final stop map indexed by internal IDs.
93
+ * Excludes all stops that do not have at least one valid stopId
94
+ * as a child, a parent, or being valid itself.
95
+ *
96
+ * @param parsedStops - The map of parsed stops.
97
+ * @param validStops - A set of valid stop IDs.
98
+ * @returns A map of stops indexed by internal IDs.
99
+ */
100
+ export const indexStops = (
101
+ parsedStops: ParsedStopsMap,
102
+ validStops?: Set<StopId>,
103
+ ): StopsMap => {
104
+ const stops = new Map<StopId, Stop>();
105
+
106
+ for (const [, stop] of parsedStops) {
107
+ if (
108
+ !validStops ||
109
+ validStops.has(stop.id) ||
110
+ (stop.parent && validStops.has(stop.parent)) ||
111
+ stop.children.some((childId) => validStops.has(childId))
112
+ ) {
113
+ stops.set(stop.id, {
114
+ id: stop.id,
115
+ sourceStopId: stop.sourceStopId,
116
+ name: stop.name,
117
+ lat: stop.lat,
118
+ lon: stop.lon,
119
+ locationType: stop.locationType,
120
+ platform: stop.platform,
121
+ children: stop.children.filter(
122
+ (childId) => !validStops || validStops.has(childId),
123
+ ),
124
+ parent: stop.parent,
125
+ });
91
126
  }
92
127
  }
93
128
  return stops;
@@ -1,10 +1,11 @@
1
- import { StopId } from '../stops/stops.js';
1
+ import { SourceStopId, StopId } from '../stops/stops.js';
2
2
  import { Duration } from '../timetable/duration.js';
3
3
  import {
4
4
  ServiceRouteId,
5
5
  Transfer,
6
6
  TransferType,
7
7
  } from '../timetable/timetable.js';
8
+ import { ParsedStopsMap } from './stops.js';
8
9
  import { TripId } from './trips.js';
9
10
  import { parseCsv } from './utils.js';
10
11
 
@@ -19,8 +20,8 @@ export type GtfsTransferType =
19
20
  export type TransfersMap = Map<StopId, Transfer[]>;
20
21
 
21
22
  export type TransferEntry = {
22
- from_stop_id?: StopId;
23
- to_stop_id?: StopId;
23
+ from_stop_id?: SourceStopId;
24
+ to_stop_id?: SourceStopId;
24
25
  from_trip_id?: TripId;
25
26
  to_trip_id?: TripId;
26
27
  from_route_id?: ServiceRouteId;
@@ -37,11 +38,13 @@ export type TransferEntry = {
37
38
  */
38
39
  export const parseTransfers = async (
39
40
  transfersStream: NodeJS.ReadableStream,
41
+ stopsMap: ParsedStopsMap,
40
42
  ): Promise<TransfersMap> => {
41
43
  const transfers: TransfersMap = new Map();
42
44
 
43
45
  for await (const rawLine of parseCsv(transfersStream)) {
44
46
  const transferEntry = rawLine as TransferEntry;
47
+
45
48
  if (
46
49
  transferEntry.transfer_type === 3 ||
47
50
  transferEntry.transfer_type === 5
@@ -70,17 +73,22 @@ export const parseTransfers = async (
70
73
  );
71
74
  }
72
75
 
76
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
77
+ const fromStop = stopsMap.get(transferEntry.from_stop_id + '')!;
78
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
79
+ const toStop = stopsMap.get(transferEntry.to_stop_id + '')!;
80
+
73
81
  const transfer: Transfer = {
74
- destination: transferEntry.to_stop_id,
82
+ destination: toStop.id,
75
83
  type: parseGtfsTransferType(transferEntry.transfer_type),
76
84
  ...(transferEntry.min_transfer_time && {
77
85
  minTransferTime: Duration.fromSeconds(transferEntry.min_transfer_time),
78
86
  }),
79
87
  };
80
88
 
81
- const fromStopTransfers = transfers.get(transferEntry.from_stop_id) || [];
89
+ const fromStopTransfers = transfers.get(fromStop.id) || [];
82
90
  fromStopTransfers.push(transfer);
83
- transfers.set(transferEntry.from_stop_id, fromStopTransfers);
91
+ transfers.set(fromStop.id, fromStopTransfers);
84
92
  }
85
93
  return transfers;
86
94
  };
package/src/gtfs/trips.ts CHANGED
@@ -1,19 +1,22 @@
1
- import { StopId } from '../stops/stops.js';
1
+ import { SourceStopId, StopId } from '../stops/stops.js';
2
2
  import {
3
+ MUST_COORDINATE_WITH_DRIVER,
4
+ MUST_PHONE_AGENCY,
5
+ NOT_AVAILABLE,
3
6
  PickUpDropOffType,
7
+ REGULAR,
4
8
  Route,
5
9
  RouteId,
6
10
  RoutesAdjacency,
7
11
  ServiceRouteId,
8
12
  ServiceRoutesMap,
9
13
  StopsAdjacency,
10
- StopTimes,
11
14
  } from '../timetable/timetable.js';
12
15
  import { ServiceIds } from './services.js';
13
- import { StopIds } from './stops.js';
16
+ import { ParsedStopsMap } from './stops.js';
14
17
  import { GtfsTime, toTime } from './time.js';
15
18
  import { TransfersMap } from './transfers.js';
16
- import { hash, parseCsv } from './utils.js';
19
+ import { hashIds, parseCsv } from './utils.js';
17
20
 
18
21
  export type TripId = string;
19
22
 
@@ -36,7 +39,7 @@ type StopTimeEntry = {
36
39
  trip_id: TripId;
37
40
  arrival_time?: GtfsTime;
38
41
  departure_time?: GtfsTime;
39
- stop_id: StopId;
42
+ stop_id: SourceStopId;
40
43
  stop_sequence: number;
41
44
  pickup_type?: GtfsPickupDropOffType;
42
45
  drop_off_type?: GtfsPickupDropOffType;
@@ -72,7 +75,7 @@ export const parseTrips = async (
72
75
  };
73
76
 
74
77
  export const buildStopsAdjacencyStructure = (
75
- validStops: StopIds,
78
+ validStops: Set<StopId>,
76
79
  routes: RoutesAdjacency,
77
80
  transfersMap: TransfersMap,
78
81
  ): StopsAdjacency => {
@@ -104,65 +107,130 @@ export const buildStopsAdjacencyStructure = (
104
107
  * Parses the stop_times.txt data from a GTFS feed.
105
108
  *
106
109
  * @param stopTimesStream The readable stream containing the stop times data.
110
+ * @param stopsMap A map of parsed stops from the GTFS feed.
107
111
  * @param validTripIds A map of valid trip IDs to corresponding route IDs.
108
- * @param validStopIds A map of valid stop IDs.
109
- * @returns A mapping of route IDs to route details. The routes return corresponds to the set of trips from GTFS that share the same stop list.
112
+ * @param validStopIds A set of valid stop IDs.
113
+ * @returns A mapping of route IDs to route details. The routes returned correspond to the set of trips from GTFS that share the same stop list.
110
114
  */
111
115
  export const parseStopTimes = async (
112
116
  stopTimesStream: NodeJS.ReadableStream,
117
+ stopsMap: ParsedStopsMap,
113
118
  validTripIds: TripIdsMap,
114
- validStopIds: StopIds,
119
+ validStopIds: Set<StopId>,
115
120
  ): Promise<RoutesAdjacency> => {
121
+ /**
122
+ * Inserts a trip at the right place in the routes adjacency structure.
123
+ */
116
124
  const addTrip = (currentTripId: TripId) => {
117
125
  const gtfsRouteId = validTripIds.get(currentTripId);
118
126
  if (!gtfsRouteId) {
119
127
  stops = [];
120
- stopTimes = [];
128
+ arrivalTimes = [];
129
+ departureTimes = [];
130
+ pickUpTypes = [];
131
+ dropOffTypes = [];
121
132
  return;
122
133
  }
123
- const routeId = `${gtfsRouteId}_${hash(stops.join('$'))}`;
134
+ const routeId = `${gtfsRouteId}_${hashIds(stops)}`;
124
135
 
125
136
  let route = routes.get(routeId);
126
137
  if (!route) {
138
+ const stopsCount = stops.length;
139
+ const stopsArray = new Uint32Array(stops);
140
+ const stopTimesArray = new Uint32Array(stopsCount * 2);
141
+ for (let i = 0; i < stopsCount; i++) {
142
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
143
+ stopTimesArray[i * 2] = arrivalTimes[i]!;
144
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
145
+ stopTimesArray[i * 2 + 1] = departureTimes[i]!;
146
+ }
147
+ const pickUpDropOffTypesArray = new Uint8Array(stopsCount * 2);
148
+ for (let i = 0; i < stopsCount; i++) {
149
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
150
+ pickUpDropOffTypesArray[i * 2] = pickUpTypes[i]!;
151
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
152
+ pickUpDropOffTypesArray[i * 2 + 1] = dropOffTypes[i]!;
153
+ }
127
154
  route = {
128
155
  serviceRouteId: gtfsRouteId,
129
- stops: [...stops],
156
+ stops: stopsArray,
130
157
  stopIndices: new Map(stops.map((stop, i) => [stop, i])),
131
- stopTimes: [...stopTimes],
158
+ stopTimes: stopTimesArray,
159
+ pickUpDropOffTypes: pickUpDropOffTypesArray,
132
160
  };
133
161
  routes.set(routeId, route);
134
162
  for (const stop of stops) {
135
163
  validStopIds.add(stop);
136
164
  }
137
165
  } else {
138
- const tripFirstStop = stopTimes[0];
139
- if (!tripFirstStop) {
166
+ const tripFirstStopDeparture = departureTimes[0];
167
+ if (tripFirstStopDeparture === undefined) {
140
168
  throw new Error(`Empty trip ${currentTripId}`);
141
169
  }
142
- // insert the stopTimes at the right position
143
- let stopTimesIndex = 0;
144
- for (let i = 0; i < route.stopTimes.length; i += stops.length) {
145
- const currentDeparture = route.stopTimes[i];
146
- if (
147
- currentDeparture &&
148
- tripFirstStop.departure > currentDeparture.departure
149
- ) {
150
- stopTimesIndex = i + stops.length;
170
+
171
+ // Find the correct position to insert the new trip
172
+ const stopsCount = stops.length;
173
+ let insertPosition = 0;
174
+ const existingTripsCount = route.stopTimes.length / (stopsCount * 2);
175
+
176
+ for (let tripIndex = 0; tripIndex < existingTripsCount; tripIndex++) {
177
+ const currentDeparture =
178
+ route.stopTimes[tripIndex * stopsCount * 2 + 1];
179
+ if (currentDeparture && tripFirstStopDeparture > currentDeparture) {
180
+ insertPosition = (tripIndex + 1) * stopsCount;
151
181
  } else {
152
182
  break;
153
183
  }
154
184
  }
155
- route.stopTimes.splice(stopTimesIndex, 0, ...stopTimes);
185
+
186
+ // insert data for the new trip at the right place
187
+ const newStopTimesLength = route.stopTimes.length + stopsCount * 2;
188
+ const newStopTimes = new Uint32Array(newStopTimesLength);
189
+ const newPickUpDropOffTypes = new Uint8Array(newStopTimesLength);
190
+
191
+ newStopTimes.set(route.stopTimes.slice(0, insertPosition * 2), 0);
192
+ newPickUpDropOffTypes.set(
193
+ route.pickUpDropOffTypes.slice(0, insertPosition * 2),
194
+ 0,
195
+ );
196
+ for (let i = 0; i < stopsCount; i++) {
197
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
198
+ newStopTimes[(insertPosition + i) * 2] = arrivalTimes[i]!;
199
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
200
+ newStopTimes[(insertPosition + i) * 2 + 1] = departureTimes[i]!;
201
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
202
+ newPickUpDropOffTypes[(insertPosition + i) * 2] = pickUpTypes[i]!;
203
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
204
+ newPickUpDropOffTypes[(insertPosition + i) * 2 + 1] = dropOffTypes[i]!;
205
+ }
206
+ const afterInsertionSlice = route.stopTimes.slice(insertPosition * 2);
207
+ newStopTimes.set(afterInsertionSlice, (insertPosition + stopsCount) * 2);
208
+ const afterInsertionTypesSlice = route.pickUpDropOffTypes.slice(
209
+ insertPosition * 2,
210
+ );
211
+ newPickUpDropOffTypes.set(
212
+ afterInsertionTypesSlice,
213
+ (insertPosition + stopsCount) * 2,
214
+ );
215
+
216
+ route.stopTimes = newStopTimes;
217
+ route.pickUpDropOffTypes = newPickUpDropOffTypes;
156
218
  }
157
219
  stops = [];
158
- stopTimes = [];
220
+ arrivalTimes = [];
221
+ departureTimes = [];
222
+ pickUpTypes = [];
223
+ dropOffTypes = [];
159
224
  };
160
225
 
161
226
  const routes: RoutesAdjacency = new Map();
162
227
 
163
228
  let previousSeq = 0;
164
229
  let stops: StopId[] = [];
165
- let stopTimes: StopTimes[] = [];
230
+ let arrivalTimes: number[] = [];
231
+ let departureTimes: number[] = [];
232
+ let pickUpTypes: PickUpDropOffType[] = [];
233
+ let dropOffTypes: PickUpDropOffType[] = [];
166
234
  let currentTripId: TripId | undefined = undefined;
167
235
 
168
236
  for await (const rawLine of parseCsv(stopTimesStream)) {
@@ -180,26 +248,21 @@ export const parseStopTimes = async (
180
248
  if (line.pickup_type === 1 && line.drop_off_type === 1) {
181
249
  continue;
182
250
  }
183
- if (
184
- currentTripId &&
185
- line.trip_id !== currentTripId &&
186
- stops.length > 0 &&
187
- stopTimes.length > 0
188
- ) {
251
+ if (currentTripId && line.trip_id !== currentTripId && stops.length > 0) {
189
252
  addTrip(currentTripId);
190
253
  }
191
254
  currentTripId = line.trip_id;
192
- stops.push(line.stop_id);
255
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
256
+ stops.push(stopsMap.get(line.stop_id + '')!.id);
193
257
  const departure = line.departure_time ?? line.arrival_time;
194
258
  const arrival = line.arrival_time ?? line.departure_time;
195
- stopTimes.push({
196
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
197
- departure: toTime(departure!),
198
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
199
- arrival: toTime(arrival!),
200
- pickUpType: parsePickupDropOffType(line.pickup_type),
201
- dropOffType: parsePickupDropOffType(line.drop_off_type),
202
- });
259
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
260
+ arrivalTimes.push(toTime(arrival!).toSeconds());
261
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
262
+ departureTimes.push(toTime(departure!).toSeconds());
263
+ pickUpTypes.push(parsePickupDropOffType(line.pickup_type));
264
+ dropOffTypes.push(parsePickupDropOffType(line.drop_off_type));
265
+
203
266
  previousSeq = line.stop_sequence;
204
267
  }
205
268
  if (currentTripId) {
@@ -215,14 +278,14 @@ const parsePickupDropOffType = (
215
278
  switch (gtfsType) {
216
279
  default:
217
280
  console.warn(`Unknown pickup/drop-off type ${gtfsType}`);
218
- return 'REGULAR';
281
+ return REGULAR;
219
282
  case 0:
220
- return 'REGULAR';
283
+ return REGULAR;
221
284
  case 1:
222
- return 'NOT_AVAILABLE';
285
+ return NOT_AVAILABLE;
223
286
  case 2:
224
- return 'MUST_PHONE_AGENCY';
287
+ return MUST_PHONE_AGENCY;
225
288
  case 3:
226
- return 'MUST_COORDINATE_WITH_DRIVER';
289
+ return MUST_COORDINATE_WITH_DRIVER;
227
290
  }
228
291
  };
package/src/gtfs/utils.ts CHANGED
@@ -3,23 +3,23 @@ import { parse, Parser } from 'csv-parse';
3
3
  export type Maybe<T> = T | undefined;
4
4
 
5
5
  /**
6
- * Generates a simple hash from a string.
6
+ * Generates a simple hash from an array of numeric IDs.
7
7
  *
8
- * This function computes a hash for a given string by iterating over each
9
- * character and applying bitwise operations to accumulate a hash value.
10
- * The final hash is then converted to a base-36 string and padded to
11
- * ensure a minimum length of 6 characters.
8
+ * This function computes a hash for a given array of numbers by iterating over each
9
+ * ID and applying bitwise operations to accumulate a hash value.
10
+ * The final hash is then converted to a base-36 string.
12
11
  *
13
- * @param str - The input string to hash.
14
- * @returns A hashed string representation of the input.
12
+ * @param ids - The array of numeric IDs to hash.
13
+ * @returns A hashed string representation of the input array.
15
14
  */
16
- export const hash = (str: string): string => {
15
+ export const hashIds = (ids: number[]): string => {
17
16
  let hash = 0;
18
- for (let i = 0; i < str.length; i++) {
19
- hash = (hash << 5) - hash + str.charCodeAt(i);
17
+ for (let i = 0; i < ids.length; i++) {
18
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
19
+ hash = (hash << 5) - hash + ids[i]!;
20
20
  hash &= hash;
21
21
  }
22
- return (hash >>> 0).toString(36).padStart(6, '0');
22
+ return (hash >>> 0).toString(36);
23
23
  };
24
24
 
25
25
  /**
package/src/router.ts CHANGED
@@ -3,12 +3,11 @@ import { Query } from './routing/query.js';
3
3
  import { Result } from './routing/result.js';
4
4
  import { Leg, Route, Transfer, VehicleLeg } from './routing/route.js';
5
5
  import { ReachingTime, Router } from './routing/router.js';
6
- import { LocationType, Stop, StopId } from './stops/stops.js';
6
+ import { LocationType, SourceStopId, Stop } from './stops/stops.js';
7
7
  import { StopsIndex } from './stops/stopsIndex.js';
8
8
  import { Duration } from './timetable/duration.js';
9
9
  import { Time } from './timetable/time.js';
10
10
  import {
11
- PickUpDropOffType,
12
11
  RouteType,
13
12
  ServiceRoute,
14
13
  Timetable,
@@ -19,7 +18,6 @@ export {
19
18
  Duration,
20
19
  Leg,
21
20
  LocationType,
22
- PickUpDropOffType,
23
21
  Plotter,
24
22
  Query,
25
23
  ReachingTime,
@@ -29,7 +27,7 @@ export {
29
27
  RouteType,
30
28
  ServiceRoute,
31
29
  Stop,
32
- StopId,
30
+ SourceStopId as StopId,
33
31
  StopsIndex,
34
32
  Time,
35
33
  Timetable,
@@ -0,0 +1,112 @@
1
+ import assert from 'node:assert';
2
+ import { describe, it } from 'node:test';
3
+
4
+ import { Stop } from '../../stops/stops.js';
5
+ import { Duration } from '../../timetable/duration.js';
6
+ import { Time } from '../../timetable/time.js';
7
+ import { ServiceRoute, TransferType } from '../../timetable/timetable.js';
8
+ import { Route } from '../route.js';
9
+
10
+ describe('Route', () => {
11
+ const stopA: Stop = {
12
+ id: 1,
13
+ sourceStopId: 'A',
14
+ name: 'Stop A',
15
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
16
+ children: [],
17
+ };
18
+
19
+ const stopB: Stop = {
20
+ id: 2,
21
+ sourceStopId: 'B',
22
+ name: 'Stop B',
23
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
24
+ children: [],
25
+ };
26
+
27
+ const stopC: Stop = {
28
+ id: 3,
29
+ sourceStopId: 'C',
30
+ name: 'Stop C',
31
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
32
+ children: [],
33
+ };
34
+
35
+ const stopD: Stop = {
36
+ id: 4,
37
+ sourceStopId: 'D',
38
+ name: 'Stop D',
39
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
40
+ children: [],
41
+ };
42
+
43
+ const serviceRoute: ServiceRoute = {
44
+ type: 'BUS',
45
+ name: 'Route 1',
46
+ };
47
+
48
+ const serviceRoute2: ServiceRoute = {
49
+ type: 'RAIL',
50
+ name: 'Route 2',
51
+ };
52
+
53
+ const vehicleLeg = {
54
+ from: stopA,
55
+ to: stopB,
56
+ route: serviceRoute,
57
+ departureTime: Time.fromHMS(8, 0, 0),
58
+ arrivalTime: Time.fromHMS(8, 30, 0),
59
+ };
60
+
61
+ const transferLeg = {
62
+ from: stopB,
63
+ to: stopC,
64
+ type: 'RECOMMENDED' as TransferType,
65
+ minTransferTime: Duration.fromMinutes(5),
66
+ };
67
+
68
+ const secondVehicleLeg = {
69
+ from: stopC,
70
+ to: stopD,
71
+ route: serviceRoute2,
72
+ departureTime: Time.fromHMS(8, 40, 0),
73
+ arrivalTime: Time.fromHMS(9, 0, 0),
74
+ };
75
+
76
+ it('should calculate the correct departure time', () => {
77
+ const route = new Route([vehicleLeg, transferLeg, secondVehicleLeg]);
78
+ const departureTime = route.departureTime();
79
+ assert.strictEqual(
80
+ departureTime.toSeconds(),
81
+ Time.fromHMS(8, 0, 0).toSeconds(),
82
+ );
83
+ });
84
+
85
+ it('should calculate the correct arrival time', () => {
86
+ const route = new Route([vehicleLeg, transferLeg, secondVehicleLeg]);
87
+ const arrivalTime = route.arrivalTime();
88
+ assert.strictEqual(
89
+ arrivalTime.toSeconds(),
90
+ Time.fromHMS(9, 0, 0).toSeconds(),
91
+ );
92
+ });
93
+
94
+ it('should calculate the total duration of the route', () => {
95
+ const route = new Route([vehicleLeg, transferLeg, secondVehicleLeg]);
96
+ const totalDuration = route.totalDuration();
97
+ assert.strictEqual(
98
+ totalDuration.toSeconds(),
99
+ Duration.fromMinutes(60).toSeconds(),
100
+ );
101
+ });
102
+
103
+ it('should throw an error if no vehicle leg is found for departure time', () => {
104
+ const route = new Route([transferLeg]);
105
+ assert.throws(() => route.departureTime(), /No vehicle leg found in route/);
106
+ });
107
+
108
+ it('should throw an error if no vehicle leg is found for arrival time', () => {
109
+ const route = new Route([transferLeg]);
110
+ assert.throws(() => route.arrivalTime(), /No vehicle leg found in route/);
111
+ });
112
+ });