@opentripplanner/core-utils 14.0.0-alpha.1 → 14.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 (59) hide show
  1. package/README.md +0 -4
  2. package/esm/itinerary.js +74 -32
  3. package/esm/itinerary.js.map +1 -1
  4. package/esm/otpSchema.json +7321 -5821
  5. package/esm/planQuery.graphql +12 -0
  6. package/esm/profile.js +1 -1
  7. package/esm/profile.js.map +1 -1
  8. package/esm/query-gen.js +2 -2
  9. package/esm/query-gen.js.map +1 -1
  10. package/esm/query.js +2 -5
  11. package/esm/query.js.map +1 -1
  12. package/esm/storage.js +2 -6
  13. package/esm/storage.js.map +1 -1
  14. package/esm/time.js +1 -2
  15. package/esm/time.js.map +1 -1
  16. package/lib/index.js +50 -35
  17. package/lib/index.js.map +1 -1
  18. package/lib/itinerary.d.ts +25 -8
  19. package/lib/itinerary.d.ts.map +1 -1
  20. package/lib/itinerary.js +524 -491
  21. package/lib/itinerary.js.map +1 -1
  22. package/lib/map.js +40 -39
  23. package/lib/map.js.map +1 -1
  24. package/lib/otpSchema.json +7321 -5821
  25. package/lib/planQuery.graphql +12 -0
  26. package/lib/profile.js +1 -1
  27. package/lib/profile.js.map +1 -1
  28. package/lib/query-gen.js +134 -138
  29. package/lib/query-gen.js.map +1 -1
  30. package/lib/query.js +3 -6
  31. package/lib/query.js.map +1 -1
  32. package/lib/route.js +230 -248
  33. package/lib/route.js.map +1 -1
  34. package/lib/storage.d.ts.map +1 -1
  35. package/lib/storage.js +22 -28
  36. package/lib/storage.js.map +1 -1
  37. package/lib/suspense.js +28 -16
  38. package/lib/suspense.js.map +1 -1
  39. package/lib/time.js +36 -49
  40. package/lib/time.js.map +1 -1
  41. package/lib/ui.js +33 -36
  42. package/lib/ui.js.map +1 -1
  43. package/package.json +4 -4
  44. package/src/__tests__/__snapshots__/query-params.ts.snap +201 -201
  45. package/src/__tests__/__snapshots__/query.js.snap +28 -28
  46. package/src/__tests__/__snapshots__/route.js.snap +76 -76
  47. package/src/__tests__/__snapshots__/time.js.snap +2 -2
  48. package/src/__tests__/itinerary.ts +108 -23
  49. package/src/__tests__/query.js +1 -1
  50. package/src/__tests__/route.js +1 -1
  51. package/src/core-utils.story.tsx +34 -23
  52. package/src/itinerary.ts +88 -37
  53. package/src/otpSchema.json +7321 -5821
  54. package/src/planQuery.graphql +12 -0
  55. package/src/profile.js +1 -1
  56. package/src/query-gen.ts +1 -1
  57. package/src/query.js +2 -5
  58. package/src/storage.ts +5 -9
  59. package/tsconfig.tsbuildinfo +1 -1
@@ -13,7 +13,7 @@ import {
13
13
  planParamsToQuery
14
14
  } from "../query";
15
15
 
16
- const config = require("./__mocks__/config.json");
16
+ import config from "./__mocks__/config.json";
17
17
 
