mirror of
https://github.com/brendan-ch/project-inter-server.git
synced 2026-04-17 07:50:31 +00:00
376 lines
11 KiB
TypeScript
376 lines
11 KiB
TypeScript
import { GetterRepository } from "./GetterRepository";
|
|
import { IEta, IOrderedStop, IRoute, IShuttle, IStop, ISystem } from "../entities/entities";
|
|
|
|
const baseUrl = "https://passiogo.com/mapGetData.php"
|
|
|
|
// TODO: implement TTL functionality
|
|
// TODO: add TTL values to everything
|
|
// TODO: remove RepositoryDataLoader and UnoptimizedInMemoryRepository
|
|
// TODO: make milliseconds (TTL) required on everything
|
|
// TODO: extract cache into its own class
|
|
|
|
export interface ApiBasedRepositoryCache {
|
|
etasForShuttleId?: {
|
|
[shuttleId: string]: IEta[],
|
|
},
|
|
etasForStopId?: {
|
|
[stopId: string]: IEta[],
|
|
},
|
|
stopsBySystemId?: {
|
|
[systemId: string]: IStop[],
|
|
},
|
|
stopByStopId?: {
|
|
[stopId: string]: IStop,
|
|
},
|
|
shuttleByShuttleId?: {
|
|
[shuttleId: string]: IShuttle,
|
|
},
|
|
shuttlesBySystemId?: {
|
|
[systemId: string]: IShuttle[],
|
|
},
|
|
// To speed things up, implement caches for other data later
|
|
}
|
|
|
|
export interface ApiBasedRepositoryMillisecondTTLs {
|
|
etasForShuttleId?: number,
|
|
etasForStopId?: number,
|
|
shuttleByShuttleId?: number,
|
|
shuttlesBySystemId?: number,
|
|
}
|
|
|
|
const emptyCache: ApiBasedRepositoryCache = {
|
|
etasForShuttleId: {},
|
|
etasForStopId: {},
|
|
shuttleByShuttleId: {},
|
|
shuttlesBySystemId: {},
|
|
}
|
|
|
|
const defaultTtls: ApiBasedRepositoryMillisecondTTLs = {
|
|
etasForShuttleId: 10000,
|
|
etasForStopId: 10000,
|
|
}
|
|
|
|
export class ApiBasedRepository implements GetterRepository {
|
|
private cache: ApiBasedRepositoryCache;
|
|
|
|
constructor(
|
|
initialCache: ApiBasedRepositoryCache | undefined = emptyCache,
|
|
private ttls: ApiBasedRepositoryMillisecondTTLs = defaultTtls,
|
|
) {
|
|
this.cache = initialCache;
|
|
}
|
|
|
|
/**
|
|
* Seed the initial data in the cache, so data can be updated
|
|
* given the correct ID.
|
|
* Alternatively, pass in an `initialCache` with existing data
|
|
* (useful for testing).
|
|
* @param systemId
|
|
*/
|
|
public async seedCacheForSystemId(systemId: string): Promise<void> {
|
|
await this.getShuttlesBySystemId(systemId);
|
|
await this.getShuttlesByRouteId(systemId);
|
|
await this.getStopsBySystemId(systemId);
|
|
await this.getSystemById(systemId);
|
|
}
|
|
|
|
public async getEtaForShuttleAndStopId(shuttleId: string, stopId: string): Promise<IEta | null> {
|
|
const shuttle = await this.getShuttleById(shuttleId);
|
|
const systemId = shuttle?.systemId;
|
|
if (!systemId) {
|
|
return null;
|
|
}
|
|
await this.updateEtasForSystemIfTTL(systemId);
|
|
|
|
if (this.cache?.etasForStopId && this.cache.etasForStopId[stopId]) {
|
|
const etas = this.cache.etasForStopId[stopId];
|
|
const foundEta = etas.find((eta) => eta.stopId === stopId);
|
|
if (foundEta) {
|
|
return foundEta;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public async getEtasForShuttleId(shuttleId: string): Promise<IEta[]> {
|
|
const shuttle = await this.getShuttleById(shuttleId);
|
|
const systemId = shuttle?.systemId;
|
|
if (!systemId) {
|
|
return [];
|
|
}
|
|
await this.updateEtasForSystemIfTTL(systemId);
|
|
|
|
if (!this.cache?.etasForShuttleId || !this.cache.etasForShuttleId[shuttleId]) {
|
|
return [];
|
|
}
|
|
|
|
return this.cache.etasForShuttleId[shuttleId];
|
|
}
|
|
|
|
public async getEtasForStopId(stopId: string): Promise<IEta[]> {
|
|
const stop = await this.getStopById(stopId);
|
|
const systemId = stop?.systemId;
|
|
if (!systemId) {
|
|
return [];
|
|
}
|
|
await this.updateEtasForSystemIfTTL(systemId);
|
|
|
|
if (!this.cache?.etasForStopId || !this.cache.etasForStopId[stopId]) {
|
|
return [];
|
|
}
|
|
|
|
return this.cache.etasForStopId[stopId];
|
|
}
|
|
|
|
public async updateEtasForSystemIfTTL(systemId: string) {
|
|
// TODO: check if TTL
|
|
try {
|
|
const stops = await this.getStopsBySystemId(systemId);
|
|
await Promise.all(stops.map(async (stop) => {
|
|
const params = {
|
|
eta: "3",
|
|
stopIds: stop.id,
|
|
};
|
|
|
|
const query = new URLSearchParams(params).toString();
|
|
const response = await fetch(`${baseUrl}?${query}`, {
|
|
method: "GET",
|
|
});
|
|
const json = await response.json();
|
|
|
|
if (json.ETAs && json.ETAs[stop.id]) {
|
|
if (!this.cache.etasForStopId) {
|
|
this.cache.etasForStopId = {};
|
|
}
|
|
this.cache.etasForStopId[stop.id] = [];
|
|
|
|
// This is technically incorrect, the entire shuttle cache
|
|
// should not be reset like this
|
|
// TODO: restore normal cache behavior
|
|
this.cache.etasForShuttleId = {};
|
|
|
|
// Continue with the parsing
|
|
json.ETAs[stop.id].forEach((jsonEta: any) => {
|
|
// Update cache
|
|
if (!this.cache.etasForStopId) {
|
|
this.cache.etasForStopId = {};
|
|
}
|
|
|
|
if (!this.cache.etasForShuttleId) {
|
|
this.cache.etasForShuttleId = {};
|
|
}
|
|
|
|
// TODO: create cache abstraction to deal with possibly undefined properties
|
|
|
|
const shuttleId: string = jsonEta.busId;
|
|
if (!this.cache.etasForShuttleId[shuttleId]) {
|
|
this.cache.etasForShuttleId[shuttleId] = [];
|
|
}
|
|
|
|
const eta: IEta = {
|
|
secondsRemaining: jsonEta.secondsSpent,
|
|
shuttleId: `${shuttleId}`,
|
|
stopId: stop.id,
|
|
millisecondsSinceEpoch: Date.now(),
|
|
};
|
|
|
|
this.cache.etasForStopId[stop.id].push(eta);
|
|
this.cache.etasForShuttleId[shuttleId].push(eta);
|
|
});
|
|
}
|
|
}));
|
|
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
|
|
// TODO: migrate rest of logic over to this class
|
|
public async getOrderedStopByRouteAndStopId(routeId: string, stopId: string): Promise<| null> {
|
|
return null;
|
|
}
|
|
|
|
public async getOrderedStopsByRouteId(routeId: string): Promise<[]> {
|
|
return Promise.resolve([]);
|
|
}
|
|
|
|
public async getOrderedStopsByStopId(stopId: string): Promise<[]> {
|
|
return Promise.resolve([]);
|
|
}
|
|
|
|
public async getRouteById(routeId: string): Promise<| null> {
|
|
return Promise.resolve(null);
|
|
}
|
|
|
|
public async getRoutesBySystemId(systemId: string): Promise<[]> {
|
|
return Promise.resolve([]);
|
|
}
|
|
|
|
public async getShuttleById(shuttleId: string): Promise<IShuttle | null> {
|
|
if (!this.cache.shuttleByShuttleId) return null;
|
|
let shuttle = this.cache.shuttleByShuttleId[shuttleId];
|
|
if (!shuttle) return null;
|
|
|
|
// Call getShuttlesBySystemId to update the data if not TTL
|
|
await this.updateShuttlesForSystemIfTTL(shuttle.systemId);
|
|
|
|
shuttle = this.cache.shuttleByShuttleId[shuttleId];
|
|
if (!shuttle) return null;
|
|
return shuttle;
|
|
}
|
|
|
|
public async getShuttlesByRouteId(routeId: string): Promise<[]> {
|
|
return Promise.resolve([]);
|
|
}
|
|
|
|
public async getShuttlesBySystemId(systemId: string): Promise<IShuttle[]> {
|
|
return Promise.resolve([]);
|
|
}
|
|
|
|
public async updateShuttlesForSystemIfTTL(systemId: string) {
|
|
// TODO: check if TTL
|
|
try {
|
|
// TODO: update shuttlesByRouteId
|
|
// Update shuttleByShuttleId, shuttlesBySystemId
|
|
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();
|
|
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];
|
|
});
|
|
|
|
// Store shuttles by system, with the additional side effect that
|
|
// shuttleByShuttleId is updated
|
|
const shuttles = 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: systemId,
|
|
id: `${jsonBus.busId}`
|
|
}
|
|
|
|
if (this.cache.shuttleByShuttleId) {
|
|
this.cache.shuttleByShuttleId[jsonBus.busId] = constructedShuttle;
|
|
}
|
|
|
|
return constructedShuttle;
|
|
}));
|
|
|
|
if (this.cache.shuttlesBySystemId) {
|
|
this.cache.shuttlesBySystemId[systemId] = shuttles;
|
|
}
|
|
} else {
|
|
console.warn(`No shuttle data available for system ID ${systemId} and JSON output
|
|
${json}`);
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
|
|
public async getStopById(stopId: string): Promise<IStop | null> {
|
|
if (!this.cache.stopByStopId) return null;
|
|
const oldStop = this.cache.stopByStopId[stopId];
|
|
if (!oldStop) return null;
|
|
|
|
await this.updateStopsForSystemIdIfTTL(oldStop.systemId);
|
|
|
|
const newStop = this.cache.stopByStopId[stopId];
|
|
if (!newStop) return null;
|
|
|
|
return newStop;
|
|
}
|
|
|
|
public async getStopsBySystemId(systemId: string): Promise<IStop[]> {
|
|
await this.updateStopsForSystemIdIfTTL(systemId);
|
|
|
|
if (!this.cache.stopsBySystemId || this.cache.stopsBySystemId[systemId]) {
|
|
return [];
|
|
}
|
|
return this.cache.stopsBySystemId[systemId];
|
|
}
|
|
|
|
public async updateStopsForSystemIdIfTTL(systemId: string) {
|
|
// TODO: check if TTL
|
|
|
|
try {
|
|
const params = {
|
|
getStops: "2",
|
|
};
|
|
|
|
const formDataJsonObject = {
|
|
"s0": systemId,
|
|
"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();
|
|
|
|
// TODO: update polyline data
|
|
// TODO: update ordered stop data
|
|
|
|
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: systemId,
|
|
coordinates: {
|
|
latitude: parseFloat(stop.latitude),
|
|
longitude: parseFloat(stop.longitude),
|
|
},
|
|
};
|
|
|
|
if (this.cache.stopsBySystemId) {
|
|
this.cache.stopsBySystemId[systemId]?.push(constructedStop);
|
|
}
|
|
|
|
if (this.cache.stopByStopId) {
|
|
this.cache.stopByStopId[constructedStop.id] = constructedStop;
|
|
}
|
|
}));
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
|
|
public async getSystemById(systemId: string): Promise<ISystem| null> {
|
|
return null;
|
|
}
|
|
|
|
public async getSystems(): Promise<[]> {
|
|
return Promise.resolve([]);
|
|
}
|
|
|
|
} |