@opentripplanner/core-utils 12.0.2 → 12.2.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.
@@ -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"
@@ -55,3 +66,191 @@ 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/query-gen.ts CHANGED
@@ -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/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/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)