minotor 11.1.2 → 11.2.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 (71) hide show
  1. package/.cspell.json +7 -1
  2. package/CHANGELOG.md +3 -3
  3. package/README.md +111 -86
  4. package/dist/cli/perf.d.ts +57 -18
  5. package/dist/cli.mjs +1371 -342
  6. package/dist/cli.mjs.map +1 -1
  7. package/dist/parser.cjs.js +57 -4
  8. package/dist/parser.cjs.js.map +1 -1
  9. package/dist/parser.esm.js +57 -4
  10. package/dist/parser.esm.js.map +1 -1
  11. package/dist/router.cjs.js +1 -1
  12. package/dist/router.cjs.js.map +1 -1
  13. package/dist/router.d.ts +5 -5
  14. package/dist/router.esm.js +1 -1
  15. package/dist/router.esm.js.map +1 -1
  16. package/dist/router.umd.js +1 -1
  17. package/dist/router.umd.js.map +1 -1
  18. package/dist/routing/__tests__/access.test.d.ts +1 -0
  19. package/dist/routing/__tests__/plainRouter.test.d.ts +1 -0
  20. package/dist/routing/__tests__/rangeResult.test.d.ts +1 -0
  21. package/dist/routing/__tests__/rangeRouter.test.d.ts +1 -0
  22. package/dist/routing/__tests__/rangeState.test.d.ts +1 -0
  23. package/dist/routing/__tests__/raptor.test.d.ts +1 -0
  24. package/dist/routing/__tests__/state.test.d.ts +1 -0
  25. package/dist/routing/access.d.ts +55 -0
  26. package/dist/routing/plainRouter.d.ts +21 -0
  27. package/dist/routing/plotter.d.ts +9 -0
  28. package/dist/routing/query.d.ts +132 -13
  29. package/dist/routing/rangeResult.d.ts +155 -0
  30. package/dist/routing/rangeRouter.d.ts +24 -0
  31. package/dist/routing/rangeState.d.ts +83 -0
  32. package/dist/routing/raptor.d.ts +96 -0
  33. package/dist/routing/result.d.ts +27 -7
  34. package/dist/routing/route.d.ts +5 -21
  35. package/dist/routing/router.d.ts +20 -91
  36. package/dist/routing/state.d.ts +92 -17
  37. package/dist/timetable/route.d.ts +8 -0
  38. package/dist/timetable/timetable.d.ts +17 -1
  39. package/package.json +1 -1
  40. package/src/__e2e__/benchmark.json +18 -0
  41. package/src/__e2e__/router.test.ts +461 -127
  42. package/src/cli/minotor.ts +39 -3
  43. package/src/cli/perf.ts +324 -60
  44. package/src/cli/repl.ts +96 -41
  45. package/src/router.ts +11 -3
  46. package/src/routing/__tests__/access.test.ts +294 -0
  47. package/src/routing/__tests__/plainRouter.test.ts +1633 -0
  48. package/src/routing/__tests__/plotter.test.ts +8 -8
  49. package/src/routing/__tests__/rangeResult.test.ts +273 -0
  50. package/src/routing/__tests__/rangeRouter.test.ts +472 -0
  51. package/src/routing/__tests__/rangeState.test.ts +246 -0
  52. package/src/routing/__tests__/raptor.test.ts +366 -0
  53. package/src/routing/__tests__/result.test.ts +27 -27
  54. package/src/routing/__tests__/route.test.ts +28 -0
  55. package/src/routing/__tests__/router.test.ts +75 -1587
  56. package/src/routing/__tests__/state.test.ts +78 -0
  57. package/src/routing/access.ts +144 -0
  58. package/src/routing/plainRouter.ts +60 -0
  59. package/src/routing/plotter.ts +53 -6
  60. package/src/routing/query.ts +116 -13
  61. package/src/routing/rangeResult.ts +292 -0
  62. package/src/routing/rangeRouter.ts +167 -0
  63. package/src/routing/rangeState.ts +150 -0
  64. package/src/routing/raptor.ts +416 -0
  65. package/src/routing/result.ts +68 -26
  66. package/src/routing/route.ts +15 -53
  67. package/src/routing/router.ts +40 -480
  68. package/src/routing/state.ts +191 -32
  69. package/src/timetable/__tests__/timetable.test.ts +373 -0
  70. package/src/timetable/route.ts +16 -4
  71. package/src/timetable/timetable.ts +54 -1
