bengaluru-transit 0.1.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/LICENSE +21 -0
- package/README.md +507 -0
- package/dist/api/info.d.ts +77 -0
- package/dist/api/info.d.ts.map +1 -0
- package/dist/api/info.js +197 -0
- package/dist/api/info.js.map +1 -0
- package/dist/api/locations.d.ts +26 -0
- package/dist/api/locations.d.ts.map +1 -0
- package/dist/api/locations.js +57 -0
- package/dist/api/locations.js.map +1 -0
- package/dist/api/routes.d.ts +341 -0
- package/dist/api/routes.d.ts.map +1 -0
- package/dist/api/routes.js +1133 -0
- package/dist/api/routes.js.map +1 -0
- package/dist/api/stops.d.ts +92 -0
- package/dist/api/stops.d.ts.map +1 -0
- package/dist/api/stops.js +237 -0
- package/dist/api/stops.js.map +1 -0
- package/dist/api/vehicles.d.ts +49 -0
- package/dist/api/vehicles.d.ts.map +1 -0
- package/dist/api/vehicles.js +154 -0
- package/dist/api/vehicles.js.map +1 -0
- package/dist/client/base-client.d.ts +52 -0
- package/dist/client/base-client.d.ts.map +1 -0
- package/dist/client/base-client.js +76 -0
- package/dist/client/base-client.js.map +1 -0
- package/dist/client/transit-client.d.ts +91 -0
- package/dist/client/transit-client.d.ts.map +1 -0
- package/dist/client/transit-client.js +98 -0
- package/dist/client/transit-client.js.map +1 -0
- package/dist/constants/api.d.ts +16 -0
- package/dist/constants/api.d.ts.map +1 -0
- package/dist/constants/api.js +16 -0
- package/dist/constants/api.js.map +1 -0
- package/dist/constants/routes.d.ts +16 -0
- package/dist/constants/routes.d.ts.map +1 -0
- package/dist/constants/routes.js +16 -0
- package/dist/constants/routes.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/schemas/common.d.ts +34 -0
- package/dist/schemas/common.d.ts.map +1 -0
- package/dist/schemas/common.js +20 -0
- package/dist/schemas/common.js.map +1 -0
- package/dist/schemas/index.d.ts +7 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +7 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/info.d.ts +390 -0
- package/dist/schemas/info.d.ts.map +1 -0
- package/dist/schemas/info.js +110 -0
- package/dist/schemas/info.js.map +1 -0
- package/dist/schemas/locations.d.ts +84 -0
- package/dist/schemas/locations.d.ts.map +1 -0
- package/dist/schemas/locations.js +31 -0
- package/dist/schemas/locations.js.map +1 -0
- package/dist/schemas/routes.d.ts +3967 -0
- package/dist/schemas/routes.d.ts.map +1 -0
- package/dist/schemas/routes.js +532 -0
- package/dist/schemas/routes.js.map +1 -0
- package/dist/schemas/stops.d.ts +543 -0
- package/dist/schemas/stops.d.ts.map +1 -0
- package/dist/schemas/stops.js +129 -0
- package/dist/schemas/stops.js.map +1 -0
- package/dist/schemas/vehicles.d.ts +602 -0
- package/dist/schemas/vehicles.d.ts.map +1 -0
- package/dist/schemas/vehicles.js +116 -0
- package/dist/schemas/vehicles.js.map +1 -0
- package/dist/types/api.d.ts +9 -0
- package/dist/types/api.d.ts.map +1 -0
- package/dist/types/api.js +5 -0
- package/dist/types/api.js.map +1 -0
- package/dist/types/coordinates.d.ts +7 -0
- package/dist/types/coordinates.d.ts.map +1 -0
- package/dist/types/coordinates.js +2 -0
- package/dist/types/coordinates.js.map +1 -0
- package/dist/types/geojson.d.ts +84 -0
- package/dist/types/geojson.d.ts.map +1 -0
- package/dist/types/geojson.js +2 -0
- package/dist/types/geojson.js.map +1 -0
- package/dist/types/index.d.ts +16 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +12 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/info.d.ts +133 -0
- package/dist/types/info.d.ts.map +1 -0
- package/dist/types/info.js +5 -0
- package/dist/types/info.js.map +1 -0
- package/dist/types/locations.d.ts +59 -0
- package/dist/types/locations.d.ts.map +1 -0
- package/dist/types/locations.js +5 -0
- package/dist/types/locations.js.map +1 -0
- package/dist/types/routes.d.ts +1137 -0
- package/dist/types/routes.d.ts.map +1 -0
- package/dist/types/routes.js +14 -0
- package/dist/types/routes.js.map +1 -0
- package/dist/types/stops.d.ts +286 -0
- package/dist/types/stops.d.ts.map +1 -0
- package/dist/types/stops.js +26 -0
- package/dist/types/stops.js.map +1 -0
- package/dist/types/vehicles.d.ts +138 -0
- package/dist/types/vehicles.d.ts.map +1 -0
- package/dist/types/vehicles.js +5 -0
- package/dist/types/vehicles.js.map +1 -0
- package/dist/utils/date.d.ts +35 -0
- package/dist/utils/date.d.ts.map +1 -0
- package/dist/utils/date.js +49 -0
- package/dist/utils/date.js.map +1 -0
- package/dist/utils/errors.d.ts +34 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +41 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/geojson.d.ts +36 -0
- package/dist/utils/geojson.d.ts.map +1 -0
- package/dist/utils/geojson.js +115 -0
- package/dist/utils/geojson.js.map +1 -0
- package/dist/utils/validation.d.ts +40 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +62 -0
- package/dist/utils/validation.js.map +1 -0
- package/package.json +77 -0
|
@@ -0,0 +1,1133 @@
|
|
|
1
|
+
import { validate, parseId, stringifyId } from "../utils/validation";
|
|
2
|
+
import { TransitError } from "../utils/errors";
|
|
3
|
+
import { formatDateTime, formatDate, formatISODate } from "../utils/date";
|
|
4
|
+
import { DEFAULT_DEVICE_TYPE } from "../constants/api";
|
|
5
|
+
import { rawRoutePointsResponseSchema, routePointsParamsSchema, rawRouteSearchResponseSchema, routeSearchParamsSchema, rawAllRoutesResponseSchema, rawTimetableResponseSchema, timetableRequestSchema, rawRouteDetailsResponseSchema, routeDetailsParamsSchema, rawRoutesBetweenStationsResponseSchema, routesBetweenStopsParamsSchema, rawFareDataResponseSchema, fareDataParamsSchema, rawTripPlannerResponseSchema, tripPlannerParamsSchema, rawPathDetailsResponseSchema, pathDetailsParamsSchema, pathDetailsApiParamsSchema, waypointsParamsSchema, rawWaypointsResponseSchema, rawTimetableByStationResponseSchema, timetableByStationRequestSchema, } from "../schemas/routes";
|
|
6
|
+
import { WALK_ROUTE_PREFIX, WALK_PREFIX, EMPTY_SUBROUTE_ID } from "../constants/routes";
|
|
7
|
+
import { createRouteFeature, createFeatureCollection, createLocationFeature, } from "../utils/geojson";
|
|
8
|
+
import { decode as hereDecode } from "@here/flexpolyline";
|
|
9
|
+
import { tripPlannerFilterToNumber } from "../types/routes";
|
|
10
|
+
/**
|
|
11
|
+
* Transform raw route points API response to clean, normalized format
|
|
12
|
+
*/
|
|
13
|
+
function transformRoutePointsResponse(raw, routeId) {
|
|
14
|
+
// Convert string coordinates to numbers and create LineString coordinates
|
|
15
|
+
// GeoJSON format: [lng, lat]
|
|
16
|
+
const coordinates = raw.data.map((item) => [parseFloat(item.longitude), parseFloat(item.latitude)]);
|
|
17
|
+
// If no coordinates, return empty FeatureCollection
|
|
18
|
+
if (coordinates.length === 0) {
|
|
19
|
+
return {
|
|
20
|
+
routePath: createFeatureCollection([]),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
// Create a single LineString feature for the route path
|
|
24
|
+
const routeFeature = createRouteFeature(coordinates, {
|
|
25
|
+
routeId: routeId,
|
|
26
|
+
});
|
|
27
|
+
return {
|
|
28
|
+
routePath: createFeatureCollection([routeFeature]),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Transform raw route search API response to clean, normalized format
|
|
33
|
+
*/
|
|
34
|
+
function transformRouteSearchResponse(raw) {
|
|
35
|
+
const items = raw.data.map((item) => ({
|
|
36
|
+
unionRowNo: item.union_rowno,
|
|
37
|
+
row: item.row,
|
|
38
|
+
routeNo: item.routeno,
|
|
39
|
+
parentRouteId: stringifyId(item.routeparentid),
|
|
40
|
+
}));
|
|
41
|
+
return {
|
|
42
|
+
items,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Transform raw all routes API response to clean, normalized format
|
|
47
|
+
*/
|
|
48
|
+
function transformAllRoutesResponse(raw) {
|
|
49
|
+
const items = raw.data.map((item) => ({
|
|
50
|
+
subrouteId: stringifyId(item.routeid),
|
|
51
|
+
routeNo: item.routeno,
|
|
52
|
+
routeName: item.routename,
|
|
53
|
+
fromStopId: stringifyId(item.fromstationid),
|
|
54
|
+
fromStop: item.fromstation,
|
|
55
|
+
toStopId: stringifyId(item.tostationid),
|
|
56
|
+
toStop: item.tostation,
|
|
57
|
+
}));
|
|
58
|
+
return {
|
|
59
|
+
items,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Transform raw timetable API response to clean, normalized format
|
|
64
|
+
*/
|
|
65
|
+
function transformTimetableResponse(raw) {
|
|
66
|
+
const items = raw.data.map((item) => ({
|
|
67
|
+
fromStopName: item.fromstationname,
|
|
68
|
+
toStopName: item.tostationname,
|
|
69
|
+
fromStopId: item.fromstationid,
|
|
70
|
+
toStopId: item.tostationid,
|
|
71
|
+
approximateTime: item.apptime,
|
|
72
|
+
distance: parseFloat(item.distance),
|
|
73
|
+
platformName: item.platformname,
|
|
74
|
+
platformNumber: item.platformnumber,
|
|
75
|
+
bayNumber: item.baynumber,
|
|
76
|
+
tripDetails: item.tripdetails.map((trip) => ({
|
|
77
|
+
startTime: trip.starttime,
|
|
78
|
+
endTime: trip.endtime,
|
|
79
|
+
})),
|
|
80
|
+
}));
|
|
81
|
+
return {
|
|
82
|
+
items,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Transform raw timetable by station API response to clean, normalized format
|
|
87
|
+
*/
|
|
88
|
+
function transformTimetableByStationResponse(raw) {
|
|
89
|
+
const items = raw.data.map((item) => ({
|
|
90
|
+
routeId: stringifyId(item.routeid),
|
|
91
|
+
id: item.id,
|
|
92
|
+
fromStopId: stringifyId(item.fromstationid),
|
|
93
|
+
toStopId: stringifyId(item.tostationid),
|
|
94
|
+
fromStopOffset: item.f,
|
|
95
|
+
toStopOffset: item.t,
|
|
96
|
+
routeNo: item.routeno,
|
|
97
|
+
routeName: item.routename,
|
|
98
|
+
fromStopName: item.fromstationname,
|
|
99
|
+
toStopName: item.tostationname,
|
|
100
|
+
travelTime: item.traveltime,
|
|
101
|
+
distance: item.distance,
|
|
102
|
+
approximateTime: item.apptime,
|
|
103
|
+
approximateTimeSeconds: parseId(item.apptimesecs),
|
|
104
|
+
startTime: item.starttime,
|
|
105
|
+
platformName: item.platformname,
|
|
106
|
+
platformNumber: item.platformnumber,
|
|
107
|
+
bayNumber: item.baynumber,
|
|
108
|
+
}));
|
|
109
|
+
return {
|
|
110
|
+
items,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Transform raw vehicle detail item to clean, normalized format
|
|
115
|
+
*/
|
|
116
|
+
function transformVehicleDetailItem(raw) {
|
|
117
|
+
return {
|
|
118
|
+
vehicleId: stringifyId(raw.vehicleid),
|
|
119
|
+
vehicleNumber: raw.vehiclenumber,
|
|
120
|
+
serviceTypeId: stringifyId(raw.servicetypeid),
|
|
121
|
+
serviceType: raw.servicetype,
|
|
122
|
+
centerLat: raw.centerlat,
|
|
123
|
+
centerLong: raw.centerlong,
|
|
124
|
+
eta: raw.eta,
|
|
125
|
+
scheduledArrivalTime: raw.sch_arrivaltime,
|
|
126
|
+
scheduledDepartureTime: raw.sch_departuretime,
|
|
127
|
+
actualArrivalTime: raw.actual_arrivaltime,
|
|
128
|
+
actualDepartureTime: raw.actual_departuretime,
|
|
129
|
+
scheduledTripStartTime: raw.sch_tripstarttime,
|
|
130
|
+
scheduledTripEndTime: raw.sch_tripendtime,
|
|
131
|
+
lastLocationId: stringifyId(raw.lastlocationid),
|
|
132
|
+
currentLocationId: stringifyId(raw.currentlocationid),
|
|
133
|
+
nextLocationId: stringifyId(raw.nextlocationid),
|
|
134
|
+
currentStop: raw.currentstop,
|
|
135
|
+
nextStop: raw.nextstop,
|
|
136
|
+
lastStop: raw.laststop,
|
|
137
|
+
stopCoveredStatus: raw.stopCoveredStatus,
|
|
138
|
+
heading: raw.heading,
|
|
139
|
+
lastRefreshOn: raw.lastrefreshon,
|
|
140
|
+
lastReceivedDateTimeFlag: raw.lastreceiveddatetimeflag,
|
|
141
|
+
tripPosition: raw.tripposition,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Transform raw direction data to clean, normalized format
|
|
146
|
+
*/
|
|
147
|
+
function transformDirectionData(raw) {
|
|
148
|
+
// Convert stations to GeoJSON Point features (without vehicleDetails in properties)
|
|
149
|
+
const stationFeatures = raw.data.map((station) => {
|
|
150
|
+
const properties = {
|
|
151
|
+
stopId: stringifyId(station.stationid),
|
|
152
|
+
stopName: station.stationname,
|
|
153
|
+
subrouteId: stringifyId(station.routeid), // This is the subroute ID for the specific direction
|
|
154
|
+
from: station.from,
|
|
155
|
+
to: station.to,
|
|
156
|
+
routeNo: station.routeno,
|
|
157
|
+
distanceOnStation: station.distance_on_station,
|
|
158
|
+
isNotify: station.isnotify,
|
|
159
|
+
};
|
|
160
|
+
return {
|
|
161
|
+
type: "Feature",
|
|
162
|
+
geometry: {
|
|
163
|
+
type: "Point",
|
|
164
|
+
coordinates: [station.centerlong, station.centerlat],
|
|
165
|
+
},
|
|
166
|
+
properties,
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
// Collect all vehicles from all stations into a single FeatureCollection
|
|
170
|
+
const stationVehicleFeatures = raw.data.flatMap((station) => station.vehicleDetails.map((vehicle) => {
|
|
171
|
+
const { centerLat, centerLong, ...properties } = transformVehicleDetailItem(vehicle);
|
|
172
|
+
return {
|
|
173
|
+
type: "Feature",
|
|
174
|
+
geometry: {
|
|
175
|
+
type: "Point",
|
|
176
|
+
coordinates: [centerLong, centerLat],
|
|
177
|
+
},
|
|
178
|
+
properties: {
|
|
179
|
+
...properties,
|
|
180
|
+
stationId: stringifyId(station.stationid), // Link vehicle to station
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}));
|
|
184
|
+
// Convert mapData vehicles (live vehicles) to GeoJSON Point features
|
|
185
|
+
const liveVehicleFeatures = raw.mapData.map((vehicle) => createLocationFeature([vehicle.centerlong, vehicle.centerlat], // GeoJSON: [lng, lat]
|
|
186
|
+
{
|
|
187
|
+
vehicleId: stringifyId(vehicle.vehicleid),
|
|
188
|
+
vehicleNumber: vehicle.vehiclenumber,
|
|
189
|
+
serviceTypeId: stringifyId(vehicle.servicetypeid),
|
|
190
|
+
serviceType: vehicle.servicetype,
|
|
191
|
+
eta: vehicle.eta,
|
|
192
|
+
scheduledArrivalTime: vehicle.sch_arrivaltime,
|
|
193
|
+
scheduledDepartureTime: vehicle.sch_departuretime,
|
|
194
|
+
actualArrivalTime: vehicle.actual_arrivaltime,
|
|
195
|
+
actualDepartureTime: vehicle.actual_departuretime,
|
|
196
|
+
scheduledTripStartTime: vehicle.sch_tripstarttime,
|
|
197
|
+
scheduledTripEndTime: vehicle.sch_tripendtime,
|
|
198
|
+
lastLocationId: stringifyId(vehicle.lastlocationid),
|
|
199
|
+
currentLocationId: stringifyId(vehicle.currentlocationid),
|
|
200
|
+
nextLocationId: stringifyId(vehicle.nextlocationid),
|
|
201
|
+
currentStop: vehicle.currentstop,
|
|
202
|
+
nextStop: vehicle.nextstop,
|
|
203
|
+
lastStop: vehicle.laststop,
|
|
204
|
+
stopCoveredStatus: vehicle.stopCoveredStatus,
|
|
205
|
+
heading: vehicle.heading,
|
|
206
|
+
lastRefreshOn: vehicle.lastrefreshon,
|
|
207
|
+
lastReceivedDateTimeFlag: vehicle.lastreceiveddatetimeflag,
|
|
208
|
+
tripPosition: vehicle.tripposition,
|
|
209
|
+
}));
|
|
210
|
+
return {
|
|
211
|
+
stops: createFeatureCollection(stationFeatures),
|
|
212
|
+
stationVehicles: createFeatureCollection(stationVehicleFeatures),
|
|
213
|
+
liveVehicles: createFeatureCollection(liveVehicleFeatures),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Transform raw route details API response to clean, normalized format
|
|
218
|
+
*/
|
|
219
|
+
function transformRouteDetailsResponse(raw) {
|
|
220
|
+
return {
|
|
221
|
+
up: transformDirectionData(raw.up),
|
|
222
|
+
down: transformDirectionData(raw.down),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Transform raw route between stops item to clean, normalized format
|
|
227
|
+
*/
|
|
228
|
+
function transformRouteBetweenStopsItem(raw) {
|
|
229
|
+
return {
|
|
230
|
+
id: stringifyId(raw.id),
|
|
231
|
+
fromStopId: stringifyId(raw.fromstationid),
|
|
232
|
+
sourceCode: raw.source_code,
|
|
233
|
+
fromDisplayName: raw.from_displayname,
|
|
234
|
+
toStopId: stringifyId(raw.tostationid),
|
|
235
|
+
destinationCode: raw.destination_code,
|
|
236
|
+
toDisplayName: raw.to_displayname,
|
|
237
|
+
fromDistance: raw.fromdistance,
|
|
238
|
+
toDistance: raw.todistance,
|
|
239
|
+
subrouteId: stringifyId(raw.routeid),
|
|
240
|
+
routeNo: raw.routeno,
|
|
241
|
+
routeName: raw.routename,
|
|
242
|
+
routeDirection: raw.route_direction.toLowerCase(),
|
|
243
|
+
fromStopName: raw.fromstationname,
|
|
244
|
+
toStopName: raw.tostationname,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Transform raw routes between stops API response to clean, normalized format
|
|
249
|
+
*/
|
|
250
|
+
function transformRoutesBetweenStopsResponse(raw) {
|
|
251
|
+
return {
|
|
252
|
+
items: raw.data.map(transformRouteBetweenStopsItem),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Transform raw fare data API response to clean, normalized format
|
|
257
|
+
*/
|
|
258
|
+
function transformFareDataResponse(raw) {
|
|
259
|
+
const items = raw.data.map((item) => ({
|
|
260
|
+
serviceType: item.servicetype,
|
|
261
|
+
fare: item.fare,
|
|
262
|
+
}));
|
|
263
|
+
return {
|
|
264
|
+
items,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Transform raw path detail item to clean, normalized format
|
|
269
|
+
*/
|
|
270
|
+
function transformPathDetailItem(raw) {
|
|
271
|
+
return {
|
|
272
|
+
tripId: stringifyId(raw.tripId),
|
|
273
|
+
subrouteId: stringifyId(raw.routeId),
|
|
274
|
+
routeNo: raw.routeNo,
|
|
275
|
+
stopId: stringifyId(raw.stationId),
|
|
276
|
+
stopName: raw.stationName,
|
|
277
|
+
latitude: raw.latitude,
|
|
278
|
+
longitude: raw.longitude,
|
|
279
|
+
eta: raw.eta || null,
|
|
280
|
+
scheduledArrivalTime: raw.sch_arrivaltime || null,
|
|
281
|
+
scheduledDepartureTime: raw.sch_departuretime || null,
|
|
282
|
+
actualArrivalTime: raw.actual_arrivaltime || null,
|
|
283
|
+
actualDepartureTime: raw.actual_departuretime || null,
|
|
284
|
+
distance: raw.distance,
|
|
285
|
+
duration: raw.duration,
|
|
286
|
+
isTransfer: raw.isTransfer,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Transform raw path details response to clean, normalized format
|
|
291
|
+
*/
|
|
292
|
+
function transformPathDetailsResponse(raw) {
|
|
293
|
+
const items = raw.data.map(transformPathDetailItem);
|
|
294
|
+
// Create GeoJSON FeatureCollection from stops (Point features)
|
|
295
|
+
const stationFeatures = items.map((item) => createLocationFeature([item.longitude, item.latitude], {
|
|
296
|
+
tripId: item.tripId,
|
|
297
|
+
subrouteId: item.subrouteId,
|
|
298
|
+
routeNo: item.routeNo,
|
|
299
|
+
stopId: item.stopId,
|
|
300
|
+
stopName: item.stopName,
|
|
301
|
+
eta: item.eta,
|
|
302
|
+
scheduledArrivalTime: item.scheduledArrivalTime,
|
|
303
|
+
scheduledDepartureTime: item.scheduledDepartureTime,
|
|
304
|
+
actualArrivalTime: item.actualArrivalTime,
|
|
305
|
+
actualDepartureTime: item.actualDepartureTime,
|
|
306
|
+
distance: item.distance,
|
|
307
|
+
duration: item.duration,
|
|
308
|
+
isTransfer: item.isTransfer,
|
|
309
|
+
}));
|
|
310
|
+
return createFeatureCollection(stationFeatures);
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Transform raw trip planner path leg to clean, normalized format
|
|
314
|
+
*/
|
|
315
|
+
function transformTripPlannerPathLeg(raw) {
|
|
316
|
+
const durationSeconds = parseDurationToSeconds(raw.duration);
|
|
317
|
+
const totalDurationSeconds = parseDurationToSeconds(raw.totalDuration);
|
|
318
|
+
return {
|
|
319
|
+
pathSrNo: raw.pathSrno,
|
|
320
|
+
transferSrNo: raw.transferSrNo,
|
|
321
|
+
tripId: stringifyId(raw.tripId),
|
|
322
|
+
subrouteId: stringifyId(raw.routeid),
|
|
323
|
+
routeNo: raw.routeno,
|
|
324
|
+
scheduleNo: raw.schNo,
|
|
325
|
+
vehicleId: stringifyId(raw.vehicleId),
|
|
326
|
+
busNo: raw.busNo,
|
|
327
|
+
distance: raw.distance,
|
|
328
|
+
duration: raw.duration,
|
|
329
|
+
durationSeconds,
|
|
330
|
+
fromStopId: stringifyId(raw.fromStationId),
|
|
331
|
+
fromStopName: raw.fromStationName,
|
|
332
|
+
toStopId: stringifyId(raw.toStationId),
|
|
333
|
+
toStopName: raw.toStationName,
|
|
334
|
+
etaFromStop: raw.etaFromStation,
|
|
335
|
+
etaToStop: raw.etaToStation,
|
|
336
|
+
serviceTypeId: stringifyId(raw.serviceTypeId),
|
|
337
|
+
fromLatitude: raw.fromLatitude,
|
|
338
|
+
fromLongitude: raw.fromLongitude,
|
|
339
|
+
toLatitude: raw.toLatitude,
|
|
340
|
+
toLongitude: raw.toLongitude,
|
|
341
|
+
routeParentId: stringifyId(raw.routeParentId),
|
|
342
|
+
totalDuration: raw.totalDuration,
|
|
343
|
+
totalDurationSeconds,
|
|
344
|
+
waitingDuration: raw.waitingDuration,
|
|
345
|
+
platformNumber: raw.platformnumber,
|
|
346
|
+
bayNumber: raw.baynumber,
|
|
347
|
+
deviceStatusName: raw.devicestatusnameflag,
|
|
348
|
+
deviceStatusFlag: raw.devicestatusflag,
|
|
349
|
+
srNo: raw.srno,
|
|
350
|
+
approxFare: raw.approx_fare,
|
|
351
|
+
fromStageNumber: raw.fromstagenumber,
|
|
352
|
+
toStageNumber: raw.tostagenumber,
|
|
353
|
+
minSrNo: raw.minsrno,
|
|
354
|
+
maxSrNo: raw.maxsrno,
|
|
355
|
+
tollFees: raw.tollfees,
|
|
356
|
+
totalStages: raw.totalStages,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Parse duration string "HH:mm:ss" or "HH:mm" to total seconds
|
|
361
|
+
*/
|
|
362
|
+
function parseDurationToSeconds(duration) {
|
|
363
|
+
const parts = duration.split(":").map(Number);
|
|
364
|
+
if (parts.length === 3) {
|
|
365
|
+
// HH:mm:ss format
|
|
366
|
+
return parts[0] * 3600 + parts[1] * 60 + parts[2];
|
|
367
|
+
}
|
|
368
|
+
else if (parts.length === 2) {
|
|
369
|
+
// HH:mm format
|
|
370
|
+
return parts[0] * 3600 + parts[1] * 60;
|
|
371
|
+
}
|
|
372
|
+
return 0;
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Format seconds to "HH:mm:ss" duration string
|
|
376
|
+
*/
|
|
377
|
+
function formatSecondsToDuration(totalSeconds) {
|
|
378
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
379
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
380
|
+
const seconds = totalSeconds % 60;
|
|
381
|
+
return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Check if a route leg is a walking segment
|
|
385
|
+
*/
|
|
386
|
+
function isWalkingRoute(leg) {
|
|
387
|
+
return leg.routeNo === WALK_ROUTE_PREFIX || leg.routeNo.startsWith(WALK_PREFIX);
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Check if a route leg is a bus segment (not walking, has valid route ID)
|
|
391
|
+
*/
|
|
392
|
+
function isBusSegment(leg) {
|
|
393
|
+
return leg.subrouteId !== EMPTY_SUBROUTE_ID && !isWalkingRoute(leg);
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Calculate route totals from legs
|
|
397
|
+
*/
|
|
398
|
+
function calculateRouteTotals(legs) {
|
|
399
|
+
let totalFare = 0;
|
|
400
|
+
let totalDistance = 0;
|
|
401
|
+
let totalSeconds = 0;
|
|
402
|
+
let hasWalking = false;
|
|
403
|
+
let busSegmentCount = 0;
|
|
404
|
+
for (const leg of legs) {
|
|
405
|
+
totalFare += leg.approxFare;
|
|
406
|
+
totalDistance += leg.distance;
|
|
407
|
+
totalSeconds += parseDurationToSeconds(leg.duration);
|
|
408
|
+
// Add waiting duration if present
|
|
409
|
+
if (leg.waitingDuration) {
|
|
410
|
+
totalSeconds += parseDurationToSeconds(leg.waitingDuration);
|
|
411
|
+
}
|
|
412
|
+
// Check for walking segments
|
|
413
|
+
if (isWalkingRoute(leg)) {
|
|
414
|
+
hasWalking = true;
|
|
415
|
+
}
|
|
416
|
+
// Count bus segments (non-walking, non-zero routeId)
|
|
417
|
+
if (isBusSegment(leg)) {
|
|
418
|
+
busSegmentCount++;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
// Transfer count = bus segments - 1 (0 for direct routes)
|
|
422
|
+
const transferCount = Math.max(0, busSegmentCount - 1);
|
|
423
|
+
return {
|
|
424
|
+
totalDuration: formatSecondsToDuration(totalSeconds),
|
|
425
|
+
totalDurationSeconds: totalSeconds,
|
|
426
|
+
totalFare,
|
|
427
|
+
totalDistance,
|
|
428
|
+
transferCount,
|
|
429
|
+
hasWalking,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Transform a single route (array of raw legs) to TripPlannerRoute
|
|
434
|
+
*/
|
|
435
|
+
function transformRoute(route) {
|
|
436
|
+
const legs = route.map(transformTripPlannerPathLeg);
|
|
437
|
+
const totals = calculateRouteTotals(legs);
|
|
438
|
+
return {
|
|
439
|
+
legs,
|
|
440
|
+
...totals,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Transform raw trip planner API response to clean, normalized format
|
|
445
|
+
* Includes computed totals for each route
|
|
446
|
+
* Merges directRoutes and transferRoutes into a single routes array
|
|
447
|
+
*/
|
|
448
|
+
function transformTripPlannerResponse(raw) {
|
|
449
|
+
// Transform direct routes with computed totals
|
|
450
|
+
const directRoutes = raw.data.directRoutes.map(transformRoute);
|
|
451
|
+
// Transform transfer routes with computed totals
|
|
452
|
+
const transferRoutes = raw.data.transferRoutes.map(transformRoute);
|
|
453
|
+
// Merge both arrays into a single routes array (preserves all data)
|
|
454
|
+
const routes = [...directRoutes, ...transferRoutes];
|
|
455
|
+
return {
|
|
456
|
+
routes,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Build centerPoints string from via points array
|
|
461
|
+
* Format: "&via=lat1,lng1&via=lat2,lng2&..."
|
|
462
|
+
*/
|
|
463
|
+
function buildCenterPoints(viaPoints) {
|
|
464
|
+
if (viaPoints.length === 0) {
|
|
465
|
+
return "";
|
|
466
|
+
}
|
|
467
|
+
return viaPoints.map(([lat, lng]) => `&via=${lat},${lng}`).join("");
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Decode polyline strings to path segments
|
|
471
|
+
* Each encoded string represents a path segment decoded using HERE Flexible Polyline format
|
|
472
|
+
*/
|
|
473
|
+
function decodeWaypointsToGeoJSON(encodedStrings) {
|
|
474
|
+
const segments = [];
|
|
475
|
+
for (const encoded of encodedStrings) {
|
|
476
|
+
try {
|
|
477
|
+
// HERE decoder returns absolute coordinates in [lat, lng] format
|
|
478
|
+
const decoded = hereDecode(encoded);
|
|
479
|
+
const polyline = decoded.polyline;
|
|
480
|
+
// Convert [lat, lng] to [lng, lat] format
|
|
481
|
+
const coordinates = polyline.map((point) => [point[1], point[0]]);
|
|
482
|
+
// Validate coordinates
|
|
483
|
+
const isValid = coordinates.every((coord) => coord[0] >= -180 &&
|
|
484
|
+
coord[0] <= 180 &&
|
|
485
|
+
coord[1] >= -90 &&
|
|
486
|
+
coord[1] <= 90);
|
|
487
|
+
if (isValid && coordinates.length > 0) {
|
|
488
|
+
segments.push({
|
|
489
|
+
coordinates: coordinates,
|
|
490
|
+
pointCount: coordinates.length,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
catch (error) {
|
|
495
|
+
// Skip segments that fail to decode
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return segments;
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Transform raw waypoints response to GeoJSON FeatureCollection with LineString features
|
|
502
|
+
* Each encoded string from the API corresponds to one LineString segment
|
|
503
|
+
*/
|
|
504
|
+
function transformWaypointsResponse(raw) {
|
|
505
|
+
const segments = decodeWaypointsToGeoJSON(raw);
|
|
506
|
+
// Create LineString features for each segment (one API response string = one LineString feature)
|
|
507
|
+
// Properties are empty as they're not provided by the API
|
|
508
|
+
const lineFeatures = segments.map((segment) => ({
|
|
509
|
+
type: "Feature",
|
|
510
|
+
geometry: {
|
|
511
|
+
type: "LineString",
|
|
512
|
+
coordinates: segment.coordinates,
|
|
513
|
+
},
|
|
514
|
+
properties: {},
|
|
515
|
+
}));
|
|
516
|
+
return createFeatureCollection(lineFeatures);
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Routes API methods
|
|
520
|
+
*/
|
|
521
|
+
export class RoutesAPI {
|
|
522
|
+
client;
|
|
523
|
+
constructor(client) {
|
|
524
|
+
this.client = client;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Get route points (path) for a given route ID
|
|
528
|
+
* @param params - Parameters including route ID
|
|
529
|
+
* @param params.routeId - Route ID (always string for consistency)
|
|
530
|
+
* @returns Route path as GeoJSON LineString FeatureCollection
|
|
531
|
+
* @throws {TransitValidationError} If routeId is invalid or validation fails
|
|
532
|
+
* @throws {HTTPError} If the API request fails (network error, 4xx, 5xx)
|
|
533
|
+
* @example
|
|
534
|
+
* ```typescript
|
|
535
|
+
* const routePath = await client.routes.getRoutePoints({
|
|
536
|
+
* routeId: "11797"
|
|
537
|
+
* });
|
|
538
|
+
* // Use routePath.routePath.features[0].geometry.coordinates to draw on map
|
|
539
|
+
* ```
|
|
540
|
+
*/
|
|
541
|
+
async getRoutePoints(params) {
|
|
542
|
+
// Validate input parameters - API expects number, convert from string
|
|
543
|
+
const validatedParams = validate(routePointsParamsSchema, { routeid: parseId(params.routeId) }, "Invalid route points parameters");
|
|
544
|
+
const response = await this.client.getClient().post("RoutePoints", {
|
|
545
|
+
json: validatedParams,
|
|
546
|
+
});
|
|
547
|
+
const data = await response.json();
|
|
548
|
+
// Validate raw response with Zod schema
|
|
549
|
+
const rawResponse = validate(rawRoutePointsResponseSchema, data, "Invalid route points response");
|
|
550
|
+
// Transform to clean, normalized format
|
|
551
|
+
return transformRoutePointsResponse(rawResponse, params.routeId);
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Search for routes by query text (partial match)
|
|
555
|
+
* @param params - Parameters including search query
|
|
556
|
+
* @param params.query - Search query for routes (partial match supported)
|
|
557
|
+
* @returns List of matching routes in normalized format
|
|
558
|
+
* @throws {TransitValidationError} If query is invalid or validation fails
|
|
559
|
+
* @throws {HTTPError} If the API request fails (network error, 4xx, 5xx)
|
|
560
|
+
* @example
|
|
561
|
+
* ```typescript
|
|
562
|
+
* const routes = await client.routes.searchRoutes({ query: "285-M" });
|
|
563
|
+
* routes.items.forEach(route => {
|
|
564
|
+
* console.log(`${route.routeNo} - ${route.parentRouteId}`);
|
|
565
|
+
* });
|
|
566
|
+
* ```
|
|
567
|
+
*/
|
|
568
|
+
async searchRoutes(params) {
|
|
569
|
+
// Validate input parameters
|
|
570
|
+
const validatedParams = validate(routeSearchParamsSchema, { routetext: params.query }, "Invalid route search parameters");
|
|
571
|
+
const response = await this.client.getClient().post("SearchRoute_v2", {
|
|
572
|
+
json: validatedParams,
|
|
573
|
+
});
|
|
574
|
+
const data = await response.json();
|
|
575
|
+
// Validate raw response with Zod schema
|
|
576
|
+
const rawResponse = validate(rawRouteSearchResponseSchema, data, "Invalid route search response");
|
|
577
|
+
// Transform to clean, normalized format
|
|
578
|
+
return transformRouteSearchResponse(rawResponse);
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Get all routes list
|
|
582
|
+
* @returns List of all routes in normalized format
|
|
583
|
+
* @throws {HTTPError} If the API request fails (network error, 4xx, 5xx)
|
|
584
|
+
* @example
|
|
585
|
+
* ```typescript
|
|
586
|
+
* const allRoutes = await client.routes.getAllRoutes();
|
|
587
|
+
* console.log(`Total routes: ${allRoutes.items.length}`);
|
|
588
|
+
*
|
|
589
|
+
* // Find routes by name
|
|
590
|
+
* const matchingRoutes = allRoutes.items.filter(r =>
|
|
591
|
+
* r.routeName.includes("KBS")
|
|
592
|
+
* );
|
|
593
|
+
* ```
|
|
594
|
+
*/
|
|
595
|
+
async getAllRoutes() {
|
|
596
|
+
const response = await this.client.getClient().post("GetAllRouteList", {
|
|
597
|
+
json: {},
|
|
598
|
+
});
|
|
599
|
+
const data = await response.json();
|
|
600
|
+
// Validate raw response with Zod schema
|
|
601
|
+
const rawResponse = validate(rawAllRoutesResponseSchema, data, "Invalid all routes response");
|
|
602
|
+
// Transform to clean, normalized format
|
|
603
|
+
return transformAllRoutesResponse(rawResponse);
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Get timetable by route ID
|
|
607
|
+
* @param params - Parameters including route ID and optional filters
|
|
608
|
+
* @param params.routeId - Route ID (always string for consistency)
|
|
609
|
+
* @param params.startTime - Optional: Start time for timetable (Date object, defaults to current time)
|
|
610
|
+
* @param params.endTime - Optional: End time for timetable (Date object, defaults to 23:59 of startTime date)
|
|
611
|
+
* @param params.fromStopId - Optional: Filter by from stop ID (requires toStopId)
|
|
612
|
+
* @param params.toStopId - Optional: Filter by to stop ID (requires fromStopId)
|
|
613
|
+
* @returns Timetable data in normalized format
|
|
614
|
+
* @throws {TransitValidationError} If routeId is invalid, validation fails, or stop IDs are provided incorrectly
|
|
615
|
+
* @throws {HTTPError} If the API request fails (network error, 4xx, 5xx)
|
|
616
|
+
* @example
|
|
617
|
+
* ```typescript
|
|
618
|
+
* // Get timetable for entire route
|
|
619
|
+
* const timetable = await client.routes.getTimetableByRoute({
|
|
620
|
+
* routeId: "11797"
|
|
621
|
+
* });
|
|
622
|
+
*
|
|
623
|
+
* // Get timetable between specific stops
|
|
624
|
+
* const timetable = await client.routes.getTimetableByRoute({
|
|
625
|
+
* routeId: "11797",
|
|
626
|
+
* fromStopId: "22357",
|
|
627
|
+
* toStopId: "21447",
|
|
628
|
+
* startTime: new Date("2026-01-20T09:00:00")
|
|
629
|
+
* });
|
|
630
|
+
* ```
|
|
631
|
+
*/
|
|
632
|
+
async getTimetableByRoute(params) {
|
|
633
|
+
// Generate current date in ISO 8601 format
|
|
634
|
+
const currentDate = formatISODate(new Date());
|
|
635
|
+
// Determine start time - use current time if not provided
|
|
636
|
+
const startTimeDate = params.startTime ?? new Date();
|
|
637
|
+
const startTime = formatDateTime(startTimeDate);
|
|
638
|
+
// Determine end time - use 23:59 of startTime date if not provided
|
|
639
|
+
let endTime;
|
|
640
|
+
if (params.endTime) {
|
|
641
|
+
endTime = formatDateTime(params.endTime);
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
// Extract date from startTime (format: "YYYY-MM-DD HH:mm")
|
|
645
|
+
const startTimeDateStr = formatDate(startTimeDate);
|
|
646
|
+
endTime = `${startTimeDateStr} 23:59`;
|
|
647
|
+
}
|
|
648
|
+
// Build request payload - API expects numbers for IDs, convert from strings
|
|
649
|
+
const requestPayload = {
|
|
650
|
+
current_date: currentDate,
|
|
651
|
+
routeid: parseId(params.routeId),
|
|
652
|
+
starttime: startTime,
|
|
653
|
+
endtime: endTime,
|
|
654
|
+
};
|
|
655
|
+
// Add stop IDs if provided (type-safe: both are required together)
|
|
656
|
+
if ("fromStopId" in params && "toStopId" in params) {
|
|
657
|
+
requestPayload.fromStationId = params.fromStopId;
|
|
658
|
+
requestPayload.toStationId = params.toStopId;
|
|
659
|
+
}
|
|
660
|
+
// Validate request payload
|
|
661
|
+
const validatedParams = validate(timetableRequestSchema, requestPayload, "Invalid timetable parameters");
|
|
662
|
+
const response = await this.client
|
|
663
|
+
.getClient()
|
|
664
|
+
.post("GetTimetableByRouteid_v3", {
|
|
665
|
+
json: validatedParams,
|
|
666
|
+
});
|
|
667
|
+
const data = await response.json();
|
|
668
|
+
// Validate raw response with Zod schema
|
|
669
|
+
const rawResponse = validate(rawTimetableResponseSchema, data, "Invalid timetable response");
|
|
670
|
+
// Transform to clean, normalized format
|
|
671
|
+
return transformTimetableResponse(rawResponse);
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Search route details by parent route ID
|
|
675
|
+
* @param params - Parameters including parent route ID and optional service type ID
|
|
676
|
+
* @param params.parentRouteId - Parent route ID (always string for consistency, obtained from searchRoutes)
|
|
677
|
+
* @param params.serviceTypeId - Optional: Filter by service type ID
|
|
678
|
+
* @returns Route details with live vehicle information for both directions (up and down)
|
|
679
|
+
* @throws {Error} If parentRouteId is invalid or validation fails
|
|
680
|
+
* @throws {HTTPError} If the API request fails (network error, 4xx, 5xx)
|
|
681
|
+
* @remarks
|
|
682
|
+
* The parentRouteId should be obtained from searchRoutes().parentRouteId.
|
|
683
|
+
* The response contains subroute IDs in up.stops and down.stops (subrouteId property).
|
|
684
|
+
* @example
|
|
685
|
+
* ```typescript
|
|
686
|
+
* // First search for route
|
|
687
|
+
* const routes = await client.routes.searchRoutes({ query: "500-CA" });
|
|
688
|
+
* const parentRouteId = routes.items[0].parentRouteId;
|
|
689
|
+
*
|
|
690
|
+
* // Get route details with live vehicles
|
|
691
|
+
* const details = await client.routes.searchByRouteDetails({ parentRouteId });
|
|
692
|
+
*
|
|
693
|
+
* // Access live vehicles for up direction
|
|
694
|
+
* details.up.vehicles.features.forEach(vehicle => {
|
|
695
|
+
* console.log(`Vehicle ${vehicle.properties.vehicleRegNo} at ${vehicle.geometry.coordinates}`);
|
|
696
|
+
* });
|
|
697
|
+
* ```
|
|
698
|
+
*/
|
|
699
|
+
async searchByRouteDetails(params) {
|
|
700
|
+
// Build request payload - API expects numbers for IDs, convert from strings
|
|
701
|
+
const requestPayload = {
|
|
702
|
+
routeid: parseId(params.parentRouteId),
|
|
703
|
+
};
|
|
704
|
+
// Add service type ID if provided
|
|
705
|
+
if (params.serviceTypeId) {
|
|
706
|
+
requestPayload.servicetypeid = parseId(params.serviceTypeId);
|
|
707
|
+
}
|
|
708
|
+
// Validate request payload
|
|
709
|
+
const validatedParams = validate(routeDetailsParamsSchema, requestPayload, "Invalid route details parameters");
|
|
710
|
+
const response = await this.client
|
|
711
|
+
.getClient()
|
|
712
|
+
.post("SearchByRouteDetails_v4", {
|
|
713
|
+
json: validatedParams,
|
|
714
|
+
});
|
|
715
|
+
const data = await response.json();
|
|
716
|
+
// Validate raw response with Zod schema
|
|
717
|
+
const rawResponse = validate(rawRouteDetailsResponseSchema, data, "Invalid route details response");
|
|
718
|
+
// Transform to clean, normalized format
|
|
719
|
+
return transformRouteDetailsResponse(rawResponse);
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Get routes between two stops
|
|
723
|
+
* @param params - Parameters including from and to stop IDs
|
|
724
|
+
* @param params.fromStopId - From stop ID (always string for consistency)
|
|
725
|
+
* @param params.toStopId - To stop ID (always string for consistency)
|
|
726
|
+
* @returns List of routes connecting the two stops in normalized format
|
|
727
|
+
* @throws {TransitValidationError} If stop IDs are invalid or validation fails
|
|
728
|
+
* @throws {HTTPError} If the API request fails (network error, 4xx, 5xx)
|
|
729
|
+
* @remarks
|
|
730
|
+
* The routeId in the response is likely a subroute ID (specific to direction/variant).
|
|
731
|
+
* It can be used with searchByRouteDetails() endpoint.
|
|
732
|
+
* Note: This differs from parentRouteId returned by searchRoutes().
|
|
733
|
+
* @example
|
|
734
|
+
* ```typescript
|
|
735
|
+
* const routes = await client.routes.getRoutesBetweenStops({
|
|
736
|
+
* fromStopId: "22357",
|
|
737
|
+
* toStopId: "21447"
|
|
738
|
+
* });
|
|
739
|
+
*
|
|
740
|
+
* routes.items.forEach(route => {
|
|
741
|
+
* console.log(`Route ${route.routeNo}: ${route.fromStop} → ${route.toStop}`);
|
|
742
|
+
* });
|
|
743
|
+
* ```
|
|
744
|
+
*/
|
|
745
|
+
async getRoutesBetweenStops(params) {
|
|
746
|
+
// Validate input parameters - API expects numbers, convert from strings
|
|
747
|
+
const validatedParams = validate(routesBetweenStopsParamsSchema, {
|
|
748
|
+
fromStationId: parseId(params.fromStopId), // API uses "station" in field names
|
|
749
|
+
toStationId: parseId(params.toStopId), // API uses "station" in field names
|
|
750
|
+
}, "Invalid routes between stops parameters");
|
|
751
|
+
// Get language from client and map to API format
|
|
752
|
+
const language = this.client.getLanguage();
|
|
753
|
+
const lan = language === "en" ? "English" : "Kannada";
|
|
754
|
+
const response = await this.client.getClient().post("GetFareRoutes", {
|
|
755
|
+
json: {
|
|
756
|
+
fromStationId: validatedParams.fromStationId,
|
|
757
|
+
toStationId: validatedParams.toStationId,
|
|
758
|
+
lan,
|
|
759
|
+
},
|
|
760
|
+
});
|
|
761
|
+
const data = await response.json();
|
|
762
|
+
// Validate raw response with Zod schema
|
|
763
|
+
const rawResponse = validate(rawRoutesBetweenStationsResponseSchema, data, "Invalid routes between stops response");
|
|
764
|
+
// Transform to clean, normalized format
|
|
765
|
+
return transformRoutesBetweenStopsResponse(rawResponse);
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Get fares for a route between stops
|
|
769
|
+
* @param params - Parameters from getRoutesBetweenStops() response
|
|
770
|
+
* @param params.routeId - Subroute ID (always string for consistency, from RouteBetweenStopsItem)
|
|
771
|
+
* @param params.routeDirection - Route direction: "up" or "down"
|
|
772
|
+
* @param params.sourceCode - Source code (from RouteBetweenStopsItem)
|
|
773
|
+
* @param params.destinationCode - Destination code (from RouteBetweenStopsItem)
|
|
774
|
+
* @returns Fares with service types and their fare amounts
|
|
775
|
+
* @throws {Error} If parameters are invalid or validation fails
|
|
776
|
+
* @throws {HTTPError} If the API request fails (network error, 4xx, 5xx)
|
|
777
|
+
* @remarks
|
|
778
|
+
* The parameters should come from a RouteBetweenStopsItem returned by getRoutesBetweenStops().
|
|
779
|
+
* This endpoint requires the subroute ID (not parent route ID) along with route direction,
|
|
780
|
+
* source and destination codes to determine the exact fare for that route variant.
|
|
781
|
+
* @example
|
|
782
|
+
* ```typescript
|
|
783
|
+
* // First get routes between stops
|
|
784
|
+
* const routes = await client.routes.getRoutesBetweenStops({
|
|
785
|
+
* fromStopId: "22357",
|
|
786
|
+
* toStopId: "21447"
|
|
787
|
+
* });
|
|
788
|
+
*
|
|
789
|
+
* // Get fare for first route
|
|
790
|
+
* const route = routes.items[0];
|
|
791
|
+
* const fare = await client.routes.getFares({
|
|
792
|
+
* routeId: route.subrouteId,
|
|
793
|
+
* routeDirection: route.routeDirection,
|
|
794
|
+
* sourceCode: route.sourceCode,
|
|
795
|
+
* destinationCode: route.destinationCode
|
|
796
|
+
* });
|
|
797
|
+
*
|
|
798
|
+
* fare.items.forEach(item => {
|
|
799
|
+
* console.log(`Service: ${item.serviceType}, Fare: ₹${item.fare}`);
|
|
800
|
+
* });
|
|
801
|
+
* ```
|
|
802
|
+
*/
|
|
803
|
+
async getFares(params) {
|
|
804
|
+
// Validate input parameters - API expects numbers for routeid, convert from string
|
|
805
|
+
// Normalize routeDirection to lowercase for validation, then convert to uppercase for API
|
|
806
|
+
const validatedParams = validate(fareDataParamsSchema, {
|
|
807
|
+
routeno: params.routeNo,
|
|
808
|
+
routeid: parseId(params.subrouteId),
|
|
809
|
+
route_direction: params.routeDirection, // Already lowercase "up" | "down"
|
|
810
|
+
source_code: params.sourceCode,
|
|
811
|
+
destination_code: params.destinationCode,
|
|
812
|
+
}, "Invalid fare data parameters");
|
|
813
|
+
// Convert lowercase "up" | "down" to uppercase "UP" | "DOWN" for API request
|
|
814
|
+
const apiParams = {
|
|
815
|
+
...validatedParams,
|
|
816
|
+
route_direction: validatedParams.route_direction.toUpperCase(),
|
|
817
|
+
};
|
|
818
|
+
const response = await this.client.getClient().post("GetMobileFareData_v2", {
|
|
819
|
+
json: apiParams,
|
|
820
|
+
});
|
|
821
|
+
const data = await response.json();
|
|
822
|
+
// Validate raw response with Zod schema
|
|
823
|
+
const rawResponse = validate(rawFareDataResponseSchema, data, "Invalid fare data response");
|
|
824
|
+
// Transform to clean, normalized format
|
|
825
|
+
return transformFareDataResponse(rawResponse);
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Plan a trip with multiple route options
|
|
829
|
+
* @param params - Trip planner parameters with 4 possible combinations:
|
|
830
|
+
* - Stop to Stop: fromStopId, toStopId
|
|
831
|
+
* - Stop to Location: fromStopId, toCoordinates
|
|
832
|
+
* - Location to Stop: fromCoordinates, toStopId
|
|
833
|
+
* - Location to Location: fromCoordinates, toCoordinates
|
|
834
|
+
* @returns Trip plans with all available routes (merged from directRoutes and transferRoutes)
|
|
835
|
+
* @remarks
|
|
836
|
+
* This endpoint supports 4 combinations of origin/destination:
|
|
837
|
+
* - Stop IDs (always string for consistency, will be converted to numbers for API)
|
|
838
|
+
* - Coordinates (latitude/longitude)
|
|
839
|
+
* Optional parameters:
|
|
840
|
+
* - serviceTypeId: Filter by service type (from getAllServiceTypes)
|
|
841
|
+
* - fromDateTime: Future datetime (Date object, converted to "YYYY-MM-DD HH:mm" format)
|
|
842
|
+
* - filterBy: "minimum-transfers" or "shortest-time"
|
|
843
|
+
*
|
|
844
|
+
* All routes are returned in a single `routes` array. Filter by `transferCount === 0` to identify direct routes.
|
|
845
|
+
* @throws {TransitError} If fromDateTime is not in the future
|
|
846
|
+
* @throws {TransitValidationError} If parameters are invalid or validation fails
|
|
847
|
+
* @throws {HTTPError} If the API request fails (network error, 4xx, 5xx)
|
|
848
|
+
* @example
|
|
849
|
+
* ```typescript
|
|
850
|
+
* // Location to Stop
|
|
851
|
+
* const trip = await client.routes.planTrip({
|
|
852
|
+
* fromCoordinates: [13.09784, 77.59167],
|
|
853
|
+
* toStopId: "20922"
|
|
854
|
+
* });
|
|
855
|
+
*
|
|
856
|
+
* // Find fastest direct route
|
|
857
|
+
* const fastest = trip.routes
|
|
858
|
+
* .filter(r => r.transferCount === 0)
|
|
859
|
+
* .sort((a, b) => a.totalDurationSeconds - b.totalDurationSeconds)[0];
|
|
860
|
+
* ```
|
|
861
|
+
* @example
|
|
862
|
+
* ```typescript
|
|
863
|
+
* // Stop to Stop with filters
|
|
864
|
+
* const trip = await client.routes.planTrip({
|
|
865
|
+
* fromStopId: "22357",
|
|
866
|
+
* toStopId: "21447",
|
|
867
|
+
* filterBy: "shortest-time",
|
|
868
|
+
* fromDateTime: new Date("2026-01-20T09:00:00")
|
|
869
|
+
* });
|
|
870
|
+
* ```
|
|
871
|
+
*/
|
|
872
|
+
async planTrip(params) {
|
|
873
|
+
// Convert discriminated union params to API payload format
|
|
874
|
+
const apiPayload = {};
|
|
875
|
+
// Set "from" type (either stop ID or coordinates)
|
|
876
|
+
if ("fromStopId" in params && params.fromStopId) {
|
|
877
|
+
apiPayload.fromStationId = parseId(params.fromStopId);
|
|
878
|
+
}
|
|
879
|
+
else if ("fromCoordinates" in params && params.fromCoordinates) {
|
|
880
|
+
const [lat, lng] = params.fromCoordinates;
|
|
881
|
+
apiPayload.fromLatitude = lat;
|
|
882
|
+
apiPayload.fromLongitude = lng;
|
|
883
|
+
}
|
|
884
|
+
// Set "to" type (either stop ID or coordinates)
|
|
885
|
+
if ("toStopId" in params && params.toStopId) {
|
|
886
|
+
apiPayload.toStationId = parseId(params.toStopId);
|
|
887
|
+
}
|
|
888
|
+
else if ("toCoordinates" in params && params.toCoordinates) {
|
|
889
|
+
const [lat, lng] = params.toCoordinates;
|
|
890
|
+
apiPayload.toLatitude = lat;
|
|
891
|
+
apiPayload.toLongitude = lng;
|
|
892
|
+
}
|
|
893
|
+
// Add optional parameters
|
|
894
|
+
if (params.serviceTypeId !== undefined) {
|
|
895
|
+
apiPayload.serviceTypeId = parseId(params.serviceTypeId);
|
|
896
|
+
}
|
|
897
|
+
if (params.fromDateTime !== undefined) {
|
|
898
|
+
// Validate that the date is in the future
|
|
899
|
+
const now = new Date();
|
|
900
|
+
if (params.fromDateTime <= now) {
|
|
901
|
+
throw new TransitError("fromDateTime must be in the future", "VALIDATION_ERROR");
|
|
902
|
+
}
|
|
903
|
+
// Convert Date to "YYYY-MM-DD HH:mm" format
|
|
904
|
+
apiPayload.fromDateTime = formatDateTime(params.fromDateTime);
|
|
905
|
+
}
|
|
906
|
+
if (params.filterBy !== undefined) {
|
|
907
|
+
apiPayload.filterBy = tripPlannerFilterToNumber(params.filterBy);
|
|
908
|
+
}
|
|
909
|
+
// Validate API payload format
|
|
910
|
+
const validatedParams = validate(tripPlannerParamsSchema, apiPayload, "Invalid trip planner parameters");
|
|
911
|
+
const response = await this.client.getClient().post("TripPlannerMSMD", {
|
|
912
|
+
json: validatedParams,
|
|
913
|
+
});
|
|
914
|
+
const data = await response.json();
|
|
915
|
+
// Validate raw response with Zod schema
|
|
916
|
+
const rawResponse = validate(rawTripPlannerResponseSchema, data, "Invalid trip planner response");
|
|
917
|
+
// Transform to clean, normalized format
|
|
918
|
+
return transformTripPlannerResponse(rawResponse);
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* Get all stops/stations along trip legs
|
|
922
|
+
* @param params - Path details parameters with array of trip leg segments
|
|
923
|
+
* @param params.trips - Array of trip leg segments, each with tripId, fromStopId, and toStopId
|
|
924
|
+
* @returns All stops along the trip legs with station details, scheduled times, and coordinates as GeoJSON FeatureCollection
|
|
925
|
+
* @remarks
|
|
926
|
+
* This endpoint is typically used after planning a trip to get detailed
|
|
927
|
+
* station-by-station information for each leg of the journey.
|
|
928
|
+
* Each item in the request should have tripId, fromStopId, and toStopId
|
|
929
|
+
* (typically from TripPlannerPathLeg).
|
|
930
|
+
*
|
|
931
|
+
* Note: This returns stops/stations, not a geographic path (for route paths, use getTripPath).
|
|
932
|
+
* @throws {TransitValidationError} If trip parameters are invalid or validation fails
|
|
933
|
+
* @throws {HTTPError} If the API request fails (network error, 4xx, 5xx)
|
|
934
|
+
* @example
|
|
935
|
+
* ```typescript
|
|
936
|
+
* // After planning a trip, get all stops along the route
|
|
937
|
+
* const trip = await client.routes.planTrip({
|
|
938
|
+
* fromStopId: "22357",
|
|
939
|
+
* toStopId: "21447"
|
|
940
|
+
* });
|
|
941
|
+
*
|
|
942
|
+
* // Extract trip legs (excluding walking segments)
|
|
943
|
+
* const tripLegs = trip.routes[0].legs
|
|
944
|
+
* .filter(leg => !leg.routeNo.startsWith('walk'))
|
|
945
|
+
* .map(leg => ({
|
|
946
|
+
* tripId: leg.tripId,
|
|
947
|
+
* fromStopId: leg.fromStopId,
|
|
948
|
+
* toStopId: leg.toStopId
|
|
949
|
+
* }));
|
|
950
|
+
*
|
|
951
|
+
* // Get all stops as GeoJSON
|
|
952
|
+
* const stops = await client.routes.getTripStops({ trips: tripLegs });
|
|
953
|
+
* // Use stops.features to display on map
|
|
954
|
+
* ```
|
|
955
|
+
*/
|
|
956
|
+
async getTripStops(params) {
|
|
957
|
+
// Validate user-facing params first
|
|
958
|
+
const validatedUserParams = validate(pathDetailsParamsSchema, params, "Invalid path details parameters");
|
|
959
|
+
// Convert string IDs to numbers and map 'trips' to 'data' for API
|
|
960
|
+
const apiPayload = {
|
|
961
|
+
data: validatedUserParams.trips.map((item) => ({
|
|
962
|
+
tripId: parseId(item.tripId),
|
|
963
|
+
fromStationId: parseId(item.fromStopId), // API uses "station" in field names
|
|
964
|
+
toStationId: parseId(item.toStopId), // API uses "station" in field names
|
|
965
|
+
})),
|
|
966
|
+
};
|
|
967
|
+
// Validate API payload format
|
|
968
|
+
const validatedParams = validate(pathDetailsApiParamsSchema, apiPayload, "Invalid path details API parameters");
|
|
969
|
+
const response = await this.client.getClient().post("GetPathDetails", {
|
|
970
|
+
json: validatedParams,
|
|
971
|
+
});
|
|
972
|
+
const data = await response.json();
|
|
973
|
+
// Validate raw response with Zod schema
|
|
974
|
+
const rawResponse = validate(rawPathDetailsResponseSchema, data, "Invalid path details response");
|
|
975
|
+
// Transform to clean, normalized format
|
|
976
|
+
return transformPathDetailsResponse(rawResponse);
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Get trip path as GeoJSON FeatureCollection with LineString features
|
|
980
|
+
* @param params - Trip path parameters with via points (bus stops)
|
|
981
|
+
* @param params.viaPoints - Array of [latitude, longitude] coordinates or GeoJSON FeatureCollection from getTripStops()
|
|
982
|
+
* @returns GeoJSON FeatureCollection with LineString features representing the route path
|
|
983
|
+
* @remarks
|
|
984
|
+
* This endpoint returns encoded polyline strings representing path segments between origin, via points, and destination.
|
|
985
|
+
* The wrapper decodes these using HERE Flexible Polyline format into GeoJSON LineString features.
|
|
986
|
+
* Each segment becomes a LineString feature with [lng, lat] coordinates.
|
|
987
|
+
*
|
|
988
|
+
* The first point in viaPoints is the origin, the last point is the destination.
|
|
989
|
+
* All intermediate points are treated as via points (bus stops along the route).
|
|
990
|
+
*
|
|
991
|
+
* You can pass GeoJSON FeatureCollection from `getTripStops()` - coordinates will be extracted, properties ignored.
|
|
992
|
+
* @throws {TransitValidationError} If viaPoints are invalid or validation fails
|
|
993
|
+
* @throws {HTTPError} If the API request fails (network error, 4xx, 5xx)
|
|
994
|
+
* @example
|
|
995
|
+
* ```typescript
|
|
996
|
+
* // Get stops first
|
|
997
|
+
* const stops = await client.routes.getTripStops({ trips: tripLegs });
|
|
998
|
+
*
|
|
999
|
+
* // Get path using stops (coordinates extracted automatically)
|
|
1000
|
+
* const path = await client.routes.getTripPath({ viaPoints: stops });
|
|
1001
|
+
* // Use path.features to draw route lines on map
|
|
1002
|
+
* ```
|
|
1003
|
+
* @example
|
|
1004
|
+
* ```typescript
|
|
1005
|
+
* // Or use coordinates directly
|
|
1006
|
+
* const path = await client.routes.getTripPath({
|
|
1007
|
+
* viaPoints: [
|
|
1008
|
+
* [13.09784, 77.59167], // Origin
|
|
1009
|
+
* [13.09884, 77.59267], // Via point
|
|
1010
|
+
* [13.09984, 77.59367] // Destination
|
|
1011
|
+
* ]
|
|
1012
|
+
* });
|
|
1013
|
+
* ```
|
|
1014
|
+
*/
|
|
1015
|
+
async getTripPath(params) {
|
|
1016
|
+
let viaPoints;
|
|
1017
|
+
// Check if viaPoints is a FeatureCollection (GeoJSON)
|
|
1018
|
+
if ("type" in params.viaPoints && params.viaPoints.type === "FeatureCollection") {
|
|
1019
|
+
// Extract coordinates from GeoJSON FeatureCollection (ignore properties)
|
|
1020
|
+
viaPoints = params.viaPoints.features
|
|
1021
|
+
.filter((feature) => feature.geometry.type === "Point")
|
|
1022
|
+
.map((feature) => {
|
|
1023
|
+
// TypeScript knows this is Point geometry after filter
|
|
1024
|
+
const geometry = feature.geometry;
|
|
1025
|
+
if (geometry.type === "Point") {
|
|
1026
|
+
const [lng, lat] = geometry.coordinates;
|
|
1027
|
+
return [lat, lng]; // Convert [lng, lat] to [lat, lng]
|
|
1028
|
+
}
|
|
1029
|
+
return null;
|
|
1030
|
+
})
|
|
1031
|
+
.filter((point) => point !== null);
|
|
1032
|
+
}
|
|
1033
|
+
else {
|
|
1034
|
+
// Validate input parameters for array of coordinates
|
|
1035
|
+
const validatedParams = validate(waypointsParamsSchema, params, "Invalid waypoints parameters");
|
|
1036
|
+
viaPoints = validatedParams.viaPoints;
|
|
1037
|
+
}
|
|
1038
|
+
// Extract origin (first point) and destination (last point)
|
|
1039
|
+
const [originLat, originLng] = viaPoints[0];
|
|
1040
|
+
const [destLat, destLng] = viaPoints[viaPoints.length - 1];
|
|
1041
|
+
// Build centerPoints string from all via points (origin + intermediate + destination)
|
|
1042
|
+
const centerPoints = buildCenterPoints(viaPoints);
|
|
1043
|
+
// Prepare API payload
|
|
1044
|
+
const apiPayload = {
|
|
1045
|
+
FromLat: originLat,
|
|
1046
|
+
FromLong: originLng,
|
|
1047
|
+
ToLat: destLat,
|
|
1048
|
+
ToLong: destLng,
|
|
1049
|
+
centerPoints,
|
|
1050
|
+
AppName: "BMTC",
|
|
1051
|
+
DeviceType: DEFAULT_DEVICE_TYPE,
|
|
1052
|
+
};
|
|
1053
|
+
const response = await this.client.getClient().post("getWaypoints_v1", {
|
|
1054
|
+
json: apiPayload,
|
|
1055
|
+
});
|
|
1056
|
+
const data = await response.json();
|
|
1057
|
+
// Validate raw response (array of strings)
|
|
1058
|
+
const rawResponse = validate(rawWaypointsResponseSchema, data, "Invalid waypoints response");
|
|
1059
|
+
// Transform to clean, normalized format with decoded coordinates
|
|
1060
|
+
return transformWaypointsResponse(rawResponse);
|
|
1061
|
+
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Get routes that pass through both stations (in sequence)
|
|
1064
|
+
* @param params - Parameters including from station ID, to station ID, and optional filters
|
|
1065
|
+
* @param params.fromStopId - From stop ID (always string for consistency)
|
|
1066
|
+
* @param params.toStopId - To stop ID (always string for consistency)
|
|
1067
|
+
* @param params.routeId - Optional: Filter by specific route ID
|
|
1068
|
+
* @param params.date - Optional: Date for timetable (Date object, defaults to current date)
|
|
1069
|
+
* @returns Routes that go through both stations with schedule information
|
|
1070
|
+
* @remarks
|
|
1071
|
+
* This is NOT a full timetable - each route has one startTime, not multiple scheduled trips.
|
|
1072
|
+
* Routes may start before fromStop and/or continue after toStop.
|
|
1073
|
+
* @throws {TransitValidationError} If stop IDs are invalid or validation fails
|
|
1074
|
+
* @throws {HTTPError} If the API request fails (network error, 4xx, 5xx)
|
|
1075
|
+
* @example
|
|
1076
|
+
* ```typescript
|
|
1077
|
+
* // Find routes passing through both stops
|
|
1078
|
+
* const routes = await client.routes.getRoutesThroughStations({
|
|
1079
|
+
* fromStopId: "30475",
|
|
1080
|
+
* toStopId: "35376"
|
|
1081
|
+
* });
|
|
1082
|
+
*
|
|
1083
|
+
* routes.items.forEach(route => {
|
|
1084
|
+
* console.log(`Route ${route.routeNo} starts at ${route.startTime}`);
|
|
1085
|
+
* console.log(`Travel time: ${route.travelTime}, Distance: ${route.distance} km`);
|
|
1086
|
+
* });
|
|
1087
|
+
* ```
|
|
1088
|
+
* @example
|
|
1089
|
+
* ```typescript
|
|
1090
|
+
* // Filter by specific route
|
|
1091
|
+
* const routes = await client.routes.getRoutesThroughStations({
|
|
1092
|
+
* fromStopId: "30475",
|
|
1093
|
+
* toStopId: "35376",
|
|
1094
|
+
* routeId: "2292",
|
|
1095
|
+
* date: new Date("2026-01-20")
|
|
1096
|
+
* });
|
|
1097
|
+
* ```
|
|
1098
|
+
* @remarks
|
|
1099
|
+
* This endpoint returns all routes that pass through both the fromStation and toStation in sequence.
|
|
1100
|
+
* The routes may start before fromStation and/or continue after toStation - they just need to pass through both.
|
|
1101
|
+
* Returns one startTime per route (not a full timetable with multiple trips).
|
|
1102
|
+
* For a full timetable with multiple trips, use getTimetableByRoute() instead.
|
|
1103
|
+
* The date, start time, and end time parameters are required by the API but don't affect the output.
|
|
1104
|
+
* Use routeId to filter results to a specific route.
|
|
1105
|
+
*/
|
|
1106
|
+
async getRoutesThroughStations(params) {
|
|
1107
|
+
// Use provided date or default to current date
|
|
1108
|
+
const date = params.date ?? new Date();
|
|
1109
|
+
const dateStr = formatDate(date);
|
|
1110
|
+
// Build request payload - API expects numbers for IDs, convert from strings
|
|
1111
|
+
const requestPayload = {
|
|
1112
|
+
fromStationId: parseId(params.fromStopId),
|
|
1113
|
+
toStationId: parseId(params.toStopId),
|
|
1114
|
+
p_startdate: `${dateStr} 00:00`,
|
|
1115
|
+
p_enddate: `${dateStr} 23:59`,
|
|
1116
|
+
p_routeid: params.routeId ?? "",
|
|
1117
|
+
p_date: dateStr,
|
|
1118
|
+
};
|
|
1119
|
+
// Validate request payload
|
|
1120
|
+
const validatedParams = validate(timetableByStationRequestSchema, requestPayload, "Invalid timetable by station parameters");
|
|
1121
|
+
const response = await this.client
|
|
1122
|
+
.getClient()
|
|
1123
|
+
.post("GetTimetableByStation_v4", {
|
|
1124
|
+
json: validatedParams,
|
|
1125
|
+
});
|
|
1126
|
+
const data = await response.json();
|
|
1127
|
+
// Validate raw response with Zod schema
|
|
1128
|
+
const rawResponse = validate(rawTimetableByStationResponseSchema, data, "Invalid timetable by station response");
|
|
1129
|
+
// Transform to clean, normalized format
|
|
1130
|
+
return transformTimetableByStationResponse(rawResponse);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
//# sourceMappingURL=routes.js.map
|