/** * @fileoverview * * Fetch and aggregate information about vehicles in transit on the TaM network * from the official endpoints. */ import * as tam from "./sources/tam.js"; import * as routing from "./routing.js"; import network from "./network.json"; /** * Information about the course of a vehicle. * @typedef {Object} Course * @property {string} id Unique identifier for this course. * @property {string} line Transport line number. * @property {string} finalStop Final stop to which the course is headed. * @property {Array.} passings Next stations to which * the vehicle will stop, associated to the passing timestamp, ordered by * increasing passing timestamp. */ /** Parse time information relative to the current date. */ const parseTime = (time, reference) => { const [hours, minutes, seconds] = time.split(':').map(x => parseInt(x, 10)); const result = new Date(reference); result.setHours(hours); result.setMinutes(minutes); result.setSeconds(seconds); if (reference > result.getTime()) { // Timestamps in the past refer to the next day result.setDate(result.getDate() + 1); } return result; }; /** List of OSM nodes that are stops. */ const stopsSet = new Set( Object.values(network.stops).map(stop => stop.properties.node) ); /** Guess whether a stop comes directly before another one. */ const isPenultimateStop = (stop, finalStop) => { // If there is a standard segment linking both stops, it’s certainly // the penultimate stop if ((stop + "-" + finalStop) in network.segments) { return true; } const route = routing.findPath(stop, finalStop); // If there is no way to link both stops, it can’t be if (route === null) { return false; } // If there is another stop in the way, it can’t be either for (const nodeId of route.slice(1, -1)) { if (stopsSet.has(nodeId)) { return false; } } // Otherwise, assume it’s the penultimate stop return true; }; /** * Fetch information about courses in the TaM network. * * @async * @param {string} kind Pass 'realtime' to get real-time information, * or 'theoretical' to get planned courses for the day. * @returns {Object.} Mapping from active course IDs to * information about each course. */ export const fetch = async (kind = 'realtime') => { const courses = {}; const passings = ( kind === 'realtime' ? tam.fetchRealtime() : tam.fetchTheoretical() ); const timing = (await passings.next()).value; // Aggregate passings relative to the same course for await (const passing of passings) { const { course: id, routeShortName: line, stopId, destArCode: finalStopId, } = passing; const direction = ( 'direction' in passing ? passing.direction : passing.directionId ); const departureTime = ( 'delaySec' in passing ? timing.lastUpdate + parseInt(passing.delaySec, 10) * 1000 : parseTime(passing.departureTime, timing.lastUpdate) ); if (!(id in courses)) { courses[id] = { id, line, direction, finalStopId, passings: {}, }; } if (!(stopId in courses[id].passings) || courses[id].passings[stopId] < departureTime) { // Only consider passings with an increased passing time // or for stops not seen before courses[id].passings[stopId] = departureTime; } } // Filter courses to only keep those referring to known data for (const courseId of Object.keys(courses)) { const course = courses[courseId]; if (!(course.line in network.lines)) { delete courses[courseId]; } else { for (const stopId of Object.keys(course.passings)) { if (!(stopId in network.stops)) { delete courses[courseId]; break; } } } } // Order next passings by increasing passing time for (const course of Object.values(courses)) { course.passings = ( Object.entries(course.passings).sort( ([, time1], [, time2]) => time1 - time2 ) ); const lastPassing = course.passings[course.passings.length - 1]; if (course.finalStopId === undefined) { course.finalStopId = lastPassing[0]; } else if (course.finalStopId !== lastPassing[0]) { if (isPenultimateStop(lastPassing[0], course.finalStopId)) { course.passings.push([course.finalStopId, lastPassing[1] + 60000]); } } } return courses; };