@opentripplanner/core-utils 15.0.0-alpha.2 → 16.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.
- package/README.md +0 -3
- package/esm/__tests__/__mocks__/three-transfer-itinerary.json +2195 -0
- package/esm/index.js +3 -0
- package/esm/index.js.map +1 -1
- package/esm/itinerary.js +113 -109
- package/esm/itinerary.js.map +1 -1
- package/esm/map.js +2 -2
- package/esm/map.js.map +1 -1
- package/esm/otpSchema.json +1 -1
- package/esm/profile.js +1 -1
- package/esm/profile.js.map +1 -1
- package/esm/query-gen.js +10 -6
- package/esm/query-gen.js.map +1 -1
- package/esm/query.js +1 -4
- package/esm/query.js.map +1 -1
- package/esm/route.js +26 -20
- package/esm/route.js.map +1 -1
- package/esm/storage.js +6 -7
- package/esm/storage.js.map +1 -1
- package/esm/three-transfer-itinerary.json +2195 -0
- package/esm/time.js +6 -5
- package/esm/time.js.map +1 -1
- package/esm/ui.js +4 -2
- package/esm/ui.js.map +1 -1
- package/lib/__tests__/__mocks__/three-transfer-itinerary.json +2195 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +40 -50
- package/lib/index.js.map +1 -1
- package/lib/itinerary.d.ts +21 -18
- package/lib/itinerary.d.ts.map +1 -1
- package/lib/itinerary.js +560 -540
- package/lib/itinerary.js.map +1 -1
- package/lib/map.d.ts +2 -2
- package/lib/map.d.ts.map +1 -1
- package/lib/map.js +39 -40
- package/lib/map.js.map +1 -1
- package/lib/otpSchema.json +1 -1
- package/lib/profile.js +1 -1
- package/lib/profile.js.map +1 -1
- package/lib/query-gen.d.ts +1 -19
- package/lib/query-gen.d.ts.map +1 -1
- package/lib/query-gen.js +142 -134
- package/lib/query-gen.js.map +1 -1
- package/lib/query.js +1 -4
- package/lib/query.js.map +1 -1
- package/lib/route.d.ts +10 -8
- package/lib/route.d.ts.map +1 -1
- package/lib/route.js +255 -231
- package/lib/route.js.map +1 -1
- package/lib/storage.d.ts +1 -1
- package/lib/storage.d.ts.map +1 -1
- package/lib/storage.js +28 -27
- package/lib/storage.js.map +1 -1
- package/lib/suspense.d.ts.map +1 -1
- package/lib/suspense.js +15 -28
- package/lib/suspense.js.map +1 -1
- package/lib/time.d.ts +3 -1
- package/lib/time.d.ts.map +1 -1
- package/lib/time.js +49 -36
- package/lib/time.js.map +1 -1
- package/lib/ui.d.ts.map +1 -1
- package/lib/ui.js +38 -33
- package/lib/ui.js.map +1 -1
- package/package.json +9 -7
- package/src/__snapshots__/core-utils.story.tsx.snap +1 -1
- package/src/__tests__/__mocks__/three-transfer-itinerary.json +2195 -0
- package/src/__tests__/itinerary.ts +97 -25
- package/src/index.ts +3 -0
- package/src/itinerary.ts +158 -126
- package/src/map.ts +5 -3
- package/src/otpSchema.json +1 -1
- package/src/profile.js +1 -1
- package/src/query-gen.ts +16 -10
- package/src/query.js +1 -4
- package/src/route.ts +65 -38
- package/src/storage.ts +13 -11
- package/src/time.ts +7 -6
- package/src/ui.ts +8 -6
- package/tsconfig.json +1 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/itinerary.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
Currency,
|
|
7
7
|
ElevationProfile,
|
|
8
8
|
ElevationProfileComponent,
|
|
9
|
+
FareProduct,
|
|
9
10
|
FlexBookingInfo,
|
|
10
11
|
ItineraryOnlyLegsRequired,
|
|
11
12
|
LatLngArray,
|
|
@@ -86,11 +87,12 @@ export function startsWithGeometry(leg: Leg): boolean {
|
|
|
86
87
|
export function legContainsGeometry(leg: Leg): boolean {
|
|
87
88
|
return endsWithGeometry(leg) || startsWithGeometry(leg);
|
|
88
89
|
}
|
|
89
|
-
export function isAdvanceBookingRequired(info
|
|
90
|
-
|
|
90
|
+
export function isAdvanceBookingRequired(info?: FlexBookingInfo): boolean {
|
|
91
|
+
const daysPrior = info?.latestBookingTime?.daysPrior;
|
|
92
|
+
return typeof daysPrior === "number" && daysPrior > 0;
|
|
91
93
|
}
|
|
92
94
|
export function legDropoffRequiresAdvanceBooking(leg: Leg): boolean {
|
|
93
|
-
return isAdvanceBookingRequired(leg
|
|
95
|
+
return isAdvanceBookingRequired(leg.dropOffBookingInfo);
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
/**
|
|
@@ -106,8 +108,6 @@ export function isFlex(leg: Leg): boolean {
|
|
|
106
108
|
) || false
|
|
107
109
|
);
|
|
108
110
|
}
|
|
109
|
-
|
|
110
|
-
// alpha-only comment
|
|
111
111
|
export function isRideshareLeg(leg: Leg): boolean {
|
|
112
112
|
return !!leg.rideHailingEstimate?.provider?.id;
|
|
113
113
|
}
|
|
@@ -201,6 +201,7 @@ export function hasRental(modesStr: string): boolean {
|
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
export function getMapColor(mode: string): string {
|
|
204
|
+
// @ts-expect-error this is not typed
|
|
204
205
|
mode = mode || this.get("mode");
|
|
205
206
|
if (mode === "WALK") return "#444";
|
|
206
207
|
if (mode === "BICYCLE") return "#0073e5";
|
|
@@ -226,7 +227,7 @@ export function toSentenceCase(str: string): string {
|
|
|
226
227
|
/**
|
|
227
228
|
* Derive the company string based on mode and network associated with leg.
|
|
228
229
|
*/
|
|
229
|
-
export function getCompanyFromLeg(leg
|
|
230
|
+
export function getCompanyFromLeg(leg?: Leg): string | null {
|
|
230
231
|
if (!leg) return null;
|
|
231
232
|
const {
|
|
232
233
|
from,
|
|
@@ -236,14 +237,20 @@ export function getCompanyFromLeg(leg: Leg): string {
|
|
|
236
237
|
rentedVehicle,
|
|
237
238
|
rideHailingEstimate
|
|
238
239
|
} = leg;
|
|
240
|
+
|
|
241
|
+
const firstNetwork =
|
|
242
|
+
Array.isArray(from.networks) && from.networks.length > 0
|
|
243
|
+
? from.networks[0]
|
|
244
|
+
: null;
|
|
245
|
+
|
|
239
246
|
if (mode === "CAR" && rentedCar) {
|
|
240
|
-
return
|
|
247
|
+
return firstNetwork;
|
|
241
248
|
}
|
|
242
249
|
if (mode === "CAR" && rideHailingEstimate) {
|
|
243
250
|
return rideHailingEstimate.provider.id;
|
|
244
251
|
}
|
|
245
|
-
if (mode === "BICYCLE" && rentedBike
|
|
246
|
-
return
|
|
252
|
+
if (mode === "BICYCLE" && rentedBike) {
|
|
253
|
+
return firstNetwork;
|
|
247
254
|
}
|
|
248
255
|
if (from.rentalVehicle) {
|
|
249
256
|
return from.rentalVehicle.network;
|
|
@@ -251,12 +258,8 @@ export function getCompanyFromLeg(leg: Leg): string {
|
|
|
251
258
|
if (from.vehicleRentalStation?.rentalNetwork) {
|
|
252
259
|
return from.vehicleRentalStation.rentalNetwork.networkId;
|
|
253
260
|
}
|
|
254
|
-
if (
|
|
255
|
-
|
|
256
|
-
rentedVehicle &&
|
|
257
|
-
from.networks
|
|
258
|
-
) {
|
|
259
|
-
return from.networks[0];
|
|
261
|
+
if ((mode === "MICROMOBILITY" || mode === "SCOOTER") && rentedVehicle) {
|
|
262
|
+
return firstNetwork;
|
|
260
263
|
}
|
|
261
264
|
return null;
|
|
262
265
|
}
|
|
@@ -264,12 +267,12 @@ export function getCompanyFromLeg(leg: Leg): string {
|
|
|
264
267
|
export function getItineraryBounds(
|
|
265
268
|
itinerary: ItineraryOnlyLegsRequired
|
|
266
269
|
): LatLngArray[] {
|
|
267
|
-
|
|
270
|
+
const coords: LatLngArray[] = [];
|
|
268
271
|
itinerary.legs.forEach(leg => {
|
|
269
272
|
const legCoords = polyline
|
|
270
273
|
.toGeoJSON(leg.legGeometry.points)
|
|
271
|
-
.coordinates.map((c:
|
|
272
|
-
coords
|
|
274
|
+
.coordinates.map((c): LatLngArray => [c[1], c[0]]);
|
|
275
|
+
coords.push(...legCoords);
|
|
273
276
|
});
|
|
274
277
|
return coords;
|
|
275
278
|
}
|
|
@@ -292,62 +295,56 @@ export function getLegBounds(leg: Leg): number[][] {
|
|
|
292
295
|
}
|
|
293
296
|
|
|
294
297
|
/* Returns an interpolated lat-lon at a specified distance along a leg */
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
+
export function legLocationAtDistance(
|
|
299
|
+
leg: Leg,
|
|
300
|
+
distance: number
|
|
301
|
+
): LatLngArray | undefined | null {
|
|
302
|
+
if (!leg.legGeometry) return undefined;
|
|
298
303
|
|
|
299
304
|
try {
|
|
300
305
|
const line = polyline.toGeoJSON(leg.legGeometry.points);
|
|
301
306
|
const pt = turfAlong(line, distance, { units: "meters" });
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
// FIXME handle error!
|
|
307
|
+
const coords = pt?.geometry?.coordinates;
|
|
308
|
+
return [coords[1], coords[0]];
|
|
309
|
+
} catch {
|
|
310
|
+
// This is designed to catch the toGeoJSON from throwing if the geometry is not in the correct format
|
|
307
311
|
}
|
|
308
312
|
|
|
309
313
|
return null;
|
|
310
314
|
}
|
|
311
315
|
|
|
312
|
-
|
|
316
|
+
/**
|
|
317
|
+
* Returns an interpolated elevation at a specified distance along a leg
|
|
318
|
+
* @param points - The points of the elevation profile. Each point is a tuple of [distance, elevation].
|
|
319
|
+
* @param distance - The distance along the leg to interpolate the elevation at
|
|
320
|
+
* @returns The interpolated elevation at the specified distance
|
|
321
|
+
*/
|
|
313
322
|
|
|
314
323
|
export function legElevationAtDistance(
|
|
315
|
-
points: number
|
|
324
|
+
points: [number, number][],
|
|
316
325
|
distance: number
|
|
317
|
-
): number {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
if (start[1] === null) {
|
|
332
|
-
console.warn(
|
|
333
|
-
"Elevation value does not exist for distance.",
|
|
334
|
-
distance,
|
|
335
|
-
traversed
|
|
336
|
-
);
|
|
337
|
-
return null;
|
|
338
|
-
}
|
|
339
|
-
const pct = (distance - traversed) / elevDistanceSpan;
|
|
340
|
-
const elevSpan = points[i][1] - start[1];
|
|
341
|
-
return start[1] + elevSpan * pct;
|
|
326
|
+
): number | undefined {
|
|
327
|
+
const elevation = points.reduce<number | undefined>((acc, point, index) => {
|
|
328
|
+
const prevPoint = points[index - 1];
|
|
329
|
+
// at the first index there is no previous point
|
|
330
|
+
if (!prevPoint) return acc;
|
|
331
|
+
const [pointDistance, pointElevation] = point;
|
|
332
|
+
const [prevPointDistance, prevPointElevation] = prevPoint;
|
|
333
|
+
if (distance >= prevPointDistance && distance <= pointDistance) {
|
|
334
|
+
return (
|
|
335
|
+
prevPointElevation +
|
|
336
|
+
((pointElevation - prevPointElevation) *
|
|
337
|
+
(distance - prevPointDistance)) /
|
|
338
|
+
(pointDistance - prevPointDistance)
|
|
339
|
+
);
|
|
342
340
|
}
|
|
343
|
-
|
|
341
|
+
return acc;
|
|
342
|
+
}, undefined);
|
|
343
|
+
if (elevation === undefined) {
|
|
344
|
+
console.warn("Elevation value does not exist for distance.", distance);
|
|
345
|
+
return undefined;
|
|
344
346
|
}
|
|
345
|
-
|
|
346
|
-
"Elevation value does not exist for distance.",
|
|
347
|
-
distance,
|
|
348
|
-
traversed
|
|
349
|
-
);
|
|
350
|
-
return null;
|
|
347
|
+
return elevation;
|
|
351
348
|
}
|
|
352
349
|
|
|
353
350
|
export function mapOldElevationComponentToNew(oldElev: {
|
|
@@ -372,7 +369,7 @@ export function getElevationProfile(
|
|
|
372
369
|
let gain = 0;
|
|
373
370
|
let loss = 0;
|
|
374
371
|
let previous: ElevationProfileComponent | null = null;
|
|
375
|
-
const points = [];
|
|
372
|
+
const points: [number, number][] = [];
|
|
376
373
|
steps.forEach(step => {
|
|
377
374
|
// Support for old REST response data (in step.elevation)
|
|
378
375
|
const stepElevationProfile =
|
|
@@ -432,6 +429,7 @@ export function getTextWidth(text: string, font = "22px Arial"): number {
|
|
|
432
429
|
(getTextWidth as GetTextWidth).canvas ||
|
|
433
430
|
((getTextWidth as GetTextWidth).canvas = document.createElement("canvas"));
|
|
434
431
|
const context = canvas.getContext("2d");
|
|
432
|
+
if (!context) return 0;
|
|
435
433
|
context.font = font;
|
|
436
434
|
const metrics = context.measureText(text);
|
|
437
435
|
return metrics.width;
|
|
@@ -444,7 +442,7 @@ export function getTextWidth(text: string, font = "22px Arial"): number {
|
|
|
444
442
|
export function getCompanyForNetwork(
|
|
445
443
|
networkString: string,
|
|
446
444
|
companies: Company[] = []
|
|
447
|
-
): Company {
|
|
445
|
+
): Company | undefined {
|
|
448
446
|
const company = companies.find(co => co.id === networkString);
|
|
449
447
|
if (!company) {
|
|
450
448
|
console.warn(
|
|
@@ -475,7 +473,10 @@ export function getCompaniesLabelFromNetworks(
|
|
|
475
473
|
.join("/");
|
|
476
474
|
}
|
|
477
475
|
|
|
478
|
-
export function getTNCLocation(
|
|
476
|
+
export function getTNCLocation(
|
|
477
|
+
leg: Pick<Leg, "from" | "to">,
|
|
478
|
+
type: "from" | "to"
|
|
479
|
+
): string {
|
|
479
480
|
const location = leg[type];
|
|
480
481
|
return `${location.lat.toFixed(5)},${location.lon.toFixed(5)}`;
|
|
481
482
|
}
|
|
@@ -511,15 +512,21 @@ export function calculateTncFares(
|
|
|
511
512
|
itinerary: ItineraryOnlyLegsRequired
|
|
512
513
|
): TncFare {
|
|
513
514
|
return itinerary.legs
|
|
514
|
-
.filter(
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
515
|
+
.filter(
|
|
516
|
+
(
|
|
517
|
+
leg
|
|
518
|
+
): leg is Leg & {
|
|
519
|
+
rideHailingEstimate: NonNullable<Leg["rideHailingEstimate"]>;
|
|
520
|
+
} => leg.mode === "CAR" && leg.rideHailingEstimate !== undefined
|
|
521
|
+
)
|
|
522
|
+
.reduce<TncFare>(
|
|
523
|
+
(acc, leg) => {
|
|
524
|
+
const { minPrice, maxPrice } = leg.rideHailingEstimate;
|
|
518
525
|
return {
|
|
519
526
|
// Assumes a single currency for entire itinerary.
|
|
520
527
|
currencyCode: minPrice.currency.code,
|
|
521
|
-
maxTNCFare: maxTNCFare + maxPrice.amount,
|
|
522
|
-
minTNCFare: minTNCFare + minPrice.amount
|
|
528
|
+
maxTNCFare: acc.maxTNCFare + maxPrice.amount,
|
|
529
|
+
minTNCFare: acc.minTNCFare + minPrice.amount
|
|
523
530
|
};
|
|
524
531
|
},
|
|
525
532
|
{
|
|
@@ -537,7 +544,7 @@ export function calculateTncFares(
|
|
|
537
544
|
* - https://www.itf-oecd.org/sites/default/files/life-cycle-assessment-calculations-2020.xlsx
|
|
538
545
|
* Other values extrapolated.
|
|
539
546
|
*/
|
|
540
|
-
const CARBON_INTENSITY_DEFAULTS = {
|
|
547
|
+
const CARBON_INTENSITY_DEFAULTS: Record<string, number> = {
|
|
541
548
|
walk: 0.026,
|
|
542
549
|
bicycle: 0.017,
|
|
543
550
|
car: 0.162,
|
|
@@ -597,28 +604,30 @@ export function calculateEmissions(
|
|
|
597
604
|
}
|
|
598
605
|
|
|
599
606
|
/**
|
|
600
|
-
* Returns the user-facing stop
|
|
601
|
-
* 1. stop code,
|
|
602
|
-
* 2. stop id without the agency id portion, if stop id contains an agency portion,
|
|
603
|
-
* 3. stop id, whether null or not (this is the fallback case).
|
|
607
|
+
* Returns the user-facing stop code to display for a stop or place
|
|
604
608
|
*/
|
|
605
|
-
export function
|
|
606
|
-
|
|
607
|
-
|
|
609
|
+
export function getDisplayedStopCode(
|
|
610
|
+
placeOrStop: Place | Stop
|
|
611
|
+
): string | undefined {
|
|
608
612
|
if ("stopId" in placeOrStop) {
|
|
609
|
-
|
|
610
|
-
} else if ("id" in placeOrStop) {
|
|
611
|
-
({ code: stopCode, id: stopId } = placeOrStop);
|
|
613
|
+
return placeOrStop.stopCode ?? undefined;
|
|
612
614
|
}
|
|
613
|
-
|
|
615
|
+
if ("id" in placeOrStop) {
|
|
616
|
+
return placeOrStop.code ?? undefined;
|
|
617
|
+
}
|
|
618
|
+
return undefined;
|
|
614
619
|
}
|
|
615
620
|
|
|
616
621
|
/**
|
|
617
|
-
* Removes the first part of the OTP standard scope (":"), if it is present
|
|
622
|
+
* Removes the first part of the OTP standard scope (":"), if it is present.
|
|
618
623
|
* @param item String that is potentially scoped with `:` character
|
|
619
624
|
* @returns descoped string
|
|
620
625
|
*/
|
|
621
|
-
export const descope = (item
|
|
626
|
+
export const descope = (item?: string | null): string | null | undefined => {
|
|
627
|
+
if (!item) return item;
|
|
628
|
+
const index = item.indexOf(":");
|
|
629
|
+
return index === -1 ? item : item.substring(index + 1);
|
|
630
|
+
};
|
|
622
631
|
|
|
623
632
|
export type ExtendedMoney = Money & { originalAmount?: number };
|
|
624
633
|
|
|
@@ -627,6 +636,8 @@ export const zeroDollars = (currency: Currency): Money => ({
|
|
|
627
636
|
currency
|
|
628
637
|
});
|
|
629
638
|
|
|
639
|
+
type FareProductWithPrice = FareProduct & { price: Money };
|
|
640
|
+
|
|
630
641
|
/**
|
|
631
642
|
* Extracts useful data from the fare products on a leg, such as the leg cost and transfer info.
|
|
632
643
|
* @param leg Leg with Fares v2 information
|
|
@@ -640,9 +651,9 @@ export const zeroDollars = (currency: Currency): Money => ({
|
|
|
640
651
|
*/
|
|
641
652
|
export function getLegCost(
|
|
642
653
|
leg: Leg,
|
|
643
|
-
mediumId
|
|
644
|
-
riderCategoryId
|
|
645
|
-
seenFareIds?: string[]
|
|
654
|
+
mediumId?: string | null,
|
|
655
|
+
riderCategoryId?: string | null,
|
|
656
|
+
seenFareIds?: string[] | null
|
|
646
657
|
): {
|
|
647
658
|
alternateFareProducts?: AppliedFareProduct[];
|
|
648
659
|
appliedFareProduct?: AppliedFareProduct;
|
|
@@ -664,6 +675,7 @@ export function getLegCost(
|
|
|
664
675
|
|
|
665
676
|
const productMediaId =
|
|
666
677
|
descope(product?.medium?.id) || product?.medium?.id || null;
|
|
678
|
+
|
|
667
679
|
return (
|
|
668
680
|
productRiderCategoryId === riderCategoryId &&
|
|
669
681
|
productMediaId === mediumId &&
|
|
@@ -672,14 +684,22 @@ export function getLegCost(
|
|
|
672
684
|
product?.price
|
|
673
685
|
);
|
|
674
686
|
})
|
|
687
|
+
// Make sure there's a price
|
|
688
|
+
// Some fare products don't have a price at all.
|
|
689
|
+
.filter(
|
|
690
|
+
(fare): fare is { id: string; product: FareProductWithPrice } =>
|
|
691
|
+
fare.product?.price !== undefined
|
|
692
|
+
)
|
|
675
693
|
.map(fare => {
|
|
676
|
-
const alreadySeen = seenFareIds?.indexOf(fare.id) > -1;
|
|
677
|
-
const { currency } = fare.product.price;
|
|
694
|
+
const alreadySeen = !!seenFareIds && seenFareIds?.indexOf(fare.id) > -1;
|
|
678
695
|
return {
|
|
679
696
|
id: fare.id,
|
|
680
697
|
product: {
|
|
681
698
|
...fare.product,
|
|
682
|
-
legPrice:
|
|
699
|
+
legPrice:
|
|
700
|
+
alreadySeen && fare.product.price
|
|
701
|
+
? zeroDollars(fare.product.price.currency)
|
|
702
|
+
: fare.product.price
|
|
683
703
|
} as AppliedFareProduct
|
|
684
704
|
};
|
|
685
705
|
})
|
|
@@ -711,38 +731,58 @@ export function getLegCost(
|
|
|
711
731
|
*/
|
|
712
732
|
export function getItineraryCost(
|
|
713
733
|
legs: Leg[],
|
|
714
|
-
mediumId
|
|
715
|
-
riderCategoryId
|
|
734
|
+
mediumId?: string | (string | null)[] | null,
|
|
735
|
+
riderCategoryId?: string | (string | null)[] | null
|
|
716
736
|
): Money | undefined {
|
|
717
|
-
// TODO: Better input type handling
|
|
718
737
|
if (Array.isArray(mediumId) || Array.isArray(riderCategoryId)) {
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
738
|
+
// TODO: Better input type handling
|
|
739
|
+
if (Array.isArray(mediumId) && Array.isArray(riderCategoryId)) {
|
|
740
|
+
if (mediumId.length !== riderCategoryId.length) {
|
|
741
|
+
console.warn(
|
|
742
|
+
"Invalid input types, only using first item. medium id list and rider category list must have same number of items"
|
|
743
|
+
);
|
|
744
|
+
return getItineraryCost(legs, mediumId[0], riderCategoryId[0]);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const total = mediumId.reduce<Money>(
|
|
748
|
+
(acc, currentMediumId, index) => {
|
|
749
|
+
const newCost = getItineraryCost(
|
|
750
|
+
legs,
|
|
751
|
+
currentMediumId,
|
|
752
|
+
riderCategoryId[index]
|
|
753
|
+
);
|
|
754
|
+
if (!newCost) return acc;
|
|
755
|
+
|
|
756
|
+
return {
|
|
757
|
+
amount: acc.amount + (newCost.amount || 0),
|
|
758
|
+
currency:
|
|
759
|
+
acc.currency.code !== ""
|
|
760
|
+
? acc.currency
|
|
761
|
+
: newCost.currency ?? acc.currency
|
|
762
|
+
};
|
|
763
|
+
},
|
|
764
|
+
{ amount: 0, currency: { code: "", digits: 0 } }
|
|
722
765
|
);
|
|
723
|
-
return getItineraryCost(legs, mediumId[0], riderCategoryId[0]);
|
|
724
|
-
}
|
|
725
766
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
const newCost = getItineraryCost(legs, mediumId[i], riderCategoryId[i]);
|
|
729
|
-
if (newCost) {
|
|
730
|
-
total = {
|
|
731
|
-
amount: total?.amount + (newCost?.amount || 0),
|
|
732
|
-
currency: total.currency ?? newCost?.currency
|
|
733
|
-
};
|
|
734
|
-
}
|
|
767
|
+
if (!total.currency?.code) return undefined;
|
|
768
|
+
return total;
|
|
735
769
|
}
|
|
736
|
-
|
|
737
|
-
|
|
770
|
+
console.warn(
|
|
771
|
+
"Invalid input types, only using first item. medium id list and rider category list must have same number of items"
|
|
772
|
+
);
|
|
773
|
+
return undefined;
|
|
738
774
|
}
|
|
739
775
|
|
|
740
776
|
const legCosts = legs
|
|
741
777
|
// Only legs with fares (no walking legs)
|
|
742
|
-
.filter(leg => leg.fareProducts?.length > 0)
|
|
778
|
+
.filter(leg => leg.fareProducts?.length && leg.fareProducts.length > 0)
|
|
743
779
|
// Get the leg cost object of each leg
|
|
744
780
|
.reduce<{ seenIds: string[]; legCosts: AppliedFareProduct[] }>(
|
|
745
781
|
(acc, leg) => {
|
|
782
|
+
// getLegCost handles filtering out duplicate use IDs
|
|
783
|
+
// One fare product can be used on multiple legs,
|
|
784
|
+
// and we don't want to count it more than once.
|
|
785
|
+
// Use an object keyed by productUseId to deduplicate, then extract prices
|
|
746
786
|
const { appliedFareProduct, productUseId } = getLegCost(
|
|
747
787
|
leg,
|
|
748
788
|
mediumId,
|
|
@@ -750,6 +790,7 @@ export function getItineraryCost(
|
|
|
750
790
|
acc.seenIds
|
|
751
791
|
);
|
|
752
792
|
if (!appliedFareProduct) return acc;
|
|
793
|
+
if (!productUseId) return acc;
|
|
753
794
|
return {
|
|
754
795
|
legCosts: [...acc.legCosts, appliedFareProduct],
|
|
755
796
|
seenIds: [...acc.seenIds, productUseId]
|
|
@@ -758,29 +799,20 @@ export function getItineraryCost(
|
|
|
758
799
|
{ seenIds: [], legCosts: [] }
|
|
759
800
|
)
|
|
760
801
|
.legCosts.map(lc => lc.legPrice);
|
|
761
|
-
// Filter out duplicate use IDs
|
|
762
|
-
// One fare product can be used on multiple legs,
|
|
763
|
-
// and we don't want to count it more than once.
|
|
764
|
-
// Use an object keyed by productUseId to deduplicate, then extract prices
|
|
765
|
-
// .reduce<{ [productUseId: string]: Money }>((acc, cur) => {
|
|
766
|
-
// if (cur.productUseId && acc[cur.productUseId] === undefined) {
|
|
767
|
-
// acc[cur.productUseId] = cur.appliedFareProduct?.legPrice;
|
|
768
|
-
// }
|
|
769
|
-
// return acc;
|
|
770
|
-
// }, {});
|
|
771
802
|
|
|
772
803
|
if (legCosts.length === 0) return undefined;
|
|
773
804
|
// Calculate the total
|
|
774
805
|
return legCosts.reduce<Money>(
|
|
775
806
|
(prev, cur) => ({
|
|
776
807
|
amount: prev.amount + cur?.amount || 0,
|
|
777
|
-
currency: prev.currency
|
|
808
|
+
currency: prev.currency.code !== "" ? prev.currency : cur.currency
|
|
778
809
|
}),
|
|
779
|
-
|
|
810
|
+
// eslint-disable-next-line prettier/prettier -- old eslint doesn't know satisfies
|
|
811
|
+
{ amount: 0, currency: { code: "", digits: 0 } satisfies Currency }
|
|
780
812
|
);
|
|
781
813
|
}
|
|
782
814
|
|
|
783
|
-
const pickupDropoffTypeToOtp1 = otp2Type => {
|
|
815
|
+
const pickupDropoffTypeToOtp1 = (otp2Type: string): string | null => {
|
|
784
816
|
switch (otp2Type) {
|
|
785
817
|
case "COORDINATE_WITH_DRIVER":
|
|
786
818
|
return "coordinateWithDriver";
|
|
@@ -795,7 +827,7 @@ const pickupDropoffTypeToOtp1 = otp2Type => {
|
|
|
795
827
|
}
|
|
796
828
|
};
|
|
797
829
|
|
|
798
|
-
export const convertGraphQLResponseToLegacy = (leg: any):
|
|
830
|
+
export const convertGraphQLResponseToLegacy = (leg: any): Leg => ({
|
|
799
831
|
...leg,
|
|
800
832
|
agencyBrandingUrl: leg.agency?.url,
|
|
801
833
|
agencyId: leg.agency?.id,
|
|
@@ -805,7 +837,7 @@ export const convertGraphQLResponseToLegacy = (leg: any): any => ({
|
|
|
805
837
|
boardRule: pickupDropoffTypeToOtp1(leg.pickupType),
|
|
806
838
|
bookingRuleInfo: {
|
|
807
839
|
dropOff: leg?.dropOffBookingInfo || {},
|
|
808
|
-
pickUp: leg?.
|
|
840
|
+
pickUp: leg?.pickupBookingInfo || {}
|
|
809
841
|
},
|
|
810
842
|
dropOffBookingInfo: {
|
|
811
843
|
latestBookingTime: leg.dropOffBookingInfo
|
|
@@ -844,7 +876,7 @@ export const getLegRouteShortName = (
|
|
|
844
876
|
/** Extract the route long name for a leg returned from OTP1 or OTP2. */
|
|
845
877
|
export const getLegRouteLongName = (
|
|
846
878
|
leg: Pick<Leg, "route" | "routeLongName">
|
|
847
|
-
): string |
|
|
879
|
+
): string | undefined => {
|
|
848
880
|
const { route, routeLongName } = leg;
|
|
849
881
|
// typeof route === "object" denotes newer OTP2 responses. routeLongName is OTP1.
|
|
850
882
|
return typeof route === "object" ? route?.longName : routeLongName;
|
|
@@ -856,6 +888,6 @@ export const getLegRouteLongName = (
|
|
|
856
888
|
*/
|
|
857
889
|
export const getLegRouteName = (
|
|
858
890
|
leg: Pick<Leg, "route" | "routeLongName" | "routeShortName">
|
|
859
|
-
): string => {
|
|
891
|
+
): string | undefined => {
|
|
860
892
|
return getLegRouteShortName(leg) || getLegRouteLongName(leg);
|
|
861
893
|
};
|
package/src/map.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { LatLngArray, Location, UserPosition } from "@opentripplanner/types";
|
|
|
2
2
|
|
|
3
3
|
export function currentPositionToLocation(
|
|
4
4
|
currentPosition: UserPosition
|
|
5
|
-
): Location {
|
|
5
|
+
): Location | null {
|
|
6
6
|
if (currentPosition.error || !currentPosition.coords) {
|
|
7
7
|
console.warn(
|
|
8
8
|
"Cannot construct location from current position due to geolocation error or missing coordinates."
|
|
@@ -18,8 +18,10 @@ export function currentPositionToLocation(
|
|
|
18
18
|
|
|
19
19
|
// TRICKY: This method is used in query.js and in the context of
|
|
20
20
|
// otp-rr actions where the intl context is not available/does not apply.
|
|
21
|
-
export function coordsToString(coords: number[]): string {
|
|
22
|
-
return coords.length
|
|
21
|
+
export function coordsToString(coords: number[]): string | undefined {
|
|
22
|
+
return coords.length > 0
|
|
23
|
+
? coords.map(c => (+c).toFixed(5)).join(", ")
|
|
24
|
+
: undefined;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
export function stringToCoords(str: string): number[] {
|
package/src/otpSchema.json
CHANGED
|
@@ -17763,4 +17763,4 @@
|
|
|
17763
17763
|
]
|
|
17764
17764
|
}
|
|
17765
17765
|
}
|
|
17766
|
-
}
|
|
17766
|
+
}
|
package/src/profile.js
CHANGED
package/src/query-gen.ts
CHANGED
|
@@ -58,7 +58,7 @@ export function extractAdditionalModes(
|
|
|
58
58
|
return prev;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
// In checkboxes
|
|
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 &&
|
|
@@ -102,7 +102,7 @@ function combinations(array: TransportMode[]): TransportMode[][] {
|
|
|
102
102
|
* This constant maps all the transport mode to a broader mode type,
|
|
103
103
|
* which is used to determine the valid combinations of modes used in query generation.
|
|
104
104
|
*/
|
|
105
|
-
export const SIMPLIFICATIONS = {
|
|
105
|
+
export const SIMPLIFICATIONS: Record<string, string> = {
|
|
106
106
|
AIRPLANE: "TRANSIT",
|
|
107
107
|
BICYCLE: "PERSONAL",
|
|
108
108
|
BUS: "TRANSIT",
|
|
@@ -123,7 +123,7 @@ export const SIMPLIFICATIONS = {
|
|
|
123
123
|
};
|
|
124
124
|
|
|
125
125
|
// Inclusion of "TRANSIT" alone automatically implies "WALK" in OTP
|
|
126
|
-
const VALID_COMBOS = [
|
|
126
|
+
const VALID_COMBOS: string[][] = [
|
|
127
127
|
["WALK"],
|
|
128
128
|
["PERSONAL"],
|
|
129
129
|
["TRANSIT", "SHARED"],
|
|
@@ -220,19 +220,25 @@ export function generateOtp2Query(
|
|
|
220
220
|
const { from, modeSettings, to, ...otherOtpQueryParams } = otpQueryParams;
|
|
221
221
|
|
|
222
222
|
// This extracts the values from the mode settings to key value pairs
|
|
223
|
-
const modeSettingValues = modeSettings.reduce
|
|
224
|
-
|
|
223
|
+
const modeSettingValues = modeSettings.reduce<
|
|
224
|
+
Record<string, string | number | boolean>
|
|
225
|
+
>((prev, cur) => {
|
|
226
|
+
if (cur.type === "SLIDER" && cur.inverseKey && cur.value) {
|
|
225
227
|
prev[cur.inverseKey] = cur.high - cur.value + cur.low;
|
|
228
|
+
} else if (cur.value) {
|
|
229
|
+
prev[cur.key] = cur.value;
|
|
226
230
|
}
|
|
227
|
-
prev[cur.key] = cur.value;
|
|
228
231
|
|
|
229
232
|
// If we assign a value on true, return the value (or null) instead of a boolean.
|
|
230
|
-
if (cur.type === "CHECKBOX" && cur.truthValue) {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
+
if (cur.type === "CHECKBOX" && cur.truthValue && cur.falseValue) {
|
|
234
|
+
const newVal = cur.value === true ? cur.truthValue : cur.falseValue;
|
|
235
|
+
if (newVal) {
|
|
236
|
+
prev[cur.key] = newVal;
|
|
237
|
+
}
|
|
233
238
|
}
|
|
234
239
|
return prev;
|
|
235
|
-
|
|
240
|
+
// eslint-disable-next-line prettier/prettier -- old eslint doesn't know satisfies
|
|
241
|
+
}, {}) satisfies ModeSettingValues;
|
|
236
242
|
|
|
237
243
|
const {
|
|
238
244
|
bikeReluctance,
|
package/src/query.js
CHANGED
|
@@ -72,10 +72,7 @@ export function ensureSingleAccessMode(queryModes) {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
export function getUrlParams() {
|
|
75
|
-
|
|
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() {
|