@@ -75,7 +75,7 @@ describe('Plotter', () => {
75
75
  describe('plotDotGraph', () => {
76
76
  it('should generate valid DOT graph structure', () => {
77
77
  const result = new Result(
78
- mockQuery,
78
+ mockQuery.to,
79
79
  RoutingState.fromTestData({ nbStops: NB_STOPS }),
80
80
  mockStopsIndex,
81
81
  mockTimetable,
@@ -92,12 +92,12 @@ describe('Plotter', () => {
92
92
 
93
93
  it('should include station nodes', () => {
94
94
  const result = new Result(
95
- mockQuery,
95
+ mockQuery.to,
96
96
  RoutingState.fromTestData({
97
97
  nbStops: NB_STOPS,
98
98
  origins: [0],
99
99
  destinations: [0],
100
- graph: [[[0, { arrival: timeFromHMS(8, 0, 0) }]]],
100
+ graph: [[[0, { stopId: 0, arrival: timeFromHMS(8, 0, 0) }]]],
101
101
  }),
102
102
  mockStopsIndex,
103
103
  mockTimetable,
@@ -113,7 +113,7 @@ describe('Plotter', () => {
113
113
 
114
114
  it('should handle empty graph gracefully', () => {
115
115
  const result = new Result(
116
- mockQuery,
116
+ mockQuery.to,
117
117
  RoutingState.fromTestData({ nbStops: NB_STOPS }),
118
118
  mockStopsIndex,
119
119
  mockTimetable,
@@ -141,12 +141,12 @@ describe('Plotter', () => {
141
141
  const specialStopsIndex = new StopsIndex([stop1, stop2, specialStop]);
142
142
 
143
143
  const result = new Result(
144
- mockQuery,
144
+ mockQuery.to,
145
145
  RoutingState.fromTestData({
146
146
  nbStops: NB_STOPS,
147
147
  origins: [2],
148
148
  destinations: [2],
149
- graph: [[[2, { arrival: timeFromHMS(8, 0, 0) }]]],
149
+ graph: [[[2, { stopId: 2, arrival: timeFromHMS(8, 0, 0) }]]],
150
150
  }),
151
151
  specialStopsIndex,
152
152
  mockTimetable,
@@ -166,13 +166,13 @@ describe('Plotter', () => {
166
166
 
167
167
  it('should use correct colors', () => {
168
168
  const result = new Result(
169
- mockQuery,
169
+ mockQuery.to,
170
170
  RoutingState.fromTestData({
171
171
  nbStops: NB_STOPS,
172
172
  origins: [0],
173
173
  destinations: [1],
174
174
  graph: [
175
- [[0, { arrival: timeFromHMS(8, 0, 0) }]], // round 0 – origins
175
+ [[0, { stopId: 0, arrival: timeFromHMS(8, 0, 0) }]], // round 0 – origins
176
176
  [
177
177
  [
178
178
  1,
@@ -0,0 +1,273 @@
1
+ import assert from 'node:assert';
2
+ import { describe, it } from 'node:test';
3
+
4
+ import { Timetable } from '../../router.js';
5
+ import { Stop } from '../../stops/stops.js';
6
+ import { StopsIndex } from '../../stops/stopsIndex.js';
7
+ import { Route } from '../../timetable/route.js';
8
+ import { timeFromHM } from '../../timetable/time.js';
9
+ import { ServiceRoute, StopAdjacency } from '../../timetable/timetable.js';
10
+ import { ParetoRun, RangeResult } from '../rangeResult.js';
11
+ import { Result } from '../result.js';
12
+ import { RoutingState, VehicleEdge } from '../router.js';
13
+
14
+ // Two-stop timetable with two trips on a single route:
15
+ // trip 0: stop 0 departs 09:00, stop 1 arrives 09:30
16
+ // trip 1: stop 0 departs 08:30, stop 1 arrives 09:10
17
+ const stops: Stop[] = [
18
+ {
19
+ id: 0,
20
+ sourceStopId: 'A',
21
+ name: 'Stop A',
22
+ lat: 0,
23
+ lon: 0,
24
+ children: [],
25
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
26
+ },
27
+ {
28
+ id: 1,
29
+ sourceStopId: 'B',
30
+ name: 'Stop B',
31
+ lat: 0,
32
+ lon: 0,
33
+ children: [],
34
+ locationType: 'SIMPLE_STOP_OR_PLATFORM',
35
+ },
36
+ ];
37
+ const stopsIndex = new StopsIndex(stops);
38
+
39
+ const stopsAdjacency: StopAdjacency[] = [{ routes: [0] }, { routes: [0] }];
40
+ const routesAdjacency = [
41
+ Route.of({
42
+ id: 0,
43
+ serviceRouteId: 0,
44
+ trips: [
45
+ {
46
+ stops: [
47
+ {
48
+ id: 0,
49
+ arrivalTime: timeFromHM(9, 0),
50
+ departureTime: timeFromHM(9, 0),
51
+ },
52
+ {
53
+ id: 1,
54
+ arrivalTime: timeFromHM(9, 30),
55
+ departureTime: timeFromHM(9, 30),
56
+ },
57
+ ],
58
+ },
59
+ {
60
+ stops: [
61
+ {
62
+ id: 0,
63
+ arrivalTime: timeFromHM(8, 30),
64
+ departureTime: timeFromHM(8, 30),
65
+ },
66
+ {
67
+ id: 1,
68
+ arrivalTime: timeFromHM(9, 10),
69
+ departureTime: timeFromHM(9, 10),
70
+ },
71
+ ],
72
+ },
73
+ ],
74
+ }),
75
+ ];
76
+ const serviceRoutes: ServiceRoute[] = [
77
+ { type: 'BUS', name: 'Line 1', routes: [0] },
78
+ ];
79
+ const timetable = new Timetable(stopsAdjacency, routesAdjacency, serviceRoutes);
80
+
81
+ const DEST = 1;
82
+ const DESTINATIONS = new Set([DEST]);
83
+
84
+ // Run A — later departure (09:00→09:30, 30-minute duration)
85
+ const edgeA: VehicleEdge = {
86
+ arrival: timeFromHM(9, 30),
87
+ stopIndex: 0,
88
+ hopOffStopIndex: 1,
89
+ routeId: 0,
90
+ tripIndex: 0,
91
+ };
92
+ const runA: ParetoRun = {
93
+ departureTime: timeFromHM(9, 0),
94
+ result: new Result(
95
+ DESTINATIONS,
96
+ RoutingState.fromTestData({
97
+ nbStops: 2,
98
+ origins: [0],
99
+ destinations: [DEST],
100
+ arrivals: [
101
+ [0, timeFromHM(9, 0), 0],
102
+ [DEST, timeFromHM(9, 30), 1],
103
+ ],
104
+ graph: [[[0, { stopId: 0, arrival: timeFromHM(9, 0) }]], [[DEST, edgeA]]],
105
+ }),
106
+ stopsIndex,
107
+ timetable,
108
+ ),
109
+ };
110
+
111
+ // Run B — earlier departure (08:30→09:10, 40-minute duration)
112
+ const edgeB: VehicleEdge = {
113
+ arrival: timeFromHM(9, 10),
114
+ stopIndex: 0,
115
+ hopOffStopIndex: 1,
116
+ routeId: 0,
117
+ tripIndex: 1,
118
+ };
119
+ const runB: ParetoRun = {
120
+ departureTime: timeFromHM(8, 30),
121
+ result: new Result(
122
+ DESTINATIONS,
123
+ RoutingState.fromTestData({
124
+ nbStops: 2,
125
+ origins: [0],
126
+ destinations: [DEST],
127
+ arrivals: [
128
+ [0, timeFromHM(8, 30), 0],
129
+ [DEST, timeFromHM(9, 10), 1],
130
+ ],
131
+ graph: [
132
+ [[0, { stopId: 0, arrival: timeFromHM(8, 30) }]],
133
+ [[DEST, edgeB]],
134
+ ],
135
+ }),
136
+ stopsIndex,
137
+ timetable,
138
+ ),
139
+ };
140
+
141
+ // Runs are stored latest-departure-first: [runA, runB]
142
+ const rangeResult = new RangeResult([runA, runB], DESTINATIONS);
143
+
144
+ describe('RangeResult', () => {
145
+ describe('size and destinations', () => {
146
+ it('size reflects the number of Pareto runs', () => {
147
+ assert.strictEqual(rangeResult.size, 2);
148
+ });
149
+
150
+ it('destinations returns the resolved destination stop set', () => {
151
+ assert.deepStrictEqual(rangeResult.destinations, DESTINATIONS);
152
+ });
153
+ });
154
+
155
+ describe('[Symbol.iterator]', () => {
156
+ it('iterates runs in latest-departure-first order', () => {
157
+ const runs = [...rangeResult];
158
+ assert.strictEqual(runs.length, 2);
159
+ assert.strictEqual(runs[0]?.departureTime, timeFromHM(9, 0));
160
+ assert.strictEqual(runs[1]?.departureTime, timeFromHM(8, 30));
161
+ });
162
+ });
163
+
164
+ describe('bestRoute', () => {
165
+ it('returns the route that arrives earliest at the destination', () => {
166
+ const route = rangeResult.bestRoute();
167
+ assert(route);
168
+ assert.strictEqual(route.arrivalTime(), timeFromHM(9, 10));
169
+ assert.strictEqual(route.departureTime(), timeFromHM(8, 30));
170
+ });
171
+
172
+ it('accepts a specific stop ID', () => {
173
+ const route = rangeResult.bestRoute(DEST);
174
+ assert(route);
175
+ assert.strictEqual(route.arrivalTime(), timeFromHM(9, 10));
176
+ });
177
+
178
+ it('returns undefined for an unreachable stop', () => {
179
+ assert.strictEqual(rangeResult.bestRoute(99), undefined);
180
+ });
181
+ });
182
+
183
+ describe('latestDepartureRoute', () => {
184
+ it('returns the route with the latest possible departure', () => {
185
+ const route = rangeResult.latestDepartureRoute();
186
+ assert(route);
187
+ assert.strictEqual(route.departureTime(), timeFromHM(9, 0));
188
+ });
189
+
190
+ it('returns undefined for an unreachable stop', () => {
191
+ assert.strictEqual(rangeResult.latestDepartureRoute(99), undefined);
192
+ });
193
+ });
194
+
195
+ describe('fastestRoute', () => {
196
+ it('returns the route with the shortest travel duration', () => {
197
+ // runA: 30 min, runB: 40 min
198
+ const route = rangeResult.fastestRoute();
199
+ assert(route);
200
+ assert.strictEqual(route.departureTime(), timeFromHM(9, 0));
201
+ assert.strictEqual(route.totalDuration(), 30);
202
+ });
203
+
204
+ it('returns undefined for an unreachable stop', () => {
205
+ assert.strictEqual(rangeResult.fastestRoute(99), undefined);
206
+ });
207
+ });
208
+
209
+ describe('getRoutes', () => {
210
+ it('returns all routes ordered earliest-departure-first', () => {
211
+ const routes = rangeResult.getRoutes();
212
+ assert.strictEqual(routes.length, 2);
213
+ assert.strictEqual(routes[0]?.departureTime(), timeFromHM(8, 30));
214
+ assert.strictEqual(routes[1]?.departureTime(), timeFromHM(9, 0));
215
+ });
216
+ });
217
+
218
+ describe('earliestArrivalAt', () => {
219
+ it('returns the earliest arrival across all Pareto runs', () => {
220
+ const arrival = rangeResult.earliestArrivalAt(DEST);
221
+ assert(arrival);
222
+ assert.strictEqual(arrival.arrival, timeFromHM(9, 10));
223
+ });
224
+
225
+ it('returns undefined for a stop that was never reached', () => {
226
+ assert.strictEqual(rangeResult.earliestArrivalAt(99), undefined);
227
+ });
228
+ });
229
+
230
+ describe('shortestDurationTo', () => {
231
+ it('returns the run with the minimum travel duration', () => {
232
+ const result = rangeResult.shortestDurationTo(DEST);
233
+ assert(result);
234
+ assert.strictEqual(result.duration, 30);
235
+ assert.strictEqual(result.arrival, timeFromHM(9, 30));
236
+ });
237
+
238
+ it('returns undefined for a stop that was never reached', () => {
239
+ assert.strictEqual(rangeResult.shortestDurationTo(99), undefined);
240
+ });
241
+ });
242
+
243
+ describe('allShortestDurations', () => {
244
+ it('maps every reached stop to the shortest duration across all runs', () => {
245
+ const durations = rangeResult.allShortestDurations();
246
+ assert.strictEqual(durations.get(0)?.duration, 0);
247
+ assert.strictEqual(durations.get(DEST)?.duration, 30);
248
+ });
249
+ });
250
+
251
+ describe('allEarliestArrivals', () => {
252
+ it('maps every reached stop to the earliest arrival across all runs', () => {
253
+ const arrivals = rangeResult.allEarliestArrivals();
254
+ assert.strictEqual(arrivals.get(0)?.arrival, timeFromHM(8, 30));
255
+ assert.strictEqual(arrivals.get(DEST)?.arrival, timeFromHM(9, 10));
256
+ });
257
+ });
258
+
259
+ describe('empty result', () => {
260
+ it('handles an empty Pareto set gracefully', () => {
261
+ const empty = new RangeResult([], DESTINATIONS);
262
+ assert.strictEqual(empty.size, 0);
263
+ assert.strictEqual(empty.bestRoute(), undefined);
264
+ assert.strictEqual(empty.latestDepartureRoute(), undefined);
265
+ assert.strictEqual(empty.fastestRoute(), undefined);
266
+ assert.deepStrictEqual(empty.getRoutes(), []);
267
+ assert.strictEqual(empty.earliestArrivalAt(DEST), undefined);
268
+ assert.strictEqual(empty.shortestDurationTo(DEST), undefined);
269
+ assert.deepStrictEqual(empty.allShortestDurations(), new Map());
270
+ assert.deepStrictEqual(empty.allEarliestArrivals(), new Map());
271
+ });
272
+ });
273
+ });