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

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,150 @@
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
+ departureDelay
44
+ distance
45
+ duration
46
+ endTime
47
+ mode
48
+ realTime
49
+ realtimeState
50
+ startTime
51
+ transitLeg
52
+ trip {
53
+ id
54
+ gtfsId
55
+ tripHeadsign
56
+ }
57
+ agency {
58
+ name
59
+ id
60
+ timezone
61
+ url
62
+ alerts {
63
+ alertHeaderText
64
+ alertDescriptionText
65
+ alertUrl
66
+ effectiveStartDate
67
+ }
68
+ }
69
+ legGeometry {
70
+ length
71
+ points
72
+ }
73
+ intermediateStops {
74
+ lat
75
+ lon
76
+ name
77
+ stopCode: code
78
+ stopId: id
79
+ locationType
80
+ }
81
+ route {
82
+ shortName
83
+ longName
84
+ color
85
+ textColor
86
+ id
87
+ type
88
+ alerts {
89
+ alertHeaderText
90
+ alertDescriptionText
91
+ alertUrl
92
+ effectiveStartDate
93
+ }
94
+ }
95
+ from {
96
+ lat
97
+ lon
98
+ name
99
+ vertexType
100
+ stop {
101
+ id
102
+ code
103
+ alerts {
104
+ alertHeaderText
105
+ alertDescriptionText
106
+ alertUrl
107
+ effectiveStartDate
108
+ }
109
+ }
110
+ }
111
+ to {
112
+ lat
113
+ lon
114
+ name
115
+ vertexType
116
+ stop {
117
+ id
118
+ code
119
+ alerts {
120
+ alertHeaderText
121
+ alertDescriptionText
122
+ alertUrl
123
+ effectiveStartDate
124
+ }
125
+ }
126
+ }
127
+ steps {
128
+ distance
129
+ lat
130
+ lon
131
+ relativeDirection
132
+ absoluteDirection
133
+ stayOn
134
+ streetName
135
+ area
136
+ alerts{
137
+ alertHeaderText
138
+ alertDescriptionText
139
+ alertUrl
140
+ effectiveStartDate
141
+ }
142
+ elevationProfile {
143
+ distance
144
+ elevation
145
+ }
146
+ }
147
+ }
148
+ }
149
+ }
150
+ }
@@ -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
  }