minotor 7.0.2 → 9.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/.cspell.json +11 -1
  2. package/CHANGELOG.md +8 -3
  3. package/README.md +26 -24
  4. package/dist/cli.mjs +1786 -791
  5. package/dist/cli.mjs.map +1 -1
  6. package/dist/gtfs/transfers.d.ts +29 -5
  7. package/dist/gtfs/trips.d.ts +10 -5
  8. package/dist/parser.cjs.js +972 -525
  9. package/dist/parser.cjs.js.map +1 -1
  10. package/dist/parser.esm.js +972 -525
  11. package/dist/parser.esm.js.map +1 -1
  12. package/dist/router.cjs.js +1 -1
  13. package/dist/router.cjs.js.map +1 -1
  14. package/dist/router.d.ts +2 -2
  15. package/dist/router.esm.js +1 -1
  16. package/dist/router.esm.js.map +1 -1
  17. package/dist/router.umd.js +1 -1
  18. package/dist/router.umd.js.map +1 -1
  19. package/dist/routing/__tests__/plotter.test.d.ts +1 -0
  20. package/dist/routing/plotter.d.ts +42 -3
  21. package/dist/routing/result.d.ts +23 -7
  22. package/dist/routing/route.d.ts +2 -0
  23. package/dist/routing/router.d.ts +78 -19
  24. package/dist/timetable/__tests__/tripBoardingId.test.d.ts +1 -0
  25. package/dist/timetable/io.d.ts +4 -2
  26. package/dist/timetable/proto/timetable.d.ts +15 -1
  27. package/dist/timetable/route.d.ts +48 -23
  28. package/dist/timetable/timetable.d.ts +24 -7
  29. package/dist/timetable/tripBoardingId.d.ts +34 -0
  30. package/package.json +1 -1
  31. package/src/__e2e__/router.test.ts +114 -105
  32. package/src/__e2e__/timetable/stops.bin +2 -2
  33. package/src/__e2e__/timetable/timetable.bin +2 -2
  34. package/src/cli/repl.ts +245 -1
  35. package/src/gtfs/__tests__/parser.test.ts +19 -4
  36. package/src/gtfs/__tests__/transfers.test.ts +773 -37
  37. package/src/gtfs/__tests__/trips.test.ts +308 -27
  38. package/src/gtfs/parser.ts +36 -6
  39. package/src/gtfs/transfers.ts +193 -19
  40. package/src/gtfs/trips.ts +58 -21
  41. package/src/router.ts +2 -2
  42. package/src/routing/__tests__/plotter.test.ts +230 -0
  43. package/src/routing/__tests__/result.test.ts +486 -125
  44. package/src/routing/__tests__/route.test.ts +7 -3
  45. package/src/routing/__tests__/router.test.ts +380 -172
  46. package/src/routing/plotter.ts +279 -48
  47. package/src/routing/result.ts +114 -34
  48. package/src/routing/route.ts +0 -3
  49. package/src/routing/router.ts +344 -211
  50. package/src/timetable/__tests__/io.test.ts +34 -1
  51. package/src/timetable/__tests__/route.test.ts +74 -81
  52. package/src/timetable/__tests__/timetable.test.ts +232 -61
  53. package/src/timetable/__tests__/tripBoardingId.test.ts +57 -0
  54. package/src/timetable/io.ts +72 -10
  55. package/src/timetable/proto/timetable.proto +16 -2
  56. package/src/timetable/proto/timetable.ts +256 -22
  57. package/src/timetable/route.ts +174 -58
  58. package/src/timetable/timetable.ts +66 -16
  59. package/src/timetable/tripBoardingId.ts +94 -0
  60. package/tsconfig.json +2 -2
