minotor 2.0.1 → 3.0.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/.cspell.json CHANGED
@@ -28,7 +28,9 @@
28
28
  "Davos",
29
29
  "moleson",
30
30
  "Klosters",
31
- "Engadin"
31
+ "Engadin",
32
+ "isochrone",
33
+ "Delling"
32
34
  ],
33
35
  "flagWords": [],
34
36
  "ignorePaths": [
package/CHANGELOG.md CHANGED
@@ -1,6 +1,6 @@
1
- ## [2.0.1](https://github.com/aubryio/minotor/compare/v2.0.0...v2.0.1) (2025-05-26)
1
+ ## [3.0.1](https://github.com/aubryio/minotor/compare/v3.0.0...v3.0.1) (2025-06-03)
2
2
 
3
3
 
4
4
  ### Bug Fixes
5
5
 
6
- * fix StopId export ([c53db0a](https://github.com/aubryio/minotor/commit/c53db0ad6d3751dc0b0cc38d60016b570c041bab))
6
+ * do not pin point lib version for the repl ([#18](https://github.com/aubryio/minotor/issues/18)) ([2cef9e7](https://github.com/aubryio/minotor/commit/2cef9e7488f09743aba52e2ebb0ae52bead476c6))
package/README.md CHANGED
@@ -13,13 +13,23 @@ Privacy-conscious applications where the user does not want to share their locat
13
13
  The transit router and the stops index of **minotor** can run in the browser, on react-native or in a Node.js environment.
14
14
  Transit data (GTFS) parsing runs on Node.js, and the resulting data is serialized as a protobuf binary that can be loaded from the router.
15
15
 
16
+ Minotor routing algorithm is mostly based on RAPTOR. See [Round-Based Public Transit Routing, D. Delling et al. 2012](https://www.microsoft.com/en-us/research/wp-content/uploads/2012/01/raptor_alenex.pdf).
17
+
16
18
  ## Examples
17
19
 
18
20
  ### In-browser transit router
19
21
 
20
22
  An example client-side transit router running in the browser with a web worker.
21
23
 
22
- [Demo](https://www.minotor.dev/#example-usage) | [Code](https://github.com/aubryio/minotor.dev/tree/main/app/examples/router)
24
+ [Demo](https://www.minotor.dev/#router) | [Code](https://github.com/aubryio/minotor.dev/tree/main/app/examples/planner)
25
+
26
+ ### Isochrone maps
27
+
28
+ An example implementation of dynamic isochrone maps using minotor in the browser.
29
+
30
+ [Demo](https://www.minotor.dev/#isochrones) | [Code](https://github.com/aubryio/minotor.dev/tree/main/app/examples/isochrones)
31
+
32
+ A more complete isochrone map showcase can be found on [isochrone.ch](https://isochrone.ch).
23
33
 
24
34
  ## Features
25
35
 
@@ -32,7 +42,7 @@ An example client-side transit router running in the browser with a web worker.
32
42
 
33
43
  | Feed | Parsing time | Timetable Size for a Day (Compressed) |
34
44
  | ------------------------------------------------------------------------------------------ | ------------ | ------------------------------------- |
35
- | [Swiss GTFS feed](https://data.opentransportdata.swiss/en/dataset/timetable-2025-gtfs2020) | ~2 minutes | 44 MB (9MB) |
45
+ | [Swiss GTFS feed](https://data.opentransportdata.swiss/en/dataset/timetable-2025-gtfs2020) | ~2 minutes | 20 MB (5MB) |
36
46
 
37
47
  ## Get started
38
48
 
@@ -63,7 +73,7 @@ const destinations = stopsIndex.findStopsByName('Moles'); // Partial name search
63
73
 
64
74
  Query stops by ID:
65
75
 
66
- `const stopFromId = stopsIndex.findStopById('8592374:0:A');`
76
+ `const stopFromId = stopsIndex.findStopBySourceId('8592374:0:A');`
67
77
 
68
78
  Or by location:
69
79
 
package/dist/cli.mjs CHANGED
@@ -16756,6 +16756,35 @@ function bytesToUint32Array(bytes) {
16756
16756
  }
16757
16757
  return result;
16758
16758
  }
16759
+ function uint16ArrayToBytes(array) {
16760
+ if (isLittleEndian === STANDARD_ENDIANNESS) {
16761
+ return new Uint8Array(array.buffer, array.byteOffset, array.byteLength);
16762
+ }
16763
+ // If endianness doesn't match, we need to swap byte order
16764
+ const result = new Uint8Array(array.length * 2);
16765
+ const view = new DataView(result.buffer);
16766
+ for (let i = 0; i < array.length; i++) {
16767
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
16768
+ view.setUint16(i * 2, array[i], STANDARD_ENDIANNESS);
16769
+ }
16770
+ return result;
16771
+ }
16772
+ function bytesToUint16Array(bytes) {
16773
+ if (bytes.byteLength % 2 !== 0) {
16774
+ throw new Error('Byte array length must be a multiple of 2 to convert to Uint16Array');
16775
+ }
16776
+ // If system endianness matches our standard, we can create a view directly
16777
+ if (isLittleEndian === STANDARD_ENDIANNESS) {
16778
+ return new Uint16Array(bytes.buffer, bytes.byteOffset, bytes.byteLength / 2);
16779
+ }
16780
+ // If endianness doesn't match, we need to swap byte order
16781
+ const result = new Uint16Array(bytes.byteLength / 2);
16782
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
16783
+ for (let i = 0; i < result.length; i++) {
16784
+ result[i] = view.getUint16(i * 2, STANDARD_ENDIANNESS);
16785
+ }
16786
+ return result;
16787
+ }
16759
16788
  const serializeStopsAdjacency = (stopsAdjacency) => {
16760
16789
  const protoStopsAdjacency = {
16761
16790
  stops: {},
@@ -16776,7 +16805,7 @@ const serializeRoutesAdjacency = (routesAdjacency) => {
16776
16805
  };
16777
16806
  routesAdjacency.forEach((value, key) => {
16778
16807
  protoRoutesAdjacency.routes[key] = {
16779
- stopTimes: uint32ArrayToBytes(value.stopTimes),
16808
+ stopTimes: uint16ArrayToBytes(value.stopTimes),
16780
16809
  pickUpDropOffTypes: value.pickUpDropOffTypes,
16781
16810
  stops: uint32ArrayToBytes(value.stops),
16782
16811
  serviceRouteId: value.serviceRouteId,
@@ -16819,7 +16848,7 @@ const deserializeRoutesAdjacency = (protoRoutesAdjacency) => {
16819
16848
  indices.set(stops[i], i);
16820
16849
  }
16821
16850
  routesAdjacency.set(key, {
16822
- stopTimes: bytesToUint32Array(value.stopTimes),
16851
+ stopTimes: bytesToUint16Array(value.stopTimes),
16823
16852
  pickUpDropOffTypes: value.pickUpDropOffTypes,
16824
16853
  stops: stops,
16825
16854
  stopIndices: indices,
@@ -16917,7 +16946,7 @@ const serializeRouteType = (type) => {
16917
16946
  };
16918
16947
 
16919
16948
  /**
16920
- * A class representing a time in hours, minutes, and seconds.
16949
+ * A class representing a time as minutes since midnight.
16921
16950
  */
16922
16951
  class Time {
16923
16952
  /**
@@ -16932,25 +16961,26 @@ class Time {
16932
16961
  /**
16933
16962
  * Gets the midnight time as a Time instance.
16934
16963
  *
16935
- * @returns A Time instance representing midnight (00:00:00).
16964
+ * @returns A Time instance representing midnight.
16936
16965
  */
16937
16966
  static origin() {
16938
16967
  return new Time(0);
16939
16968
  }
16940
- constructor(seconds) {
16941
- this.secondsSinceMidnight = seconds;
16969
+ constructor(minutes) {
16970
+ this.minutesSinceMidnight = minutes;
16942
16971
  }
16943
16972
  /**
16944
- * Creates a Time instance from the number of seconds since midnight.
16973
+ * Creates a Time instance from the number of minutes since midnight.
16945
16974
  *
16946
- * @param seconds - The number of seconds since midnight.
16975
+ * @param minutes - The number of minutes since midnight.
16947
16976
  * @returns A Time instance representing the specified time.
16948
16977
  */
16949
- static fromSeconds(seconds) {
16950
- return new Time(seconds);
16978
+ static fromMinutes(minutes) {
16979
+ return new Time(minutes);
16951
16980
  }
16952
16981
  /**
16953
16982
  * Creates a Time instance from hours, minutes, and seconds.
16983
+ * Rounds to the closest minute as times are represented in minutes from midnight.
16954
16984
  *
16955
16985
  * @param hours - The hours component of the time.
16956
16986
  * @param minutes - The minutes component of the time.
@@ -16965,7 +16995,22 @@ class Time {
16965
16995
  seconds >= 60) {
16966
16996
  throw new Error('Invalid time. Ensure hours, minutes, and seconds are valid values.');
16967
16997
  }
16968
- return new Time(seconds + 60 * minutes + 3600 * hours);
16998
+ const totalSeconds = seconds + 60 * minutes + 3600 * hours;
16999
+ const roundedMinutes = Math.round(totalSeconds / 60);
17000
+ return new Time(roundedMinutes);
17001
+ }
17002
+ /**
17003
+ * Creates a Time instance from hours, minutes.
17004
+ *
17005
+ * @param hours - The hours component of the time.
17006
+ * @param minutes - The minutes component of the time.
17007
+ * @returns A Time instance representing the specified time.
17008
+ */
17009
+ static fromHM(hours, minutes) {
17010
+ if (hours < 0 || minutes < 0 || minutes >= 60) {
17011
+ throw new Error('Invalid time. Ensure hours and minutes are valid values.');
17012
+ }
17013
+ return new Time(minutes + hours * 60);
16969
17014
  }
16970
17015
  /**
16971
17016
  * Parses a JavaScript Date object and creates a Time instance.
@@ -16977,7 +17022,7 @@ class Time {
16977
17022
  const hours = date.getHours();
16978
17023
  const minutes = date.getMinutes();
16979
17024
  const seconds = date.getSeconds();
16980
- return new Time(seconds + 60 * minutes + 3600 * hours);
17025
+ return Time.fromHMS(hours, minutes, seconds);
16981
17026
  }
16982
17027
  /**
16983
17028
  * Parses a time string in the format "HH:MM:SS" or "HH:MM" and creates a Time instance.
@@ -16997,7 +17042,7 @@ class Time {
16997
17042
  const hours = parseInt(hoursStr, 10);
16998
17043
  const minutes = parseInt(minutesStr, 10);
16999
17044
  const seconds = secondsStr !== undefined ? parseInt(secondsStr, 10) : 0;
17000
- return new Time(seconds + 60 * minutes + 3600 * hours);
17045
+ return Time.fromHMS(hours, minutes, seconds);
17001
17046
  }
17002
17047
  /**
17003
17048
  * Converts the Time instance to a string in "HH:MM:SS" format.
@@ -17005,20 +17050,17 @@ class Time {
17005
17050
  * @returns A string representing the time.
17006
17051
  */
17007
17052
  toString() {
17008
- const hours = Math.floor(this.secondsSinceMidnight / 3600);
17009
- const minutes = Math.floor((this.secondsSinceMidnight % 3600) / 60);
17010
- const seconds = this.secondsSinceMidnight % 60;
17011
- return `${hours.toString().padStart(2, '0')}:${minutes
17012
- .toString()
17013
- .padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
17053
+ const hours = Math.floor(this.minutesSinceMidnight / 60);
17054
+ const minutes = Math.floor(this.minutesSinceMidnight % 60);
17055
+ return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
17014
17056
  }
17015
17057
  /**
17016
- * Gets the time as the number of seconds since midnight.
17058
+ * Converts the Time instance to the total number of minutes since midnight, rounded to the closest minute.
17017
17059
  *
17018
- * @returns The time in seconds since midnight.
17060
+ * @returns The time in minutes since midnight.
17019
17061
  */
17020
- toSeconds() {
17021
- return this.secondsSinceMidnight;
17062
+ toMinutes() {
17063
+ return this.minutesSinceMidnight;
17022
17064
  }
17023
17065
  /**
17024
17066
  * Adds a Duration to the current Time instance and returns a new Time instance.
@@ -17027,8 +17069,8 @@ class Time {
17027
17069
  * @returns A new Time instance with the added duration.
17028
17070
  */
17029
17071
  plus(duration) {
17030
- const totalSeconds = this.secondsSinceMidnight + duration.toSeconds();
17031
- return new Time(totalSeconds);
17072
+ const totalSeconds = this.minutesSinceMidnight * 60 + duration.toSeconds();
17073
+ return new Time(Math.round(totalSeconds / 60));
17032
17074
  }
17033
17075
  /**
17034
17076
  * Subtracts a Duration from the current Time instance and returns a new Time instance.
@@ -17037,11 +17079,11 @@ class Time {
17037
17079
  * @returns A new Time instance with the subtracted duration.
17038
17080
  */
17039
17081
  minus(duration) {
17040
- let totalSeconds = this.secondsSinceMidnight - duration.toSeconds();
17082
+ let totalSeconds = this.minutesSinceMidnight * 60 - duration.toSeconds();
17041
17083
  if (totalSeconds < 0) {
17042
17084
  totalSeconds += 24 * 3600; // Adjust for negative time to loop back to previous day
17043
17085
  }
17044
- return new Time(totalSeconds);
17086
+ return new Time(Math.round(totalSeconds / 60));
17045
17087
  }
17046
17088
  /**
17047
17089
  * Subtracts another Time instance from the current Time instance and returns the Duration.
@@ -17050,8 +17092,8 @@ class Time {
17050
17092
  * @returns A Duration instance representing the time difference.
17051
17093
  */
17052
17094
  diff(otherTime) {
17053
- const totalSeconds = this.secondsSinceMidnight - otherTime.toSeconds();
17054
- return Duration.fromSeconds(Math.abs(totalSeconds));
17095
+ const totalMinutes = this.minutesSinceMidnight - otherTime.toMinutes();
17096
+ return Duration.fromSeconds(Math.abs(totalMinutes * 60));
17055
17097
  }
17056
17098
  /**
17057
17099
  * Computes the maximum Time instance among the provided Time instances.
@@ -17064,7 +17106,7 @@ class Time {
17064
17106
  throw new Error('At least one Time instance is required.');
17065
17107
  }
17066
17108
  return times.reduce((maxTime, currentTime) => {
17067
- return currentTime.toSeconds() > maxTime.toSeconds()
17109
+ return currentTime.toMinutes() > maxTime.toMinutes()
17068
17110
  ? currentTime
17069
17111
  : maxTime;
17070
17112
  });
@@ -17080,7 +17122,7 @@ class Time {
17080
17122
  throw new Error('At least one Time instance is required.');
17081
17123
  }
17082
17124
  return times.reduce((minTime, currentTime) => {
17083
- return currentTime.toSeconds() < minTime.toSeconds()
17125
+ return currentTime.toMinutes() < minTime.toMinutes()
17084
17126
  ? currentTime
17085
17127
  : minTime;
17086
17128
  });
@@ -17103,7 +17145,7 @@ const ALL_TRANSPORT_MODES = [
17103
17145
  'TROLLEYBUS',
17104
17146
  'MONORAIL',
17105
17147
  ];
17106
- const CURRENT_VERSION = '0.0.2';
17148
+ const CURRENT_VERSION = '0.0.3';
17107
17149
  /**
17108
17150
  * The internal transit timetable format
17109
17151
  * reuses some GTFS concepts for the sake of simplicity for now.
@@ -17225,7 +17267,7 @@ class Timetable {
17225
17267
  const stopTimeIndex = tripIndex * stopsNumber + stopIndex;
17226
17268
  const departure = route.stopTimes[stopTimeIndex * 2 + 1];
17227
17269
  const pickUpType = route.pickUpDropOffTypes[stopTimeIndex * 2];
17228
- if (departure >= after.toSeconds() && pickUpType !== NOT_AVAILABLE) {
17270
+ if (departure >= after.toMinutes() && pickUpType !== NOT_AVAILABLE) {
17229
17271
  return tripIndex;
17230
17272
  }
17231
17273
  }
@@ -17239,14 +17281,14 @@ class Timetable {
17239
17281
  const stopTimeIndex = tripIndex * stopsNumber + stopIndex;
17240
17282
  const departure = route.stopTimes[stopTimeIndex * 2 + 1];
17241
17283
  const pickUpType = route.pickUpDropOffTypes[stopTimeIndex * 2];
17242
- if (departure < after.toSeconds()) {
17284
+ if (departure < after.toMinutes()) {
17243
17285
  break;
17244
17286
  }
17245
17287
  if (pickUpType !== NOT_AVAILABLE &&
17246
17288
  (earliestDeparture === undefined ||
17247
- departure < earliestDeparture.toSeconds())) {
17289
+ departure < earliestDeparture.toMinutes())) {
17248
17290
  earliestTripIndex = tripIndex;
17249
- earliestDeparture = Time.fromSeconds(departure);
17291
+ earliestDeparture = Time.fromMinutes(departure);
17250
17292
  }
17251
17293
  }
17252
17294
  return earliestTripIndex;
@@ -19611,7 +19653,7 @@ const parseStopTimes = (stopTimesStream, stopsMap, validTripIds, validStopIds) =
19611
19653
  if (!route) {
19612
19654
  const stopsCount = stops.length;
19613
19655
  const stopsArray = new Uint32Array(stops);
19614
- const stopTimesArray = new Uint32Array(stopsCount * 2);
19656
+ const stopTimesArray = new Uint16Array(stopsCount * 2);
19615
19657
  for (let i = 0; i < stopsCount; i++) {
19616
19658
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
19617
19659
  stopTimesArray[i * 2] = arrivalTimes[i];
@@ -19657,7 +19699,7 @@ const parseStopTimes = (stopTimesStream, stopsMap, validTripIds, validStopIds) =
19657
19699
  }
19658
19700
  // insert data for the new trip at the right place
19659
19701
  const newStopTimesLength = route.stopTimes.length + stopsCount * 2;
19660
- const newStopTimes = new Uint32Array(newStopTimesLength);
19702
+ const newStopTimes = new Uint16Array(newStopTimesLength);
19661
19703
  const newPickUpDropOffTypes = new Uint8Array(newStopTimesLength);
19662
19704
  newStopTimes.set(route.stopTimes.slice(0, insertPosition * 2), 0);
19663
19705
  newPickUpDropOffTypes.set(route.pickUpDropOffTypes.slice(0, insertPosition * 2), 0);
@@ -19718,9 +19760,9 @@ const parseStopTimes = (stopTimesStream, stopsMap, validTripIds, validStopIds) =
19718
19760
  const departure = (_d = line.departure_time) !== null && _d !== void 0 ? _d : line.arrival_time;
19719
19761
  const arrival = (_e = line.arrival_time) !== null && _e !== void 0 ? _e : line.departure_time;
19720
19762
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
19721
- arrivalTimes.push(toTime(arrival).toSeconds());
19763
+ arrivalTimes.push(toTime(arrival).toMinutes());
19722
19764
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
19723
- departureTimes.push(toTime(departure).toSeconds());
19765
+ departureTimes.push(toTime(departure).toMinutes());
19724
19766
  pickUpTypes.push(parsePickupDropOffType(line.pickup_type));
19725
19767
  dropOffTypes.push(parsePickupDropOffType(line.drop_off_type));
19726
19768
  previousSeq = line.stop_sequence;
@@ -20099,7 +20141,7 @@ class Result {
20099
20141
  const arrivalTime = this.earliestArrivals.get(destination.id);
20100
20142
  if (arrivalTime !== undefined) {
20101
20143
  if (fastestTime === undefined ||
20102
- arrivalTime.time.toSeconds() < fastestTime.time.toSeconds()) {
20144
+ arrivalTime.time.toMinutes() < fastestTime.time.toMinutes()) {
20103
20145
  fastestDestination = destination.id;
20104
20146
  fastestTime = arrivalTime;
20105
20147
  }
@@ -20142,7 +20184,7 @@ class Result {
20142
20184
  const arrivalTime = relevantArrivals.get(equivalentStop.id);
20143
20185
  if (arrivalTime !== undefined) {
20144
20186
  if (earliestArrival === undefined ||
20145
- arrivalTime.time.toSeconds() < earliestArrival.time.toSeconds()) {
20187
+ arrivalTime.time.toMinutes() < earliestArrival.time.toMinutes()) {
20146
20188
  earliestArrival = arrivalTime;
20147
20189
  }
20148
20190
  }
@@ -20182,7 +20224,7 @@ class Router {
20182
20224
  .get(stop)
20183
20225
  .time.plus(transferTime);
20184
20226
  const originalArrival = (_b = (_a = arrivalsAtCurrentRound.get(transfer.destination)) === null || _a === undefined ? undefined : _a.time) !== null && _b !== undefined ? _b : UNREACHED;
20185
- if (arrivalAfterTransfer.toSeconds() < originalArrival.toSeconds()) {
20227
+ if (arrivalAfterTransfer.toMinutes() < originalArrival.toMinutes()) {
20186
20228
  const origin = (_d = (_c = arrivalsAtCurrentRound.get(stop)) === null || _c === undefined ? undefined : _c.origin) !== null && _d !== undefined ? _d : stop;
20187
20229
  arrivalsAtCurrentRound.set(transfer.destination, {
20188
20230
  time: arrivalAfterTransfer,
@@ -20260,7 +20302,7 @@ class Router {
20260
20302
  const stopNumbers = route.stops.length;
20261
20303
  if (currentTrip !== undefined) {
20262
20304
  const currentArrivalIndex = (currentTrip.trip * stopNumbers + i) * 2;
20263
- const currentArrivalTime = Time.fromSeconds(route.stopTimes[currentArrivalIndex]);
20305
+ const currentArrivalTime = Time.fromMinutes(route.stopTimes[currentArrivalIndex]);
20264
20306
  const currentDropOffType = route.pickUpDropOffTypes[i * 2 + 1];
20265
20307
  const earliestArrivalAtCurrentStop = (_b = (_a = earliestArrivals.get(currentStop)) === null || _a === undefined ? undefined : _a.time) !== null && _b !== undefined ? _b : UNREACHED;
20266
20308
  let arrivalToImprove = earliestArrivalAtCurrentStop;
@@ -20276,10 +20318,10 @@ class Router {
20276
20318
  arrivalToImprove = Time.min(earliestArrivalAtCurrentStop, earliestArrivalAtDestination);
20277
20319
  }
20278
20320
  if (currentDropOffType !== NOT_AVAILABLE &&
20279
- currentArrivalTime.toSeconds() < arrivalToImprove.toSeconds()) {
20321
+ currentArrivalTime.toMinutes() < arrivalToImprove.toMinutes()) {
20280
20322
  const bestHopOnStopIndex = route.stopIndices.get(currentTrip.bestHopOnStop);
20281
20323
  const bestHopOnStopDepartureIndex = currentTrip.trip * stopNumbers * 2 + bestHopOnStopIndex * 2 + 1;
20282
- const bestHopOnDepartureTime = Time.fromSeconds(route.stopTimes[bestHopOnStopDepartureIndex]);
20324
+ const bestHopOnDepartureTime = Time.fromMinutes(route.stopTimes[bestHopOnStopDepartureIndex]);
20283
20325
  arrivalsAtCurrentRound.set(currentStop, {
20284
20326
  time: currentArrivalTime,
20285
20327
  legNumber: round,
@@ -20305,7 +20347,7 @@ class Router {
20305
20347
  const earliestArrivalOnPreviousRound = (_e = arrivalsAtPreviousRound.get(currentStop)) === null || _e === undefined ? undefined : _e.time;
20306
20348
  if (earliestArrivalOnPreviousRound !== undefined &&
20307
20349
  (currentTrip === undefined ||
20308
- earliestArrivalOnPreviousRound.toSeconds() <=
20350
+ earliestArrivalOnPreviousRound.toMinutes() <=
20309
20351
  route.stopTimes[(currentTrip.trip * stopNumbers + i) * 2])) {
20310
20352
  const earliestTrip = this.timetable.findEarliestTrip(route, currentStop, currentTrip === null || currentTrip === undefined ? undefined : currentTrip.trip, earliestArrivalOnPreviousRound);
20311
20353
  if (earliestTrip !== undefined) {