minotor 11.1.0 → 11.1.1

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.0](https://github.com/aubryio/minotor/compare/v11.0.0...v11.1.0) (2026-04-15)
1
+ ## [11.1.1](https://github.com/aubryio/minotor/compare/v11.1.0...v11.1.1) (2026-04-18)
2
2
 
3
3
 
4
- ### Features
4
+ ### Performance Improvements
5
5
 
6
- * basic GTFS frequency.txt support (scheduled only) ([#63](https://github.com/aubryio/minotor/issues/63)) ([05cf559](https://github.com/aubryio/minotor/commit/05cf559cd34ba336dae7f35e9eb7901ae23ea7b4)), closes [#50](https://github.com/aubryio/minotor/issues/50)
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))
package/dist/cli.mjs CHANGED
@@ -17025,7 +17025,6 @@ function isSet(value) {
17025
17025
  return value !== null && value !== undefined;
17026
17026
  }
17027
17027
 
17028
- const TIME_INFINITY = Number.MAX_SAFE_INTEGER;
17029
17028
  const TIME_ORIGIN = 0;
17030
17029
  const DURATION_ZERO = 0;
17031
17030
  /**
@@ -17763,6 +17762,12 @@ class Timetable {
17763
17762
  isActive(stopId) {
17764
17763
  return this.activeStops.has(stopId);
17765
17764
  }
17765
+ /**
17766
+ * Returns the total number of stops in the timetable.
17767
+ */
17768
+ nbStops() {
17769
+ return this.stopsAdjacency.length;
17770
+ }
17766
17771
  /**
17767
17772
  * Retrieves the route associated with the given route ID.
17768
17773
  *
@@ -21224,8 +21229,8 @@ class Plotter {
21224
21229
  * Determines station type (origin/destination) information.
21225
21230
  */
21226
21231
  getStationInfo(stopId) {
21227
- var _a, _b;
21228
- const isOrigin = (_b = (_a = this.result.routingState.graph[0]) === null || _a === void 0 ? void 0 : _a.has(stopId)) !== null && _b !== void 0 ? _b : false;
21232
+ var _a;
21233
+ const isOrigin = ((_a = this.result.routingState.graph[0]) === null || _a === void 0 ? void 0 : _a[stopId]) !== undefined;
21229
21234
  const isDestination = this.result.routingState.destinations.includes(stopId);
21230
21235
  return { isOrigin, isDestination };
21231
21236
  }
@@ -21357,8 +21362,11 @@ class Plotter {
21357
21362
  collectStations() {
21358
21363
  const stations = new Set();
21359
21364
  const graph = this.result.routingState.graph;
21360
- for (const roundMap of graph) {
21361
- for (const [stopId, edge] of roundMap) {
21365
+ for (const roundEdges of graph) {
21366
+ for (let stopId = 0; stopId < roundEdges.length; stopId++) {
21367
+ const edge = roundEdges[stopId];
21368
+ if (edge === undefined)
21369
+ continue;
21362
21370
  stations.add(stopId);
21363
21371
  if (isVehicleEdge(edge)) {
21364
21372
  const fromStopId = this.getVehicleEdgeFromStopId(edge);
@@ -21395,14 +21403,17 @@ class Plotter {
21395
21403
  const continuationEdges = [];
21396
21404
  const graph = this.result.routingState.graph;
21397
21405
  for (let round = 0; round < graph.length; round++) {
21398
- const roundMap = graph[round];
21399
- if (!roundMap)
21406
+ const roundEdges = graph[round];
21407
+ if (!roundEdges)
21400
21408
  continue;
21401
21409
  // Skip round 0 as it contains only origin nodes
21402
21410
  if (round === 0) {
21403
21411
  continue;
21404
21412
  }
21405
- for (const edge of roundMap.values()) {
21413
+ for (let stopId = 0; stopId < roundEdges.length; stopId++) {
21414
+ const edge = roundEdges[stopId];
21415
+ if (edge === undefined)
21416
+ continue;
21406
21417
  if (isVehicleEdge(edge)) {
21407
21418
  edges.push(...this.createVehicleEdge(edge, round));
21408
21419
  if (edge.continuationOf) {
@@ -21684,30 +21695,31 @@ class Result {
21684
21695
  const destinationIterable = to instanceof Set ? to : to ? [to] : this.query.to;
21685
21696
  // Find the fastest-reached destination across all equivalent stops.
21686
21697
  let fastestDestination = undefined;
21687
- let fastestTime = undefined;
21698
+ let fastestArrivalTime = undefined;
21699
+ let fastestLegNumber = undefined;
21688
21700
  for (const sourceDestination of destinationIterable) {
21689
21701
  const equivalentStops = this.stopsIndex.equivalentStops(sourceDestination);
21690
21702
  for (const destination of equivalentStops) {
21691
- const arrivalTime = this.routingState.earliestArrivals.get(destination.id);
21692
- if (arrivalTime !== undefined) {
21693
- if (fastestTime === undefined ||
21694
- arrivalTime.arrival < fastestTime.arrival) {
21695
- fastestDestination = destination.id;
21696
- fastestTime = arrivalTime;
21697
- }
21703
+ const arrivalData = this.routingState.getArrival(destination.id);
21704
+ if (arrivalData !== undefined &&
21705
+ (fastestArrivalTime === undefined ||
21706
+ arrivalData.arrival < fastestArrivalTime)) {
21707
+ fastestDestination = destination.id;
21708
+ fastestArrivalTime = arrivalData.arrival;
21709
+ fastestLegNumber = arrivalData.legNumber;
21698
21710
  }
21699
21711
  }
21700
21712
  }
21701
- if (!fastestDestination || !fastestTime) {
21713
+ if (fastestDestination === undefined || fastestLegNumber === undefined) {
21702
21714
  return undefined;
21703
21715
  }
21704
21716
  // Reconstruct the path by walking backwards through the routing graph.
21705
21717
  const route = [];
21706
21718
  let currentStop = fastestDestination;
21707
- let round = fastestTime.legNumber;
21719
+ let round = fastestLegNumber;
21708
21720
  let previousVehicleEdge;
21709
21721
  while (round > 0) {
21710
- const edge = (_a = this.routingState.graph[round]) === null || _a === void 0 ? void 0 : _a.get(currentStop);
21722
+ const edge = (_a = this.routingState.graph[round]) === null || _a === void 0 ? void 0 : _a[currentStop];
21711
21723
  if (!edge) {
21712
21724
  throw new Error(`No edge arriving at stop ${currentStop} at round ${round}`);
21713
21725
  }
@@ -21844,19 +21856,21 @@ class Result {
21844
21856
  * @returns The arrival time if the target stop is reachable, otherwise undefined.
21845
21857
  */
21846
21858
  arrivalAt(stop, maxTransfers) {
21847
- var _a;
21859
+ var _a, _b;
21848
21860
  const equivalentStops = this.stopsIndex.equivalentStops(stop);
21849
21861
  let earliestArrival = undefined;
21850
21862
  for (const equivalentStop of equivalentStops) {
21851
21863
  let arrivalTime;
21852
- if (maxTransfers === undefined) {
21853
- arrivalTime = this.routingState.earliestArrivals.get(equivalentStop.id);
21864
+ if (maxTransfers === undefined ||
21865
+ ((_a = this.routingState.getArrival(equivalentStop.id)) === null || _a === void 0 ? void 0 : _a.legNumber) ===
21866
+ maxTransfers + 1) {
21867
+ arrivalTime = this.routingState.getArrival(equivalentStop.id);
21854
21868
  }
21855
21869
  else {
21856
21870
  // We have no guarantee that the stop was visited in the last round,
21857
21871
  // so we need to check all rounds if it's not found in the last one.
21858
21872
  for (let i = maxTransfers + 1; i >= 0; i--) {
21859
- const arrivalEdge = (_a = this.routingState.graph[i]) === null || _a === void 0 ? void 0 : _a.get(equivalentStop.id);
21873
+ const arrivalEdge = (_b = this.routingState.graph[i]) === null || _b === void 0 ? void 0 : _b[equivalentStop.id];
21860
21874
  if (arrivalEdge !== undefined) {
21861
21875
  arrivalTime = {
21862
21876
  arrival: arrivalEdge.arrival,
@@ -21877,7 +21891,117 @@ class Result {
21877
21891
  }
21878
21892
  }
21879
21893
 
21880
- const UNREACHED = TIME_INFINITY;
21894
+ /**
21895
+ * Sentinel value used in the internal arrival-time array to mark stops not yet reached.
21896
+ * 0xFFFF = 65 535 minutes ≈ 45.5 days, safely beyond any realistic transit arrival time.
21897
+ */
21898
+ const UNREACHED_TIME = 0xffff;
21899
+ /**
21900
+ * Encapsulates all mutable state for a single RAPTOR routing query.
21901
+ */
21902
+ class RoutingState {
21903
+ /**
21904
+ * Initializes the routing state for a fresh query.
21905
+ *
21906
+ * All stops start as unreached. Each origin is immediately recorded at the
21907
+ * departure time with leg number 0, and a corresponding OriginNode is placed
21908
+ * in round 0 of the graph.
21909
+ *
21910
+ * @param origins Stop IDs to depart from (may be several equivalent stops).
21911
+ * @param destinations Stop IDs that count as the target of the query.
21912
+ * @param departureTime Earliest departure time in minutes from midnight.
21913
+ * @param nbStops Total number of stops in the timetable (sets array sizes).
21914
+ */
21915
+ constructor(origins, destinations, departureTime, nbStops) {
21916
+ this.origins = origins;
21917
+ this.destinations = destinations;
21918
+ const earliestArrivalTimes = new Uint16Array(nbStops).fill(UNREACHED_TIME);
21919
+ const earliestArrivalLegs = new Uint8Array(nbStops); // zero-initialized = leg 0
21920
+ const graph0 = new Array(nbStops);
21921
+ for (const stop of origins) {
21922
+ earliestArrivalTimes[stop] = departureTime;
21923
+ graph0[stop] = { arrival: departureTime };
21924
+ }
21925
+ this.earliestArrivalTimes = earliestArrivalTimes;
21926
+ this.earliestArrivalLegs = earliestArrivalLegs;
21927
+ this.graph = [graph0];
21928
+ }
21929
+ /** Total number of stops in the timetable */
21930
+ get nbStops() {
21931
+ return this.earliestArrivalTimes.length;
21932
+ }
21933
+ /**
21934
+ * Returns the earliest known arrival time at a stop.
21935
+ * Returns UNREACHED_TIME if the stop has not been reached yet.
21936
+ */
21937
+ arrivalTime(stop) {
21938
+ return this.earliestArrivalTimes[stop];
21939
+ }
21940
+ /**
21941
+ * Records a new earliest arrival at a stop.
21942
+
21943
+ *
21944
+ * @param stop The stop that was reached.
21945
+ * @param time The arrival time in minutes from midnight.
21946
+ * @param leg The round number (number of transit legs taken so far).
21947
+ */
21948
+ updateArrival(stop, time, leg) {
21949
+ this.earliestArrivalTimes[stop] = time;
21950
+ this.earliestArrivalLegs[stop] = leg;
21951
+ }
21952
+ /**
21953
+ * Returns the earliest arrival at a stop as an {@link Arrival} object,
21954
+ * or undefined if the stop has not been reached.
21955
+ */
21956
+ getArrival(stop) {
21957
+ const time = this.earliestArrivalTimes[stop];
21958
+ if (time >= UNREACHED_TIME)
21959
+ return undefined;
21960
+ return { arrival: time, legNumber: this.earliestArrivalLegs[stop] };
21961
+ }
21962
+ /**
21963
+ * Creates a {@link RoutingState} from fully-specified raw data.
21964
+ *
21965
+ * Use this in tests instead of constructing the object through the production
21966
+ * constructor, which is designed for incremental algorithm state.
21967
+ *
21968
+ * @param nbStops Total number of stops (sets array sizes).
21969
+ * @param origins Origin stop IDs.
21970
+ * @param destinations Destination stop IDs.
21971
+ * @param arrivals Each entry is `[stop, time, leg]` — the earliest arrival
21972
+ * time in minutes and the round number for one stop.
21973
+ * @param graph One element per round. Each round is a sparse list of
21974
+ * `[stop, edge]` pairs; stops absent from the list are
21975
+ * left as `undefined` in the dense output array.
21976
+ *
21977
+ * @internal For use in tests only.
21978
+ */
21979
+ static fromTestData({ nbStops, origins = [], destinations = [], arrivals = [], graph = [], }) {
21980
+ const state = new RoutingState(origins, destinations, 0, nbStops);
21981
+ // Replace the arrival arrays with freshly built ones so the constructor's
21982
+ // origin-seeding doesn't bleed into the test state.
21983
+ const earliestArrivalTimes = new Uint16Array(nbStops).fill(UNREACHED_TIME);
21984
+ const earliestArrivalLegs = new Uint8Array(nbStops);
21985
+ for (const [stop, time, leg] of arrivals) {
21986
+ earliestArrivalTimes[stop] = time;
21987
+ earliestArrivalLegs[stop] = leg;
21988
+ }
21989
+ state.earliestArrivalTimes = earliestArrivalTimes;
21990
+ state.earliestArrivalLegs = earliestArrivalLegs;
21991
+ // Convert the sparse per-round representation to dense arrays and replace
21992
+ // the graph in-place.
21993
+ const denseRounds = graph.map((round) => {
21994
+ const arr = new Array(nbStops);
21995
+ for (const [stop, edge] of round) {
21996
+ arr[stop] = edge;
21997
+ }
21998
+ return arr;
21999
+ });
22000
+ state.graph.splice(0, state.graph.length, ...denseRounds);
22001
+ return state;
22002
+ }
22003
+ }
22004
+
21881
22005
  /**
21882
22006
  * A public transportation router implementing the RAPTOR algorithm.
21883
22007
  * For more information on the RAPTOR algorithm,
@@ -21897,17 +22021,14 @@ class Router {
21897
22021
  */
21898
22022
  route(query) {
21899
22023
  const routingState = this.initRoutingState(query);
21900
- const markedStops = new Set();
21901
- for (const originStop of routingState.graph[0].keys()) {
21902
- markedStops.add(originStop);
21903
- }
22024
+ const markedStops = new Set(routingState.origins);
21904
22025
  // Initial transfer consideration for origins
21905
22026
  const newlyMarkedStops = this.considerTransfers(query, 0, markedStops, routingState);
21906
22027
  for (const newStop of newlyMarkedStops) {
21907
22028
  markedStops.add(newStop);
21908
22029
  }
21909
22030
  for (let round = 1; round <= query.options.maxTransfers + 1; round++) {
21910
- const edgesAtCurrentRound = new Map();
22031
+ const edgesAtCurrentRound = new Array(routingState.nbStops);
21911
22032
  routingState.graph.push(edgesAtCurrentRound);
21912
22033
  const reachableRoutes = this.timetable.findReachableRoutes(markedStops, query.options.transportModes);
21913
22034
  markedStops.clear();
@@ -21946,13 +22067,13 @@ class Router {
21946
22067
  /**
21947
22068
  * Finds trip continuations for the given marked stops and edges at the current round.
21948
22069
  * @param markedStops The set of marked stops.
21949
- * @param edgesAtCurrentRound The map of edges at the current round.
22070
+ * @param edgesAtCurrentRound The array of edges at the current round, indexed by stop ID.
21950
22071
  * @returns An array of trip continuations.
21951
22072
  */
21952
22073
  findTripContinuations(markedStops, edgesAtCurrentRound) {
21953
22074
  const continuations = [];
21954
22075
  for (const stopId of markedStops) {
21955
- const arrival = edgesAtCurrentRound.get(stopId);
22076
+ const arrival = edgesAtCurrentRound[stopId];
21956
22077
  if (!arrival || !('routeId' in arrival))
21957
22078
  continue;
21958
22079
  const continuousTrips = this.timetable.getContinuousTrips(arrival.hopOffStopIndex, arrival.routeId, arrival.tripIndex);
@@ -21988,22 +22109,7 @@ class Router {
21988
22109
  const destinations = Array.from(to)
21989
22110
  .flatMap((destination) => this.stopsIndex.equivalentStops(destination))
21990
22111
  .map((destination) => destination.id);
21991
- const earliestArrivals = new Map();
21992
- const earliestArrivalsWithoutAnyLeg = new Map();
21993
- const earliestArrivalsPerRound = [earliestArrivalsWithoutAnyLeg];
21994
- const initialState = {
21995
- arrival: departureTime,
21996
- legNumber: 0,
21997
- };
21998
- for (const originStop of origins) {
21999
- earliestArrivals.set(originStop, initialState);
22000
- earliestArrivalsWithoutAnyLeg.set(originStop, initialState);
22001
- }
22002
- return {
22003
- destinations,
22004
- earliestArrivals,
22005
- graph: earliestArrivalsPerRound,
22006
- };
22112
+ return new RoutingState(origins, destinations, departureTime, this.timetable.nbStops());
22007
22113
  }
22008
22114
  /**
22009
22115
  * Scans a route to find the earliest possible trips (if not provided) and updates arrival times.
@@ -22020,7 +22126,6 @@ class Router {
22020
22126
  * @param routingState The current routing state containing arrival times and marked stops
22021
22127
  */
22022
22128
  scanRoute(route, hopOnStopIndex, round, routingState, options, tripContinuation) {
22023
- var _a, _b;
22024
22129
  const newlyMarkedStops = new Set();
22025
22130
  let activeTrip = tripContinuation
22026
22131
  ? {
@@ -22032,7 +22137,7 @@ class Router {
22032
22137
  const edgesAtCurrentRound = routingState.graph[round];
22033
22138
  const edgesAtPreviousRound = routingState.graph[round - 1];
22034
22139
  // Compute target pruning criteria only once per route
22035
- const earliestArrivalAtAnyDestination = this.earliestArrivalAtAnyStop(routingState.earliestArrivals, routingState.destinations);
22140
+ const earliestArrivalAtAnyDestination = this.earliestArrivalAtAnyStop(routingState);
22036
22141
  for (let currentStopIndex = hopOnStopIndex; currentStopIndex < route.getNbStops(); currentStopIndex++) {
22037
22142
  const currentStop = route.stops[currentStopIndex];
22038
22143
  // If we're currently on a trip,
@@ -22040,7 +22145,7 @@ class Router {
22040
22145
  if (activeTrip !== undefined) {
22041
22146
  const arrivalTime = route.arrivalAt(currentStopIndex, activeTrip.tripIndex);
22042
22147
  const dropOffType = route.dropOffTypeAt(currentStopIndex, activeTrip.tripIndex);
22043
- const earliestArrivalAtCurrentStop = (_b = (_a = routingState.earliestArrivals.get(currentStop)) === null || _a === void 0 ? void 0 : _a.arrival) !== null && _b !== void 0 ? _b : UNREACHED;
22148
+ const earliestArrivalAtCurrentStop = routingState.arrivalTime(currentStop);
22044
22149
  if (dropOffType !== NOT_AVAILABLE &&
22045
22150
  arrivalTime < earliestArrivalAtCurrentStop &&
22046
22151
  arrivalTime < earliestArrivalAtAnyDestination) {
@@ -22055,11 +22160,8 @@ class Router {
22055
22160
  // In case of continuous trip, we set a pointer to the previous edge
22056
22161
  edge.continuationOf = tripContinuation.previousEdge;
22057
22162
  }
22058
- edgesAtCurrentRound.set(currentStop, edge);
22059
- routingState.earliestArrivals.set(currentStop, {
22060
- arrival: arrivalTime,
22061
- legNumber: round,
22062
- });
22163
+ edgesAtCurrentRound[currentStop] = edge;
22164
+ routingState.updateArrival(currentStop, arrivalTime, round);
22063
22165
  newlyMarkedStops.add(currentStop);
22064
22166
  }
22065
22167
  }
@@ -22069,7 +22171,7 @@ class Router {
22069
22171
  }
22070
22172
  // check if we can board an earlier trip at the current stop
22071
22173
  // if there was no current trip, find the first one reachable
22072
- const previousEdge = edgesAtPreviousRound.get(currentStop);
22174
+ const previousEdge = edgesAtPreviousRound[currentStop];
22073
22175
  const earliestArrivalOnPreviousRound = previousEdge === null || previousEdge === void 0 ? void 0 : previousEdge.arrival;
22074
22176
  if (earliestArrivalOnPreviousRound !== undefined &&
22075
22177
  (activeTrip === undefined ||
@@ -22146,12 +22248,11 @@ class Router {
22146
22248
  * @param routingState The current routing state containing arrival times and marked stops
22147
22249
  */
22148
22250
  considerTransfers(query, round, markedStops, routingState) {
22149
- var _a, _b;
22150
22251
  const { options } = query;
22151
22252
  const arrivalsAtCurrentRound = routingState.graph[round];
22152
22253
  const newlyMarkedStops = new Set();
22153
22254
  for (const stop of markedStops) {
22154
- const currentArrival = arrivalsAtCurrentRound.get(stop);
22255
+ const currentArrival = arrivalsAtCurrentRound[stop];
22155
22256
  // Skip transfers if the last leg was also a transfer
22156
22257
  if (!currentArrival || 'type' in currentArrival)
22157
22258
  continue;
@@ -22170,19 +22271,16 @@ class Router {
22170
22271
  transferTime = options.minTransferTime;
22171
22272
  }
22172
22273
  const arrivalAfterTransfer = currentArrival.arrival + transferTime;
22173
- const originalArrival = (_b = (_a = routingState.earliestArrivals.get(transfer.destination)) === null || _a === void 0 ? void 0 : _a.arrival) !== null && _b !== void 0 ? _b : UNREACHED;
22274
+ const originalArrival = routingState.arrivalTime(transfer.destination);
22174
22275
  if (arrivalAfterTransfer < originalArrival) {
22175
- arrivalsAtCurrentRound.set(transfer.destination, {
22276
+ arrivalsAtCurrentRound[transfer.destination] = {
22176
22277
  arrival: arrivalAfterTransfer,
22177
22278
  from: stop,
22178
22279
  to: transfer.destination,
22179
22280
  minTransferTime: transfer.minTransferTime,
22180
22281
  type: transfer.type,
22181
- });
22182
- routingState.earliestArrivals.set(transfer.destination, {
22183
- arrival: arrivalAfterTransfer,
22184
- legNumber: round,
22185
- });
22282
+ };
22283
+ routingState.updateArrival(transfer.destination, arrivalAfterTransfer, round);
22186
22284
  newlyMarkedStops.add(transfer.destination);
22187
22285
  }
22188
22286
  }
@@ -22192,17 +22290,16 @@ class Router {
22192
22290
  /**
22193
22291
  * Finds the earliest arrival time at any stop from a given set of destinations.
22194
22292
  *
22195
- * @param earliestArrivals A map of stops to their earliest reaching times.
22196
- * @param destinations An array of destination stops to evaluate.
22293
+ * @param routingState The routing state containing arrival times and destinations.
22197
22294
  * @returns The earliest arrival time among the provided destinations.
22198
22295
  */
22199
- earliestArrivalAtAnyStop(earliestArrivals, destinations) {
22200
- var _a, _b;
22201
- let earliestArrivalAtAnyDestination = UNREACHED;
22202
- for (let i = 0; i < destinations.length; i++) {
22203
- const destination = destinations[i];
22204
- const arrival = (_b = (_a = earliestArrivals.get(destination)) === null || _a === void 0 ? void 0 : _a.arrival) !== null && _b !== void 0 ? _b : UNREACHED;
22205
- earliestArrivalAtAnyDestination = Math.min(earliestArrivalAtAnyDestination, arrival);
22296
+ earliestArrivalAtAnyStop(routingState) {
22297
+ let earliestArrivalAtAnyDestination = UNREACHED_TIME;
22298
+ for (let i = 0; i < routingState.destinations.length; i++) {
22299
+ const arrival = routingState.arrivalTime(routingState.destinations[i]);
22300
+ if (arrival < earliestArrivalAtAnyDestination) {
22301
+ earliestArrivalAtAnyDestination = arrival;
22302
+ }
22206
22303
  }
22207
22304
  return earliestArrivalAtAnyDestination;
22208
22305
  }