Compare commits

..

No commits in common. "d9869ce2f76ab7382e11e8812ffb7e5debfd4689" and "5db750adcaa60cb1dc95eb70263b93bf15e015b9" have entirely different histories.

7 changed files with 139 additions and 822 deletions

View File

@ -7,31 +7,21 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<style> <style>
body, html { body, html
{
padding: 0; padding: 0;
margin: 0; margin: 0;
height: 100%; height: 100%;
} }
body { #map
display: flex; {
flex-direction: row;
}
#informations {
flex: 0 0 600px;
padding: 20px;
overflow-y: auto;
}
#map {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
</style> </style>
</head> </head>
<body> <body>
<div id="informations"></div>
<div id="map"></div> <div id="map"></div>
<script src="index.js"></script> <script src="index.js"></script>
</body> </body>

View File

@ -1,89 +1,6 @@
// eslint-disable-next-line node/no-extraneous-require // eslint-disable-next-line node/no-extraneous-require
require("regenerator-runtime/runtime"); require("regenerator-runtime/runtime");
const network = require("../tam/network.json");
const simulation = require("../tam/simulation");
const map = require("./map/index.js"); const map = require("./map/index.js");
// Run courses simulation map.create(/* map = */ "map");
const coursesSimulation = simulation.start();
global.courses = coursesSimulation.courses;
const informations = document.querySelector("#informations");
let courseId = null;
const displayTime = date => [
date.getHours(),
date.getMinutes(),
date.getSeconds()
].map(number => number.toString().padStart(2, '0')).join(':');
setInterval(() => {
let html = `
<dl>
<dt>Heure actuelle</dt>
<dd>${displayTime(new Date())}</dd>
</dl>
`;
if (courseId !== null && courseId in coursesSimulation.courses) {
const course = coursesSimulation.courses[courseId];
const timeToHTML = time => Math.ceil((time - Date.now()) / 1000);
const stopToHTML = stopId => network.stops[stopId].properties.name;
const passingsToHTML = passings => passings.map(([stopId, time]) => `
<tr>
<td>${stopToHTML(stopId)}</td>
<td>${displayTime(new Date(time))}</td>
</tr>
`).join('\n');
html += `
<dl>
<dt>ID</dt>
<dd>${courseId}</dd>
<dt>Ligne</dt>
<dd>${course.line}</dd>
<dt>Destination</dt>
<dd>${stopToHTML(course.finalStop)}</dd>
<dt>État</dt>
<dd>${course.state === 'moving'
? `Entre ${stopToHTML(course.departureStop)}
et ${stopToHTML(course.arrivalStop)}`
: `À larrêt ${stopToHTML(course.currentStop)}`}</dd>
${course.state === 'moving' ? `
<dt>Arrivée dans</dt>
<dd>${timeToHTML(course.arrivalTime)} s</dd>
<dt>Distance parcourue</dt>
<dd>${Math.ceil(course.traveledDistance)} m</dd>
<dt>Vitesse</dt>
<dd>${Math.ceil(course.speed * 3600)} km/h</dd>
` : `
<dt>Départ dans</dt>
<dd>${timeToHTML(course.departureTime)} s</dd>
`}
</dl>
<h2>Arrêts précédents</h2>
<table>${passingsToHTML(course.prevPassings)}</table>
<h2>Arrêts suivants</h2>
<table>${passingsToHTML(course.nextPassings)}</table>
`;
}
informations.innerHTML = html;
}, 1000);
// Create the network and courses map
map.create(/* map = */ "map", coursesSimulation, course => {
courseId = course;
});

View File

