From 5dfa49b75f6bc7cb054826009a82e41fee227eaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matt=C3=A9o=20Delabre?= Date: Fri, 24 Jul 2020 19:05:43 +0200 Subject: [PATCH] Improve simulation code stability --- src/tam/simulation.js | 399 ++++++++++++++++++++++++++---------------- 1 file changed, 247 insertions(+), 152 deletions(-) diff --git a/src/tam/simulation.js b/src/tam/simulation.js index b24dfa3..4bef094 100644 --- a/src/tam/simulation.js +++ b/src/tam/simulation.js @@ -4,188 +4,168 @@ const network = require('./network.json'); const server = 'http://localhost:4321'; -const arriveAtStop = (course, stop) => +class Course { - course.state = 'stopped'; - course.currentStop = stop; - delete course.departureStop; - delete course.arrivalStop; - delete course.arrivalTime; - delete course.traveledDistance; - delete course.speed; -}; - -const moveToStop = (course, stop, arrivalTime) => -{ - course.state = 'moving'; - course.departureStop = course.currentStop; - course.arrivalStop = stop; - course.arrivalTime = arrivalTime; - course.traveledDistance = 0; - course.speed = 0; - delete course.currentStop; - - const segment = `${course.departureStop}-${course.arrivalStop}`; - - if (!(segment in network.segments)) + constructor(data) { - // There is no segment between the two requested stops, jump - // directly to the arrival stop - arriveAtStop(course, course.arrivalStop); - } - else - { - updateSpeed(course); - } -}; + this.id = data.id; + this.passings = {}; + this.state = null; -const getCurrentSegment = course => -{ - if (course.state === 'stopped') - { - return null; + // Attributes for the `stopped` state + this.currentStop = null; + + // Attributes for the `moving` state + this.departureStop = null; + this.arrivalStop = null; + this.arrivalTime = 0; + this.traveledDistance = 0; + this.speed = 0; + + this.position = [0, 0]; + this.angle = 0; } - return network.segments[`${course.departureStop}-${course.arrivalStop}`]; -}; - -const updateSpeed = course => -{ - const segment = getCurrentSegment(course); - const length = segment.points[segment.points.length - 1].distance; - - const remainingTime = course.arrivalTime - Date.now(); - const remainingDistance = length - course.traveledDistance; - - if (remainingTime <= 0 || remainingDistance <= 0) + get currentSegment() { - arriveAtStop(course, course.arrivalStop); - return; - } - - course.speed = remainingDistance / remainingTime; -}; - -const updateFromTam = async (courses) => -{ - const currentCourses = (await axios.get(`${server}/courses`)).data; - - for (let [id, course] of Object.entries(currentCourses)) - { - // Find out the next stop, ignoring the ones that are in the past - let nextStop = null; - let arrivalTime = null; - - for (let {stopId, arrivalTime: time} of course.nextPassings) + if (this.state !== 'moving') { - if (time > Date.now()) - { - nextStop = stopId; - arrivalTime = time; - break; - } + return undefined; } - if (nextStop === null) - { - continue; - } + return network.segments[`${this.departureStop}-${this.arrivalStop}`]; + } - // Update an existing course - if (id in courses) - { - const prev = courses[id]; + updateData(data) + { + this.line = data.line; + this.finalStop = data.finalStop; + Object.assign(this.passings, data.nextPassings); - if (prev.state === 'stopped') + const now = Date.now(); + + // Make sure we’re on the right `stopped`/`moving` state + if (this.state === null) + { + let previousStop = null; + let departureTime = 0; + + let nextStop = null; + let arrivalTime = Infinity; + + for (let [stopId, time] of Object.entries(this.passings)) { - if (prev.currentStop !== nextStop) + if (time > now && time < arrivalTime) { - // Start traveling from the current stop to the next - moveToStop(prev, nextStop, arrivalTime); + nextStop = stopId; + arrivalTime = time; } + + if (time < now && time > departureTime) + { + previousStop = stopId; + departureTime = time; + } + } + + if (nextStop === null) + { + return false; + } + + if (previousStop === null) + { + // Teleport to the first known stop + this.arriveToStop(nextStop); } else { - // Update the ETA if we’re still headed to the same stop - if (prev.arrivalStop === nextStop) - { - prev.arrivalTime = arrivalTime; - updateSpeed(prev); - } - // Otherwise, we missed a stop, try to go directly to the - // next segment - else - { - arriveAtStop(prev, prev.arrivalStop); - moveToStop(prev, nextStop, arrivalTime); - } + // Teleport to the first known segment + this.arriveToStop(previousStop); + this.moveToStop(nextStop, arrivalTime); } } - // Create a new course - else + else if (this.state === 'moving') { - courses[id] = { - id, - line: course.line, - finalStop: course.finalStop, - position: [0, 0], - angle: 0, - }; - - arriveAtStop(courses[id], nextStop); + // Should already be at the next stop + if (this.passings[this.arrivalStop] <= now) + { + this.arriveToStop(this.arrivalStop); + } + // On the right track, update the arrival time + else + { + this.arrivalTime = this.passings[this.arrivalStop]; + } } + else // this.state === 'stopped' + { + // Try moving to the next stop + let nextStop = null; + let arrivalTime = Infinity; + + for (let [stopId, time] of Object.entries(this.passings)) + { + if (time > now && time < arrivalTime) + { + nextStop = stopId; + arrivalTime = time; + } + } + + if (nextStop === null) + { + // This course is finished + return false; + } + + if (nextStop !== this.currentStop) + { + this.moveToStop(nextStop, arrivalTime); + } + } + + if (this.state === 'moving') + { + this.speed = this.computeTheoreticalSpeed(); + } + + return true; } - // Remove stale courses - for (let id of Object.keys(courses)) + tick(time) { - if (!(id in currentCourses)) + if (this.state === null) { - delete courses[id]; + // Ignore uninitalized courses } - } -}; - -const updatePositions = (courses, time) => -{ - for (let [id, course] of Object.entries(courses)) - { - if (course.state === 'moving') + else if (this.state === 'moving') { - // Increase the travelled distance respective to the current speed - const delta = course.speed * time; + // Integrate current speed in travelled distance + const delta = this.speed * time; - const segment = getCurrentSegment(course); + const segment = this.currentSegment; const length = segment.points[segment.points.length - 1].distance; + this.traveledDistance += delta; - if (course.traveledDistance + delta >= length) + if (this.traveledDistance >= length) { - course.traveledDistance = length; - } - else - { - course.traveledDistance += delta; + this.arriveToStop(this.arrivalStop); + return; } // Recompute updated position - const departureStop = network.stops[course.departureStop]; - const arrivalStop = network.stops[course.arrivalStop]; + const departureStop = network.stops[this.departureStop]; + const arrivalStop = network.stops[this.arrivalStop]; const nextNodeIndex = segment.points.findIndex( - ({distance}) => distance >= course.traveledDistance); + ({distance}) => distance >= this.traveledDistance); if (nextNodeIndex === 0) { - course.position = { - lat: departureStop.lat, - lon: departureStop.lon - }; - } - else if (nextNodeIndex === -1) - { - course.position = { - lat: arrivalStop.lat, - lon: arrivalStop.lon - }; + this.position = turf.toMercator([ + departureStop.lon, + departureStop.lat + ]); } else { @@ -202,33 +182,148 @@ const updatePositions = (courses, time) => nextNode.lat ]); - const curLength = course.traveledDistance + const curLength = this.traveledDistance - previousNode.distance; const totalLength = nextNode.distance - previousNode.distance; const t = curLength / totalLength; - course.position = [ + this.position = [ t * nextPoint[0] + (1 - t) * previousPoint[0], t * nextPoint[1] + (1 - t) * previousPoint[1], ]; - course.angle = Math.atan2( + this.angle = Math.atan2( previousPoint[1] - nextPoint[1], nextPoint[0] - previousPoint[0], ); } } - else + else // this.state === 'stopped' { - const currentNode = network.stops[course.currentStop]; + const currentNode = network.stops[this.currentStop]; - course.position = turf.toMercator([ + this.position = turf.toMercator([ currentNode.lon, currentNode.lat ]); } } + + /** + * Transition this course to a state where it has arrived to a stop. + * + * @param stop Identifier for the stop to which the course arrives. + */ + arriveToStop(stop) + { + this.state = 'stopped'; + this.currentStop = stop; + this.position = turf.toMercator([ + network.stops[this.currentStop].lon, + network.stops[this.currentStop].lat, + ]); + } + + /** + * Transition this course to a state where it is moving to a stop. + * + * @param stop Next stop for this course. + * @param arrivalTime Planned arrival time to that stop. + */ + moveToStop(stop, arrivalTime) + { + this.state = 'moving'; + this.departureStop = this.currentStop; + this.arrivalStop = stop; + this.arrivalTime = arrivalTime; + this.traveledDistance = 0; + this.speed = 0; + this.position = turf.toMercator([ + network.stops[this.departureStop].lon, + network.stops[this.departureStop].lat, + ]); + + console.log(`Course ${this.id} leaving stop ${this.currentStop} \ +with initial speed ${this.computeTheoreticalSpeed() * 3600} km/h`); + + if (this.currentSegment === undefined) + { + console.error(`Course ${this.id} is on an undefined segment from \ +stop ${this.departureStop} to stop ${this.arrivalStop}`); + } + } + + /** + * Compute the speed that needs to be maintained to arrive on time. + */ + computeTheoreticalSpeed() + { + if (this.state !== 'moving') + { + return 0; + } + + const segment = this.currentSegment; + const length = segment.points[segment.points.length - 1].distance; + + const remainingTime = this.arrivalTime - Date.now(); + const remainingDistance = length - this.traveledDistance; + + if (remainingDistance <= 0) + { + return 0; + } + else if (remainingTime <= 0) + { + // We’re late, go to maximum speed + return 50 / 3600; // 50 km/h + } + else + { + return remainingDistance / remainingTime; + } + } +} + +const updateData = async (courses) => +{ + const dataset = (await axios.get(`${server}/courses`)).data; + + // Update or create new courses + for (let [id, data] of Object.entries(dataset)) + { + if (id in courses) + { + courses[id].updateData(data); + } + else + { + courses[id] = new Course(data); + + if (!courses[id].updateData(data)) + { + console.info(`Ignoring course ${id} which is outdated.`); + } + } + } + + // Remove stale courses + for (let id of Object.keys(courses)) + { + if (!(id in dataset)) + { + delete courses[id]; + } + } +}; + +const tick = (courses, time) => +{ + for (let course of Object.values(courses)) + { + course.tick(time); + } }; const start = () => @@ -244,12 +339,12 @@ const start = () => if (lastUpdate === null || lastUpdate + 5000 <= now) { lastUpdate = now; - updateFromTam(courses); + updateData(courses); } const time = lastFrame === null ? 0 : now - lastFrame; lastFrame = now; - updatePositions(courses, time); + tick(courses, time); }; return {courses, update};