tracktracker/src/tam/simulation.js

369 lines
9.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const axios = require('axios');
const turf = require('@turf/turf');
const network = require('./network.json');
const server = 'http://localhost:4321';
class Course
{
constructor(data)
{
this.id = data.id;
this.passings = {};
this.state = 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;
this.history = [];
}
get currentSegment()
{
if (this.state !== 'moving')
{
return undefined;
}
return network.segments[`${this.departureStop}-${this.arrivalStop}`];
}
updateData(data)
{
this.line = data.line;
this.finalStop = data.finalStop;
Object.assign(this.passings, data.nextPassings);
const now = Date.now();
// Make sure were 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 (time > now && time < 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
{
// Teleport to the first known segment
this.arriveToStop(previousStop);
this.moveToStop(nextStop, arrivalTime);
}
}
else if (this.state === 'moving')
{
// 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;
}
tick(time)
{
if (this.state === null)
{
// Ignore uninitalized courses
}
else if (this.state === 'moving')
{
// Integrate current speed in travelled distance
const delta = this.speed * time;
const segment = this.currentSegment;
const length = segment.points[segment.points.length - 1].distance;
this.traveledDistance += delta;
if (this.traveledDistance >= length)
{
this.arriveToStop(this.arrivalStop);
return;
}
// Recompute updated position
const departureStop = network.stops[this.departureStop];
const arrivalStop = network.stops[this.arrivalStop];
const nextNodeIndex = segment.points.findIndex(
({distance}) => distance >= this.traveledDistance);
if (nextNodeIndex === 0)
{
this.position = turf.toMercator([
departureStop.lon,
departureStop.lat
]);
}
else
{
const previousNode = segment.points[nextNodeIndex - 1];
const nextNode = segment.points[nextNodeIndex];
const previousPoint = turf.toMercator([
previousNode.lon,
previousNode.lat
]);
const nextPoint = turf.toMercator([
nextNode.lon,
nextNode.lat
]);
const curLength = this.traveledDistance
- previousNode.distance;
const totalLength = nextNode.distance
- previousNode.distance;
const t = curLength / totalLength;
this.position = [
t * nextPoint[0] + (1 - t) * previousPoint[0],
t * nextPoint[1] + (1 - t) * previousPoint[1],
];
this.angle = Math.atan2(
previousPoint[1] - nextPoint[1],
nextPoint[0] - previousPoint[0],
);
}
}
else // this.state === 'stopped'
{
const currentNode = network.stops[this.currentStop];
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,
]);
this.history.push(['arriveToStop', stop]);
}
/**
* 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)
{
if (!(`${this.currentStop}-${stop}` in network.segments))
{
console.warn(`Course ${this.id} is cannot go from stop
${this.currentStop} to stop ${stop}. Teleporting to ${stop}`);
this.arriveToStop(stop);
return;
}
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,
]);
this.history.push(['moveToStop', stop, arrivalTime]);
console.info(`Course ${this.id} leaving stop ${this.currentStop} \
with initial speed ${this.computeTheoreticalSpeed() * 3600} km/h`);
}
/**
* 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)
{
// Were 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)
{
if (!courses[id].updateData(data))
{
console.info(`Course ${id} is finished.`);
delete courses[id];
}
}
else
{
const newCourse = new Course(data);
if (!newCourse.updateData(data))
{
console.info(`Ignoring course ${id} which is outdated.`);
}
else
{
console.info(`Course ${id} starting.`);
courses[id] = newCourse;
}
}
}
// 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 = () =>
{
const courses = {};
let lastFrame = null;
let lastUpdate = null;
const update = () =>
{
const now = Date.now();
if (lastUpdate === null || lastUpdate + 5000 <= now)
{
lastUpdate = now;
updateData(courses);
}
const time = lastFrame === null ? 0 : now - lastFrame;
lastFrame = now;
tick(courses, time);
};
return {courses, update};
};
exports.start = start;