@opentripplanner/core-utils 15.0.0-alpha.2 → 16.0.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 (80) hide show
  1. package/README.md +0 -3
  2. package/esm/__tests__/__mocks__/three-transfer-itinerary.json +2195 -0
  3. package/esm/index.js +3 -0
  4. package/esm/index.js.map +1 -1
  5. package/esm/itinerary.js +113 -109
  6. package/esm/itinerary.js.map +1 -1
  7. package/esm/map.js +2 -2
  8. package/esm/map.js.map +1 -1
  9. package/esm/otpSchema.json +1 -1
  10. package/esm/profile.js +1 -1
  11. package/esm/profile.js.map +1 -1
  12. package/esm/query-gen.js +10 -6
  13. package/esm/query-gen.js.map +1 -1
  14. package/esm/query.js +1 -4
  15. package/esm/query.js.map +1 -1
  16. package/esm/route.js +26 -20
  17. package/esm/route.js.map +1 -1
  18. package/esm/storage.js +6 -7
  19. package/esm/storage.js.map +1 -1
  20. package/esm/three-transfer-itinerary.json +2195 -0
  21. package/esm/time.js +6 -5
  22. package/esm/time.js.map +1 -1
  23. package/esm/ui.js +4 -2
  24. package/esm/ui.js.map +1 -1
  25. package/lib/__tests__/__mocks__/three-transfer-itinerary.json +2195 -0
  26. package/lib/index.d.ts.map +1 -1
  27. package/lib/index.js +40 -50
  28. package/lib/index.js.map +1 -1
  29. package/lib/itinerary.d.ts +21 -18
  30. package/lib/itinerary.d.ts.map +1 -1
  31. package/lib/itinerary.js +560 -540
  32. package/lib/itinerary.js.map +1 -1
  33. package/lib/map.d.ts +2 -2
  34. package/lib/map.d.ts.map +1 -1
  35. package/lib/map.js +39 -40
  36. package/lib/map.js.map +1 -1
  37. package/lib/otpSchema.json +1 -1
  38. package/lib/profile.js +1 -1
  39. package/lib/profile.js.map +1 -1
  40. package/lib/query-gen.d.ts +1 -19
  41. package/lib/query-gen.d.ts.map +1 -1
  42. package/lib/query-gen.js +142 -134
  43. package/lib/query-gen.js.map +1 -1
  44. package/lib/query.js +1 -4
  45. package/lib/query.js.map +1 -1
  46. package/lib/route.d.ts +10 -8
  47. package/lib/route.d.ts.map +1 -1
  48. package/lib/route.js +255 -231
  49. package/lib/route.js.map +1 -1
  50. package/lib/storage.d.ts +1 -1
  51. package/lib/storage.d.ts.map +1 -1
  52. package/lib/storage.js +28 -27
  53. package/lib/storage.js.map +1 -1
  54. package/lib/suspense.d.ts.map +1 -1
  55. package/lib/suspense.js +15 -28
  56. package/lib/suspense.js.map +1 -1
  57. package/lib/time.d.ts +3 -1
  58. package/lib/time.d.ts.map +1 -1
  59. package/lib/time.js +49 -36
  60. package/lib/time.js.map +1 -1
  61. package/lib/ui.d.ts.map +1 -1
  62. package/lib/ui.js +38 -33
  63. package/lib/ui.js.map +1 -1
  64. package/package.json +9 -7
  65. package/src/__snapshots__/core-utils.story.tsx.snap +1 -1
  66. package/src/__tests__/__mocks__/three-transfer-itinerary.json +2195 -0
  67. package/src/__tests__/itinerary.ts +97 -25
  68. package/src/index.ts +3 -0
  69. package/src/itinerary.ts +158 -126
  70. package/src/map.ts +5 -3
  71. package/src/otpSchema.json +1 -1
  72. package/src/profile.js +1 -1
  73. package/src/query-gen.ts +16 -10
  74. package/src/query.js +1 -4
  75. package/src/route.ts +65 -38
  76. package/src/storage.ts +13 -11
  77. package/src/time.ts +7 -6
  78. package/src/ui.ts +8 -6
  79. package/tsconfig.json +1 -0
  80. package/tsconfig.tsbuildinfo +1 -1
