165 lines
4.8 KiB
JavaScript
165 lines
4.8 KiB
JavaScript
/**
|
||
* @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.<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.<string,Course>} 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;
|
||
};
|