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
@@ -1,9 +1,10 @@
1
1
  /* eslint-disable @typescript-eslint/no-non-null-assertion */
2
- import { StopId } from '../stops/stops.js';
2
+ import { Stop, StopId } from '../stops/stops.js';
3
3
  import { StopsIndex } from '../stops/stopsIndex.js';
4
4
  import { Duration } from '../timetable/duration.js';
5
+ import { TripIndex } from '../timetable/route.js';
5
6
  import { Time } from '../timetable/time.js';
6
- import { NOT_AVAILABLE, Timetable } from '../timetable/timetable.js';
7
+ import { Timetable } from '../timetable/timetable.js';
7
8
  import { Query } from './query.js';
8
9
  import { Result } from './result.js';
9
10
  import { Leg } from './route.js';
@@ -15,17 +16,23 @@ export type TripLeg = ReachingTime & {
15
16
  };
16
17
 
17
18
  export type ReachingTime = {
18
- time: Time;
19
+ arrival: Time;
19
20
  legNumber: number;
20
21
  origin: StopId;
21
22
  };
22
23
 
23
24
  type CurrentTrip = {
24
- trip: number;
25
+ tripIndex: TripIndex;
25
26
  origin: StopId;
26
27
  bestHopOnStop: StopId;
27
28
  };
28
29
 
