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

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,5 +1,6 @@
1
1
  import { reduceOtpFlexModes } from "../query";
2
2
  import queryParams, { getCustomQueryParams } from "../query-params";
3
+ import { extractAdditionalModes, generateCombinations } from "../query-gen";
3
4
 
4
5
  const customWalkDistanceOptions = [
5
6
  {
@@ -12,6 +13,189 @@ const customWalkDistanceOptions = [
12
13
  }
13
14
  ];
14
15
 
16
+ function modeStrToTransportMode(m) {
17
+ const splitVals = m.split("_");
18
+ return {
19
+ mode: splitVals[0],
20
+ qualifier: splitVals?.[1] || null
21
+ };
22
+ }
23
+
24
+ const mockLatLon = {
25
+ lat: 1,
26
+ lon: 2
27
+ };
28
+
29
+ function expectModes(modes, expectedModes) {
30
+ const generatedModesList = generateCombinations({
31
+ modes: modes.map(modeStrToTransportMode),
32
+ to: mockLatLon,
33
+ from: mockLatLon,
34
+ modeSettings: []
35
+ });
36
+ const expandedExpectedModesList = expectedModes.map(em => ({
37
+ modes: em.map(modeStrToTransportMode),
38
+ to: mockLatLon,
39
+ from: mockLatLon,
40
+ modeSettings: []
41
+ }));
42
+ return it(
43
+ modes.join(" "),
44
+ () =>
45
+ expect(generatedModesList.length === expandedExpectedModesList.length) &&
46
+ expect(new Set(generatedModesList)).toEqual(
47
+ new Set(expandedExpectedModesList)
48
+ )
49
+ );
50
+ }
51
+
52
+ describe("extract-modes", () => {
53
+ const mode = {
54
+ mode: "UNICYCLE"
55
+ };
56
+
57
+ const testTransportMode = {
58
+ mode: "testMode"
59
+ };
60
+
61
+ const checkboxModeSetting = {
62
+ type: "CHECKBOX",
63
+ icon: null,
64
+ label: "test",
65
+ applicableMode: testTransportMode.mode,
66
+ key: "test",
67
+ value: true,
68
+ addTransportMode: mode
69
+ };
70
+
71
+ const dropdownModeSetting = {
72
+ type: "DROPDOWN",
73
+ label: "test",
74
+ applicableMode: testTransportMode.mode,
75
+ key: "test",
76
+ options: [{ text: "testop", value: "1", addTransportMode: mode }],
77
+ value: "1"
78
+ };
79
+
80
+ it("a checkbox setting", () => {
81
+ expect(
82
+ extractAdditionalModes([checkboxModeSetting], [testTransportMode])
83
+ ).toEqual([mode]);
84
+ });
85
+ it("a dropdown setting", () => {
86
+ expect(
87
+ extractAdditionalModes([dropdownModeSetting], [testTransportMode])
88
+ ).toEqual([mode]);
89
+ });
90
+ it("a disabled mode setting", () => {
91
+ expect(
92
+ extractAdditionalModes(
93
+ [{ ...checkboxModeSetting, value: false }],
94
+ [testTransportMode]
95
+ )
96
+ ).toEqual([]);
97
+ });
98
+ it("a setting from a disabled mode", () => {
99
+ expect(extractAdditionalModes([{ ...checkboxModeSetting }], [])).toEqual(
100
+ []
101
+ );
102
+ });
103
+ });
104
+
105
+ describe("query-gen", () => {
106
+ describe("generateCombinations", () => {
107
+ expectModes(["WALK"], [["WALK"]]);
108
+ expectModes(["WALK", "TRANSIT"], [["WALK"], ["TRANSIT"]]);
109
+ expectModes(
110
+ ["WALK", "TRANSIT", "BICYCLE"],
111
+ [["WALK"], ["TRANSIT"], ["BICYCLE"], ["TRANSIT", "BICYCLE"]]
112
+ );
113
+ expectModes(
114
+ ["WALK", "TRANSIT", "CAR"],
115
+ [["WALK"], ["TRANSIT"], ["TRANSIT", "CAR"]]
116
+ );
117
+ expectModes(["TRANSIT", "CAR"], [["TRANSIT"], ["TRANSIT", "CAR"]]);
118
+ expectModes(["CAR"], []);
119
+ expectModes(
120
+ ["WALK", "TRANSIT", "BICYCLE", "CAR"],
121
+ [
122
+ ["WALK"],
123
+ ["TRANSIT"],
124
+ ["TRANSIT", "BICYCLE"],
125
+ ["BICYCLE"],
126
+ ["TRANSIT", "CAR"]
127
+ ]
128
+ );
129
+ expectModes(
130
+ ["BICYCLE_RENT", "TRANSIT", "WALK"],
131
+ [
132
+ ["TRANSIT"],
133
+ ["BICYCLE_RENT", "TRANSIT"],
134
+ ["BICYCLE_RENT", "WALK"],
135
+ ["WALK"]
136
+ ]
137
+ );
138
+ expectModes(
139
+ ["BICYCLE_RENT", "BICYCLE", "TRANSIT", "WALK"],
140
+ [
141
+ ["TRANSIT"],
142
+ ["BICYCLE_RENT", "TRANSIT"],
143
+ ["BICYCLE", "TRANSIT"],
144
+ ["BICYCLE_RENT", "WALK"],
145
+ ["BICYCLE"],
146
+ ["WALK"]
147
+ ]
148
+ );
149
+ expectModes(
150
+ ["SCOOTER_RENT", "BICYCLE_RENT", "TRANSIT", "WALK"],
151
+ [
152
+ ["TRANSIT"],
153
+ ["BICYCLE_RENT", "TRANSIT"],
154
+ ["BICYCLE_RENT", "WALK"],
155
+ ["SCOOTER_RENT", "TRANSIT"],
156
+ ["SCOOTER_RENT", "WALK"],
157
+ ["WALK"]
158
+ ]
159
+ );
160
+ expectModes(
161
+ ["FLEX", "TRANSIT", "WALK"],
162
+ [["TRANSIT"], ["FLEX", "TRANSIT"], ["FLEX", "WALK"], ["WALK"]]
163
+ );
164
+ expectModes(
165
+ ["FLEX", "SCOOTER_RENT", "TRANSIT", "WALK"],
166
+ [
167
+ ["TRANSIT"],
168
+ ["FLEX", "TRANSIT"],
169
+ ["WALK"],
170
+ ["FLEX", "WALK"],
171
+ ["FLEX", "SCOOTER_RENT", "WALK"], // Is this sensible?
172
+ ["FLEX", "SCOOTER_RENT", "TRANSIT"],
173
+ ["SCOOTER_RENT", "WALK"],
174
+ ["SCOOTER_RENT", "TRANSIT"]
175
+ ]
176
+ );
177
+ expectModes(
178
+ ["FLEX", "SCOOTER_RENT", "TRANSIT"],
179
+ [
180
+ ["TRANSIT"],
181
+ ["FLEX", "TRANSIT"],
182
+ ["FLEX", "SCOOTER_RENT", "TRANSIT"],
183
+ ["SCOOTER_RENT", "TRANSIT"]
184
+ ]
185
+ );
186
+ expectModes(
187
+ // Transit is required to enable other transit submodes
188
+ ["BUS", "RAIL", "GONDOLA", "TRAM", "TRANSIT"],
189
+ [["BUS", "RAIL", "GONDOLA", "TRAM"]]
190
+ );
191
+ expectModes(
192
+ // Transit is required to enable other transit submodes
193
+ ["TRANSIT"],
194
+ [["TRANSIT"]]
195
+ );
196
+ });
197
+ });
198
+
15
199
  describe("query-params", () => {
16
200
  describe("getCustomQueryParams", () => {
17
201
  it("should return the original unmodified queryParams if no customizations", () => {
@@ -0,0 +1,7 @@
1
+ declare module "*.graphql" {
2
+ import { DocumentNode } from "graphql";
3
+
4
+ const Schema: DocumentNode;
5
+
6
+ export = Schema;
7
+ }
package/src/index.ts CHANGED
@@ -7,12 +7,14 @@ import * as route from "./route";
7
7
  import * as storage from "./storage";
8
8
  import * as time from "./time";
9
9
  import * as ui from "./ui";
10
+ import * as queryGen from "./query-gen";
10
11
 
11
12
  const core = {
12
13
  itinerary,
13
14
  map,
14
15
  profile,
15
16
  query,
17
+ queryGen,
16
18
  queryParams,
17
19
  route,
18
20
  storage,
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,
@@ -212,7 +213,9 @@ export function getCompanyFromLeg(leg: Leg): string {
212
213
  return null;
213
214
  }
214
215
 
215
- export function getItineraryBounds(itinerary: Itinerary): LatLngArray[] {
216
+ export function getItineraryBounds(
217
+ itinerary: ItineraryOnlyLegsRequired
218
+ ): LatLngArray[] {
216
219
  let coords = [];
217
220
  itinerary.legs.forEach(leg => {
218
221
  const legCoords = polyline
@@ -407,7 +410,7 @@ export function getTNCLocation(leg: Leg, type: string): string {
407
410
  }
408
411
 
409
412
  export function calculatePhysicalActivity(
410
- itinerary: Itinerary
413
+ itinerary: ItineraryOnlyLegsRequired
411
414
  ): {
412
415
  bikeDuration: number;
413
416
  caloriesBurned: number;
@@ -433,7 +436,9 @@ export function calculatePhysicalActivity(
433
436
  * these values and currency info.
434
437
  * It is assumed that the same currency is used for all TNC legs.
435
438
  */
436
- export function calculateTncFares(itinerary: Itinerary): TncFare {
439
+ export function calculateTncFares(
440
+ itinerary: ItineraryOnlyLegsRequired
441
+ ): TncFare {
437
442
  return itinerary.legs
438
443
  .filter(leg => leg.mode === "CAR" && leg.hailedCar && leg.tncData)
439
444
  .reduce(
@@ -501,15 +506,16 @@ const CARBON_INTENSITY_DEFAULTS = {
501
506
  };
502
507
 
503
508
  /**
504
- * @param {itinerary} itinerary OTP trip itinierary
505
- * @param {carbonIntensity} carbonIntensity carbon intensity by mode in grams/meter
509
+ * @param {itinerary} itinerary OTP trip itinierary, only legs is required.
510
+ * @param {carbonIntensity} carbonIntensity carbon intensity by mode in grams/meter
506
511
  * @param {units} units units to be used in return value
507
512
  * @return Amount of carbon in chosen unit
508
513
  */
509
514
  export function calculateEmissions(
510
- itinerary: Itinerary,
515
+ // This type makes all the properties from Itinerary optional except legs.
516
+ itinerary: ItineraryOnlyLegsRequired,
511
517
  carbonIntensity: Record<string, number> = {},
512
- units?: string
518
+ units?: MassUnitOption
513
519
  ): number {
514
520
  // Apply defaults for any values that we don't have.
515
521
  const carbonIntensityWithDefaults = {
@@ -0,0 +1,141 @@
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
+ locale: "en",
22
+ time: $time,
23
+ date: $date,
24
+ wheelchair: $wheelchair,
25
+ bikeReluctance: $bikeReluctance,
26
+ carReluctance: $carReluctance,
27
+ walkReluctance: $walkReluctance,
28
+ arriveBy: $arriveBy,
29
+ intermediatePlaces: $intermediatePlaces,
30
+ preferred: $preferred,
31
+ unpreferred: $unpreferred,
32
+ banned: $banned,
33
+ ) {
34
+ itineraries {
35
+ duration
36
+ endTime
37
+ startTime
38
+ waitingTime
39
+ walkTime
40
+ legs {
41
+ distance
42
+ duration
43
+ endTime
44
+ mode
45
+ realTime
46
+ realtimeState
47
+ startTime
48
+ transitLeg
49
+ agency {
50
+ name
51
+ id
52
+ timezone
53
+ url
54
+ alerts {
55
+ alertHeaderText
56
+ alertDescriptionText
57
+ alertUrl
58
+ effectiveStartDate
59
+ }
60
+ }
61
+ legGeometry {
62
+ length
63
+ points
64
+ }
65
+ intermediateStops {
66
+ lat
67
+ lon
68
+ name
69
+ stopCode: code
70
+ stopId: id
71
+ locationType
72
+ }
73
+ route {
74
+ shortName
75
+ color
76
+ textColor
77
+ id
78
+ type
79
+ alerts {
80
+ alertHeaderText
81
+ alertDescriptionText
82
+ alertUrl
83
+ effectiveStartDate
84
+ }
85
+ }
86
+ from {
87
+ lat
88
+ lon
89
+ name
90
+ vertexType
91
+ stop {
92
+ id
93
+ code
94
+ alerts {
95
+ alertHeaderText
96
+ alertDescriptionText
97
+ alertUrl
98
+ effectiveStartDate
99
+ }
100
+ }
101
+ }
102
+ to {
103
+ lat
104
+ lon
105
+ name
106
+ vertexType
107
+ stop {
108
+ id
109
+ code
110
+ alerts {
111
+ alertHeaderText
112
+ alertDescriptionText
113
+ alertUrl
114
+ effectiveStartDate
115
+ }
116
+ }
117
+ }
118
+ steps {
119
+ distance
120
+ lat
121
+ lon
122
+ relativeDirection
123
+ absoluteDirection
124
+ stayOn
125
+ streetName
126
+ area
127
+ alerts{
128
+ alertHeaderText
129
+ alertDescriptionText
130
+ alertUrl
131
+ effectiveStartDate
132
+ }
133
+ elevationProfile {
134
+ distance
135
+ elevation
136
+ }
137
+ }
138
+ }
139
+ }
140
+ }
141
+ }
@@ -0,0 +1,206 @@
1
+ /* eslint-disable @typescript-eslint/no-unused-vars */
2
+ import { print } from "graphql";
3
+ import {
4
+ ModeSetting,
5
+ ModeSettingValues,
6
+ TransportMode
7
+ } from "@opentripplanner/types";
8
+ import { LonLatOutput } from "@conveyal/lonlat";
9
+ import PlanQuery from "./planQuery.graphql";
10
+
11
+ type OTPQueryParams = {
12
+ to: LonLatOutput & { name?: string };
13
+ from: LonLatOutput & { name?: string };
14
+ modes: Array<TransportMode>;
15
+ modeSettings: ModeSetting[];
16
+ };
17
+
18
+ type GraphQLQuery = {
19
+ query: string;
20
+ variables: Record<string, unknown>;
21
+ };
22
+
23
+ /**
24
+ * Mode Settings can contain additional modes to add to the query,
25
+ * this function extracts those additional modes from the settings
26
+ * and returns them in an array.
27
+ * @param modeSettings List of mode settings with values populated
28
+ * @returns Additional transport modes to add to query
29
+ */
30
+ export function extractAdditionalModes(
31
+ modeSettings: ModeSetting[],
32
+ enabledModes: TransportMode[]
33
+ ): Array<TransportMode> {
34
+ return modeSettings.reduce<TransportMode[]>((prev, cur) => {
35
+ // First, ensure that the mode associated with this setting is even enabled
36
+ if (!enabledModes.map(m => m.mode).includes(cur.applicableMode)) {
37
+ return prev;
38
+ }
39
+
40
+ // In checkboxes, mode must be enabled and have a transport mode in it
41
+ if (cur.type === "CHECKBOX" && cur.addTransportMode && cur.value) {
42
+ return [...prev, cur.addTransportMode];
43
+ }
44
+ if (cur.type === "DROPDOWN") {
45
+ const transportMode = cur.options.find(o => o.value === cur.value)
46
+ .addTransportMode;
47
+ if (transportMode) {
48
+ return [
49
+ ...prev,
50
+ cur.options.find(o => o.value === cur.value).addTransportMode
51
+ ];
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
+ export const SIMPLIFICATIONS = {
77
+ AIRPLANE: "TRANSIT",
78
+ BICYCLE: "PERSONAL",
79
+ BUS: "TRANSIT",
80
+ CABLE_CAR: "TRANSIT",
81
+ CAR: "CAR",
82
+ FERRY: "TRANSIT",
83
+ FLEX: "SHARED", // TODO: this allows FLEX+WALK. Is this reasonable?
84
+ FUNICULAR: "TRANSIT",
85
+ GONDOLA: "TRANSIT",
86
+ RAIL: "TRANSIT",
87
+ SCOOTER: "PERSONAL",
88
+ SUBWAY: "TRANSIT",
89
+ TRAM: "TRANSIT",
90
+ TRANSIT: "TRANSIT",
91
+ WALK: "WALK"
92
+ };
93
+
94
+ export const TRANSIT_SUBMODES = Object.keys(SIMPLIFICATIONS).filter(
95
+ mode => SIMPLIFICATIONS[mode] === "TRANSIT" && mode !== "TRANSIT"
96
+ );
97
+ export const TRANSIT_SUBMODES_AND_TRANSIT = Object.keys(SIMPLIFICATIONS).filter(
98
+ mode => SIMPLIFICATIONS[mode] === "TRANSIT"
99
+ );
100
+
101
+ /**
102
+ * Generates list of queries for OTP to get a comprehensive
103
+ * set of results based on the modes input.
104
+ * @param params OTP Query Params
105
+ * @returns Set of parameters to generate queries
106
+ */
107
+ export function generateCombinations(params: OTPQueryParams): OTPQueryParams[] {
108
+ const VALID_COMBOS = [
109
+ ["WALK"],
110
+ ["PERSONAL"],
111
+ ["TRANSIT", "SHARED"],
112
+ ["WALK", "SHARED"],
113
+ ["TRANSIT"],
114
+ ["TRANSIT", "PERSONAL"],
115
+ ["TRANSIT", "CAR"]
116
+ ];
117
+
118
+ const BANNED_TOGETHER = ["SCOOTER", "BICYCLE"];
119
+
120
+ const completeModeList = [
121
+ ...extractAdditionalModes(params.modeSettings, params.modes),
122
+ ...params.modes
123
+ ];
124
+
125
+ // List of the transit *submodes* that are included in the input params
126
+ const queryTransitSubmodes = completeModeList
127
+ .filter(mode => TRANSIT_SUBMODES.includes(mode.mode))
128
+ .map(mode => mode.mode);
129
+
130
+ return combinations(completeModeList)
131
+ .filter(combo => {
132
+ if (combo.length === 0) return false;
133
+
134
+ // All current qualifiers currently simplify to "SHARED"
135
+ const simplifiedModes = Array.from(
136
+ new Set(
137
+ combo.map(c => (c.qualifier ? "SHARED" : SIMPLIFICATIONS[c.mode]))
138
+ )
139
+ );
140
+
141
+ // Ensure that if we have one transit mode, then we include ALL transit modes
142
+ if (simplifiedModes.includes("TRANSIT")) {
143
+ // Don't allow TRANSIT along with any other submodes
144
+ if (
145
+ queryTransitSubmodes.length &&
146
+ combo.find(c => c.mode === "TRANSIT")
147
+ ) {
148
+ return false;
149
+ }
150
+
151
+ if (
152
+ combo.reduce((prev, cur) => {
153
+ if (queryTransitSubmodes.includes(cur.mode)) {
154
+ return prev - 1;
155
+ }
156
+ return prev;
157
+ }, queryTransitSubmodes.length) !== 0
158
+ ) {
159
+ return false;
160
+ }
161
+ // Continue to the other checks
162
+ }
163
+
164
+ // OTP doesn't support multiple non-walk modes
165
+ if (BANNED_TOGETHER.every(m => combo.find(c => c.mode === m)))
166
+ return false;
167
+
168
+ return !!VALID_COMBOS.find(
169
+ vc =>
170
+ simplifiedModes.every(m => vc.includes(m)) &&
171
+ vc.every(m => simplifiedModes.includes(m))
172
+ );
173
+ })
174
+ .map(combo => ({ ...params, modes: combo }));
175
+ }
176
+
177
+ // eslint-disable-next-line import/prefer-default-export
178
+ export function generateOtp2Query(params: OTPQueryParams): GraphQLQuery {
179
+ const { to, from, modeSettings } = params;
180
+
181
+ // This extracts the values from the mode settings to key value pairs
182
+ const modeSettingValues = modeSettings.reduce((prev, cur) => {
183
+ prev[cur.key] = cur.value;
184
+ return prev;
185
+ }, {}) as ModeSettingValues;
186
+
187
+ const {
188
+ walkReluctance,
189
+ wheelchair,
190
+ bikeReluctance,
191
+ carReluctance
192
+ } = modeSettingValues;
193
+
194
+ return {
195
+ query: print(PlanQuery),
196
+ variables: {
197
+ fromPlace: `${from.name}::${from.lat},${from.lon}}`,
198
+ toPlace: `${to.name}::${to.lat},${to.lon}}`,
199
+ modes: params.modes,
200
+ walkReluctance,
201
+ wheelchair,
202
+ bikeReluctance,
203
+ carReluctance
204
+ }
205
+ };
206
+ }
package/src/state.ts ADDED
File without changes
package/tsconfig.json CHANGED
@@ -2,6 +2,7 @@
2
2
  "extends": "../../tsconfig.json",
3
3
  "compilerOptions": {
4
4
  "composite": true,
5
+ "target": "es2019",
5
6
  "outDir": "./lib",
6
7
  "rootDir": "./src",
7
8
  "skipLibCheck": true