|
|
@ -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)) |
|
|
|
{ |
|
|
|
// There is no segment between the two requested stops, jump
|
|
|
|
// directly to the arrival stop
|
|
|
|
arriveAtStop(course, course.arrivalStop); |
|
|
|
} |
|
|
|
else |
|
|
|
constructor(data) |
|
|
|
{ |
|
|
|
updateSpeed(course); |
|
|
|
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; |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
const getCurrentSegment = course => |
|
|
|
{ |
|
|
|
if (course.state === 'stopped') |
|
|
|
get currentSegment() |
|
|
|
{ |
|
|
|
return null; |
|
|
|
if (this.state !== 'moving') |
|
|
|
{ |
|
|
|
return undefined; |
|
|
|
} |
|
|
|
|
|
|
|
return network.segments[`${this.departureStop}-${this.arrivalStop}`]; |
|
|
|
} |
|
|
|
|
|
|
|
return network.segments[`${course.departureStop}-${course.arrivalStop}`]; |
|
|
|
}; |
|
|
|
updateData(data) |
|
|
|
{ |
|
|
|
this.line = data.line; |
|
|
|
this.finalStop = data.finalStop; |
|
|
|
Object.assign(this.passings, data.nextPassings); |
|
|
|
|
|
|
|
const updateSpeed = course => |
|
|
|
{ |
|
|
|
const segment = getCurrentSegment(course); |
|
|
|
const length = segment.points[segment.points.length - 1].distance; |
|
|
|
const now = Date.now(); |
|
|
|
|
|
|
|
const remainingTime = course.arrivalTime - Date.now(); |
|
|
|
const remainingDistance = length - course.traveledDistance; |
|
|
|
// Make sure we’re on the right `stopped`/`moving` state
|
|
|
|
if (this.state === null) |
|
|
|
{ |
|
|
|
let previousStop = null; |
|
|
|
let departureTime = 0; |
|
|
|
|
|
|
|
if (remainingTime <= 0 || remainingDistance <= 0) |
|
|
|
{ |
|
|
|
arriveAtStop(course, course.arrivalStop); |
|
|
|
return; |
|
|
|
} |
|
|
|
let nextStop = null; |
|
|
|
let arrivalTime = Infinity; |
|
|
|
|
|
|
|
course.speed = remainingDistance / remainingTime; |
|
|
|
}; |
|
|
|
for (let [stopId, time] of Object.entries(this.passings)) |
|
|
|
{ |
|
|
|
if (time > now && time < arrivalTime) |
|
|
|
{ |
|
|
|
nextStop = stopId; |
|
|
|
arrivalTime = time; |
|
|
|
} |
|
|
|
|
|
|
|
const updateFromTam = async (courses) => |
|
|
|
{ |
|
|
|
const currentCourses = (await axios.get(`${server}/courses`)).data; |
|
|
|
if (time < now && time > departureTime) |
|
|
|
{ |
|
|
|
previousStop = stopId; |
|
|
|
departureTime = time; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
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; |
|
|
|
if (nextStop === null) |
|
|
|
{ |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
for (let {stopId, arrivalTime: time} of course.nextPassings) |
|
|
|
{ |
|
|
|
if (time > Date.now()) |
|
|
|
if (previousStop === null) |
|
|
|
{ |
|
|
|
// Teleport to the first known stop
|
|
|
|
this.arriveToStop(nextStop); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
nextStop = stopId; |
|
|
|
arrivalTime = time; |
|
|
|
break; |
|
|
|
// Teleport to the first known segment
|
|
|
|
this.arriveToStop(previousStop); |
|
|
|
this.moveToStop(nextStop, arrivalTime); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (nextStop === null) |
|
|
|
else if (this.state === 'moving') |
|
|
|
{ |
|
|
|
continue; |
|
|
|
// 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]; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// Update an existing course
|
|
|
|
if (id in courses) |
|
|
|
else // this.state === 'stopped'
|
|
|
|
{ |
|
|
|
const prev = courses[id]; |
|
|
|
// Try moving to the next stop
|
|
|
|
let nextStop = null; |
|
|
|
let arrivalTime = Infinity; |
|
|
|
|
|
|
|
if (prev.state === 'stopped') |
|
|
|
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; |
|
|
|
} |
|
|
|
} |
|
|
|
else |
|
|
|
|
|
|
|
if (nextStop === null) |
|
|
|
{ |
|
|
|
// 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); |
|
|
|
} |
|
|
|
// This course is finished
|
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
if (nextStop !== this.currentStop) |
|
|
|
{ |
|
|
|
this.moveToStop(nextStop, arrivalTime); |
|
|
|
} |
|
|
|
} |
|
|
|
// Create a new course
|
|
|
|
else |
|
|
|
|
|
|
|
if (this.state === 'moving') |
|
|
|
{ |
|
|
|
courses[id] = { |
|
|
|
id, |
|
|
|
line: course.line, |
|
|
|
finalStop: course.finalStop, |
|
|
|
position: [0, 0], |
|
|
|
angle: 0, |
|
|
|
}; |
|
|
|
|
|
|
|
arriveAtStop(courses[id], nextStop); |
|
|
|
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) |
|
|
|
{ |
|
|
|
course.traveledDistance = length; |
|
|
|
} |
|
|
|
else |
|
|
|
if (this.traveledDistance >= length) |
|
|
|
{ |
|
|
|
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}; |
|
|
|