import BratislavaOpendata from "./BratislavaOpendata.js"; import config from "../config.js"; import {bearing, removeMilis} from "./math.js"; import ImhdClient from "./ImhdClient.js"; import Recorder from "./Recorder.js"; import getDatabase from "./getDatabase.js"; export default class MhdMapApp { constructor(websockets) { this.opendata = new BratislavaOpendata(config.openDataKey); this.websockets = websockets; this.stops = null; this.oldVehicles = null; this.vehicles = null; this.stopCache = {}; this.lastUpdatedStatic = null; this.lastUpdatedDynamic = null; this.lastDelta = null; this.imhd = new ImhdClient(); this.recorder = new Recorder(getDatabase()); } async start() { await this.recorder.start(); this.websockets.on('join', ws => { this.sendAction(ws, 'updateStops', this.stops); this.sendAction(ws, 'updateVehicles', this.vehicles); }); this.websockets.on('message', this._onWsMessage.bind(this)); setInterval(() => { this.updateStatic(); }, 1000 * 60 * 60 * 24); await this.updateStatic(); await this.updateDynamic(); setInterval(async () => { await this.updateDynamic(); if (this.lastDelta.length > 0) { this.broadcastAction('updateVehicles', this.lastDelta); } }, 1000 * 5); } async updateStatic() { let tmp = await this.opendata.fetchAllStops(); if (tmp) { this.stops = tmp; this.lastUpdatedStatic = new Date(); } } async updateDynamic() { let tmp = await this.opendata.fetchAllVehicles(); if (!tmp) return; // TODO: We're in Slovakia. tmp = tmp.filter(el => el.gpsLongitude > 10); tmp = this._vehiclesToObj(tmp); this.oldVehicles = this.vehicles; this.vehicles = tmp; this.lastUpdatedDynamic = new Date(); if (this.oldVehicles) { this.lastDelta = this.calculateVehiclesDelta(this.oldVehicles, this.vehicles); this.vehicles = this.addRotationToVehicles(this.oldVehicles, this.vehicles); for (let veh of Object.values(this.lastDelta)) { await this.recorder.putVehicle(veh); } } else { for (let veh of Object.values(this.vehicles)) { await this.recorder.putVehicle(veh); } } this.vehicles = this.addImhdDataToVehicles(this.vehicles); let down = 0; for (let vehId in this.vehicles) { if (await this.imhd.downloadVehicleIcon(vehId) === true) { if (down++ === 100) { break; } } } } async getStopInfo(stationStopId) { let cached = this.stopCache[stationStopId] || null; if (cached && cached.t.getTime() > (Date.now() - 15 * 1000)) { // cache for 15 seconds return cached; } const data = await this.opendata.fetchOneStop(stationStopId); if (!data) return cached; // null or stale return (this.stopCache[stationStopId] = { t: new Date(), data: data, }); } calculateVehiclesDelta(beforeObj, afterObj) { let delta = {}; for (let vehId in beforeObj) { let beforeVeh = beforeObj[vehId]; if (afterObj.hasOwnProperty(vehId)) { let afterVeh = afterObj[vehId]; if (!this.isVehicleObjectEqual(beforeVeh, afterVeh)) { delta[vehId] = afterVeh; } } else { // removed vehicle beforeVeh.gpsLatitude = -1000; beforeVeh.gpsLongitude = -1000; delta[vehId] = beforeVeh; console.log('vehicle left', beforeVeh); } } for (let vehId in afterObj) { if (!beforeObj.hasOwnProperty(vehId)) { // new vehicle delta[vehId] = afterObj[vehId]; // console.log('new vehicle joined', afterObj[vehId]); } } return delta; } _vehiclesToObj(arr) { let obj = {}; for (let vehicle of arr) { let id = this._vehicleToId(vehicle); vehicle.lastModified = removeMilis(new Date(vehicle.lastModified)); if (obj.hasOwnProperty(id)) { console.log('DUPLICATE vehicle?!', 'old:', obj[id], 'new:', vehicle); } obj[id] = vehicle; } return obj; } _vehicleToId(vehicle) { return vehicle.vehicleNumber; } isVehicleObjectEqual(a, b, checkLastModified = false) { return ( a.vehicleNumber === b.vehicleNumber && a.lineNumber === b.lineNumber && a.gpsLatitude === b.gpsLatitude && a.gpsLongitude === b.gpsLongitude && (!checkLastModified || (a.lastModified === b.lastModified)) ); } isVehiclePositionEqual(a, b) { return a.gpsLatitude === b.gpsLatitude && a.gpsLongitude === b.gpsLongitude; } addImhdDataToVehicles(vehicles) { for (let vehId in vehicles) { const data = this.imhd.getVehicleData(vehId); if (!data) continue; let vehicle = vehicles[vehId]; vehicle.img = data.img; vehicle.type = data.type; vehicle.imhdinfo = data.info; } return vehicles; } addRotationToVehicles(before, after) { for (let vehId in after) { if (before.hasOwnProperty(vehId)) { after[vehId].bearing = bearing(before[vehId], after[vehId]); } } return after; } broadcastAction(action, data) { this.websockets.broadcast(JSON.stringify({ action, data })); } sendAction(ws, action, data) { ws.send(JSON.stringify({ action, data })); } async _onWsMessage(ws, data) { // Note: before sending the response, it should be checked if this ws is still open try { data = JSON.parse(data); } catch (e) { console.log('Malformed request: ', data, e); return null; } const msgid = data.id; let res = await this.handleWsRequest(ws, data.action, data.msg); if (ws.readyState !== 1) { // 1 = WebSocket.OPEN // closed while handling the request return; } this.sendAction(ws, 'response', { id: msgid, msg: res, }); } async handleWsRequest(ws, action, data) { if (action === 'requestVehicleTrace') { return await this.recorder.getVehicleTrace(data.vehicle, data.count || 20); } else if (action === 'requestLineTrace') { return await this.recorder.getSimpleLineTrace(data.line, data.count || 20); } else if (action === 'requestStopInfo') { return await this.getStopInfo(data.stationStopId); } return null; } }