import { GetterSetterRepository } from "../repositories/GetterSetterRepository"; import { IRoute, IShuttle, IStop, ISystem } from "../entities/entities"; const timeout = 60000; const systemIdsToSupport = ["263"]; const baseUrl = "https://passiogo.com/mapGetData.php"; // Ideas to break this into smaller pieces in the future: // Have one repository data loader running for each supported system // Each data loader independently updates data based on frequency of usage // TODO implement reloading of data // Notes on this: we only need to reload ETA data frequently // Other data can be reloaded periodically // Detailed list: // - ETA: reload frequently or switch to write-through approach // - Shuttles: reload every minute // - Routes: reload every few minutes // - Stops: reload every few minutes // - OrderedStops: reload every few minutes // - Systems: reload once a day export class RepositoryDataLoader { private shouldBeRunning: boolean = false; constructor( private repository: GetterSetterRepository, ) {} public async start() { if (this.shouldBeRunning) { console.warn("DataLoader timer is already running"); return; } this.shouldBeRunning = true; await this.startFetchDataAndUpdate(); } public stop() { this.shouldBeRunning = false; } private async startFetchDataAndUpdate() { if (!this.shouldBeRunning) return; try { await this.fetchAndUpdateSystemData(); await this.fetchAndUpdateRouteDataForExistingSystems(); await this.fetchAndUpdateStopAndPolylineDataForRoutesInExistingSystems(); await this.fetchAndUpdateShuttleDataForExistingSystems(); await this.fetchAndUpdateEtaDataForExistingOrderedStops(); } catch (e) { console.error(e); } finally { setTimeout(this.startFetchDataAndUpdate, timeout); } } private async fetchAndUpdateSystemData() { const params = { getSystems: "2", }; const query = new URLSearchParams(params).toString(); const response = await fetch(`${baseUrl}?${query}`); const json = await response.json() if (typeof json.all === "object") { // filter down to supported systems const filteredSystems = json.all.filter((jsonSystem: any) => systemIdsToSupport.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); })); } } private async fetchAndUpdateRouteDataForExistingSystems() { const systems = await this.repository.getSystems(); await Promise.all(systems.map(async (system) => { const params = { getRoutes: "2", }; const formDataJsonObject = { "systemSelected0": system.id, "amount": "1", } const formData = new FormData(); formData.set("json", JSON.stringify(formDataJsonObject)); const query = new URLSearchParams(params).toString(); const response = await fetch(`${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: system.id, }; await this.repository.addOrUpdateRoute(constructedRoute); })) } })); } private 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(`${baseUrl}?${query}`, { method: "POST", body: formData, }); const json = await response.json(); await this.updateStopDataForSystemAndApiResponse(system, json); await this.updateOrderedStopDataForExistingStops(json); await this.updatePolylineDataForExistingRoutesAndApiResponse(json); })); } private 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(`${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); })) } })); } private async fetchAndUpdateEtaDataForExistingOrderedStops() { // TODO implement once I figure out how to associate ETA data with shuttles // 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: IStop) => { // const orderedStops = await this.repository.getOrderedStopsByStopId(stop.id); // // await Promise.all(orderedStops.map(async (orderedStop) => { // const params = { // eta: "3", // stopIds: stop.id, // routeId: orderedStop.routeId, // position: orderedStop.position, // }; // // // How to get shuttle ID????????? // // API doesn't provide it // // I might be cooked // })); // })); // })); } private 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); })); } } private 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); } })); } } private 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); })) } } }