@opentripplanner/core-utils 9.0.0-alpha.8 → 9.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/esm/itinerary.js +91 -61
- package/esm/itinerary.js.map +1 -1
- package/esm/query-gen.js +27 -6
- package/esm/query-gen.js.map +1 -1
- package/lib/itinerary.d.ts +6 -18
- package/lib/itinerary.d.ts.map +1 -1
- package/lib/itinerary.js +95 -60
- package/lib/itinerary.js.map +1 -1
- package/lib/query-gen.d.ts +24 -3
- package/lib/query-gen.d.ts.map +1 -1
- package/lib/query-gen.js +26 -6
- package/lib/query-gen.js.map +1 -1
- package/package.json +4 -3
- package/src/__tests__/__mocks__/fare-products-itinerary.json +1283 -0
- package/src/__tests__/__mocks__/tnc-itinerary.json +34 -18
- package/src/__tests__/itinerary.js +74 -28
- package/src/__tests__/query-params.ts +24 -0
- package/src/itinerary.ts +97 -69
- package/src/otpSchema.json +10781 -0
- package/src/planQuery.graphql +222 -132
- package/src/query-gen.ts +57 -16
- package/tsconfig.tsbuildinfo +1 -1
- package/src/__tests__/__snapshots__/itinerary.js.snap +0 -5
|
@@ -66,15 +66,23 @@
|
|
|
66
66
|
"rentedCar": false,
|
|
67
67
|
"rentedVehicle": false,
|
|
68
68
|
"hailedCar": true,
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
69
|
+
"rideHailingEstimate": {
|
|
70
|
+
"provider": {
|
|
71
|
+
"id": "uber"
|
|
72
|
+
},
|
|
73
|
+
"arrival": "PT4M",
|
|
74
|
+
"minPrice": {
|
|
75
|
+
"currency": {
|
|
76
|
+
"code": "USD"
|
|
77
|
+
},
|
|
78
|
+
"amount": 17
|
|
79
|
+
},
|
|
80
|
+
"maxPrice": {
|
|
81
|
+
"currency": {
|
|
82
|
+
"code": "USD"
|
|
83
|
+
},
|
|
84
|
+
"amount": 19
|
|
85
|
+
}
|
|
78
86
|
},
|
|
79
87
|
"duration": 180,
|
|
80
88
|
"transitLeg": false,
|
|
@@ -1045,15 +1053,23 @@
|
|
|
1045
1053
|
"rentedCar": false,
|
|
1046
1054
|
"rentedVehicle": false,
|
|
1047
1055
|
"hailedCar": true,
|
|
1048
|
-
"
|
|
1049
|
-
"
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
"
|
|
1053
|
-
"
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1056
|
+
"rideHailingEstimate": {
|
|
1057
|
+
"provider": {
|
|
1058
|
+
"id": "uber"
|
|
1059
|
+
},
|
|
1060
|
+
"arrival": "PT4M",
|
|
1061
|
+
"minPrice": {
|
|
1062
|
+
"currency": {
|
|
1063
|
+
"code": "USD"
|
|
1064
|
+
},
|
|
1065
|
+
"amount": 17
|
|
1066
|
+
},
|
|
1067
|
+
"maxPrice": {
|
|
1068
|
+
"currency": {
|
|
1069
|
+
"code": "USD"
|
|
1070
|
+
},
|
|
1071
|
+
"amount": 19
|
|
1072
|
+
}
|
|
1057
1073
|
},
|
|
1058
1074
|
"duration": 341,
|
|
1059
1075
|
"transitLeg": false,
|
|
@@ -2,12 +2,14 @@ import {
|
|
|
2
2
|
calculateTncFares,
|
|
3
3
|
getCompanyFromLeg,
|
|
4
4
|
getDisplayedStopId,
|
|
5
|
-
|
|
5
|
+
getItineraryCost,
|
|
6
|
+
getLegCost,
|
|
6
7
|
isTransit
|
|
7
8
|
} from "../itinerary";
|
|
8
9
|
|
|
9
10
|
const bikeRentalItinerary = require("./__mocks__/bike-rental-itinerary.json");
|
|
10
11
|
const tncItinerary = require("./__mocks__/tnc-itinerary.json");
|
|
12
|
+
const fareProductItinerary = require("./__mocks__/fare-products-itinerary.json");
|
|
11
13
|
|
|
12
14
|
const basePlace = {
|
|
13
15
|
lat: 0,
|
|
@@ -31,31 +33,7 @@ describe("util > itinerary", () => {
|
|
|
31
33
|
|
|
32
34
|
it("should return company for TNC leg", () => {
|
|
33
35
|
const company = getCompanyFromLeg(tncItinerary.legs[0]);
|
|
34
|
-
expect(company).toEqual("
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
describe("getTransitFare", () => {
|
|
39
|
-
it("should return defaults with missing fare", () => {
|
|
40
|
-
const { transitFare } = getTransitFare(null);
|
|
41
|
-
// transit fare value should be zero
|
|
42
|
-
expect(transitFare).toMatchSnapshot();
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("should work with valid fare component", () => {
|
|
46
|
-
const fareComponent = {
|
|
47
|
-
currency: {
|
|
48
|
-
currency: "USD",
|
|
49
|
-
defaultFractionDigits: 2,
|
|
50
|
-
currencyCode: "USD",
|
|
51
|
-
symbol: "$"
|
|
52
|
-
},
|
|
53
|
-
cents: 575
|
|
54
|
-
};
|
|
55
|
-
const { currencyCode, transitFare } = getTransitFare(fareComponent);
|
|
56
|
-
expect(currencyCode).toEqual(fareComponent.currency.currencyCode);
|
|
57
|
-
// Snapshot tests
|
|
58
|
-
expect(transitFare).toMatchSnapshot();
|
|
36
|
+
expect(company).toEqual("uber");
|
|
59
37
|
});
|
|
60
38
|
});
|
|
61
39
|
|
|
@@ -63,8 +41,8 @@ describe("util > itinerary", () => {
|
|
|
63
41
|
it("should return the correct amounts and currency for an itinerary with TNC", () => {
|
|
64
42
|
const fareResult = calculateTncFares(tncItinerary, true);
|
|
65
43
|
expect(fareResult.currencyCode).toEqual("USD");
|
|
66
|
-
expect(fareResult.maxTNCFare).toEqual(
|
|
67
|
-
expect(fareResult.minTNCFare).toEqual(
|
|
44
|
+
expect(fareResult.maxTNCFare).toEqual(38);
|
|
45
|
+
expect(fareResult.minTNCFare).toEqual(34);
|
|
68
46
|
});
|
|
69
47
|
});
|
|
70
48
|
|
|
@@ -111,4 +89,72 @@ describe("util > itinerary", () => {
|
|
|
111
89
|
expect(getDisplayedStopId(basePlace)).toBeFalsy();
|
|
112
90
|
});
|
|
113
91
|
});
|
|
92
|
+
|
|
93
|
+
describe("getLegCost", () => {
|
|
94
|
+
const leg = {
|
|
95
|
+
fareProducts: [
|
|
96
|
+
{
|
|
97
|
+
id: "testId",
|
|
98
|
+
product: {
|
|
99
|
+
medium: { id: "cash" },
|
|
100
|
+
name: "rideCost",
|
|
101
|
+
price: { amount: 200, currency: "USD" },
|
|
102
|
+
riderCategory: { id: "regular" }
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: "testId",
|
|
107
|
+
product: {
|
|
108
|
+
medium: { id: "cash" },
|
|
109
|
+
name: "transfer",
|
|
110
|
+
price: { amount: 50, currency: "USD" },
|
|
111
|
+
riderCategory: { id: "regular" }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
]
|
|
115
|
+
};
|
|
116
|
+
it("should return the total cost for a leg", () => {
|
|
117
|
+
const result = getLegCost(leg, "cash", "regular");
|
|
118
|
+
expect(result.price).toEqual({ amount: 200, currency: "USD" });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("should return the transfer discount amount if a transfer was used", () => {
|
|
122
|
+
const result = getLegCost(leg, "cash", "regular");
|
|
123
|
+
expect(result.price).toEqual({ amount: 200, currency: "USD" });
|
|
124
|
+
expect(result.transferAmount).toEqual({ amount: 50, currency: "USD" });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("should return undefined if no fare products exist on the leg", () => {
|
|
128
|
+
const emptyleg = {};
|
|
129
|
+
const result = getLegCost(emptyleg, "cash", "regular");
|
|
130
|
+
expect(result.price).toBeUndefined();
|
|
131
|
+
});
|
|
132
|
+
it("should return undefined if the keys are invalid", () => {
|
|
133
|
+
const result = getLegCost(leg, "invalidkey", "invalidkey");
|
|
134
|
+
expect(result.price).toBeUndefined();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("getItineraryCost", () => {
|
|
139
|
+
it("should calculate the total cost of an itinerary", () => {
|
|
140
|
+
const result = getItineraryCost(
|
|
141
|
+
fareProductItinerary.legs,
|
|
142
|
+
"orca:cash",
|
|
143
|
+
"orca:regular"
|
|
144
|
+
);
|
|
145
|
+
expect(result.amount).toEqual(5.75);
|
|
146
|
+
expect(result.currency).toEqual({
|
|
147
|
+
code: "USD",
|
|
148
|
+
digits: 2
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
it("should return undefined when the keys are invalid", () => {
|
|
152
|
+
const result = getItineraryCost(
|
|
153
|
+
fareProductItinerary.legs,
|
|
154
|
+
"invalidkey",
|
|
155
|
+
"invalidkey"
|
|
156
|
+
);
|
|
157
|
+
expect(result).toBeUndefined();
|
|
158
|
+
});
|
|
159
|
+
});
|
|
114
160
|
});
|
|
@@ -148,6 +148,10 @@ describe("query-gen", () => {
|
|
|
148
148
|
["WALK"]
|
|
149
149
|
]
|
|
150
150
|
);
|
|
151
|
+
expectModes(
|
|
152
|
+
["BICYCLE_RENT", "BICYCLE", "WALK"],
|
|
153
|
+
[["BICYCLE_RENT", "WALK"], ["BICYCLE"], ["WALK"]]
|
|
154
|
+
);
|
|
151
155
|
expectModes(
|
|
152
156
|
["SCOOTER_RENT", "BICYCLE_RENT", "TRANSIT", "WALK"],
|
|
153
157
|
[
|
|
@@ -159,6 +163,26 @@ describe("query-gen", () => {
|
|
|
159
163
|
["WALK"]
|
|
160
164
|
]
|
|
161
165
|
);
|
|
166
|
+
|
|
167
|
+
expectModes(
|
|
168
|
+
["CAR_HAIL", "TRANSIT"],
|
|
169
|
+
[["TRANSIT"], ["CAR_HAIL", "TRANSIT"]]
|
|
170
|
+
);
|
|
171
|
+
expectModes(
|
|
172
|
+
["CAR_HAIL", "BICYCLE_RENT", "TRANSIT"],
|
|
173
|
+
[["TRANSIT"], ["CAR_HAIL", "TRANSIT"], ["BICYCLE_RENT", "TRANSIT"]]
|
|
174
|
+
);
|
|
175
|
+
expectModes(
|
|
176
|
+
["CAR_HAIL", "BICYCLE_RENT", "TRANSIT", "WALK"],
|
|
177
|
+
[
|
|
178
|
+
["TRANSIT"],
|
|
179
|
+
["CAR_HAIL", "TRANSIT"],
|
|
180
|
+
["CAR_HAIL", "WALK"],
|
|
181
|
+
["BICYCLE_RENT", "TRANSIT"],
|
|
182
|
+
["BICYCLE_RENT", "WALK"],
|
|
183
|
+
["WALK"]
|
|
184
|
+
]
|
|
185
|
+
);
|
|
162
186
|
expectModes(
|
|
163
187
|
["FLEX", "TRANSIT", "WALK"],
|
|
164
188
|
[["TRANSIT"], ["FLEX", "TRANSIT"], ["FLEX", "WALK"], ["WALK"]]
|
package/src/itinerary.ts
CHANGED
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
Config,
|
|
5
5
|
ElevationProfile,
|
|
6
6
|
FlexBookingInfo,
|
|
7
|
-
Itinerary,
|
|
8
7
|
ItineraryOnlyLegsRequired,
|
|
9
8
|
LatLngArray,
|
|
10
9
|
Leg,
|
|
@@ -79,6 +78,10 @@ export function legDropoffRequiresAdvanceBooking(leg: Leg): boolean {
|
|
|
79
78
|
return isAdvanceBookingRequired(leg.dropOffBookingInfo);
|
|
80
79
|
}
|
|
81
80
|
|
|
81
|
+
export function isRideshareLeg(leg: Leg): boolean {
|
|
82
|
+
return !!leg.rideHailingEstimate?.provider?.id;
|
|
83
|
+
}
|
|
84
|
+
|
|
82
85
|
export function isWalk(mode: string): boolean {
|
|
83
86
|
if (!mode) return false;
|
|
84
87
|
|
|
@@ -194,16 +197,26 @@ export function toSentenceCase(str: string): string {
|
|
|
194
197
|
*/
|
|
195
198
|
export function getCompanyFromLeg(leg: Leg): string {
|
|
196
199
|
if (!leg) return null;
|
|
197
|
-
const {
|
|
200
|
+
const {
|
|
201
|
+
from,
|
|
202
|
+
mode,
|
|
203
|
+
rentedBike,
|
|
204
|
+
rentedCar,
|
|
205
|
+
rentedVehicle,
|
|
206
|
+
rideHailingEstimate
|
|
207
|
+
} = leg;
|
|
198
208
|
if (mode === "CAR" && rentedCar) {
|
|
199
209
|
return from.networks[0];
|
|
200
210
|
}
|
|
201
|
-
if (mode === "CAR" &&
|
|
202
|
-
return
|
|
211
|
+
if (mode === "CAR" && rideHailingEstimate) {
|
|
212
|
+
return rideHailingEstimate.provider.id;
|
|
203
213
|
}
|
|
204
214
|
if (mode === "BICYCLE" && rentedBike && from.networks) {
|
|
205
215
|
return from.networks[0];
|
|
206
216
|
}
|
|
217
|
+
if (from.rentalVehicle) {
|
|
218
|
+
return from.rentalVehicle.network;
|
|
219
|
+
}
|
|
207
220
|
if (
|
|
208
221
|
(mode === "MICROMOBILITY" || mode === "SCOOTER") &&
|
|
209
222
|
rentedVehicle &&
|
|
@@ -441,15 +454,15 @@ export function calculateTncFares(
|
|
|
441
454
|
itinerary: ItineraryOnlyLegsRequired
|
|
442
455
|
): TncFare {
|
|
443
456
|
return itinerary.legs
|
|
444
|
-
.filter(leg => leg.mode === "CAR" && leg.
|
|
457
|
+
.filter(leg => leg.mode === "CAR" && leg.rideHailingEstimate)
|
|
445
458
|
.reduce(
|
|
446
|
-
({ maxTNCFare, minTNCFare }, {
|
|
447
|
-
const {
|
|
459
|
+
({ maxTNCFare, minTNCFare }, { rideHailingEstimate }) => {
|
|
460
|
+
const { minPrice, maxPrice } = rideHailingEstimate;
|
|
448
461
|
return {
|
|
449
462
|
// Assumes a single currency for entire itinerary.
|
|
450
|
-
currencyCode: currency,
|
|
451
|
-
maxTNCFare: maxTNCFare +
|
|
452
|
-
minTNCFare: minTNCFare +
|
|
463
|
+
currencyCode: minPrice.currency.code,
|
|
464
|
+
maxTNCFare: maxTNCFare + maxPrice.amount,
|
|
465
|
+
minTNCFare: minTNCFare + minPrice.amount
|
|
453
466
|
};
|
|
454
467
|
},
|
|
455
468
|
{
|
|
@@ -460,27 +473,6 @@ export function calculateTncFares(
|
|
|
460
473
|
);
|
|
461
474
|
}
|
|
462
475
|
|
|
463
|
-
/**
|
|
464
|
-
* For a given fare component (either total fare or component parts), returns
|
|
465
|
-
* an object with the fare value (in cents).
|
|
466
|
-
*/
|
|
467
|
-
export function getTransitFare(
|
|
468
|
-
fareComponent: Money
|
|
469
|
-
): {
|
|
470
|
-
currencyCode: string;
|
|
471
|
-
transitFare: number;
|
|
472
|
-
} {
|
|
473
|
-
return fareComponent
|
|
474
|
-
? {
|
|
475
|
-
currencyCode: fareComponent.currency.currencyCode,
|
|
476
|
-
transitFare: fareComponent.cents
|
|
477
|
-
}
|
|
478
|
-
: {
|
|
479
|
-
currencyCode: "USD",
|
|
480
|
-
transitFare: 0
|
|
481
|
-
};
|
|
482
|
-
}
|
|
483
|
-
|
|
484
476
|
/**
|
|
485
477
|
* Sources:
|
|
486
478
|
* - https://www.itf-oecd.org/sites/default/files/docs/environmental-performance-new-mobility.pdf
|
|
@@ -563,20 +555,6 @@ export function getDisplayedStopId(placeOrStop: Place | Stop): string {
|
|
|
563
555
|
return stopCode || stopId?.split(":")[1] || stopId;
|
|
564
556
|
}
|
|
565
557
|
|
|
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
558
|
/**
|
|
581
559
|
* Extracts useful data from the fare products on a leg, such as the leg cost and transfer info.
|
|
582
560
|
* @param leg Leg with fare products (must have used getLegsWithFares)
|
|
@@ -586,23 +564,26 @@ export function getLegsWithFares(itinerary: Itinerary): Leg[] {
|
|
|
586
564
|
*/
|
|
587
565
|
export function getLegCost(
|
|
588
566
|
leg: Leg,
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
): { price?: Money; transferAmount?:
|
|
567
|
+
mediumId: string,
|
|
568
|
+
riderCategoryId: string
|
|
569
|
+
): { price?: Money; transferAmount?: Money | undefined } {
|
|
592
570
|
if (!leg.fareProducts) return { price: undefined };
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
571
|
+
const relevantFareProducts = leg.fareProducts.filter(({ product }) => {
|
|
572
|
+
return (
|
|
573
|
+
product.riderCategory.id === riderCategoryId &&
|
|
574
|
+
product.medium.id === mediumId
|
|
575
|
+
);
|
|
576
|
+
});
|
|
577
|
+
const totalCost = relevantFareProducts.find(
|
|
578
|
+
fp => fp.product.name === "rideCost"
|
|
579
|
+
)?.product?.price;
|
|
599
580
|
const transferFareProduct = relevantFareProducts.find(
|
|
600
|
-
fp => fp.name === "transfer"
|
|
581
|
+
fp => fp.product.name === "transfer"
|
|
601
582
|
);
|
|
602
583
|
|
|
603
584
|
return {
|
|
604
585
|
price: totalCost,
|
|
605
|
-
transferAmount: transferFareProduct?.
|
|
586
|
+
transferAmount: transferFareProduct?.product.price
|
|
606
587
|
};
|
|
607
588
|
}
|
|
608
589
|
|
|
@@ -615,17 +596,64 @@ export function getLegCost(
|
|
|
615
596
|
*/
|
|
616
597
|
export function getItineraryCost(
|
|
617
598
|
legs: Leg[],
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
): Money {
|
|
621
|
-
|
|
622
|
-
.filter(leg =>
|
|
623
|
-
.map(leg => getLegCost(leg,
|
|
624
|
-
.
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
)
|
|
599
|
+
mediumId: string,
|
|
600
|
+
riderCategoryId: string
|
|
601
|
+
): Money | undefined {
|
|
602
|
+
const legCosts = legs
|
|
603
|
+
.filter(leg => leg.fareProducts?.length > 0)
|
|
604
|
+
.map(leg => getLegCost(leg, mediumId, riderCategoryId).price)
|
|
605
|
+
.filter(cost => cost !== undefined);
|
|
606
|
+
if (legCosts.length === 0) return undefined;
|
|
607
|
+
return legCosts.reduce<Money>(
|
|
608
|
+
(prev, cur) => ({
|
|
609
|
+
amount: prev.amount + cur?.amount || 0,
|
|
610
|
+
currency: prev.currency ?? cur?.currency
|
|
611
|
+
}),
|
|
612
|
+
{ amount: 0, currency: null }
|
|
613
|
+
);
|
|
631
614
|
}
|
|
615
|
+
|
|
616
|
+
const pickupDropoffTypeToOtp1 = otp2Type => {
|
|
617
|
+
switch (otp2Type) {
|
|
618
|
+
case "COORDINATE_WITH_DRIVER":
|
|
619
|
+
return "coordinateWithDriver";
|
|
620
|
+
case "CALL_AGENCY":
|
|
621
|
+
return "mustPhone";
|
|
622
|
+
case "SCHEDULED":
|
|
623
|
+
return "scheduled";
|
|
624
|
+
case "NONE":
|
|
625
|
+
return "none";
|
|
626
|
+
default:
|
|
627
|
+
return null;
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
export const convertGraphQLResponseToLegacy = (leg: any): any => ({
|
|
632
|
+
...leg,
|
|
633
|
+
agencyBrandingUrl: leg.agency?.url,
|
|
634
|
+
agencyName: leg.agency?.name,
|
|
635
|
+
agencyUrl: leg.agency?.url,
|
|
636
|
+
alightRule: pickupDropoffTypeToOtp1(leg.dropoffType),
|
|
637
|
+
boardRule: pickupDropoffTypeToOtp1(leg.pickupType),
|
|
638
|
+
dropOffBookingInfo: {
|
|
639
|
+
latestBookingTime: leg.dropOffBookingInfo
|
|
640
|
+
},
|
|
641
|
+
from: {
|
|
642
|
+
...leg.from,
|
|
643
|
+
stopCode: leg.from.stop?.code,
|
|
644
|
+
stopId: leg.from.stop?.gtfsId
|
|
645
|
+
},
|
|
646
|
+
route: leg.route?.shortName,
|
|
647
|
+
routeColor: leg.route?.color,
|
|
648
|
+
routeId: leg.route?.id,
|
|
649
|
+
routeLongName: leg.route?.longName,
|
|
650
|
+
routeShortName: leg.route?.shortName,
|
|
651
|
+
routeTextColor: leg.route?.textColor,
|
|
652
|
+
to: {
|
|
653
|
+
...leg.to,
|
|
654
|
+
stopCode: leg.to.stop?.code,
|
|
655
|
+
stopId: leg.to.stop?.gtfsId
|
|
656
|
+
},
|
|
657
|
+
tripHeadsign: leg.trip?.tripHeadsign,
|
|
658
|
+
tripId: leg.trip?.gtfsId
|
|
659
|
+
});
|