@opentripplanner/core-utils 9.0.0-alpha.1 → 9.0.0-alpha.10

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/src/itinerary.ts CHANGED
@@ -5,8 +5,10 @@ import {
5
5
  ElevationProfile,
6
6
  FlexBookingInfo,
7
7
  Itinerary,
8
+ ItineraryOnlyLegsRequired,
8
9
  LatLngArray,
9
10
  Leg,
11
+ MassUnitOption,
10
12
  Money,
11
13
  Place,
12
14
  Step,
@@ -212,7 +214,9 @@ export function getCompanyFromLeg(leg: Leg): string {
212
214
  return null;
213
215
  }
214
216
 
215
- export function getItineraryBounds(itinerary: Itinerary): LatLngArray[] {
217
+ export function getItineraryBounds(
218
+ itinerary: ItineraryOnlyLegsRequired
219
+ ): LatLngArray[] {
216
220
  let coords = [];
217
221
  itinerary.legs.forEach(leg => {
218
222
  const legCoords = polyline
@@ -407,7 +411,7 @@ export function getTNCLocation(leg: Leg, type: string): string {
407
411
  }
408
412
 
409
413
  export function calculatePhysicalActivity(
410
- itinerary: Itinerary
414
+ itinerary: ItineraryOnlyLegsRequired
411
415
  ): {
412
416
  bikeDuration: number;
413
417
  caloriesBurned: number;
@@ -433,7 +437,9 @@ export function calculatePhysicalActivity(
433
437
  * these values and currency info.
434
438
  * It is assumed that the same currency is used for all TNC legs.
435
439
  */
436
- export function calculateTncFares(itinerary: Itinerary): TncFare {
440
+ export function calculateTncFares(
441
+ itinerary: ItineraryOnlyLegsRequired
442
+ ): TncFare {
437
443
  return itinerary.legs
438
444
  .filter(leg => leg.mode === "CAR" && leg.hailedCar && leg.tncData)
439
445
  .reduce(
@@ -501,15 +507,16 @@ const CARBON_INTENSITY_DEFAULTS = {
501
507
  };
502
508
 
503
509
  /**
504
- * @param {itinerary} itinerary OTP trip itinierary
505
- * @param {carbonIntensity} carbonIntensity carbon intensity by mode in grams/meter
510
+ * @param {itinerary} itinerary OTP trip itinierary, only legs is required.
511
+ * @param {carbonIntensity} carbonIntensity carbon intensity by mode in grams/meter
506
512
  * @param {units} units units to be used in return value
507
513
  * @return Amount of carbon in chosen unit
508
514
  */
509
515
  export function calculateEmissions(
510
- itinerary: Itinerary,
516
+ // This type makes all the properties from Itinerary optional except legs.
517
+ itinerary: ItineraryOnlyLegsRequired,
511
518
  carbonIntensity: Record<string, number> = {},
512
- units?: string
519
+ units?: MassUnitOption
513
520
  ): number {
514
521
  // Apply defaults for any values that we don't have.
515
522
  const carbonIntensityWithDefaults = {
@@ -555,3 +562,70 @@ export function getDisplayedStopId(placeOrStop: Place | Stop): string {
555
562
  }
556
563
  return stopCode || stopId?.split(":")[1] || stopId;
557
564
  }
565
+
566
+ /**
567
+ * Adds the fare product info to each leg in an itinerary, from the itinerary's fare property
568
+ * @param itinerary Itinerary with legProducts in fare object
569
+ * @returns Itinerary with legs that have fare products attached to them
570
+ */
571
+ export function getLegsWithFares(itinerary: Itinerary): Leg[] {
572
+ return itinerary.legs.map((leg, i) => ({
573
+ ...leg,
574
+ fareProducts: itinerary.fare?.legProducts
575
+ ?.filter(lp => lp?.legIndices?.includes(i))
576
+ .flatMap(lp => lp.products)
577
+ }));
578
+ }
579
+
580
+ /**
581
+ * Extracts useful data from the fare products on a leg, such as the leg cost and transfer info.
582
+ * @param leg Leg with fare products (must have used getLegsWithFares)
583
+ * @param category Rider category
584
+ * @param container Fare container (cash, electronic)
585
+ * @returns Object containing price as well as the transfer discount amount, if a transfer was used.
586
+ */
587
+ export function getLegCost(
588
+ leg: Leg,
589
+ category: string,
590
+ container: string
591
+ ): { price?: Money; transferAmount?: number } {
592
+ if (!leg.fareProducts) return { price: undefined };
593
+
594
+ const relevantFareProducts = leg.fareProducts.filter(
595
+ fp => fp.category.name === category && fp.container.name === container
596
+ );
597
+ const totalCost = relevantFareProducts.find(fp => fp.name === "rideCost")
598
+ ?.amount;
599
+ const transferFareProduct = relevantFareProducts.find(
600
+ fp => fp.name === "transfer"
601
+ );
602
+
603
+ return {
604
+ price: totalCost,
605
+ transferAmount: transferFareProduct?.amount?.cents
606
+ };
607
+ }
608
+
609
+ /**
610
+ * Returns the total itinerary cost for a given set of legs.
611
+ * @param legs Itinerary legs with fare products (must have used getLegsWithFares)
612
+ * @param category Rider category (youth, regular, senior)
613
+ * @param container Fare container (cash, electronic)
614
+ * @returns Money object for the total itinerary cost.
615
+ */
616
+ export function getItineraryCost(
617
+ legs: Leg[],
618
+ category: string,
619
+ container: string
620
+ ): Money {
621
+ return legs
622
+ .filter(leg => !!leg.fareProducts)
623
+ .map(leg => getLegCost(leg, category, container).price)
624
+ .reduce<Money>(
625
+ (prev, cur) => ({
626
+ cents: prev.cents + cur?.cents || 0,
627
+ currency: prev.currency ?? cur?.currency
628
+ }),
629
+ { cents: 0, currency: null }
630
+ );
631
+ }
@@ -0,0 +1,149 @@
1
+ query PlanQuery(
2
+ $fromPlace: String!,
3
+ $toPlace: String!,
4
+ $modes: [TransportMode],
5
+ $time: String,
6
+ $date: String,
7
+ $wheelchair: Boolean,
8
+ $bikeReluctance: Float,
9
+ $carReluctance: Float,
10
+ $walkReluctance: Float,
11
+ $arriveBy: Boolean,
12
+ $intermediatePlaces: [InputCoordinates],
13
+ $preferred: InputPreferred,
14
+ $unpreferred: InputUnpreferred,
15
+ $banned: InputBanned,
16
+ ) {
17
+ plan(
18
+ fromPlace: $fromPlace
19
+ toPlace: $toPlace
20
+ transportModes: $modes
21
+ # Currently only supporting EN locale, used for times and text
22
+ locale: "en",
23
+ time: $time,
24
+ date: $date,
25
+ wheelchair: $wheelchair,
26
+ bikeReluctance: $bikeReluctance,
27
+ carReluctance: $carReluctance,
28
+ walkReluctance: $walkReluctance,
29
+ arriveBy: $arriveBy,
30
+ intermediatePlaces: $intermediatePlaces,
31
+ preferred: $preferred,
32
+ unpreferred: $unpreferred,
33
+ banned: $banned,
34
+ ) {
35
+ itineraries {
36
+ duration
37
+ endTime
38
+ startTime
39
+ waitingTime
40
+ walkTime
41
+ legs {
42
+ interlineWithPreviousLeg
43
+ distance
44
+ duration
45
+ endTime
46
+ mode
47
+ realTime
48
+ realtimeState
49
+ startTime
50
+ transitLeg
51
+ trip {
52
+ id
53
+ gtfsId
54
+ tripHeadsign
55
+ }
56
+ agency {
57
+ name
58
+ id
59
+ timezone
60
+ url
61
+ alerts {
62
+ alertHeaderText
63
+ alertDescriptionText
64
+ alertUrl
65
+ effectiveStartDate
66
+ }
67
+ }
68
+ legGeometry {
69
+ length
70
+ points
71
+ }
72
+ intermediateStops {
73
+ lat
74
+ lon
75
+ name
76
+ stopCode: code
77
+ stopId: id
78
+ locationType
79
+ }
80
+ route {
81
+ shortName
82
+ longName
83
+ color
84
+ textColor
85
+ id
86
+ type
87
+ alerts {
88
+ alertHeaderText
89
+ alertDescriptionText
90
+ alertUrl
91
+ effectiveStartDate
92
+ }
93
+ }
94
+ from {
95
+ lat
96
+ lon
97
+ name
98
+ vertexType
99
+ stop {
100
+ id
101
+ code
102
+ alerts {
103
+ alertHeaderText
104
+ alertDescriptionText
105
+ alertUrl
106
+ effectiveStartDate
107
+ }
108
+ }
109
+ }
110
+ to {
111
+ lat
112
+ lon
113
+ name
114
+ vertexType
115
+ stop {
116
+ id
117
+ code
118
+ alerts {
119
+ alertHeaderText
120
+ alertDescriptionText
121
+ alertUrl
122
+ effectiveStartDate
123
+ }
124
+ }
125
+ }
126
+ steps {
127
+ distance
128
+ lat
129
+ lon
130
+ relativeDirection
131
+ absoluteDirection
132
+ stayOn
133
+ streetName
134
+ area
135
+ alerts{
136
+ alertHeaderText
137
+ alertDescriptionText
138
+ alertUrl
139
+ effectiveStartDate
140
+ }
141
+ elevationProfile {
142
+ distance
143
+ elevation
144
+ }
145
+ }
146
+ }
147
+ }
148
+ }
149
+ }
@@ -0,0 +1,218 @@
1
+ import { LonLatOutput } from "@conveyal/lonlat";
2
+ import { print } from "graphql";
3
+ import {
4
+ ModeSetting,
5
+ ModeSettingValues,
6
+ TransportMode
7
+ } from "@opentripplanner/types";
8
+
9
+ import PlanQuery from "./planQuery.graphql";
10
+
11
+ type OTPQueryParams = {
12
+ arriveBy: boolean;
13
+ date: string;
14
+ from: LonLatOutput & { name?: string };
15
+ modes: TransportMode[];
16
+ modeSettings: ModeSetting[];
17
+ time: string;
18
+ to: LonLatOutput & { name?: string };
19
+ };
20
+
21
+ type GraphQLQuery = {
22
+ query: string;
23
+ variables: Record<string, unknown>;
24
+ };
25
+
26
+ /**
27
+ * Mode Settings can contain additional modes to add to the query,
28
+ * this function extracts those additional modes from the settings
29
+ * and returns them in an array.
30
+ * @param modeSettings List of mode settings with values populated
31
+ * @returns Additional transport modes to add to query
32
+ */
33
+ export function extractAdditionalModes(
34
+ modeSettings: ModeSetting[],
35
+ enabledModes: TransportMode[]
36
+ ): TransportMode[] {
37
+ return modeSettings.reduce<TransportMode[]>((prev, cur) => {
38
+ // First, ensure that the mode associated with this setting is even enabled
39
+ if (!enabledModes.map(m => m.mode).includes(cur.applicableMode)) {
40
+ return prev;
41
+ }
42
+
43
+ // In checkboxes, mode must be enabled and have a transport mode in it
44
+ if (cur.type === "CHECKBOX" && cur.addTransportMode && cur.value) {
45
+ return [...prev, cur.addTransportMode];
46
+ }
47
+ if (cur.type === "DROPDOWN") {
48
+ const transportMode = cur.options.find(o => o.value === cur.value)
49
+ .addTransportMode;
50
+ if (transportMode) {
51
+ return [...prev, transportMode];
52
+ }
53
+ }
54
+ return prev;
55
+ }, []);
56
+ }
57
+
58
+ /**
59
+ * Generates every possible mathematical subset of the input TransportModes.
60
+ * Uses code from:
61
+ * https://stackoverflow.com/questions/5752002/find-all-possible-subset-combos-in-an-array
62
+ * @param array Array of input transport modes
63
+ * @returns 2D array representing every possible subset of transport modes from input
64
+ */
65
+ function combinations(array: TransportMode[]): TransportMode[][] {
66
+ if (!array) return [];
67
+ return (
68
+ // eslint-disable-next-line no-bitwise
69
+ new Array(1 << array.length)
70
+ .fill(null)
71
+ // eslint-disable-next-line no-bitwise
72
+ .map((e1, i) => array.filter((e2, j) => i & (1 << j)))
73
+ );
74
+ }
75
+
76
+ /**
77
+ * This constant maps all the transport mode to a broader mode type,
78
+ * which is used to determine the valid combinations of modes used in query generation.
79
+ */
80
+ export const SIMPLIFICATIONS = {
81
+ AIRPLANE: "TRANSIT",
82
+ BICYCLE: "PERSONAL",
83
+ BUS: "TRANSIT",
84
+ CABLE_CAR: "TRANSIT",
85
+ CAR: "CAR",
86
+ FERRY: "TRANSIT",
87
+ FLEX: "SHARED", // TODO: this allows FLEX+WALK. Is this reasonable?
88
+ FUNICULAR: "TRANSIT",
89
+ GONDOLA: "TRANSIT",
90
+ RAIL: "TRANSIT",
91
+ SCOOTER: "PERSONAL",
92
+ SUBWAY: "TRANSIT",
93
+ TRAM: "TRANSIT",
94
+ TRANSIT: "TRANSIT",
95
+ WALK: "WALK"
96
+ };
97
+
98
+ // Inclusion of "TRANSIT" alone automatically implies "WALK" in OTP
99
+ const VALID_COMBOS = [
100
+ ["WALK"],
101
+ ["PERSONAL"],
102
+ ["TRANSIT", "SHARED"],
103
+ ["WALK", "SHARED"],
104
+ ["TRANSIT"],
105
+ ["TRANSIT", "PERSONAL"],
106
+ ["TRANSIT", "CAR"]
107
+ ];
108
+
109
+ const BANNED_TOGETHER = ["SCOOTER", "BICYCLE"];
110
+
111
+ export const TRANSIT_SUBMODES = Object.keys(SIMPLIFICATIONS).filter(
112
+ mode => SIMPLIFICATIONS[mode] === "TRANSIT" && mode !== "TRANSIT"
113
+ );
114
+ export const TRANSIT_SUBMODES_AND_TRANSIT = Object.keys(SIMPLIFICATIONS).filter(
115
+ mode => SIMPLIFICATIONS[mode] === "TRANSIT"
116
+ );
117
+
118
+ function isCombinationValid(
119
+ combo: TransportMode[],
120
+ queryTransitSubmodes: string[]
121
+ ): boolean {
122
+ if (combo.length === 0) return false;
123
+
124
+ // All current qualifiers currently simplify to "SHARED"
125
+ const simplifiedModes = Array.from(
126
+ new Set(combo.map(c => (c.qualifier ? "SHARED" : SIMPLIFICATIONS[c.mode])))
127
+ );
128
+
129
+ // Ensure that if we have one transit mode, then we include ALL transit modes
130
+ if (simplifiedModes.includes("TRANSIT")) {
131
+ // Don't allow TRANSIT along with any other submodes
132
+ if (queryTransitSubmodes.length && combo.find(c => c.mode === "TRANSIT")) {
133
+ return false;
134
+ }
135
+
136
+ if (
137
+ combo.reduce((prev, cur) => {
138
+ if (queryTransitSubmodes.includes(cur.mode)) {
139
+ return prev - 1;
140
+ }
141
+ return prev;
142
+ }, queryTransitSubmodes.length) !== 0
143
+ ) {
144
+ return false;
145
+ }
146
+ // Continue to the other checks
147
+ }
148
+
149
+ // OTP doesn't support multiple non-walk modes
150
+ if (BANNED_TOGETHER.every(m => combo.find(c => c.mode === m))) return false;
151
+
152
+ return !!VALID_COMBOS.find(
153
+ vc =>
154
+ simplifiedModes.every(m => vc.includes(m)) &&
155
+ vc.every(m => simplifiedModes.includes(m))
156
+ );
157
+ }
158
+
159
+ /**
160
+ * Generates a list of queries for OTP to get a comprehensive
161
+ * set of results based on the modes input.
162
+ * @param params OTP Query Params
163
+ * @returns Set of parameters to generate queries
164
+ */
165
+ export function generateCombinations(params: OTPQueryParams): OTPQueryParams[] {
166
+ const completeModeList = [
167
+ ...extractAdditionalModes(params.modeSettings, params.modes),
168
+ ...params.modes
169
+ ];
170
+
171
+ // List of the transit *submodes* that are included in the input params
172
+ const queryTransitSubmodes = completeModeList
173
+ .filter(mode => TRANSIT_SUBMODES.includes(mode.mode))
174
+ .map(mode => mode.mode);
175
+
176
+ return combinations(completeModeList)
177
+ .filter(combo => isCombinationValid(combo, queryTransitSubmodes))
178
+ .map(combo => ({ ...params, modes: combo }));
179
+ }
180
+
181
+ export function generateOtp2Query({
182
+ arriveBy,
183
+ date,
184
+ from,
185
+ modes,
186
+ modeSettings,
187
+ time,
188
+ to
189
+ }: OTPQueryParams): GraphQLQuery {
190
+ // This extracts the values from the mode settings to key value pairs
191
+ const modeSettingValues = modeSettings.reduce((prev, cur) => {
192
+ prev[cur.key] = cur.value;
193
+ return prev;
194
+ }, {}) as ModeSettingValues;
195
+
196
+ const {
197
+ bikeReluctance,
198
+ carReluctance,
199
+ walkReluctance,
200
+ wheelchair
201
+ } = modeSettingValues;
202
+
203
+ return {
204
+ query: print(PlanQuery),
205
+ variables: {
206
+ arriveBy,
207
+ bikeReluctance,
208
+ carReluctance,
209
+ date,
210
+ fromPlace: `${from.name}::${from.lat},${from.lon}}`,
211
+ modes,
212
+ time,
213
+ toPlace: `${to.name}::${to.lat},${to.lon}}`,
214
+ walkReluctance,
215
+ wheelchair
216
+ }
217
+ };
218
+ }
package/tsconfig.json CHANGED
@@ -2,9 +2,12 @@
2
2
  "extends": "../../tsconfig.json",
3
3
  "compilerOptions": {
4
4
  "composite": true,
5
+ "target": "es2019",
5
6
  "outDir": "./lib",
6
7
  "rootDir": "./src",
8
+ "lib": ["es2019", "dom"],
7
9
  "skipLibCheck": true
8
10
  },
9
- "include": ["src/**/*"]
11
+ "include": ["src/**/*"],
12
+ "exclude": ["src/__tests__/**/*"]
10
13
  }