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,22 @@
1
+ [
2
+ {
3
+ "from": "Parent8504100",
4
+ "to": ["Parent8504748"],
5
+ "departureTime": "08:30"
6
+ },
7
+ {
8
+ "from": "Parent8507000",
9
+ "to": ["Parent8509253"],
10
+ "departureTime": "12:30"
11
+ },
12
+ {
13
+ "from": "Parent8501008",
14
+ "to": ["Parent8501008"],
15
+ "departureTime": "08:30"
16
+ },
17
+ {
18
+ "from": "Parent8500010",
19
+ "to": ["Parent8301003"],
20
+ "departureTime": "10:00"
21
+ }
22
+ ]
@@ -0,0 +1,209 @@
1
+ import assert from 'node:assert';
2
+ import { describe, it } from 'node:test';
3
+
4
+ import fs from 'fs';
5
+
6
+ import { Query, Router, StopsIndex, Time, Timetable } from '../router.js';
7
+
8
+ const routes = [
9
+ {
10
+ from: 'Parent8504100', // Fribourg/Freiburg
11
+ to: 'Parent8504748', // Le Moléson
12
+ at: '08:30',
13
+ route: [
14
+ {
15
+ from: '8504100:0:2', // Fribourg/Freiburg, Pl. 2
16
+ to: '8504086:0:2', // Bulle, Pl. 2
17
+ departure: '08:34',
18
+ arrival: '09:11',
19
+ route: {
20
+ type: 'RAIL',
21
+ name: 'RE2',
22
+ },
23
+ },
24
+ {
25
+ from: '8504086:0:2', // Bulle, Pl. 2
26
+ to: '8504086:0:4', // Bulle, Pl. 4
27
+ type: 'REQUIRES_MINIMAL_TIME',
28
+ minTransferTime: '03:00',
29
+ },
30
+ {
31
+ from: '8504086:0:4', // Bulle, Pl. 4
32
+ to: '8504077:0:1', // Gruyères, Pl. 1
33
+ departure: '09:20',
34
+ arrival: '09:28',
35
+ route: {
36
+ type: 'RAIL',
37
+ name: 'S51',
38
+ },
39
+ },
40
+ {
41
+ from: '8504077:0:1', // Gruyères, Pl. 1
42
+ to: '8577737', // Gruyères, gare
43
+ type: 'REQUIRES_MINIMAL_TIME',
44
+ minTransferTime: '02:00',
45
+ },
46
+ {
47
+ from: '8577737', // Gruyères, gare
48
+ to: '8504880', // Moléson-sur-Gruyères
49
+ departure: '09:33',
50
+ arrival: '09:44',
51
+ route: {
52
+ type: 'BUS',
53
+ name: '263',
54
+ },
55
+ },
56
+ {
57
+ from: '8504880', // Moléson-sur-Gruyères
58
+ to: '8530024', // Moléson-sur-Gruyères (funi)
59
+ type: 'REQUIRES_MINIMAL_TIME',
60
+ minTransferTime: '02:00',
61
+ },
62
+ {
63
+ from: '8530024', // Moléson-sur-Gruyères (funi)
64
+ to: '8504749', // Plan-Francey
65
+ departure: '10:00',
66
+ arrival: '10:05',
67
+ route: {
68
+ type: 'FUNICULAR',
69
+ name: 'FUN',
70
+ },
71
+ },
72
+ {
73
+ from: '8504749', // Plan-Francey
74
+ to: '8531209', // Plan-Francey (téléphérique)
75
+ type: 'REQUIRES_MINIMAL_TIME',
76
+ minTransferTime: '02:00',
77
+ },
78
+ {
79
+ from: '8531209', // Plan-Francey (téléphérique)
80
+ to: '8504748', // Le Moléson
81
+ departure: '10:10',
82
+ arrival: '10:15',
83
+ route: {
84
+ type: 'AERIAL_LIFT',
85
+ name: 'PB',
86
+ },
87
+ },
88
+ ],
89
+ },
90
+ {
91
+ from: 'Parent8507000', // Bern
92
+ to: 'Parent8509253', // St. Moritz
93
+ at: '12:30',
94
+ route: [
95
+ {
96
+ from: '8507000:0:8', // Bern, Pl.8
97
+ to: '8503000:0:33', // Zürich HB, Pl. 33
98
+ departure: '12:31',
99
+ arrival: '13:28',
100
+ route: {
101
+ type: 'RAIL',
102
+ name: 'IC1',
103
+ },
104
+ },
105
+ {
106
+ from: '8503000:0:33', // Zürich HB, Pl. 33
107
+ to: '8503000:0:9', // Zürich HB, Pl. 9
108
+ type: 'REQUIRES_MINIMAL_TIME',
109
+ minTransferTime: '07:00',
110
+ },
111
+ {
112
+ from: '8503000:0:9', // Zürich HB, Pl. 9
113
+ to: '8509000:0:9', // Chur, Pl. 9
114
+ departure: '13:38',
115
+ arrival: '14:52',
116
+ route: {
117
+ type: 'RAIL',
118
+ name: 'IC3',
119
+ },
120
+ },
121
+ {
122
+ from: '8509000:0:9', // Chur, Pl. 9
123
+ to: '8509000:0:10', // Chur, Pl. 10
124
+ type: 'REQUIRES_MINIMAL_TIME',
125
+ minTransferTime: '03:00',
126
+ },
127
+ {
128
+ from: '8509000:0:10', // Chur, Pl. 10
129
+ to: '8509253:0:2', // St-Moritz, Pl. 2
130
+ departure: '14:58',
131
+ arrival: '16:56',
132
+ route: {
133
+ type: 'RAIL',
134
+ name: 'IR38',
135
+ },
136
+ },
137
+ ],
138
+ },
139
+ {
140
+ // Cross-border train as two routes (TODO, check if there is an in-seat transfer modeled for it)
141
+ from: 'Parent8500010', // Basel SBB
142
+ to: 'Parent8721202', // Strasbourg
143
+ at: '16:50',
144
+ route: [
145
+ {
146
+ from: '8500010:0:31', // Basel SBB, Pl. 31
147
+ to: '8718213', // Saint-Louis (Haut-Rhin)
148
+ departure: '16:50',
149
+ arrival: '16:58',
150
+ route: {
151
+ type: 'RAIL',
152
+ name: 'TER',
153
+ },
154
+ },
155
+ {
156
+ from: '8718213', // Saint-Louis (Haut-Rhin)
157
+ to: '8721202', // Strasbourg
158
+ departure: '16:59',
159
+ arrival: '18:09',
160
+ route: {
161
+ type: 'RAIL',
162
+ name: 'K200',
163
+ },
164
+ },
165
+ ],
166
+ },
167
+ ];
168
+
169
+ const stopsPath = new URL('./timetable/stops.bin', import.meta.url).pathname;
170
+ const timetablePath = new URL('./timetable/timetable.bin', import.meta.url)
171
+ .pathname;
172
+
173
+ describe('E2E Tests for Transit Router', () => {
174
+ const stopsIndex = StopsIndex.fromData(fs.readFileSync(stopsPath));
175
+ const timetable = Timetable.fromData(fs.readFileSync(timetablePath));
176
+
177
+ const router = new Router(timetable, stopsIndex);
178
+
179
+ routes.forEach(({ from, to, at, route }) => {
180
+ it(`Route from ${from} to ${to} at ${at}`, () => {
181
+ const fromStop = stopsIndex.findStopBySourceStopId(from);
182
+ const toStop = stopsIndex.findStopBySourceStopId(to);
183
+
184
+ assert.ok(fromStop, `Stop not found: ${from}`);
185
+ assert.ok(toStop, `Stop not found: ${to}`);
186
+
187
+ const departureTime = Time.fromString(at);
188
+
189
+ const queryObject = new Query.Builder()
190
+ .from(fromStop.sourceStopId)
191
+ .to(toStop.sourceStopId)
192
+ .departureTime(departureTime)
193
+ .maxTransfers(5)
194
+ .build();
195
+
196
+ const result = router.route(queryObject);
197
+ const bestRoute = result.bestRoute(toStop.sourceStopId);
198
+
199
+ assert.ok(bestRoute, 'No route found');
200
+ const actualRoute = bestRoute.asJson();
201
+
202
+ assert.deepStrictEqual(
203
+ actualRoute,
204
+ route,
205
+ `Route mismatch for query from ${from} to ${to} at ${at}`,
206
+ );
207
+ });
208
+ });
209
+ });
@@ -0,0 +1,3 @@
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:af51977d74fcf12d054680c4de134b1fd411ada37fdc84588c45000797f69b3d
3
+ size 5880221
@@ -0,0 +1,3 @@
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6725771bfd0e287d515b3ddd74202eab8023b39808818e422bd57b564013b37c
3
+ size 29833126
@@ -4,6 +4,12 @@ import log from 'loglevel';
4
4
  import { DateTime } from 'luxon';
