@opentripplanner/core-utils 15.0.0 → 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 (58) hide show
  1. package/esm/index.js +3 -0
  2. package/esm/index.js.map +1 -1
  3. package/esm/itinerary.js +95 -78
  4. package/esm/itinerary.js.map +1 -1
  5. package/esm/map.js +2 -2
  6. package/esm/map.js.map +1 -1
  7. package/esm/query-gen.js +9 -5
  8. package/esm/query-gen.js.map +1 -1
  9. package/esm/route.js +26 -20
  10. package/esm/route.js.map +1 -1
  11. package/esm/storage.js +4 -1
  12. package/esm/storage.js.map +1 -1
  13. package/esm/time.js +6 -5
  14. package/esm/time.js.map +1 -1
  15. package/esm/ui.js +4 -2
  16. package/esm/ui.js.map +1 -1
  17. package/lib/index.d.ts.map +1 -1
  18. package/lib/index.js +6 -0
  19. package/lib/index.js.map +1 -1
  20. package/lib/itinerary.d.ts +17 -11
  21. package/lib/itinerary.d.ts.map +1 -1
  22. package/lib/itinerary.js +89 -84
  23. package/lib/itinerary.js.map +1 -1
  24. package/lib/map.d.ts +2 -2
  25. package/lib/map.d.ts.map +1 -1
  26. package/lib/map.js +1 -1
  27. package/lib/map.js.map +1 -1
  28. package/lib/query-gen.d.ts +1 -19
  29. package/lib/query-gen.d.ts.map +1 -1
  30. package/lib/query-gen.js +9 -5
  31. package/lib/query-gen.js.map +1 -1
  32. package/lib/route.d.ts +10 -8
  33. package/lib/route.d.ts.map +1 -1
  34. package/lib/route.js +22 -16
  35. package/lib/route.js.map +1 -1
  36. package/lib/storage.d.ts +1 -1
  37. package/lib/storage.d.ts.map +1 -1
  38. package/lib/storage.js +4 -1
  39. package/lib/storage.js.map +1 -1
  40. package/lib/time.d.ts +3 -1
  41. package/lib/time.d.ts.map +1 -1
  42. package/lib/time.js +5 -4
  43. package/lib/time.js.map +1 -1
  44. package/lib/ui.d.ts.map +1 -1
  45. package/lib/ui.js +4 -2
  46. package/lib/ui.js.map +1 -1
  47. package/package.json +9 -7
  48. package/src/__tests__/itinerary.ts +55 -5
  49. package/src/index.ts +3 -0
  50. package/src/itinerary.ts +134 -97
  51. package/src/map.ts +5 -3
  52. package/src/query-gen.ts +15 -9
  53. package/src/route.ts +65 -38
  54. package/src/storage.ts +8 -2
  55. package/src/time.ts +7 -6
  56. package/src/ui.ts +8 -6
  57. package/tsconfig.json +1 -0
  58. package/tsconfig.tsbuildinfo +1 -1
@@ -12,6 +12,7 @@ import {
12
12
  getLegRouteShortName,
13
13
  isFlex,
14
14
  isTransit,
15
+ legElevationAtDistance,
15
16
  mapOldElevationComponentToNew
16
17
  } from "../itinerary";
17
18
 