@ -9,6 +9,7 @@ const tilesLayers = require("./tiles");
const networkLayers = require("./network"); const networkLayers = require("./network");
const { sizes, makeBorderColor, makeCourseColor } = require("./common"); const { sizes, makeBorderColor, makeCourseColor } = require("./common");
const network = require("../../tam/network.json"); const network = require("../../tam/network.json");
const simulation = require("../../tam/simulation");
const courseStyles = {}; const courseStyles = {};
@ -58,7 +59,7 @@ const getCourseStyle = lineColor => {
return courseStyles[lineColor]; return courseStyles[lineColor];
}; };
const create = (target, coursesSimulation, onClick) => { const create = target => {
const view = new View({ const view = new View({
center: proj.fromLonLat([3.88, 43.605]), center: proj.fromLonLat([3.88, 43.605]),
zoom: 14, zoom: 14,
@ -77,9 +78,32 @@ const create = (target, coursesSimulation, onClick) => {
const stopsLayer = map.getLayers().item(3); const stopsLayer = map.getLayers().item(3);
// Run courses simulation
const simulInstance = simulation.start();
// Course on which the view is currently focused
let focusedCourse = null;
const startFocus = courseId => {
if (courseId in simulInstance.courses) {
const course = simulInstance.courses[courseId];
view.animate({
center: course.position,
duration: 500
}, () => {
focusedCourse = courseId;
});
}
};
const stopFocus = () => {
focusedCourse = null;
};
// Draw courses directly on the map // Draw courses directly on the map
map.on("postcompose", ev => { map.on("postcompose", ev => {
coursesSimulation.update(); simulInstance.update();
// The normal way to access a layers vector context is through the // The normal way to access a layers vector context is through the
// `postrender` event of that layer. However, `postrender` is not // `postrender` event of that layer. However, `postrender` is not
@ -97,8 +121,7 @@ const create = (target, coursesSimulation, onClick) => {
const ctx = getVectorContext(ev); const ctx = getVectorContext(ev);
for (const course of Object.values(coursesSimulation.courses)) { for (const course of Object.values(simulInstance.courses)) {
const point = new Point(course.position);
const color = network.lines[course.line].color; const color = network.lines[course.line].color;
const style = getCourseStyle(color); const style = getCourseStyle(color);
@ -108,7 +131,14 @@ const create = (target, coursesSimulation, onClick) => {
); );
ctx.setStyle(style); ctx.setStyle(style);
const point = new Point(course.position);
ctx.drawGeometry(point); ctx.drawGeometry(point);
if (course.id === focusedCourse) {
view.setCenter(course.position);
}
} }
} }
@ -119,18 +149,22 @@ const create = (target, coursesSimulation, onClick) => {
map.on("singleclick", ev => { map.on("singleclick", ev => {
const mousePixel = map.getPixelFromCoordinate(ev.coordinate); const mousePixel = map.getPixelFromCoordinate(ev.coordinate);
const maxDistance = sizes.courseSize + sizes.courseInnerBorder; const maxDistance = sizes.courseSize + sizes.courseOuterBorder;
for (const course of Object.values(coursesSimulation.courses)) { for (const course of Object.values(simulInstance.courses)) {
const coursePixel = map.getPixelFromCoordinate(course.position); const coursePixel = map.getPixelFromCoordinate(course.position);
const dx = mousePixel[0] - coursePixel[0]; const dx = mousePixel[0] - coursePixel[0];
const dy = mousePixel[1] - coursePixel[1]; const dy = mousePixel[1] - coursePixel[1];
const distance = dx * dx + dy * dy; const distance = dx * dx + dy * dy;
if (distance <= maxDistance * maxDistance) { if (distance <= maxDistance * maxDistance) {
onClick(course.id); startFocus(course.id);
return;
} }
} }
// Clicking anywhere else resets focus
stopFocus();
}); });
return map; return map;

View File

@ -88,8 +88,7 @@ a “ref” tag`);
stop.lat stop.lat
], { ], {
name: stop.tags.name, name: stop.tags.name,
routes: [[lineRef, routeRef]], routes: [[lineRef, routeRef]]
successors: [],
}); });
} else { } else {
stops[stop.tags.ref].properties.routes.push([ stops[stop.tags.ref].properties.routes.push([
@ -224,7 +223,6 @@ different sequence of nodes in two or more lines.`);
routes: [[lineRef, routeRef]] routes: [[lineRef, routeRef]]
}); });
stops[begin].properties.successors.push(end);
segments[id].properties.length = ( segments[id].properties.length = (
1000 * turfLength(segments[id])); 1000 * turfLength(segments[id]));
} }

File diff suppressed because it is too large Load Diff

View File

@ -13,9 +13,8 @@ let currentCourses = null;
* @property {string} id Unique identifier for this course. * @property {string} id Unique identifier for this course.
* @property {string} line Transport line number. * @property {string} line Transport line number.
* @property {string} finalStop Final stop to which the course is headed. * @property {string} finalStop Final stop to which the course is headed.
* @property {Array.<Array>} nextPassings Next stations to which * @property {Object.<string,number>} nextPassings Next stations to which
* the vehicle will stop, associated to the passing timestamp, ordered by * the vehicle will stop, associated to the passing timestamp.
* increasing passing timestamp.
*/ */
/** /**
@ -53,10 +52,10 @@ const fetch = async() => {
id, id,
line, line,
finalStop, finalStop,
nextPassings: [[stopId, arrivalTime]] nextPassings: { [stopId]: arrivalTime }
}; };
} else { } else {
courses[id].nextPassings.push([stopId, arrivalTime]); courses[id].nextPassings[stopId] = arrivalTime;
} }
} }
@ -67,7 +66,7 @@ const fetch = async() => {
if (!(course.line in network.lines)) { if (!(course.line in network.lines)) {
delete courses[courseId]; delete courses[courseId];
} else { } else {
for (const [stopId,] of course.nextPassings) { for (const stopId of Object.keys(course.nextPassings)) {
if (!(stopId in network.stops)) { if (!(stopId in network.stops)) {
delete courses[courseId]; delete courses[courseId];
break; break;
@ -76,13 +75,6 @@ const fetch = async() => {
} }
} }
// Order next passings by increasing passing time
for (const courseId of Object.keys(courses)) {
courses[courseId].nextPassings.sort(
([, time1], [, time2]) => time1 - time2
);
}
currentCourses = courses; currentCourses = courses;
} }

View File

@ -5,38 +5,14 @@ const network = require("./network.json");
const server = "http://localhost:4321"; const server = "http://localhost:4321";
const stops = Object.keys(network.stops);
const findRoute = (from, to) => {
const queue = [[from, []]];
while (queue.length) {
const [head, path] = queue.shift();
for (const successor of network.stops[head].properties.successors) {
if (successor === to) {
return path.concat([head, successor]);
}
if (!path.includes(successor)) {
queue.push([successor, path.concat([head])]);
}
}
}
return null;
};
class Course { class Course {
constructor(data) { constructor(data) {
this.id = data.id; this.id = data.id;
this.prevPassings = []; this.passings = {};
this.nextPassings = [];
this.state = null; this.state = null;
// Attributes for the `stopped` state // Attributes for the `stopped` state
this.currentStop = null; this.currentStop = null;
this.departureTime = 0;
// Attributes for the `moving` state // Attributes for the `moving` state
this.departureStop = null; this.departureStop = null;
@ -62,118 +38,76 @@ class Course {
updateData(data) { updateData(data) {
this.line = data.line; this.line = data.line;
this.finalStop = data.finalStop; this.finalStop = data.finalStop;
this.nextPassings = data.nextPassings; Object.assign(this.passings, data.nextPassings);
const now = Date.now(); const now = Date.now();
// Make sure were on the right `stopped`/`moving` state
if (this.state === null) { if (this.state === null) {
// Initialize the course on the first available segment let previousStop = null;
const index = this.nextPassings.findIndex( let departureTime = 0;
([, time]) => time >= now
);
if (index === -1) { let nextStop = null;
let arrivalTime = Infinity;
for (const [stopId, time] of Object.entries(this.passings)) {
if (time > now && time < arrivalTime) {
nextStop = stopId;
arrivalTime = time;
}
if (time < now && time > departureTime) {
previousStop = stopId;
departureTime = time;
}
}
if (nextStop === null) {
return false; return false;
} }
if (index === 0) { if (previousStop === null) {
this.arriveToStop(this.nextPassings[index][0]); // Teleport to the first known stop
this.arriveToStop(nextStop);
} else { } else {
this.arriveToStop(this.nextPassings[index - 1][0]);
this.moveToStop(...this.nextPassings[index]); // Teleport to the first known segment
this.arriveToStop(previousStop);
this.moveToStop(nextStop, arrivalTime);
} }
} else if (this.state === "moving") { } else if (this.state === "moving") {
const index = this.nextPassings.findIndex( if (this.passings[this.arrivalStop] <= now) {
([stop, ]) => stop === this.arrivalStop // Should already be at the next stop
); this.arriveToStop(this.arrivalStop);
if (index === -1 || this.nextPassings[index][1] <= now) {
// Next stop is not announced or in the past,
// move towards it as fast as possible
this.arrivalTime = now;
} else { } else {
// On the right track, update the arrival time // On the right track, update the arrival time
this.arrivalTime = this.nextPassings[index][1]; this.arrivalTime = this.passings[this.arrivalStop];
} }
} else { } else {
// (this.state === 'stopped') // (this.state === 'stopped')
// Try moving to the next stop // Try moving to the next stop
const index = this.nextPassings.findIndex( let nextStop = null;
([stop, ]) => stop === this.currentStop let arrivalTime = Infinity;
);
if (index !== -1) { for (const [stopId, time] of Object.entries(this.passings)) {
if (this.nextPassings[index][1] <= now) { if (time > now && time < arrivalTime) {
// Current stop is still announced but in the past nextStop = stopId;
if (index + 1 < this.nextPassings.length) { arrivalTime = time;
// Move to next stop }
this.moveToStop(...this.nextPassings[index + 1]); }
} else {
// No next stop announced, end of course if (nextStop === null) {
// This course is finished
return false; return false;
} }
} else {
// Cannot move yet, departure is in the future
this.departureTime = this.nextPassings[index][1];
}
} else {
// Current stop is not announced, find the first stop
// announced in the future to which is connection is
// possible
let found = false;
for ( if (nextStop !== this.currentStop) {
let index = 0; this.moveToStop(nextStop, arrivalTime);
index < this.nextPassings.length;
++index
) {
const [stop, arrivalTime] = this.nextPassings[index];
if (arrivalTime > now) {
const route = findRoute(this.currentStop, stop);
if (route !== null) {
// Move to the first intermediate stop, guess the
// arrival time based on the final arrival time and
// the relative distance of the stops
const midDistance = network.segments[
`${route[0]}-${route[1]}`
].properties.length;
let totalDistance = midDistance;
for (
let midIndex = 1;
midIndex + 1 < route.length;
++midIndex
) {
totalDistance += network.segments[
`${route[midIndex]}-${route[midIndex + 1]}`
].properties.length;
}
const midTime = now + (arrivalTime - now) *
midDistance / totalDistance;
this.moveToStop(route[1], midTime);
found = true;
break;
}
}
}
if (!found) {
// No valid next stop available
return false;
}
} }
} }
if (this.state === "moving") { if (this.state === "moving") {
const segment = this.currentSegment; this.speed = this.computeTheoreticalSpeed();
const distance = segment.properties.length - this.traveledDistance;
const duration = this.arrivalTime - Date.now();
this.speed = this.computeSpeed(distance, duration);
} }
return true; return true;
@ -181,6 +115,7 @@ class Course {
tick(time) { tick(time) {
if (this.state === "moving") { if (this.state === "moving") {
// Integrate current speed in travelled distance // Integrate current speed in travelled distance
this.traveledDistance += this.speed * time; this.traveledDistance += this.speed * time;
const segment = this.currentSegment; const segment = this.currentSegment;
@ -220,12 +155,10 @@ class Course {
arriveToStop(stop) { arriveToStop(stop) {
this.state = "stopped"; this.state = "stopped";
this.currentStop = stop; this.currentStop = stop;
this.departureTime = Date.now();
this.prevPassings.push([stop, Date.now()]);
this.position = ( this.position = (
turfProjection.toMercator(network.stops[stop]) turfProjection.toMercator(network.stops[stop])
.geometry.coordinates .geometry.coordinates);
); this.history.push(["arriveToStop", stop]);
} }
/** /**
@ -235,43 +168,49 @@ class Course {
* @returns {undefined} * @returns {undefined}
*/ */
moveToStop(stop, arrivalTime) { moveToStop(stop, arrivalTime) {
const segmentId = `${this.currentStop}-${stop}`; if (!(`${this.currentStop}-${stop}` in network.segments)) {
if (!(segmentId in network.segments)) {
console.warn(`Course ${this.id} cannot go from stop console.warn(`Course ${this.id} cannot go from stop
${this.currentStop} to stop ${stop}. Teleporting to ${stop}`); ${this.currentStop} to stop ${stop}. Teleporting to ${stop}`);
this.arriveToStop(stop); this.arriveToStop(stop);
return; return;
} }
const distance = network.segments[segmentId].properties.length;
const duration = arrivalTime - Date.now();
if (this.computeSpeed(distance, duration) === 0) {
// Speed would be too low, better wait for some time
return;
}
this.state = "moving"; this.state = "moving";
this.departureStop = this.currentStop; this.departureStop = this.currentStop;
this.arrivalStop = stop; this.arrivalStop = stop;
this.arrivalTime = arrivalTime; this.arrivalTime = arrivalTime;
this.traveledDistance = 0; this.traveledDistance = 0;
this.speed = 0; this.speed = 0;
this.history.push(["moveToStop", stop, arrivalTime]);
console.info(`Course ${this.id} leaving stop ${this.currentStop} \
with initial speed ${this.computeTheoreticalSpeed() * 3600} km/h`);
} }
computeSpeed(distance, duration) { /**
if (duration <= 0) { * Compute the speed that needs to be maintained to arrive on time.
// Late: go to maximum speed * @returns {number} Speed in meters per millisecond.
return 50 / 3600; */
} computeTheoreticalSpeed() {
if (this.state !== "moving") {
if (distance / duration <= 10 / 3600) {
// Too slow: pause until speed is sufficient
return 0; return 0;
} }
return distance / duration; const segment = this.currentSegment;
const remainingTime = this.arrivalTime - Date.now();
const remainingDistance = (
segment.properties.length - this.traveledDistance
);
if (remainingDistance <= 0) {
return 0;
}
if (remainingTime <= 0) {
// Were late, go to maximum speed
return 50 / 3600; // 50 km/h
}
return remainingDistance / remainingTime;
} }
} }
@ -282,12 +221,16 @@ const updateData = async courses => {
for (const [id, data] of Object.entries(dataset)) { for (const [id, data] of Object.entries(dataset)) {
if (id in courses) { if (id in courses) {
if (!courses[id].updateData(data)) { if (!courses[id].updateData(data)) {
console.info(`Course ${id} is finished.`);
delete courses[id]; delete courses[id];
} }
} else { } else {
const newCourse = new Course(data); const newCourse = new Course(data);
if (newCourse.updateData(data)) { if (!newCourse.updateData(data)) {
console.info(`Ignoring course ${id} which is outdated.`);
} else {
console.info(`Course ${id} starting.`);
courses[id] = newCourse; courses[id] = newCourse;
} }
} }