@opentripplanner/core-utils 13.0.0-alpha.4 → 13.0.0

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.
Files changed (61) hide show
  1. package/README.md +0 -4
  2. package/esm/__mocks__/fake-route-data.story.json +5425 -0
  3. package/esm/__mocks__/fake-transit-operators.story.json +97 -0
  4. package/esm/__tests__/__mocks__/routes.json +234 -105
  5. package/esm/itinerary.js +0 -2
  6. package/esm/itinerary.js.map +1 -1
  7. package/esm/otpSchema.json +0 -12
  8. package/esm/profile.js +1 -1
  9. package/esm/profile.js.map +1 -1
  10. package/esm/query-gen.js +2 -4
  11. package/esm/query-gen.js.map +1 -1
  12. package/esm/query.js +1 -4
  13. package/esm/query.js.map +1 -1
  14. package/esm/route.js +36 -21
  15. package/esm/route.js.map +1 -1
  16. package/esm/routes.json +234 -105
  17. package/esm/storage.js +2 -6
  18. package/esm/storage.js.map +1 -1
  19. package/esm/time.js +10 -0
  20. package/esm/time.js.map +1 -1
  21. package/lib/__mocks__/fake-route-data.story.json +5425 -0
  22. package/lib/__mocks__/fake-transit-operators.story.json +97 -0
  23. package/lib/__tests__/__mocks__/routes.json +234 -105
  24. package/lib/itinerary.d.ts.map +1 -1
  25. package/lib/itinerary.js +0 -2
  26. package/lib/itinerary.js.map +1 -1
  27. package/lib/otpSchema.json +0 -12
  28. package/lib/profile.js +1 -1
  29. package/lib/profile.js.map +1 -1
  30. package/lib/query-gen.js +2 -2
  31. package/lib/query-gen.js.map +1 -1
  32. package/lib/query.js +1 -4
  33. package/lib/query.js.map +1 -1
  34. package/lib/route.d.ts +35 -1
  35. package/lib/route.d.ts.map +1 -1
  36. package/lib/route.js +38 -17
  37. package/lib/route.js.map +1 -1
  38. package/lib/storage.d.ts.map +1 -1
  39. package/lib/storage.js +2 -6
  40. package/lib/storage.js.map +1 -1
  41. package/lib/time.d.ts +7 -0
  42. package/lib/time.d.ts.map +1 -1
  43. package/lib/time.js +10 -1
  44. package/lib/time.js.map +1 -1
  45. package/package.json +3 -3
  46. package/src/__mocks__/fake-route-data.story.json +5425 -0
  47. package/src/__mocks__/fake-transit-operators.story.json +97 -0
  48. package/src/__snapshots__/core-utils.story.tsx.snap +8382 -0
  49. package/src/__tests__/__mocks__/routes.json +234 -105
  50. package/src/__tests__/__snapshots__/route.js.snap +285 -95
  51. package/src/__tests__/route.js +27 -55
  52. package/src/core-utils.story.tsx +201 -2
  53. package/src/itinerary.ts +0 -2
  54. package/src/otpSchema.json +0 -12
  55. package/src/profile.js +1 -1
  56. package/src/query-gen.ts +2 -2
  57. package/src/query.js +1 -4
  58. package/src/route.ts +41 -21
  59. package/src/storage.ts +5 -9
  60. package/src/time.ts +9 -0
  61. package/tsconfig.tsbuildinfo +1 -1
@@ -1,6 +1,17 @@
1
1
  import React, { useState } from "react";
2
2
  import styled from "styled-components";
3
- import { getMostReadableTextColor } from "./route";
3
+ import {
4
+ alphabeticShortNameComparator,
5
+ getMostReadableTextColor,
6
+ getRouteSortOrderValue,
7
+ makeMultiCriteriaSort,
8
+ makeNumericValueComparator,
9
+ makeStringValueComparator,
10
+ makeTransitOperatorComparator,
11
+ routeTypeComparator
12
+ } from "./route";
13
+ import { routes } from "./__mocks__/fake-route-data.story.json";
14
+ import { fakeTransitOperators } from "./__mocks__/fake-transit-operators.story.json";
4
15
 
5
16
  export default {
6
17
  title: "core-utils"
@@ -50,8 +61,196 @@ export const RouteColorTester = (): JSX.Element => {
50
61
  </>
51
62
  );
52
63
  };
53
- // Disable color contrast checking for the uncorrected color pairs.
64
+ // Disable color contrast checking for the uncorrected color pairs
54
65
  RouteColorTester.parameters = {
55
66
  a11y: { config: { rules: [{ id: "color-contrast", reviewOnFail: true }] } },
56
67
  storyshots: { disable: true }
57
68
  };
69
+
70
+ // Route sort logic story:
71
+
72
+ const Columns = styled.div`
73
+ display: flex;
74
+ flex-direction: row;
75
+ position: relative;
76
+ width: 100%;
77
+ `;
78
+
79
+ const StyledTable = styled.table`
80
+ td,
81
+ th {
82
+ border: 1px solid black;
83
+ padding: 2px;
84
+ }
85
+
86
+ tr {
87
+ background-color: white;
88
+ display: grid;
89
+ grid-template-columns: 1fr 2fr 2fr 4fr 2fr 2fr;
90
+ width: 75%;
91
+ img {
92
+ height: 30px;
93
+ width: 30px;
94
+ }
95
+ }
96
+ `;
97
+
98
+ function makeRouteComparator(sortArray): (a: number, b: number) => number {
99
+ return makeMultiCriteriaSort(
100
+ ...(sortArray as Array<(a: any, b: any) => number>)
101
+ );
102
+ }
103
+
104
+ /**
105
+ * This is based on the logic in the makeRouteComparator function in route.ts
106
+ * If another route comparator is added to makeRouteComparator, this component
107
+ * will need to be updated to reflect the new comparator.
108
+ */
109
+ export const RouteSortingLogic = (): JSX.Element => {
110
+ const [useOperatorComparator, setUseOperatorComparator] = useState(true);
111
+ const [useSortOrderComparator, setUseSortOrderComparator] = useState(true);
112
+ const [useRouteComparator, setUseRouteComparator] = useState(true);
113
+ const [useAlphaSortName, setUseAlphaSortName] = useState(true);
114
+ const [useNumericShortName, setUseNumericShortName] = useState(true);
115
+ const [useStringShortName, setUseStringShortName] = useState(true);
116
+ const [useStringLongName, setUseLongName] = useState(true);
117
+
118
+ const transitOperatorComparator = useOperatorComparator
119
+ ? makeTransitOperatorComparator(fakeTransitOperators)
120
+ : null;
121
+ const sortOrderComparator = useSortOrderComparator
122
+ ? makeNumericValueComparator(obj => getRouteSortOrderValue(obj))
123
+ : null;
124
+ const routeComparator = useRouteComparator ? routeTypeComparator : null;
125
+ const alphaShortName = useAlphaSortName
126
+ ? alphabeticShortNameComparator
127
+ : null;
128
+ const numericShortName = useNumericShortName
129
+ ? makeNumericValueComparator(obj => parseInt(obj.shortName, 10))
130
+ : null;
131
+ const stringShortName = useStringShortName
132
+ ? makeStringValueComparator(obj => obj.shortName)
133
+ : null;
134
+ const stringLongName = useStringLongName
135
+ ? makeStringValueComparator(obj => obj.longName || "")
136
+ : null;
137
+
138
+ const sortArray = [
139
+ transitOperatorComparator,
140
+ sortOrderComparator,
141
+ routeComparator,
142
+ alphaShortName,
143
+ numericShortName,
144
+ stringShortName,
145
+ stringLongName
146
+ ].filter(x => x !== null);
147
+
148
+ const sortedRoutes = Array.from(routes as Array<any>).sort(
149
+ makeRouteComparator(sortArray)
150
+ );
151
+
152
+ return (
153
+ <Columns>
154
+ <StyledTable>
155
+ <tr>
156
+ <th>Logo</th>
157
+ <th>Mode</th>
158
+ <th>Short Name</th>
159
+ <th>Long Name</th>
160
+ <th>Agency Order</th>
161
+ <th>Sort Order</th>
162
+ </tr>
163
+ {sortedRoutes.map(r => {
164
+ const operator = fakeTransitOperators.find(
165
+ x => x.agencyId === r.agency?.id
166
+ );
167
+ return (
168
+ <tr key={r.id}>
169
+ <td>
170
+ <img src={operator?.logo} alt={r.agency?.name || ""} />
171
+ </td>
172
+ <td>{r.mode}</td>
173
+ <td>{r.shortName}</td>
174
+ <td>{r.longName}</td>
175
+ <td>{operator?.order}</td>
176
+ <td>{r.sortOrder}</td>
177
+ </tr>
178
+ );
179
+ })}
180
+ </StyledTable>
181
+ <div
182
+ style={{
183
+ display: "flex",
184
+ flexDirection: "column",
185
+ position: "fixed",
186
+ right: 0,
187
+ width: "25%"
188
+ }}
189
+ >
190
+ <label htmlFor="operator-comparator">
191
+ <input
192
+ checked={useOperatorComparator}
193
+ id="operator-comparator"
194
+ onChange={() => setUseOperatorComparator(!useOperatorComparator)}
195
+ type="checkbox"
196
+ />
197
+ Transit Operator Comparator
198
+ </label>
199
+ <label htmlFor="sort-order-comparator">
200
+ <input
201
+ id="sort-order-comparator"
202
+ type="checkbox"
203
+ checked={useSortOrderComparator}
204
+ onChange={() => setUseSortOrderComparator(!useSortOrderComparator)}
205
+ />
206
+ SortOrder Comparator
207
+ </label>
208
+ <label htmlFor="route-comparator">
209
+ <input
210
+ id="route-comparator"
211
+ type="checkbox"
212
+ checked={useRouteComparator}
213
+ onChange={() => setUseRouteComparator(!useRouteComparator)}
214
+ />
215
+ Route Comparator
216
+ </label>
217
+ <label htmlFor="alpha-sort-name">
218
+ <input
219
+ id="alpha-sort-name"
220
+ type="checkbox"
221
+ checked={useAlphaSortName}
222
+ onChange={() => setUseAlphaSortName(!useAlphaSortName)}
223
+ />
224
+ Alpha Short Name
225
+ </label>
226
+ <label htmlFor="numeric-short-name">
227
+ <input
228
+ id="numeric-short-name"
229
+ type="checkbox"
230
+ checked={useNumericShortName}
231
+ onChange={() => setUseNumericShortName(!useNumericShortName)}
232
+ />
233
+ Numeric Short Name
234
+ </label>
235
+ <label htmlFor="string-short-name">
236
+ <input
237
+ id="string-short-name"
238
+ type="checkbox"
239
+ checked={useStringShortName}
240
+ onChange={() => setUseStringShortName(!useStringShortName)}
241
+ />
242
+ String Short Name
243
+ </label>
244
+ <label htmlFor="string-long-name">
245
+ <input
246
+ id="string-long-name"
247
+ type="checkbox"
248
+ checked={useStringLongName}
249
+ onChange={() => setUseLongName(!useStringLongName)}
250
+ />
251
+ String Long Name
252
+ </label>
253
+ </div>
254
+ </Columns>
255
+ );
256
+ };
package/src/itinerary.ts CHANGED
@@ -104,8 +104,6 @@ export function isFlex(leg: Leg): boolean {
104
104
  legContainsGeometry(leg)
105
105
  );