@@ -261,21 +262,21 @@ describe("util > itinerary", () => {
261
262
  "cash",
262
263
  "regular"
263
264
  );
264
- expect(result.amount).toEqual(5.75);
265
- expect(result.currency).toEqual({
265
+ expect(result?.amount).toEqual(5.75);
266
+ expect(result?.currency).toEqual({
266
267
  code: "USD",
267
268
  digits: 2
268
269
  });
269
270
  });
270
271
  it("should calculate the total cost of an itinerary using fares v2", () => {
271
272
  const result = getItineraryCost(faresv2Itinerary.legs, "3", "ADULT");
272
- expect(result.amount).toEqual(2.8);
273
+ expect(result?.amount).toEqual(2.8);
273
274
  const complexResult = getItineraryCost(
274
275
  faresv2Itinerary.legs,
275
276
  ["0", "0"],
276
277
  ["ADULT", "ADULT"]
277
278
  );
278
- expect(complexResult.amount).toEqual(2.8 * 2);
279
+ expect(complexResult?.amount).toEqual(2.8 * 2);
279
280
  });
280
281
  it("should calculate the total cost of an itinerary with multiple v2 fares & transfers", () => {
281
282
  const result = getItineraryCost(complexItinerary.legs, "0", "ADULT");
@@ -287,7 +288,7 @@ describe("util > itinerary", () => {
287
288
  ["0", "0"],
288
289
  ["ADULT", null]
289
290
  );
290
- expect(result.amount).toEqual(11.55);
291
+ expect(result?.amount).toEqual(11.55);
291
292
  });
292
293
  it("should calculate the individual leg cost of a fares v2 legs", () => {
293
294
  const firstLegResult = getLegCost(faresv2Itinerary.legs[1], "3", "ADULT");
@@ -404,4 +405,53 @@ describe("util > itinerary", () => {
404
405
  ).toBe("15");
405
406
  });
406
407
  });
408
+
409
+ describe("legElevationAtDistance", () => {
410
+ it("should interpolate elevation within a segment", () => {
411
+ const points: any = [
412
+ [0, 100],
413
+ [10, 110],
414
+ [20, 120]
415
+ ];
416
+ // halfway between 100 and 110
417
+ expect(legElevationAtDistance(points, 5)).toBe(105);
418
+ expect(legElevationAtDistance(points, 15)).toBe(115);
419
+ });
420
+
421
+ it("should return start elevation at distance 0", () => {
422
+ const points: any = [
423
+ [0, 42],
424
+ [10, 52]
425
+ ];
426
+ expect(legElevationAtDistance(points, 0)).toBe(42);
427
+ });
428
+
429
+ it("should return end elevation at the segment boundary", () => {
430
+ const points: any = [
431
+ [0, 100],
432
+ [10, 110],
433
+ [20, 120]
434
+ ];
435
+ expect(legElevationAtDistance(points, 10)).toBe(110);
436
+ expect(legElevationAtDistance(points, 20)).toBe(120);
437
+ });
438
+
439
+ it("should return undefined when distance is before the profile", () => {
440
+ const points: any = [
441
+ [0, 100],
442
+ [10, 110],
443
+ [20, 120]
444
+ ];
445
+ expect(legElevationAtDistance(points, -1)).toBeUndefined();
446
+ expect(legElevationAtDistance(points, 21)).toBeUndefined();
447
+ });
448
+
449
+ it("should return undefined when first point is not at zero", () => {
450
+ const points: any = [
451
+ [5, 100],
452
+ [15, 110]
453
+ ];
454
+ expect(legElevationAtDistance(points, 4)).toBeUndefined();
455
+ });
456
+ });
407
457
  });
package/src/index.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  import * as itinerary from "./itinerary";
2
2
  import * as map from "./map";
3
+ // @ts-expect-error not typed
3
4
  import * as profile from "./profile";
5
+ // @ts-expect-error not typed
4
6
  import * as query from "./query";
7
+ // @ts-expect-error not typed
5
8
  import * as queryParams from "./query-params";
6
9
  import * as route from "./route";
7
10
  import * as storage from "./storage";
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
  /**
@@ -199,6 +201,7 @@ export function hasRental(modesStr: string): boolean {
199
201
  }
200
202
 
201
203
  export function getMapColor(mode: string): string {
204
+ // @ts-expect-error this is not typed
202
205
  mode = mode || this.get("mode");
203
206
  if (mode === "WALK") return "#444";
204
207
  if (mode === "BICYCLE") return "#0073e5";
@@ -224,7 +227,7 @@ export function toSentenceCase(str: string): string {
224
227
  /**
225
228
  * Derive the company string based on mode and network associated with leg.
226
229
  */
