diff --git a/script/show-courses b/script/show-courses new file mode 100755 index 0000000..18afcd9 --- /dev/null +++ b/script/show-courses @@ -0,0 +1,89 @@ +#!/usr/bin/env node + +const courses = require('../src/tam/courses'); +const network = require('../src/tam/network.json'); +const {displayTime} = require('../src/util'); +const process = require('process'); +const path = require('path'); + +/** + * Convert stop ID to human-readable stop name. + * + * If the stop ID is not known, the ID will be kept as-is. + */ +const getStopName = stopId => +{ + if (stopId in network.stops) + { + return network.stops[stopId].properties.name; + } + + return stopId; +}; + +/** Create a string representing a course for printing. */ +const courseToString = course => +{ + let result = `Course #${course.id} +Line ${course.line} - Direction ${course.direction} - Bound for ${getStopName(course.finalStopId)} + +Next stops: +`; + + for (let [stopId, time] of course.passings) + { + result += `${displayTime(new Date(time))} - ${getStopName(stopId)}\n`; + } + + return result; +}; + +/** Show user help. */ +const doHelp = () => +{ + const name = "./" + path.relative(process.cwd(), process.argv[1]); + process.stdout.write(`Usage: ${name} TYPE [COURSE] +Show TaM courses data. + +Set TYPE to 'realtime' to fetch real-time data (limited time scope) or to +'theoretical' to fetch planned courses for the day. + +Set COURSE to a valid course ID to limit the output to a given course. +`); +}; + +/** Print realtime information for a course or all courses. */ +const doPrint = async (kind, courseId) => +{ + const results = await courses.fetch(kind); + + if (courseId) + { + if (courseId in results) + { + console.log(courseToString(results[courseId])); + } + else + { + console.log('Unknown course'); + } + } + else + { + for (let course of Object.values(results)) + { + console.log(courseToString(course)); + console.log("======\n"); + } + } +}; + +const argv = process.argv.slice(2); + +if (argv.length === 0) +{ + doHelp(); + process.exit(1); +} + +doPrint(argv[0], argv[1]); diff --git a/src/back/index.js b/src/back/index.js index 8b808f4..8cae1f9 100644 --- a/src/back/index.js +++ b/src/back/index.js @@ -1,12 +1,12 @@ const express = require("express"); -const realtime = require("../tam/realtime"); +const courses = require("../tam/courses"); const app = express(); const port = 4321; app.get("/courses", async(req, res) => { res.header("Access-Control-Allow-Origin", "*"); - return res.json(await realtime.fetch()); + return res.json(await courses.fetch("realtime")); }); app.listen(port, () => console.info(`App listening on port ${port}`)); diff --git a/src/tam/courses.js b/src/tam/courses.js new file mode 100644 index 0000000..e64a280 --- /dev/null +++ b/src/tam/courses.js @@ -0,0 +1,123 @@ +const path = require("path"); +const fs = require("fs").promises; +const tam = require("./sources/tam"); +const network = require("./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; +}; + +/** + * Fetch information about courses in the TaM network. + * + * @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. + */ +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 + ) + ); + + if (course.finalStopId === undefined) { + course.finalStopId = course.passings[course.passings.length - 1][0]; + } + } + + return courses; +}; + +exports.fetch = fetch; diff --git a/src/tam/realtime.js b/src/tam/realtime.js deleted file mode 100644 index 38efc77..0000000 --- a/src/tam/realtime.js +++ /dev/null @@ -1,100 +0,0 @@ -const tam = require("./sources/tam"); -const network = require("./network.json"); - -// Time at which the course data needs to be updated next -let nextUpdate = null; - -// Current information about courses -let currentCourses = null; - -/** - * 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.} nextPassings Next stations to which - * the vehicle will stop, associated to the passing timestamp, ordered by - * increasing passing timestamp. - */ - -/** - * Fetch real-time information about active courses in the TaM network. - * - * New data will only be fetched from the TaM server once every minute, - * otherwise pulling from the in-memory cache. - * @returns {Object.} Mapping from active course IDs to - * information about each course. - */ -const fetch = async() => { - if (nextUpdate === null || Date.now() >= nextUpdate) { - const courses = {}; - const passings = tam.fetchRealtime(); - const timing = (await passings.next()).value; - - nextUpdate = timing.nextUpdate; - - // Aggregate passings relative to the same course - for await (const passing of passings) { - const { - course: id, - routeShortName: line, - stopId, - destArCode: finalStop - } = passing; - - const arrivalTime = ( - timing.lastUpdate + - parseInt(passing.delaySec, 10) * 1000 - ); - - if (!(id in courses)) { - courses[id] = { - id, - line, - finalStop, - - // Initially accumulate passings in an object - // to prevent duplicates - nextPassings: { [stopId]: arrivalTime } - }; - } else if (!(stopId in courses[id].nextPassings) || - courses[id].nextPassings[stopId] < arrivalTime) { - // Only consider passings with an increased passing time - // or for stops not seen before - courses[id].nextPassings[stopId] = arrivalTime; - } - } - - // 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.nextPassings)) { - if (!(stopId in network.stops)) { - delete courses[courseId]; - break; - } - } - } - } - - // Order next passings by increasing passing time - for (const courseId of Object.keys(courses)) { - courses[courseId].nextPassings = ( - Object.entries(courses[courseId].nextPassings).sort( - ([, time1], [, time2]) => time1 - time2 - ) - ); - } - - currentCourses = courses; - } - - return currentCourses; -}; - -exports.fetch = fetch; diff --git a/src/tam/simulation.js b/src/tam/simulation.js index a6701bb..7e41902 100644 --- a/src/tam/simulation.js +++ b/src/tam/simulation.js @@ -59,8 +59,9 @@ class Course { updateData(data) { this.line = data.line; - this.finalStop = data.finalStop; - this.nextPassings = data.nextPassings; + this.direction = data.direction; + this.finalStop = data.finalStopId; + this.nextPassings = data.passings; const now = Date.now(); diff --git a/src/util.js b/src/util.js index aeceb8d..b5f2632 100644 --- a/src/util.js +++ b/src/util.js @@ -63,3 +63,11 @@ const unzipFile = (data, fileName) => new Promise((res, rej) => { }); exports.unzipFile = unzipFile; + +const displayTime = date => [ + date.getHours(), + date.getMinutes(), + date.getSeconds() +].map(number => number.toString().padStart(2, "0")).join(":"); + +exports.displayTime = displayTime;