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.
- package/.cspell.json +11 -1
- package/CHANGELOG.md +8 -3
- package/README.md +26 -24
- package/dist/cli.mjs +1786 -791
- package/dist/cli.mjs.map +1 -1
- package/dist/gtfs/transfers.d.ts +29 -5
- package/dist/gtfs/trips.d.ts +10 -5
- package/dist/parser.cjs.js +972 -525
- package/dist/parser.cjs.js.map +1 -1
- package/dist/parser.esm.js +972 -525
- 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 +2 -2
- 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__/plotter.test.d.ts +1 -0
- package/dist/routing/plotter.d.ts +42 -3
- package/dist/routing/result.d.ts +23 -7
- package/dist/routing/route.d.ts +2 -0
- package/dist/routing/router.d.ts +78 -19
- package/dist/timetable/__tests__/tripBoardingId.test.d.ts +1 -0
- package/dist/timetable/io.d.ts +4 -2
- package/dist/timetable/proto/timetable.d.ts +15 -1
- package/dist/timetable/route.d.ts +48 -23
- package/dist/timetable/timetable.d.ts +24 -7
- package/dist/timetable/tripBoardingId.d.ts +34 -0
- package/package.json +1 -1
- package/src/__e2e__/router.test.ts +114 -105
- package/src/__e2e__/timetable/stops.bin +2 -2
- package/src/__e2e__/timetable/timetable.bin +2 -2
- package/src/cli/repl.ts +245 -1
- package/src/gtfs/__tests__/parser.test.ts +19 -4
- package/src/gtfs/__tests__/transfers.test.ts +773 -37
- package/src/gtfs/__tests__/trips.test.ts +308 -27
- package/src/gtfs/parser.ts +36 -6
- package/src/gtfs/transfers.ts +193 -19
- package/src/gtfs/trips.ts +58 -21
- package/src/router.ts +2 -2
- package/src/routing/__tests__/plotter.test.ts +230 -0
- package/src/routing/__tests__/result.test.ts +486 -125
- package/src/routing/__tests__/route.test.ts +7 -3
- package/src/routing/__tests__/router.test.ts +380 -172
- package/src/routing/plotter.ts +279 -48
- package/src/routing/result.ts +114 -34
- package/src/routing/route.ts +0 -3
- package/src/routing/router.ts +344 -211
- package/src/timetable/__tests__/io.test.ts +34 -1
- package/src/timetable/__tests__/route.test.ts +74 -81
- package/src/timetable/__tests__/timetable.test.ts +232 -61
- package/src/timetable/__tests__/tripBoardingId.test.ts +57 -0
- package/src/timetable/io.ts +72 -10
- package/src/timetable/proto/timetable.proto +16 -2
- package/src/timetable/proto/timetable.ts +256 -22
- package/src/timetable/route.ts +174 -58
- package/src/timetable/timetable.ts +66 -16
- package/src/timetable/tripBoardingId.ts +94 -0
- package/tsconfig.json +2 -2
|
@@ -3,8 +3,17 @@ import { Readable } from 'node:stream';
|
|
|
3
3
|
import { describe, it } from 'node:test';
|
|
4
4
|
|
|
5
5
|
import { Duration } from '../../timetable/duration.js';
|
|
6
|
+
import { Route } from '../../timetable/route.js';
|
|
7
|
+
import { Time } from '../../timetable/time.js';
|
|
8
|
+
import { Timetable } from '../../timetable/timetable.js';
|
|
9
|
+
import { encode } from '../../timetable/tripBoardingId.js';
|
|
6
10
|
import { GtfsStopsMap } from '../stops.js';
|
|
7
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
buildTripContinuations,
|
|
13
|
+
GtfsTripContinuation,
|
|
14
|
+
parseTransfers,
|
|
15
|
+
} from '../transfers.js';
|
|
16
|
+
import { TripsMapping } from '../trips.js';
|
|
8
17
|
|
|
9
18
|
describe('GTFS transfers parser', () => {
|
|
10
19
|
it('should correctly parse valid transfers', async () => {
|
|
@@ -13,7 +22,7 @@ describe('GTFS transfers parser', () => {
|
|
|
13
22
|
'from_stop_id,to_stop_id,transfer_type,min_transfer_time\n',
|
|
14
23
|
);
|
|
15
24
|
mockedStream.push('"1100084","8014440:0:1","2","180"\n');
|
|
16
|
-
mockedStream.push('"1100097","8014447","
|
|
25
|
+
mockedStream.push('"1100097","8014447","0","240"\n');
|
|
17
26
|
mockedStream.push(null);
|
|
18
27
|
|
|
19
28
|
const stopsMap: GtfsStopsMap = new Map([
|
|
@@ -59,7 +68,8 @@ describe('GTFS transfers parser', () => {
|
|
|
59
68
|
],
|
|
60
69
|
]);
|
|
61
70
|
|
|
62
|
-
const
|
|
71
|
+
const result = await parseTransfers(mockedStream, stopsMap);
|
|
72
|
+
|
|
63
73
|
const expectedTransfers = new Map([
|
|
64
74
|
[
|
|
65
75
|
0, // Internal ID for stop '1100084'
|
|
@@ -76,17 +86,18 @@ describe('GTFS transfers parser', () => {
|
|
|
76
86
|
[
|
|
77
87
|
{
|
|
78
88
|
destination: 3, // Internal ID for stop '8014447'
|
|
79
|
-
type: '
|
|
89
|
+
type: 'RECOMMENDED',
|
|
80
90
|
minTransferTime: Duration.fromSeconds(240),
|
|
81
91
|
},
|
|
82
92
|
],
|
|
83
93
|
],
|
|
84
94
|
]);
|
|
85
95
|
|
|
86
|
-
assert.deepEqual(transfers, expectedTransfers);
|
|
96
|
+
assert.deepEqual(result.transfers, expectedTransfers);
|
|
97
|
+
assert.deepEqual(result.tripContinuations, []);
|
|
87
98
|
});
|
|
88
99
|
|
|
89
|
-
it('should ignore impossible transfer types', async () => {
|
|
100
|
+
it('should ignore impossible transfer types (3 and 5)', async () => {
|
|
90
101
|
const mockedStream = new Readable();
|
|
91
102
|
mockedStream.push(
|
|
92
103
|
'from_stop_id,to_stop_id,transfer_type,min_transfer_time\n',
|
|
@@ -138,16 +149,57 @@ describe('GTFS transfers parser', () => {
|
|
|
138
149
|
],
|
|
139
150
|
]);
|
|
140
151
|
|
|
141
|
-
const
|
|
142
|
-
|
|
152
|
+
const result = await parseTransfers(mockedStream, stopsMap);
|
|
153
|
+
|
|
154
|
+
assert.deepEqual(result.transfers, new Map());
|
|
155
|
+
assert.deepEqual(result.tripContinuations, []);
|
|
143
156
|
});
|
|
144
157
|
|
|
145
|
-
it('should ignore
|
|
158
|
+
it('should ignore transfers with missing stop IDs', async () => {
|
|
159
|
+
const mockedStream = new Readable();
|
|
160
|
+
mockedStream.push(
|
|
161
|
+
'from_stop_id,to_stop_id,transfer_type,min_transfer_time\n',
|
|
162
|
+
);
|
|
163
|
+
mockedStream.push(',"8014440:0:1","2","180"\n');
|
|
164
|
+
mockedStream.push('"1100097",,"0","240"\n');
|
|
165
|
+
mockedStream.push(null);
|
|
166
|
+
|
|
167
|
+
const stopsMap: GtfsStopsMap = new Map([
|
|
168
|
+
[
|
|
169
|
+
'8014440:0:1',
|
|
170
|
+
{
|
|
171
|
+
id: 1,
|
|
172
|
+
sourceStopId: '8014440:0:1',
|
|
173
|
+
name: 'Test Stop 2',
|
|
174
|
+
children: [],
|
|
175
|
+
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
[
|
|
179
|
+
'1100097',
|
|
180
|
+
{
|
|
181
|
+
id: 2,
|
|
182
|
+
sourceStopId: '1100097',
|
|
183
|
+
name: 'Test Stop 3',
|
|
184
|
+
children: [],
|
|
185
|
+
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
]);
|
|
189
|
+
|
|
190
|
+
const result = await parseTransfers(mockedStream, stopsMap);
|
|
191
|
+
|
|
192
|
+
assert.deepEqual(result.transfers, new Map());
|
|
193
|
+
assert.deepEqual(result.tripContinuations, []);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should correctly parse in-seat transfers (type 4)', async () => {
|
|
146
197
|
const mockedStream = new Readable();
|
|
147
198
|
mockedStream.push(
|
|
148
|
-
'
|
|
199
|
+
'from_stop_id,to_stop_id,from_trip_id,to_trip_id,transfer_type,min_transfer_time\n',
|
|
149
200
|
);
|
|
150
|
-
mockedStream.push('"1100084","8014440","
|
|
201
|
+
mockedStream.push('"1100084","8014440:0:1","trip1","trip2","4","0"\n');
|
|
202
|
+
mockedStream.push('"1100097","8014447","trip3","trip4","4","0"\n');
|
|
151
203
|
mockedStream.push(null);
|
|
152
204
|
|
|
153
205
|
const stopsMap: GtfsStopsMap = new Map([
|
|
@@ -162,27 +214,161 @@ describe('GTFS transfers parser', () => {
|
|
|
162
214
|
},
|
|
163
215
|
],
|
|
164
216
|
[
|
|
165
|
-
'8014440',
|
|
217
|
+
'8014440:0:1',
|
|
166
218
|
{
|
|
167
219
|
id: 1,
|
|
168
|
-
sourceStopId: '8014440',
|
|
220
|
+
sourceStopId: '8014440:0:1',
|
|
169
221
|
name: 'Test Stop 2',
|
|
170
222
|
children: [],
|
|
171
223
|
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
172
224
|
},
|
|
173
225
|
],
|
|
226
|
+
[
|
|
227
|
+
'1100097',
|
|
228
|
+
{
|
|
229
|
+
id: 2,
|
|
230
|
+
sourceStopId: '1100097',
|
|
231
|
+
name: 'Test Stop 3',
|
|
232
|
+
children: [],
|
|
233
|
+
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
[
|
|
237
|
+
'8014447',
|
|
238
|
+
{
|
|
239
|
+
id: 3,
|
|
240
|
+
sourceStopId: '8014447',
|
|
241
|
+
name: 'Test Stop 4',
|
|
242
|
+
children: [],
|
|
243
|
+
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
244
|
+
},
|
|
245
|
+
],
|
|
174
246
|
]);
|
|
175
247
|
|
|
176
|
-
const
|
|
177
|
-
|
|
248
|
+
const result = await parseTransfers(mockedStream, stopsMap);
|
|
249
|
+
|
|
250
|
+
const expectedTripContinuations = [
|
|
251
|
+
{
|
|
252
|
+
fromStop: 0,
|
|
253
|
+
fromTrip: 'trip1',
|
|
254
|
+
toStop: 1,
|
|
255
|
+
toTrip: 'trip2',
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
fromStop: 2,
|
|
259
|
+
fromTrip: 'trip3',
|
|
260
|
+
toStop: 3,
|
|
261
|
+
toTrip: 'trip4',
|
|
262
|
+
},
|
|
263
|
+
];
|
|
264
|
+
|
|
265
|
+
assert.deepEqual(result.transfers, new Map());
|
|
266
|
+
assert.deepEqual(result.tripContinuations, expectedTripContinuations);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('should ignore in-seat transfers with missing trip IDs', async () => {
|
|
270
|
+
const mockedStream = new Readable();
|
|
271
|
+
mockedStream.push(
|
|
272
|
+
'from_stop_id,to_stop_id,from_trip_id,to_trip_id,transfer_type,min_transfer_time\n',
|
|
273
|
+
);
|
|
274
|
+
mockedStream.push('"1100084","8014440:0:1",,"trip2","4","0"\n');
|
|
275
|
+
mockedStream.push('"1100097","8014447","trip3",,"4","0"\n');
|
|
276
|
+
mockedStream.push('"1100098","8014448","","trip5","4","0"\n');
|
|
277
|
+
mockedStream.push('"1100099","8014449","trip6","","4","0"\n');
|
|
278
|
+
mockedStream.push(null);
|
|
279
|
+
|
|
280
|
+
const stopsMap: GtfsStopsMap = new Map([
|
|
281
|
+
[
|
|
282
|
+
'1100084',
|
|
283
|
+
{
|
|
284
|
+
id: 0,
|
|
285
|
+
sourceStopId: '1100084',
|
|
286
|
+
name: 'Test Stop 1',
|
|
287
|
+
children: [],
|
|
288
|
+
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
289
|
+
},
|
|
290
|
+
],
|
|
291
|
+
[
|
|
292
|
+
'8014440:0:1',
|
|
293
|
+
{
|
|
294
|
+
id: 1,
|
|
295
|
+
sourceStopId: '8014440:0:1',
|
|
296
|
+
name: 'Test Stop 2',
|
|
297
|
+
children: [],
|
|
298
|
+
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
299
|
+
},
|
|
300
|
+
],
|
|
301
|
+
[
|
|
302
|
+
'1100097',
|
|
303
|
+
{
|
|
304
|
+
id: 2,
|
|
305
|
+
sourceStopId: '1100097',
|
|
306
|
+
name: 'Test Stop 3',
|
|
307
|
+
children: [],
|
|
308
|
+
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
309
|
+
},
|
|
310
|
+
],
|
|
311
|
+
[
|
|
312
|
+
'8014447',
|
|
313
|
+
{
|
|
314
|
+
id: 3,
|
|
315
|
+
sourceStopId: '8014447',
|
|
316
|
+
name: 'Test Stop 4',
|
|
317
|
+
children: [],
|
|
318
|
+
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
319
|
+
},
|
|
320
|
+
],
|
|
321
|
+
]);
|
|
322
|
+
|
|
323
|
+
const result = await parseTransfers(mockedStream, stopsMap);
|
|
324
|
+
|
|
325
|
+
assert.deepEqual(result.transfers, new Map());
|
|
326
|
+
assert.deepEqual(result.tripContinuations, []);
|
|
178
327
|
});
|
|
179
328
|
|
|
180
329
|
it('should ignore unsupported transfer types between trips', async () => {
|
|
181
330
|
const mockedStream = new Readable();
|
|
182
331
|
mockedStream.push(
|
|
183
|
-
'from_trip_id,to_trip_id,transfer_type,min_transfer_time\n',
|
|
332
|
+
'from_stop_id,to_stop_id,from_trip_id,to_trip_id,transfer_type,min_transfer_time\n',
|
|
333
|
+
);
|
|
334
|
+
mockedStream.push('"1100084","8014440:0:1","trip1","trip2","1","0"\n');
|
|
335
|
+
mockedStream.push(null);
|
|
336
|
+
|
|
337
|
+
const stopsMap: GtfsStopsMap = new Map([
|
|
338
|
+
[
|
|
339
|
+
'1100084',
|
|
340
|
+
{
|
|
341
|
+
id: 0,
|
|
342
|
+
sourceStopId: '1100084',
|
|
343
|
+
name: 'Test Stop 1',
|
|
344
|
+
children: [],
|
|
345
|
+
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
346
|
+
},
|
|
347
|
+
],
|
|
348
|
+
[
|
|
349
|
+
'8014440:0:1',
|
|
350
|
+
{
|
|
351
|
+
id: 1,
|
|
352
|
+
sourceStopId: '8014440:0:1',
|
|
353
|
+
name: 'Test Stop 2',
|
|
354
|
+
children: [],
|
|
355
|
+
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
356
|
+
},
|
|
357
|
+
],
|
|
358
|
+
]);
|
|
359
|
+
|
|
360
|
+
const result = await parseTransfers(mockedStream, stopsMap);
|
|
361
|
+
|
|
362
|
+
assert.deepEqual(result.transfers, new Map());
|
|
363
|
+
assert.deepEqual(result.tripContinuations, []);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('should ignore unsupported transfer types between routes', async () => {
|
|
367
|
+
const mockedStream = new Readable();
|
|
368
|
+
mockedStream.push(
|
|
369
|
+
'from_stop_id,to_stop_id,from_route_id,to_route_id,transfer_type,min_transfer_time\n',
|
|
184
370
|
);
|
|
185
|
-
mockedStream.push('"1100084","8014440","
|
|
371
|
+
mockedStream.push('"1100084","8014440:0:1","route1","route2","1","0"\n');
|
|
186
372
|
mockedStream.push(null);
|
|
187
373
|
|
|
188
374
|
const stopsMap: GtfsStopsMap = new Map([
|
|
@@ -197,10 +383,10 @@ describe('GTFS transfers parser', () => {
|
|
|
197
383
|
},
|
|
198
384
|
],
|
|
199
385
|
[
|
|
200
|
-
'8014440',
|
|
386
|
+
'8014440:0:1',
|
|
201
387
|
{
|
|
202
388
|
id: 1,
|
|
203
|
-
sourceStopId: '8014440',
|
|
389
|
+
sourceStopId: '8014440:0:1',
|
|
204
390
|
name: 'Test Stop 2',
|
|
205
391
|
children: [],
|
|
206
392
|
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
@@ -208,16 +394,19 @@ describe('GTFS transfers parser', () => {
|
|
|
208
394
|
],
|
|
209
395
|
]);
|
|
210
396
|
|
|
211
|
-
const
|
|
212
|
-
|
|
397
|
+
const result = await parseTransfers(mockedStream, stopsMap);
|
|
398
|
+
|
|
399
|
+
assert.deepEqual(result.transfers, new Map());
|
|
400
|
+
assert.deepEqual(result.tripContinuations, []);
|
|
213
401
|
});
|
|
214
402
|
|
|
215
|
-
it('should
|
|
403
|
+
it('should handle transfers without minimum transfer time', async () => {
|
|
216
404
|
const mockedStream = new Readable();
|
|
217
405
|
mockedStream.push(
|
|
218
406
|
'from_stop_id,to_stop_id,transfer_type,min_transfer_time\n',
|
|
219
407
|
);
|
|
220
408
|
mockedStream.push('"1100084","8014440:0:1","2"\n');
|
|
409
|
+
mockedStream.push('"1100097","8014447","1","0"\n');
|
|
221
410
|
mockedStream.push(null);
|
|
222
411
|
|
|
223
412
|
const stopsMap: GtfsStopsMap = new Map([
|
|
@@ -241,25 +430,137 @@ describe('GTFS transfers parser', () => {
|
|
|
241
430
|
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
242
431
|
},
|
|
243
432
|
],
|
|
433
|
+
[
|
|
434
|
+
'1100097',
|
|
435
|
+
{
|
|
436
|
+
id: 2,
|
|
437
|
+
sourceStopId: '1100097',
|
|
438
|
+
name: 'Test Stop 3',
|
|
439
|
+
children: [],
|
|
440
|
+
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
441
|
+
},
|
|
442
|
+
],
|
|
443
|
+
[
|
|
444
|
+
'8014447',
|
|
445
|
+
{
|
|
446
|
+
id: 3,
|
|
447
|
+
sourceStopId: '8014447',
|
|
448
|
+
name: 'Test Stop 4',
|
|
449
|
+
children: [],
|
|
450
|
+
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
451
|
+
},
|
|
452
|
+
],
|
|
244
453
|
]);
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
454
|
+
|
|
455
|
+
const result = await parseTransfers(mockedStream, stopsMap);
|
|
456
|
+
|
|
457
|
+
const expectedTransfers = new Map([
|
|
458
|
+
[
|
|
459
|
+
0,
|
|
249
460
|
[
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
type: 'REQUIRES_MINIMAL_TIME',
|
|
255
|
-
},
|
|
256
|
-
],
|
|
461
|
+
{
|
|
462
|
+
destination: 1,
|
|
463
|
+
type: 'REQUIRES_MINIMAL_TIME',
|
|
464
|
+
},
|
|
257
465
|
],
|
|
258
|
-
]
|
|
466
|
+
],
|
|
467
|
+
[
|
|
468
|
+
2,
|
|
469
|
+
[
|
|
470
|
+
{
|
|
471
|
+
destination: 3,
|
|
472
|
+
type: 'GUARANTEED',
|
|
473
|
+
minTransferTime: Duration.fromSeconds(0),
|
|
474
|
+
},
|
|
475
|
+
],
|
|
476
|
+
],
|
|
477
|
+
]);
|
|
478
|
+
|
|
479
|
+
assert.deepEqual(result.transfers, expectedTransfers);
|
|
480
|
+
assert.deepEqual(result.tripContinuations, []);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it('should handle mixed transfers and trip continuations', async () => {
|
|
484
|
+
const mockedStream = new Readable();
|
|
485
|
+
mockedStream.push(
|
|
486
|
+
'from_stop_id,to_stop_id,from_trip_id,to_trip_id,transfer_type,min_transfer_time\n',
|
|
259
487
|
);
|
|
488
|
+
mockedStream.push('"1100084","8014440:0:1","","","1","120"\n');
|
|
489
|
+
mockedStream.push('"1100097","8014447","trip1","trip2","4","0"\n');
|
|
490
|
+
mockedStream.push(null);
|
|
491
|
+
|
|
492
|
+
const stopsMap: GtfsStopsMap = new Map([
|
|
493
|
+
[
|
|
494
|
+
'1100084',
|
|
495
|
+
{
|
|
496
|
+
id: 0,
|
|
497
|
+
sourceStopId: '1100084',
|
|
498
|
+
name: 'Test Stop 1',
|
|
499
|
+
children: [],
|
|
500
|
+
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
501
|
+
},
|
|
502
|
+
],
|
|
503
|
+
[
|
|
504
|
+
'8014440:0:1',
|
|
505
|
+
{
|
|
506
|
+
id: 1,
|
|
507
|
+
sourceStopId: '8014440:0:1',
|
|
508
|
+
name: 'Test Stop 2',
|
|
509
|
+
children: [],
|
|
510
|
+
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
511
|
+
},
|
|
512
|
+
],
|
|
513
|
+
[
|
|
514
|
+
'1100097',
|
|
515
|
+
{
|
|
516
|
+
id: 2,
|
|
517
|
+
sourceStopId: '1100097',
|
|
518
|
+
name: 'Test Stop 3',
|
|
519
|
+
children: [],
|
|
520
|
+
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
521
|
+
},
|
|
522
|
+
],
|
|
523
|
+
[
|
|
524
|
+
'8014447',
|
|
525
|
+
{
|
|
526
|
+
id: 3,
|
|
527
|
+
sourceStopId: '8014447',
|
|
528
|
+
name: 'Test Stop 4',
|
|
529
|
+
children: [],
|
|
530
|
+
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
531
|
+
},
|
|
532
|
+
],
|
|
533
|
+
]);
|
|
534
|
+
|
|
535
|
+
const result = await parseTransfers(mockedStream, stopsMap);
|
|
536
|
+
|
|
537
|
+
const expectedTransfers = new Map([
|
|
538
|
+
[
|
|
539
|
+
0,
|
|
540
|
+
[
|
|
541
|
+
{
|
|
542
|
+
destination: 1,
|
|
543
|
+
type: 'GUARANTEED',
|
|
544
|
+
minTransferTime: Duration.fromSeconds(120),
|
|
545
|
+
},
|
|
546
|
+
],
|
|
547
|
+
],
|
|
548
|
+
]);
|
|
549
|
+
|
|
550
|
+
const expectedTripContinuations = [
|
|
551
|
+
{
|
|
552
|
+
fromStop: 2,
|
|
553
|
+
fromTrip: 'trip1',
|
|
554
|
+
toStop: 3,
|
|
555
|
+
toTrip: 'trip2',
|
|
556
|
+
},
|
|
557
|
+
];
|
|
558
|
+
|
|
559
|
+
assert.deepEqual(result.transfers, expectedTransfers);
|
|
560
|
+
assert.deepEqual(result.tripContinuations, expectedTripContinuations);
|
|
260
561
|
});
|
|
261
562
|
|
|
262
|
-
it('should handle empty transfers', async () => {
|
|
563
|
+
it('should handle empty transfers file', async () => {
|
|
263
564
|
const mockedStream = new Readable();
|
|
264
565
|
mockedStream.push(
|
|
265
566
|
'from_stop_id,to_stop_id,transfer_type,min_transfer_time\n',
|
|
@@ -268,7 +569,442 @@ describe('GTFS transfers parser', () => {
|
|
|
268
569
|
|
|
269
570
|
const stopsMap: GtfsStopsMap = new Map();
|
|
270
571
|
|
|
271
|
-
const
|
|
272
|
-
|
|
572
|
+
const result = await parseTransfers(mockedStream, stopsMap);
|
|
573
|
+
|
|
574
|
+
assert.deepEqual(result.transfers, new Map());
|
|
575
|
+
assert.deepEqual(result.tripContinuations, []);
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
it('should ignore transfers with non-existent stops', async () => {
|
|
579
|
+
const mockedStream = new Readable();
|
|
580
|
+
mockedStream.push(
|
|
581
|
+
'from_stop_id,to_stop_id,transfer_type,min_transfer_time\n',
|
|
582
|
+
);
|
|
583
|
+
mockedStream.push('"unknown_stop","8014440:0:1","0","120"\n');
|
|
584
|
+
mockedStream.push('"1100084","unknown_stop","1","60"\n');
|
|
585
|
+
mockedStream.push('"1100084","8014440:0:1","2","180"\n');
|
|
586
|
+
mockedStream.push(null);
|
|
587
|
+
|
|
588
|
+
const stopsMap: GtfsStopsMap = new Map([
|
|
589
|
+
[
|
|
590
|
+
'1100084',
|
|
591
|
+
{
|
|
592
|
+
id: 0,
|
|
593
|
+
sourceStopId: '1100084',
|
|
594
|
+
name: 'Test Stop 1',
|
|
595
|
+
children: [],
|
|
596
|
+
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
597
|
+
},
|
|
598
|
+
],
|
|
599
|
+
[
|
|
600
|
+
'8014440:0:1',
|
|
601
|
+
{
|
|
602
|
+
id: 1,
|
|
603
|
+
sourceStopId: '8014440:0:1',
|
|
604
|
+
name: 'Test Stop 2',
|
|
605
|
+
children: [],
|
|
606
|
+
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
607
|
+
},
|
|
608
|
+
],
|
|
609
|
+
]);
|
|
610
|
+
|
|
611
|
+
const result = await parseTransfers(mockedStream, stopsMap);
|
|
612
|
+
|
|
613
|
+
const expectedTransfers = new Map([
|
|
614
|
+
[
|
|
615
|
+
0,
|
|
616
|
+
[
|
|
617
|
+
{
|
|
618
|
+
destination: 1,
|
|
619
|
+
type: 'REQUIRES_MINIMAL_TIME',
|
|
620
|
+
minTransferTime: Duration.fromSeconds(180),
|
|
621
|
+
},
|
|
622
|
+
],
|
|
623
|
+
],
|
|
624
|
+
]);
|
|
625
|
+
|
|
626
|
+
assert.deepEqual(result.transfers, expectedTransfers);
|
|
627
|
+
assert.deepEqual(result.tripContinuations, []);
|
|
628
|
+
});
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
describe('buildTripContinuations', () => {
|
|
632
|
+
it('should build trip continuations for valid data', () => {
|
|
633
|
+
const tripsMapping: TripsMapping = new Map([
|
|
634
|
+
['trip1', { routeId: 0, tripRouteIndex: 0 }],
|
|
635
|
+
['trip2', { routeId: 1, tripRouteIndex: 0 }],
|
|
636
|
+
]);
|
|
637
|
+
|
|
638
|
+
const tripContinuations = [
|
|
639
|
+
{
|
|
640
|
+
fromStop: 100,
|
|
641
|
+
fromTrip: 'trip1',
|
|
642
|
+
toStop: 200,
|
|
643
|
+
toTrip: 'trip2',
|
|
644
|
+
},
|
|
645
|
+
];
|
|
646
|
+
|
|
647
|
+
// Mock route with simple stops and timing
|
|
648
|
+
const mockFromRoute = {
|
|
649
|
+
stopRouteIndices: () => [0],
|
|
650
|
+
arrivalAt: () => Time.fromMinutes(60), // 1:00
|
|
651
|
+
} as unknown as Route;
|
|
652
|
+
|
|
653
|
+
const mockToRoute = {
|
|
654
|
+
stopRouteIndices: () => [1],
|
|
655
|
+
departureFrom: () => Time.fromMinutes(75), // 1:15
|
|
656
|
+
} as unknown as Route;
|
|
657
|
+
|
|
658
|
+
const mockTimetable = {
|
|
659
|
+
getRoute: (routeId: number) =>
|
|
660
|
+
routeId === 0 ? mockFromRoute : mockToRoute,
|
|
661
|
+
} as unknown as Timetable;
|
|
662
|
+
|
|
663
|
+
const activeStopIds = new Set([100, 200]);
|
|
664
|
+
|
|
665
|
+
const result = buildTripContinuations(
|
|
666
|
+
tripsMapping,
|
|
667
|
+
tripContinuations,
|
|
668
|
+
mockTimetable,
|
|
669
|
+
activeStopIds,
|
|
670
|
+
);
|
|
671
|
+
|
|
672
|
+
const expectedTripBoardingId = encode(0, 0, 0);
|
|
673
|
+
const continuations = result.get(expectedTripBoardingId);
|
|
674
|
+
|
|
675
|
+
assert(continuations);
|
|
676
|
+
assert.strictEqual(continuations.length, 1);
|
|
677
|
+
assert.deepEqual(continuations[0], {
|
|
678
|
+
hopOnStopIndex: 1,
|
|
679
|
+
routeId: 1,
|
|
680
|
+
tripIndex: 0,
|
|
681
|
+
});
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
it('should ignore trip continuations with inactive stops', () => {
|
|
685
|
+
const tripsMapping: TripsMapping = new Map([
|
|
686
|
+
['trip1', { routeId: 0, tripRouteIndex: 0 }],
|
|
687
|
+
['trip2', { routeId: 1, tripRouteIndex: 0 }],
|
|
688
|
+
]);
|
|
689
|
+
|
|
690
|
+
const tripContinuations = [
|
|
691
|
+
{
|
|
692
|
+
fromStop: 100, // inactive stop
|
|
693
|
+
fromTrip: 'trip1',
|
|
694
|
+
toStop: 200,
|
|
695
|
+
toTrip: 'trip2',
|
|
696
|
+
},
|
|
697
|
+
];
|
|
698
|
+
|
|
699
|
+
const mockTimetable = {} as unknown as Timetable;
|
|
700
|
+
const activeStopIds = new Set([200]); // only toStop is active
|
|
701
|
+
|
|
702
|
+
const result = buildTripContinuations(
|
|
703
|
+
tripsMapping,
|
|
704
|
+
tripContinuations,
|
|
705
|
+
mockTimetable,
|
|
706
|
+
activeStopIds,
|
|
707
|
+
);
|
|
708
|
+
|
|
709
|
+
assert.strictEqual(result.size, 0);
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
it('should ignore trip continuations with unknown trip IDs', () => {
|
|
713
|
+
const tripsMapping: TripsMapping = new Map([
|
|
714
|
+
['trip1', { routeId: 0, tripRouteIndex: 0 }],
|
|
715
|
+
]);
|
|
716
|
+
|
|
717
|
+
const tripContinuations = [
|
|
718
|
+
{
|
|
719
|
+
fromStop: 100,
|
|
720
|
+
fromTrip: 'unknown_trip', // not in tripsMapping
|
|
721
|
+
toStop: 200,
|
|
722
|
+
toTrip: 'trip1',
|
|
723
|
+
},
|
|
724
|
+
];
|
|
725
|
+
|
|
726
|
+
const mockTimetable = {} as unknown as Timetable;
|
|
727
|
+
const activeStopIds = new Set([100, 200]);
|
|
728
|
+
|
|
729
|
+
const result = buildTripContinuations(
|
|
730
|
+
tripsMapping,
|
|
731
|
+
tripContinuations,
|
|
732
|
+
mockTimetable,
|
|
733
|
+
activeStopIds,
|
|
734
|
+
);
|
|
735
|
+
|
|
736
|
+
assert.strictEqual(result.size, 0);
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
it('should ignore trip continuations with unknown routes', () => {
|
|
740
|
+
const tripsMapping: TripsMapping = new Map([
|
|
741
|
+
['trip1', { routeId: 0, tripRouteIndex: 0 }],
|
|
742
|
+
['trip2', { routeId: 1, tripRouteIndex: 0 }],
|
|
743
|
+
]);
|
|
744
|
+
|
|
745
|
+
const tripContinuations = [
|
|
746
|
+
{
|
|
747
|
+
fromStop: 100,
|
|
748
|
+
fromTrip: 'trip1',
|
|
749
|
+
toStop: 200,
|
|
750
|
+
toTrip: 'trip2',
|
|
751
|
+
},
|
|
752
|
+
];
|
|
753
|
+
|
|
754
|
+
const mockTimetable = {
|
|
755
|
+
getRoute: () => undefined, // no routes found
|
|
756
|
+
} as unknown as Timetable;
|
|
757
|
+
|
|
758
|
+
const activeStopIds = new Set([100, 200]);
|
|
759
|
+
|
|
760
|
+
const result = buildTripContinuations(
|
|
761
|
+
tripsMapping,
|
|
762
|
+
tripContinuations,
|
|
763
|
+
mockTimetable,
|
|
764
|
+
activeStopIds,
|
|
765
|
+
);
|
|
766
|
+
|
|
767
|
+
assert.strictEqual(result.size, 0);
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
it('should ignore trip continuations with no valid timing', () => {
|
|
771
|
+
const tripsMapping: TripsMapping = new Map([
|
|
772
|
+
['trip1', { routeId: 0, tripRouteIndex: 0 }],
|
|
773
|
+
['trip2', { routeId: 1, tripRouteIndex: 0 }],
|
|
774
|
+
]);
|
|
775
|
+
|
|
776
|
+
const tripContinuations = [
|
|
777
|
+
{
|
|
778
|
+
fromStop: 100,
|
|
779
|
+
fromTrip: 'trip1',
|
|
780
|
+
toStop: 200,
|
|
781
|
+
toTrip: 'trip2',
|
|
782
|
+
},
|
|
783
|
+
];
|
|
784
|
+
|
|
785
|
+
const mockFromRoute = {
|
|
786
|
+
stopRouteIndices: () => [0],
|
|
787
|
+
arrivalAt: () => Time.fromMinutes(75), // 1:15 - arrives AFTER departure
|
|
788
|
+
} as unknown as Route;
|
|
789
|
+
|
|
790
|
+
const mockToRoute = {
|
|
791
|
+
stopRouteIndices: () => [1],
|
|
792
|
+
departureFrom: () => Time.fromMinutes(60), // 1:00 - departs BEFORE arrival
|
|
793
|
+
} as unknown as Route;
|
|
794
|
+
|
|
795
|
+
const mockTimetable = {
|
|
796
|
+
getRoute: (routeId: number) =>
|
|
797
|
+
routeId === 0 ? mockFromRoute : mockToRoute,
|
|
798
|
+
} as unknown as Timetable;
|
|
799
|
+
|
|
800
|
+
const activeStopIds = new Set([100, 200]);
|
|
801
|
+
|
|
802
|
+
const result = buildTripContinuations(
|
|
803
|
+
tripsMapping,
|
|
804
|
+
tripContinuations,
|
|
805
|
+
mockTimetable,
|
|
806
|
+
activeStopIds,
|
|
807
|
+
);
|
|
808
|
+
|
|
809
|
+
assert.strictEqual(result.size, 0);
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
it('should handle multiple continuations from same trip boarding', () => {
|
|
813
|
+
const tripsMapping: TripsMapping = new Map([
|
|
814
|
+
['trip1', { routeId: 0, tripRouteIndex: 0 }],
|
|
815
|
+
['trip2', { routeId: 1, tripRouteIndex: 0 }],
|
|
816
|
+
['trip3', { routeId: 2, tripRouteIndex: 0 }],
|
|
817
|
+
]);
|
|
818
|
+
|
|
819
|
+
const tripContinuations = [
|
|
820
|
+
{
|
|
821
|
+
fromStop: 100,
|
|
822
|
+
fromTrip: 'trip1',
|
|
823
|
+
toStop: 200,
|
|
824
|
+
toTrip: 'trip2',
|
|
825
|
+
},
|
|
826
|
+
{
|
|
827
|
+
fromStop: 100,
|
|
828
|
+
fromTrip: 'trip1',
|
|
829
|
+
toStop: 300,
|
|
830
|
+
toTrip: 'trip3',
|
|
831
|
+
},
|
|
832
|
+
];
|
|
833
|
+
|
|
834
|
+
const mockFromRoute = {
|
|
835
|
+
stopRouteIndices: () => [0],
|
|
836
|
+
arrivalAt: () => Time.fromMinutes(60),
|
|
837
|
+
} as unknown as Route;
|
|
838
|
+
|
|
839
|
+
const mockToRoute1 = {
|
|
840
|
+
stopRouteIndices: () => [1],
|
|
841
|
+
departureFrom: () => Time.fromMinutes(70),
|
|
842
|
+
} as unknown as Route;
|
|
843
|
+
|
|
844
|
+
const mockToRoute2 = {
|
|
845
|
+
stopRouteIndices: () => [2],
|
|
846
|
+
departureFrom: () => Time.fromMinutes(80),
|
|
847
|
+
} as unknown as Route;
|
|
848
|
+
|
|
849
|
+
const mockTimetable = {
|
|
850
|
+
getRoute: (routeId: number) => {
|
|
851
|
+
if (routeId === 0) return mockFromRoute;
|
|
852
|
+
if (routeId === 1) return mockToRoute1;
|
|
853
|
+
if (routeId === 2) return mockToRoute2;
|
|
854
|
+
return undefined;
|
|
855
|
+
},
|
|
856
|
+
} as unknown as Timetable;
|
|
857
|
+
|
|
858
|
+
const activeStopIds = new Set([100, 200, 300]);
|
|
859
|
+
|
|
860
|
+
const result = buildTripContinuations(
|
|
861
|
+
tripsMapping,
|
|
862
|
+
tripContinuations,
|
|
863
|
+
mockTimetable,
|
|
864
|
+
activeStopIds,
|
|
865
|
+
);
|
|
866
|
+
|
|
867
|
+
const expectedTripBoardingId = encode(0, 0, 0);
|
|
868
|
+
const continuations = result.get(expectedTripBoardingId);
|
|
869
|
+
|
|
870
|
+
assert(continuations);
|
|
871
|
+
assert.strictEqual(continuations.length, 2);
|
|
872
|
+
assert.deepEqual(continuations[0], {
|
|
873
|
+
hopOnStopIndex: 1,
|
|
874
|
+
routeId: 1,
|
|
875
|
+
tripIndex: 0,
|
|
876
|
+
});
|
|
877
|
+
assert.deepEqual(continuations[1], {
|
|
878
|
+
hopOnStopIndex: 2,
|
|
879
|
+
routeId: 2,
|
|
880
|
+
tripIndex: 0,
|
|
881
|
+
});
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
it('should handle empty input gracefully', () => {
|
|
885
|
+
const tripsMapping: TripsMapping = new Map();
|
|
886
|
+
const tripContinuations: GtfsTripContinuation[] = [];
|
|
887
|
+
const mockTimetable = {} as unknown as Timetable;
|
|
888
|
+
const activeStopIds = new Set<number>();
|
|
889
|
+
|
|
890
|
+
const result = buildTripContinuations(
|
|
891
|
+
tripsMapping,
|
|
892
|
+
tripContinuations,
|
|
893
|
+
mockTimetable,
|
|
894
|
+
activeStopIds,
|
|
895
|
+
);
|
|
896
|
+
|
|
897
|
+
assert.strictEqual(result.size, 0);
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
it('should disambiguate transfers when routes visit same stop multiple times', () => {
|
|
901
|
+
const tripsMapping: TripsMapping = new Map([
|
|
902
|
+
['trip1', { routeId: 0, tripRouteIndex: 0 }],
|
|
903
|
+
['trip2', { routeId: 1, tripRouteIndex: 0 }],
|
|
904
|
+
]);
|
|
905
|
+
|
|
906
|
+
const tripContinuations = [
|
|
907
|
+
{
|
|
908
|
+
fromStop: 100, // This stop appears multiple times in the route
|
|
909
|
+
fromTrip: 'trip1',
|
|
910
|
+
toStop: 200, // This stop also appears multiple times
|
|
911
|
+
toTrip: 'trip2',
|
|
912
|
+
},
|
|
913
|
+
];
|
|
914
|
+
|
|
915
|
+
// Mock route that visits stop 100 at indices 0 and 3 (circular route)
|
|
916
|
+
const mockFromRoute = {
|
|
917
|
+
stopRouteIndices: () => [0, 3], // Stop 100 appears twice
|
|
918
|
+
arrivalAt: (stopIndex: number) => {
|
|
919
|
+
// First visit at 1:00, second visit at 2:00
|
|
920
|
+
return stopIndex === 0 ? Time.fromMinutes(60) : Time.fromMinutes(120);
|
|
921
|
+
},
|
|
922
|
+
} as unknown as Route;
|
|
923
|
+
|
|
924
|
+
// Mock route that visits stop 200 at indices 1 and 4
|
|
925
|
+
const mockToRoute = {
|
|
926
|
+
stopRouteIndices: () => [1, 4], // Stop 200 appears twice
|
|
927
|
+
departureFrom: (stopIndex: number) => {
|
|
928
|
+
// First departure at 1:10, second departure at 2:30
|
|
929
|
+
return stopIndex === 1 ? Time.fromMinutes(70) : Time.fromMinutes(150);
|
|
930
|
+
},
|
|
931
|
+
} as unknown as Route;
|
|
932
|
+
|
|
933
|
+
const mockTimetable = {
|
|
934
|
+
getRoute: (routeId: number) =>
|
|
935
|
+
routeId === 0 ? mockFromRoute : mockToRoute,
|
|
936
|
+
} as unknown as Timetable;
|
|
937
|
+
|
|
938
|
+
const activeStopIds = new Set([100, 200]);
|
|
939
|
+
|
|
940
|
+
const result = buildTripContinuations(
|
|
941
|
+
tripsMapping,
|
|
942
|
+
tripContinuations,
|
|
943
|
+
mockTimetable,
|
|
944
|
+
activeStopIds,
|
|
945
|
+
);
|
|
946
|
+
|
|
947
|
+
// Should pick the best timing: arrive at stop 0 (1:00) -> depart from stop 1 (1:10)
|
|
948
|
+
// This is better than arrive at stop 3 (2:00) -> depart from stop 4 (2:30)
|
|
949
|
+
const expectedTripBoardingId = encode(0, 0, 0); // stopIndex=0, routeId=0, tripIndex=0
|
|
950
|
+
const continuations = result.get(expectedTripBoardingId);
|
|
951
|
+
|
|
952
|
+
assert(continuations);
|
|
953
|
+
assert.strictEqual(continuations.length, 1);
|
|
954
|
+
assert.deepEqual(continuations[0], {
|
|
955
|
+
hopOnStopIndex: 1, // Best to-stop index
|
|
956
|
+
routeId: 1,
|
|
957
|
+
tripIndex: 0,
|
|
958
|
+
});
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
it('should handle case where no valid transfer timing exists between duplicate stops', () => {
|
|
962
|
+
const tripsMapping: TripsMapping = new Map([
|
|
963
|
+
['trip1', { routeId: 0, tripRouteIndex: 0 }],
|
|
964
|
+
['trip2', { routeId: 1, tripRouteIndex: 0 }],
|
|
965
|
+
]);
|
|
966
|
+
|
|
967
|
+
const tripContinuations = [
|
|
968
|
+
{
|
|
969
|
+
fromStop: 100,
|
|
970
|
+
fromTrip: 'trip1',
|
|
971
|
+
toStop: 200,
|
|
972
|
+
toTrip: 'trip2',
|
|
973
|
+
},
|
|
974
|
+
];
|
|
975
|
+
|
|
976
|
+
// Mock route where all arrivals are AFTER all departures (impossible transfer)
|
|
977
|
+
const mockFromRoute = {
|
|
978
|
+
stopRouteIndices: () => [0, 3], // Stop 100 appears twice
|
|
979
|
+
arrivalAt: (stopIndex: number) => {
|
|
980
|
+
// Both arrivals are late: 2:00 and 3:00
|
|
981
|
+
return stopIndex === 0 ? Time.fromMinutes(120) : Time.fromMinutes(180);
|
|
982
|
+
},
|
|
983
|
+
} as unknown as Route;
|
|
984
|
+
|
|
985
|
+
const mockToRoute = {
|
|
986
|
+
stopRouteIndices: () => [1, 4], // Stop 200 appears twice
|
|
987
|
+
departureFrom: (stopIndex: number) => {
|
|
988
|
+
// Both departures are early: 1:00 and 1:30
|
|
989
|
+
return stopIndex === 1 ? Time.fromMinutes(60) : Time.fromMinutes(90);
|
|
990
|
+
},
|
|
991
|
+
} as unknown as Route;
|
|
992
|
+
|
|
993
|
+
const mockTimetable = {
|
|
994
|
+
getRoute: (routeId: number) =>
|
|
995
|
+
routeId === 0 ? mockFromRoute : mockToRoute,
|
|
996
|
+
} as unknown as Timetable;
|
|
997
|
+
|
|
998
|
+
const activeStopIds = new Set([100, 200]);
|
|
999
|
+
|
|
1000
|
+
const result = buildTripContinuations(
|
|
1001
|
+
tripsMapping,
|
|
1002
|
+
tripContinuations,
|
|
1003
|
+
mockTimetable,
|
|
1004
|
+
activeStopIds,
|
|
1005
|
+
);
|
|
1006
|
+
|
|
1007
|
+
// Should find no valid continuations since all departures are before arrivals
|
|
1008
|
+
assert.strictEqual(result.size, 0);
|
|
273
1009
|
});
|
|
274
1010
|
});
|