5
5
 
6
6
  import { chGtfsProfile, GtfsParser, GtfsProfile } from '../parser.js';
7
+ import { Router, StopsIndex, Timetable } from '../router.js';
8
+ import {
9
+ loadQueriesFromJson,
10
+ prettyPrintPerformanceResults,
11
+ testRouterPerformance,
12
+ } from './perf.js';
7
13
  import { startRepl } from './repl.js';
8
14
 
9
15
  const program = new Command();
@@ -71,7 +77,7 @@ program
71
77
 
72
78
  program
73
79
  .command('parse-stops')
74
- .description('Parse a GTFS feed and output a timetable and stops file.')
80
+ .description('Parse a GTFS feed and output a stops file.')
75
81
  .argument('<gtfsPath>', 'Path to GTFS data')
76
82
  .option('-s, --outputPath <path>', 'Path to output stops file', '/tmp/stops')
77
83
  .option('-p, --profileName <name>', 'Profile name for GTFS config', 'CH')
@@ -109,4 +115,48 @@ program
109
115
  startRepl(options.stopsPath, options.timetablePath);
110
116
  });
111
117
 
118
+ program
119
+ .command('perf')
120
+ .description('Evaluate the performance of the router on a set of routes.')
121
+ .argument('<routesPath>', 'Path to the JSON file containing the routes')
122
+ .option('-s, --stopsPath <path>', 'Path to the stops file', '/tmp/stops')
123
+ .option(
124
+ '-t, --timetablePath <path>',
125
+ 'Path to the timetable file',
126
+ '/tmp/timetable',
127
+ )
128
+ .option(
129
+ '-i, --iterations <number>',
130
+ 'Number of iterations for performance tests',
131
+ '20',
132
+ )
133
+ .action(
134
+ (
135
+ routesPath: string,
136
+ options: {
137
+ stopsPath: string;
138
+ timetablePath: string;
139
+ iterations: string;
140
+ },
141
+ ) => {
142
+ const stopsIndex = StopsIndex.fromData(
143
+ fs.readFileSync(options.stopsPath),
144
+ );
145
+ const timetable = Timetable.fromData(
146
+ fs.readFileSync(options.timetablePath),
147
+ );
148
+ const router = new Router(timetable, stopsIndex);
149
+
150
+ const queries = loadQueriesFromJson(routesPath);
151
+ const performanceResults = testRouterPerformance(
152
+ router,
153
+ stopsIndex,
154
+ queries,
155
+ parseInt(options.iterations, 10),
156
+ );
157
+
158
+ prettyPrintPerformanceResults(performanceResults);
159
+ },
160
+ );
161
+
112
162
  program.parse(process.argv);
