minotor 3.0.0 → 3.0.2

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 (81) hide show
  1. package/.cspell.json +14 -1
  2. package/.gitattributes +3 -0
  3. package/.github/PULL_REQUEST_TEMPLATE.md +3 -0
  4. package/.github/workflows/minotor.yml +17 -1
  5. package/CHANGELOG.md +3 -9
  6. package/README.md +47 -17
  7. package/dist/__e2e__/router.test.d.ts +1 -0
  8. package/dist/cli/perf.d.ts +28 -0
  9. package/dist/cli/utils.d.ts +6 -2
  10. package/dist/cli.mjs +1967 -823
  11. package/dist/cli.mjs.map +1 -1
  12. package/dist/gtfs/trips.d.ts +1 -0
  13. package/dist/gtfs/utils.d.ts +1 -1
  14. package/dist/parser.cjs.js +1030 -627
  15. package/dist/parser.cjs.js.map +1 -1
  16. package/dist/parser.d.ts +4 -2
  17. package/dist/parser.esm.js +1030 -627
  18. package/dist/parser.esm.js.map +1 -1
  19. package/dist/router.cjs.js +1 -1
  20. package/dist/router.cjs.js.map +1 -1
  21. package/dist/router.d.ts +10 -5
  22. package/dist/router.esm.js +1 -1
  23. package/dist/router.esm.js.map +1 -1
  24. package/dist/router.umd.js +1 -1
  25. package/dist/router.umd.js.map +1 -1
  26. package/dist/routing/__tests__/result.test.d.ts +1 -0
  27. package/dist/routing/query.d.ts +27 -6
  28. package/dist/routing/result.d.ts +1 -1
  29. package/dist/routing/route.d.ts +47 -2
  30. package/dist/routing/router.d.ts +15 -1
  31. package/dist/stops/stopsIndex.d.ts +3 -3
  32. package/dist/timetable/__tests__/route.test.d.ts +1 -0
  33. package/dist/timetable/__tests__/time.test.d.ts +1 -0
  34. package/dist/timetable/io.d.ts +7 -1
  35. package/dist/timetable/proto/timetable.d.ts +1 -1
  36. package/dist/timetable/route.d.ts +155 -0
  37. package/dist/timetable/time.d.ts +21 -0
  38. package/dist/timetable/timetable.d.ts +41 -61
  39. package/package.json +36 -35
  40. package/src/__e2e__/benchmark.json +22 -0
  41. package/src/__e2e__/router.test.ts +209 -0
  42. package/src/__e2e__/timetable/stops.bin +3 -0
  43. package/src/__e2e__/timetable/timetable.bin +3 -0
  44. package/src/cli/minotor.ts +51 -1
  45. package/src/cli/perf.ts +136 -0
  46. package/src/cli/repl.ts +26 -13
  47. package/src/cli/utils.ts +6 -28
  48. package/src/gtfs/__tests__/parser.test.ts +12 -15
  49. package/src/gtfs/__tests__/services.test.ts +1 -0
  50. package/src/gtfs/__tests__/transfers.test.ts +0 -1
  51. package/src/gtfs/__tests__/trips.test.ts +67 -74
  52. package/src/gtfs/profiles/ch.ts +1 -1
  53. package/src/gtfs/routes.ts +4 -4
  54. package/src/gtfs/services.ts +15 -2
  55. package/src/gtfs/stops.ts +7 -3
  56. package/src/gtfs/transfers.ts +6 -3
  57. package/src/gtfs/trips.ts +33 -16
  58. package/src/gtfs/utils.ts +13 -2
  59. package/src/parser.ts +4 -2
  60. package/src/router.ts +17 -11
  61. package/src/routing/__tests__/result.test.ts +392 -0
  62. package/src/routing/__tests__/router.test.ts +94 -137
  63. package/src/routing/query.ts +28 -7
  64. package/src/routing/result.ts +10 -5
  65. package/src/routing/route.ts +95 -9
  66. package/src/routing/router.ts +82 -66
  67. package/src/stops/__tests__/io.test.ts +1 -1
  68. package/src/stops/__tests__/stopFinder.test.ts +1 -1
  69. package/src/stops/proto/stops.ts +4 -4
  70. package/src/stops/stopsIndex.ts +3 -3
  71. package/src/timetable/__tests__/io.test.ts +16 -23
  72. package/src/timetable/__tests__/route.test.ts +317 -0
  73. package/src/timetable/__tests__/time.test.ts +494 -0
  74. package/src/timetable/__tests__/timetable.test.ts +64 -75
  75. package/src/timetable/io.ts +32 -26
  76. package/src/timetable/proto/timetable.proto +1 -1
  77. package/src/timetable/proto/timetable.ts +13 -13
  78. package/src/timetable/route.ts +347 -0
  79. package/src/timetable/time.ts +40 -8
  80. package/src/timetable/timetable.ts +74 -165
  81. package/tsconfig.build.json +1 -1