package/src/itinerary.ts CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  Currency,
7
7
  ElevationProfile,
8
8
  ElevationProfileComponent,
9
+ FareProduct,
9
10
  FlexBookingInfo,
10
11
  ItineraryOnlyLegsRequired,
11
12
  LatLngArray,
@@ -86,11 +87,12 @@ export function startsWithGeometry(leg: Leg): boolean {
86
87
  export function legContainsGeometry(leg: Leg): boolean {
87
88
  return endsWithGeometry(leg) || startsWithGeometry(leg);
88
89
  }
89
- export function isAdvanceBookingRequired(info: FlexBookingInfo): boolean {
90
- return info?.latestBookingTime?.daysPrior > 0;
90
+ export function isAdvanceBookingRequired(info?: FlexBookingInfo): boolean {
91
+ const daysPrior = info?.latestBookingTime?.daysPrior;
92
+ return typeof daysPrior === "number" && daysPrior > 0;
91
93
  }
92
94
  export function legDropoffRequiresAdvanceBooking(leg: Leg): boolean {
93
- return isAdvanceBookingRequired(leg?.dropOffBookingInfo);
95
+ return isAdvanceBookingRequired(leg.dropOffBookingInfo);
94
96
  }
95
97
 
96
98
  /**
@@ -106,8 +108,6 @@ export function isFlex(leg: Leg): boolean {
106
108
  ) || false
107
109
  );
108
110
  }
109
-
110
- // alpha-only comment
111
111
  export function isRideshareLeg(leg: Leg): boolean {
112
112
  return !!leg.rideHailingEstimate?.provider?.id;
113
113
  }
@@ -201,6 +201,7 @@ export function hasRental(modesStr: string): boolean {
201
201
  }
202
202
 
203
203
  export function getMapColor(mode: string): string {
204
+ // @ts-expect-error this is not typed
204
205
  mode = mode || this.get("mode");
205
206
  if (mode === "WALK") return "#444";
206
207
  if (mode === "BICYCLE") return "#0073e5";
@@ -226,7 +227,7 @@ export function toSentenceCase(str: string): string {
226
227
  /**
227
228
  * Derive the company string based on mode and network associated with leg.
228
229
  */
229
- export function getCompanyFromLeg(leg: Leg): string {
230
+ export function getCompanyFromLeg(leg?: Leg): string | null {
230
231
  if (!leg) return null;
231
232
  const {
232
233
  from,
@@ -236,14 +237,20 @@ export function getCompanyFromLeg(leg: Leg): string {
236
237
  rentedVehicle,
237
238
  rideHailingEstimate
238
239
  } = leg;
240
+
241
+ const firstNetwork =
242
+ Array.isArray(from.networks) && from.networks.length > 0
243
+ ? from.networks[0]
244
+ : null;
245
+
239
246
  if (mode === "CAR" && rentedCar) {
240
- return from.networks[0];
247
+ return firstNetwork;
241
248
  }
242
249
  if (mode === "CAR" && rideHailingEstimate) {
243
250
  return rideHailingEstimate.provider.id;
244
251
  }
245
- if (mode === "BICYCLE" && rentedBike && from.networks) {
246
- return from.networks[0];
252
+ if (mode === "BICYCLE" && rentedBike) {
253
+ return firstNetwork;
247
254
  }
248
255
  if (from.rentalVehicle) {
249
256
  return from.rentalVehicle.network;
@@ -251,12 +258,8 @@ export function getCompanyFromLeg(leg: Leg): string {
251
258
  if (from.vehicleRentalStation?.rentalNetwork) {
252
259
  return from.vehicleRentalStation.rentalNetwork.networkId;
253
260
  }
254
- if (
255
- (mode === "MICROMOBILITY" || mode === "SCOOTER") &&
256
- rentedVehicle &&
257
- from.networks
258
- ) {
259
- return from.networks[0];
261
+ if ((mode === "MICROMOBILITY" || mode === "SCOOTER") && rentedVehicle) {
262
+ return firstNetwork;
260
263
  }
261
264
  return null;
262
265
  }
@@ -264,12 +267,12 @@ export function getCompanyFromLeg(leg: Leg): string {
264
267
  export function getItineraryBounds(
265
268
  itinerary: ItineraryOnlyLegsRequired
266
269
  ): LatLngArray[] {
267
- let coords = [];
270
+ const coords: LatLngArray[] = [];
268
271
  itinerary.legs.forEach(leg => {
269
272
  const legCoords = polyline
270
273
  .toGeoJSON(leg.legGeometry.points)
271
- .coordinates.map((c: number[]) => [c[1], c[0]]);
272
- coords = [...coords, ...legCoords];
274
+ .coordinates.map((c): LatLngArray => [c[1], c[0]]);
275
+ coords.push(...legCoords);
273
276
  });
274
277
  return coords;
275
278
  }
@@ -292,62 +295,56 @@ export function getLegBounds(leg: Leg): number[][] {
292
295
  }
293
296
 
294
297
  /* Returns an interpolated lat-lon at a specified distance along a leg */
295
-
296
- export function legLocationAtDistance(leg: Leg, distance: number): number[] {
297
- if (!leg.legGeometry) return null;
298
+ export function legLocationAtDistance(
299
+ leg: Leg,
300
+ distance: number
301
+ ): LatLngArray | undefined | null {
302
+ if (!leg.legGeometry) return undefined;
298
303
 
299
304
  try {
300
305
  const line = polyline.toGeoJSON(leg.legGeometry.points);
301
306
  const pt = turfAlong(line, distance, { units: "meters" });
302
- if (pt && pt.geometry && pt.geometry.coordinates) {
303
- return [pt.geometry.coordinates[1], pt.geometry.coordinates[0]];
304
- }
305
- } catch (e) {
306
- // FIXME handle error!
307
+ const coords = pt?.geometry?.coordinates;
308
+ return [coords[1], coords[0]];
309
+ } catch {
310
+ // This is designed to catch the toGeoJSON from throwing if the geometry is not in the correct format
307
311
  }
308
312
 
309
313
  return null;
310
314
  }
311
315
 
312
- /* Returns an interpolated elevation at a specified distance along a leg */
316
+ /**
317
+ * Returns an interpolated elevation at a specified distance along a leg
318
+ * @param points - The points of the elevation profile. Each point is a tuple of [distance, elevation].
319
+ * @param distance - The distance along the leg to interpolate the elevation at
320
+ * @returns The interpolated elevation at the specified distance
321
+ */
313
322
 
314
323
  export function legElevationAtDistance(
315
- points: number[][],
324
+ points: [number, number][],
316
325
  distance: number
317
- ): number {
318
- // Iterate through the combined elevation profile
319
- let traversed = 0;
320
- // If first point distance is not zero, insert starting point at zero with
321
- // null elevation. Encountering this value should trigger the warning below.
322
- if (points[0][0] > 0) {
323
- points.unshift([0, null]);
324
- }
325
- for (let i = 1; i < points.length; i++) {
326
- const start = points[i - 1];
327
- const elevDistanceSpan = points[i][0] - start[0];
328
- if (distance >= traversed && distance <= traversed + elevDistanceSpan) {
329
- // Distance falls within this point and the previous one;
330
- // compute & return interpolated elevation value
331
- if (start[1] === null) {
332
- console.warn(
333
- "Elevation value does not exist for distance.",
334
- distance,
335
- traversed
336
- );
337
- return null;
338
- }
339
- const pct = (distance - traversed) / elevDistanceSpan;
340
- const elevSpan = points[i][1] - start[1];
341
- return start[1] + elevSpan * pct;
326
+ ): number | undefined {
327
+ const elevation = points.reduce<number | undefined>((acc, point, index) => {
328
+ const prevPoint = points[index - 1];
329
+ // at the first index there is no previous point
330
+ if (!prevPoint) return acc;
331
+ const [pointDistance, pointElevation] = point;
332
+ const [prevPointDistance, prevPointElevation] = prevPoint;
333
+ if (distance >= prevPointDistance && distance <= pointDistance) {
334
+ return (
335
+ prevPointElevation +
336
+ ((pointElevation - prevPointElevation) *
337
+ (distance - prevPointDistance)) /
338
+ (pointDistance - prevPointDistance)
339
+ );
342
340
  }
343
- traversed += elevDistanceSpan;
341
+ return acc;
342
+ }, undefined);
343
+ if (elevation === undefined) {
344
+ console.warn("Elevation value does not exist for distance.", distance);
345
+ return undefined;
344
346
  }
345
- console.warn(
346
- "Elevation value does not exist for distance.",
347
- distance,
348
- traversed
349
- );
350
- return null;
347
+ return elevation;
351
348
  }
352
349
 
353
350
  export function mapOldElevationComponentToNew(oldElev: {
@@ -372,7 +369,7 @@ export function getElevationProfile(
372
369
  let gain = 0;
373
370
  let loss = 0;
374
371
  let previous: ElevationProfileComponent | null = null;
375
- const points = [];
372
+ const points: [number, number][] = [];
376
373
  steps.forEach(step => {
377
374
  // Support for old REST response data (in step.elevation)
378
375
  const stepElevationProfile =
@@ -432,6 +429,7 @@ export function getTextWidth(text: string, font = "22px Arial"): number {
432
429
  (getTextWidth as GetTextWidth).canvas ||
433
430
  ((getTextWidth as GetTextWidth).canvas = document.createElement("canvas"));
434
431
  const context = canvas.getContext("2d");
432
+ if (!context) return 0;
435
433
  context.font = font;
436
434
  const metrics = context.measureText(text);
437
435
  return metrics.width;
@@ -444,7 +442,7 @@ export function getTextWidth(text: string, font = "22px Arial"): number {
444
442
  export function getCompanyForNetwork(
445
443
  networkString: string,
446
444
  companies: Company[] = []
447
- ): Company {
445
+ ): Company | undefined {
448
446
  const company = companies.find(co => co.id === networkString);
449
447
  if (!company) {
450
448
  console.warn(
@@ -475,7 +473,10 @@ export function getCompaniesLabelFromNetworks(
475
473
  .join("/");
476
474
  }
477
475
 
478
- export function getTNCLocation(leg: Leg, type: string): string {
476
+ export function getTNCLocation(
477
+ leg: Pick<Leg, "from" | "to">,
478
+ type: "from" | "to"
479
+ ): string {
479
480
  const location = leg[type];
480
481
  return `${location.lat.toFixed(5)},${location.lon.toFixed(5)}`;
481
482
  }
@@ -511,15 +512,21 @@ export function calculateTncFares(
511
512
  itinerary: ItineraryOnlyLegsRequired
512
513
  ): TncFare {
513
514
  return itinerary.legs
514
- .filter(leg => leg.mode === "CAR" && leg.rideHailingEstimate)
515
- .reduce(
516
- ({ maxTNCFare, minTNCFare }, { rideHailingEstimate }) => {
517
- const { minPrice, maxPrice } = rideHailingEstimate;
515
+ .filter(
516
+ (
517
+ leg
518
+ ): leg is Leg & {
519
+ rideHailingEstimate: NonNullable<Leg["rideHailingEstimate"]>;
520
+ } => leg.mode === "CAR" && leg.rideHailingEstimate !== undefined
521
+ )
522
+ .reduce<TncFare>(
523
+ (acc, leg) => {
524
+ const { minPrice, maxPrice } = leg.rideHailingEstimate;
518
525
  return {
519
526
  // Assumes a single currency for entire itinerary.
520
527
  currencyCode: minPrice.currency.code,
521
- maxTNCFare: maxTNCFare + maxPrice.amount,
522
- minTNCFare: minTNCFare + minPrice.amount
528
+ maxTNCFare: acc.maxTNCFare + maxPrice.amount,
529
+ minTNCFare: acc.minTNCFare + minPrice.amount
523
530
  };
524
531
  },
525
532
  {
@@ -537,7 +544,7 @@ export function calculateTncFares(
537
544
  * - https://www.itf-oecd.org/sites/default/files/life-cycle-assessment-calculations-2020.xlsx
538
545
  * Other values extrapolated.
539
546
  */
540
- const CARBON_INTENSITY_DEFAULTS = {
547
+ const CARBON_INTENSITY_DEFAULTS: Record<string, number> = {
541
548
  walk: 0.026,
542
549
  bicycle: 0.017,
543
550
  car: 0.162,
@@ -597,28 +604,30 @@ export function calculateEmissions(
597
604
  }
598
605
 
599
606
  /**
600
- * Returns the user-facing stop id to display for a stop or place, using the following priority:
601
- * 1. stop code,
602
- * 2. stop id without the agency id portion, if stop id contains an agency portion,
603
- * 3. stop id, whether null or not (this is the fallback case).
607
+ * Returns the user-facing stop code to display for a stop or place
604
608
  */
605
- export function getDisplayedStopId(placeOrStop: Place | Stop): string {
606
- let stopId;
607
- let stopCode;
609
+ export function getDisplayedStopCode(
610
+ placeOrStop: Place | Stop
611
+ ): string | undefined {
608
612
  if ("stopId" in placeOrStop) {
609
- ({ stopCode, stopId } = placeOrStop);
610
- } else if ("id" in placeOrStop) {
611
- ({ code: stopCode, id: stopId } = placeOrStop);
613
+ return placeOrStop.stopCode ?? undefined;
612
614
  }
613
- return stopCode || stopId?.split(":")[1] || stopId;
615
+ if ("id" in placeOrStop) {
616
+ return placeOrStop.code ?? undefined;
617
+ }
618
+ return undefined;
614
619
  }
615
620
 
616
621
  /**
617
- * Removes the first part of the OTP standard scope (":"), if it is present
622
+ * Removes the first part of the OTP standard scope (":"), if it is present.
618
623
  * @param item String that is potentially scoped with `:` character
619
624
  * @returns descoped string
620
625
  */
621
- export const descope = (item: string): string => item?.split(":")?.[1];
626
+ export const descope = (item?: string | null): string | null | undefined => {
627
+ if (!item) return item;
628
+ const index = item.indexOf(":");
629
+ return index === -1 ? item : item.substring(index + 1);
630
+ };
622
631
 
623
632
  export type ExtendedMoney = Money & { originalAmount?: number };
624
633
 
@@ -627,6 +636,8 @@ export const zeroDollars = (currency: Currency): Money => ({
627
636
  currency
628
637
  });
629
638
 
639
+ type FareProductWithPrice = FareProduct & { price: Money };
640
+
630
641
  /**
631
642
  * Extracts useful data from the fare products on a leg, such as the leg cost and transfer info.
632
643
  * @param leg Leg with Fares v2 information
@@ -640,9 +651,9 @@ export const zeroDollars = (currency: Currency): Money => ({
640
651
  */
641
652
  export function getLegCost(
642
653
  leg: Leg,
643
- mediumId: string | null,
644
- riderCategoryId: string | null,
645
- seenFareIds?: string[]
654
+ mediumId?: string | null,
655
+ riderCategoryId?: string | null,
656
+ seenFareIds?: string[] | null
646
657
  ): {
647
658
  alternateFareProducts?: AppliedFareProduct[];
648
659
  appliedFareProduct?: AppliedFareProduct;
@@ -664,6 +675,7 @@ export function getLegCost(
664
675
 
665
676
  const productMediaId =
666
677
  descope(product?.medium?.id) || product?.medium?.id || null;
678
+
667
679
  return (
668
680
  productRiderCategoryId === riderCategoryId &&
669
681
  productMediaId === mediumId &&
@@ -672,14 +684,22 @@ export function getLegCost(
672
684
  product?.price
673
685
  );
674
686
  })
687
+ // Make sure there's a price
688
+ // Some fare products don't have a price at all.
689
+ .filter(
690
+ (fare): fare is { id: string; product: FareProductWithPrice } =>
691
+ fare.product?.price !== undefined
692
+ )
675
693
  .map(fare => {
676
- const alreadySeen = seenFareIds?.indexOf(fare.id) > -1;
677
- const { currency } = fare.product.price;
694
+ const alreadySeen = !!seenFareIds && seenFareIds?.indexOf(fare.id) > -1;
678
695
  return {
679
696
  id: fare.id,
680
697
  product: {
681
698
  ...fare.product,
682
- legPrice: alreadySeen ? zeroDollars(currency) : fare.product.price
699
+ legPrice:
700
+ alreadySeen && fare.product.price
701
+ ? zeroDollars(fare.product.price.currency)
702
+ : fare.product.price
683
703
  } as AppliedFareProduct
684
704
  };
685
705
  })
@@ -711,38 +731,58 @@ export function getLegCost(
711
731
  */
712
732
  export function getItineraryCost(
713
733
  legs: Leg[],
714
- mediumId: string | string[] | null,
715
- riderCategoryId: string | string[] | null
734
+ mediumId?: string | (string | null)[] | null,
735
+ riderCategoryId?: string | (string | null)[] | null
716
736
  ): Money | undefined {
717
- // TODO: Better input type handling
718
737
  if (Array.isArray(mediumId) || Array.isArray(riderCategoryId)) {
719
- if (mediumId?.length !== riderCategoryId.length) {
720
- console.warn(
721
- "Invalid input types, only using first item. medium id list and rider category list must have same number of items"
738
+ // TODO: Better input type handling
739
+ if (Array.isArray(mediumId) && Array.isArray(riderCategoryId)) {
740
+ if (mediumId.length !== riderCategoryId.length) {
741
+ console.warn(
742
+ "Invalid input types, only using first item. medium id list and rider category list must have same number of items"
743
+ );
744
+ return getItineraryCost(legs, mediumId[0], riderCategoryId[0]);
745
+ }
746
+
747
+ const total = mediumId.reduce<Money>(
748
+ (acc, currentMediumId, index) => {
749
+ const newCost = getItineraryCost(
750
+ legs,
751
+ currentMediumId,
752
+ riderCategoryId[index]
753
+ );
754
+ if (!newCost) return acc;
755
+
756
+ return {
757
+ amount: acc.amount + (newCost.amount || 0),
758
+ currency:
759
+ acc.currency.code !== ""
760
+ ? acc.currency
761
+ : newCost.currency ?? acc.currency
762
+ };
763
+ },
764
+ { amount: 0, currency: { code: "", digits: 0 } }
722
765
  );
723
- return getItineraryCost(legs, mediumId[0], riderCategoryId[0]);
724
- }
725
766
 
726
- let total = { amount: 0, currency: null };
727
- for (let i = 0; i < mediumId.length; i++) {
728
- const newCost = getItineraryCost(legs, mediumId[i], riderCategoryId[i]);
729
- if (newCost) {
730
- total = {
731
- amount: total?.amount + (newCost?.amount || 0),
732
- currency: total.currency ?? newCost?.currency
733
- };
734
- }
767
+ if (!total.currency?.code) return undefined;
768
+ return total;
735
769
  }
736
- if (total.currency === null) return undefined;
737
- return total;
770
+ console.warn(
771
+ "Invalid input types, only using first item. medium id list and rider category list must have same number of items"
772
+ );
773
+ return undefined;
738
774
  }
739
775
 
740
776
  const legCosts = legs
741
777
  // Only legs with fares (no walking legs)
742
- .filter(leg => leg.fareProducts?.length > 0)
778
+ .filter(leg => leg.fareProducts?.length && leg.fareProducts.length > 0)
743
779
  // Get the leg cost object of each leg
744
780
  .reduce<{ seenIds: string[]; legCosts: AppliedFareProduct[] }>(
745
781
  (acc, leg) => {
782
+ // getLegCost handles filtering out duplicate use IDs
783
+ // One fare product can be used on multiple legs,
784
+ // and we don't want to count it more than once.
785
+ // Use an object keyed by productUseId to deduplicate, then extract prices
746
786
  const { appliedFareProduct, productUseId } = getLegCost(
747
787
  leg,
748
788
  mediumId,
@@ -750,6 +790,7 @@ export function getItineraryCost(
750
790
  acc.seenIds
751
791
  );
752
792
  if (!appliedFareProduct) return acc;
793
+ if (!productUseId) return acc;
753
794
  return {
754
795
  legCosts: [...acc.legCosts, appliedFareProduct],
755
796
  seenIds: [...acc.seenIds, productUseId]
@@ -758,29 +799,20 @@ export function getItineraryCost(
758
799
  { seenIds: [], legCosts: [] }
759
800
  )
760
801
  .legCosts.map(lc => lc.legPrice);
761
- // Filter out duplicate use IDs
762
- // One fare product can be used on multiple legs,
763
- // and we don't want to count it more than once.
764
- // Use an object keyed by productUseId to deduplicate, then extract prices
765
- // .reduce<{ [productUseId: string]: Money }>((acc, cur) => {
766
- // if (cur.productUseId && acc[cur.productUseId] === undefined) {
767
- // acc[cur.productUseId] = cur.appliedFareProduct?.legPrice;
768
- // }
769
- // return acc;
770
- // }, {});
771
802
 
772
803
  if (legCosts.length === 0) return undefined;
773
804
  // Calculate the total
774
805
  return legCosts.reduce<Money>(
775
806
  (prev, cur) => ({
776
807
  amount: prev.amount + cur?.amount || 0,
777
- currency: prev.currency ?? cur?.currency
808
+ currency: prev.currency.code !== "" ? prev.currency : cur.currency
778
809
  }),
779
- { amount: 0, currency: null }
810
+ // eslint-disable-next-line prettier/prettier -- old eslint doesn't know satisfies
811
+ { amount: 0, currency: { code: "", digits: 0 } satisfies Currency }
780
812
  );
781
813
  }
782
814
 
783
- const pickupDropoffTypeToOtp1 = otp2Type => {
815
+ const pickupDropoffTypeToOtp1 = (otp2Type: string): string | null => {
784
816
  switch (otp2Type) {
785
817
  case "COORDINATE_WITH_DRIVER":
786
818
  return "coordinateWithDriver";
@@ -795,7 +827,7 @@ const pickupDropoffTypeToOtp1 = otp2Type => {
795
827
  }
796
828
  };
797
829
 
798
- export const convertGraphQLResponseToLegacy = (leg: any): any => ({
830
+ export const convertGraphQLResponseToLegacy = (leg: any): Leg => ({
799
831
  ...leg,
800
832
  agencyBrandingUrl: leg.agency?.url,
801
833
  agencyId: leg.agency?.id,
@@ -805,7 +837,7 @@ export const convertGraphQLResponseToLegacy = (leg: any): any => ({
805
837
  boardRule: pickupDropoffTypeToOtp1(leg.pickupType),
806
838
  bookingRuleInfo: {
807
839
  dropOff: leg?.dropOffBookingInfo || {},
808
- pickUp: leg?.pickUpBookingInfo || {}
840
+ pickUp: leg?.pickupBookingInfo || {}
809
841
  },
810
842
  dropOffBookingInfo: {
811
843
  latestBookingTime: leg.dropOffBookingInfo
@@ -844,7 +876,7 @@ export const getLegRouteShortName = (
844
876
  /** Extract the route long name for a leg returned from OTP1 or OTP2. */
845
877
  export const getLegRouteLongName = (
846
878
  leg: Pick<Leg, "route" | "routeLongName">
847
- ): string | null => {
879
+ ): string | undefined => {
848
880
  const { route, routeLongName } = leg;
849
881
  // typeof route === "object" denotes newer OTP2 responses. routeLongName is OTP1.
850
882
  return typeof route === "object" ? route?.longName : routeLongName;
@@ -856,6 +888,6 @@ export const getLegRouteLongName = (
856
888
  */
857
889
  export const getLegRouteName = (
858
890
  leg: Pick<Leg, "route" | "routeLongName" | "routeShortName">
859
- ): string => {
891
+ ): string | undefined => {
860
892
  return getLegRouteShortName(leg) || getLegRouteLongName(leg);
861
893
  };
package/src/map.ts CHANGED
@@ -2,7 +2,7 @@ import { LatLngArray, Location, UserPosition } from "@opentripplanner/types";
2
2
 
3
3
  export function currentPositionToLocation(
4
4
  currentPosition: UserPosition
5
- ): Location {
5
+ ): Location | null {
6
6
  if (currentPosition.error || !currentPosition.coords) {
7
7
  console.warn(
8
8
  "Cannot construct location from current position due to geolocation error or missing coordinates."
@@ -18,8 +18,10 @@ export function currentPositionToLocation(
18
18
 
19
19
  // TRICKY: This method is used in query.js and in the context of
20
20
  // otp-rr actions where the intl context is not available/does not apply.
21
- export function coordsToString(coords: number[]): string {
22
- return coords.length && coords.map(c => (+c).toFixed(5)).join(", ");
21
+ export function coordsToString(coords: number[]): string | undefined {
22
+ return coords.length > 0
23
+ ? coords.map(c => (+c).toFixed(5)).join(", ")
24
+ : undefined;
23
25
  }
24
26
 
25
27
  export function stringToCoords(str: string): number[] {
@@ -17763,4 +17763,4 @@
17763
17763
  ]
17764
17764
  }
17765
17765
  }
17766
- }
17766
+ }
package/src/profile.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export function filterProfileOptions(response) {
2
- // Filter out similar options. TODO: handle on server??
2
+ // Filter out similar options. TODO: handle on server?
3
3
  const optStrs = [];
4
4
  const filteredIndices = [];
5
5
 
package/src/query-gen.ts CHANGED
@@ -58,7 +58,7 @@ export function extractAdditionalModes(
58
58
  return prev;
59
59
  }
60
60
 
61
- // In checkboxes (or submode checkboxes), mode must be enabled and have a transport mode in it
61
+ // In checkboxes, mode must be enabled and have a transport mode in it
62
62
  if (
63
63
  (cur.type === "CHECKBOX" || cur.type === "SUBMODE") &&
64
64
  cur.addTransportMode &&
@@ -102,7 +102,7 @@ function combinations(array: TransportMode[]): TransportMode[][] {
102
102
  * This constant maps all the transport mode to a broader mode type,
103
103
  * which is used to determine the valid combinations of modes used in query generation.
104
104
  */
105
- export const SIMPLIFICATIONS = {
105
+ export const SIMPLIFICATIONS: Record<string, string> = {
106
106
  AIRPLANE: "TRANSIT",
107
107
  BICYCLE: "PERSONAL",
108
108
  BUS: "TRANSIT",
@@ -123,7 +123,7 @@ export const SIMPLIFICATIONS = {
123
123
  };
124
124
 
125
125
  // Inclusion of "TRANSIT" alone automatically implies "WALK" in OTP
126
- const VALID_COMBOS = [
126
+ const VALID_COMBOS: string[][] = [
127
127
  ["WALK"],
128
128
  ["PERSONAL"],
129
129
  ["TRANSIT", "SHARED"],
@@ -220,19 +220,25 @@ export function generateOtp2Query(
220
220
  const { from, modeSettings, to, ...otherOtpQueryParams } = otpQueryParams;
221
221
 
222
222
  // This extracts the values from the mode settings to key value pairs
223
- const modeSettingValues = modeSettings.reduce((prev, cur) => {
224
- if (cur.type === "SLIDER" && cur.inverseKey) {
223
+ const modeSettingValues = modeSettings.reduce<
224
+ Record<string, string | number | boolean>
225
+ >((prev, cur) => {
226
+ if (cur.type === "SLIDER" && cur.inverseKey && cur.value) {
225
227
  prev[cur.inverseKey] = cur.high - cur.value + cur.low;
228
+ } else if (cur.value) {
229
+ prev[cur.key] = cur.value;
226
230
  }
227
- prev[cur.key] = cur.value;
228
231
 
229
232
  // If we assign a value on true, return the value (or null) instead of a boolean.
230
- if (cur.type === "CHECKBOX" && cur.truthValue) {
231
- prev[cur.key] =
232
- cur.value === true ? cur.truthValue : cur.falseValue ?? null;
233
+ if (cur.type === "CHECKBOX" && cur.truthValue && cur.falseValue) {
234
+ const newVal = cur.value === true ? cur.truthValue : cur.falseValue;
235
+ if (newVal) {
236
+ prev[cur.key] = newVal;
237
+ }
233
238
  }
234
239
  return prev;
235
- }, {}) as ModeSettingValues;
240
+ // eslint-disable-next-line prettier/prettier -- old eslint doesn't know satisfies
241
+ }, {}) satisfies ModeSettingValues;
236
242
 
237
243
  const {
238
244
  bikeReluctance,
package/src/query.js CHANGED
@@ -72,10 +72,7 @@ export function ensureSingleAccessMode(queryModes) {
72
72
  }
73
73
 
74
74
  export function getUrlParams() {
75
- if (window) {
76
- return qs.parse(window.location.href.split("?")[1]);
77
- }
78
- return undefined;
75
+ return qs.parse(window.location.href.split("?")[1]);
79
76
  }
80
77
 
81
78
  export function getOtpUrlParams() {