tracktracker/src/data/simulation.js

330 lines
9.9 KiB
JavaScript
Raw Normal View History

import axios from "axios";
import turfAlong from "@turf/along";
import * as turfProjection from "@turf/projection";
2021-05-16 10:01:51 +00:00
import * as routing from "./routing.js";
import network from "./network.json";
const server = "http://localhost:4321";
2021-05-16 16:34:54 +00:00
// Time to stay at each stop (milliseconds)
2021-05-14 22:43:45 +00:00
const stopTime = 10000;
2021-05-16 16:34:54 +00:00
// Step used to compute the vehicle bearing (meters)
2021-05-14 22:43:45 +00:00
const angleStep = 10;
2021-05-16 16:34:54 +00:00
// Maximum speed of a vehicle (meters per millisecond)
2021-05-14 22:43:45 +00:00
const maxSpeed = 60 / 3600;
2021-05-16 16:34:54 +00:00
// Minimum speed of a vehicle (meters per millisecond)
2021-05-14 22:43:45 +00:00
const minSpeed = 10 / 3600;
2021-05-14 22:43:45 +00:00
// Normal speed of a vehicle
const normSpeed = (2 * maxSpeed + minSpeed) / 3;
2021-05-14 22:43:45 +00:00
/** Simulate the evolution of a vehicle course in the network. */
class Course {
2021-05-14 22:43:45 +00:00
constructor(id) {
// Unique identifier of this course
this.id = id;
// Line on which this vehicle operates
this.line = null;
// Line direction of this course
this.direction = null;
// Stop to which this course is headed
this.finalStop = null;
2021-05-16 16:34:54 +00:00
// Previous stops that this course left (stop id/timestamp pairs)
this.prevPassings = [];
2021-05-14 22:43:45 +00:00
2021-05-16 16:34:54 +00:00
// Next stops that this course will leave (stop id/timestamp pairs)
this.nextPassings = [];
2020-07-24 17:05:43 +00:00
2021-05-14 22:43:45 +00:00
// Stop that this course just left or will leave
this.departureStop = null;
2021-05-16 16:34:54 +00:00
// Time at which the last stop was left or will be left (timestamp)
this.departureTime = 0;
2020-07-24 17:05:43 +00:00
2021-05-14 22:43:45 +00:00
// Next stop that this course will reach
2020-07-24 17:05:43 +00:00
this.arrivalStop = null;
2021-05-14 22:43:45 +00:00
2021-05-16 16:34:54 +00:00
// Time at which the next stop will be left (timestamp)
2020-07-24 17:05:43 +00:00
this.arrivalTime = 0;
2021-05-14 22:43:45 +00:00
2021-05-16 16:34:54 +00:00
// Route between the current departure and arrival stops
2021-05-14 22:43:45 +00:00
this.segment = null;
2021-05-16 16:34:54 +00:00
// Distance already travelled between the two stops (meters)
2020-07-24 17:05:43 +00:00
this.traveledDistance = 0;
2021-05-14 22:43:45 +00:00
2021-05-16 16:34:54 +00:00
// Current vehicle speed (meters per millisecond)
2020-07-24 17:05:43 +00:00
this.speed = 0;
2021-05-14 22:43:45 +00:00
// Current vehicle latitude and longitude
2020-07-24 17:05:43 +00:00
this.position = [0, 0];
2021-05-16 16:34:54 +00:00
// Current vehicle bearing (clockwise degrees from north)
2021-05-14 22:43:45 +00:00
this.angle = 0;
}
2021-05-16 16:34:54 +00:00
/** Find a route between the current departure and arrival stops. */
2021-05-14 22:43:45 +00:00
updateSegment() {
if (this.departureStop === null || this.arrivalStop === null) {
this.segment = null;
return;
2020-07-24 17:05:43 +00:00
}
2021-05-14 22:43:45 +00:00
const name = `${this.departureStop}-${this.arrivalStop}`;
2021-05-16 10:01:51 +00:00
// Use predefined segment if it exists
2021-05-14 22:43:45 +00:00
if (name in network.segments) {
this.segment = network.segments[name];
return;
}
if (!(this.departureStop in network.stops)) {
console.warn(`Unknown stop: ${this.departureStop}`);
this.segment = null;
return;
}
if (!(this.arrivalStop in network.stops)) {
console.warn(`Unknown stop: ${this.arrivalStop}`);
this.segment = null;
return;
}
2021-05-16 10:01:51 +00:00
// Compute a custom route between two stops
this.segment = routing.findSegment(this.departureStop, this.arrivalStop);
2021-05-16 10:01:51 +00:00
if (this.segment === null) {
2021-05-16 10:01:51 +00:00
console.warn(`No route from ${this.departureStop} \
to ${this.arrivalStop}`);
}
}
2021-05-16 16:34:54 +00:00
/** Merge passings data received from the server. */
2021-05-14 22:43:45 +00:00
receiveData(data) {
2020-07-24 17:05:43 +00:00
this.line = data.line;
2021-05-11 15:20:12 +00:00
this.direction = data.direction;
this.finalStop = data.finalStopId;
2021-05-14 22:43:45 +00:00
const passings = Object.assign(
Object.fromEntries(this.nextPassings),
Object.fromEntries(data.passings),
);
2021-05-14 22:43:45 +00:00
// Remove older passings from next passings
2021-05-16 16:34:54 +00:00
for (let [stop, _] of this.prevPassings) {
2021-05-14 22:43:45 +00:00
delete passings[stop];
}
2020-07-17 17:17:06 +00:00
2021-05-14 22:43:45 +00:00
// Update departure time if still announced
if (this.departureStop !== null) {
if (this.departureStop in passings) {
this.departureTime = passings[this.departureStop];
delete passings[this.departureStop];
2020-07-24 17:05:43 +00:00
}
2021-05-14 22:43:45 +00:00
}
2021-05-14 22:43:45 +00:00
// Update arrival time
if (this.arrivalStop !== null) {
if (this.arrivalStop in passings) {
// Use announced time if available
this.arrivalTime = passings[this.arrivalStop];
delete passings[this.arrivalStop];
} else {
2021-05-14 22:43:45 +00:00
// Otherwise, arrive using a normal speed from current position
const segment = this.segment;
const distance = segment.properties.length - this.traveledDistance;
const time = Math.floor(distance / normSpeed);
this.arrivalTime = Date.now() + time;
}
2021-05-14 22:43:45 +00:00
}
2021-05-14 22:43:45 +00:00
this.nextPassings = Object.entries(passings).sort(
([, time1], [, time2]) => time1 - time2
);
}
2021-05-14 22:43:45 +00:00
/** Update the vehicle state. */
update() {
const now = Date.now();
2020-07-24 17:05:43 +00:00
2021-05-14 22:43:45 +00:00
// When initializing, use the first available passing as start
if (this.departureStop === null) {
if (this.nextPassings.length > 0) {
const [stopId, time] = this.nextPassings.shift();
this.departureStop = stopId;
this.departureTime = time;
this.updateSegment();
}
}
2020-07-24 17:05:43 +00:00
2021-05-14 22:43:45 +00:00
// …and the second one as the arrival
if (this.arrivalStop === null) {
if (this.nextPassings.length > 0) {
const [stopId, time] = this.nextPassings.shift();
this.arrivalStop = stopId;
this.arrivalTime = time;
this.updateSegment();
}
}
2020-07-24 17:05:43 +00:00
2021-05-14 22:43:45 +00:00
if (this.segment !== null) {
const segment = this.segment;
const distance = segment.properties.length - this.traveledDistance;
const duration = this.arrivalTime - stopTime - now;
// Arrive to the next stop
if (distance === 0) {
this.prevPassings.push([this.departureStop, this.departureTime]);
this.departureStop = this.arrivalStop;
this.departureTime = this.arrivalTime;
if (this.nextPassings.length > 0) {
const [stopId, time] = this.nextPassings.shift();
this.arrivalStop = stopId;
this.arrivalTime = time;
} else {
this.arrivalStop = null;
this.arrivalTime = 0;
}
2021-05-14 22:43:45 +00:00
this.traveledDistance = 0;
this.updateSegment();
}
2020-07-23 15:29:35 +00:00
2021-05-14 22:43:45 +00:00
if (this.departureTime > now) {
// Wait for departure
this.speed = 0;
} else {
if (this.traveledDistance === 0 && this.speed === 0) {
// Were late, record the actual departure time
this.departureTime = now;
}
2021-05-14 22:43:45 +00:00
// Update current speed to arrive on time if possible
this.speed = Course.computeSpeed(distance, duration);
}
2020-07-17 17:17:06 +00:00
}
2020-07-24 17:05:43 +00:00
2021-05-14 22:43:45 +00:00
return true;
2020-07-24 17:05:43 +00:00
}
2021-05-14 22:43:45 +00:00
/** Integrate the current vehicle speed and update distance. */
move(time) {
if (this.segment === null) {
return;
}
2021-05-14 22:43:45 +00:00
if (this.speed > 0) {
this.traveledDistance = Math.min(
this.traveledDistance + this.speed * time,
this.segment.properties.length,
);
}
// Compute updated position and angle based on a small step
let positionBehind;
let positionInFront;
2021-05-14 22:43:45 +00:00
if (this.traveledDistance < angleStep / 2) {
positionBehind = this.traveledDistance;
positionInFront = angleStep;
} else {
positionBehind = this.traveledDistance - angleStep / 2;
positionInFront = this.traveledDistance + angleStep / 2;
}
2021-05-14 22:43:45 +00:00
const positions = [
positionBehind,
this.traveledDistance,
positionInFront,
].map(distance => turfProjection.toMercator(turfAlong(
this.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];
2020-07-24 17:05:43 +00:00
}
2021-05-16 16:34:54 +00:00
/**
* Compute the optimal speed to arrive on time.
* @param {number} distance Distance to cover (meters)
* @param {number} duration Remaining time (seconds)
* @return {number} Optimal speed (meters per second)
*/
2020-07-27 19:21:07 +00:00
static computeSpeed(distance, duration) {
if (duration <= 0) {
// Late: go to maximum speed
2021-05-14 22:43:45 +00:00
return maxSpeed;
2020-07-24 17:05:43 +00:00
}
2021-05-14 22:43:45 +00:00
const speed = distance / duration;
if (speed < minSpeed) {
// Too slow: pause until speed is sufficient
2020-07-24 17:05:43 +00:00
return 0;
}
2021-05-14 22:43:45 +00:00
return Math.min(maxSpeed, speed);
2020-07-24 17:05:43 +00:00
}
}
2021-05-16 16:34:54 +00:00
/** Fetch passing data from the server and update simulation. */
const updateData = async courses => {
2020-07-24 17:05:43 +00:00
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) {
2021-05-14 22:43:45 +00:00
courses[id].receiveData(data);
} else {
2021-05-14 22:43:45 +00:00
const newCourse = new Course(data.id);
newCourse.receiveData(data);
courses[id] = newCourse;
2020-07-24 17:05:43 +00:00
}
}
// Remove stale courses
for (const id of Object.keys(courses)) {
if (courses[id].departureStop === courses[id].finalStop) {
delete courses[id];
}
}
2020-07-24 17:05:43 +00:00
};
export const start = () => {
2020-07-23 15:29:35 +00:00
const courses = {};
let lastFrame = null;
let lastUpdate = null;
const update = () => {
2020-07-23 15:29:35 +00:00
const now = Date.now();
if (lastUpdate === null || lastUpdate + 5000 <= now) {
2020-07-23 15:29:35 +00:00
lastUpdate = now;
2020-07-24 17:05:43 +00:00
updateData(courses);
2020-07-23 15:29:35 +00:00
}
const time = lastFrame === null ? 0 : now - lastFrame;
lastFrame = now;
2021-05-16 16:34:54 +00:00
for (const course of Object.values(courses)) {
course.update();
course.move(time);
}
2020-07-23 15:29:35 +00:00
};
return { courses, update };
2020-07-17 17:17:06 +00:00
};