Jämför commits
No commits in common. "6d76d874a1f656921bdd00a15793131340ca2851" and "d0c2fb3a63e49c81772119385322b2b4f026c615" have entirely different histories.
6d76d874a1
...
d0c2fb3a63
Filskillnaden har hållits tillbaka eftersom den är för stor
Load Diff
44
package.json
44
package.json
|
@ -1,34 +1,38 @@
|
||||||
{
|
{
|
||||||
"name": "tamview",
|
"name": "tamview",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"type": "module",
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"back": "node src/back",
|
"back": "node src/back",
|
||||||
"front:dev": "vite src/front",
|
"front:dev": "parcel serve src/front/index.html",
|
||||||
"front:prod": "vite build src/front",
|
"front:prod": "npx parcel build src/front/index.html --no-source-maps --no-autoinstall",
|
||||||
"lint": "eslint ."
|
"lint": "eslint ."
|
||||||
},
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "Mattéo Delabre",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@turf/along": "^6.3.0",
|
"@turf/along": "^6.0.1",
|
||||||
"@turf/helpers": "^6.3.0",
|
"@turf/helpers": "^6.1.4",
|
||||||
"@turf/length": "^6.3.0",
|
"@turf/length": "^6.0.2",
|
||||||
"@turf/projection": "^6.3.0",
|
"@turf/projection": "^6.0.1",
|
||||||
"@turf/turf": "^6.3.0",
|
"@turf/turf": "^5.1.6",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.19.2",
|
||||||
"color": "^3.1.3",
|
"color": "^3.1.2",
|
||||||
"csv-parse": "^4.15.4",
|
"csv-parse": "^4.8.3",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"ol": "^6.5.0",
|
"ol": "^6.1.1",
|
||||||
"unzip-stream": "^0.3.1",
|
"unzip-stream": "^0.3.0"
|
||||||
"vue": "^3.0.5"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^1.2.2",
|
"eslint": "^6.8.0",
|
||||||
"@vue/compiler-sfc": "^3.0.5",
|
"eslint-config-eslint": "^6.0.0",
|
||||||
"eslint": "^7.26.0",
|
"eslint-plugin-jsdoc": "^30.0.3",
|
||||||
"eslint-config-eslint": "^7.0.0",
|
|
||||||
"eslint-plugin-jsdoc": "^34.0.1",
|
|
||||||
"eslint-plugin-node": "^11.1.0",
|
"eslint-plugin-node": "^11.1.0",
|
||||||
"vite": "^2.3.0"
|
"parcel-bundler": "^1.12.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,10 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
import * as courses from '../src/tam/courses.js';
|
const courses = require('../src/tam/courses');
|
||||||
import {displayTime} from '../src/util.js';
|
const network = require('../src/tam/network.json');
|
||||||
import process from 'process';
|
const {displayTime} = require('../src/util');
|
||||||
import path from 'path';
|
const process = require('process');
|
||||||
import { readFile } from 'fs/promises';
|
const path = require('path');
|
||||||
|
|
||||||
const network = JSON.parse(await readFile(
|
|
||||||
new URL('../src/tam/network.json', import.meta.url)
|
|
||||||
));
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert stop ID to human-readable stop name.
|
* Convert stop ID to human-readable stop name.
|
|
@ -0,0 +1,29 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const network = require('../src/tam/network');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
(async () =>
|
||||||
|
{
|
||||||
|
const lines = ['1', '2', '3', '4'];
|
||||||
|
const data = await network.fetch(lines);
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(__dirname, '../src/tam/network.json'),
|
||||||
|
JSON.stringify(
|
||||||
|
data,
|
||||||
|
(_, value) =>
|
||||||
|
{
|
||||||
|
if (value instanceof Set)
|
||||||
|
{
|
||||||
|
// Convert sets to arrays for JSON representation
|
||||||
|
return Array.from(value.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
4
|
||||||
|
)
|
||||||
|
);
|
||||||
|
})();
|
|
@ -1,25 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
import * as network from '../src/tam/network.js';
|
|
||||||
import fs from 'fs/promises';
|
|
||||||
|
|
||||||
const lines = ['1', '2', '3', '4'];
|
|
||||||
const data = await network.fetch(lines);
|
|
||||||
|
|
||||||
await fs.writeFile(
|
|
||||||
new URL("../src/tam/network.json", import.meta.url),
|
|
||||||
JSON.stringify(
|
|
||||||
data,
|
|
||||||
(_, value) =>
|
|
||||||
{
|
|
||||||
if (value instanceof Set)
|
|
||||||
{
|
|
||||||
// Convert sets to arrays for JSON representation
|
|
||||||
return Array.from(value.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
},
|
|
||||||
4
|
|
||||||
)
|
|
||||||
);
|
|
|
@ -1,10 +1,10 @@
|
||||||
import express from "express";
|
const express = require("express");
|
||||||
import * as courses from "../tam/courses.js";
|
const courses = require("../tam/courses");
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const port = 4321;
|
const port = 4321;
|
||||||
|
|
||||||
app.get("/courses", async (_, res) => {
|
app.get("/courses", async(req, res) => {
|
||||||
res.header("Access-Control-Allow-Origin", "*");
|
res.header("Access-Control-Allow-Origin", "*");
|
||||||
return res.json(await courses.fetch("realtime"));
|
return res.json(await courses.fetch("realtime"));
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
<template>
|
|
||||||
<div>Hello {{ name }}!</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
name: "Vue",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
|
@ -18,8 +18,8 @@
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
|
|
||||||
#panel {
|
#informations {
|
||||||
flex: 0 0 400px;
|
flex: 0 0 600px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
@ -31,8 +31,8 @@
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<aside id="panel"></aside>
|
<div id="informations"></div>
|
||||||
<div id="map"></div>
|
<div id="map"></div>
|
||||||
<script type="module" src="/index.js"></script>
|
<script src="index.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,32 +1,21 @@
|
||||||
import network from "../tam/network.json";
|
// eslint-disable-next-line node/no-extraneous-require
|
||||||
import * as simulation from "../tam/simulation.js";
|
require("regenerator-runtime/runtime");
|
||||||
import * as map from "./map/index.js";
|
|
||||||
|
const network = require("../tam/network.json");
|
||||||
|
const simulation = require("../tam/simulation");
|
||||||
|
const map = require("./map/index.js");
|
||||||
|
|
||||||
// Run courses simulation
|
// Run courses simulation
|
||||||
const coursesSimulation = simulation.start();
|
const coursesSimulation = simulation.start();
|
||||||
|
const informations = document.querySelector("#informations");
|
||||||
let courseId = null;
|
let courseId = null;
|
||||||
|
|
||||||
// Create display panel
|
|
||||||
const panel = document.querySelector("#panel");
|
|
||||||
|
|
||||||
const displayTime = date => [
|
const displayTime = date => [
|
||||||
date.getHours(),
|
date.getHours(),
|
||||||
date.getMinutes(),
|
date.getMinutes(),
|
||||||
date.getSeconds()
|
date.getSeconds()
|
||||||
].map(number => number.toString().padStart(2, "0")).join(":");
|
].map(number => number.toString().padStart(2, "0")).join(":");
|
||||||
|
|
||||||
const timeToHTML = time => {
|
|
||||||
const delta = Math.ceil((time - Date.now()) / 1000);
|
|
||||||
|
|
||||||
if (delta <= 0) {
|
|
||||||
return `Imminent`;
|
|
||||||
} else if (delta < 60) {
|
|
||||||
return `${delta} s`;
|
|
||||||
} else {
|
|
||||||
return `${Math.floor(delta / 60)} min ${delta % 60} s`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
let html = `
|
let html = `
|
||||||
<dl>
|
<dl>
|
||||||
|
@ -38,6 +27,8 @@ setInterval(() => {
|
||||||
if (courseId !== null && courseId in coursesSimulation.courses) {
|
if (courseId !== null && courseId in coursesSimulation.courses) {
|
||||||
const course = coursesSimulation.courses[courseId];
|
const course = coursesSimulation.courses[courseId];
|
||||||
|
|
||||||
|
const timeToHTML = time => Math.ceil((time - Date.now()) / 1000);
|
||||||
|
|
||||||
const stopToHTML = stopId => stopId in network.stops ?
|
const stopToHTML = stopId => stopId in network.stops ?
|
||||||
network.stops[stopId].properties.name :
|
network.stops[stopId].properties.name :
|
||||||
'<em>Arrêt inconnu</em>';
|
'<em>Arrêt inconnu</em>';
|
||||||
|
@ -49,17 +40,6 @@ setInterval(() => {
|
||||||
</tr>
|
</tr>
|
||||||
`).join("\n");
|
`).join("\n");
|
||||||
|
|
||||||
const state = (
|
|
||||||
course.traveledDistance === 0 && course.speed === 0
|
|
||||||
? "stopped" : "moving"
|
|
||||||
);
|
|
||||||
|
|
||||||
let prevPassings = course.prevPassings;
|
|
||||||
|
|
||||||
if (state === "moving") {
|
|
||||||
prevPassings = prevPassings.concat([[course.departureStop, course.departureTime]]);
|
|
||||||
}
|
|
||||||
|
|
||||||
html += `
|
html += `
|
||||||
<dl>
|
<dl>
|
||||||
<dt>ID</dt>
|
<dt>ID</dt>
|
||||||
|
@ -72,14 +52,14 @@ setInterval(() => {
|
||||||
<dd>${stopToHTML(course.finalStop)}</dd>
|
<dd>${stopToHTML(course.finalStop)}</dd>
|
||||||
|
|
||||||
<dt>État</dt>
|
<dt>État</dt>
|
||||||
<dd>${state === "moving"
|
<dd>${course.state === "moving"
|
||||||
? `Entre ${stopToHTML(course.departureStop)}
|
? `Entre ${stopToHTML(course.departureStop)}
|
||||||
et ${stopToHTML(course.arrivalStop)}`
|
et ${stopToHTML(course.arrivalStop)}`
|
||||||
: `À l’arrêt ${stopToHTML(course.departureStop)}`}</dd>
|
: `À l’arrêt ${stopToHTML(course.currentStop)}`}</dd>
|
||||||
|
|
||||||
${state === "moving" ? `
|
${course.state === "moving" ? `
|
||||||
<dt>Arrivée dans</dt>
|
<dt>Arrivée dans</dt>
|
||||||
<dd>${timeToHTML(course.arrivalTime - 10000)}</dd>
|
<dd>${timeToHTML(course.arrivalTime)} s</dd>
|
||||||
|
|
||||||
<dt>Distance parcourue</dt>
|
<dt>Distance parcourue</dt>
|
||||||
<dd>${Math.ceil(course.traveledDistance)} m</dd>
|
<dd>${Math.ceil(course.traveledDistance)} m</dd>
|
||||||
|
@ -88,19 +68,19 @@ setInterval(() => {
|
||||||
<dd>${Math.ceil(course.speed * 3600)} km/h</dd>
|
<dd>${Math.ceil(course.speed * 3600)} km/h</dd>
|
||||||
` : `
|
` : `
|
||||||
<dt>Départ dans</dt>
|
<dt>Départ dans</dt>
|
||||||
<dd>${timeToHTML(course.departureTime)}</dd>
|
<dd>${timeToHTML(course.departureTime)} s</dd>
|
||||||
`}
|
`}
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
<h2>Arrêts précédents</h2>
|
<h2>Arrêts précédents</h2>
|
||||||
<table>${passingsToHTML(prevPassings)}</table>
|
<table>${passingsToHTML(course.prevPassings)}</table>
|
||||||
|
|
||||||
<h2>Arrêts suivants</h2>
|
<h2>Arrêts suivants</h2>
|
||||||
<table>${passingsToHTML(course.nextPassings)}</table>
|
<table>${passingsToHTML(course.nextPassings)}</table>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
panel.innerHTML = html;
|
informations.innerHTML = html;
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
// Create the network and courses map
|
// Create the network and courses map
|
||||||
|
|
|
@ -1,24 +1,34 @@
|
||||||
import color from "color";
|
const colorModule = require("color");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Turn the main color of a line into a color suitable for using as a border.
|
* Turn the main color of a line into a color suitable for using as a border.
|
||||||
* @param {string} mainColor Original color.
|
* @param {string} mainColor Original color.
|
||||||
* @returns {string} Hexadecimal representation of the border color.
|
* @returns {string} Hexadecimal representation of the border color.
|
||||||
*/
|
*/
|
||||||
export const makeBorderColor = mainColor => {
|
const makeBorderColor = mainColor => {
|
||||||
return color(mainColor).darken(0.2).hex();
|
const hsl = colorModule(mainColor).hsl();
|
||||||
|
|
||||||
|
hsl.color = Math.max(0, hsl.color[2] -= 20);
|
||||||
|
return hsl.hex();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.makeBorderColor = makeBorderColor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Turn the main color of a line into a color suitable for using as a border.
|
* Turn the main color of a line into a color suitable for using as a border.
|
||||||
* @param {string} mainColor Original color.
|
* @param {string} mainColor Original color.
|
||||||
* @returns {string} Hexadecimal representation of the border color.
|
* @returns {string} Hexadecimal representation of the border color.
|
||||||
*/
|
*/
|
||||||
export const makeCourseColor = mainColor => {
|
const makeCourseColor = mainColor => {
|
||||||
return color(mainColor).lighten(0.2).hex();
|
const hsl = colorModule(mainColor).hsl();
|
||||||
|
|
||||||
|
hsl.color = Math.max(0, hsl.color[2] += 10);
|
||||||
|
return hsl.hex();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sizes = {
|
exports.makeCourseColor = makeCourseColor;
|
||||||
|
|
||||||
|
const sizes = {
|
||||||
segmentOuter: 8,
|
segmentOuter: 8,
|
||||||
segmentInner: 6,
|
segmentInner: 6,
|
||||||
stopRadius: 6,
|
stopRadius: 6,
|
||||||
|
@ -28,3 +38,5 @@ export const sizes = {
|
||||||
courseBorder: 10,
|
courseBorder: 10,
|
||||||
courseInnerBorder: 7
|
courseInnerBorder: 7
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.sizes = sizes;
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import "ol/ol.css";
|
require("ol/ol.css");
|
||||||
|
|
||||||
import { Map, View } from "ol";
|
const { Map, View } = require("ol");
|
||||||
import { getVectorContext } from "ol/render";
|
const { getVectorContext } = require("ol/render");
|
||||||
import Point from "ol/geom/Point";
|
const Point = require("ol/geom/Point").default;
|
||||||
import * as proj from "ol/proj";
|
const proj = require("ol/proj");
|
||||||
import { Style, Icon } from "ol/style";
|
const { Style, Icon } = require("ol/style");
|
||||||
import * as tilesLayers from "./tiles";
|
const tilesLayers = require("./tiles");
|
||||||
import * as networkLayers from "./network";
|
const networkLayers = require("./network");
|
||||||
import { sizes, makeBorderColor, makeCourseColor } from "./common";
|
const { sizes, makeBorderColor, makeCourseColor } = require("./common");
|
||||||
import network from "../../tam/network.json";
|
const network = require("../../tam/network.json");
|
||||||
|
|
||||||
const courseStyles = {};
|
const courseStyles = {};
|
||||||
|
|
||||||
|
@ -58,7 +58,7 @@ const getCourseStyle = lineColor => {
|
||||||
return courseStyles[lineColor];
|
return courseStyles[lineColor];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const create = (target, coursesSimulation, onClick) => {
|
const create = (target, coursesSimulation, onClick) => {
|
||||||
const view = new View({
|
const view = new View({
|
||||||
center: proj.fromLonLat([3.88, 43.605]),
|
center: proj.fromLonLat([3.88, 43.605]),
|
||||||
zoom: 14,
|
zoom: 14,
|
||||||
|
@ -138,3 +138,5 @@ export const create = (target, coursesSimulation, onClick) => {
|
||||||
|
|
||||||
return map;
|
return map;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.create = create;
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import network from "../../tam/network.json";
|
const network = require("../../tam/network.json");
|
||||||
import { makeBorderColor, sizes } from "./common";
|
const { makeBorderColor, sizes } = require("./common");
|
||||||
|
|
||||||
import GeoJSON from "ol/format/GeoJSON";
|
const GeoJSON = require("ol/format/GeoJSON").default;
|
||||||
import VectorLayer from "ol/layer/Vector";
|
const VectorLayer = require("ol/layer/Vector").default;
|
||||||
import VectorSource from "ol/source/Vector";
|
const VectorSource = require("ol/source/Vector").default;
|
||||||
|
|
||||||
import { Style, Fill, Stroke, Circle } from "ol/style";
|
const { Style, Fill, Stroke, Circle } = require("ol/style");
|
||||||
|
|
||||||
const geojsonReader = new GeoJSON({ featureProjection: "EPSG:3857" });
|
const geojsonReader = new GeoJSON({ featureProjection: "EPSG:3857" });
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@ const lineFeaturesOrder = (feature1, feature2) => {
|
||||||
* Create the list of layers for displaying the transit network.
|
* Create the list of layers for displaying the transit network.
|
||||||
* @returns {Array.<Layer>} List of map layers.
|
* @returns {Array.<Layer>} List of map layers.
|
||||||
*/
|
*/
|
||||||
export const getLayers = () => {
|
const getLayers = () => {
|
||||||
const segmentsSource = new VectorSource();
|
const segmentsSource = new VectorSource();
|
||||||
const stopsSource = new VectorSource();
|
const stopsSource = new VectorSource();
|
||||||
|
|
||||||
|
@ -128,3 +128,5 @@ export const getLayers = () => {
|
||||||
|
|
||||||
return [segmentsBorderLayer, segmentsInnerLayer, stopsLayer];
|
return [segmentsBorderLayer, segmentsInnerLayer, stopsLayer];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.getLayers = getLayers;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import TileLayer from "ol/layer/Tile";
|
const TileLayer = require("ol/layer/Tile").default;
|
||||||
import XYZSource from "ol/source/XYZ";
|
const XYZSource = require("ol/source/XYZ").default;
|
||||||
|
|
||||||
const mapboxToken = "pk.eyJ1IjoibWF0dGVvZGVsYWJyZSIsImEiOiJja2NxaTUyMmUwcmFhMn\
|
const mapboxToken = "pk.eyJ1IjoibWF0dGVvZGVsYWJyZSIsImEiOiJja2NxaTUyMmUwcmFhMn\
|
||||||
h0NmFsdzQ3emxqIn0.cyxF0h36emIMTk3cc4VqUw";
|
h0NmFsdzQ3emxqIn0.cyxF0h36emIMTk3cc4VqUw";
|
||||||
|
@ -8,7 +8,7 @@ h0NmFsdzQ3emxqIn0.cyxF0h36emIMTk3cc4VqUw";
|
||||||
* Create the list of layers for displaying the background map.
|
* Create the list of layers for displaying the background map.
|
||||||
* @returns {Array.<Layer>} List of map layers.
|
* @returns {Array.<Layer>} List of map layers.
|
||||||
*/
|
*/
|
||||||
export const getLayers = () => {
|
const getLayers = () => {
|
||||||
const backgroundSource = new XYZSource({
|
const backgroundSource = new XYZSource({
|
||||||
url: `https://api.mapbox.com/${[
|
url: `https://api.mapbox.com/${[
|
||||||
"styles", "v1", "mapbox", "streets-v11",
|
"styles", "v1", "mapbox", "streets-v11",
|
||||||
|
@ -21,3 +21,5 @@ export const getLayers = () => {
|
||||||
source: backgroundSource
|
source: backgroundSource
|
||||||
})];
|
})];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.getLayers = getLayers;
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
const root = path.join(
|
|
||||||
path.dirname(new URL(import.meta.url).pathname),
|
|
||||||
"..", ".."
|
|
||||||
);
|
|
||||||
|
|
||||||
export default {
|
|
||||||
server: {
|
|
||||||
fsServe: {
|
|
||||||
root
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -5,12 +5,8 @@
|
||||||
* from the official endpoints.
|
* from the official endpoints.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as tam from "./sources/tam.js";
|
const tam = require("./sources/tam");
|
||||||
import { readFile } from 'fs/promises';
|
const network = require("./network.json");
|
||||||
|
|
||||||
const network = JSON.parse(await readFile(
|
|
||||||
new URL('./network.json', import.meta.url)
|
|
||||||
));
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Information about the course of a vehicle.
|
* Information about the course of a vehicle.
|
||||||
|
@ -44,13 +40,12 @@ const parseTime = (time, reference) =>
|
||||||
/**
|
/**
|
||||||
* Fetch information about courses in the TaM network.
|
* Fetch information about courses in the TaM network.
|
||||||
*
|
*
|
||||||
* @async
|
|
||||||
* @param {string} kind Pass 'realtime' to get real-time information,
|
* @param {string} kind Pass 'realtime' to get real-time information,
|
||||||
* or 'theoretical' to get planned courses for the day.
|
* or 'theoretical' to get planned courses for the day.
|
||||||
* @returns {Object.<string,Course>} Mapping from active course IDs to
|
* @returns {Object.<string,Course>} Mapping from active course IDs to
|
||||||
* information about each course.
|
* information about each course.
|
||||||
*/
|
*/
|
||||||
export const fetch = async (kind = 'realtime') => {
|
const fetch = async (kind = 'realtime') => {
|
||||||
const courses = {};
|
const courses = {};
|
||||||
const passings = (
|
const passings = (
|
||||||
kind === 'realtime'
|
kind === 'realtime'
|
||||||
|
@ -127,9 +122,11 @@ export const fetch = async (kind = 'realtime') => {
|
||||||
if (course.finalStopId === undefined) {
|
if (course.finalStopId === undefined) {
|
||||||
course.finalStopId = lastPassing[0];
|
course.finalStopId = lastPassing[0];
|
||||||
} else if (course.finalStopId !== lastPassing[0]) {
|
} else if (course.finalStopId !== lastPassing[0]) {
|
||||||
course.passings.push([course.finalStopId, lastPassing[1] + 60000]);
|
course.passings.push([course.finalStopId, lastPassing[1] + 30000]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return courses;
|
return courses;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.fetch = fetch;
|
||||||
|
|
|
@ -11,10 +11,10 @@
|
||||||
* the `script/update-network` script.
|
* the `script/update-network` script.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as turfHelpers from "@turf/helpers";
|
const turfHelpers = require("@turf/helpers");
|
||||||
import turfLength from "@turf/length";
|
const turfLength = require("@turf/length").default;
|
||||||
import * as util from "../util.js";
|
const util = require("../util");
|
||||||
import * as osm from "./sources/osm.js";
|
const osm = require("./sources/osm");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch stops and lines of the network.
|
* Fetch stops and lines of the network.
|
||||||
|
@ -22,7 +22,7 @@ import * as osm from "./sources/osm.js";
|
||||||
* @returns {{stops: Object, lines: Object, segments: Object}} Set of stops,
|
* @returns {{stops: Object, lines: Object, segments: Object}} Set of stops,
|
||||||
* segments and lines.
|
* segments and lines.
|
||||||
*/
|
*/
|
||||||
export const fetch = async lineRefs => {
|
const fetch = async lineRefs => {
|
||||||
// Retrieve routes, ways and stops from OpenStreetMap
|
// Retrieve routes, ways and stops from OpenStreetMap
|
||||||
const rawData = await osm.runQuery(`[out:json];
|
const rawData = await osm.runQuery(`[out:json];
|
||||||
|
|
||||||
|
@ -250,3 +250,5 @@ different sequence of nodes in two or more lines.`);
|
||||||
|
|
||||||
return { stops, lines, segments };
|
return { stops, lines, segments };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.fetch = fetch;
|
||||||
|
|
|
@ -3405,29 +3405,7 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"43161": {
|
"43159": {
|
||||||
"type": "Feature",
|
|
||||||
"properties": {
|
|
||||||
"name": "Cougourlude",
|
|
||||||
"routes": [
|
|
||||||
[
|
|
||||||
"3",
|
|
||||||
2
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"successors": [
|
|
||||||
"43163"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"geometry": {
|
|
||||||
"type": "Point",
|
|
||||||
"coordinates": [
|
|
||||||
3.9139627,
|
|
||||||
43.5713482
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"43163": {
|
|
||||||
"type": "Feature",
|
"type": "Feature",
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": "Lattes Centre",
|
"name": "Lattes Centre",
|
||||||
|
@ -3447,6 +3425,28 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"43161": {
|
||||||
|
"type": "Feature",
|
||||||
|
"properties": {
|
||||||
|
"name": "Cougourlude",
|
||||||
|
"routes": [
|
||||||
|
[
|
||||||
|
"3",
|
||||||
|
2
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"successors": [
|
||||||
|
"43159"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"geometry": {
|
||||||
|
"type": "Point",
|
||||||
|
"coordinates": [
|
||||||
|
3.9139627,
|
||||||
|
43.5713482
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"43201": {
|
"43201": {
|
||||||
"type": "Feature",
|
"type": "Feature",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -22712,7 +22712,7 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"43161-43163": {
|
"43161-43159": {
|
||||||
"type": "Feature",
|
"type": "Feature",
|
||||||
"properties": {
|
"properties": {
|
||||||
"routes": [
|
"routes": [
|
||||||
|
|
|
@ -1,275 +1,277 @@
|
||||||
import axios from "axios";
|
const axios = require("axios");
|
||||||
import turfAlong from "@turf/along";
|
const turfAlong = require("@turf/along").default;
|
||||||
import turfLength from "@turf/length";
|
const turfProjection = require("@turf/projection");
|
||||||
import * as turfHelpers from "@turf/helpers";
|
const network = require("./network.json");
|
||||||
import * as turfProjection from "@turf/projection";
|
|
||||||
import network from "./network.json";
|
|
||||||
|
|
||||||
const server = "http://localhost:4321";
|
const server = "http://localhost:4321";
|
||||||
|
|
||||||
// Number of milliseconds to stay at each stop
|
const findRoute = (from, to) => {
|
||||||
const stopTime = 10000;
|
const queue = [[from, []]];
|
||||||
|
|
||||||
// Step used to compute the vehicle angle in meters
|
while (queue.length) {
|
||||||
const angleStep = 10;
|
const [head, path] = queue.shift();
|
||||||
|
|
||||||
// Maximum speed of a vehicle
|
for (const successor of network.stops[head].properties.successors) {
|
||||||
const maxSpeed = 60 / 3600;
|
if (successor === to) {
|
||||||
|
return path.concat([head, successor]);
|
||||||
|
}
|
||||||
|
|
||||||
// Minimum speed of a vehicle
|
if (!path.includes(successor)) {
|
||||||
const minSpeed = 10 / 3600;
|
queue.push([successor, path.concat([head])]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Normal speed of a vehicle
|
return null;
|
||||||
const normSpeed = (2 * maxSpeed + minSpeed) / 3;
|
};
|
||||||
|
|
||||||
/** Simulate the evolution of a vehicle course in the network. */
|
|
||||||
class Course {
|
class Course {
|
||||||
constructor(id) {
|
constructor(data) {
|
||||||
// Unique identifier of this course
|
this.id = data.id;
|
||||||
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;
|
|
||||||
|
|
||||||
// Previous stops that this course left (with timestamps)
|
|
||||||
this.prevPassings = [];
|
this.prevPassings = [];
|
||||||
|
|
||||||
// Next stops that this course will leave (with timestamps)
|
|
||||||
this.nextPassings = [];
|
this.nextPassings = [];
|
||||||
|
this.state = null;
|
||||||
|
|
||||||
// Stop that this course just left or will leave
|
// Attributes for the `stopped` state
|
||||||
this.departureStop = null;
|
this.currentStop = null;
|
||||||
|
|
||||||
// Time at which the last stop was left or will be left
|
|
||||||
this.departureTime = 0;
|
this.departureTime = 0;
|
||||||
|
|
||||||
// Next stop that this course will reach
|
// Attributes for the `moving` state
|
||||||
// (if equal to departureStop, the course has reached its last stop)
|
this.departureStop = null;
|
||||||
this.arrivalStop = null;
|
this.arrivalStop = null;
|
||||||
|
|
||||||
// Time at which the next stop will be left
|
|
||||||
this.arrivalTime = 0;
|
this.arrivalTime = 0;
|
||||||
|
|
||||||
// Segment of points between the current departure and arrival
|
|
||||||
this.segment = null;
|
|
||||||
|
|
||||||
// Number of meters travelled between the two stops
|
|
||||||
this.traveledDistance = 0;
|
this.traveledDistance = 0;
|
||||||
|
|
||||||
// Current vehicle speed in meters per millisecond
|
|
||||||
this.speed = 0;
|
this.speed = 0;
|
||||||
|
|
||||||
// Current vehicle latitude and longitude
|
|
||||||
this.position = [0, 0];
|
this.position = [0, 0];
|
||||||
|
|
||||||
// Current vehicle bearing
|
|
||||||
this.angle = 0;
|
this.angle = 0;
|
||||||
|
|
||||||
|
this.history = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Retrieve information about the current segment used by the vehicle. */
|
get currentSegment() {
|
||||||
updateSegment() {
|
if (this.state !== "moving") {
|
||||||
if (this.departureStop === null || this.arrivalStop === null) {
|
return null;
|
||||||
this.segment = null;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = `${this.departureStop}-${this.arrivalStop}`;
|
return network.segments[`${this.departureStop}-${this.arrivalStop}`];
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.segment = turfHelpers.lineString([
|
|
||||||
network.stops[this.departureStop].geometry.coordinates,
|
|
||||||
network.stops[this.arrivalStop].geometry.coordinates,
|
|
||||||
]);
|
|
||||||
this.segment.properties.length = turfLength(this.segment);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Merge data received from the server. */
|
updateData(data) {
|
||||||
receiveData(data) {
|
|
||||||
this.line = data.line;
|
this.line = data.line;
|
||||||
this.direction = data.direction;
|
this.direction = data.direction;
|
||||||
this.finalStop = data.finalStopId;
|
this.finalStop = data.finalStopId;
|
||||||
|
this.nextPassings = data.passings;
|
||||||
|
|
||||||
const passings = Object.assign(
|
|
||||||
Object.fromEntries(this.nextPassings),
|
|
||||||
Object.fromEntries(data.passings),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Remove older passings from next passings
|
|
||||||
for (let [stop, time] of this.prevPassings) {
|
|
||||||
delete passings[stop];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update departure time if still announced
|
|
||||||
if (this.departureStop !== null) {
|
|
||||||
if (this.departureStop in passings) {
|
|
||||||
this.departureTime = passings[this.departureStop];
|
|
||||||
delete passings[this.departureStop];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.nextPassings = Object.entries(passings).sort(
|
|
||||||
([, time1], [, time2]) => time1 - time2
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Update the vehicle state. */
|
|
||||||
update() {
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
// When initializing, use the first available passing as start
|
if (this.state === null) {
|
||||||
if (this.departureStop === null) {
|
// Initialize the course on the first available segment
|
||||||
if (this.nextPassings.length > 0) {
|
const index = this.nextPassings.findIndex(
|
||||||
const [stopId, time] = this.nextPassings.shift();
|
([, time]) => time >= now
|
||||||
this.departureStop = stopId;
|
);
|
||||||
this.departureTime = time;
|
|
||||||
this.updateSegment();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// …and the second one as the arrival
|
if (index === -1) {
|
||||||
if (this.arrivalStop === null) {
|
return false;
|
||||||
if (this.nextPassings.length > 0) {
|
|
||||||
const [stopId, time] = this.nextPassings.shift();
|
|
||||||
this.arrivalStop = stopId;
|
|
||||||
this.arrivalTime = time;
|
|
||||||
this.updateSegment();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.traveledDistance = 0;
|
|
||||||
this.updateSegment();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.departureTime > now) {
|
if (index === 0) {
|
||||||
// Wait for departure
|
this.arriveToStop(this.nextPassings[index][0]);
|
||||||
this.speed = 0;
|
|
||||||
} else {
|
} else {
|
||||||
if (this.traveledDistance === 0 && this.speed === 0) {
|
this.arriveToStop(this.nextPassings[index - 1][0]);
|
||||||
// We’re late, record the actual departure time
|
this.moveToStop(...this.nextPassings[index]);
|
||||||
this.departureTime = now;
|
}
|
||||||
|
} else if (this.state === "moving") {
|
||||||
|
const index = this.nextPassings.findIndex(
|
||||||
|
([stop]) => stop === this.arrivalStop
|
||||||
|
);
|
||||||
|
|
||||||
|
if (index === -1 || this.nextPassings[index][1] <= now) {
|
||||||
|
// Next stop is not announced or in the past,
|
||||||
|
// move towards it as fast as possible
|
||||||
|
this.arrivalTime = now;
|
||||||
|
} else {
|
||||||
|
// On the right track, update the arrival time
|
||||||
|
this.arrivalTime = this.nextPassings[index][1];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// (this.state === 'stopped')
|
||||||
|
// Try moving to the next stop
|
||||||
|
const index = this.nextPassings.findIndex(
|
||||||
|
([stop]) => stop === this.currentStop
|
||||||
|
);
|
||||||
|
|
||||||
|
if (index !== -1) {
|
||||||
|
if (this.nextPassings[index][1] <= now) {
|
||||||
|
// Current stop is still announced but in the past
|
||||||
|
if (index + 1 < this.nextPassings.length) {
|
||||||
|
// Move to next stop
|
||||||
|
this.moveToStop(...this.nextPassings[index + 1]);
|
||||||
|
} else {
|
||||||
|
// No next stop announced, end of course
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Cannot move yet, departure is in the future
|
||||||
|
this.departureTime = this.nextPassings[index][1];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Current stop is not announced, find the first stop
|
||||||
|
// announced in the future to which is connection is
|
||||||
|
// possible
|
||||||
|
let found = false;
|
||||||
|
|
||||||
|
for (
|
||||||
|
let nextIndex = 0;
|
||||||
|
nextIndex < this.nextPassings.length;
|
||||||
|
++nextIndex
|
||||||
|
) {
|
||||||
|
const [stop, arrivalTime] = this.nextPassings[nextIndex];
|
||||||
|
|
||||||
|
if (arrivalTime > now) {
|
||||||
|
const route = findRoute(this.currentStop, stop);
|
||||||
|
|
||||||
|
if (route !== null) {
|
||||||
|
// Move to the first intermediate stop, guess the
|
||||||
|
// arrival time based on the final arrival time and
|
||||||
|
// the relative distance of the stops
|
||||||
|
const midDistance = network.segments[
|
||||||
|
`${route[0]}-${route[1]}`
|
||||||
|
].properties.length;
|
||||||
|
|
||||||
|
let totalDistance = midDistance;
|
||||||
|
|
||||||
|
for (
|
||||||
|
let midIndex = 1;
|
||||||
|
midIndex + 1 < route.length;
|
||||||
|
++midIndex
|
||||||
|
) {
|
||||||
|
totalDistance += network.segments[
|
||||||
|
`${route[midIndex]}-${route[midIndex + 1]}`
|
||||||
|
].properties.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
const midTime = now + (arrivalTime - now) *
|
||||||
|
midDistance / totalDistance;
|
||||||
|
|
||||||
|
this.moveToStop(route[1], midTime);
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update current speed to arrive on time if possible
|
if (!found) {
|
||||||
this.speed = Course.computeSpeed(distance, duration);
|
// No valid next stop available
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.state === "moving") {
|
||||||
|
const segment = this.currentSegment;
|
||||||
|
const distance = segment.properties.length - this.traveledDistance;
|
||||||
|
const duration = this.arrivalTime - Date.now();
|
||||||
|
|
||||||
|
this.speed = Course.computeSpeed(distance, duration);
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Integrate the current vehicle speed and update distance. */
|
tick(time) {
|
||||||
move(time) {
|
if (this.state === "moving") {
|
||||||
if (this.segment === null) {
|
// 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.departureTime = Date.now();
|
||||||
|
this.prevPassings.push([stop, Date.now()]);
|
||||||
|
this.position = (
|
||||||
|
turfProjection.toMercator(network.stops[stop])
|
||||||
|
.geometry.coordinates
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
const segmentId = `${this.currentStop}-${stop}`;
|
||||||
|
|
||||||
|
if (!(segmentId in network.segments)) {
|
||||||
|
console.warn(`Course ${this.id} cannot go from stop
|
||||||
|
${this.currentStop} to stop ${stop}. Teleporting to ${stop}`);
|
||||||
|
this.arriveToStop(stop);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.speed > 0) {
|
const distance = network.segments[segmentId].properties.length;
|
||||||
this.traveledDistance = Math.min(
|
const duration = arrivalTime - Date.now();
|
||||||
this.traveledDistance + this.speed * time,
|
|
||||||
this.segment.properties.length,
|
if (Course.computeSpeed(distance, duration) === 0) {
|
||||||
);
|
// Speed would be too low, better wait for some time
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute updated position and angle based on a small step
|
this.state = "moving";
|
||||||
let positionBehind;
|
this.departureStop = this.currentStop;
|
||||||
let positionInFront;
|
this.arrivalStop = stop;
|
||||||
|
this.arrivalTime = arrivalTime;
|
||||||
if (this.traveledDistance < angleStep / 2) {
|
this.traveledDistance = 0;
|
||||||
positionBehind = this.traveledDistance;
|
this.speed = 0;
|
||||||
positionInFront = angleStep;
|
|
||||||
} else {
|
|
||||||
positionBehind = this.traveledDistance - angleStep / 2;
|
|
||||||
positionInFront = this.traveledDistance + angleStep / 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
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];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static computeSpeed(distance, duration) {
|
static computeSpeed(distance, duration) {
|
||||||
if (duration <= 0) {
|
if (duration <= 0) {
|
||||||
// Late: go to maximum speed
|
// Late: go to maximum speed
|
||||||
return maxSpeed;
|
return 50 / 3600;
|
||||||
}
|
}
|
||||||
|
|
||||||
const speed = distance / duration;
|
if (distance / duration <= 10 / 3600) {
|
||||||
|
|
||||||
if (speed < minSpeed) {
|
|
||||||
// Too slow: pause until speed is sufficient
|
// Too slow: pause until speed is sufficient
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Math.min(maxSpeed, speed);
|
return distance / duration;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -279,23 +281,33 @@ const updateData = async courses => {
|
||||||
// Update or create new courses
|
// Update or create new courses
|
||||||
for (const [id, data] of Object.entries(dataset)) {
|
for (const [id, data] of Object.entries(dataset)) {
|
||||||
if (id in courses) {
|
if (id in courses) {
|
||||||
courses[id].receiveData(data);
|
if (!courses[id].updateData(data)) {
|
||||||
|
delete courses[id];
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const newCourse = new Course(data.id);
|
const newCourse = new Course(data);
|
||||||
newCourse.receiveData(data);
|
|
||||||
courses[id] = newCourse;
|
if (newCourse.updateData(data)) {
|
||||||
|
courses[id] = newCourse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove stale courses
|
||||||
|
for (const id of Object.keys(courses)) {
|
||||||
|
if (!(id in dataset)) {
|
||||||
|
delete courses[id];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const tick = (courses, time) => {
|
const tick = (courses, time) => {
|
||||||
for (const course of Object.values(courses)) {
|
for (const course of Object.values(courses)) {
|
||||||
course.update();
|
course.tick(time);
|
||||||
course.move(time);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const start = () => {
|
const start = () => {
|
||||||
const courses = {};
|
const courses = {};
|
||||||
let lastFrame = null;
|
let lastFrame = null;
|
||||||
let lastUpdate = null;
|
let lastUpdate = null;
|
||||||
|
@ -314,7 +326,7 @@ export const start = () => {
|
||||||
tick(courses, time);
|
tick(courses, time);
|
||||||
};
|
};
|
||||||
|
|
||||||
window.__courses = courses;
|
|
||||||
|
|
||||||
return { courses, update };
|
return { courses, update };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.start = start;
|
||||||
|
|
|
@ -4,8 +4,8 @@
|
||||||
* Interface with the OpenStreetMap collaborative mapping database.
|
* Interface with the OpenStreetMap collaborative mapping database.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import axios from "axios";
|
const axios = require("axios");
|
||||||
import { isObject } from "../../util.js";
|
const { isObject } = require("../../util");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Submit a query to an Overpass endpoint.
|
* Submit a query to an Overpass endpoint.
|
||||||
|
@ -19,7 +19,7 @@ import { isObject } from "../../util.js";
|
||||||
* is requested in the query, the result will automatically be parsed into
|
* is requested in the query, the result will automatically be parsed into
|
||||||
* a JS object.
|
* a JS object.
|
||||||
*/
|
*/
|
||||||
export const runQuery = (
|
const runQuery = (
|
||||||
query,
|
query,
|
||||||
endpoint = "https://lz4.overpass-api.de/api/interpreter"
|
endpoint = "https://lz4.overpass-api.de/api/interpreter"
|
||||||
) => (
|
) => (
|
||||||
|
@ -27,12 +27,16 @@ export const runQuery = (
|
||||||
.then(res => res.data)
|
.then(res => res.data)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
exports.runQuery = runQuery;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a link to view a node.
|
* Create a link to view a node.
|
||||||
* @param {string|number} id Identifier for the node to view.
|
* @param {string|number} id Identifier for the node to view.
|
||||||
* @returns {string} Link to view this node on the OSM website.
|
* @returns {string} Link to view this node on the OSM website.
|
||||||
*/
|
*/
|
||||||
export const viewNode = id => `https://www.osm.org/node/${id}`;
|
const viewNode = id => `https://www.osm.org/node/${id}`;
|
||||||
|
|
||||||
|
exports.viewNode = viewNode;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if an OSM way is one-way or not.
|
* Determine if an OSM way is one-way or not.
|
||||||
|
@ -41,13 +45,15 @@ export const viewNode = id => `https://www.osm.org/node/${id}`;
|
||||||
* @param {Object} obj OSM way object.
|
* @param {Object} obj OSM way object.
|
||||||
* @returns {boolean} Whether the way is one-way.
|
* @returns {boolean} Whether the way is one-way.
|
||||||
*/
|
*/
|
||||||
export const isOneWay = obj => (
|
const isOneWay = obj => (
|
||||||
obj.type === "way" &&
|
obj.type === "way" &&
|
||||||
isObject(obj.tags) &&
|
isObject(obj.tags) &&
|
||||||
(obj.tags.oneway === "yes" || obj.tags.junction === "roundabout" ||
|
(obj.tags.oneway === "yes" || obj.tags.junction === "roundabout" ||
|
||||||
obj.tags.highway === "motorway")
|
obj.tags.highway === "motorway")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
exports.isOneWay = isOneWay;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if an OSM object is a public transport line (route master).
|
* Determine if an OSM object is a public transport line (route master).
|
||||||
*
|
*
|
||||||
|
@ -56,8 +62,10 @@ export const isOneWay = obj => (
|
||||||
* @param {Object} obj OSM relation object.
|
* @param {Object} obj OSM relation object.
|
||||||
* @returns {boolean} Whether the relation is a public transport line.
|
* @returns {boolean} Whether the relation is a public transport line.
|
||||||
*/
|
*/
|
||||||
export const isTransportLine = obj => (
|
const isTransportLine = obj => (
|
||||||
obj.type === "relation" &&
|
obj.type === "relation" &&
|
||||||
isObject(obj.tags) &&
|
isObject(obj.tags) &&
|
||||||
obj.tags.type === "route_master"
|
obj.tags.type === "route_master"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
exports.isTransportLine = isTransportLine;
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import csv from "csv-parse";
|
const csv = require("csv-parse");
|
||||||
import axios from "axios";
|
const axios = require("axios");
|
||||||
import path from "path";
|
const path = require("path");
|
||||||
import fs from "fs/promises";
|
const fs = require("fs").promises;
|
||||||
import { snakeToCamelCase, unzipFile } from "../../util.js";
|
const { snakeToCamelCase, unzipFile } = require("../../util");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data available for each passing of a vehicle at a station.
|
* Data available for each passing of a vehicle at a station.
|
||||||
|
@ -70,14 +70,14 @@ const makeCached = (func, cachePath) => {
|
||||||
yield passing;
|
yield passing;
|
||||||
}
|
}
|
||||||
|
|
||||||
await fs.writeFile(cachePath, JSON.stringify(newCache));
|
fs.writeFile(cachePath, JSON.stringify(newCache));
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const cacheDir = new URL("../../../cache/", import.meta.url);
|
const cacheDir = path.join(__dirname, "..", "..", "..", "cache");
|
||||||
|
|
||||||
const realtimeEndpoint = "http://data.montpellier3m.fr/node/10732/download";
|
const realtimeEndpoint = "http://data.montpellier3m.fr/node/10732/download";
|
||||||
const realtimeCachePath = new URL("./realtime.json", cacheDir);
|
const realtimeCachePath = path.join(cacheDir, "realtime.json");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch real time passings of vehicles across the network.
|
* Fetch real time passings of vehicles across the network.
|
||||||
|
@ -86,7 +86,7 @@ const realtimeCachePath = new URL("./realtime.json", cacheDir);
|
||||||
* update of this information. Next values are informations about each vehicle
|
* update of this information. Next values are informations about each vehicle
|
||||||
* passing.
|
* passing.
|
||||||
*/
|
*/
|
||||||
const fetchRealtimeRaw = async function *() {
|
const fetchRealtime = async function *() {
|
||||||
const res = await axios.get(realtimeEndpoint, {
|
const res = await axios.get(realtimeEndpoint, {
|
||||||
responseType: "stream"
|
responseType: "stream"
|
||||||
});
|
});
|
||||||
|
@ -106,10 +106,10 @@ const fetchRealtimeRaw = async function *() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchRealtime = makeCached(fetchRealtimeRaw, realtimeCachePath);
|
exports.fetchRealtime = makeCached(fetchRealtime, realtimeCachePath);
|
||||||
|
|
||||||
const theoreticalEndpoint = "http://data.montpellier3m.fr/node/10731/download";
|
const theoreticalEndpoint = "http://data.montpellier3m.fr/node/10731/download";
|
||||||
const theoreticalCachePath = new URL("./theoretical.json", cacheDir);
|
const theoreticalCachePath = path.join(cacheDir, "theoretical.json");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch theoretical passings for the current day across the network.
|
* Fetch theoretical passings for the current day across the network.
|
||||||
|
@ -118,7 +118,7 @@ const theoreticalCachePath = new URL("./theoretical.json", cacheDir);
|
||||||
* update of this information. Next values are informations about each vehicle
|
* update of this information. Next values are informations about each vehicle
|
||||||
* passing.
|
* passing.
|
||||||
*/
|
*/
|
||||||
const fetchTheoreticalRaw = async function *() {
|
const fetchTheoretical = async function *() {
|
||||||
const res = await axios.get(theoreticalEndpoint, {
|
const res = await axios.get(theoreticalEndpoint, {
|
||||||
responseType: "stream"
|
responseType: "stream"
|
||||||
});
|
});
|
||||||
|
@ -154,5 +154,4 @@ const fetchTheoreticalRaw = async function *() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchTheoretical = makeCached(
|
exports.fetchTheoretical = makeCached(fetchTheoretical, theoreticalCachePath);
|
||||||
fetchTheoreticalRaw, theoreticalCachePath);
|
|
||||||
|
|
27
src/util.js
27
src/util.js
|
@ -1,19 +1,23 @@
|
||||||
import unzip from "unzip-stream";
|
const unzip = require("unzip-stream");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a snake-cased string to a camel-cased one.
|
* Convert a snake-cased string to a camel-cased one.
|
||||||
* @param {string} str Original string.
|
* @param {string} str Original string.
|
||||||
* @returns {string} Transformed string.
|
* @returns {string} Transformed string.
|
||||||
*/
|
*/
|
||||||
export const snakeToCamelCase = str => str.replace(/([-_][a-z])/gu, group =>
|
const snakeToCamelCase = str => str.replace(/([-_][a-z])/gu, group =>
|
||||||
group.toUpperCase().replace("-", "").replace("_", ""));
|
group.toUpperCase().replace("-", "").replace("_", ""));
|
||||||
|
|
||||||
|
exports.snakeToCamelCase = snakeToCamelCase;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a value is a JS object.
|
* Check if a value is a JS object.
|
||||||
* @param {*} value Value to check.
|
* @param {*} value Value to check.
|
||||||
* @returns {boolean} Whether `value` is a JS object.
|
* @returns {boolean} Whether `value` is a JS object.
|
||||||
*/
|
*/
|
||||||
export const isObject = value => value !== null && typeof value === "object";
|
const isObject = value => value !== null && typeof value === "object";
|
||||||
|
|
||||||
|
exports.isObject = isObject;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if two arrays are equal in a shallow manner.
|
* Check if two arrays are equal in a shallow manner.
|
||||||
|
@ -21,18 +25,20 @@ export const isObject = value => value !== null && typeof value === "object";
|
||||||
* @param {Array} array2 Second array.
|
* @param {Array} array2 Second array.
|
||||||
* @returns {boolean} Whether the two arrays are equal.
|
* @returns {boolean} Whether the two arrays are equal.
|
||||||
*/
|
*/
|
||||||
export const arraysEqual = (array1, array2) => (
|
const arraysEqual = (array1, array2) => (
|
||||||
array1.length === array2.length &&
|
array1.length === array2.length &&
|
||||||
array1.every((elt1, index) => elt1 === array2[index])
|
array1.every((elt1, index) => elt1 === array2[index])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
exports.arraysEqual = arraysEqual;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find a file in a zipped stream and unzip it.
|
* Find a file in a zipped stream and unzip it.
|
||||||
* @param {stream.Readable} data Input zipped stream.
|
* @param {stream.Readable} data Input zipped stream.
|
||||||
* @param {string} fileName Name of the file to find.
|
* @param {string} fileName Name of the file to find.
|
||||||
* @returns {Promise.<stream.Readable>} Stream of the unzipped file.
|
* @returns {Promise.<stream.Readable>} Stream of the unzipped file.
|
||||||
*/
|
*/
|
||||||
export const unzipFile = (data, fileName) => new Promise((res, rej) => {
|
const unzipFile = (data, fileName) => new Promise((res, rej) => {
|
||||||
// eslint-disable-next-line new-cap
|
// eslint-disable-next-line new-cap
|
||||||
const stream = data.pipe(unzip.Parse());
|
const stream = data.pipe(unzip.Parse());
|
||||||
let found = false;
|
let found = false;
|
||||||
|
@ -56,13 +62,12 @@ export const unzipFile = (data, fileName) => new Promise((res, rej) => {
|
||||||
stream.on("error", err => rej(err));
|
stream.on("error", err => rej(err));
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
exports.unzipFile = unzipFile;
|
||||||
* Display the time component of a date.
|
|
||||||
* @param {Date} date Date to display.
|
const displayTime = date => [
|
||||||
* @returns {string} Formatted date as 'HH:MM:SS'.
|
|
||||||
*/
|
|
||||||
export const displayTime = date => [
|
|
||||||
date.getHours(),
|
date.getHours(),
|
||||||
date.getMinutes(),
|
date.getMinutes(),
|
||||||
date.getSeconds()
|
date.getSeconds()
|
||||||
].map(number => number.toString().padStart(2, "0")).join(":");
|
].map(number => number.toString().padStart(2, "0")).join(":");
|
||||||
|
|
||||||
|
exports.displayTime = displayTime;
|
||||||
|
|
Laddar…
Referens i nytt ärende