minotor 11.1.1 → 11.1.3

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/CHANGELOG.md CHANGED
@@ -1,6 +1,6 @@
1
- ## [11.1.1](https://github.com/aubryio/minotor/compare/v11.1.0...v11.1.1) (2026-04-18)
1
+ ## [11.1.3](https://github.com/aubryio/minotor/compare/v11.1.2...v11.1.3) (2026-04-18)
2
2
 
3
3
 
4
- ### Performance Improvements
4
+ ### Bug Fixes
5
5
 
6
- * optimize routing state by swapping maps for arrays ([#64](https://github.com/aubryio/minotor/issues/64)) ([b37d086](https://github.com/aubryio/minotor/commit/b37d08653f115d22c4b49f88f15521efbcfdae62))
6
+ * re-expose arrivals iterator ([681ee3a](https://github.com/aubryio/minotor/commit/681ee3a7c131c097734333cd60f5b0eed35cf3bd))
package/dist/cli.mjs CHANGED
@@ -17263,6 +17263,45 @@ let Route$1 = class Route {
17263
17263
  }
17264
17264
  return arrival;
17265
17265
  }
17266
+ /**
17267
+ * Computes the per-trip base offset used by the hot-path scanning methods.
17268
+ *
17269
+ * Cache the result once when boarding a new trip and pass it to
17270
+ * {@link arrivalAtOffset} and {@link dropOffTypeAtOffset} throughout the
17271
+ * scanning loop to avoid recomputing `tripIndex × nbStops` on every stop.
17272
+ *
17273
+ * @param tripIndex - The index of the trip.
17274
+ * @returns `tripIndex × nbStops`.
17275
+ */
17276
+ tripStopOffset(tripIndex) {
17277
+ return tripIndex * this.nbStops;
17278
+ }
17279
+ /**
17280
+ * Hot-path variant of {@link arrivalAt} that accepts a precomputed base offset.
17281
+ *
17282
+ * @param stopIndex - The index of the stop in the route.
17283
+ * @param offset - Precomputed value from {@link tripStopOffset}.
17284
+ * @returns The arrival time at the specified stop.
17285
+ */
17286
+ arrivalAtOffset(stopIndex, offset) {
17287
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
17288
+ return this.stopTimes[(offset + stopIndex) * 2];
17289
+ }
17290
+ /**
17291
+ * Hot-path variant of {@link dropOffTypeAt} that accepts a precomputed base offset.
17292
+ *
17293
+ * @param stopIndex - The index of the stop in the route.
17294
+ * @param offset - Precomputed value from {@link tripStopOffset}.
17295
+ * @returns The drop-off type at the specified stop.
17296
+ */
17297
+ dropOffTypeAtOffset(stopIndex, offset) {
17298
+ const globalIndex = offset + stopIndex;
17299
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
17300
+ const byte = this.pickupDropOffTypes[globalIndex >> 1];
17301
+ // Bit layout per byte (two pairs): [pickup_1(2)][dropOff_1(2)][pickup_0(2)][dropOff_0(2)]
17302
+ // First pair → drop-off in lower 2 bits; second pair → drop-off in bits 4-5.
17303
+ return (globalIndex & 1 ? (byte >> 4) & 0x03 : byte & 0x03);
17304
+ }
17266
17305
  /**
17267
17306
  * Retrieves the departure time at a specific stop for a given trip.
17268
17307
  *
@@ -17854,21 +17893,26 @@ class Timetable {
17854
17893
  */
17855
17894
  findReachableRoutes(fromStops, transportModes = ALL_TRANSPORT_MODES) {
17856
17895
  const reachableRoutes = new Map();
17857
- const fromStopsArray = Array.from(fromStops);
17858
- for (let i = 0; i < fromStopsArray.length; i++) {
17859
- const originStop = fromStopsArray[i];
17860
- const validRoutes = this.routesPassingThrough(originStop).filter((route) => {
17861
- const serviceRoute = this.getServiceRouteInfo(route);
17862
- return transportModes.has(serviceRoute.type);
17863
- });
17864
- for (let j = 0; j < validRoutes.length; j++) {
17865
- const route = validRoutes[j];
17866
- const originStopIndices = route.stopRouteIndices(originStop);
17867
- const originStopIndex = originStopIndices[0];
17896
+ // Skip the per-route mode check entirely when all modes are allowed,
17897
+ // which is the common case.
17898
+ const filterByMode = transportModes !== ALL_TRANSPORT_MODES;
17899
+ for (const originStop of fromStops) {
17900
+ const stopData = this.stopsAdjacency[originStop];
17901
+ if (!stopData)
17902
+ continue;
17903
+ for (let i = 0; i < stopData.routes.length; i++) {
17904
+ const route = this.routesAdjacency[stopData.routes[i]];
17905
+ if (!route)
17906
+ continue;
17907
+ if (filterByMode) {
17908
+ const serviceRoute = this.serviceRoutes[route.serviceRoute()];
17909
+ if (!serviceRoute || !transportModes.has(serviceRoute.type))
17910
+ continue;
17911
+ }
17912
+ const originStopIndex = route.stopRouteIndices(originStop)[0];
17868
17913
  const existingHopOnStopIndex = reachableRoutes.get(route);
17869
17914
  if (existingHopOnStopIndex !== undefined) {
17870
17915
  if (originStopIndex < existingHopOnStopIndex) {
17871
- // if the current stop is before the existing hop on stop, replace it
17872
17916
  reachableRoutes.set(route, originStopIndex);
17873
17917
  }
17874
17918
  }
@@ -17893,9 +17937,15 @@ class Timetable {
17893
17937
  if (!guaranteedTransfers) {
17894
17938
  return false;
17895
17939
  }
17896
- return guaranteedTransfers.some((transfer) => transfer.stopIndex === toTripStop.stopIndex &&
17897
- transfer.routeId === toTripStop.routeId &&
17898
- transfer.tripIndex === toTripStop.tripIndex);
17940
+ for (let i = 0; i < guaranteedTransfers.length; i++) {
17941
+ const transfer = guaranteedTransfers[i];
17942
+ if (transfer.stopIndex === toTripStop.stopIndex &&
17943
+ transfer.routeId === toTripStop.routeId &&
17944
+ transfer.tripIndex === toTripStop.tripIndex) {
17945
+ return true;
17946
+ }
17947
+ }
17948
+ return false;
17899
17949
  }
17900
17950
  /**
17901
17951
  * Retrieves all guaranteed trip transfer options available at the specified stop for a given trip.
@@ -21949,6 +21999,31 @@ class RoutingState {
21949
21999
  this.earliestArrivalTimes[stop] = time;
21950
22000
  this.earliestArrivalLegs[stop] = leg;
21951
22001
  }
22002
+ /**
22003
+ * Iterates over every stop that has been reached, yielding its stop ID,
22004
+ * earliest arrival time, and the number of legs taken to reach it.
22005
+ *
22006
+ * Unreached stops (those still at UNREACHED_TIME) are skipped entirely.
22007
+ *
22008
+ * @example
22009
+ * ```ts
22010
+ * for (const { stop, arrival, legNumber } of routingState.arrivals()) {
22011
+ * console.log(`Stop ${stop}: arrived at ${arrival} after ${legNumber} leg(s)`);
22012
+ * }
22013
+ * ```
22014
+ */
22015
+ *arrivals() {
22016
+ for (let stop = 0; stop < this.earliestArrivalTimes.length; stop++) {
22017
+ const time = this.earliestArrivalTimes[stop];
22018
+ if (time < UNREACHED_TIME) {
22019
+ yield {
22020
+ stop,
22021
+ arrival: time,
22022
+ legNumber: this.earliestArrivalLegs[stop],
22023
+ };
22024
+ }
22025
+ }
22026
+ }
21952
22027
  /**
21953
22028
  * Returns the earliest arrival at a stop as an {@link Arrival} object,
21954
22029
  * or undefined if the stop has not been reached.
@@ -22041,11 +22116,12 @@ class Router {
22041
22116
  }
22042
22117
  // process in-seat trip continuations
22043
22118
  let continuations = this.findTripContinuations(markedStops, edgesAtCurrentRound);
22119
+ const stopsFromContinuations = new Set();
22044
22120
  while (continuations.length > 0) {
22045
- const stopsFromContinuations = new Set();
22121
+ stopsFromContinuations.clear();
22046
22122
  for (const continuation of continuations) {
22047
22123
  const route = this.timetable.getRoute(continuation.routeId);
22048
- const routeScanResults = this.scanRoute(route, continuation.stopIndex, round, routingState, query.options, continuation);
22124
+ const routeScanResults = this.scanRouteContinuation(route, continuation.stopIndex, round, routingState, continuation);
22049
22125
  for (const newStop of routeScanResults) {
22050
22126
  stopsFromContinuations.add(newStop);
22051
22127
  }
@@ -22112,84 +22188,113 @@ class Router {
22112
22188
  return new RoutingState(origins, destinations, departureTime, this.timetable.nbStops());
22113
22189
  }
22114
22190
  /**
22115
- * Scans a route to find the earliest possible trips (if not provided) and updates arrival times.
22191
+ * Scans a route for an in-seat trip continuation.
22116
22192
  *
22117
- * This method implements the core route scanning logic of the RAPTOR algorithm.
22118
- * It iterates through all stops on a given route starting from the hop-on stop,
22119
- * maintaining the current best trip and updating arrival times when improvements
22120
- * are found. The method also handles boarding new trips when earlier departures
22121
- * are available if no given trip is provided as a parameter.
22193
+ * The boarded trip and entry stop are fixed, so there is no need to probe for
22194
+ * earlier boardings.
22122
22195
  *
22123
- * @param route The route to scan for possible trips
22124
- * @param hopOnStopIndex The stop index where passengers can board the route
22125
- * @param round The current round number in the RAPTOR algorithm
22126
- * @param routingState The current routing state containing arrival times and marked stops
22196
+ * @param route The route to scan
22197
+ * @param hopOnStopIndex The stop index where the continuation begins
22198
+ * @param round The current RAPTOR round
22199
+ * @param routingState Current routing state
22200
+ * @param tripContinuation The in-seat continuation descriptor
22127
22201
  */
22128
- scanRoute(route, hopOnStopIndex, round, routingState, options, tripContinuation) {
22202
+ scanRouteContinuation(route, hopOnStopIndex, round, routingState, tripContinuation) {
22129
22203
  const newlyMarkedStops = new Set();
22130
- let activeTrip = tripContinuation
22131
- ? {
22132
- routeId: route.id,
22133
- stopIndex: hopOnStopIndex,
22134
- tripIndex: tripContinuation.tripIndex,
22204
+ const edgesAtCurrentRound = routingState.graph[round];
22205
+ const earliestArrivalAtAnyDestination = this.earliestArrivalAtAnyStop(routingState);
22206
+ const nbStops = route.getNbStops();
22207
+ const routeId = route.id;
22208
+ const tripIndex = tripContinuation.tripIndex;
22209
+ const tripStopOffset = route.tripStopOffset(tripIndex);
22210
+ const previousEdge = tripContinuation.previousEdge;
22211
+ for (let currentStopIndex = hopOnStopIndex; currentStopIndex < nbStops; currentStopIndex++) {
22212
+ const currentStop = route.stops[currentStopIndex];
22213
+ const arrivalTime = route.arrivalAtOffset(currentStopIndex, tripStopOffset);
22214
+ const dropOffType = route.dropOffTypeAtOffset(currentStopIndex, tripStopOffset);
22215
+ const earliestArrivalAtCurrentStop = routingState.arrivalTime(currentStop);
22216
+ if (dropOffType !== NOT_AVAILABLE &&
22217
+ arrivalTime < earliestArrivalAtCurrentStop &&
22218
+ arrivalTime < earliestArrivalAtAnyDestination) {
22219
+ edgesAtCurrentRound[currentStop] = {
22220
+ routeId,
22221
+ stopIndex: hopOnStopIndex,
22222
+ tripIndex,
22223
+ arrival: arrivalTime,
22224
+ hopOffStopIndex: currentStopIndex,
22225
+ continuationOf: previousEdge,
22226
+ };
22227
+ routingState.updateArrival(currentStop, arrivalTime, round);
22228
+ newlyMarkedStops.add(currentStop);
22135
22229
  }
22136
- : undefined;
22230
+ }
22231
+ return newlyMarkedStops;
22232
+ }
22233
+ /**
22234
+ * Scans a route using the standard RAPTOR boarding logic.
22235
+ *
22236
+ * Iterates through all stops from the hop-on point, maintaining the current
22237
+ * best trip and improving arrival times when possible. At each marked stop it
22238
+ * also checks whether an earlier (or first) trip can be boarded, upgrading the
22239
+ * active trip when one is found.
22240
+ *
22241
+ * @param route The route to scan
22242
+ * @param hopOnStopIndex The stop index where passengers can first board
22243
+ * @param round The current RAPTOR round
22244
+ * @param routingState Current routing state
22245
+ * @param options Query options (minTransferTime, etc.)
22246
+ */
22247
+ scanRoute(route, hopOnStopIndex, round, routingState, options) {
22248
+ const newlyMarkedStops = new Set();
22137
22249
  const edgesAtCurrentRound = routingState.graph[round];
22138
22250
  const edgesAtPreviousRound = routingState.graph[round - 1];
22139
- // Compute target pruning criteria only once per route
22140
22251
  const earliestArrivalAtAnyDestination = this.earliestArrivalAtAnyStop(routingState);
22141
- for (let currentStopIndex = hopOnStopIndex; currentStopIndex < route.getNbStops(); currentStopIndex++) {
22252
+ const nbStops = route.getNbStops();
22253
+ const routeId = route.id;
22254
+ let activeTripIndex;
22255
+ let activeTripBoardStopIndex = hopOnStopIndex;
22256
+ // tripStopOffset = activeTripIndex * nbStops, precomputed when the trip changes.
22257
+ // Only valid while activeTripIndex !== undefined.
22258
+ let activeTripStopOffset = 0;
22259
+ for (let currentStopIndex = hopOnStopIndex; currentStopIndex < nbStops; currentStopIndex++) {
22142
22260
  const currentStop = route.stops[currentStopIndex];
22143
- // If we're currently on a trip,
22144
- // check if arrival at the stop improves the earliest arrival time
22145
- if (activeTrip !== undefined) {
22146
- const arrivalTime = route.arrivalAt(currentStopIndex, activeTrip.tripIndex);
22147
- const dropOffType = route.dropOffTypeAt(currentStopIndex, activeTrip.tripIndex);
22261
+ // If on a trip, check whether alighting here improves the global best.
22262
+ if (activeTripIndex !== undefined) {
22263
+ const arrivalTime = route.arrivalAtOffset(currentStopIndex, activeTripStopOffset);
22264
+ const dropOffType = route.dropOffTypeAtOffset(currentStopIndex, activeTripStopOffset);
22148
22265
  const earliestArrivalAtCurrentStop = routingState.arrivalTime(currentStop);
22149
22266
  if (dropOffType !== NOT_AVAILABLE &&
22150
22267
  arrivalTime < earliestArrivalAtCurrentStop &&
22151
22268
  arrivalTime < earliestArrivalAtAnyDestination) {
22152
- const edge = {
22153
- routeId: activeTrip.routeId,
22154
- stopIndex: activeTrip.stopIndex,
22155
- tripIndex: activeTrip.tripIndex,
22269
+ edgesAtCurrentRound[currentStop] = {
22270
+ routeId,
22271
+ stopIndex: activeTripBoardStopIndex,
22272
+ tripIndex: activeTripIndex,
22156
22273
  arrival: arrivalTime,
22157
22274
  hopOffStopIndex: currentStopIndex,
22158
22275
  };
22159
- if (tripContinuation) {
22160
- // In case of continuous trip, we set a pointer to the previous edge
22161
- edge.continuationOf = tripContinuation.previousEdge;
22162
- }
22163
- edgesAtCurrentRound[currentStop] = edge;
22164
22276
  routingState.updateArrival(currentStop, arrivalTime, round);
22165
22277
  newlyMarkedStops.add(currentStop);
22166
22278
  }
22167
22279
  }
22168
- if (tripContinuation) {
22169
- // If it's a trip continuation, no need to check for earlier trips
22170
- continue;
22171
- }
22172
- // check if we can board an earlier trip at the current stop
22173
- // if there was no current trip, find the first one reachable
22280
+ // Check whether we can board an earlier (or first) trip at this stop.
22174
22281
  const previousEdge = edgesAtPreviousRound[currentStop];
22175
22282
  const earliestArrivalOnPreviousRound = previousEdge === null || previousEdge === void 0 ? void 0 : previousEdge.arrival;
22176
22283
  if (earliestArrivalOnPreviousRound !== undefined &&
22177
- (activeTrip === undefined ||
22284
+ (activeTripIndex === undefined ||
22178
22285
  earliestArrivalOnPreviousRound <=
22179
- route.departureFrom(currentStopIndex, activeTrip.tripIndex))) {
22180
- const earliestTrip = route.findEarliestTrip(currentStopIndex, earliestArrivalOnPreviousRound, activeTrip === null || activeTrip === void 0 ? void 0 : activeTrip.tripIndex);
22286
+ route.departureFrom(currentStopIndex, activeTripIndex))) {
22287
+ const earliestTrip = route.findEarliestTrip(currentStopIndex, earliestArrivalOnPreviousRound, activeTripIndex);
22181
22288
  if (earliestTrip === undefined) {
22182
22289
  continue;
22183
22290
  }
22184
- const firstBoardableTrip = this.findFirstBoardableTrip(currentStopIndex, route, earliestTrip, earliestArrivalOnPreviousRound, activeTrip === null || activeTrip === void 0 ? void 0 : activeTrip.tripIndex,
22185
- // provide the previous trip if the previous edge was a vehicle
22291
+ const firstBoardableTrip = this.findFirstBoardableTrip(currentStopIndex, route, earliestTrip, earliestArrivalOnPreviousRound, activeTripIndex,
22292
+ // provide the previous edge only if it was a vehicle leg
22186
22293
  previousEdge && 'routeId' in previousEdge ? previousEdge : undefined, options.minTransferTime);
22187
22294
  if (firstBoardableTrip !== undefined) {
22188
- activeTrip = {
22189
- routeId: route.id,
22190
- tripIndex: firstBoardableTrip,
22191
- stopIndex: currentStopIndex,
22192
- };
22295
+ activeTripIndex = firstBoardableTrip;
22296
+ activeTripBoardStopIndex = currentStopIndex;
22297
+ activeTripStopOffset = route.tripStopOffset(firstBoardableTrip);
22193
22298
  }
22194
22299
  }
22195
22300
  }