minotor 1.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 (131) hide show
  1. package/.cspell.json +43 -0
  2. package/.czrc +3 -0
  3. package/.editorconfig +10 -0
  4. package/.github/ISSUE_TEMPLATE/bug_report.md +32 -0
  5. package/.github/ISSUE_TEMPLATE/config.yml +5 -0
  6. package/.github/ISSUE_TEMPLATE/feature_request.md +29 -0
  7. package/.github/PULL_REQUEST_TEMPLATE.md +4 -0
  8. package/.github/workflows/minotor.yml +85 -0
  9. package/.prettierrc +7 -0
  10. package/.releaserc.json +27 -0
  11. package/CHANGELOG.md +6 -0
  12. package/LICENSE +21 -0
  13. package/README.md +166 -0
  14. package/dist/bundle.cjs.js +16507 -0
  15. package/dist/bundle.cjs.js.map +1 -0
  16. package/dist/bundle.esm.js +16496 -0
  17. package/dist/bundle.esm.js.map +1 -0
  18. package/dist/bundle.umd.js +2 -0
  19. package/dist/bundle.umd.js.map +1 -0
  20. package/dist/cli/__tests__/minotor.test.d.ts +1 -0
  21. package/dist/cli/minotor.d.ts +5 -0
  22. package/dist/cli/repl.d.ts +1 -0
  23. package/dist/cli/utils.d.ts +3 -0
  24. package/dist/cli.mjs +20504 -0
  25. package/dist/cli.mjs.map +1 -0
  26. package/dist/gtfs/__tests__/parser.test.d.ts +1 -0
  27. package/dist/gtfs/__tests__/routes.test.d.ts +1 -0
  28. package/dist/gtfs/__tests__/services.test.d.ts +1 -0
  29. package/dist/gtfs/__tests__/stops.test.d.ts +1 -0
  30. package/dist/gtfs/__tests__/time.test.d.ts +1 -0
  31. package/dist/gtfs/__tests__/transfers.test.d.ts +1 -0
  32. package/dist/gtfs/__tests__/trips.test.d.ts +1 -0
  33. package/dist/gtfs/__tests__/utils.test.d.ts +1 -0
  34. package/dist/gtfs/parser.d.ts +34 -0
  35. package/dist/gtfs/profiles/__tests__/ch.test.d.ts +1 -0
  36. package/dist/gtfs/profiles/ch.d.ts +2 -0
  37. package/dist/gtfs/profiles/standard.d.ts +2 -0
  38. package/dist/gtfs/routes.d.ts +11 -0
  39. package/dist/gtfs/services.d.ts +19 -0
  40. package/dist/gtfs/stops.d.ts +20 -0
  41. package/dist/gtfs/time.d.ts +17 -0
  42. package/dist/gtfs/transfers.d.ts +22 -0
  43. package/dist/gtfs/trips.d.ts +26 -0
  44. package/dist/gtfs/utils.d.ts +21 -0
  45. package/dist/index.d.ts +11 -0
  46. package/dist/routing/__tests__/router.test.d.ts +1 -0
  47. package/dist/routing/plotter.d.ts +11 -0
  48. package/dist/routing/query.d.ts +35 -0
  49. package/dist/routing/result.d.ts +28 -0
  50. package/dist/routing/route.d.ts +25 -0
  51. package/dist/routing/router.d.ts +33 -0
  52. package/dist/stops/__tests__/io.test.d.ts +1 -0
  53. package/dist/stops/__tests__/stopFinder.test.d.ts +1 -0
  54. package/dist/stops/i18n.d.ts +10 -0
  55. package/dist/stops/io.d.ts +4 -0
  56. package/dist/stops/proto/stops.d.ts +53 -0
  57. package/dist/stops/stops.d.ts +16 -0
  58. package/dist/stops/stopsIndex.d.ts +52 -0
  59. package/dist/timetable/__tests__/io.test.d.ts +1 -0
  60. package/dist/timetable/__tests__/timetable.test.d.ts +1 -0
  61. package/dist/timetable/duration.d.ts +51 -0
  62. package/dist/timetable/io.d.ts +8 -0
  63. package/dist/timetable/proto/timetable.d.ts +122 -0
  64. package/dist/timetable/time.d.ts +98 -0
  65. package/dist/timetable/timetable.d.ts +82 -0
  66. package/dist/umdIndex.d.ts +9 -0
  67. package/eslint.config.mjs +52 -0
  68. package/package.json +109 -0
  69. package/rollup.config.js +44 -0
  70. package/src/cli/__tests__/minotor.test.ts +23 -0
  71. package/src/cli/minotor.ts +112 -0
  72. package/src/cli/repl.ts +200 -0
  73. package/src/cli/utils.ts +36 -0
  74. package/src/gtfs/__tests__/parser.test.ts +591 -0
  75. package/src/gtfs/__tests__/resources/sample-feed/agency.txt +2 -0
  76. package/src/gtfs/__tests__/resources/sample-feed/calendar.txt +3 -0
  77. package/src/gtfs/__tests__/resources/sample-feed/calendar_dates.txt +2 -0
  78. package/src/gtfs/__tests__/resources/sample-feed/fare_attributes.txt +3 -0
  79. package/src/gtfs/__tests__/resources/sample-feed/fare_rules.txt +5 -0
  80. package/src/gtfs/__tests__/resources/sample-feed/frequencies.txt +12 -0
  81. package/src/gtfs/__tests__/resources/sample-feed/routes.txt +6 -0
  82. package/src/gtfs/__tests__/resources/sample-feed/sample-feed.zip +0 -0
  83. package/src/gtfs/__tests__/resources/sample-feed/shapes.txt +1 -0
  84. package/src/gtfs/__tests__/resources/sample-feed/stop_times.txt +34 -0
  85. package/src/gtfs/__tests__/resources/sample-feed/stops.txt +10 -0
  86. package/src/gtfs/__tests__/resources/sample-feed/trips.txt +13 -0
  87. package/src/gtfs/__tests__/resources/sample-feed.zip +0 -0
  88. package/src/gtfs/__tests__/routes.test.ts +63 -0
  89. package/src/gtfs/__tests__/services.test.ts +209 -0
  90. package/src/gtfs/__tests__/stops.test.ts +177 -0
  91. package/src/gtfs/__tests__/time.test.ts +27 -0
  92. package/src/gtfs/__tests__/transfers.test.ts +117 -0
  93. package/src/gtfs/__tests__/trips.test.ts +463 -0
  94. package/src/gtfs/__tests__/utils.test.ts +13 -0
  95. package/src/gtfs/parser.ts +154 -0
  96. package/src/gtfs/profiles/__tests__/ch.test.ts +43 -0
  97. package/src/gtfs/profiles/ch.ts +70 -0
  98. package/src/gtfs/profiles/standard.ts +39 -0
  99. package/src/gtfs/routes.ts +48 -0
  100. package/src/gtfs/services.ts +98 -0
  101. package/src/gtfs/stops.ts +112 -0
  102. package/src/gtfs/time.ts +33 -0
  103. package/src/gtfs/transfers.ts +102 -0
  104. package/src/gtfs/trips.ts +228 -0
  105. package/src/gtfs/utils.ts +42 -0
  106. package/src/index.ts +28 -0
  107. package/src/routing/__tests__/router.test.ts +760 -0
  108. package/src/routing/plotter.ts +70 -0
  109. package/src/routing/query.ts +74 -0
  110. package/src/routing/result.ts +108 -0
  111. package/src/routing/route.ts +94 -0
  112. package/src/routing/router.ts +262 -0
  113. package/src/stops/__tests__/io.test.ts +43 -0
  114. package/src/stops/__tests__/stopFinder.test.ts +185 -0
  115. package/src/stops/i18n.ts +40 -0
  116. package/src/stops/io.ts +94 -0
  117. package/src/stops/proto/stops.proto +26 -0
  118. package/src/stops/proto/stops.ts +445 -0
  119. package/src/stops/stops.ts +24 -0
  120. package/src/stops/stopsIndex.ts +151 -0
  121. package/src/timetable/__tests__/io.test.ts +175 -0
  122. package/src/timetable/__tests__/timetable.test.ts +180 -0
  123. package/src/timetable/duration.ts +85 -0
  124. package/src/timetable/io.ts +265 -0
  125. package/src/timetable/proto/timetable.proto +76 -0
  126. package/src/timetable/proto/timetable.ts +1304 -0
  127. package/src/timetable/time.ts +192 -0
  128. package/src/timetable/timetable.ts +286 -0
  129. package/src/umdIndex.ts +14 -0
  130. package/tsconfig.build.json +4 -0
  131. package/tsconfig.json +21 -0