@@ -0,0 +1,136 @@
1
+ import fs from 'fs';
2
+ import { performance } from 'perf_hooks';
3
+
4
+ import { Query, Router, StopsIndex, Time } from '../router.js';
5
+
6
+ type PerformanceResult = {
7
+ task: Query;
8
+ meanTimeMs: number;
9
+ meanMemoryMb: number;
10
+ };
11
+
12
+ type SerializedQuery = {
13
+ from: string;
14
+ to: string[];
15
+ departureTime: string;
16
+ maxTransfers?: number;
17
+ };
18
+
19
+ /**
20
+ *
21
+ * @param filePath
22
+ * @returns
23
+ */
24
+ export const loadQueriesFromJson = (filePath: string): Query[] => {
25
+ const fileContent = fs.readFileSync(filePath, 'utf-8');
26
+ const serializedQueries: SerializedQuery[] = JSON.parse(
27
+ fileContent,
28
+ ) as SerializedQuery[];
29
+
30
+ return serializedQueries.map((serializedQuery) => {
31
+ const queryBuilder = new Query.Builder()
32
+ .from(serializedQuery.from)
33
+ .to(new Set(serializedQuery.to))
34
+ .departureTime(Time.fromString(serializedQuery.departureTime));
35
+
36
+ if (serializedQuery.maxTransfers !== undefined) {
37
+ queryBuilder.maxTransfers(serializedQuery.maxTransfers);
38
+ }
39
+
40
+ return queryBuilder.build();
41
+ });
42
+ };
43
+
44
+ /**
45
+ *
46
+ * @param router
47
+ * @param stopsIndex
48
+ * @param tasks
49
+ * @param iterations
50
+ * @returns
51
+ */
52
+ export const testRouterPerformance = (
53
+ router: Router,
54
+ stopsIndex: StopsIndex,
55
+ tasks: Query[],
56
+ iterations: number,
57
+ ): PerformanceResult[] => {
58
+ const results: PerformanceResult[] = [];
59
+
60
+ for (const task of tasks) {
61
+ const fromStop = stopsIndex.findStopBySourceStopId(task.from);
62
+ const toStops = Array.from(task.to).map((stopId) =>
63
+ stopsIndex.findStopBySourceStopId(stopId),
64
+ );
65
+
66
+ if (!fromStop || toStops.some((toStop) => !toStop)) {
67
+ throw new Error(
68
+ `Invalid task: Start or end station not found for task ${JSON.stringify(task)}`,
69
+ );
70
+ }
71
+
72
+ let totalTime = 0;
73
+ let totalMemory = 0;
74
+
75
+ for (let i = 0; i < iterations; i++) {
76
+ if (global.gc) {
77
+ global.gc();
78
+ }
79
+
80
+ const startMemory = process.memoryUsage().heapUsed;
81
+ const startTime = performance.now();
82
+
83
+ router.route(task);
84
+
85
+ const endTime = performance.now();
86
+ const endMemory = process.memoryUsage().heapUsed;
87
+
88
+ totalTime += endTime - startTime;
89
+ if (endMemory >= startMemory) {
90
+ totalMemory += endMemory - startMemory;
91
+ }
92
+ }
93
+
94
+ results.push({
95
+ task,
96
+ meanTimeMs: totalTime / iterations,
97
+ meanMemoryMb: totalMemory / iterations / (1024 * 1024),
98
+ });
99
+ }
100
+
101
+ return results;
102
+ };
103
+
104
+ /**
105
+ *
106
+ * @param results
107
+ * @returns
108
+ */
109
+ export const prettyPrintPerformanceResults = (
110
+ results: PerformanceResult[],
111
+ ): void => {
112
+ if (results.length === 0) {
113
+ console.log('No performance results to display.');
114
+ return;
115
+ }
116
+
117
+ const overallMeanTimeMs =
118
+ results.reduce((sum, result) => sum + result.meanTimeMs, 0) /
119
+ results.length;
120
+ const overallMeanMemoryMb =
121
+ results.reduce((sum, result) => sum + result.meanMemoryMb, 0) /
122
+ results.length;
123
+
124
+ console.log('Overall Performance Results:');
125
+ console.log(` Mean Time (ms): ${overallMeanTimeMs.toFixed(2)}`);
126
+ console.log(` Mean Memory (MB): ${overallMeanMemoryMb.toFixed(2)}`);
127
+ console.log('');
128
+
129
+ console.log('Individual Task Results:');
130
+ results.forEach((result, index) => {
131
+ console.log(`Task ${index + 1}:`);
132
+ console.log(` Mean Time (ms): ${result.meanTimeMs.toFixed(2)}`);
133
+ console.log(` Mean Memory (MB): ${result.meanMemoryMb.toFixed(2)}`);
134
+ console.log('');
135
+ });
136
+ };
package/src/cli/repl.ts CHANGED
@@ -3,7 +3,7 @@ import repl from 'node:repl';
3
3
  import fs from 'fs';
