import { GetterSetterRepository } from "../repositories/GetterSetterRepository"; import { IEta, IRoute, IShuttle, IStop, ISystem } from "../entities/entities"; export class ApiResponseError extends Error { constructor(message: string) { super(message); this.name = "ApiResponseError"; } } export class ApiBasedRepositoryLoader { supportedSystemIds = ["263"]; baseUrl = "https://passiogo.com/mapGetData.php"; constructor( public repository: GetterSetterRepository, ) { } public async fetchAndUpdateSystemData() { const params = { getSystems: "2", }; const query = new URLSearchParams(params).toString(); try { const response = await fetch(`${this.baseUrl}?${query}`); const json = await response.json(); if (!response.ok) { throw new Error(`HTTP error with status ${response.status}`) } if (typeof json.all === "object") { // filter down to supported systems const filteredSystems = json.all.filter((jsonSystem: any) => this.supportedSystemIds.includes(jsonSystem.id)); await Promise.all(filteredSystems.map(async (system: any) => { const constructedSystem: ISystem = { id: system.id, name: system.fullname, }; await this.repository.addOrUpdateSystem(constructedSystem); })); } else { throw new Error("Received JSON object does not contain `all` field") } } catch(e: any) { console.error("fetchAndUpdateSystemData call failed: ", e); throw new ApiResponseError(e.message); } } public async fetchAndUpdateRouteDataForExistingSystemsInRepository() { const systems = await this.repository.getSystems(); await Promise.all(systems.map(async (system) => { await this.fetchAndUpdateRouteDataForSystemId(system.id); })); } public async fetchAndUpdateRouteDataForSystemId(systemId: string) { const params = { getRoutes: "2", }; const formDataJsonObject = { "systemSelected0": systemId, "amount": "1", } const formData = new FormData(); formData.set("json", JSON.stringify(formDataJsonObject)); const query = new URLSearchParams(params).toString(); const response = await fetch(`${this.baseUrl}?${query}`, { method: "POST", body: formData, }); const json = await response.json(); if (typeof json.all === "object") { await Promise.all(json.all.map(async (jsonRoute: any) => { const constructedRoute: IRoute = { name: jsonRoute.name, color: jsonRoute.color, id: jsonRoute.myid, polylineCoordinates: [], systemId: systemId, }; await this.repository.addOrUpdateRoute(constructedRoute); })) } } public async fetchAndUpdateStopAndPolylineDataForRoutesInExistingSystems() { // Fetch from the API // Pass JSON output into two different methods to update repository const systems = await this.repository.getSystems(); await Promise.all(systems.map(async (system: ISystem) => { const params = { getStops: "2", }; const formDataJsonObject = { "s0": system.id, "sA": 1 }; const formData = new FormData(); formData.set("json", JSON.stringify(formDataJsonObject)); const query = new URLSearchParams(params).toString(); const response = await fetch(`${this.baseUrl}?${query}`, { method: "POST", body: formData, }); const json = await response.json(); await this.updateStopDataForSystemAndApiResponse(system, json); await this.updateOrderedStopDataForExistingStops(json); await this.updatePolylineDataForExistingRoutesAndApiResponse(json); })); } public async fetchAndUpdateShuttleDataForExistingSystems() { const systems = await this.repository.getSystems(); await Promise.all(systems.map(async (system: ISystem) => { const params = { getBuses: "2" }; const formDataJsonObject = { "s0": system.id, "sA": "1" }; const formData = new FormData(); formData.set("json", JSON.stringify(formDataJsonObject)); const query = new URLSearchParams(params).toString(); const response = await fetch(`${this.baseUrl}?${query}`, { method: "POST", body: formData, }); const json = await response.json(); if (json.buses && json.buses["-1"] === undefined) { const jsonBuses = Object.values(json.buses).map((busesArr: any) => { return busesArr[0]; }); await Promise.all(jsonBuses.map(async (jsonBus: any) => { const constructedShuttle: IShuttle = { name: jsonBus.bus, coordinates: { latitude: parseFloat(jsonBus.latitude), longitude: parseFloat(jsonBus.longitude), }, routeId: jsonBus.routeId, systemId: system.id, id: `${jsonBus.busId}` } await this.repository.addOrUpdateShuttle(constructedShuttle); })) } })); } public async fetchAndUpdateEtaDataForExistingOrderedStops() { const systems = await this.repository.getSystems() await Promise.all(systems.map(async (system: ISystem) => { const stops = await this.repository.getStopsBySystemId(system.id); await Promise.all(stops.map(async (stop) => { const params = { eta: "3", stopIds: stop.id, }; const query = new URLSearchParams(params).toString(); const response = await fetch(`${this.baseUrl}?${query}`, { method: "GET", }); const json = await response.json(); if (json.ETAs && json.ETAs[stop.id]) { // Continue with the parsing json.ETAs[stop.id].forEach((jsonEta: any) => { // Update cache const shuttleId: string = jsonEta.busId; const eta: IEta = { secondsRemaining: jsonEta.secondsSpent, shuttleId: `${shuttleId}`, stopId: stop.id, millisecondsSinceEpoch: Date.now(), }; this.repository.addOrUpdateEta(eta); }); } })); })) } protected async updateStopDataForSystemAndApiResponse(system: ISystem, json: any) { if (json.stops) { const jsonStops = Object.values(json.stops); await Promise.all(jsonStops.map(async (stop: any) => { const constructedStop: IStop = { name: stop.name, id: stop.id, systemId: system.id, coordinates: { latitude: parseFloat(stop.latitude), longitude: parseFloat(stop.longitude), }, }; await this.repository.addOrUpdateStop(constructedStop); })); } } protected async updateOrderedStopDataForExistingStops(json: any) { if (json.routes) { await Promise.all(Object.keys(json.routes).map(async (routeId) => { const jsonOrderedStopData: any[][] = json.routes[routeId].slice(2); // The API may return the same stop twice to indicate that // the route runs in a loop // If the API does not do this, assume the route is one-way // To account for this, check if ordered stop already exists in repo // If it does, write to that stop instead for (let index = 0; index < jsonOrderedStopData.length; index++) { const orderedStopDataArray = jsonOrderedStopData[index]; const stopId = orderedStopDataArray[1]; let constructedOrderedStop = await this.repository.getOrderedStopByRouteAndStopId(routeId, stopId) if (constructedOrderedStop === null) { constructedOrderedStop = { routeId, stopId, position: index + 1, }; } if (index >= 1) { constructedOrderedStop.previousStop = { routeId, stopId: jsonOrderedStopData[index - 1][1], position: index, }; } if (index < jsonOrderedStopData.length - 1) { constructedOrderedStop.nextStop = { routeId, stopId: jsonOrderedStopData[index + 1][1], position: index + 2, }; } await this.repository.addOrUpdateOrderedStop(constructedOrderedStop); } })); } } protected async updatePolylineDataForExistingRoutesAndApiResponse(json: any) { if (json.routePoints) { await Promise.all(Object.keys(json.routePoints).map(async (routeId) => { const routePoints = json.routePoints[routeId][0]; const existingRoute = await this.repository.getRouteById(routeId); if (!existingRoute) return; existingRoute.polylineCoordinates = routePoints.map((point: any) => { return { latitude: parseFloat(point.lat), longitude: parseFloat(point.lng), }; }); await this.repository.addOrUpdateRoute(existingRoute); })) } } }