import { ShuttleGetterSetterRepository } from "../../repositories/ShuttleGetterSetterRepository"; import { IEta, IRoute, IShuttle, IStop } from "../../entities/ShuttleRepositoryEntities"; import { ShuttleRepositoryLoader } from "./ShuttleRepositoryLoader"; import { IEntityWithId } from "../../entities/SharedEntities"; import { ApiResponseError } from "../ApiResponseError"; /** * Class which can load data into a repository from the * Passio Go API. Supports automatic pruning of all data types * which inherit from `IEntityWithId`. */ export class ApiBasedShuttleRepositoryLoader implements ShuttleRepositoryLoader { baseUrl = "https://passiogo.com/mapGetData.php"; constructor( public passioSystemId: string, public systemIdForConstructedData: string, public repository: ShuttleGetterSetterRepository, ) { } private async constructExistingEntityIdSet(entitySearchCallback: () => Promise) { const existingEntities = await entitySearchCallback(); const ids = new Set(); existingEntities.forEach((entity) => { ids.add(entity.id); }); return ids; } public async fetchAndUpdateAll() { await this.fetchAndUpdateRouteDataForSystem(); await this.fetchAndUpdateStopAndPolylineDataForRoutesInSystem(); await this.fetchAndUpdateShuttleDataForSystem(); // Because ETA method doesn't support pruning yet, // add a call to the clear method here await this.repository.clearEtaData(); await this.fetchAndUpdateEtaDataForExistingStopsForSystem(); } public async fetchAndUpdateRouteDataForSystem() { const systemId = this.passioSystemId; const routeIdsToPrune = await this.constructExistingEntityIdSet(async () => { return await this.repository.getRoutes(); }); 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(); try { 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: this.systemIdForConstructedData, updatedTime: new Date(), }; await this.repository.addOrUpdateRoute(constructedRoute); routeIdsToPrune.delete(constructedRoute.id); })) } await Promise.all(Array.from(routeIdsToPrune).map(async (routeId) => { await this.repository.removeRouteIfExists(routeId); })); } catch(e: any) { throw new ApiResponseError(e.message); } } public async fetchAndUpdateStopAndPolylineDataForRoutesInSystem() { const passioSystemId = this.passioSystemId; // Fetch from the API // Pass JSON output into two different methods to update repository const stopIdsToPrune = await this.constructExistingEntityIdSet(async () => { return await this.repository.getStops(); }); const params = { getStops: "2", }; const formDataJsonObject = { "s0": passioSystemId, "sA": 1 }; const formData = new FormData(); formData.set("json", JSON.stringify(formDataJsonObject)); const query = new URLSearchParams(params).toString(); try { const response = await fetch(`${this.baseUrl}?${query}`, { method: "POST", body: formData, }); const json = await response.json(); await this.updateStopDataForSystemAndApiResponse(json, stopIdsToPrune); await this.updateOrderedStopDataForExistingStops(json); await this.updatePolylineDataForExistingRoutesAndApiResponse(json); await Promise.all(Array.from(stopIdsToPrune).map(async (stopId) => { await this.repository.removeStopIfExists(stopId); })); } catch(e: any) { throw new ApiResponseError(e.message); } } public async fetchAndUpdateShuttleDataForSystem() { const systemId = this.passioSystemId; const shuttleIdsToPrune = await this.constructExistingEntityIdSet(async () => { return await this.repository.getShuttles(); }); const params = { getBuses: "2" }; const formDataJsonObject = { "s0": systemId, "sA": "1" }; const formData = new FormData(); formData.set("json", JSON.stringify(formDataJsonObject)); const query = new URLSearchParams(params).toString(); try { 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: this.systemIdForConstructedData, id: `${jsonBus.busId}`, orientationInDegrees: parseFloat(jsonBus.calculatedCourse), updatedTime: new Date(), } await this.repository.addOrUpdateShuttle(constructedShuttle); shuttleIdsToPrune.delete(constructedShuttle.id); })); } await Promise.all(Array.from(shuttleIdsToPrune).map(async (shuttleId) => { await this.repository.removeShuttleIfExists(shuttleId); })); } catch(e: any) { throw new ApiResponseError(e.message); } } public async fetchAndUpdateEtaDataForExistingStopsForSystem() { const stops = await this.repository.getStops(); await Promise.all(stops.map(async (stop) => { let stopId = stop.id; await this.fetchAndUpdateEtaDataForStopId(stopId); })); } public async fetchAndUpdateEtaDataForStopId(stopId: string) { const params = { eta: "3", stopIds: stopId, }; const query = new URLSearchParams(params).toString(); try { const response = await fetch(`${this.baseUrl}?${query}`, { method: "GET", }); const json = await response.json(); if (json.ETAs && json.ETAs[stopId]) { // Continue with the parsing json.ETAs[stopId].forEach((jsonEta: any) => { // Update cache const shuttleId: string = jsonEta.busId; const eta: IEta = { secondsRemaining: jsonEta.secondsSpent, shuttleId: `${shuttleId}`, stopId: stopId, updatedTime: new Date(), systemId: this.systemIdForConstructedData, }; this.repository.addOrUpdateEta(eta); }); } } catch(e: any) { throw new ApiResponseError(e.message); } } protected async updateStopDataForSystemAndApiResponse( json: any, setOfIdsToPrune: Set = new Set(), ) { 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: this.systemIdForConstructedData, coordinates: { latitude: parseFloat(stop.latitude), longitude: parseFloat(stop.longitude), }, updatedTime: new Date(), }; await this.repository.addOrUpdateStop(constructedStop); setOfIdsToPrune.delete(constructedStop.id); })); } } 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, systemId: this.systemIdForConstructedData, updatedTime: new Date(), }; } if (index >= 1) { constructedOrderedStop.previousStop = { routeId, stopId: jsonOrderedStopData[index - 1][1], position: index, systemId: this.systemIdForConstructedData, updatedTime: new Date(), }; } if (index < jsonOrderedStopData.length - 1) { constructedOrderedStop.nextStop = { routeId, stopId: jsonOrderedStopData[index + 1][1], position: index + 2, systemId: this.systemIdForConstructedData, updatedTime: new Date(), }; } 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); })) } } }