minotor 3.0.1 → 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 +12 -1
- package/.gitattributes +3 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +3 -0
- package/.github/workflows/minotor.yml +17 -1
- package/CHANGELOG.md +2 -2
- package/README.md +34 -14
- 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 -34
- 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,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
|
+
});
|
package/src/cli/minotor.ts
CHANGED
|
@@ -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
|
|
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);
|
package/src/cli/perf.ts
ADDED
|
@@ -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
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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
|
|
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.
|
|
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.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
assert
|
|
47
|
-
assert
|
|
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.
|
|
49
|
+
const routes = timetable.routesPassingThrough(furCreekResId);
|
|
53
50
|
assert.strictEqual(routes.length, 2);
|
|
54
|
-
assert.strictEqual(routes[0], '
|
|
55
|
-
assert.strictEqual(routes[1], '
|
|
51
|
+
assert.strictEqual(routes[0]?.serviceRoute(), 'BFC');
|
|
52
|
+
assert.strictEqual(routes[1]?.serviceRoute(), 'BFC');
|
|
56
53
|
|
|
57
|
-
const serviceRoute = timetable.getServiceRoute(
|
|
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 () => {
|