@@ -58,7 +58,13 @@ describe('Route', () => {
58
58
  const stops = new Uint32Array([1001, 1002]);
59
59
  const serviceRouteId = 0;
60
60
 
61
- const route = new Route(stopTimes, pickUpDropOffTypes, stops, serviceRouteId);
61
+ const route = new Route(
62
+ 0,
63
+ stopTimes,
64
+ pickUpDropOffTypes,
65
+ stops,
66
+ serviceRouteId,
67
+ );
62
68
 
63
69
  describe('constructor', () => {
64
70
  it('should create a route with correct properties', () => {
@@ -68,6 +74,7 @@ describe('Route', () => {
68
74
 
69
75
  it('should handle empty route', () => {
70
76
  const emptyRoute = new Route(
77
+ 0,
71
78
  new Uint16Array([]),
72
79
  new Uint8Array([]),
73
80
  new Uint32Array([]),
@@ -88,34 +95,6 @@ describe('Route', () => {
88
95
  });
89
96
  });
90
97
 
91
- describe('isBefore', () => {
92
- it('should return true when stopA is before stopB', () => {
93
- assert.strictEqual(route.isBefore(1001, 1002), true);
94
- });
95
-
96
- it('should return false when stopA is after stopB', () => {
97
- assert.strictEqual(route.isBefore(1002, 1001), false);
98
- });
99
-
100
- it('should return false when stopA equals stopB', () => {
101
- assert.strictEqual(route.isBefore(1001, 1001), false);
102
- });
103
-
104
- it('should throw error when stopA is not found', () => {
105
- assert.throws(
106
- () => route.isBefore(9999, 1002),
107
- /Stop index undefined not found in route 0/,
108
- );
109
- });
110
-
111
- it('should throw error when stopB is not found', () => {
112
- assert.throws(
113
- () => route.isBefore(1001, 9999),
114
- /Stop index undefined not found in route 0/,
115
- );
116
- });
117
- });
118
-
119
98
  describe('getNbStops', () => {
120
99
  it('should return correct number of stops', () => {
121
100
  assert.strictEqual(route.getNbStops(), 2);
@@ -129,178 +108,192 @@ describe('Route', () => {
129
108
  });
130
109
 
131
110
  describe('arrivalAt', () => {
132
- it('should return correct arrival time for trip 0 at stop 1001', () => {
133
- const arrival = route.arrivalAt(1001, 0);
111
+ it('should return correct arrival time for trip 0 at stop index 0', () => {
112
+ const arrival = route.arrivalAt(0, 0);
134
113
  assert.strictEqual(
135
114
  arrival.toMinutes(),
136
115
  Time.fromHMS(8, 0, 0).toMinutes(),
137
116
  );
138
117
  });
139
118
 
140
- it('should return correct arrival time for trip 1 at stop 1002', () => {
141
- const arrival = route.arrivalAt(1002, 1);
119
+ it('should return correct arrival time for trip 1 at stop index 1', () => {
120
+ const arrival = route.arrivalAt(1, 1);
142
121
  assert.strictEqual(
143
122
  arrival.toMinutes(),
144
123
  Time.fromHMS(9, 30, 0).toMinutes(),
145
124
  );
146
125
  });
147
126
 
148
- it('should throw error for invalid stop ID', () => {
127
+ it('should throw error for invalid stop index', () => {
149
128
  assert.throws(
150
- () => route.arrivalAt(9999, 0),
151
- /Stop index for 9999 not found in route 0/,
129
+ () => route.arrivalAt(999, 0),
130
+ /StopId for stop at index 999 not found/,
152
131
  );
153
132
  });
154
133
 
155
134
  it('should throw error for invalid trip index', () => {
156
- assert.throws(
157
- () => route.arrivalAt(1001, 999),
158
- /Arrival time not found for stop 1001 at trip index 999/,
159
- );
135
+ assert.throws(() => route.arrivalAt(0, 999), /Arrival time not found/);
160
136
  });
161
137
  });
162
138
 
163
139
  describe('departureFrom', () => {
164
- it('should return correct departure time for trip 0 at stop 1001', () => {
165
- const departure = route.departureFrom(1001, 0);
140
+ it('should return correct departure time for trip 0 at stop index 0', () => {
141
+ const departure = route.departureFrom(0, 0);
166
142
  assert.strictEqual(
167
143
  departure.toMinutes(),
168
144
  Time.fromHMS(8, 1, 0).toMinutes(),
169
145
  );
170
146
  });
171
147
 
172
- it('should return correct departure time for trip 2 at stop 1002', () => {
173
- const departure = route.departureFrom(1002, 2);
148
+ it('should return correct departure time for trip 2 at stop index 1', () => {
149
+ const departure = route.departureFrom(1, 2);
174
150
  assert.strictEqual(
175
151
  departure.toMinutes(),
176
152
  Time.fromHMS(10, 31, 0).toMinutes(),
177
153
  );
178
154
  });
179
155
 
180
- it('should throw error for invalid stop ID', () => {
156
+ it('should throw error for invalid stop index', () => {
181
157
  assert.throws(
182
- () => route.departureFrom(9999, 0),
183
- /Stop index for 9999 not found in route 0/,
158
+ () => route.departureFrom(999, 0),
159
+ /StopId for stop at index 999 not found/,
184
160
  );
185
161
  });
186
162
 
187
163
  it('should throw error for invalid trip index', () => {
188
164
  assert.throws(
189
- () => route.departureFrom(1001, 999),
190
- /Departure time not found for stop 1001 at trip index 999/,
165
+ () => route.departureFrom(0, 999),
166
+ /Departure time not found/,
191
167
  );
192
168
  });
193
169
  });
194
170
 
195
171
  describe('pickUpTypeFrom', () => {
196
- it('should return REGULAR pickup type for trip 0 at stop 1001', () => {
197
- const pickUpType = route.pickUpTypeFrom(1001, 0);
172
+ it('should return REGULAR pickup type for trip 0 at stop index 0', () => {
173
+ const pickUpType = route.pickUpTypeFrom(0, 0);
198
174
  assert.strictEqual(pickUpType, 'REGULAR');
199
175
  });
200
176
 
201
- it('should return NOT_AVAILABLE pickup type for trip 0 at stop 1002', () => {
202
- const pickUpType = route.pickUpTypeFrom(1002, 0);
177
+ it('should return NOT_AVAILABLE pickup type for trip 0 at stop index 1', () => {
178
+ const pickUpType = route.pickUpTypeFrom(1, 0);
203
179
  assert.strictEqual(pickUpType, 'NOT_AVAILABLE');
204
180
  });
205
181
 
206
- it('should return MUST_PHONE_AGENCY pickup type for trip 2 at stop 1001', () => {
207
- const pickUpType = route.pickUpTypeFrom(1001, 2);
182
+ it('should return MUST_PHONE_AGENCY pickup type for trip 2 at stop index 0', () => {
183
+ const pickUpType = route.pickUpTypeFrom(0, 2);
208
184
  assert.strictEqual(pickUpType, 'MUST_PHONE_AGENCY');
209
185
  });
210
186
 
211
- it('should throw error for invalid stop ID', () => {
187
+ it('should throw error for invalid stop index', () => {
212
188
  assert.throws(
213
- () => route.pickUpTypeFrom(9999, 0),
214
- /Stop index for 9999 not found in route 0/,
189
+ () => route.pickUpTypeFrom(999, 0),
190
+ /StopId for stop at index 999 not found/,
215
191
  );
216
192
  });
217
193
 
218
194
  it('should throw error for invalid trip index', () => {
219
195
  assert.throws(
220
- () => route.pickUpTypeFrom(1001, 999),
221
- /Pick up type not found for stop 1001 at trip index 999/,
196
+ () => route.pickUpTypeFrom(0, 999),
197
+ /Pick up type not found/,
222
198
  );
223
199
  });
224
200
  });
225
201
 
226
202
  describe('dropOffTypeAt', () => {
227
- it('should return REGULAR drop off type for trip 0 at stop 1001', () => {
228
- const dropOffType = route.dropOffTypeAt(1001, 0);
203
+ it('should return REGULAR drop off type for trip 0 at stop index 0', () => {
204
+ const dropOffType = route.dropOffTypeAt(0, 0);
229
205
  assert.strictEqual(dropOffType, 'REGULAR');
230
206
  });
231
207
 
232
- it('should return REGULAR drop off type for trip 1 at stop 1002', () => {
233
- const dropOffType = route.dropOffTypeAt(1002, 1);
208
+ it('should return REGULAR drop off type for trip 1 at stop index 1', () => {
209
+ const dropOffType = route.dropOffTypeAt(1, 1);
234
210
  assert.strictEqual(dropOffType, 'REGULAR');
235
211
  });
236
212
 
237
- it('should throw error for invalid stop ID', () => {
213
+ it('should throw error for invalid stop index', () => {
238
214
  assert.throws(
239
- () => route.dropOffTypeAt(9999, 0),
240
- /Stop index for 9999 not found in route 0/,
215
+ () => route.dropOffTypeAt(999, 0),
216
+ /StopId for stop at index 999 not found/,
241
217
  );
242
218
  });
243
219
 
244
220
  it('should throw error for invalid trip index', () => {
245
221
  assert.throws(
246
- () => route.dropOffTypeAt(1001, 999),
247
- /Drop off type not found for stop 1001 at trip index 999/,
222
+ () => route.dropOffTypeAt(0, 999),
223
+ /Drop off type not found/,
248
224
  );
249
225
  });
250
226
  });
251
227
 
252
228
  describe('findEarliestTrip', () => {
253
229
  it('should find earliest trip without time constraint', () => {
254
- const tripIndex = route.findEarliestTrip(1001);
230
+ const tripIndex = route.findEarliestTrip(0);
255
231
  assert.strictEqual(tripIndex, 0);
256
232
  });
257
233
 
258
234
  it('should find earliest trip after specified time', () => {
259
235
  const afterTime = Time.fromHMS(8, 30, 0);
260
- const tripIndex = route.findEarliestTrip(1001, afterTime);
236
+ const tripIndex = route.findEarliestTrip(0, afterTime);
261
237
  assert.strictEqual(tripIndex, 1);
262
238
  });
263
239
 
264
240
  it('should find earliest trip with exact match time', () => {
265
241
  const afterTime = Time.fromHMS(9, 1, 0);
266
- const tripIndex = route.findEarliestTrip(1001, afterTime);
242
+ const tripIndex = route.findEarliestTrip(0, afterTime);
267
243
  assert.strictEqual(tripIndex, 1);
268
244
  });
269
245
 
270
246
  it('should return undefined when no trip is available after specified time', () => {
271
247
  const afterTime = Time.fromHMS(23, 0, 0);
272
- const tripIndex = route.findEarliestTrip(1001, afterTime);
248
+ const tripIndex = route.findEarliestTrip(0, afterTime);
273
249
  assert.strictEqual(tripIndex, undefined);
274
250
  });
275
251
 
276
252
  it('should skip trips where pickup is not available', () => {
277
- const tripIndex = route.findEarliestTrip(1002);
278
- // Trip 0 has NOT_AVAILABLE pickup at stop 1002, so should return trip 1
253
+ const tripIndex = route.findEarliestTrip(1);
254
+ // Trip 0 has NOT_AVAILABLE pickup at stop index 1, so should return trip 1
279
255
  assert.strictEqual(tripIndex, 1);
280
256
  });
281
257
 
282
258
  it('should respect beforeTrip constraint', () => {
283
- const tripIndex = route.findEarliestTrip(1001, Time.fromHMS(8, 2, 0), 1);
259
+ const tripIndex = route.findEarliestTrip(0, Time.fromHMS(8, 2, 0), 1);
284
260
  assert.strictEqual(tripIndex, undefined);
285
261
  });
286
262
 
287
263
  it('should return undefined when beforeTrip is 0', () => {
288
- const tripIndex = route.findEarliestTrip(1001, Time.origin(), 0);
264
+ const tripIndex = route.findEarliestTrip(0, Time.origin(), 0);
289
265
  assert.strictEqual(tripIndex, undefined);
290
266
  });
291
267
 
292
268
  it('should handle MUST_PHONE_AGENCY pickup type', () => {
293
269
  const afterTime = Time.fromHMS(9, 30, 0);
294
- const tripIndex = route.findEarliestTrip(1001, afterTime);
270
+ const tripIndex = route.findEarliestTrip(0, afterTime);
295
271
  // Should find trip 2 even though it requires phone agency
296
272
  assert.strictEqual(tripIndex, 2);
297
273
  });
298
274
 
299
- it('should throw error for invalid stop ID', () => {
275
+ it('should throw error for invalid stop index', () => {
300
276
  assert.throws(
301
- () => route.findEarliestTrip(9999),
302
- /Stop index for 9999 not found in route 0/,
277
+ () => route.findEarliestTrip(999),
278
+ /StopId for stop at index 999 not found/,
303
279
  );
304
280
  });
305
281
  });
282
+
283
+ describe('stopRouteIndices', () => {
284
+ it('should return correct stop route indices for existing stop', () => {
285
+ const indices = route.stopRouteIndices(1001);
286
+ assert.deepStrictEqual(indices, [0]);
287
+ });
288
+
289
+ it('should return correct stop route indices for second stop', () => {
290
+ const indices = route.stopRouteIndices(1002);
291
+ assert.deepStrictEqual(indices, [1]);
292
+ });
293
+
294
+ it('should return empty array for non-existent stop', () => {
295
+ const indices = route.stopRouteIndices(9999);
296
+ assert.deepStrictEqual(indices, []);
297
+ });
298
+ });
306
299
  });
@@ -1,20 +1,21 @@
1
1
  import assert from 'node:assert';
2
2
  import { describe, it } from 'node:test';
3
3
 
4
- import { encodePickUpDropOffTypes } from '../../gtfs/trips.js';
5
4
  import { Duration } from '../duration.js';
6
- import { NOT_AVAILABLE, REGULAR, Route } from '../route.js';
5
+ import { NOT_AVAILABLE, Route } from '../route.js';
7
6
  import { Time } from '../time.js';
8
7
  import {
9
8
  RouteType,
10
9
  ServiceRoute,
11
10
  StopAdjacency,
12
11
  Timetable,
12
+ TripBoarding,
13
13
  } from '../timetable.js';
14
+ import { encode } from '../tripBoardingId.js';
14
15
 
15
16
  describe('Timetable', () => {
16
17
  const stopsAdjacency: StopAdjacency[] = [
17
- { transfers: [], routes: [] },
18
+ { routes: [] },
18
19
  {
19
20
  transfers: [{ destination: 2, type: 'RECOMMENDED' }],
20
21
  routes: [0, 1],
@@ -30,40 +31,65 @@ describe('Timetable', () => {
30
31
  routes: [1, 0],
31
32
  },
32
33
  {
33
- transfers: [],
34
34
  routes: [],
35
35
  },
36
36
  ];
37
37
 
38
- const route1 = new Route(
39
- new Uint16Array([
40
- Time.fromHMS(16, 40, 0).toMinutes(),
41
- Time.fromHMS(16, 50, 0).toMinutes(),
42
- Time.fromHMS(17, 20, 0).toMinutes(),
43
- Time.fromHMS(17, 30, 0).toMinutes(),
44
- Time.fromHMS(18, 0, 0).toMinutes(),
45
- Time.fromHMS(18, 10, 0).toMinutes(),
46
- Time.fromHMS(19, 0, 0).toMinutes(),
47
- Time.fromHMS(19, 10, 0).toMinutes(),
48
- ]),
49
- encodePickUpDropOffTypes(
50
- [REGULAR, NOT_AVAILABLE, REGULAR, REGULAR],
51
- [REGULAR, REGULAR, REGULAR, REGULAR],
52
- ),
53
- new Uint32Array([1, 2]),
54
- 0,
55
- );
56
- const route2 = new Route(
57
- new Uint16Array([
58
- Time.fromHMS(18, 20, 0).toMinutes(),
59
- Time.fromHMS(18, 30, 0).toMinutes(),
60
- Time.fromHMS(23, 20, 0).toMinutes(),
61
- Time.fromHMS(23, 30, 0).toMinutes(),
62
- ]),
63
- encodePickUpDropOffTypes([REGULAR, REGULAR], [REGULAR, REGULAR]),
64
- new Uint32Array([2, 1]),
65
- 1,
66
- );
38
+ const route1 = Route.of({
39
+ id: 0,
40
+ serviceRouteId: 0,
41
+ trips: [
42
+ {
43
+ stops: [
44
+ {
45
+ id: 1,
46
+ arrivalTime: Time.fromHMS(16, 40, 0),
47
+ departureTime: Time.fromHMS(16, 50, 0),
48
+ },
49
+ {
50
+ id: 2,
51
+ arrivalTime: Time.fromHMS(17, 20, 0),
52
+ departureTime: Time.fromHMS(17, 30, 0),
53
+ pickUpType: NOT_AVAILABLE,
54
+ },
55
+ ],
56
+ },
57
+ {
58
+ stops: [
59
+ {
60
+ id: 1,
61
+ arrivalTime: Time.fromHMS(18, 0, 0),
62
+ departureTime: Time.fromHMS(18, 10, 0),
63
+ },
64
+ {
65
+ id: 2,
66
+ arrivalTime: Time.fromHMS(19, 0, 0),
67
+ departureTime: Time.fromHMS(19, 10, 0),
68
+ },
69
+ ],
70
+ },
71
+ ],
72
+ });
73
+ const route2 = Route.of({
74
+ id: 1,
75
+ serviceRouteId: 1,
76
+ trips: [
77
+ {
78
+ stops: [
79
+ {
80
+ id: 2,
81
+ arrivalTime: Time.fromHMS(18, 20, 0),
82
+ departureTime: Time.fromHMS(18, 30, 0),
83
+ },
84
+ {
85
+ id: 1,
86
+ arrivalTime: Time.fromHMS(19, 0, 0),
87
+ departureTime: Time.fromHMS(19, 10, 0),
88
+ },
89
+ ],
90
+ },
91
+ ],
92
+ });
67
93
  const routesAdjacency = [route1, route2];
68
94
  const routes: ServiceRoute[] = [
69
95
  { type: 'RAIL', name: 'Route 1', routes: [0] },
@@ -74,6 +100,7 @@ describe('Timetable', () => {
74
100
  stopsAdjacency,
75
101
  routesAdjacency,
76
102
  routes,
103
+ new Map(),
77
104
  );
78
105
 
79
106
  it('should serialize a timetable to a Uint8Array', () => {
@@ -90,7 +117,7 @@ describe('Timetable', () => {
90
117
  it('should find the earliest trip for stop1 on route1', () => {
91
118
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
92
119
  const route = sampleTimetable.getRoute(0)!;
93
- const tripIndex = route.findEarliestTrip(1);
120
+ const tripIndex = route.findEarliestTrip(0);
94
121
  assert.strictEqual(tripIndex, 0);
95
122
  });
96
123
 
@@ -98,7 +125,7 @@ describe('Timetable', () => {
98
125
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
99
126
  const route = sampleTimetable.getRoute(0)!;
100
127
  const afterTime = Time.fromHMS(17, 0, 0);
101
- const tripIndex = route.findEarliestTrip(1, afterTime);
128
+ const tripIndex = route.findEarliestTrip(0, afterTime);
102
129
  assert.strictEqual(tripIndex, 1);
103
130
  });
104
131
 
@@ -106,40 +133,184 @@ describe('Timetable', () => {
106
133
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
107
134
  const route = sampleTimetable.getRoute(0)!;
108
135
  const afterTime = Time.fromHMS(23, 40, 0);
109
- const tripIndex = route.findEarliestTrip(1, afterTime);
136
+ const tripIndex = route.findEarliestTrip(0, afterTime);
110
137
  assert.strictEqual(tripIndex, undefined);
111
138
  });
112
139
  it('should return undefined if the stop on a trip has pick up not available', () => {
113
140
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
114
141
  const route = sampleTimetable.getRoute(0)!;
115
- const tripIndex = route.findEarliestTrip(2);
142
+ const tripIndex = route.findEarliestTrip(1);
116
143
  assert.strictEqual(tripIndex, 1);
117
144
  });
118
- it('should find reachable routes from a set of stop IDs', () => {
119
- const fromStops = new Set([1]);
120
- const reachableRoutes = sampleTimetable.findReachableRoutes(fromStops);
121
- assert.strictEqual(reachableRoutes.size, 2);
122
- assert.deepStrictEqual(
123
- reachableRoutes,
124
- new Map([
125
- [route1, 1],
126
- [route2, 1],
127
- ]),
128
- );
129
- });
145
+ describe('findReachableRoutes', () => {
146
+ it('should find reachable routes from a single stop', () => {
147
+ const fromStops = new Set([1]);
148
+ const reachableRoutes = sampleTimetable.findReachableRoutes(fromStops);
149
+ assert.strictEqual(reachableRoutes.size, 2);
150
+ assert.deepStrictEqual(
151
+ reachableRoutes,
152
+ new Map([
153
+ [route1, 0],
154
+ [route2, 1],
155
+ ]),
156
+ );
157
+ });
130
158
 
131
- it('should find no reachable routes if starting from a non-existent stop', () => {
132
- const fromStops = new Set([3]);
133
- const reachableRoutes = sampleTimetable.findReachableRoutes(fromStops);
134
- assert.strictEqual(reachableRoutes.size, 0);
135
- });
159
+ it('should find reachable routes from multiple stops', () => {
160
+ const fromStops = new Set([1, 2]);
161
+ const reachableRoutes = sampleTimetable.findReachableRoutes(fromStops);
162
+ assert.strictEqual(reachableRoutes.size, 2);
163
+
164
+ assert.deepStrictEqual(
165
+ reachableRoutes,
166
+ new Map([
167
+ [route1, 0],
168
+ [route2, 0],
169
+ ]),
170
+ );
171
+ });
172
+
173
+ it('should find no reachable routes from stops with no routes', () => {
174
+ const fromStops = new Set([3]); // Stop 3 has no routes in sample timetable
175
+ const reachableRoutes = sampleTimetable.findReachableRoutes(fromStops);
176
+ assert.strictEqual(reachableRoutes.size, 0);
177
+ assert.deepStrictEqual(reachableRoutes, new Map());
178
+ });
179
+
180
+ it('should find no reachable routes from empty stop set', () => {
181
+ const fromStops = new Set<number>();
182
+ const reachableRoutes = sampleTimetable.findReachableRoutes(fromStops);
183
+ assert.strictEqual(reachableRoutes.size, 0);
184
+ assert.deepStrictEqual(reachableRoutes, new Map());
185
+ });
186
+
187
+ it('should find no reachable routes from non-existent stops', () => {
188
+ const fromStops = new Set([999, 1000]); // Non-existent stops
189
+ const reachableRoutes = sampleTimetable.findReachableRoutes(fromStops);
190
+ assert.strictEqual(reachableRoutes.size, 0);
191
+ assert.deepStrictEqual(reachableRoutes, new Map());
192
+ });
193
+
194
+ it('should filter routes by transport modes correctly', () => {
195
+ const fromStops = new Set([1]);
196
+
197
+ const railRoutes = sampleTimetable.findReachableRoutes(
198
+ fromStops,
199
+ new Set<RouteType>(['RAIL']),
200
+ );
201
+ assert.strictEqual(railRoutes.size, 2);
202
+ assert.deepStrictEqual(
203
+ railRoutes,
204
+ new Map([
205
+ [route1, 0],
206
+ [route2, 1],
207
+ ]),
208
+ );
209
+
210
+ const busRoutes = sampleTimetable.findReachableRoutes(
211
+ fromStops,
212
+ new Set<RouteType>(['BUS']),
213
+ );
214
+ assert.strictEqual(busRoutes.size, 0);
215
+ assert.deepStrictEqual(busRoutes, new Map());
216
+
217
+ const multiModeRoutes = sampleTimetable.findReachableRoutes(
218
+ fromStops,
219
+ new Set<RouteType>(['RAIL', 'BUS', 'SUBWAY']),
220
+ );
221
+ assert.strictEqual(multiModeRoutes.size, 2);
222
+ });
223
+
224
+ it('should return earliest hop-on stop when route is accessible from multiple stops', () => {
225
+ // Create scenario where same route is accessible from multiple stops in the query
226
+ // route1 has stops [1, 2] in that order, so we need to test with those actual stops
227
+ const fromStops = new Set([1, 2]); // Both stops are on route1
228
+ const reachableRoutes = sampleTimetable.findReachableRoutes(fromStops);
229
+
230
+ // route1 should use stop index 0 (stop 1 comes before stop 2 on route1)
231
+ // route2 should use stop index 0 (stop 2 comes before stop 1 on route2)
232
+ assert.strictEqual(reachableRoutes.size, 2);
233
+ assert.deepStrictEqual(
234
+ reachableRoutes,
235
+ new Map([
236
+ [route1, 0], // Stop index 0 (stop 1) comes before stop index 1 (stop 2) on route1
237
+ [route2, 0], // Stop index 0 (stop 2) comes before stop index 1 (stop 1) on route2
238
+ ]),
239
+ );
240
+ });
241
+
242
+ describe('getContinuousTrips', () => {
243
+ it('should return empty array when stop has no trip continuations', () => {
244
+ const continuousTrips = sampleTimetable.getContinuousTrips(0, 0, 0);
245
+ assert.deepStrictEqual(continuousTrips, []);
246
+ });
247
+
248
+ it('should return empty array when stop has trip continuations but not for the specified trip', () => {
249
+ // Create a timetable with trip continuations that don't match the query
250
+ const tripContinuationsMap = new Map([
251
+ [encode(0, 0, 1), [{ hopOnStopIndex: 0, routeId: 1, tripIndex: 0 }]], // Different trip index
252
+ ]);
253
+
254
+ const stopsWithContinuations: StopAdjacency[] = [
255
+ { routes: [] },
256
+ {
257
+ routes: [0, 1],
258
+ },
259
+ { routes: [1] },
260
+ ];
261
+
262
+ const timetableWithContinuations = new Timetable(
263
+ stopsWithContinuations,
264
+ routesAdjacency,
265
+ routes,
266
+ tripContinuationsMap,
267
+ );
268
+
269
+ const continuousTrips = timetableWithContinuations.getContinuousTrips(
270
+ 0,
271
+ 0,
272
+ 0,
273
+ ); // Query trip index 0, but continuations are for trip index 1
274
+ assert.deepStrictEqual(continuousTrips, []);
275
+ });
276
+
277
+ it('should return trip continuations when they exist for the specified trip', () => {
278
+ const expectedContinuations: TripBoarding[] = [
279
+ { hopOnStopIndex: 0, routeId: 1, tripIndex: 0 },
280
+ { hopOnStopIndex: 0, routeId: 1, tripIndex: 1 },
281
+ ];
282
+
283
+ const tripContinuationsMap = new Map([
284
+ [encode(0, 0, 0), expectedContinuations],
285
+ ]);
286
+
287
+ const stopsWithContinuations: StopAdjacency[] = [
288
+ { routes: [] },
289
+ {
290
+ routes: [0, 1],
291
+ },
292
+ { routes: [1] },
293
+ ];
294
+
295
+ const timetableWithContinuations = new Timetable(
296
+ stopsWithContinuations,
297
+ routesAdjacency,
298
+ routes,
299
+ tripContinuationsMap,
300
+ );
301
+
302
+ const continuousTrips = timetableWithContinuations.getContinuousTrips(
303
+ 0,
304
+ 0,
305
+ 0,
306
+ );
307
+ assert.deepStrictEqual(continuousTrips, expectedContinuations);
308
+ });
136
309
 
137
- it('should find reachable routes filtered by transport modes', () => {
138
- const fromStops = new Set([1]);
139
- const reachableRoutes = sampleTimetable.findReachableRoutes(
140
- fromStops,
141
- new Set<RouteType>(['BUS']),
142
- );
143
- assert.strictEqual(reachableRoutes.size, 0);
310
+ it('should return empty array when querying with non-matching parameters', () => {
311
+ const continuousTrips = sampleTimetable.getContinuousTrips(999, 0, 0);
312
+ assert.deepStrictEqual(continuousTrips, []);
313
+ });
314
+ });
144
315
  });
145
316
  });