4
4
 
5
5
  import { Query, Router, StopsIndex, Time, Timetable } from '../router.js';
6
- import { plotGraphToDotFile, prettyPrintRoute } from './utils.js';
6
+ import { plotGraphToDotFile } from './utils.js';
7
7
 
8
8
  export const startRepl = (stopsPath: string, timetablePath: string) => {
9
9
  const stopsIndex = StopsIndex.fromData(fs.readFileSync(stopsPath));
@@ -21,9 +21,20 @@ export const startRepl = (stopsPath: string, timetablePath: string) => {
21
21
  help: 'Find stops by name using .find <query>',
22
22
  action(query: string) {
23
23
  this.clearBufferedCommand();
24
- const stops = stopsIndex.findStopsByName(query);
24
+ let stops = [];
25
+ const stopBySourceId = stopsIndex.findStopBySourceStopId(query);
26
+ if (stopBySourceId !== undefined) {
27
+ stops.push(stopBySourceId);
28
+ } else if (!isNaN(Number(query))) {
29
+ const stopById = stopsIndex.findStopById(Number(query));
30
+ if (stopById !== undefined) {
31
+ stops.push(stopById);
32
+ }
33
+ } else {
34
+ stops = stopsIndex.findStopsByName(query);
35
+ }
25
36
  stops.forEach((stop) => {
26
- console.log(`${stop.name} (${stop.id})`);
37
+ console.log(`${stop.name} (${stop.sourceStopId} - ${stop.id})`);
27
38
  });
28
39
  this.displayPrompt();
29
40
  },
@@ -49,12 +60,7 @@ export const startRepl = (stopsPath: string, timetablePath: string) => {
49
60
  const fromIndex = parts.indexOf('from');
50
61
  const toIndex = parts.indexOf('to');
51
62
  const fromId = parts.slice(fromIndex + 1, toIndex).join(' ');
52
- const toId = parts
53
- .slice(
54
- toIndex + 1,
55
- withTransfersIndex === -1 ? parts.indexOf('at') : parts.indexOf('at'),
56
- )
57
- .join(' ');
63
+ const toId = parts.slice(toIndex + 1, parts.indexOf('at')).join(' ');
58
64
 
59
65
  if (!fromId || !toId || !atTime) {
60
66
  console.log(
@@ -66,9 +72,15 @@ export const startRepl = (stopsPath: string, timetablePath: string) => {
66
72
 
67
73
  const fromStop =
68
74
  stopsIndex.findStopBySourceStopId(fromId) ||
75
+ (isNaN(Number(fromId))
76
+ ? undefined
77
+ : stopsIndex.findStopById(Number(fromId))) ||
69
78
  stopsIndex.findStopsByName(fromId)[0];
70
79
  const toStop =
71
80
  stopsIndex.findStopBySourceStopId(toId) ||
81
+ (isNaN(Number(toId))
82
+ ? undefined
83
+ : stopsIndex.findStopById(Number(toId))) ||
72
84
  stopsIndex.findStopsByName(toId)[0];
73
85
 
74
86
  if (!fromStop) {
@@ -88,7 +100,7 @@ export const startRepl = (stopsPath: string, timetablePath: string) => {
88
100
  try {
89
101
  const query = new Query.Builder()
90
102
  .from(fromStop.sourceStopId)
91
- .to([toStop.sourceStopId])
103
+ .to(toStop.sourceStopId)
92
104
  .departureTime(departureTime)
93
105
  .maxTransfers(maxTransfers)
94
106
  .build();
@@ -101,14 +113,15 @@ export const startRepl = (stopsPath: string, timetablePath: string) => {
101
113
  console.log(`Destination not reachable`);
102
114
  } else {
103
115
  console.log(
104
- `Arriving to ${toStop.name} at ${arrivalTime.time.toString()} with ${arrivalTime.legNumber - 1} transfers from ${stopsIndex.findStopById(arrivalTime.origin)?.name}.`,
116
+ `Arriving to ${toStop.name} at ${arrivalTime.arrival.toString()} with ${arrivalTime.legNumber - 1} transfers from ${stopsIndex.findStopById(arrivalTime.origin)?.name}.`,
105
117
  );
106
118
  }
107
119
  const bestRoute = result.bestRoute(toStop.sourceStopId);
108
120
 
109
121
  if (bestRoute) {
110
122
  console.log(`Found route from ${fromStop.name} to ${toStop.name}:`);
111
- prettyPrintRoute(bestRoute);
123
+ console.log(bestRoute.toString());
124
+ console.log(JSON.stringify(bestRoute.asJson(), null, 2));
112
125
  } else {
113
126
  console.log('No route found');
114
127
  }
@@ -184,7 +197,7 @@ export const startRepl = (stopsPath: string, timetablePath: string) => {
184
197
  try {
185
198
  const query = new Query.Builder()
186
199
  .from(fromStop.sourceStopId)
187
- .to([toStop.sourceStopId])
200
+ .to(toStop.sourceStopId)
188
201
  .departureTime(departureTime)
189
202
  .maxTransfers(maxTransfers)
190
203
  .build();
package/src/cli/utils.ts CHANGED
@@ -1,34 +1,12 @@
1
1
  import fs from 'fs';
2
2
 
3
- import { Plotter, Result, Route } from '../router.js';
4
-
5
- export const prettyPrintRoute = (route: Route): void => {
6
- route.legs.forEach((leg, index) => {
7
- const fromStop = `From: ${leg.from.name}${leg.from.platform ? ' (Pl. ' + leg.from.platform + ')' : ''}`;
8
- const toStop = `To: ${leg.to.name}${leg.to.platform ? ' (Pl. ' + leg.to.platform + ')' : ''}`;
9
- let transferDetails = '';
10
- let travelDetails = '';
11
-
12
- if ('minTransferTime' in leg) {
13
- transferDetails = `Minimum Transfer Time: ${leg.minTransferTime?.toString()}`;
14
- }
15
- if ('route' in leg && 'departureTime' in leg && 'arrivalTime' in leg) {
16
- travelDetails = `Route: ${leg.route.type} ${leg.route.name}, Departure: ${leg.departureTime.toString()}, Arrival: ${leg.arrivalTime.toString()}`;
17
- }
18
-
19
- console.log(`Leg ${index + 1}:`);
20
- console.log(` ${fromStop}`);
21
- console.log(` ${toStop}`);
22
- if (transferDetails) {
23
- console.log(` ${transferDetails}`);
24
- }
25
- if (travelDetails) {
26
- console.log(` ${travelDetails}`);
27
- }
28
- console.log('');
29
- });
30
- };
3
+ import { Plotter, Result } from '../router.js';
31
4
 
5
+ /**
6
+ * Plots the graph of the result to a dot file.
7
+ * @param result - The result object to plot.
8
+ * @param filePath - The path where the dot file will be saved.
9
+ */
32
10
  export const plotGraphToDotFile = (result: Result, filePath: string): void => {
33
11
  const plotter = new Plotter(result);
34
12
  const dotContent = plotter.plotDotGraph();
@@ -33,28 +33,25 @@ describe('GTFS parser', () => {
33
33
 
34
34
  const route = timetable.getRoute('AB_x');
35
35
  assert(route);
36
- assert.strictEqual(route.serviceRouteId, 'AB');
36
+ assert.strictEqual(route.serviceRoute(), 'AB');
37
37
  const beattyAirportId =
38
38
  stopsIndex.findStopBySourceStopId('BEATTY_AIRPORT')?.id;
39
39
  const bullfrogId = stopsIndex.findStopBySourceStopId('BULLFROG')?.id;
40
40
  assert(beattyAirportId !== undefined && bullfrogId !== undefined);
41
- assert.deepStrictEqual(Array.from(route.stops), [
42
- beattyAirportId,
43
- bullfrogId,
44
- ]);
45
- assert.strictEqual(route.stopTimes.length, 4);
46
- assert.strictEqual(route.pickUpDropOffTypes.length, 4);
47
- assert.strictEqual(route.stopTimes[0], Time.fromHMS(8, 0, 0).toMinutes());
48
- assert.strictEqual(route.stopTimes[1], Time.fromHMS(8, 0, 0).toMinutes());
49
- assert.strictEqual(route.stopTimes[2], Time.fromHMS(8, 10, 0).toMinutes());
50
- assert.strictEqual(route.stopTimes[3], Time.fromHMS(8, 15, 0).toMinutes());
41
+ assert.strictEqual(route.getNbStops(), 2);
42
+ assert(route.arrivalAt(beattyAirportId, 0).equals(Time.fromHMS(8, 0, 0)));
43
+ assert(
44
+ route.departureFrom(beattyAirportId, 0).equals(Time.fromHMS(8, 0, 0)),
45
+ );
46
+ assert(route.arrivalAt(bullfrogId, 0).equals(Time.fromHMS(8, 10, 0)));
47
+ assert(route.departureFrom(bullfrogId, 0).equals(Time.fromHMS(8, 15, 0)));
51
48
 
52
- const routes = timetable.getRoutesThroughStop(furCreekResId);
49
+ const routes = timetable.routesPassingThrough(furCreekResId);
53
50
  assert.strictEqual(routes.length, 2);
54
- assert.strictEqual(routes[0], 'BFC_1q');
55
- assert.strictEqual(routes[1], 'BFC_2');
51
+ assert.strictEqual(routes[0]?.serviceRoute(), 'BFC');
52
+ assert.strictEqual(routes[1]?.serviceRoute(), 'BFC');
56
53
 
57
- const serviceRoute = timetable.getServiceRoute('AB');
54
+ const serviceRoute = timetable.getServiceRoute(route);
58
55
  assert(serviceRoute);
59
56
  assert.strictEqual(serviceRoute.name, '10');
60
57
  assert.strictEqual(serviceRoute.type, 'BUS');
@@ -5,6 +5,7 @@ import { describe, it } from 'node:test';
5
5
  import { DateTime } from 'luxon';
6
6
 
7
7
  import { parseCalendar, parseCalendarDates, ServiceIds } from '../services.js';
8
+
8
9
  describe('GTFS calendar parser', () => {
9
10
  describe('parsing a well formed stream', () => {
10
11
  it('should find valid services present in the source', async () => {
@@ -242,7 +242,6 @@ describe('GTFS transfers parser', () => {
242
242
  },
243
243
  ],
244
244
  ]);
245
- console.log(JSON.stringify(stopsMap));
246
245
  const transfers = await parseTransfers(mockedStream, stopsMap);
247
246
  assert.deepEqual(
248
247
  transfers,