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.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +507 -0
  3. package/dist/api/info.d.ts +77 -0
  4. package/dist/api/info.d.ts.map +1 -0
  5. package/dist/api/info.js +197 -0
  6. package/dist/api/info.js.map +1 -0
  7. package/dist/api/locations.d.ts +26 -0
  8. package/dist/api/locations.d.ts.map +1 -0
  9. package/dist/api/locations.js +57 -0
  10. package/dist/api/locations.js.map +1 -0
  11. package/dist/api/routes.d.ts +341 -0
  12. package/dist/api/routes.d.ts.map +1 -0
  13. package/dist/api/routes.js +1133 -0
  14. package/dist/api/routes.js.map +1 -0
  15. package/dist/api/stops.d.ts +92 -0
  16. package/dist/api/stops.d.ts.map +1 -0
  17. package/dist/api/stops.js +237 -0
  18. package/dist/api/stops.js.map +1 -0
  19. package/dist/api/vehicles.d.ts +49 -0
  20. package/dist/api/vehicles.d.ts.map +1 -0
  21. package/dist/api/vehicles.js +154 -0
  22. package/dist/api/vehicles.js.map +1 -0
  23. package/dist/client/base-client.d.ts +52 -0
  24. package/dist/client/base-client.d.ts.map +1 -0
  25. package/dist/client/base-client.js +76 -0
  26. package/dist/client/base-client.js.map +1 -0
  27. package/dist/client/transit-client.d.ts +91 -0
  28. package/dist/client/transit-client.d.ts.map +1 -0
  29. package/dist/client/transit-client.js +98 -0
  30. package/dist/client/transit-client.js.map +1 -0
  31. package/dist/constants/api.d.ts +16 -0
  32. package/dist/constants/api.d.ts.map +1 -0
  33. package/dist/constants/api.js +16 -0
  34. package/dist/constants/api.js.map +1 -0
  35. package/dist/constants/routes.d.ts +16 -0
  36. package/dist/constants/routes.d.ts.map +1 -0
  37. package/dist/constants/routes.js +16 -0
  38. package/dist/constants/routes.js.map +1 -0
  39. package/dist/index.d.ts +11 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +10 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/schemas/common.d.ts +34 -0
  44. package/dist/schemas/common.d.ts.map +1 -0
  45. package/dist/schemas/common.js +20 -0
  46. package/dist/schemas/common.js.map +1 -0
  47. package/dist/schemas/index.d.ts +7 -0
  48. package/dist/schemas/index.d.ts.map +1 -0
  49. package/dist/schemas/index.js +7 -0
  50. package/dist/schemas/index.js.map +1 -0
  51. package/dist/schemas/info.d.ts +390 -0
  52. package/dist/schemas/info.d.ts.map +1 -0
  53. package/dist/schemas/info.js +110 -0
  54. package/dist/schemas/info.js.map +1 -0
  55. package/dist/schemas/locations.d.ts +84 -0
  56. package/dist/schemas/locations.d.ts.map +1 -0
  57. package/dist/schemas/locations.js +31 -0
  58. package/dist/schemas/locations.js.map +1 -0
  59. package/dist/schemas/routes.d.ts +3967 -0
  60. package/dist/schemas/routes.d.ts.map +1 -0
  61. package/dist/schemas/routes.js +532 -0
  62. package/dist/schemas/routes.js.map +1 -0
  63. package/dist/schemas/stops.d.ts +543 -0
  64. package/dist/schemas/stops.d.ts.map +1 -0
  65. package/dist/schemas/stops.js +129 -0
  66. package/dist/schemas/stops.js.map +1 -0
  67. package/dist/schemas/vehicles.d.ts +602 -0
  68. package/dist/schemas/vehicles.d.ts.map +1 -0
  69. package/dist/schemas/vehicles.js +116 -0
  70. package/dist/schemas/vehicles.js.map +1 -0
  71. package/dist/types/api.d.ts +9 -0
  72. package/dist/types/api.d.ts.map +1 -0
  73. package/dist/types/api.js +5 -0
  74. package/dist/types/api.js.map +1 -0
  75. package/dist/types/coordinates.d.ts +7 -0
  76. package/dist/types/coordinates.d.ts.map +1 -0
  77. package/dist/types/coordinates.js +2 -0
  78. package/dist/types/coordinates.js.map +1 -0
  79. package/dist/types/geojson.d.ts +84 -0
  80. package/dist/types/geojson.d.ts.map +1 -0
  81. package/dist/types/geojson.js +2 -0
  82. package/dist/types/geojson.js.map +1 -0
  83. package/dist/types/index.d.ts +16 -0
  84. package/dist/types/index.d.ts.map +1 -0
  85. package/dist/types/index.js +12 -0
  86. package/dist/types/index.js.map +1 -0
  87. package/dist/types/info.d.ts +133 -0
  88. package/dist/types/info.d.ts.map +1 -0
  89. package/dist/types/info.js +5 -0
  90. package/dist/types/info.js.map +1 -0
  91. package/dist/types/locations.d.ts +59 -0
  92. package/dist/types/locations.d.ts.map +1 -0
  93. package/dist/types/locations.js +5 -0
  94. package/dist/types/locations.js.map +1 -0
  95. package/dist/types/routes.d.ts +1137 -0
  96. package/dist/types/routes.d.ts.map +1 -0
  97. package/dist/types/routes.js +14 -0
  98. package/dist/types/routes.js.map +1 -0
  99. package/dist/types/stops.d.ts +286 -0
  100. package/dist/types/stops.d.ts.map +1 -0
  101. package/dist/types/stops.js +26 -0
  102. package/dist/types/stops.js.map +1 -0
  103. package/dist/types/vehicles.d.ts +138 -0
  104. package/dist/types/vehicles.d.ts.map +1 -0
  105. package/dist/types/vehicles.js +5 -0
  106. package/dist/types/vehicles.js.map +1 -0
  107. package/dist/utils/date.d.ts +35 -0
  108. package/dist/utils/date.d.ts.map +1 -0
  109. package/dist/utils/date.js +49 -0
  110. package/dist/utils/date.js.map +1 -0
  111. package/dist/utils/errors.d.ts +34 -0
  112. package/dist/utils/errors.d.ts.map +1 -0
  113. package/dist/utils/errors.js +41 -0
  114. package/dist/utils/errors.js.map +1 -0
  115. package/dist/utils/geojson.d.ts +36 -0
  116. package/dist/utils/geojson.d.ts.map +1 -0
  117. package/dist/utils/geojson.js +115 -0
  118. package/dist/utils/geojson.js.map +1 -0
  119. package/dist/utils/validation.d.ts +40 -0
  120. package/dist/utils/validation.d.ts.map +1 -0
  121. package/dist/utils/validation.js +62 -0
  122. package/dist/utils/validation.js.map +1 -0
  123. 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