227
- export function getCompanyFromLeg(leg: Leg): string {
230
+ export function getCompanyFromLeg(leg?: Leg): string | null {
228
231
  if (!leg) return null;
229
232
  const {
230
233
  from,
@@ -234,14 +237,20 @@ export function getCompanyFromLeg(leg: Leg): string {
234
237
  rentedVehicle,
235
238
  rideHailingEstimate
236
239
  } = leg;
240
+
241
+ const firstNetwork =
242
+ Array.isArray(from.networks) && from.networks.length > 0
243
+ ? from.networks[0]
244
+ : null;
245
+
237
246
  if (mode === "CAR" && rentedCar) {
238
- return from.networks[0];
247
+ return firstNetwork;
239
248
  }
240
249
  if (mode === "CAR" && rideHailingEstimate) {
241
250
  return rideHailingEstimate.provider.id;
242
251
  }
243
- if (mode === "BICYCLE" && rentedBike && from.networks) {
244
- return from.networks[0];
252
+ if (mode === "BICYCLE" && rentedBike) {
253
+ return firstNetwork;
245
254
  }
246
255
  if (from.rentalVehicle) {
247
256
  return from.rentalVehicle.network;
@@ -249,12 +258,8 @@ export function getCompanyFromLeg(leg: Leg): string {
249
258
  if (from.vehicleRentalStation?.rentalNetwork) {
250
259
  return from.vehicleRentalStation.rentalNetwork.networkId;
251
260
  }
252
- if (
253
- (mode === "MICROMOBILITY" || mode === "SCOOTER") &&
254
- rentedVehicle &&
255
- from.networks
256
- ) {
257
- return from.networks[0];
261
+ if ((mode === "MICROMOBILITY" || mode === "SCOOTER") && rentedVehicle) {
262
+ return firstNetwork;
258
263
  }
259
264
  return null;
260
265
  }
@@ -262,12 +267,12 @@ export function getCompanyFromLeg(leg: Leg): string {
262
267
  export function getItineraryBounds(
263
268
  itinerary: ItineraryOnlyLegsRequired
264
269
  ): LatLngArray[] {
265
- let coords = [];
270
+ const coords: LatLngArray[] = [];
266
271
  itinerary.legs.forEach(leg => {
267
272
  const legCoords = polyline
268
273
  .toGeoJSON(leg.legGeometry.points)
269
- .coordinates.map((c: number[]) => [c[1], c[0]]);
270
- coords = [...coords, ...legCoords];
274
+ .coordinates.map((c): LatLngArray => [c[1], c[0]]);
275
+ coords.push(...legCoords);
271
276
  });
272
277
  return coords;
273
278
  }
@@ -290,62 +295,56 @@ export function getLegBounds(leg: Leg): number[][] {
290
295
  }
291
296
 
292
297
  /* Returns an interpolated lat-lon at a specified distance along a leg */
293
-
294
- export function legLocationAtDistance(leg: Leg, distance: number): number[] {
295
- 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;
296
303
 
297
304
  try {
298
305
  const line = polyline.toGeoJSON(leg.legGeometry.points);
299
306
  const pt = turfAlong(line, distance, { units: "meters" });
300
- if (pt && pt.geometry && pt.geometry.coordinates) {
301
- return [pt.geometry.coordinates[1], pt.geometry.coordinates[0]];
302
- }
303
- } catch (e) {
304
- // 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
305
311
  }
306
312
 
307
313
  return null;
308
314
  }
309
315
 
310
- /* 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
+ */
311
322
 
312
323
  export function legElevationAtDistance(
313
- points: number[][],
324
+ points: [number, number][],
314
325
  distance: number
315
- ): number {
316
- // Iterate through the combined elevation profile
317
- let traversed = 0;
318
- // If first point distance is not zero, insert starting point at zero with
319
- // null elevation. Encountering this value should trigger the warning below.
320
- if (points[0][0] > 0) {
321
- points.unshift([0, null]);
322
- }
323
- for (let i = 1; i < points.length; i++) {
324
- const start = points[i - 1];
325
- const elevDistanceSpan = points[i][0] - start[0];
326
- if (distance >= traversed && distance <= traversed + elevDistanceSpan) {
327
- // Distance falls within this point and the previous one;
328
- // compute & return interpolated elevation value
329
- if (start[1] === null) {
330
- console.warn(
331
- "Elevation value does not exist for distance.",
332
- distance,
333
- traversed
334
- );
335
- return null;
336
- }
337
- const pct = (distance - traversed) / elevDistanceSpan;
338
- const elevSpan = points[i][1] - start[1];
339
- 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
+ );
340
340
  }
341
- 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;
342
346
  }
343
- console.warn(
344
- "Elevation value does not exist for distance.",
345
- distance,
346
- traversed
347
- );
348
- return null;
347
+ return elevation;
349
348
  }
350
349
 
351
350
  export function mapOldElevationComponentToNew(oldElev: {
@@ -370,7 +369,7 @@ export function getElevationProfile(
370
369
  let gain = 0;
371
370
  let loss = 0;
372
371
  let previous: ElevationProfileComponent | null = null;
373
- const points = [];
372
+ const points: [number, number][] = [];
374
373
  steps.forEach(step => {
375
374
  // Support for old REST response data (in step.elevation)
376
375
  const stepElevationProfile =
@@ -430,6 +429,7 @@ export function getTextWidth(text: string, font = "22px Arial"): number {
430
429
  (getTextWidth as GetTextWidth).canvas ||
431
430
  ((getTextWidth as GetTextWidth).canvas = document.createElement("canvas"));
432
431
  const context = canvas.getContext("2d");
432
+ if (!context) return 0;
433
433
  context.font = font;
434
434
  const metrics = context.measureText(text);
435
435
  return metrics.width;
@@ -442,7 +442,7 @@ export function getTextWidth(text: string, font = "22px Arial"): number {
442
442
  export function getCompanyForNetwork(
443
443
  networkString: string,
444
444
  companies: Company[] = []
445
- ): Company {
445
+ ): Company | undefined {
446
446
  const company = companies.find(co => co.id === networkString);
447
447
  if (!company) {
448
448
  console.warn(
@@ -473,7 +473,10 @@ export function getCompaniesLabelFromNetworks(
473
473
  .join("/");
474
474
  }
475
475
 
476
- export function getTNCLocation(leg: Leg, type: string): string {
476
+ export function getTNCLocation(
477
+ leg: Pick<Leg, "from" | "to">,
478
+ type: "from" | "to"
479
+ ): string {
477
480
  const location = leg[type];
478
481
  return `${location.lat.toFixed(5)},${location.lon.toFixed(5)}`;
479
482
  }
@@ -509,15 +512,21 @@ export function calculateTncFares(
509
512
  itinerary: ItineraryOnlyLegsRequired
510
513
  ): TncFare {
511
514
  return itinerary.legs
512
- .filter(leg => leg.mode === "CAR" && leg.rideHailingEstimate)
513
- .reduce(
514
- ({ maxTNCFare, minTNCFare }, { rideHailingEstimate }) => {
515
- 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;
516
525
  return {
517
526
  // Assumes a single currency for entire itinerary.
518
527
  currencyCode: minPrice.currency.code,
519
- maxTNCFare: maxTNCFare + maxPrice.amount,
520
- minTNCFare: minTNCFare + minPrice.amount
528
+ maxTNCFare: acc.maxTNCFare + maxPrice.amount,
529
+ minTNCFare: acc.minTNCFare + minPrice.amount
521
530
  };
522
531
  },
523
532
  {
@@ -535,7 +544,7 @@ export function calculateTncFares(
535
544
  * - https://www.itf-oecd.org/sites/default/files/life-cycle-assessment-calculations-2020.xlsx
536
545
  * Other values extrapolated.
537
546
  */
538
- const CARBON_INTENSITY_DEFAULTS = {
547
+ const CARBON_INTENSITY_DEFAULTS: Record<string, number> = {
539
548
  walk: 0.026,
540
549
  bicycle: 0.017,
541
550
  car: 0.162,
@@ -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
@@ -642,7 +653,7 @@ export function getLegCost(
642
653
  leg: Leg,
643
654
  mediumId?: string | null,
644
655
  riderCategoryId?: string | null,
645
- seenFareIds?: string[]
656
+ seenFareIds?: string[] | null
646
657
  ): {
647
658
  alternateFareProducts?: AppliedFareProduct[];
648
659
  appliedFareProduct?: AppliedFareProduct;
@@ -673,14 +684,22 @@ export function getLegCost(
673
684
  product?.price
674
685
  );
675
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
+ )
676
693
  .map(fare => {
677
- const alreadySeen = seenFareIds?.indexOf(fare.id) > -1;
678
- const { currency } = fare.product.price;
694
+ const alreadySeen = !!seenFareIds && seenFareIds?.indexOf(fare.id) > -1;
679
695
  return {
680
696
  id: fare.id,
681
697
  product: {
682
698
  ...fare.product,
683
- legPrice: alreadySeen ? zeroDollars(currency) : fare.product.price
699
+ legPrice:
700
+ alreadySeen && fare.product.price
701
+ ? zeroDollars(fare.product.price.currency)
702
+ : fare.product.price
684
703
  } as AppliedFareProduct
685
704
  };
686
705
  })
@@ -712,35 +731,51 @@ export function getLegCost(
712
731
  */
713
732
  export function getItineraryCost(
714
733
  legs: Leg[],
715
- mediumId?: string | string[] | null,
716
- riderCategoryId?: string | string[] | null
734
+ mediumId?: string | (string | null)[] | null,
735
+ riderCategoryId?: string | (string | null)[] | null
717
736
  ): Money | undefined {
718
- // TODO: Better input type handling
719
737
  if (Array.isArray(mediumId) || Array.isArray(riderCategoryId)) {
720
- if (mediumId?.length !== riderCategoryId.length) {
721
- console.warn(
722
- "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 } }
723
765
  );
724
- return getItineraryCost(legs, mediumId[0], riderCategoryId[0]);
725
- }
726
766
 
727
- let total = { amount: 0, currency: null };
728
- for (let i = 0; i < mediumId.length; i++) {
729
- const newCost = getItineraryCost(legs, mediumId[i], riderCategoryId[i]);
730
- if (newCost) {
731
- total = {
732
- amount: total?.amount + (newCost?.amount || 0),
733
- currency: total.currency ?? newCost?.currency
734
- };
735
- }
767
+ if (!total.currency?.code) return undefined;
768
+ return total;
736
769
  }
737
- if (total.currency === null) return undefined;
738
- 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;
739
774
  }
740
775
 
741
776
  const legCosts = legs
742
777
  // Only legs with fares (no walking legs)
743
- .filter(leg => leg.fareProducts?.length > 0)
778
+ .filter(leg => leg.fareProducts?.length && leg.fareProducts.length > 0)
744
779
  // Get the leg cost object of each leg
745
780
  .reduce<{ seenIds: string[]; legCosts: AppliedFareProduct[] }>(
746
781
  (acc, leg) => {
@@ -755,6 +790,7 @@ export function getItineraryCost(
755
790
  acc.seenIds
756
791
  );
757
792
  if (!appliedFareProduct) return acc;
793
+ if (!productUseId) return acc;
758
794
  return {
759
795
  legCosts: [...acc.legCosts, appliedFareProduct],
760
796
  seenIds: [...acc.seenIds, productUseId]
@@ -769,9 +805,10 @@ export function getItineraryCost(
769
805
  return legCosts.reduce<Money>(
770
806
  (prev, cur) => ({
771
807
  amount: prev.amount + cur?.amount || 0,
772
- currency: prev.currency ?? cur?.currency
808
+ currency: prev.currency.code !== "" ? prev.currency : cur.currency
773
809
  }),
774
- { 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 }
775
812
  );
776
813
  }
777
814
 
@@ -790,7 +827,7 @@ const pickupDropoffTypeToOtp1 = (otp2Type: string): string | null => {
790
827
  }
791
828
  };
792
829
 
793
- export const convertGraphQLResponseToLegacy = (leg: any): any => ({
830
+ export const convertGraphQLResponseToLegacy = (leg: any): Leg => ({
794
831
  ...leg,
795
832
  agencyBrandingUrl: leg.agency?.url,
796
833
  agencyId: leg.agency?.id,
@@ -839,7 +876,7 @@ export const getLegRouteShortName = (
839
876
  /** Extract the route long name for a leg returned from OTP1 or OTP2. */
840
877
  export const getLegRouteLongName = (
841
878
  leg: Pick<Leg, "route" | "routeLongName">
842
- ): string | null => {
879
+ ): string | undefined => {
843
880
  const { route, routeLongName } = leg;
844
881
  // typeof route === "object" denotes newer OTP2 responses. routeLongName is OTP1.
845
882
  return typeof route === "object" ? route?.longName : routeLongName;
@@ -851,6 +888,6 @@ export const getLegRouteLongName = (
851
888
  */
852
889
  export const getLegRouteName = (
853
890
  leg: Pick<Leg, "route" | "routeLongName" | "routeShortName">
854
- ): string => {
891
+ ): string | undefined => {
855
892
  return getLegRouteShortName(leg) || getLegRouteLongName(leg);
856
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[] {
package/src/query-gen.ts CHANGED
@@ -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,