Skip to content

Commit 2d8d815

Browse files
authored
Merge pull request #19 from kir-dev/feature/custom-map-v2
2 parents b5c146d + 4fc8a91 commit 2d8d815

File tree

18 files changed

+2122
-1243
lines changed

18 files changed

+2122
-1243
lines changed

packages/admin/src/types/kiosk.types.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -116,10 +116,8 @@ export const WidgetConfigFields: Record<WidgetName, WidgetConfigField[]> = {
116116
],
117117
weathercam: [],
118118
map: [
119-
{ name: 'radius', type: 'number', label: 'Hatósugár' },
120119
{ name: 'zoom', type: 'number', label: 'Nagyítás' },
121-
{ name: 'xOffset', type: 'number', label: 'X eltolás' },
122-
{ name: 'yOffset', type: 'number', label: 'Y eltolás' },
120+
{ name: 'radius', type: 'number', label: 'Hatósugár' },
123121
],
124122
cmschEvents: [{ name: 'baseUrl', type: 'text', label: 'Szerver Hoszt' }],
125123
};
@@ -170,10 +168,8 @@ export interface WeatherCamConfig extends WidgetConfigBase {
170168
}
171169
export interface MapConfig extends WidgetConfigBase {
172170
name: 'map';
173-
radius: number;
174171
zoom: number;
175-
yOffset: number;
176-
xOffset: number;
172+
radius: number;
177173
}
178174

179175
export interface CMSchEventsConfig extends WidgetConfigBase {

packages/backend/src/client/client.controller.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Body, Controller, Get, NotFoundException, Param, Post } from '@nestjs/c
22

33
import { KioskService } from '../kiosk/kiosk.service';
44
import { MessageService } from '../message/message.service';
5-
import { DepartureQueryDto } from '../types/client.types';
5+
import { DepartureQueryDto, MapQueryDto } from '../types/client.types';
66
import { ProxyQueryDto } from '../types/proxy.types';
77
import { sanitizeArray } from '../utils/sanitize';
88
import { ClientService } from './client.service';
@@ -39,6 +39,11 @@ export class ClientController {
3939
return this.clientService.getDepartures(departureQuery);
4040
}
4141

42+
@Post('map')
43+
async getMap(@Body() mapQuery: MapQueryDto) {
44+
return this.clientService.getMap(mapQuery);
45+
}
46+
4247
@Post('proxy')
4348
async getResource(@Body() proxyQueryDto: ProxyQueryDto) {
4449
return this.clientService.getResource(proxyQueryDto);
Lines changed: 140 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,53 @@
11
import { Injectable, InternalServerErrorException } from '@nestjs/common';
22
import { ConfigService } from '@nestjs/config';
3-
import axios, { AxiosError, AxiosResponse } from 'axios';
3+
import axios, { AxiosResponse } from 'axios';
44
import { decode } from 'html-entities';
55

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';
819
import { ProxyQueryDto } from '../types/proxy.types';
920
import { ConfigKeys } from '../utils/configuration';
1021

1122
@Injectable()
1223
export class ClientService {
13-
private readonly apiUrl: URL;
24+
private readonly departuresApiUrl: URL;
25+
private readonly vehiclesApiUrl: URL;
26+
private readonly routesApiUrl: URL;
1427

1528
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);
2247
}
2348

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());
2651
url.searchParams.append('radius', radius.toString());
2752
url.searchParams.append('lon', lon);
2853
url.searchParams.append('lat', lat);
@@ -31,50 +56,126 @@ export class ClientService {
3156
return url;
3257
}
3358

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+
3777
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.');
4181
}
4282

43-
const apiResponse: ApiResponse = { departures: [] };
83+
const { list, references } = response.data.data;
84+
const departures: Departure[] = [];
4485

4586
try {
46-
const { list, references } = response.data.data;
47-
4887
list?.forEach(({ stopTimes, headsign, routeId }) => {
4988
stopTimes?.forEach(({ departureTime, predictedDepartureTime, alertIds, stopId }) => {
89+
const route = references.routes[routeId];
5090
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,
5494
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,
62101
departureText: '',
63-
stopId: stopId,
102+
stopId,
64103
};
65-
const minutes = Math.floor((departure.predicted * 1000 - Date.now()) / 60000);
66104

105+
const minutes = Math.floor((departure.predicted * 1000 - Date.now()) / 60000);
67106
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+
}
69150
});
70151
});
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.');
73168
}
74-
return apiResponse;
169+
170+
return mapApiResponse;
75171
}
76172

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+
}
79180
}
80181
}

packages/backend/src/types/client.types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,13 @@ export type DepartureQueryDto = {
33
lon: string;
44
radius: number;
55
};
6+
7+
export type MapQueryDto = {
8+
lat: string;
9+
lon: string;
10+
radius: number;
11+
};
12+
13+
export type RouteQueryDto = {
14+
routeId: string[];
15+
};

packages/backend/src/types/kiosk.types.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,10 +112,8 @@ export interface BikeConfig extends WidgetConfigBase {
112112

113113
export interface MapConfig extends WidgetConfigBase {
114114
name: 'map';
115-
radius: number;
116115
zoom: number;
117-
yOffset: number;
118-
xOffset: number;
116+
radius: number;
119117
}
120118

121119
export interface CMSchEventsConfig extends WidgetConfigBase {

0 commit comments

Comments
 (0)