30
+ /**
31
+ * A public transportation network router utilizing the RAPTOR algorithm for
32
+ * efficient journey planning and routing. For more information on the RAPTOR
33
+ * algorithm, refer to its detailed explanation in the research paper:
34
+ * https://www.microsoft.com/en-us/research/wp-content/uploads/2012/01/raptor_alenex.pdf
35
+ */
29
36
  export class Router {
30
37
  private readonly timetable: Timetable;
31
38
  private readonly stopsIndex: StopsIndex;
@@ -50,6 +57,14 @@ export class Router {
50
57
  const { options } = query;
51
58
  const newlyMarkedStops: Set<StopId> = new Set();
52
59
  for (const stop of markedStops) {
60
+ const currentArrival = arrivalsAtCurrentRound.get(stop);
61
+ if (!currentArrival) continue;
62
+ // Skip transfers if the last leg was also a transfer
63
+ const previousLeg = currentArrival.leg;
64
+ if (previousLeg && !('route' in previousLeg)) {
65
+ continue;
66
+ }
67
+
53
68
  for (const transfer of this.timetable.getTransfers(stop)) {
54
69
  let transferTime: Duration;
55
70
  if (transfer.minTransferTime) {
@@ -59,15 +74,14 @@ export class Router {
59
74
  } else {
60
75
  transferTime = options.minTransferTime;
61
76
  }
62
- const arrivalAfterTransfer = arrivalsAtCurrentRound
63
- .get(stop)!
64
- .time.plus(transferTime);
77
+ const arrivalAfterTransfer = currentArrival.arrival.plus(transferTime);
65
78
  const originalArrival =
66
- arrivalsAtCurrentRound.get(transfer.destination)?.time ?? UNREACHED;
67
- if (arrivalAfterTransfer.toMinutes() < originalArrival.toMinutes()) {
68
- const origin = arrivalsAtCurrentRound.get(stop)?.origin ?? stop;
79
+ arrivalsAtCurrentRound.get(transfer.destination)?.arrival ??
80
+ UNREACHED;
81
+ if (arrivalAfterTransfer.isBefore(originalArrival)) {
82
+ const origin = currentArrival.origin;
69
83
  arrivalsAtCurrentRound.set(transfer.destination, {
70
- time: arrivalAfterTransfer,
84
+ arrival: arrivalAfterTransfer,
71
85
  legNumber: round,
72
86
  origin: origin,
73
87
  leg: {
@@ -78,7 +92,7 @@ export class Router {
78
92
  },
79
93
  });
80
94
  earliestArrivals.set(transfer.destination, {
81
- time: arrivalAfterTransfer,
95
+ arrival: arrivalAfterTransfer,
82
96
  legNumber: round,
83
97
  origin: origin,
84
98
  });
@@ -91,6 +105,29 @@ export class Router {
91
105
  }
92
106
  }
93
107
 
108
+ /**
109
+ * Finds the earliest arrival time at any stop from a given set of destinations.
110
+ *
111
+ * @param earliestArrivals A map of stops to their earliest reaching times.
112
+ * @param destinations An array of destination stops to evaluate.
113
+ * @returns The earliest arrival time among the provided destinations.
114
+ */
115
+ private earliestArrivalAtAnyStop(
116
+ earliestArrivals: Map<StopId, ReachingTime>,
117
+ destinations: Stop[],
118
+ ): Time {
119
+ let earliestArrivalAtAnyDestination = UNREACHED;
120
+ for (const destination of destinations) {
121
+ const arrival =
122
+ earliestArrivals.get(destination.id)?.arrival ?? UNREACHED;
123
+ earliestArrivalAtAnyDestination = Time.min(
124
+ earliestArrivalAtAnyDestination,
125
+ arrival,
126
+ );
127
+ }
128
+ return earliestArrivalAtAnyDestination;
129
+ }
130
+
94
131
  /**
95
132
  * The main Raptor algorithm implementation.
96
133
  *
@@ -102,7 +139,7 @@ export class Router {
102
139
  // Consider children or siblings of the "from" stop as potential origins
103
140
  const origins = this.stopsIndex.equivalentStops(from);
104
141
  // Consider children or siblings of the "to" stop(s) as potential destinations
105
- const destinations = to.flatMap((destination) =>
142
+ const destinations = Array.from(to).flatMap((destination) =>
106
143
  this.stopsIndex.equivalentStops(destination),
107
144
  );
108
145
  const earliestArrivals = new Map<StopId, ReachingTime>();
@@ -115,12 +152,12 @@ export class Router {
115
152
  for (const originStop of origins) {
116
153
  markedStops.add(originStop.id);
117
154
  earliestArrivals.set(originStop.id, {
118
- time: departureTime,
155
+ arrival: departureTime,
119
156
  legNumber: 0,
120
157
  origin: originStop.id,
121
158
  });
122
159
  earliestArrivalsWithoutAnyLeg.set(originStop.id, {
123
- time: departureTime,
160
+ arrival: departureTime,
124
161
  legNumber: 0,
125
162
  origin: originStop.id,
126
163
  });
@@ -146,57 +183,33 @@ export class Router {
146
183
  );
147
184
  markedStops.clear();
148
185
  // for each route that can be reached with at least round - 1 trips
149
- for (const [routeId, hopOnStop] of reachableRoutes.entries()) {
150
- const route = this.timetable.getRoute(routeId)!;
186
+ for (const [route, hopOnStop] of reachableRoutes.entries()) {
151
187
  let currentTrip: CurrentTrip | undefined = undefined;
152
- const hopOnIndex = route.stopIndices.get(hopOnStop)!;
153
- // for each stop in the route starting with the hop-on one
154
- for (let i = hopOnIndex; i < route.stops.length; i++) {
155
- const currentStop = route.stops[i]!;
156
- const stopNumbers = route.stops.length;
188
+ for (const currentStop of route.stopsIterator(hopOnStop)) {
157
189
  if (currentTrip !== undefined) {
158
- const currentArrivalIndex =
159
- (currentTrip.trip * stopNumbers + i) * 2;
160
- const currentArrivalTime = Time.fromMinutes(
161
- route.stopTimes[currentArrivalIndex]!,
190
+ const currentArrivalTime = route.arrivalAt(
191
+ currentStop,
192
+ currentTrip.tripIndex,
193
+ );
194
+ const currentDropOffType = route.dropOffTypeAt(
195
+ currentStop,
196
+ currentTrip.tripIndex,
162
197
  );
163
- const currentDropOffType = route.pickUpDropOffTypes[i * 2 + 1];
164
198
  const earliestArrivalAtCurrentStop =
165
- earliestArrivals.get(currentStop)?.time ?? UNREACHED;
166
- let arrivalToImprove = earliestArrivalAtCurrentStop;
167
- if (destinations.length > 0) {
168
- const earliestArrivalsAtDestinations: Time[] = [];
169
- // if multiple destinations are specified, the target pruning
170
- // should compare to the earliest arrival at any of them
171
- for (const destinationStop of destinations) {
172
- const earliestArrivalAtDestination =
173
- earliestArrivals.get(destinationStop.id)?.time ?? UNREACHED;
174
- earliestArrivalsAtDestinations.push(
175
- earliestArrivalAtDestination,
176
- );
177
- }
178
- const earliestArrivalAtDestination = Time.min(
179
- ...earliestArrivalsAtDestinations,
180
- );
181
- arrivalToImprove = Time.min(
182
- earliestArrivalAtCurrentStop,
183
- earliestArrivalAtDestination,
184
- );
185
- }
199
+ earliestArrivals.get(currentStop)?.arrival ?? UNREACHED;
186
200
  if (
187
- currentDropOffType !== NOT_AVAILABLE &&
188
- currentArrivalTime.toMinutes() < arrivalToImprove.toMinutes()
201
+ currentDropOffType !== 'NOT_AVAILABLE' &&
202
+ currentArrivalTime.isBefore(earliestArrivalAtCurrentStop) &&
203
+ currentArrivalTime.isBefore(
204
+ this.earliestArrivalAtAnyStop(earliestArrivals, destinations),
205
+ )
189
206
  ) {
190
- const bestHopOnStopIndex = route.stopIndices.get(
207
+ const bestHopOnDepartureTime = route.departureFrom(
191
208
  currentTrip.bestHopOnStop,
192
- )!;
193
- const bestHopOnStopDepartureIndex =
194
- currentTrip.trip * stopNumbers * 2 + bestHopOnStopIndex * 2 + 1;
195
- const bestHopOnDepartureTime = Time.fromMinutes(
196
- route.stopTimes[bestHopOnStopDepartureIndex]!,
209
+ currentTrip.tripIndex,
197
210
  );
198
211
  arrivalsAtCurrentRound.set(currentStop, {
199
- time: currentArrivalTime,
212
+ arrival: currentArrivalTime,
200
213
  legNumber: round,
201
214
  origin: currentTrip.origin,
202
215
  leg: {
@@ -206,11 +219,11 @@ export class Router {
206
219
  to: this.stopsIndex.findStopById(currentStop)!,
207
220
  departureTime: bestHopOnDepartureTime,
208
221
  arrivalTime: currentArrivalTime,
209
- route: this.timetable.getServiceRoute(route.serviceRouteId)!,
222
+ route: this.timetable.getServiceRoute(route),
210
223
  },
211
224
  });
212
225
  earliestArrivals.set(currentStop, {
213
- time: currentArrivalTime,
226
+ arrival: currentArrivalTime,
214
227
  legNumber: round,
215
228
  origin: currentTrip.origin,
216
229
  });
@@ -220,22 +233,25 @@ export class Router {
220
233
  // check if we can catch a previous trip at the current stop
221
234
  // if there was no current trip, find the first one reachable
222
235
  const earliestArrivalOnPreviousRound =
223
- arrivalsAtPreviousRound.get(currentStop)?.time;
236
+ arrivalsAtPreviousRound.get(currentStop)?.arrival;
224
237
  if (
225
238
  earliestArrivalOnPreviousRound !== undefined &&
226
239
  (currentTrip === undefined ||
227
- earliestArrivalOnPreviousRound.toMinutes() <=
228
- route.stopTimes[(currentTrip.trip * stopNumbers + i) * 2]!)
240
+ earliestArrivalOnPreviousRound.isBefore(
241
+ route.arrivalAt(currentStop, currentTrip.tripIndex),
242
+ ) ||
243
+ earliestArrivalOnPreviousRound.equals(
244
+ route.arrivalAt(currentStop, currentTrip.tripIndex),
245
+ ))
229
246
  ) {
230
- const earliestTrip = this.timetable.findEarliestTrip(
231
- route,
247
+ const earliestTrip = route.findEarliestTrip(
232
248
  currentStop,
233
- currentTrip?.trip,
234
249
  earliestArrivalOnPreviousRound,
250
+ currentTrip?.tripIndex,
235
251
  );
236
252
  if (earliestTrip !== undefined) {
237
253
  currentTrip = {
238
- trip: earliestTrip,
254
+ tripIndex: earliestTrip,
239
255
  // we need to keep track of the best hop-on stop to reconstruct the route at the end
240
256
  bestHopOnStop: currentStop,
241
257
  origin:
@@ -4,7 +4,7 @@ import { describe, it } from 'node:test';
4
4
  import { deserializeStopsMap, serializeStopsMap } from '../io.js';
5
5
  import { StopsMap } from '../stops.js';
6
6
 
7
- describe('stops io', () => {
7
+ describe('Stops IO', () => {
8
8
  const stopsMap: StopsMap = new Map([
9
9
  [
10
10
  1,
@@ -93,7 +93,7 @@ const mockStops: StopsMap = new Map([
93
93
  ],
94
94
  ]);
95
95
 
96
- describe('StopFinder', () => {
96
+ describe('Stop Finder', () => {
97
97
  let stopFinder: StopsIndex;
98
98
 
99
99
  beforeEach(() => {
@@ -1,6 +1,6 @@
1
1
  // Code generated by protoc-gen-ts_proto. DO NOT EDIT.
2
2
  // versions:
3
- // protoc-gen-ts_proto v2.6.1
3
+ // protoc-gen-ts_proto v2.7.7
4
4
  // protoc v4.23.4
5
5
  // source: src/stops/proto/stops.proto
6
6
 
@@ -127,7 +127,7 @@ export const Stop: MessageFns<Stop> = {
127
127
 
128
128
  decode(input: BinaryReader | Uint8Array, length?: number): Stop {
129
129
  const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
130
- let end = length === undefined ? reader.len : reader.pos + length;
130
+ const end = length === undefined ? reader.len : reader.pos + length;
131
131
  const message = createBaseStop();
132
132
  while (reader.pos < end) {
133
133
  const tag = reader.uint32();
@@ -291,7 +291,7 @@ export const StopsMap: MessageFns<StopsMap> = {
291
291
 
292
292
  decode(input: BinaryReader | Uint8Array, length?: number): StopsMap {
293
293
  const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
294
- let end = length === undefined ? reader.len : reader.pos + length;
294
+ const end = length === undefined ? reader.len : reader.pos + length;
295
295
  const message = createBaseStopsMap();
296
296
  while (reader.pos < end) {
297
297
  const tag = reader.uint32();
@@ -386,7 +386,7 @@ export const StopsMap_StopsEntry: MessageFns<StopsMap_StopsEntry> = {
386
386
 
387
387
  decode(input: BinaryReader | Uint8Array, length?: number): StopsMap_StopsEntry {
388
388
  const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
389
- let end = length === undefined ? reader.len : reader.pos + length;
389
+ const end = length === undefined ? reader.len : reader.pos + length;
390
390
  const message = createBaseStopsMap_StopsEntry();
391
391
  while (reader.pos < end) {
392
392
  const tag = reader.uint32();
@@ -74,8 +74,8 @@ export class StopsIndex {
74
74
  /**
75
75
  * Deserializes a binary representation of the stops.
76
76
  *
77
- * @param {Uint8Array} data - The binary data to deserialize.
78
- * @returns {StopsMap} - The deserialized StopFinder.
77
+ * @param data - The binary data to deserialize.
78
+ * @returns The deserialized StopFinder.
79
79
  */
80
80
  static fromData(data: Uint8Array): StopsIndex {
81
81
  const reader = new BinaryReader(data);
@@ -87,7 +87,7 @@ export class StopsIndex {
87
87
  /**
88
88
  * Serializes the stops into a binary protobuf.
89
89
  *
90
- * @returns {Uint8Array} - The serialized binary data.
90
+ * @returns The serialized binary data.
91
91
  */
92
92
  serialize(): Uint8Array {
93
93
  const protoStopsMap: ProtoStopsMap = serializeStopsMap(this.stopsMap);
@@ -10,6 +10,7 @@ import {
10
10
  serializeServiceRoutesMap,
11
11
  serializeStopsAdjacency,
12
12
  } from '../io.js';
13
+ import { REGULAR, Route } from '../route.js';
13
14
  import { Time } from '../time.js';
14
15
  import {
15
16
  RoutesAdjacency,
@@ -17,7 +18,7 @@ import {
17
18
  StopsAdjacency,
18
19
  } from '../timetable.js';
19
20
 
20
- describe('timetable io', () => {
21
+ describe('Timetable IO', () => {
21
22
  const stopsAdjacency: StopsAdjacency = new Map([
22
23
  [
23
24
  1,
@@ -43,35 +44,27 @@ describe('timetable io', () => {
43
44
  const routesAdjacency: RoutesAdjacency = new Map([
44
45
  [
45
46
  'route1',
46
- {
47
- stopTimes: new Uint16Array([
47
+ new Route(
48
+ new Uint16Array([
48
49
  Time.fromHMS(16, 40, 0).toMinutes(),
49
50
  Time.fromHMS(16, 50, 0).toMinutes(),
50
51
  ]),
51
- pickUpDropOffTypes: new Uint8Array([0, 0]), // REGULAR
52
- stops: new Uint32Array([1, 2]),
53
- stopIndices: new Map([
54
- [1, 0],
55
- [2, 1],
56
- ]),
57
- serviceRouteId: 'gtfs1',
58
- },
52
+ new Uint8Array([REGULAR, REGULAR]),
53
+ new Uint32Array([1, 2]),
54
+ 'gtfs1',
55
+ ),
59
56
  ],
60
57
  [
61
58
  'route2',
62
- {
63
- stopTimes: new Uint16Array([
59
+ new Route(
60
+ new Uint16Array([
64
61
  Time.fromHMS(15, 20, 0).toMinutes(),
65
62
  Time.fromHMS(15, 30, 0).toMinutes(),
66
63
  ]),
67
- pickUpDropOffTypes: new Uint8Array([0, 0]), // REGULAR
68
- stops: new Uint32Array([2, 1]),
69
- stopIndices: new Map([
70
- [2, 0],
71
- [1, 1],
72
- ]),
73
- serviceRouteId: 'gtfs2',
74
- },
64
+ new Uint8Array([REGULAR, REGULAR]),
65
+ new Uint32Array([2, 1]),
66
+ 'gtfs2',
67
+ ),
75
68
  ],
76
69
  ]);
77
70
  const routes: ServiceRoutesMap = new Map([
@@ -106,7 +99,7 @@ describe('timetable io', () => {
106
99
  Time.fromHMS(16, 50, 0).toMinutes(),
107
100
  ]).buffer,
108
101
  ),
109
- pickUpDropOffTypes: new Uint8Array([0, 0]), // REGULAR
102
+ pickUpDropOffTypes: new Uint8Array([REGULAR, REGULAR]),
110
103
  stops: new Uint8Array(new Uint32Array([1, 2]).buffer),
111
104
  serviceRouteId: 'gtfs1',
112
105
  },
@@ -117,7 +110,7 @@ describe('timetable io', () => {
117
110
  Time.fromHMS(15, 30, 0).toMinutes(),
118
111
  ]).buffer,
119
112
  ),
120
- pickUpDropOffTypes: new Uint8Array([0, 0]), // REGULAR
113
+ pickUpDropOffTypes: new Uint8Array([REGULAR, REGULAR]),
121
114
  stops: new Uint8Array(new Uint32Array([2, 1]).buffer),
122
115
  serviceRouteId: 'gtfs2',
123
116
  },