@@ -0,0 +1,760 @@
1
+ import assert from 'node:assert';
2
+ import { beforeEach, describe, it } from 'node:test';
3
+
4
+ import { StopsMap } from '../../stops/stops.js';
5
+ import { StopsIndex } from '../../stops/stopsIndex.js';
6
+ import { Duration } from '../../timetable/duration.js';
7
+ import { Time } from '../../timetable/time.js';
8
+ import {
9
+ RoutesAdjacency,
10
+ ServiceRoutesMap,
11
+ StopsAdjacency,
12
+ Timetable,
13
+ } from '../../timetable/timetable.js';
14
+ import { Query } from '../query.js';
15
+ import { Result } from '../result.js';
16
+ import { Router } from '../router.js';
17
+
18
+ describe('Router', () => {
19
+ describe('with a single route', () => {
20
+ let router: Router;
21
+ let timetable: Timetable;
22
+
23
+ beforeEach(() => {
24
+ const stopsAdjacency: StopsAdjacency = new Map([
25
+ ['stop1', { transfers: [], routes: ['route1'] }],
26
+ ['stop2', { transfers: [], routes: ['route1'] }],
27
+ ['stop3', { transfers: [], routes: ['route1'] }],
28
+ ]);
29
+
30
+ const routesAdjacency: RoutesAdjacency = new Map([
31
+ [
32
+ 'route1',
33
+ {
34
+ stopTimes: [
35
+ {
36
+ arrival: Time.fromString('08:00:00'),
37
+ departure: Time.fromString('08:10:00'),
38
+ pickUpType: 'REGULAR',
39
+ dropOffType: 'REGULAR',
40
+ },
41
+ {
42
+ arrival: Time.fromString('08:15:00'),
43
+ departure: Time.fromString('08:25:00'),
44
+ pickUpType: 'REGULAR',
45
+ dropOffType: 'REGULAR',
46
+ },
47
+ {
48
+ arrival: Time.fromString('08:35:00'),
49
+ departure: Time.fromString('08:45:00'),
50
+ pickUpType: 'REGULAR',
51
+ dropOffType: 'REGULAR',
52
+ },
53
+ ],
54
+ stops: ['stop1', 'stop2', 'stop3'],
55
+ stopIndices: new Map([
56
+ ['stop1', 0],
57
+ ['stop2', 1],
58
+ ['stop3', 2],
59
+ ]),
60
+ serviceRouteId: 'service_route1',
61
+ },
62
+ ],
63
+ ]);
64
+
65
+ const routes: ServiceRoutesMap = new Map([
66
+ [
67
+ 'service_route1',
68
+ {
69
+ type: 'BUS',
70
+ name: 'Line 1',
71
+ },
72
+ ],
73
+ ]);
74
+
75
+ timetable = new Timetable(stopsAdjacency, routesAdjacency, routes);
76
+ const stopsMap: StopsMap = new Map([
77
+ [
78
+ 'stop1',
79
+ {
80
+ id: 'stop1',
81
+ name: 'Stop 1',
82
+ lat: 1.0,
83
+ lon: 1.0,
84
+ children: [],
85
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
86
+ },
87
+ ],
88
+ [
89
+ 'stop2',
90
+ {
91
+ id: 'stop2',
92
+ name: 'Stop 2',
93
+ lat: 2.0,
94
+ lon: 2.0,
95
+ children: [],
96
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
97
+ },
98
+ ],
99
+ [
100
+ 'stop3',
101
+ {
102
+ id: 'stop3',
103
+ name: 'Stop 3',
104
+ lat: 3.0,
105
+ lon: 3.0,
106
+ children: [],
107
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
108
+ },
109
+ ],
110
+ ]);
111
+ const stopsIndex = new StopsIndex(stopsMap);
112
+ router = new Router(timetable, stopsIndex);
113
+ });
114
+
115
+ it('should find a direct route', () => {
116
+ const query = new Query.Builder()
117
+ .from('stop1')
118
+ .to('stop3')
119
+ .departureTime(Time.fromString('08:00:00'))
120
+ .build();
121
+
122
+ const result: Result = router.route(query);
123
+
124
+ const bestRoute = result.bestRoute();
125
+ assert.strictEqual(bestRoute?.legs.length, 1);
126
+ });
127
+ it('should return an empty result when no route is possible', () => {
128
+ const query = new Query.Builder()
129
+ .from('stop1')
130
+ .to('nonexistentStop')
131
+ .departureTime(Time.fromString('08:00:00'))
132
+ .build();
133
+
134
+ const result: Result = router.route(query);
135
+
136
+ const bestRoute = result.bestRoute();
137
+ assert.strictEqual(bestRoute, undefined);
138
+ });
139
+
140
+ it('should correctly calculate the arrival time to a stop', () => {
141
+ const query = new Query.Builder()
142
+ .from('stop1')
143
+ .to('stop3')
144
+ .departureTime(Time.fromString('08:00:00'))
145
+ .build();
146
+
147
+ const result: Result = router.route(query);
148
+
149
+ const timeToStop3 = result.arrivalAt('stop3');
150
+ assert.strictEqual(
151
+ timeToStop3?.time.toSeconds(),
152
+ Time.fromString('08:35:00').toSeconds(),
153
+ );
154
+ });
155
+ });
156
+ describe('with a route change', () => {
157
+ let router: Router;
158
+ let timetable: Timetable;
159
+
160
+ beforeEach(() => {
161
+ const stopsAdjacency: StopsAdjacency = new Map([
162
+ ['stop1', { transfers: [], routes: ['route1'] }],
163
+ ['stop2', { transfers: [], routes: ['route1', 'route2'] }],
164
+ ['stop3', { transfers: [], routes: ['route1'] }],
165
+ ['stop4', { transfers: [], routes: ['route2'] }],
166
+ ['stop5', { transfers: [], routes: ['route2'] }],
167
+ ]);
168
+
169
+ const routesAdjacency: RoutesAdjacency = new Map([
170
+ [
171
+ 'route1',
172
+ {
173
+ stopTimes: [
174
+ {
175
+ arrival: Time.fromString('08:00:00'),
176
+ departure: Time.fromString('08:15:00'),
177
+ pickUpType: 'REGULAR',
178
+ dropOffType: 'REGULAR',
179
+ },
180
+ {
181
+ arrival: Time.fromString('08:30:00'),
182
+ departure: Time.fromString('08:45:00'),
183
+ pickUpType: 'REGULAR',
184
+ dropOffType: 'REGULAR',
185
+ },
186
+ {
187
+ arrival: Time.fromString('09:00:00'),
188
+ departure: Time.fromString('09:10:00'),
189
+ pickUpType: 'REGULAR',
190
+ dropOffType: 'REGULAR',
191
+ },
192
+ ],
193
+ stops: ['stop1', 'stop2', 'stop3'],
194
+ stopIndices: new Map([
195
+ ['stop1', 0],
196
+ ['stop2', 1],
197
+ ['stop3', 2],
198
+ ]),
199
+ serviceRouteId: 'service_route1',
200
+ },
201
+ ],
202
+ [
203
+ 'route2',
204
+ {
205
+ stopTimes: [
206
+ {
207
+ arrival: Time.fromString('08:05:00'),
208
+ departure: Time.fromString('08:20:00'),
209
+ pickUpType: 'REGULAR',
210
+ dropOffType: 'REGULAR',
211
+ },
212
+ {
213
+ arrival: Time.fromString('09:00:00'),
214
+ departure: Time.fromString('09:15:00'),
215
+ pickUpType: 'REGULAR',
216
+ dropOffType: 'REGULAR',
217
+ },
218
+ {
219
+ arrival: Time.fromString('09:20:00'),
220
+ departure: Time.fromString('09:35:00'),
221
+ pickUpType: 'REGULAR',
222
+ dropOffType: 'REGULAR',
223
+ },
224
+ ],
225
+ stops: ['stop4', 'stop2', 'stop5'],
226
+ stopIndices: new Map([
227
+ ['stop4', 0],
228
+ ['stop2', 1],
229
+ ['stop5', 2],
230
+ ]),
231
+ serviceRouteId: 'service_route2',
232
+ },
233
+ ],
234
+ ]);
235
+
236
+ const routes: ServiceRoutesMap = new Map([
237
+ [
238
+ 'service_route1',
239
+ {
240
+ type: 'BUS',
241
+ name: 'Line 1',
242
+ },
243
+ ],
244
+ [
245
+ 'service_route2',
246
+ {
247
+ type: 'RAIL',
248
+ name: 'Line 2',
249
+ },
250
+ ],
251
+ ]);
252
+
253
+ timetable = new Timetable(stopsAdjacency, routesAdjacency, routes);
254
+ const stopsMap: StopsMap = new Map([
255
+ [
256
+ 'stop1',
257
+ {
258
+ id: 'stop1',
259
+ name: 'Stop 1',
260
+ lat: 1.0,
261
+ lon: 1.0,
262
+ children: [],
263
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
264
+ },
265
+ ],
266
+ [
267
+ 'stop2',
268
+ {
269
+ id: 'stop2',
270
+ name: 'Stop 2',
271
+ lat: 2.0,
272
+ lon: 2.0,
273
+ children: [],
274
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
275
+ },
276
+ ],
277
+ [
278
+ 'stop3',
279
+ {
280
+ id: 'stop3',
281
+ name: 'Stop 3',
282
+ lat: 3.0,
283
+ lon: 3.0,
284
+ children: [],
285
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
286
+ },
287
+ ],
288
+ [
289
+ 'stop4',
290
+ {
291
+ id: 'stop4',
292
+ name: 'Stop 4',
293
+ lat: 4.0,
294
+ lon: 4.0,
295
+ children: [],
296
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
297
+ },
298
+ ],
299
+ [
300
+ 'stop5',
301
+ {
302
+ id: 'stop5',
303
+ name: 'Stop 5',
304
+ lat: 5.0,
305
+ lon: 5.0,
306
+ children: [],
307
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
308
+ },
309
+ ],
310
+ ]);
311
+ const stopsIndex = new StopsIndex(stopsMap);
312
+ router = new Router(timetable, stopsIndex);
313
+ });
314
+
315
+ it('should find a route with a change', () => {
316
+ const query = new Query.Builder()
317
+ .from('stop1')
318
+ .to('stop5')
319
+ .departureTime(Time.fromString('08:00:00'))
320
+ .build();
321
+
322
+ const result: Result = router.route(query);
323
+
324
+ const bestRoute = result.bestRoute();
325
+ assert.strictEqual(bestRoute?.legs.length, 2);
326
+ });
327
+
328
+ it('should correctly calculate the arrival time to a stop', () => {
329
+ const query = new Query.Builder()
330
+ .from('stop1')
331
+ .to('stop5')
332
+ .departureTime(Time.fromString('08:00:00'))
333
+ .build();
334
+
335
+ const result: Result = router.route(query);
336
+
337
+ const timeToStop5 = result.arrivalAt('stop5');
338
+ assert.strictEqual(
339
+ timeToStop5?.time.toSeconds(),
340
+ Time.fromString('09:20:00').toSeconds(),
341
+ );
342
+ });
343
+ });
344
+ describe('with a transfer', () => {
345
+ let router: Router;
346
+ let timetable: Timetable;
347
+
348
+ beforeEach(() => {
349
+ const stopsAdjacency: StopsAdjacency = new Map([
350
+ ['stop1', { transfers: [], routes: ['route1'] }],
351
+ [
352
+ 'stop2',
353
+ {
354
+ transfers: [
355
+ {
356
+ destination: 'stop5',
357
+ type: 'REQUIRES_MINIMAL_TIME',
358
+ minTransferTime: Duration.fromSeconds(300),
359
+ },
360
+ ],
361
+ routes: ['route1'],
362
+ },
363
+ ],
364
+ ['stop3', { transfers: [], routes: ['route1'] }],
365
+ ['stop4', { transfers: [], routes: ['route2'] }],
366
+ ['stop5', { transfers: [], routes: ['route2'] }],
367
+ ['stop6', { transfers: [], routes: ['route2'] }],
368
+ ]);
369
+
370
+ const routesAdjacency: RoutesAdjacency = new Map([
371
+ [
372
+ 'route1',
373
+ {
374
+ stopTimes: [
375
+ {
376
+ arrival: Time.fromString('08:00:00'),
377
+ departure: Time.fromString('08:15:00'),
378
+ pickUpType: 'REGULAR',
379
+ dropOffType: 'REGULAR',
380
+ },
381
+ {
382
+ arrival: Time.fromString('08:25:00'),
383
+ departure: Time.fromString('08:35:00'),
384
+ pickUpType: 'REGULAR',
385
+ dropOffType: 'REGULAR',
386
+ },
387
+ {
388
+ arrival: Time.fromString('08:45:00'),
389
+ departure: Time.fromString('08:55:00'),
390
+ pickUpType: 'REGULAR',
391
+ dropOffType: 'REGULAR',
392
+ },
393
+ ],
394
+ stops: ['stop1', 'stop2', 'stop3'],
395
+ stopIndices: new Map([
396
+ ['stop1', 0],
397
+ ['stop2', 1],
398
+ ['stop3', 2],
399
+ ]),
400
+ serviceRouteId: 'service_route1',
401
+ },
402
+ ],
403
+ [
404
+ 'route2',
405
+ {
406
+ stopTimes: [
407
+ {
408
+ arrival: Time.fromString('08:10:00'),
409
+ departure: Time.fromString('08:20:00'),
410
+ pickUpType: 'REGULAR',
411
+ dropOffType: 'REGULAR',
412
+ },
413
+ {
414
+ arrival: Time.fromString('08:40:00'),
415
+ departure: Time.fromString('08:50:00'),
416
+ pickUpType: 'REGULAR',
417
+ dropOffType: 'REGULAR',
418
+ },
419
+ {
420
+ arrival: Time.fromString('09:00:00'),
421
+ departure: Time.fromString('09:10:00'),
422
+ pickUpType: 'REGULAR',
423
+ dropOffType: 'REGULAR',
424
+ },
425
+ ],
426
+ stops: ['stop4', 'stop5', 'stop6'],
427
+ stopIndices: new Map([
428
+ ['stop4', 0],
429
+ ['stop5', 1],
430
+ ['stop6', 2],
431
+ ]),
432
+ serviceRouteId: 'service_route2',
433
+ },
434
+ ],
435
+ ]);
436
+
437
+ const routes: ServiceRoutesMap = new Map([
438
+ [
439
+ 'service_route1',
440
+ {
441
+ type: 'BUS',
442
+ name: 'Line 1',
443
+ },
444
+ ],
445
+ [
446
+ 'service_route2',
447
+ {
448
+ type: 'RAIL',
449
+ name: 'Line 2',
450
+ },
451
+ ],
452
+ ]);
453
+
454
+ timetable = new Timetable(stopsAdjacency, routesAdjacency, routes);
455
+ const stopsMap: StopsMap = new Map([
456
+ [
457
+ 'stop1',
458
+ {
459
+ id: 'stop1',
460
+ name: 'Stop 1',
461
+ lat: 1.0,
462
+ lon: 1.0,
463
+ children: [],
464
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
465
+ },
466
+ ],
467
+ [
468
+ 'stop2',
469
+ {
470
+ id: 'stop2',
471
+ name: 'Stop 2',
472
+ lat: 2.0,
473
+ lon: 2.0,
474
+ children: [],
475
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
476
+ },
477
+ ],
478
+ [
479
+ 'stop3',
480
+ {
481
+ id: 'stop3',
482
+ name: 'Stop 3',
483
+ lat: 3.0,
484
+ lon: 3.0,
485
+ children: [],
486
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
487
+ },
488
+ ],
489
+ [
490
+ 'stop4',
491
+ {
492
+ id: 'stop4',
493
+ name: 'Stop 4',
494
+ lat: 4.0,
495
+ lon: 4.0,
496
+ children: [],
497
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
498
+ },
499
+ ],
500
+ [
501
+ 'stop5',
502
+ {
503
+ id: 'stop5',
504
+ name: 'Stop 5',
505
+ lat: 5.0,
506
+ lon: 5.0,
507
+ children: [],
508
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
509
+ },
510
+ ],
511
+ [
512
+ 'stop6',
513
+ {
514
+ id: 'stop6',
515
+ name: 'Stop 6',
516
+ lat: 6.0,
517
+ lon: 6.0,
518
+ children: [],
519
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
520
+ },
521
+ ],
522
+ ]);
523
+ const stopsIndex = new StopsIndex(stopsMap);
524
+ router = new Router(timetable, stopsIndex);
525
+ });
526
+
527
+ it('should find a route with a transfer', () => {
528
+ const query = new Query.Builder()
529
+ .from('stop1')
530
+ .to('stop6')
531
+ .departureTime(Time.fromString('08:00:00'))
532
+ .build();
533
+
534
+ const result: Result = router.route(query);
535
+
536
+ const bestRoute = result.bestRoute();
537
+ assert.strictEqual(bestRoute?.legs.length, 3);
538
+ });
539
+
540
+ it('should correctly calculate the time to a stop', () => {
541
+ const query = new Query.Builder()
542
+ .from('stop1')
543
+ .to('stop6')
544
+ .departureTime(Time.fromString('08:00:00'))
545
+ .build();
546
+
547
+ const result: Result = router.route(query);
548
+
549
+ const timeToStop5 = result.arrivalAt('stop5');
550
+ assert.strictEqual(
551
+ timeToStop5?.time.toSeconds(),
552
+ Time.fromString('08:30:00').toSeconds(),
553
+ );
554
+ });
555
+ });
556
+ describe('with a faster change', () => {
557
+ let router: Router;
558
+ let timetable: Timetable;
559
+
560
+ beforeEach(() => {
561
+ const stopsAdjacency: StopsAdjacency = new Map([
562
+ ['stop1', { transfers: [], routes: ['route1'] }],
563
+ ['stop2', { transfers: [], routes: ['route1', 'route2'] }],
564
+ ['stop3', { transfers: [], routes: ['route1'] }],
565
+ ['stop4', { transfers: [], routes: ['route2'] }],
566
+ ['stop5', { transfers: [], routes: ['route2'] }],
567
+ ]);
568
+
569
+ const routesAdjacency: RoutesAdjacency = new Map([
570
+ [
571
+ 'route1',
572
+ {
573
+ stopTimes: [
574
+ {
575
+ arrival: Time.fromString('08:00:00'),
576
+ departure: Time.fromString('08:15:00'),
577
+ pickUpType: 'REGULAR',
578
+ dropOffType: 'REGULAR',
579
+ },
580
+ {
581
+ arrival: Time.fromString('08:30:00'),
582
+ departure: Time.fromString('08:45:00'),
583
+ pickUpType: 'REGULAR',
584
+ dropOffType: 'REGULAR',
585
+ },
586
+ {
587
+ arrival: Time.fromString('09:00:00'),
588
+ departure: Time.fromString('09:15:00'),
589
+ pickUpType: 'REGULAR',
590
+ dropOffType: 'REGULAR',
591
+ },
592
+ ],
593
+ stops: ['stop1', 'stop2', 'stop3'],
594
+ stopIndices: new Map([
595
+ ['stop1', 0],
596
+ ['stop2', 1],
597
+ ['stop3', 2],
598
+ ]),
599
+ serviceRouteId: 'service_route1',
600
+ },
601
+ ],
602
+ [
603
+ 'route2',
604
+ {
605
+ stopTimes: [
606
+ {
607
+ arrival: Time.fromString('08:10:00'),
608
+ departure: Time.fromString('08:25:00'),
609
+ pickUpType: 'REGULAR',
610
+ dropOffType: 'REGULAR',
611
+ },
612
+ {
613
+ arrival: Time.fromString('08:50:00'),
614
+ departure: Time.fromString('09:05:00'),
615
+ pickUpType: 'REGULAR',
616
+ dropOffType: 'REGULAR',
617
+ },
618
+ {
619
+ arrival: Time.fromString('09:10:00'),
620
+ departure: Time.fromString('09:25:00'),
621
+ pickUpType: 'REGULAR',
622
+ dropOffType: 'REGULAR',
623
+ },
624
+ ],
625
+ stops: ['stop4', 'stop2', 'stop5'],
626
+ stopIndices: new Map([
627
+ ['stop4', 0],
628
+ ['stop2', 1],
629
+ ['stop5', 2],
630
+ ]),
631
+ serviceRouteId: 'service_route2',
632
+ },
633
+ ],
634
+ [
635
+ 'route3',
636
+ {
637
+ stopTimes: [
638
+ {
639
+ arrival: Time.fromString('08:00:00'),
640
+ departure: Time.fromString('08:15:00'),
641
+ pickUpType: 'REGULAR',
642
+ dropOffType: 'REGULAR',
643
+ },
644
+ {
645
+ arrival: Time.fromString('09:45:00'),
646
+ departure: Time.fromString('10:00:00'),
647
+ pickUpType: 'REGULAR',
648
+ dropOffType: 'REGULAR',
649
+ },
650
+ ],
651
+ stops: ['stop1', 'stop5'],
652
+ stopIndices: new Map([
653
+ ['stop1', 0],
654
+ ['stop5', 1],
655
+ ]),
656
+ serviceRouteId: 'service_route3',
657
+ },
658
+ ],
659
+ ]);
660
+
661
+ const routes: ServiceRoutesMap = new Map([
662
+ [
663
+ 'service_route1',
664
+ {
665
+ type: 'BUS',
666
+ name: 'Line 1',
667
+ },
668
+ ],
669
+ [
670
+ 'service_route2',
671
+ {
672
+ type: 'RAIL',
673
+ name: 'Line 2',
674
+ },
675
+ ],
676
+ [
677
+ 'service_route3',
678
+ {
679
+ type: 'FERRY',
680
+ name: 'Line 3',
681
+ },
682
+ ],
683
+ ]);
684
+
685
+ timetable = new Timetable(stopsAdjacency, routesAdjacency, routes);
686
+ const stopsMap: StopsMap = new Map([
687
+ [
688
+ 'stop1',
689
+ {
690
+ id: 'stop1',
691
+ name: 'Stop 1',
692
+ lat: 1.0,
693
+ lon: 1.0,
694
+ children: [],
695
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
696
+ },
697
+ ],
698
+ [
699
+ 'stop2',
700
+ {
701
+ id: 'stop2',
702
+ name: 'Stop 2',
703
+ lat: 2.0,
704
+ lon: 2.0,
705
+ children: [],
706
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
707
+ },
708
+ ],
709
+ [
710
+ 'stop3',
711
+ {
712
+ id: 'stop3',
713
+ name: 'Stop 3',
714
+ lat: 3.0,
715
+ lon: 3.0,
716
+ children: [],
717
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
718
+ },
719
+ ],
720
+ [
721
+ 'stop4',
722
+ {
723
+ id: 'stop4',
724
+ name: 'Stop 4',
725
+ lat: 4.0,
726
+ lon: 4.0,
727
+ children: [],
728
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
729
+ },
730
+ ],
731
+ [
732
+ 'stop5',
733
+ {
734
+ id: 'stop5',
735
+ name: 'Stop 5',
736
+ lat: 5.0,
737
+ lon: 5.0,
738
+ children: [],
739
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
740
+ },
741
+ ],
742
+ ]);
743
+ const stopsIndex = new StopsIndex(stopsMap);
744
+ router = new Router(timetable, stopsIndex);
745
+ });
746
+
747
+ it('should find a faster route with a change', () => {
748
+ const query = new Query.Builder()
749
+ .from('stop1')
750
+ .to('stop5')
751
+ .departureTime(Time.fromString('08:00:00'))
752
+ .build();
753
+
754
+ const result: Result = router.route(query);
755
+
756
+ const bestRoute = result.bestRoute();
757
+ assert.strictEqual(bestRoute?.legs.length, 2);
758
+ });
759
+ });
760
+ });