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.
- package/.cspell.json +14 -1
- package/.gitattributes +3 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +3 -0
- package/.github/workflows/minotor.yml +17 -1
- package/CHANGELOG.md +3 -9
- package/README.md +47 -17
- package/dist/__e2e__/router.test.d.ts +1 -0
- package/dist/cli/perf.d.ts +28 -0
- package/dist/cli/utils.d.ts +6 -2
- package/dist/cli.mjs +1967 -823
- package/dist/cli.mjs.map +1 -1
- package/dist/gtfs/trips.d.ts +1 -0
- package/dist/gtfs/utils.d.ts +1 -1
- package/dist/parser.cjs.js +1030 -627
- package/dist/parser.cjs.js.map +1 -1
- package/dist/parser.d.ts +4 -2
- package/dist/parser.esm.js +1030 -627
- package/dist/parser.esm.js.map +1 -1
- package/dist/router.cjs.js +1 -1
- package/dist/router.cjs.js.map +1 -1
- package/dist/router.d.ts +10 -5
- package/dist/router.esm.js +1 -1
- package/dist/router.esm.js.map +1 -1
- package/dist/router.umd.js +1 -1
- package/dist/router.umd.js.map +1 -1
- package/dist/routing/__tests__/result.test.d.ts +1 -0
- package/dist/routing/query.d.ts +27 -6
- package/dist/routing/result.d.ts +1 -1
- package/dist/routing/route.d.ts +47 -2
- package/dist/routing/router.d.ts +15 -1
- package/dist/stops/stopsIndex.d.ts +3 -3
- package/dist/timetable/__tests__/route.test.d.ts +1 -0
- package/dist/timetable/__tests__/time.test.d.ts +1 -0
- package/dist/timetable/io.d.ts +7 -1
- package/dist/timetable/proto/timetable.d.ts +1 -1
- package/dist/timetable/route.d.ts +155 -0
- package/dist/timetable/time.d.ts +21 -0
- package/dist/timetable/timetable.d.ts +41 -61
- package/package.json +36 -35
- package/src/__e2e__/benchmark.json +22 -0
- package/src/__e2e__/router.test.ts +209 -0
- package/src/__e2e__/timetable/stops.bin +3 -0
- package/src/__e2e__/timetable/timetable.bin +3 -0
- package/src/cli/minotor.ts +51 -1
- package/src/cli/perf.ts +136 -0
- package/src/cli/repl.ts +26 -13
- package/src/cli/utils.ts +6 -28
- package/src/gtfs/__tests__/parser.test.ts +12 -15
- package/src/gtfs/__tests__/services.test.ts +1 -0
- package/src/gtfs/__tests__/transfers.test.ts +0 -1
- package/src/gtfs/__tests__/trips.test.ts +67 -74
- package/src/gtfs/profiles/ch.ts +1 -1
- package/src/gtfs/routes.ts +4 -4
- package/src/gtfs/services.ts +15 -2
- package/src/gtfs/stops.ts +7 -3
- package/src/gtfs/transfers.ts +6 -3
- package/src/gtfs/trips.ts +33 -16
- package/src/gtfs/utils.ts +13 -2
- package/src/parser.ts +4 -2
- package/src/router.ts +17 -11
- package/src/routing/__tests__/result.test.ts +392 -0
- package/src/routing/__tests__/router.test.ts +94 -137
- package/src/routing/query.ts +28 -7
- package/src/routing/result.ts +10 -5
- package/src/routing/route.ts +95 -9
- package/src/routing/router.ts +82 -66
- package/src/stops/__tests__/io.test.ts +1 -1
- package/src/stops/__tests__/stopFinder.test.ts +1 -1
- package/src/stops/proto/stops.ts +4 -4
- package/src/stops/stopsIndex.ts +3 -3
- package/src/timetable/__tests__/io.test.ts +16 -23
- package/src/timetable/__tests__/route.test.ts +317 -0
- package/src/timetable/__tests__/time.test.ts +494 -0
- package/src/timetable/__tests__/timetable.test.ts +64 -75
- package/src/timetable/io.ts +32 -26
- package/src/timetable/proto/timetable.proto +1 -1
- package/src/timetable/proto/timetable.ts +13 -13
- package/src/timetable/route.ts +347 -0
- package/src/timetable/time.ts +40 -8
- package/src/timetable/timetable.ts +74 -165
- 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
|
+
});
|