106
106
  }
107
-
108
- // alpha-only comment
109
107
  export function isRideshareLeg(leg: Leg): boolean {
110
108
  return !!leg.rideHailingEstimate?.provider?.id;
111
109
  }
@@ -9671,18 +9671,6 @@
9671
9671
  "isDeprecated": false,
9672
9672
  "deprecationReason": null
9673
9673
  },
9674
- {
9675
- "name": "stopPosition",
9676
- "description": "The sequence of the stop in the pattern. This is not required to start from 0 or be consecutive - any\nincreasing integer sequence along the stops is valid.\n\nThe purpose of this field is to identify the stop within the pattern so it can be cross-referenced\nbetween it and the itinerary. It is safe to cross-reference when done quickly, i.e. within seconds.\nHowever, it should be noted that realtime updates can change the values, so don't store it for\nlonger amounts of time.\n\nDepending on the source data, this might not be the GTFS `stop_sequence` but another value, perhaps\neven generated.",
9677
- "args": [],
9678
- "type": {
9679
- "kind": "SCALAR",
9680
- "name": "Int",
9681
- "ofType": null
9682
- },
9683
- "isDeprecated": false,
9684
- "deprecationReason": null
9685
- },
9686
9674
  {
9687
9675
  "name": "scheduledArrival",
9688
9676
  "description": "Scheduled arrival time. Format: seconds since midnight of the departure date",
package/src/profile.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export function filterProfileOptions(response) {
2
- // Filter out similar options. TODO: handle on server??
2
+ // Filter out similar options. TODO: handle on server?
3
3
  const optStrs = [];
4
4
  const filteredIndices = [];
5
5
 
package/src/query-gen.ts CHANGED
@@ -58,7 +58,7 @@ export function extractAdditionalModes(
58
58
  return prev;
59
59
  }
60
60
 
61
- // In checkboxes (or submode checkboxes), mode must be enabled and have a transport mode in it
61
+ // In checkboxes, mode must be enabled and have a transport mode in it
62
62
  if (
63
63
  (cur.type === "CHECKBOX" || cur.type === "SUBMODE") &&
64
64
  cur.addTransportMode &&
@@ -180,7 +180,7 @@ function isCombinationValid(
180
180
 
181
181
  return !!VALID_COMBOS.find(
182
182
  vc =>
183
- simplifiedModes.every(m => vc.includes(m)) &&
183
+ simplifiedModes.length === vc.length &&
184
184
  vc.every(m => simplifiedModes.includes(m))
185
185
  );
186
186
  }
package/src/query.js CHANGED
@@ -72,10 +72,7 @@ export function ensureSingleAccessMode(queryModes) {
72
72
  }
73
73
 
74
74
  export function getUrlParams() {
75
- if (window) {
76
- return qs.parse(window.location.href.split("?")[1]);
77
- }
78
- return undefined;
75
+ return qs.parse(window.location.href.split("?")[1]);
79
76
  }
80
77
 
81
78
  export function getOtpUrlParams() {
package/src/route.ts CHANGED
@@ -65,10 +65,10 @@ export function getTransitOperatorFromOtpRoute(
65
65
  const feedId = route.id.split(":")[0];
66
66
  let agencyId: string | number;
67
67
  if (route.agency) {
68
- // This is returned in the OTP Route model
68
+ // This is returned in OTP2
69
69
  agencyId = route.agency.id;
70
70
  } else if (route.agencyId) {
71
- // This is returned in the OTP RouteShort model (such as in the IBI fork)
71
+ // This is returned in OTP1
72
72
  agencyId = route.agencyId;
73
73
  } else {
74
74
  return null;
@@ -112,9 +112,9 @@ function getTransitOperatorComparatorValue(
112
112
  // if the transitOperators is undefined or has zero length, use the route's
113
113
  // agency name as the comparator value
114
114
  if (!transitOperators || transitOperators.length === 0) {
115
- // OTP Route
115
+ // OTP2 Route
116
116
  if (route.agency) return route.agency.name;
117
- // OTP RouteShort (base OTP repo or IBI fork)
117
+ // OTP1 Route
118
118
  if (route.agencyName) return route.agencyName;
119
119
  // shouldn't happen as agency names will be defined
120
120
  return "zzz";
@@ -140,7 +140,9 @@ function getTransitOperatorComparatorValue(
140
140
  * Calculates the sort comparator value given two routes based off of the
141
141
  * route's agency and provided transitOperators config data.
142
142
  */
143
- function makeTransitOperatorComparator(transitOperators: TransitOperator[]) {
143
+ export function makeTransitOperatorComparator(
144
+ transitOperators: TransitOperator[]
145
+ ) {
144
146
  return (a: Route, b: Route) => {
145
147
  const aVal = getTransitOperatorComparatorValue(a, transitOperators);
146
148
  const bVal = getTransitOperatorComparatorValue(b, transitOperators);
@@ -237,7 +239,7 @@ function getRouteTypeComparatorValue(route: Route): number {
237
239
  * Calculates the sort comparator value given two routes based off of route type
238
240
  * (OTP mode).
239
241
  */
240
- function routeTypeComparator(a: Route, b: Route): number {
242
+ export function routeTypeComparator(a: Route, b: Route): number {
241
243
  return getRouteTypeComparatorValue(a) - getRouteTypeComparatorValue(b);
242
244
  }
243
245
 
@@ -261,7 +263,7 @@ function startsWithAlphabeticCharacter(val: unknown): boolean {
261
263
  * character. Routes with shortn that do start with an alphabetic character will
262
264
  * be prioritized over those that don't.
263
265
  */
264
- function alphabeticShortNameComparator(a: Route, b: Route): number {
266
+ export function alphabeticShortNameComparator(a: Route, b: Route): number {
265
267
  const aStartsWithAlphabeticCharacter = startsWithAlphabeticCharacter(
266
268
  a.shortName
267
269
  );
@@ -282,6 +284,14 @@ function alphabeticShortNameComparator(a: Route, b: Route): number {
282
284
  return 0;
283
285
  }
284
286
 
287
+ const isNullOrNaN = (val: any): boolean => {
288
+ // isNaN(null) returns false so we have to check for null explicitly.
289
+ // Note: Using the global version of isNaN (the Number version behaves differently.
290
+ // eslint-disable-next-line no-restricted-globals
291
+ if (typeof val === null || isNaN(val)) return true;
292
+
293
+ return typeof val !== "number";
294
+ };
285
295
  /**
286
296
  * Checks whether an appropriate comparison of numeric values can be made for
287
297
  * sorting purposes. If both values are not valid numbers according to the
@@ -304,19 +314,21 @@ function alphabeticShortNameComparator(a: Route, b: Route): number {
304
314
  export function makeNumericValueComparator(
305
315
  objGetterFn?: (item: Route) => number
306
316
  ) {
307
- /* Note: Using the global version of isNaN (the Number version behaves differently. */
308
- /* eslint-disable no-restricted-globals */
309
- return (a: number, b: number): number => {
317
+ return (a: number, b: number): number | null => {
310
318
  const { aVal, bVal } = getSortValues(objGetterFn, a, b);
311
- if (typeof aVal !== "number" || typeof bVal !== "number") return 0;
312
319
 
313
320
  // if both values aren't valid numbers, use the next sort criteria
314
- if (isNaN(aVal) && isNaN(bVal)) return 0;
321
+ if (isNullOrNaN(aVal) && isNullOrNaN(bVal)) {
322
+ return 0;
323
+ }
315
324
  // b is a valid number, b gets priority
316
- if (isNaN(aVal)) return 1;
325
+ if (isNullOrNaN(aVal)) return 1;
326
+
317
327
  // a is a valid number, a gets priority
318
- if (isNaN(bVal)) return -1;
328
+ if (isNullOrNaN(bVal)) return -1;
329
+
319
330
  // a and b are valid numbers, return the sort value
331
+ // @ts-expect-error We know from the checks above that both aVal and bVal are valid numbers.
320
332
  return aVal - bVal;
321
333
  };
322
334
  }
@@ -350,15 +362,23 @@ export function makeStringValueComparator(
350
362
  }
351
363
 
352
364
  /**
353
- * OpenTripPlanner sets the routeSortOrder to -999 by default. So, if that value
354
- * is encountered, assume that it actually means that the routeSortOrder is not
355
- * set in the GTFS.
365
+ * OTP1 sets the routeSortOrder to -999 by default. If we're encountering that value in OTP1,
366
+ * assume that it actually means that the route sortOrder is not set in the GTFS. If we encounter
367
+ * it in OTP2, it's a valid value, so we should return it.
356
368
  *
357
369
  * See https://github.com/opentripplanner/OpenTripPlanner/issues/2938
358
370
  * Also see https://github.com/opentripplanner/otp-react-redux/issues/122
371
+ * This was updated in OTP2 TO be empty by default. https://docs.opentripplanner.org/en/v2.3.0/OTP2-MigrationGuide/#:~:text=the%20Alerts-,Changes%20to%20the%20Index%20API,-Error%20handling%20is
359
372
  */
360
- function getRouteSortOrderValue(val: number): number {
361
- return val === -999 ? undefined : val;
373
+ export function getRouteSortOrderValue(route: Route): number {
374
+ const isOTP1 = !!route.agencyId;
375
+ const { sortOrder } = route;
376
+
377
+ if ((isOTP1 && sortOrder === -999) || sortOrder === undefined) {
378
+ return null;
379
+ }
380
+
381
+ return sortOrder;
362
382
  }
363
383
 
364
384
  /**
@@ -368,7 +388,7 @@ function getRouteSortOrderValue(val: number): number {
368
388
  * returned. If all comparison functions return equivalence, then the values
369
389
  * are assumed to be equivalent.
370
390
  */
371
- function makeMultiCriteriaSort(
391
+ export function makeMultiCriteriaSort(
372
392
  ...criteria: ((a: unknown, b: unknown) => number)[]
373
393
  ) {
374
394
  return (a: number, b: number): number => {
@@ -420,7 +440,7 @@ export function makeRouteComparator(
420
440
  ): (a: number, b: number) => number {
421
441
  return makeMultiCriteriaSort(
422
442
  makeTransitOperatorComparator(transitOperators),
423
- makeNumericValueComparator(obj => getRouteSortOrderValue(obj.sortOrder)),
443
+ makeNumericValueComparator(obj => getRouteSortOrderValue(obj)),
424
444
  routeTypeComparator,
425
445
  alphabeticShortNameComparator,
426
446
  makeNumericValueComparator(obj => parseInt(obj.shortName, 10)),
package/src/storage.ts CHANGED
@@ -6,12 +6,10 @@ const STORAGE_PREFIX = "otp";
6
6
  * Store a javascript object at the specified key.
7
7
  */
8
8
  export function storeItem(key: string, object: unknown): void {
9
- if (window) {
10
- window.localStorage.setItem(
11
- `${STORAGE_PREFIX}.${key}`,
12
- JSON.stringify(object)
13
- );
14
- }
9
+ window.localStorage.setItem(
10
+ `${STORAGE_PREFIX}.${key}`,
11
+ JSON.stringify(object)
12
+ );
15
13
  }
16
14
 
17
15
  /**
@@ -36,9 +34,7 @@ export function getItem(key: string, notFoundValue: unknown = null): unknown {
36
34
  * Remove item at specified key.
37
35
  */
38
36
  export function removeItem(key: string): void {
39
- if (window) {
40
- window.localStorage.removeItem(`${STORAGE_PREFIX}.${key}`);
41
- }
37
+ window.localStorage.removeItem(`${STORAGE_PREFIX}.${key}`);
42
38
  }
43
39
 
44
40
  /**
package/src/time.ts CHANGED
@@ -26,6 +26,15 @@ export function toHoursMinutesSeconds(
26
26
  };
27
27
  }
28
28
 
29
+ /**
30
+ * If a duration is less than 60 seconds, round it to one minute, to avoid a duration
31
+ * of 0 minutes on a leg.
32
+ * @param {number} duration The leg or trip duration in seconds
33
+ * @returns a duration in seconds of at least 60 seconds.
34
+ */
35
+ export const ensureAtLeastOneMinute = (duration: number): number =>
36
+ duration < 60 ? 60 : duration;
37
+
29
38
  /**
30
39
  * @param {[type]} config the OTP config object found in store
31
40
  * @return {string} the config-defined time formatter or HH:mm (24-hr time)