Add show-courses script
This commit is contained in:
parent
ef6b4b9741
commit
6b7333c46b
|
@ -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]);
|
|
@ -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}`));
|
||||
|
|
|
@ -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.<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.<string,Course>} 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;
|
|
@ -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.<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.<string,Course>} 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;
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue