1
1
import { Injectable , InternalServerErrorException } from '@nestjs/common' ;
2
2
import { ConfigService } from '@nestjs/config' ;
3
- import axios , { AxiosError , AxiosResponse } from 'axios' ;
3
+ import axios , { AxiosResponse } from 'axios' ;
4
4
import { decode } from 'html-entities' ;
5
5
6
- import { DepartureQueryDto } from '../types/client.types' ;
7
- import { ApiResponse , Departure , FutarAPI } from '../types/departure.types' ;
6
+ import { DepartureQueryDto , MapQueryDto , RouteQueryDto } from '../types/client.types' ;
7
+ import {
8
+ ApiResponse as DepartureApiResponse ,
9
+ Departure ,
10
+ FutarAPI as DeparturesFutarAPI ,
11
+ } from '../types/departure.types' ;
12
+ import {
13
+ ApiResponse as MapApiResponse ,
14
+ DeparturesData ,
15
+ FutarAPI as MapFutarAPI ,
16
+ RoutesData ,
17
+ VehiclesData ,
18
+ } from '../types/map.types' ;
8
19
import { ProxyQueryDto } from '../types/proxy.types' ;
9
20
import { ConfigKeys } from '../utils/configuration' ;
10
21
11
22
@Injectable ( )
12
23
export class ClientService {
13
- private readonly apiUrl : URL ;
24
+ private readonly departuresApiUrl : URL ;
25
+ private readonly vehiclesApiUrl : URL ;
26
+ private readonly routesApiUrl : URL ;
14
27
15
28
constructor ( private configService : ConfigService ) {
16
- this . apiUrl = new URL ( 'https://go.bkk.hu/api/query/v1/ws/otp/api/where/arrivals-and-departures-for-location.json' ) ;
17
- this . apiUrl . searchParams . append ( 'minutesBefore' , '0' ) ;
18
- this . apiUrl . searchParams . append ( 'limit' , '30' ) ;
19
- this . apiUrl . searchParams . append ( 'groupLimit' , '1' ) ;
20
- this . apiUrl . searchParams . append ( 'onlyDepartures' , 'true' ) ;
21
- this . apiUrl . searchParams . append ( 'key' , configService . get < string > ( ConfigKeys . FUTAR_API_KEY ) ) ;
29
+ const apiKey = configService . get < string > ( ConfigKeys . FUTAR_API_KEY ) ;
30
+
31
+ this . departuresApiUrl = new URL (
32
+ 'https://go.bkk.hu/api/query/v1/ws/otp/api/where/arrivals-and-departures-for-location.json'
33
+ ) ;
34
+ this . departuresApiUrl . searchParams . append ( 'minutesBefore' , '0' ) ;
35
+ this . departuresApiUrl . searchParams . append ( 'limit' , '30' ) ;
36
+ this . departuresApiUrl . searchParams . append ( 'groupLimit' , '1' ) ;
37
+ this . departuresApiUrl . searchParams . append ( 'onlyDepartures' , 'true' ) ;
38
+ this . departuresApiUrl . searchParams . append ( 'key' , apiKey ) ;
39
+
40
+ this . vehiclesApiUrl = new URL ( 'https://go.bkk.hu/api/query/v1/ws/otp/api/where/vehicles-for-location.json' ) ;
41
+ this . vehiclesApiUrl . searchParams . append ( 'includeReferences' , 'false' ) ;
42
+ this . vehiclesApiUrl . searchParams . append ( 'key' , apiKey ) ;
43
+
44
+ this . routesApiUrl = new URL ( 'https://go.bkk.hu/api/query/v1/ws/otp/api/where/multi-route-details.json' ) ;
45
+ this . routesApiUrl . searchParams . append ( 'includeReferences' , 'false' ) ;
46
+ this . routesApiUrl . searchParams . append ( 'key' , apiKey ) ;
22
47
}
23
48
24
- getUrl ( { lat, lon, radius } : DepartureQueryDto ) {
25
- const url = new URL ( this . apiUrl ) ;
49
+ getDeparturesUrl ( { lat, lon, radius } : DepartureQueryDto ) : URL {
50
+ const url = new URL ( this . departuresApiUrl . toString ( ) ) ;
26
51
url . searchParams . append ( 'radius' , radius . toString ( ) ) ;
27
52
url . searchParams . append ( 'lon' , lon ) ;
28
53
url . searchParams . append ( 'lat' , lat ) ;
@@ -31,50 +56,126 @@ export class ClientService {
31
56
return url ;
32
57
}
33
58
34
- async getDepartures ( query : DepartureQueryDto ) {
35
- const url = this . getUrl ( query ) . toString ( ) ;
36
- let response : AxiosResponse < FutarAPI > ;
59
+ getVehiclesUrl ( { lat, lon, radius } : MapQueryDto ) : URL {
60
+ const url = new URL ( this . vehiclesApiUrl . toString ( ) ) ;
61
+ url . searchParams . append ( 'radius' , radius . toString ( ) ) ;
62
+ url . searchParams . append ( 'lon' , lon ) ;
63
+ url . searchParams . append ( 'lat' , lat ) ;
64
+ return url ;
65
+ }
66
+
67
+ getRoutesUrl ( { routeId } : RouteQueryDto ) : URL {
68
+ const url = new URL ( this . routesApiUrl . toString ( ) ) ;
69
+ routeId . forEach ( ( id ) => url . searchParams . append ( 'routeId' , id ) ) ;
70
+ return url ;
71
+ }
72
+
73
+ async getDepartures ( query : DepartureQueryDto ) : Promise < DepartureApiResponse > {
74
+ const url = this . getDeparturesUrl ( query ) . toString ( ) ;
75
+ let response : AxiosResponse < DeparturesFutarAPI > ;
76
+
37
77
try {
38
- response = await axios . get < FutarAPI > ( url ) ;
39
- } catch ( e ) {
40
- throw new AxiosError ( ) ;
78
+ response = await axios . get < DeparturesFutarAPI > ( url ) ;
79
+ } catch ( error ) {
80
+ throw new InternalServerErrorException ( 'Failed to fetch departures.' ) ;
41
81
}
42
82
43
- const apiResponse : ApiResponse = { departures : [ ] } ;
83
+ const { list, references } = response . data . data ;
84
+ const departures : Departure [ ] = [ ] ;
44
85
45
86
try {
46
- const { list, references } = response . data . data ;
47
-
48
87
list ?. forEach ( ( { stopTimes, headsign, routeId } ) => {
49
88
stopTimes ?. forEach ( ( { departureTime, predictedDepartureTime, alertIds, stopId } ) => {
89
+ const route = references . routes [ routeId ] ;
50
90
const departure : Departure = {
51
- type : references . routes [ routeId ] . type ,
52
- style : references . routes [ routeId ] . style ,
53
- headsign : headsign ,
91
+ type : route . type ,
92
+ style : route . style ,
93
+ headsign,
54
94
scheduled : departureTime ,
55
- predicted : predictedDepartureTime || departureTime ,
56
- alert : alertIds ?. map ( ( id ) =>
57
- decode (
58
- references . alerts [ id ] . description . translations . hu . replace ( / ( \n ) + / g, ' ' ) . replace ( / < \/ ? [ ^ > ] + ( > | $ ) / g, '' )
59
- )
60
- ) ,
61
- isDelayed : ( predictedDepartureTime || departureTime ) - departureTime > 180 ,
95
+ predicted : predictedDepartureTime ?? departureTime ,
96
+ alert : alertIds ?. map ( ( id ) => {
97
+ const rawDescription = references . alerts [ id ] . description . translations . hu ;
98
+ return decode ( rawDescription . replace ( / ( \n ) + / g, ' ' ) . replace ( / < \/ ? [ ^ > ] + ( > | $ ) / g, '' ) ) ;
99
+ } ) ,
100
+ isDelayed : ( predictedDepartureTime ?? departureTime ) - departureTime > 180 ,
62
101
departureText : '' ,
63
- stopId : stopId ,
102
+ stopId,
64
103
} ;
65
- const minutes = Math . floor ( ( departure . predicted * 1000 - Date . now ( ) ) / 60000 ) ;
66
104
105
+ const minutes = Math . floor ( ( departure . predicted * 1000 - Date . now ( ) ) / 60000 ) ;
67
106
departure . departureText = minutes < 1 ? 'azonnal indul' : `${ minutes } perc` ;
68
- apiResponse . departures . push ( departure ) ;
107
+ departures . push ( departure ) ;
108
+ } ) ;
109
+ } ) ;
110
+ } catch ( error ) {
111
+ throw new InternalServerErrorException ( 'Error processing departures data.' ) ;
112
+ }
113
+
114
+ return { departures } ;
115
+ }
116
+
117
+ async getMap ( query : MapQueryDto ) : Promise < MapApiResponse > {
118
+ // Adjust departures API to include data from 30 minutes before
119
+ const departuresUrl = this . getDeparturesUrl ( query ) ;
120
+ departuresUrl . searchParams . set ( 'minutesBefore' , '30' ) ;
121
+ const vehiclesUrl = this . getVehiclesUrl ( query ) . toString ( ) ;
122
+
123
+ let departuresResponse : AxiosResponse < MapFutarAPI < DeparturesData > > ;
124
+ let vehiclesResponse : AxiosResponse < MapFutarAPI < VehiclesData > > ;
125
+ try {
126
+ [ departuresResponse , vehiclesResponse ] = await Promise . all ( [
127
+ axios . get < MapFutarAPI < DeparturesData > > ( departuresUrl . toString ( ) ) ,
128
+ axios . get < MapFutarAPI < VehiclesData > > ( vehiclesUrl ) ,
129
+ ] ) ;
130
+ } catch ( error ) {
131
+ throw new InternalServerErrorException ( 'Failed to fetch map data.' ) ;
132
+ }
133
+
134
+ const mapApiResponse : MapApiResponse = { routes : [ ] , stops : [ ] , vehicles : [ ] } ;
135
+
136
+ try {
137
+ const departuresData = departuresResponse . data . data ;
138
+ const vehiclesData = vehiclesResponse . data . data ;
139
+ const { list : departuresList , references : departuresReferences } = departuresData ;
140
+ const { list : vehiclesList } = vehiclesData ;
141
+
142
+ const routeIds = new Set < string > ( ) ;
143
+ departuresList ?. forEach ( ( { stopTimes, routeId } ) => {
144
+ routeIds . add ( routeId ) ;
145
+ stopTimes ?. forEach ( ( { stopId } ) => {
146
+ const stop = departuresReferences . stops [ stopId ] ;
147
+ if ( stop ) {
148
+ mapApiResponse . stops . push ( stop ) ;
149
+ }
69
150
} ) ;
70
151
} ) ;
71
- } catch ( e ) {
72
- throw new InternalServerErrorException ( ) ;
152
+
153
+ mapApiResponse . vehicles = vehiclesList || [ ] ;
154
+
155
+ const routesUrl = this . getRoutesUrl ( { routeId : Array . from ( routeIds ) } ) . toString ( ) ;
156
+ let routesResponse : AxiosResponse < MapFutarAPI < RoutesData > > ;
157
+ try {
158
+ routesResponse = await axios . get < MapFutarAPI < RoutesData > > ( routesUrl ) ;
159
+ const { list : routesList } = routesResponse . data . data ;
160
+ if ( routesList ) {
161
+ mapApiResponse . routes = routesList ;
162
+ }
163
+ } catch ( error ) {
164
+ throw new InternalServerErrorException ( 'Failed to fetch route details.' ) ;
165
+ }
166
+ } catch ( error ) {
167
+ throw new InternalServerErrorException ( 'Error processing map data.' ) ;
73
168
}
74
- return apiResponse ;
169
+
170
+ return mapApiResponse ;
75
171
}
76
172
77
- async getResource ( proxyQueryDto : ProxyQueryDto ) {
78
- return axios . get ( proxyQueryDto . url ) . then ( ( response ) => response . data ) ;
173
+ async getResource ( proxyQueryDto : ProxyQueryDto ) : Promise < any > {
174
+ try {
175
+ const response = await axios . get ( proxyQueryDto . url ) ;
176
+ return response . data ;
177
+ } catch ( error ) {
178
+ throw new InternalServerErrorException ( 'Failed to fetch resource.' ) ;
179
+ }
79
180
}
80
181
}
0 commit comments