@opentripplanner/core-utils 9.0.0-alpha.3 → 9.0.0-alpha.30

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.
@@ -1,6 +1,8 @@
1
+ import { ModeSetting, TransportMode } from "@opentripplanner/types";
2
+
1
3
  import { reduceOtpFlexModes } from "../query";
2
4
  import queryParams, { getCustomQueryParams } from "../query-params";
3
- import { generateCombinations } from "../query-gen";
5
+ import { extractAdditionalModes, generateCombinations } from "../query-gen";
4
6
 
5
7
  const customWalkDistanceOptions = [
6
8
  {
@@ -13,7 +15,7 @@ const customWalkDistanceOptions = [
13
15
  }
14
16
  ];
15
17
 
16
- function modeStrToTransportMode(m) {
18
+ function modeStrToTransportMode(m): TransportMode {
17
19
  const splitVals = m.split("_");
18
20
  return {
19
21
  mode: splitVals[0],
@@ -26,18 +28,18 @@ const mockLatLon = {
26
28
  lon: 2
27
29
  };
28
30
 
29
- function expectModes(modes, expectedModes) {
31
+ function expectModes(modes: string[], expectedModes: string[][]) {
30
32
  const generatedModesList = generateCombinations({
31
- modes: modes.map(modeStrToTransportMode),
32
- to: mockLatLon,
33
33
  from: mockLatLon,
34
- modeSettings: []
34
+ modes: modes.map(modeStrToTransportMode),
35
+ modeSettings: [],
36
+ to: mockLatLon
35
37
  });
36
38
  const expandedExpectedModesList = expectedModes.map(em => ({
37
- modes: em.map(modeStrToTransportMode),
38
- to: mockLatLon,
39
39
  from: mockLatLon,
40
- modeSettings: []
40
+ modes: em.map(modeStrToTransportMode),
41
+ modeSettings: [],
42
+ to: mockLatLon
41
43
  }));
42
44
  return it(
43
45
  modes.join(" "),
@@ -49,6 +51,59 @@ function expectModes(modes, expectedModes) {
49
51
  );
50
52
  }
51
53
 
54
+ describe("extract-modes", () => {
55
+ const mode = {
56
+ mode: "UNICYCLE"
57
+ };
58
+
59
+ const testTransportMode: TransportMode = {
60
+ mode: "testMode"
61
+ };
62
+
63
+ const checkboxModeSetting: ModeSetting = {
64
+ addTransportMode: mode,
65
+ applicableMode: testTransportMode.mode,
66
+ icon: null,
67
+ key: "test",
68
+ label: "test",
69
+ type: "CHECKBOX",
70
+ value: true
71
+ };
72
+
73
+ const dropdownModeSetting: ModeSetting = {
74
+ applicableMode: testTransportMode.mode,
75
+ key: "test",
76
+ label: "test",
77
+ options: [{ text: "testOption", value: "1", addTransportMode: mode }],
78
+ type: "DROPDOWN",
79
+ value: "1"
80
+ };
81
+
82
+ it("determines whether a checkbox setting is extracted correctly", () => {
83
+ expect(
84
+ extractAdditionalModes([checkboxModeSetting], [testTransportMode])
85
+ ).toEqual([mode]);
86
+ });
87
+ it("determines whether a dropdown setting is extracted correctly", () => {
88
+ expect(
89
+ extractAdditionalModes([dropdownModeSetting], [testTransportMode])
90
+ ).toEqual([mode]);
91
+ });
92
+ it("determines whether a checkbox setting set to false is ignored", () => {
93
+ expect(
94
+ extractAdditionalModes(
95
+ [{ ...checkboxModeSetting, value: false }],
96
+ [testTransportMode]
97
+ )
98
+ ).toEqual([]);
99
+ });
100
+ it("determines whether a checkbox setting with no modes is ignored", () => {
101
+ expect(extractAdditionalModes([{ ...checkboxModeSetting }], [])).toEqual(
102
+ []
103
+ );
104
+ });
105
+ });
106
+
52
107
  describe("query-gen", () => {
53
108
  describe("generateCombinations", () => {
54
109
  expectModes(["WALK"], [["WALK"]]);
@@ -93,6 +148,10 @@ describe("query-gen", () => {
93
148
  ["WALK"]
94
149
  ]
95
150
  );
151
+ expectModes(
152
+ ["BICYCLE_RENT", "BICYCLE", "WALK"],
153
+ [["BICYCLE_RENT", "WALK"], ["BICYCLE"], ["WALK"]]
154
+ );
96
155
  expectModes(
97
156
  ["SCOOTER_RENT", "BICYCLE_RENT", "TRANSIT", "WALK"],
98
157
  [
@@ -104,6 +163,26 @@ describe("query-gen", () => {
104
163
  ["WALK"]
105
164
  ]
106
165
  );
166
+
167
+ expectModes(
168
+ ["CAR_HAIL", "TRANSIT"],
169
+ [["TRANSIT"], ["CAR_HAIL", "TRANSIT"]]
170
+ );
171
+ expectModes(
172
+ ["CAR_HAIL", "BICYCLE_RENT", "TRANSIT"],
173
+ [["TRANSIT"], ["CAR_HAIL", "TRANSIT"], ["BICYCLE_RENT", "TRANSIT"]]
174
+ );
175
+ expectModes(
176
+ ["CAR_HAIL", "BICYCLE_RENT", "TRANSIT", "WALK"],
177
+ [
178
+ ["TRANSIT"],
179
+ ["CAR_HAIL", "TRANSIT"],
180
+ ["CAR_HAIL", "WALK"],
181
+ ["BICYCLE_RENT", "TRANSIT"],
182
+ ["BICYCLE_RENT", "WALK"],
183
+ ["WALK"]
184
+ ]
185
+ );
107
186
  expectModes(
108
187
  ["FLEX", "TRANSIT", "WALK"],
109
188
  [["TRANSIT"], ["FLEX", "TRANSIT"], ["FLEX", "WALK"], ["WALK"]]
@@ -131,9 +210,15 @@ describe("query-gen", () => {
131
210
  ]
132
211
  );
133
212
  expectModes(
134
- ["BUS", "RAIL", "GONDOLA", "TRAM"],
213
+ // Transit is required to enable other transit submodes
214
+ ["BUS", "RAIL", "GONDOLA", "TRAM", "TRANSIT"],
135
215
  [["BUS", "RAIL", "GONDOLA", "TRAM"]]
136
216
  );
217
+ expectModes(
218
+ // Transit is required to enable other transit submodes
219
+ ["TRANSIT"],
220
+ [["TRANSIT"]]
221
+ );
137
222
  });
138
223
  });
139
224
 
@@ -50,7 +50,7 @@ export const RouteColorTester = (): JSX.Element => {
50
50
  </>
51
51
  );
52
52
  };
53
- // Disable color contrast checking for the uncorrected color pairs
53
+ // Disable color contrast checking for the uncorrected color pairs.
54
54
  RouteColorTester.parameters = {
55
55
  a11y: { config: { rules: [{ id: "color-contrast", reviewOnFail: true }] } },
56
56
  storyshots: { disable: true }
package/src/itinerary.ts CHANGED
@@ -4,9 +4,10 @@ import {
4
4
  Config,
5
5
  ElevationProfile,
6
6
  FlexBookingInfo,
7
- Itinerary,
7
+ ItineraryOnlyLegsRequired,
8
8
  LatLngArray,
9
9
  Leg,
10
+ MassUnitOption,
10
11
  Money,
11
12
  Place,
12
13
  Step,
@@ -77,6 +78,11 @@ export function legDropoffRequiresAdvanceBooking(leg: Leg): boolean {
77
78
  return isAdvanceBookingRequired(leg.dropOffBookingInfo);
78
79
  }
79
80
 
81
+ // alpha-only comment
82
+ export function isRideshareLeg(leg: Leg): boolean {
83
+ return !!leg.rideHailingEstimate?.provider?.id;
84
+ }
85
+
80
86
  export function isWalk(mode: string): boolean {
81
87
  if (!mode) return false;
82
88
 
@@ -192,16 +198,26 @@ export function toSentenceCase(str: string): string {
192
198
  */
193
199
  export function getCompanyFromLeg(leg: Leg): string {
194
200
  if (!leg) return null;
195
- const { from, mode, rentedBike, rentedCar, rentedVehicle, tncData } = leg;
201
+ const {
202
+ from,
203
+ mode,
204
+ rentedBike,
205
+ rentedCar,
206
+ rentedVehicle,
207
+ rideHailingEstimate
208
+ } = leg;
196
209
  if (mode === "CAR" && rentedCar) {
197
210
  return from.networks[0];
198
211
  }
199
- if (mode === "CAR" && tncData) {
200
- return tncData.company;
212
+ if (mode === "CAR" && rideHailingEstimate) {
213
+ return rideHailingEstimate.provider.id;
201
214
  }
202
215
  if (mode === "BICYCLE" && rentedBike && from.networks) {
203
216
  return from.networks[0];
204
217
  }
218
+ if (from.rentalVehicle) {
219
+ return from.rentalVehicle.network;
220
+ }
205
221
  if (
206
222
  (mode === "MICROMOBILITY" || mode === "SCOOTER") &&
207
223
  rentedVehicle &&
@@ -212,7 +228,9 @@ export function getCompanyFromLeg(leg: Leg): string {
212
228
  return null;
213
229
  }
214
230
 
215
- export function getItineraryBounds(itinerary: Itinerary): LatLngArray[] {
231
+ export function getItineraryBounds(
232
+ itinerary: ItineraryOnlyLegsRequired
233
+ ): LatLngArray[] {
216
234
  let coords = [];
217
235
  itinerary.legs.forEach(leg => {
218
236
  const legCoords = polyline
@@ -391,10 +409,13 @@ export function getCompanyForNetwork(
391
409
  * @return {string} A label for use in presentation on a website.
392
410
  */
393
411
  export function getCompaniesLabelFromNetworks(
394
- networks: string[],
412
+ networks: string | string[],
395
413
  companies: Company[] = []
396
414
  ): string {
397
- return networks
415
+ let networksArray = networks;
416
+ if (typeof networks === "string") networksArray = [networks];
417
+
418
+ return (networksArray as string[])
398
419
  .map(network => getCompanyForNetwork(network, companies))
399
420
  .filter(co => !!co)
400
421
  .map(co => co.label)
@@ -407,7 +428,7 @@ export function getTNCLocation(leg: Leg, type: string): string {
407
428
  }
408
429
 
409
430
  export function calculatePhysicalActivity(
410
- itinerary: Itinerary
431
+ itinerary: ItineraryOnlyLegsRequired
411
432
  ): {
412
433
  bikeDuration: number;
413
434
  caloriesBurned: number;
@@ -433,17 +454,19 @@ export function calculatePhysicalActivity(
433
454
  * these values and currency info.
434
455
  * It is assumed that the same currency is used for all TNC legs.
435
456
  */
436
- export function calculateTncFares(itinerary: Itinerary): TncFare {
457
+ export function calculateTncFares(
458
+ itinerary: ItineraryOnlyLegsRequired
459
+ ): TncFare {
437
460
  return itinerary.legs
438
- .filter(leg => leg.mode === "CAR" && leg.hailedCar && leg.tncData)
461
+ .filter(leg => leg.mode === "CAR" && leg.rideHailingEstimate)
439
462
  .reduce(
440
- ({ maxTNCFare, minTNCFare }, { tncData }) => {
441
- const { currency, maxCost, minCost } = tncData;
463
+ ({ maxTNCFare, minTNCFare }, { rideHailingEstimate }) => {
464
+ const { minPrice, maxPrice } = rideHailingEstimate;
442
465
  return {
443
466
  // Assumes a single currency for entire itinerary.
444
- currencyCode: currency,
445
- maxTNCFare: maxTNCFare + maxCost,
446
- minTNCFare: minTNCFare + minCost
467
+ currencyCode: minPrice.currency.code,
468
+ maxTNCFare: maxTNCFare + maxPrice.amount,
469
+ minTNCFare: minTNCFare + minPrice.amount
447
470
  };
448
471
  },
449
472
  {
@@ -454,27 +477,6 @@ export function calculateTncFares(itinerary: Itinerary): TncFare {
454
477
  );
455
478
  }
456
479
 
457
- /**
458
- * For a given fare component (either total fare or component parts), returns
459
- * an object with the fare value (in cents).
460
- */
461
- export function getTransitFare(
462
- fareComponent: Money
463
- ): {
464
- currencyCode: string;
465
- transitFare: number;
466
- } {
467
- return fareComponent
468
- ? {
469
- currencyCode: fareComponent.currency.currencyCode,
470
- transitFare: fareComponent.cents
471
- }
472
- : {
473
- currencyCode: "USD",
474
- transitFare: 0
475
- };
476
- }
477
-
478
480
  /**
479
481
  * Sources:
480
482
  * - https://www.itf-oecd.org/sites/default/files/docs/environmental-performance-new-mobility.pdf
@@ -501,15 +503,16 @@ const CARBON_INTENSITY_DEFAULTS = {
501
503
  };
502
504
 
503
505
  /**
504
- * @param {itinerary} itinerary OTP trip itinierary
505
- * @param {carbonIntensity} carbonIntensity carbon intensity by mode in grams/meter
506
+ * @param {itinerary} itinerary OTP trip itinierary, only legs is required.
507
+ * @param {carbonIntensity} carbonIntensity carbon intensity by mode in grams/meter
506
508
  * @param {units} units units to be used in return value
507
509
  * @return Amount of carbon in chosen unit
508
510
  */
509
511
  export function calculateEmissions(
510
- itinerary: Itinerary,
512
+ // This type makes all the properties from Itinerary optional except legs.
513
+ itinerary: ItineraryOnlyLegsRequired,
511
514
  carbonIntensity: Record<string, number> = {},
512
- units?: string
515
+ units?: MassUnitOption
513
516
  ): number {
514
517
  // Apply defaults for any values that we don't have.
515
518
  const carbonIntensityWithDefaults = {
@@ -555,3 +558,59 @@ export function getDisplayedStopId(placeOrStop: Place | Stop): string {
555
558
  }
556
559
  return stopCode || stopId?.split(":")[1] || stopId;
557
560
  }
561
+
562
+ /**
563
+ * Extracts useful data from the fare products on a leg, such as the leg cost and transfer info.
564
+ * @param leg Leg with fare products (must have used getLegsWithFares)
565
+ * @param category Rider category
566
+ * @param container Fare container (cash, electronic)
567
+ * @returns Object containing price as well as the transfer discount amount, if a transfer was used.
568
+ */
569
+ export function getLegCost(
570
+ leg: Leg,
571
+ mediumId: string,
572
+ riderCategoryId: string
573
+ ): { price: Money; transferAmount?: Money | undefined } {
574
+ if (!leg.fareProducts) return { price: undefined };
575
+ const relevantFareProducts = leg.fareProducts.filter(({ product }) => {
576
+ return (
577
+ product.riderCategory.id === riderCategoryId &&
578
+ product.medium.id === mediumId
579
+ );
580
+ });
581
+ const totalCost = relevantFareProducts.find(
582
+ fp => fp.product.name === "rideCost"
583
+ )?.product?.price;
584
+ const transferFareProduct = relevantFareProducts.find(
585
+ fp => fp.product.name === "transfer"
586
+ );
587
+
588
+ return {
589
+ price: totalCost,
590
+ transferAmount: transferFareProduct?.product.price
591
+ };
592
+ }
593
+
594
+ /**
595
+ * Returns the total itinerary cost for a given set of legs.
596
+ * @param legs Itinerary legs with fare products (must have used getLegsWithFares)
597
+ * @param category Rider category (youth, regular, senior)
598
+ * @param container Fare container (cash, electronic)
599
+ * @returns Money object for the total itinerary cost.
600
+ */
601
+ export function getItineraryCost(
602
+ legs: Leg[],
603
+ category: string,
604
+ container: string
605
+ ): Money {
606
+ return legs
607
+ .filter(leg => !!leg.fareProducts)
608
+ .map(leg => getLegCost(leg, category, container).price)
609
+ .reduce<Money>(
610
+ (prev, cur) => ({
611
+ amount: prev.amount + cur?.amount || 0,
612
+ currency: prev.currency ?? cur?.currency
613
+ }),
614
+ { amount: 0, currency: null }
615
+ );
616
+ }