tracktracker/src/tam/simulation.js

276 lines
7.9 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 turfAlong = require("@turf/along").default;
const turfProjection = require("@turf/projection");
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 null;
}
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 (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;
}
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") {
if (this.passings[this.arrivalStop] <= now) {
// Should already be at the next stop
this.arriveToStop(this.arrivalStop);
} else {
// On the right track, update the arrival time
this.arrivalTime = this.passings[this.arrivalStop];
}
} else {
// (this.state === 'stopped')
// Try moving to the next stop
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 (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 === "moving") {
// Integrate current speed in travelled distance
this.traveledDistance += this.speed * time;
const segment = this.currentSegment;
if (this.traveledDistance >= segment.properties.length) {
this.arriveToStop(this.arrivalStop);
return;
}
// Compute updated position and angle based on a small step
const step = 10; // In meters
const positions = [
Math.max(0, this.traveledDistance - step / 2),
this.traveledDistance,
this.traveledDistance + step / 2
].map(distance => turfProjection.toMercator(turfAlong(
segment,
distance / 1000
)).geometry.coordinates);
this.angle = Math.atan2(
positions[0][1] - positions[2][1],
positions[2][0] - positions[0][0]
);
this.position = positions[1];
}
}
/**
* Transition this course to a state where it has arrived to a stop.
* @param {string} stop Identifier for the stop to which
* the course arrives.
* @returns {undefined}
*/
arriveToStop(stop) {
this.state = "stopped";
this.currentStop = stop;
this.position = (
turfProjection.toMercator(network.stops[stop])
.geometry.coordinates);
this.history.push(["arriveToStop", stop]);
}
/**
* Transition this course to a state where it is moving to a stop.
* @param {string} stop Next stop for this course.
* @param {number} arrivalTime Planned arrival time to that stop.
* @returns {undefined}
*/
moveToStop(stop, arrivalTime) {
if (!(`${this.currentStop}-${stop}` in network.segments)) {
console.warn(`Course ${this.id} 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.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.
* @returns {number} Speed in meters per millisecond.
*/
computeTheoreticalSpeed() {
if (this.state !== "moving") {
return 0;
}
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;
}
}
const updateData = async courses => {
const dataset = (await axios.get(`${server}/courses`)).data;
// Update or create new courses
for (const [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 (const id of Object.keys(courses)) {
if (!(id in dataset)) {
delete courses[id];
}
}
};
const tick = (courses, time) => {
for (const 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;