2021-05-11 19:39:24 +00:00
|
|
|
|
import axios from "axios";
|
|
|
|
|
import turfAlong from "@turf/along";
|
2022-07-09 19:50:53 +00:00
|
|
|
|
import turfBearing from "@turf/bearing";
|
2021-05-11 19:39:24 +00:00
|
|
|
|
import * as turfProjection from "@turf/projection";
|
2021-05-16 10:01:51 +00:00
|
|
|
|
import * as routing from "./routing.js";
|
2021-05-11 19:39:24 +00:00
|
|
|
|
import network from "./network.json";
|
2020-07-18 23:45:36 +00:00
|
|
|
|
|
2020-07-25 16:05:43 +00:00
|
|
|
|
const server = "http://localhost:4321";
|
2020-07-18 23:45:36 +00:00
|
|
|
|
|
2021-05-16 16:34:54 +00:00
|
|
|
|
// Time to stay at each stop (milliseconds)
|
2022-07-05 22:20:21 +00:00
|
|
|
|
const stopTime = 15000;
|
2020-07-27 19:01:21 +00:00
|
|
|
|
|
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;
|
2020-07-27 19:01:21 +00:00
|
|
|
|
|
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;
|
2020-07-27 19:01:21 +00:00
|
|
|
|
|
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;
|
2020-07-27 19:01:21 +00:00
|
|
|
|
|
2021-05-14 22:43:45 +00:00
|
|
|
|
// Normal speed of a vehicle
|
|
|
|
|
const normSpeed = (2 * maxSpeed + minSpeed) / 3;
|
2020-07-27 19:01:21 +00:00
|
|
|
|
|
2022-07-09 19:50:53 +00:00
|
|
|
|
/**
|
|
|
|
|
* GeoJSON feature representing a vehicle course with simulation of the
|
|
|
|
|
* vehicle’s movement along the course.
|
|
|
|
|
*/
|
2020-07-25 16:05:43 +00:00
|
|
|
|
class Course {
|
2021-05-14 22:43:45 +00:00
|
|
|
|
constructor(id) {
|
2022-07-09 19:50:53 +00:00
|
|
|
|
this.type = "Feature";
|
|
|
|
|
|
|
|
|
|
// Current vehicle position (latitude and longitude)
|
|
|
|
|
this.geometry = {};
|
|
|
|
|
this.geometry.type = "Point";
|
|
|
|
|
this.geometry.coordinates = [0, 0];
|
|
|
|
|
|
|
|
|
|
this.properties = {};
|
|
|
|
|
|
2021-05-14 22:43:45 +00:00
|
|
|
|
// Unique identifier of this course
|
2022-07-09 19:50:53 +00:00
|
|
|
|
this.properties.id = id;
|
2021-05-14 22:43:45 +00:00
|
|
|
|
|
|
|
|
|
// Line on which this vehicle operates
|
2022-07-09 19:50:53 +00:00
|
|
|
|
this.properties.line = null;
|
2021-05-14 22:43:45 +00:00
|
|
|
|
|
|
|
|
|
// Line direction of this course
|
2022-07-09 19:50:53 +00:00
|
|
|
|
this.properties.direction = null;
|
2021-05-14 22:43:45 +00:00
|
|
|
|
|
|
|
|
|
// Stop to which this course is headed
|
2022-07-09 19:50:53 +00:00
|
|
|
|
this.properties.finalStop = null;
|
2021-05-14 22:43:45 +00:00
|
|
|
|
|
2021-05-16 16:34:54 +00:00
|
|
|
|
// Previous stops that this course left (stop id/timestamp pairs)
|
2022-07-09 19:50:53 +00:00
|
|
|
|
this.properties.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)
|
2022-07-09 19:50:53 +00:00
|
|
|
|
this.properties.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
|
2022-07-09 19:50:53 +00:00
|
|
|
|
this.properties.departureStop = null;
|
2021-05-14 22:43:45 +00:00
|
|
|
|
|
2021-05-16 16:34:54 +00:00
|
|
|
|
// Time at which the last stop was left or will be left (timestamp)
|
2022-07-09 19:50:53 +00:00
|
|
|
|
this.properties.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
|
2022-07-09 19:50:53 +00:00
|
|
|
|
this.properties.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)
|
2022-07-09 19:50:53 +00:00
|
|
|
|
this.properties.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
|
2022-07-09 19:50:53 +00:00
|
|
|
|
this.properties.segment = null;
|
2021-05-14 22:43:45 +00:00
|
|
|
|
|
2021-05-16 16:34:54 +00:00
|
|
|
|
// Distance already travelled between the two stops (meters)
|
2022-07-09 19:50:53 +00:00
|
|
|
|
this.properties.traveledDistance = 0;
|
2021-05-14 22:43:45 +00:00
|
|
|
|
|
2021-05-16 16:34:54 +00:00
|
|
|
|
// Current vehicle speed (meters per millisecond)
|
2022-07-09 19:50:53 +00:00
|
|
|
|
this.properties.speed = 0;
|
2020-07-24 22:57:47 +00:00
|
|
|
|
|
2021-05-16 16:34:54 +00:00
|
|
|
|
// Current vehicle bearing (clockwise degrees from north)
|
2022-07-09 19:50:53 +00:00
|
|
|
|
this.properties.bearing = 0;
|
2020-07-18 23:45:36 +00:00
|
|
|
|
}
|
|
|
|
|
|
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() {
|
2022-07-09 19:50:53 +00:00
|
|
|
|
const props = this.properties;
|
|
|
|
|
|
|
|
|
|
if (props.departureStop === null || props.arrivalStop === null) {
|
|
|
|
|
props.segment = null;
|
2021-05-14 22:43:45 +00:00
|
|
|
|
return;
|
2020-07-24 17:05:43 +00:00
|
|
|
|
}
|
|
|
|
|
|
2022-07-09 19:50:53 +00:00
|
|
|
|
const name = `${props.departureStop}-${props.arrivalStop}`;
|
2021-05-14 22:43:45 +00:00
|
|
|
|
|
2022-07-09 19:50:53 +00:00
|
|
|
|
if (!(props.departureStop in network.stops)) {
|
|
|
|
|
console.warn(`Unknown stop: ${props.departureStop}`);
|
|
|
|
|
props.segment = null;
|
2021-05-14 22:43:45 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-09 19:50:53 +00:00
|
|
|
|
if (!(props.arrivalStop in network.stops)) {
|
|
|
|
|
console.warn(`Unknown stop: ${props.arrivalStop}`);
|
|
|
|
|
props.segment = null;
|
2021-05-14 22:43:45 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-16 10:01:51 +00:00
|
|
|
|
// Compute a custom route between two stops
|
2022-07-09 19:50:53 +00:00
|
|
|
|
props.segment = routing.findSegment(
|
|
|
|
|
props.departureStop,
|
|
|
|
|
props.arrivalStop,
|
|
|
|
|
);
|
2021-05-16 10:01:51 +00:00
|
|
|
|
|
2022-07-09 19:50:53 +00:00
|
|
|
|
if (props.segment === null) {
|
|
|
|
|
console.warn(`No route from ${props.departureStop} \
|
|
|
|
|
to ${props.arrivalStop}`);
|
2021-05-16 10:01:51 +00:00
|
|
|
|
}
|
2020-07-18 23:45:36 +00:00
|
|
|
|
}
|
|
|
|
|
|
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) {
|
2022-07-09 19:50:53 +00:00
|
|
|
|
const props = this.properties;
|
|
|
|
|
|
|
|
|
|
props.line = data.line;
|
|
|
|
|
props.direction = data.direction;
|
|
|
|
|
props.finalStop = data.finalStopId;
|
2020-07-18 23:45:36 +00:00
|
|
|
|
|
2021-05-14 22:43:45 +00:00
|
|
|
|
const passings = Object.assign(
|
2022-07-09 19:50:53 +00:00
|
|
|
|
Object.fromEntries(props.nextPassings),
|
2021-05-14 22:43:45 +00:00
|
|
|
|
Object.fromEntries(data.passings),
|
|
|
|
|
);
|
2020-07-18 23:45:36 +00:00
|
|
|
|
|
2021-05-14 22:43:45 +00:00
|
|
|
|
// Remove older passings from next passings
|
2022-07-09 19:50:53 +00:00
|
|
|
|
for (let [stop, _] of props.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
|
2022-07-09 19:50:53 +00:00
|
|
|
|
if (props.departureStop !== null) {
|
|
|
|
|
if (props.departureStop in passings) {
|
|
|
|
|
props.departureTime = passings[props.departureStop];
|
|
|
|
|
delete passings[props.departureStop];
|
2020-07-24 17:05:43 +00:00
|
|
|
|
}
|
2021-05-14 22:43:45 +00:00
|
|
|
|
}
|
2020-07-18 23:45:36 +00:00
|
|
|
|
|
2021-05-14 22:43:45 +00:00
|
|
|
|
// Update arrival time
|
2022-07-09 19:50:53 +00:00
|
|
|
|
if (props.arrivalStop !== null) {
|
|
|
|
|
if (props.arrivalStop in passings) {
|
2021-05-14 22:43:45 +00:00
|
|
|
|
// Use announced time if available
|
2022-07-09 19:50:53 +00:00
|
|
|
|
props.arrivalTime = passings[props.arrivalStop];
|
|
|
|
|
delete passings[props.arrivalStop];
|
2020-07-25 16:05:43 +00:00
|
|
|
|
} else {
|
2021-05-14 22:43:45 +00:00
|
|
|
|
// Otherwise, arrive using a normal speed from current position
|
2022-07-09 19:50:53 +00:00
|
|
|
|
const segment = props.segment;
|
|
|
|
|
const distance = segment.properties.length - props.traveledDistance;
|
2021-05-14 22:43:45 +00:00
|
|
|
|
const time = Math.floor(distance / normSpeed);
|
2022-07-09 19:50:53 +00:00
|
|
|
|
props.arrivalTime = Date.now() + time;
|
2020-07-18 23:45:36 +00:00
|
|
|
|
}
|
2021-05-14 22:43:45 +00:00
|
|
|
|
}
|
2020-07-27 19:01:21 +00:00
|
|
|
|
|
2022-07-09 19:50:53 +00:00
|
|
|
|
props.nextPassings = Object.entries(passings).sort(
|
2021-05-14 22:43:45 +00:00
|
|
|
|
([, time1], [, time2]) => time1 - time2
|
|
|
|
|
);
|
|
|
|
|
}
|
2020-07-18 23:45:36 +00:00
|
|
|
|
|
2021-05-14 22:43:45 +00:00
|
|
|
|
/** Update the vehicle state. */
|
|
|
|
|
update() {
|
2022-07-09 19:50:53 +00:00
|
|
|
|
const props = this.properties;
|
2021-05-14 22:43:45 +00:00
|
|
|
|
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
|
2022-07-09 19:50:53 +00:00
|
|
|
|
if (props.departureStop === null) {
|
|
|
|
|
if (props.nextPassings.length > 0) {
|
|
|
|
|
const [stopId, time] = props.nextPassings.shift();
|
|
|
|
|
props.departureStop = stopId;
|
|
|
|
|
props.departureTime = time;
|
2021-05-14 22:43:45 +00:00
|
|
|
|
this.updateSegment();
|
2020-07-18 23:45:36 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2020-07-24 17:05:43 +00:00
|
|
|
|
|
2021-05-14 22:43:45 +00:00
|
|
|
|
// …and the second one as the arrival
|
2022-07-09 19:50:53 +00:00
|
|
|
|
if (props.arrivalStop === null) {
|
|
|
|
|
if (props.nextPassings.length > 0) {
|
|
|
|
|
const [stopId, time] = props.nextPassings.shift();
|
|
|
|
|
props.arrivalStop = stopId;
|
|
|
|
|
props.arrivalTime = time;
|
2021-05-14 22:43:45 +00:00
|
|
|
|
this.updateSegment();
|
|
|
|
|
}
|
2020-07-18 23:45:36 +00:00
|
|
|
|
}
|
2020-07-24 17:05:43 +00:00
|
|
|
|
|
2022-07-09 19:50:53 +00:00
|
|
|
|
if (props.segment !== null) {
|
|
|
|
|
const segment = props.segment;
|
|
|
|
|
const distance = segment.properties.length - props.traveledDistance;
|
|
|
|
|
const duration = props.arrivalTime - stopTime - now;
|
2021-05-14 22:43:45 +00:00
|
|
|
|
|
|
|
|
|
// Arrive to the next stop
|
|
|
|
|
if (distance === 0) {
|
2022-07-09 19:50:53 +00:00
|
|
|
|
props.prevPassings.push([
|
|
|
|
|
props.departureStop,
|
|
|
|
|
props.departureTime
|
|
|
|
|
]);
|
|
|
|
|
props.departureStop = props.arrivalStop;
|
|
|
|
|
props.departureTime = props.arrivalTime;
|
|
|
|
|
|
|
|
|
|
if (props.nextPassings.length > 0) {
|
|
|
|
|
const [stopId, time] = props.nextPassings.shift();
|
|
|
|
|
props.arrivalStop = stopId;
|
|
|
|
|
props.arrivalTime = time;
|
2021-05-14 22:43:45 +00:00
|
|
|
|
} else {
|
2022-07-09 19:50:53 +00:00
|
|
|
|
props.arrivalStop = null;
|
|
|
|
|
props.arrivalTime = 0;
|
2021-05-14 22:43:45 +00:00
|
|
|
|
}
|
2020-07-18 23:45:36 +00:00
|
|
|
|
|
2022-07-09 19:50:53 +00:00
|
|
|
|
props.traveledDistance = 0;
|
2021-05-14 22:43:45 +00:00
|
|
|
|
this.updateSegment();
|
2020-07-18 23:45:36 +00:00
|
|
|
|
}
|
2020-07-23 15:29:35 +00:00
|
|
|
|
|
2022-07-09 19:50:53 +00:00
|
|
|
|
if (props.departureTime > now) {
|
2021-05-14 22:43:45 +00:00
|
|
|
|
// Wait for departure
|
2022-07-09 19:50:53 +00:00
|
|
|
|
props.speed = 0;
|
2021-05-14 22:43:45 +00:00
|
|
|
|
} else {
|
2022-07-09 19:50:53 +00:00
|
|
|
|
if (props.traveledDistance === 0 && props.speed === 0) {
|
2021-05-14 22:43:45 +00:00
|
|
|
|
// We’re late, record the actual departure time
|
2022-07-09 19:50:53 +00:00
|
|
|
|
props.departureTime = now;
|
2021-05-14 22:43:45 +00:00
|
|
|
|
}
|
2020-07-25 14:28:51 +00:00
|
|
|
|
|
2021-05-14 22:43:45 +00:00
|
|
|
|
// Update current speed to arrive on time if possible
|
2022-07-09 19:50:53 +00:00
|
|
|
|
props.speed = Course.computeSpeed(distance, duration);
|
2021-05-14 22:43:45 +00:00
|
|
|
|
}
|
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) {
|
2022-07-09 19:50:53 +00:00
|
|
|
|
const props = this.properties;
|
|
|
|
|
const segment = props.segment;
|
|
|
|
|
|
|
|
|
|
if (props.segment === null) {
|
2020-07-24 22:57:47 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-09 19:50:53 +00:00
|
|
|
|
if (props.speed > 0) {
|
|
|
|
|
props.traveledDistance = Math.min(
|
|
|
|
|
props.traveledDistance + props.speed * time,
|
|
|
|
|
segment.properties.length,
|
2021-05-14 22:43:45 +00:00
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-09 19:50:53 +00:00
|
|
|
|
// Compute angle based on a small step along the segment
|
2021-05-14 22:43:45 +00:00
|
|
|
|
let positionBehind;
|
2022-07-09 19:50:53 +00:00
|
|
|
|
let positionAhead;
|
2020-07-27 19:01:21 +00:00
|
|
|
|
|
2022-07-09 19:50:53 +00:00
|
|
|
|
if (props.traveledDistance < angleStep / 2) {
|
|
|
|
|
positionBehind = props.traveledDistance;
|
|
|
|
|
positionAhead = angleStep;
|
2021-05-14 22:43:45 +00:00
|
|
|
|
} else {
|
2022-07-09 19:50:53 +00:00
|
|
|
|
positionBehind = props.traveledDistance - angleStep / 2;
|
|
|
|
|
positionAhead = props.traveledDistance + angleStep / 2;
|
2020-07-27 19:01:21 +00:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-14 22:43:45 +00:00
|
|
|
|
const positions = [
|
|
|
|
|
positionBehind,
|
2022-07-09 19:50:53 +00:00
|
|
|
|
props.traveledDistance,
|
|
|
|
|
positionAhead,
|
|
|
|
|
].map(distance => turfAlong(
|
|
|
|
|
props.segment,
|
2021-05-14 22:43:45 +00:00
|
|
|
|
distance / 1000
|
2022-07-09 19:50:53 +00:00
|
|
|
|
));
|
2021-05-14 22:43:45 +00:00
|
|
|
|
|
2022-07-09 19:50:53 +00:00
|
|
|
|
this.geometry.coordinates = positions[1].geometry.coordinates;
|
|
|
|
|
props.bearing = turfBearing(positions[0], positions[2]);
|
|
|
|
|
}
|
2021-05-14 22:43:45 +00:00
|
|
|
|
|
2022-07-09 19:50:53 +00:00
|
|
|
|
/** Check if a course is finished. */
|
|
|
|
|
isFinished() {
|
|
|
|
|
const props = this.properties;
|
|
|
|
|
return props.departureStop === props.finalStop;
|
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) {
|
2020-07-27 19:01:21 +00:00
|
|
|
|
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) {
|
2020-07-27 19:01:21 +00:00
|
|
|
|
// Too slow: pause until speed is sufficient
|
2020-07-24 17:05:43 +00:00
|
|
|
|
return 0;
|
|
|
|
|
}
|
2020-07-25 16:05:43 +00:00
|
|
|
|
|
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. */
|
2020-07-25 16:05:43 +00:00
|
|
|
|
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
|
2020-07-25 16:05:43 +00:00
|
|
|
|
for (const [id, data] of Object.entries(dataset)) {
|
|
|
|
|
if (id in courses) {
|
2021-05-14 22:43:45 +00:00
|
|
|
|
courses[id].receiveData(data);
|
2020-07-25 16:05:43 +00:00
|
|
|
|
} 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
|
|
|
|
}
|
|
|
|
|
}
|
2021-05-15 11:10:56 +00:00
|
|
|
|
|
|
|
|
|
// Remove stale courses
|
|
|
|
|
for (const id of Object.keys(courses)) {
|
2022-07-09 19:50:53 +00:00
|
|
|
|
if (courses[id].isFinished()) {
|
2021-05-15 11:10:56 +00:00
|
|
|
|
delete courses[id];
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-07-24 17:05:43 +00:00
|
|
|
|
};
|
|
|
|
|
|
2021-05-11 19:39:24 +00:00
|
|
|
|
export const start = () => {
|
2020-07-23 15:29:35 +00:00
|
|
|
|
const courses = {};
|
|
|
|
|
let lastFrame = null;
|
|
|
|
|
let lastUpdate = null;
|
2020-07-18 23:45:36 +00:00
|
|
|
|
|
2020-07-25 16:05:43 +00:00
|
|
|
|
const update = () => {
|
2020-07-23 15:29:35 +00:00
|
|
|
|
const now = Date.now();
|
|
|
|
|
|
2020-07-25 16:05:43 +00:00
|
|
|
|
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
|
|
|
|
};
|
2020-07-18 23:45:36 +00:00
|
|
|
|
|
2020-07-25 16:05:43 +00:00
|
|
|
|
return { courses, update };
|
2020-07-17 17:17:06 +00:00
|
|
|
|
};
|