@@ -0,0 +1,392 @@
1
+ import assert from 'node:assert';
2
+ import { describe, it } from 'node:test';
3
+
4
+ import { Stop, StopId } from '../../stops/stops.js';
5
+ import { StopsIndex } from '../../stops/stopsIndex.js';
6
+ import { Time } from '../../timetable/time.js';
7
+ import { Query } from '../query.js';
8
+ import { Result } from '../result.js';
9
+ import { ReachingTime, TripLeg } from '../router.js';
10
+
11
+ describe('Result', () => {
12
+ const stop1: Stop = {
13
+ id: 1,
14
+ sourceStopId: 'stop1',
15
+ name: 'Lausanne',
16
+ lat: 0,
17
+ lon: 0,
18
+ children: [],
19
+ parent: undefined,
20
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
21
+ };
22
+
23
+ const stop2: Stop = {
24
+ id: 2,
25
+ sourceStopId: 'stop2',
26
+ name: 'Fribourg',
27
+ lat: 0,
28
+ lon: 0,
29
+ children: [],
30
+ parent: undefined,
31
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
32
+ };
33
+
34
+ const stop3: Stop = {
35
+ id: 3,
36
+ sourceStopId: 'stop3',
37
+ name: 'Bern',
38
+ lat: 0,
39
+ lon: 0,
40
+ children: [],
41
+ parent: undefined,
42
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
43
+ };
44
+
45
+ const stop4: Stop = {
46
+ id: 4,
47
+ sourceStopId: 'stop4',
48
+ name: 'Olten',
49
+ lat: 0,
50
+ lon: 0,
51
+ children: [],
52
+ parent: undefined,
53
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
54
+ };
55
+
56
+ const parentStop: Stop = {
57
+ id: 6,
58
+ sourceStopId: 'parent',
59
+ name: 'Basel',
60
+ lat: 0,
61
+ lon: 0,
62
+ children: [7, 8],
63
+ parent: undefined,
64
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
65
+ };
66
+
67
+ const childStop1: Stop = {
68
+ id: 7,
69
+ sourceStopId: 'child1',
70
+ name: 'Basel Pl. 1',
71
+ lat: 0,
72
+ lon: 0,
73
+ children: [],
74
+ parent: 6,
75
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
76
+ };
77
+
78
+ const childStop2: Stop = {
79
+ id: 8,
80
+ sourceStopId: 'child2',
81
+ name: 'Basel Pl. 2',
82
+ lat: 0,
83
+ lon: 0,
84
+ children: [],
85
+ parent: 6,
86
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
87
+ };
88
+
89
+ const stopsMap = new Map([
90
+ [1, stop1],
91
+ [2, stop2],
92
+ [3, stop3],
93
+ [4, stop4],
94
+ [6, parentStop],
95
+ [7, childStop1],
96
+ [8, childStop2],
97
+ ]);
98
+
99
+ const mockStopsIndex = new StopsIndex(stopsMap);
100
+
101
+ const mockQuery = new Query.Builder()
102
+ .from('stop1')
103
+ .to(new Set(['stop3', 'stop4']))
104
+ .departureTime(Time.fromHMS(8, 0, 0))
105
+ .build();
106
+
107
+ describe('bestRoute', () => {
108
+ it('should return undefined when no route exists', () => {
109
+ const earliestArrivals = new Map<StopId, ReachingTime>();
110
+ const earliestArrivalsPerRound: Map<StopId, TripLeg>[] = [];
111
+
112
+ const result = new Result(
113
+ mockQuery,
114
+ earliestArrivals,
115
+ earliestArrivalsPerRound,
116
+ mockStopsIndex,
117
+ );
118
+
119
+ const route = result.bestRoute();
120
+ assert.strictEqual(route, undefined);
121
+ });
122
+
123
+ it('should return undefined for unreachable destination', () => {
124
+ const earliestArrivals = new Map([
125
+ [2, { arrival: Time.fromHMS(8, 30, 0), legNumber: 0, origin: 1 }],
126
+ ]);
127
+ const earliestArrivalsPerRound: Map<StopId, TripLeg>[] = [];
128
+
129
+ const result = new Result(
130
+ mockQuery,
131
+ earliestArrivals,
132
+ earliestArrivalsPerRound,
133
+ mockStopsIndex,
134
+ );
135
+
136
+ const route = result.bestRoute('stop4'); // stop4 not in earliestArrivals
137
+ assert.strictEqual(route, undefined);
138
+ });
139
+
140
+ it('should return route to closest destination when multiple destinations exist', () => {
141
+ const earliestArrivals = new Map([
142
+ [3, { arrival: Time.fromHMS(9, 0, 0), legNumber: 0, origin: 1 }], // faster
143
+ [4, { arrival: Time.fromHMS(9, 30, 0), legNumber: 0, origin: 1 }], // slower
144
+ ]);
145
+
146
+ const vehicleLegTo3 = {
147
+ from: stop1,
148
+ to: stop3,
149
+ route: { type: 'BUS', name: 'Bus 101' },
150
+ departureTime: Time.fromHMS(8, 0, 0),
151
+ arrivalTime: Time.fromHMS(9, 0, 0),
152
+ };
153
+
154
+ const earliestArrivalsPerRound = [
155
+ new Map([
156
+ [
157
+ 3,
158
+ {
159
+ arrival: Time.fromHMS(9, 0, 0),
160
+ legNumber: 0,
161
+ origin: 1,
162
+ leg: vehicleLegTo3,
163
+ } as TripLeg,
164
+ ],
165
+ ]),
166
+ ];
167
+
168
+ const result = new Result(
169
+ mockQuery,
170
+ earliestArrivals,
171
+ earliestArrivalsPerRound,
172
+ mockStopsIndex,
173
+ );
174
+
175
+ const route = result.bestRoute();
176
+ assert(route);
177
+ assert.strictEqual(route.legs.length, 1);
178
+ assert.deepStrictEqual(route.legs[0], vehicleLegTo3);
179
+ });
180
+
181
+ it('should return route to fastest child stop when parent stop is queried', () => {
182
+ const vehicleLegToChild1 = {
183
+ from: stop1,
184
+ to: childStop1,
185
+ route: { type: 'BUS', name: 'Bus 101' },
186
+ departureTime: Time.fromHMS(8, 0, 0),
187
+ arrivalTime: Time.fromHMS(9, 0, 0),
188
+ };
189
+
190
+ const earliestArrivals = new Map([
191
+ [7, { arrival: Time.fromHMS(9, 0, 0), legNumber: 0, origin: 1 }], // child1 - faster
192
+ [8, { arrival: Time.fromHMS(9, 30, 0), legNumber: 0, origin: 1 }], // child2 - slower
193
+ ]);
194
+
195
+ const earliestArrivalsPerRound = [
196
+ new Map([
197
+ [
198
+ 7,
199
+ {
200
+ arrival: Time.fromHMS(9, 0, 0),
201
+ legNumber: 0,
202
+ origin: 1,
203
+ leg: vehicleLegToChild1,
204
+ } as TripLeg,
205
+ ],
206
+ ]),
207
+ ];
208
+
209
+ const result = new Result(
210
+ mockQuery,
211
+ earliestArrivals,
212
+ earliestArrivalsPerRound,
213
+ mockStopsIndex,
214
+ );
215
+
216
+ const route = result.bestRoute('parent');
217
+ assert(route);
218
+ assert.strictEqual(route.legs.length, 1);
219
+ assert.deepStrictEqual(route.legs[0], vehicleLegToChild1);
220
+ });
221
+
222
+ it('should handle simple single-leg route reconstruction', () => {
223
+ const vehicleLeg = {
224
+ from: stop1,
225
+ to: stop3,
226
+ route: { type: 'BUS', name: 'Bus 101' },
227
+ departureTime: Time.fromHMS(8, 0, 0),
228
+ arrivalTime: Time.fromHMS(9, 0, 0),
229
+ };
230
+
231
+ // Simple case: origin stop 1, destination stop 3, direct connection
232
+ const earliestArrivals = new Map([
233
+ [3, { arrival: Time.fromHMS(9, 0, 0), legNumber: 0, origin: 1 }],
234
+ ]);
235
+
236
+ const earliestArrivalsPerRound = [
237
+ new Map([
238
+ [
239
+ 3,
240
+ {
241
+ arrival: Time.fromHMS(9, 0, 0),
242
+ legNumber: 0,
243
+ origin: 1,
244
+ leg: vehicleLeg,
245
+ } as TripLeg,
246
+ ],
247
+ ]),
248
+ ];
249
+
250
+ const result = new Result(
251
+ mockQuery,
252
+ earliestArrivals,
253
+ earliestArrivalsPerRound,
254
+ mockStopsIndex,
255
+ );
256
+
257
+ const route = result.bestRoute('stop3');
258
+ assert(route);
259
+ assert.strictEqual(route.legs.length, 1);
260
+ assert.deepStrictEqual(route.legs[0], vehicleLeg);
261
+ });
262
+ });
263
+
264
+ describe('arrivalAt', () => {
265
+ it('should return arrival time for a reachable stop', () => {
266
+ const arrivalTime = {
267
+ arrival: Time.fromHMS(9, 0, 0),
268
+ legNumber: 1,
269
+ origin: 1,
270
+ };
271
+ const earliestArrivals = new Map([[3, arrivalTime]]);
272
+
273
+ const result = new Result(
274
+ mockQuery,
275
+ earliestArrivals,
276
+ [],
277
+ mockStopsIndex,
278
+ );
279
+
280
+ const arrival = result.arrivalAt('stop3');
281
+ assert.deepStrictEqual(arrival, arrivalTime);
282
+ });
283
+
284
+ it('should return undefined for unreachable stop', () => {
285
+ const earliestArrivals = new Map([
286
+ [3, { arrival: Time.fromHMS(9, 0, 0), legNumber: 1, origin: 1 }],
287
+ ]);
288
+
289
+ const result = new Result(
290
+ mockQuery,
291
+ earliestArrivals,
292
+ [],
293
+ mockStopsIndex,
294
+ );
295
+
296
+ const arrival = result.arrivalAt('stop4');
297
+ assert.strictEqual(arrival, undefined);
298
+ });
299
+
300
+ it('should return earliest arrival among equivalent stops', () => {
301
+ const earlierArrival = {
302
+ arrival: Time.fromHMS(9, 0, 0),
303
+ legNumber: 1,
304
+ origin: 1,
305
+ };
306
+ const laterArrival = {
307
+ arrival: Time.fromHMS(9, 30, 0),
308
+ legNumber: 1,
309
+ origin: 1,
310
+ };
311
+
312
+ const earliestArrivals = new Map([
313
+ [7, earlierArrival], // child1 - faster
314
+ [8, laterArrival], // child2 - slower
315
+ ]);
316
+
317
+ const result = new Result(
318
+ mockQuery,
319
+ earliestArrivals,
320
+ [],
321
+ mockStopsIndex,
322
+ );
323
+
324
+ const arrival = result.arrivalAt('parent');
325
+ assert.deepStrictEqual(arrival, earlierArrival);
326
+ });
327
+
328
+ it('should respect maxTransfers constraint', () => {
329
+ const tripLeg1 = {
330
+ arrival: Time.fromHMS(8, 30, 0),
331
+ legNumber: 0,
332
+ origin: 1,
333
+ };
334
+ const tripLeg2 = {
335
+ arrival: Time.fromHMS(9, 0, 0),
336
+ legNumber: 1,
337
+ origin: 1,
338
+ };
339
+ const tripLeg3 = {
340
+ arrival: Time.fromHMS(9, 30, 0),
341
+ legNumber: 2,
342
+ origin: 1,
343
+ };
344
+
345
+ const earliestArrivals = new Map([
346
+ [3, tripLeg2], // 1 transfer
347
+ ]);
348
+
349
+ const earliestArrivalsPerRound = [
350
+ new Map([[3, tripLeg1]]), // Round 0 (start)
351
+ new Map([[3, tripLeg3]]), // Round 1 (no transfers)
352
+ new Map([[3, tripLeg2]]), // Round 2 (1 transfers)
353
+ ];
354
+
355
+ const result = new Result(
356
+ mockQuery,
357
+ earliestArrivals,
358
+ earliestArrivalsPerRound,
359
+ mockStopsIndex,
360
+ );
361
+
362
+ const arrivalWithLimit = result.arrivalAt('stop3', 0);
363
+ assert.deepStrictEqual(arrivalWithLimit, tripLeg3);
364
+
365
+ const arrivalWithoutLimit = result.arrivalAt('stop3', 1);
366
+ assert.deepStrictEqual(arrivalWithoutLimit, tripLeg2);
367
+ });
368
+
369
+ it('should handle non-existent stops', () => {
370
+ const earliestArrivals = new Map([
371
+ [
372
+ 3,
373
+ {
374
+ arrival: Time.fromHMS(9, 0, 0),
375
+ legNumber: 1,
376
+ origin: 1,
377
+ } as ReachingTime,
378
+ ],
379
+ ]);
380
+
381
+ const result = new Result(
382
+ mockQuery,
383
+ earliestArrivals,
384
+ [],
385
+ mockStopsIndex,
386
+ );
387
+
388
+ const arrival = result.arrivalAt('nonexistent');
389
+ assert.strictEqual(arrival, undefined);
390
+ });
391
+ });
392
+ });