@opentripplanner/core-utils 9.0.0-alpha.2 → 9.0.0-alpha.21

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