18
18
  describe("query", () => {
19
19
  afterEach(restoreDateNowBehavior);
@@ -5,7 +5,7 @@ import {
5
5
  makeRouteComparator
6
6
  } from "../route";
7
7
 
8
- const { otp1Routes, otp2Routes } = require("./__mocks__/routes.json");
8
+ import { otp1Routes, otp2Routes } from "./__mocks__/routes.json";
9
9
 
10
10
  function sortRoutes(...routes) {
11
11
  routes.sort(makeRouteComparator());
@@ -1,3 +1,4 @@
1
+ /* eslint-disable react/display-name */
1
2
  import React, { useState } from "react";
2
3
  import styled from "styled-components";
3
4
  import {
@@ -42,29 +43,39 @@ const ColorPair = ({ fg, bg }: { fg: string; bg: string }) => {
42
43
  );
43
44
  };
44
45
 
45
- export const RouteColorTester = (): JSX.Element => {
46
- const [fg, setFg] = useState("#333333");
47
- const [bg, setBg] = useState("#cbeb55");
48
- return (
49
- <>
50
- <ColorPair bg={bg} fg={fg}></ColorPair>
51
- {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
52
- <label>
53
- Foreground Color
54
- <input onChange={e => setFg(e.target.value)} type="color" value={fg} />
55
- </label>
56
- {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
57
- <label>
58
- Background Color
59
- <input onChange={e => setBg(e.target.value)} type="color" value={bg} />
60
- </label>
61
- </>
62
- );
63
- };
64
- // Disable color contrast checking for the uncorrected color pairs.
65
- RouteColorTester.parameters = {
66
- a11y: { config: { rules: [{ id: "color-contrast", reviewOnFail: true }] } },
67
- storyshots: { disable: true }
46
+ export const RouteColorTester = {
47
+ render: (): JSX.Element => {
48
+ const [fg, setFg] = useState("#333333");
49
+ const [bg, setBg] = useState("#cbeb55");
50
+ return (
51
+ <>
52
+ <ColorPair bg={bg} fg={fg}></ColorPair>
53
+ {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
54
+ <label>
55
+ Foreground Color
56
+ <input
57
+ onChange={e => setFg(e.target.value)}
58
+ type="color"
59
+ value={fg}
60
+ />
61
+ </label>
62
+ {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
63
+ <label>
64
+ Background Color
65
+ <input
66
+ onChange={e => setBg(e.target.value)}
67
+ type="color"
68
+ value={bg}
69
+ />
70
+ </label>
71
+ </>
72
+ );
73
+ },
74
+ // Disable color contrast checking for the uncorrected color pairs
75
+ parameters: {
76
+ a11y: { config: { rules: [{ id: "color-contrast", reviewOnFail: true }] } },
77
+ storyshots: { disable: true }
78
+ }
68
79
  };
69
80
 
70
81
  // Route sort logic story:
package/src/itinerary.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import polyline from "@mapbox/polyline";
2
2
  import {
3
+ AppliedFareProduct,
3
4
  Company,
4
5
  Config,
6
+ Currency,
5
7
  ElevationProfile,
6
8
  ElevationProfileComponent,
7
9
  FlexBookingInfo,
@@ -104,8 +106,6 @@ export function isFlex(leg: Leg): boolean {
104
106
  legContainsGeometry(leg)
105
107
  );
106
108
  }
107
-
108
- // alpha-only comment
109
109
  export function isRideshareLeg(leg: Leg): boolean {
110
110
  return !!leg.rideHailingEstimate?.provider?.id;
111
111
  }
@@ -246,6 +246,9 @@ export function getCompanyFromLeg(leg: Leg): string {
246
246
  if (from.rentalVehicle) {
247
247
  return from.rentalVehicle.network;
248
248
  }
249
+ if (from.vehicleRentalStation?.rentalNetwork) {
250
+ return from.vehicleRentalStation.rentalNetwork.networkId;
251
+ }
249
252
  if (
250
253
  (mode === "MICROMOBILITY" || mode === "SCOOTER") &&
251
254
  rentedVehicle &&
@@ -606,45 +609,91 @@ export function getDisplayedStopId(placeOrStop: Place | Stop): string {
606
609
  return stopCode || stopId?.split(":")[1] || stopId;
607
610
  }
608
611
 
612
+ /**
613
+ * Removes the first part of the OTP standard scope (":"), if it is present
614
+ * @param item String that is potentially scoped with `:` character
615
+ * @returns descoped string
616
+ */
617
+ export const descope = (item: string): string => item?.split(":")?.[1];
618
+
619
+ export type ExtendedMoney = Money & { originalAmount?: number };
620
+
621
+ export const zeroDollars = (currency: Currency): Money => ({
622
+ amount: 0,
623
+ currency
624
+ });
625
+
609
626
  /**
610
627
  * Extracts useful data from the fare products on a leg, such as the leg cost and transfer info.
611
- * @param leg Leg with fare products (must have used getLegsWithFares)
612
- * @param category Rider category
613
- * @param container Fare container (cash, electronic)
614
- * @returns Object containing price as well as the transfer discount amount, if a transfer was used.
628
+ * @param leg Leg with Fares v2 information
629
+ * @param mediumId Desired medium ID to calculate fare for
630
+ * @param riderCategoryId Desire rider category to calculate fare for
631
+ * @param seenFareIds Fare IDs used on previous legs. Used to detect transfer discounts.
632
+ * @returns Object containing price as well as transfer/dependent
633
+ * fare information. `AppliedFareProduct` should contain
634
+ * all the information needed, but the other fields are kept to
635
+ * make the transition to Fares V2 less jarring.
615
636
  */
616
637
  export function getLegCost(
617
638
  leg: Leg,
618
639
  mediumId: string | null,
619
- riderCategoryId: string | null
640
+ riderCategoryId: string | null,
641
+ seenFareIds?: string[]
620
642
  ): {
643
+ alternateFareProducts?: AppliedFareProduct[];
644
+ appliedFareProduct?: AppliedFareProduct;
645
+ isDependent?: boolean;
621
646
  price?: Money;
622
- transferAmount?: Money | undefined;
623
647
  productUseId?: string;
624
648
  } {
625
649
  if (!leg.fareProducts) return { price: undefined };
626
- const relevantFareProducts = leg.fareProducts.filter(({ product }) => {
627
- // riderCategory and medium can be specifically defined as null to handle
628
- // generic GTFS based fares from OTP when there is no fare model
629
- return (
630
- (product.riderCategory === null ? null : product.riderCategory.id) ===
631
- riderCategoryId &&
632
- (product.medium === null ? null : product.medium.id) === mediumId
633
- );
634
- });
635
-
636
- // Custom fare models return "rideCost", generic GTFS fares return "regular"
637
- const totalCostProduct = relevantFareProducts.find(
638
- fp => fp.product.name === "rideCost" || fp.product.name === "regular"
639
- );
640
- const transferFareProduct = relevantFareProducts.find(
641
- fp => fp.product.name === "transfer"
642
- );
643
-
650
+ const relevantFareProducts = leg.fareProducts
651
+ .filter(({ product }) => {
652
+ // riderCategory and medium can be specifically defined as null to handle
653
+ // generic GTFS based fares from OTP when there is no fare model
654
+
655
+ // Remove (optional) agency scoping
656
+ const productRiderCategoryId =
657
+ descope(product?.riderCategory?.id) ||
658
+ product?.riderCategory?.id ||
659
+ null;
660
+
661
+ const productMediaId =
662
+ descope(product?.medium?.id) || product?.medium?.id || null;
663
+ return (
664
+ productRiderCategoryId === riderCategoryId &&
665
+ productMediaId === mediumId &&
666
+ // Make sure there's a price
667
+ // Some fare products don't have a price at all.
668
+ product?.price
669
+ );
670
+ })
671
+ .map(fare => {
672
+ const alreadySeen = seenFareIds?.indexOf(fare.id) > -1;
673
+ const { currency } = fare.product.price;
674
+ return {
675
+ id: fare.id,
676
+ product: {
677
+ ...fare.product,
678
+ legPrice: alreadySeen ? zeroDollars(currency) : fare.product.price
679
+ } as AppliedFareProduct
680
+ };
681
+ })
682
+ .sort((a, b) => a.product?.legPrice?.amount - b.product?.legPrice?.amount);
683
+
684
+ // Return the cheapest, but include other matches as well
685
+ const cheapestRelevantFareProduct = relevantFareProducts[0];
686
+
687
+ // TODO: return one object here instead of dumbing it down?
644
688
  return {
645
- price: totalCostProduct?.product.price,
646
- transferAmount: transferFareProduct?.product.price,
647
- productUseId: totalCostProduct?.id
689
+ alternateFareProducts: relevantFareProducts.splice(1).map(fp => fp.product),
690
+ appliedFareProduct: cheapestRelevantFareProduct?.product,
691
+ isDependent:
692
+ // eslint-disable-next-line no-underscore-dangle
693
+ cheapestRelevantFareProduct?.product.__typename ===
694
+ "DependentFareProduct",
695
+ price: cheapestRelevantFareProduct?.product.legPrice,
696
+ productUseId: cheapestRelevantFareProduct?.id
648
697
  };
649
698
  }
650
699
 
@@ -653,6 +702,7 @@ export function getLegCost(
653
702
  * @param legs Itinerary legs with fare products (must have used getLegsWithFares)
654
703
  * @param category Rider category (youth, regular, senior)
655
704
  * @param container Fare container (cash, electronic)
705
+ * @param seenFareIds List of fare product IDs that have already been seen on prev legs.
656
706
  * @returns Money object for the total itinerary cost.
657
707
  */
658
708
  export function getItineraryCost(
@@ -660,22 +710,23 @@ export function getItineraryCost(
660
710
  mediumId: string | null,
661
711
  riderCategoryId: string | null
662
712
  ): Money | undefined {
663
- const legCosts = legs
713
+ const legCostsObj = legs
664
714
  // Only legs with fares (no walking legs)
665
715
  .filter(leg => leg.fareProducts?.length > 0)
666
716
  // Get the leg cost object of each leg
667
717
  .map(leg => getLegCost(leg, mediumId, riderCategoryId))
668
- .filter(cost => cost.price !== undefined)
718
+ .filter(cost => cost.appliedFareProduct?.legPrice !== undefined)
669
719
  // Filter out duplicate use IDs
670
720
  // One fare product can be used on multiple legs,
671
721
  // and we don't want to count it more than once.
672
- .reduce<{ productUseId: string; price: Money }[]>((prev, cur) => {
673
- if (!prev.some(p => p.productUseId === cur.productUseId)) {
674
- prev.push({ productUseId: cur.productUseId, price: cur.price });
722
+ // Use an object keyed by productUseId to deduplicate, then extract prices
723
+ .reduce<{ [productUseId: string]: Money }>((acc, cur) => {
724
+ if (cur.productUseId && acc[cur.productUseId] === undefined) {
725
+ acc[cur.productUseId] = cur.appliedFareProduct?.legPrice;
675
726
  }
676
- return prev;
677
- }, [])
678
- .map(productUse => productUse.price);
727
+ return acc;
728
+ }, {});
729
+ const legCosts = Object.values(legCostsObj);
679
730
 
680
731
  if (legCosts.length === 0) return undefined;
681
732
  // Calculate the total