Code cleanup: Update ESLint conf, improve internal passings interface
This commit is contained in:
		
							parent
							
								
									35cd0e1e72
								
							
						
					
					
						commit
						079fbcf310
					
				| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
{
 | 
			
		||||
    "extends": [
 | 
			
		||||
        "eslint:recommended"
 | 
			
		||||
        "eslint"
 | 
			
		||||
    ],
 | 
			
		||||
    "env": {
 | 
			
		||||
        "es6": true
 | 
			
		||||
| 
						 | 
				
			
			@ -10,7 +10,7 @@
 | 
			
		|||
        "SharedArrayBuffer": "readonly"
 | 
			
		||||
    },
 | 
			
		||||
    "parserOptions": {
 | 
			
		||||
        "ecmaVersion": 2018,
 | 
			
		||||
        "ecmaVersion": 2019,
 | 
			
		||||
        "sourceType": "script"
 | 
			
		||||
    },
 | 
			
		||||
    "ignorePatterns": [
 | 
			
		||||
| 
						 | 
				
			
			@ -18,38 +18,12 @@
 | 
			
		|||
        "dist/"
 | 
			
		||||
    ],
 | 
			
		||||
    "rules": {
 | 
			
		||||
        "indent": [
 | 
			
		||||
            "error",
 | 
			
		||||
            4
 | 
			
		||||
        ],
 | 
			
		||||
        "linebreak-style": [
 | 
			
		||||
            "error",
 | 
			
		||||
            "unix"
 | 
			
		||||
        ],
 | 
			
		||||
        "quotes": [
 | 
			
		||||
            "error",
 | 
			
		||||
            "single"
 | 
			
		||||
        ],
 | 
			
		||||
        "semi": [
 | 
			
		||||
            "error",
 | 
			
		||||
            "always"
 | 
			
		||||
        ],
 | 
			
		||||
        "no-irregular-whitespace": [
 | 
			
		||||
            "error",
 | 
			
		||||
            {
 | 
			
		||||
                "skipStrings": true,
 | 
			
		||||
                "skipTemplates": true
 | 
			
		||||
            }
 | 
			
		||||
        ],
 | 
			
		||||
        "comma-dangle": [
 | 
			
		||||
            "error",
 | 
			
		||||
            {
 | 
			
		||||
                "arrays": "always-multiline",
 | 
			
		||||
                "objects": "always-multiline",
 | 
			
		||||
                "imports": "always-multiline",
 | 
			
		||||
                "exports": "always-multiline",
 | 
			
		||||
                "functions": "never"
 | 
			
		||||
            }
 | 
			
		||||
        ]
 | 
			
		||||
        "strict": ["error", "never"],
 | 
			
		||||
        "no-console": ["error", {"allow": ["info", "warn", "error"]}],
 | 
			
		||||
        "no-multi-str": "off",
 | 
			
		||||
        "func-style": ["error", "expression"],
 | 
			
		||||
        "max-len": ["error", {"code": 80}],
 | 
			
		||||
        "lines-around-comment": ["error", {"allowBlockStart": true}],
 | 
			
		||||
        "jsdoc/require-returns": "off"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										14
									
								
								package.json
								
								
								
								
							
							
						
						
									
										14
									
								
								package.json
								
								
								
								
							| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
{
 | 
			
		||||
  "name": "tamview",
 | 
			
		||||
  "version": "1.0.0",
 | 
			
		||||
  "version": "0.1.0",
 | 
			
		||||
  "description": "",
 | 
			
		||||
  "main": "index.js",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
| 
						 | 
				
			
			@ -9,13 +9,17 @@
 | 
			
		|||
    "front:prod": "npx parcel build src/front/index.html --no-source-maps --no-autoinstall",
 | 
			
		||||
    "lint": "eslint ."
 | 
			
		||||
  },
 | 
			
		||||
  "engines": {
 | 
			
		||||
    "node": ">=10.0.0"
 | 
			
		||||
  },
 | 
			
		||||
  "keywords": [],
 | 
			
		||||
  "author": "",
 | 
			
		||||
  "license": "ISC",
 | 
			
		||||
  "author": "Mattéo Delabre",
 | 
			
		||||
  "license": "MIT",
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@turf/along": "^6.0.1",
 | 
			
		||||
    "@turf/helpers": "^6.1.4",
 | 
			
		||||
    "@turf/length": "^6.0.2",
 | 
			
		||||
    "@turf/projection": "^6.0.1",
 | 
			
		||||
    "@turf/turf": "^5.1.6",
 | 
			
		||||
    "axios": "^0.19.2",
 | 
			
		||||
    "color": "^3.1.2",
 | 
			
		||||
| 
						 | 
				
			
			@ -26,7 +30,9 @@
 | 
			
		|||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "eslint": "^6.8.0",
 | 
			
		||||
    "nodemon": "^2.0.2",
 | 
			
		||||
    "eslint-config-eslint": "^6.0.0",
 | 
			
		||||
    "eslint-plugin-jsdoc": "^30.0.3",
 | 
			
		||||
    "eslint-plugin-node": "^11.1.0",
 | 
			
		||||
    "parcel-bundler": "^1.12.4"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,16 +1,12 @@
 | 
			
		|||
const express = require('express');
 | 
			
		||||
 | 
			
		||||
const util = require('../util');
 | 
			
		||||
const realtime = require('../tam/realtime');
 | 
			
		||||
const express = require("express");
 | 
			
		||||
const realtime = require("../tam/realtime");
 | 
			
		||||
 | 
			
		||||
const app = express();
 | 
			
		||||
const port = 4321;
 | 
			
		||||
 | 
			
		||||
app.get('/courses', async (req, res) =>
 | 
			
		||||
{
 | 
			
		||||
    res.header('Access-Control-Allow-Origin', '*');
 | 
			
		||||
    const courses = await realtime.getCourses();
 | 
			
		||||
    return res.json(courses);
 | 
			
		||||
app.get("/courses", async(req, res) => {
 | 
			
		||||
    res.header("Access-Control-Allow-Origin", "*");
 | 
			
		||||
    return res.json(await realtime.fetch());
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
app.listen(port, () => console.log(`App listening on port ${port}`));
 | 
			
		||||
app.listen(port, () => console.info(`App listening on port ${port}`));
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,5 @@
 | 
			
		|||
{
 | 
			
		||||
    "env": {
 | 
			
		||||
        "browser": true,
 | 
			
		||||
        "commonjs": true
 | 
			
		||||
        "browser": true
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,6 @@
 | 
			
		|||
require('regenerator-runtime/runtime');
 | 
			
		||||
// eslint-disable-next-line node/no-extraneous-require
 | 
			
		||||
require("regenerator-runtime/runtime");
 | 
			
		||||
 | 
			
		||||
const {createMap} = require('./map');
 | 
			
		||||
createMap(/* map = */ 'map');
 | 
			
		||||
const { createMap } = require("./map");
 | 
			
		||||
 | 
			
		||||
createMap(/* map = */ "map");
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										215
									
								
								src/front/map.js
								
								
								
								
							
							
						
						
									
										215
									
								
								src/front/map.js
								
								
								
								
							| 
						 | 
				
			
			@ -1,73 +1,64 @@
 | 
			
		|||
require('ol/ol.css');
 | 
			
		||||
require("ol/ol.css");
 | 
			
		||||
 | 
			
		||||
const axios = require('axios');
 | 
			
		||||
const {Map, View} = require('ol');
 | 
			
		||||
const { Map, View } = require("ol");
 | 
			
		||||
 | 
			
		||||
const GeoJSON = require('ol/format/GeoJSON').default;
 | 
			
		||||
const reader = new GeoJSON({featureProjection: 'EPSG:3857'});
 | 
			
		||||
const GeoJSON = require("ol/format/GeoJSON").default;
 | 
			
		||||
const reader = new GeoJSON({ featureProjection: "EPSG:3857" });
 | 
			
		||||
 | 
			
		||||
const TileLayer = require('ol/layer/Tile').default;
 | 
			
		||||
const XYZSource = require('ol/source/XYZ').default;
 | 
			
		||||
const TileLayer = require("ol/layer/Tile").default;
 | 
			
		||||
const XYZSource = require("ol/source/XYZ").default;
 | 
			
		||||
 | 
			
		||||
const VectorLayer = require('ol/layer/Vector').default;
 | 
			
		||||
const VectorSource = require('ol/source/Vector').default;
 | 
			
		||||
const {getVectorContext} = require('ol/render');
 | 
			
		||||
const VectorLayer = require("ol/layer/Vector").default;
 | 
			
		||||
const VectorSource = require("ol/source/Vector").default;
 | 
			
		||||
const { getVectorContext } = require("ol/render");
 | 
			
		||||
 | 
			
		||||
const Feature = require('ol/Feature').default;
 | 
			
		||||
const Point = require('ol/geom/Point').default;
 | 
			
		||||
const LineString = require('ol/geom/LineString').default;
 | 
			
		||||
const Point = require("ol/geom/Point").default;
 | 
			
		||||
 | 
			
		||||
const proj = require('ol/proj');
 | 
			
		||||
const proj = require("ol/proj");
 | 
			
		||||
 | 
			
		||||
const {Style, Fill, Stroke, Circle, Icon} = require('ol/style');
 | 
			
		||||
const color = require('color');
 | 
			
		||||
const { Style, Fill, Stroke, Circle, Icon } = require("ol/style");
 | 
			
		||||
const colorModule = require("color");
 | 
			
		||||
 | 
			
		||||
const mapboxToken = `pk.eyJ1IjoibWF0dGVvZGVsYWJyZSIsImEiOiJja2NxaTUyMmUwcmFhMn\
 | 
			
		||||
h0NmFsdzQ3emxqIn0.cyxF0h36emIMTk3cc4VqUw`;
 | 
			
		||||
const mapboxToken = "pk.eyJ1IjoibWF0dGVvZGVsYWJyZSIsImEiOiJja2NxaTUyMmUwcmFhMn\
 | 
			
		||||
h0NmFsdzQ3emxqIn0.cyxF0h36emIMTk3cc4VqUw";
 | 
			
		||||
 | 
			
		||||
const simulation = require('../tam/simulation');
 | 
			
		||||
const network = require('../tam/network.json');
 | 
			
		||||
const simulation = require("../tam/simulation");
 | 
			
		||||
const network = require("../tam/network.json");
 | 
			
		||||
 | 
			
		||||
const lineFeaturesOrder = (feature1, feature2) =>
 | 
			
		||||
{
 | 
			
		||||
    const lines1 = feature1.get('lines');
 | 
			
		||||
const lineFeaturesOrder = (feature1, feature2) => {
 | 
			
		||||
    const lines1 = feature1.get("lines");
 | 
			
		||||
 | 
			
		||||
    if (lines1.length === 0)
 | 
			
		||||
    {
 | 
			
		||||
    if (lines1.length === 0) {
 | 
			
		||||
        return -1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const lines2 = feature2.get('lines');
 | 
			
		||||
    const lines2 = feature2.get("lines");
 | 
			
		||||
 | 
			
		||||
    if (lines2.length === 0)
 | 
			
		||||
    {
 | 
			
		||||
    if (lines2.length === 0) {
 | 
			
		||||
        return 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return Math.min(...lines1) - Math.min(...lines2);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const makeDataSources = () =>
 | 
			
		||||
{
 | 
			
		||||
const makeDataSources = () => {
 | 
			
		||||
    const segmentsSource = new VectorSource();
 | 
			
		||||
    const stopsSource = new VectorSource();
 | 
			
		||||
 | 
			
		||||
    const readFeatures = hash => Object.values(hash).map(json =>
 | 
			
		||||
    {
 | 
			
		||||
    const readFeatures = hash => Object.values(hash).map(json => {
 | 
			
		||||
        json.properties.lines = json.properties.routes.filter(
 | 
			
		||||
 | 
			
		||||
            // Only consider normal routes (excluding alternate routes)
 | 
			
		||||
            ([lineRef, routeRef]) =>
 | 
			
		||||
            network.lines[lineRef].routes[routeRef].state === 'normal'
 | 
			
		||||
                network.lines[lineRef].routes[routeRef].state === "normal"
 | 
			
		||||
        ).map(([lineRef]) => lineRef);
 | 
			
		||||
 | 
			
		||||
        if (json.properties.lines.length >= 1)
 | 
			
		||||
        {
 | 
			
		||||
        if (json.properties.lines.length >= 1) {
 | 
			
		||||
            json.properties.colors = json.properties.lines.map(
 | 
			
		||||
                lineRef => network.lines[lineRef].color);
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            json.properties.colors = ['#FFFFFF'];
 | 
			
		||||
                lineRef => network.lines[lineRef].color
 | 
			
		||||
            );
 | 
			
		||||
        } else {
 | 
			
		||||
            json.properties.colors = ["#FFFFFF"];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return reader.readFeature(json);
 | 
			
		||||
| 
						 | 
				
			
			@ -75,19 +66,19 @@ const makeDataSources = () =>
 | 
			
		|||
 | 
			
		||||
    segmentsSource.addFeatures(readFeatures(network.segments));
 | 
			
		||||
    stopsSource.addFeatures(readFeatures(network.stops));
 | 
			
		||||
    return {segmentsSource, stopsSource};
 | 
			
		||||
    return { segmentsSource, stopsSource };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const makeBorderColor = mainColor =>
 | 
			
		||||
{
 | 
			
		||||
    const hsl = color(mainColor).hsl();
 | 
			
		||||
const makeBorderColor = mainColor => {
 | 
			
		||||
    const hsl = colorModule(mainColor).hsl();
 | 
			
		||||
 | 
			
		||||
    hsl.color = Math.max(0, hsl.color[2] -= 20);
 | 
			
		||||
    return hsl.hex();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const makeCourseColor = mainColor =>
 | 
			
		||||
{
 | 
			
		||||
    const hsl = color(mainColor).hsl();
 | 
			
		||||
const makeCourseColor = mainColor => {
 | 
			
		||||
    const hsl = colorModule(mainColor).hsl();
 | 
			
		||||
 | 
			
		||||
    hsl.color = Math.max(0, hsl.color[2] += 10);
 | 
			
		||||
    return hsl.hex();
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -100,43 +91,41 @@ const sizes = {
 | 
			
		|||
    courseSize: 15,
 | 
			
		||||
    courseOuterBorder: 13,
 | 
			
		||||
    courseBorder: 10,
 | 
			
		||||
    courseInnerBorder: 7,
 | 
			
		||||
    courseInnerBorder: 7
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const segmentBorderStyle = feature => new Style({
 | 
			
		||||
    stroke: new Stroke({
 | 
			
		||||
        color: makeBorderColor(feature.get('colors')[0]),
 | 
			
		||||
        width: sizes.segmentOuter,
 | 
			
		||||
    }),
 | 
			
		||||
        color: makeBorderColor(feature.get("colors")[0]),
 | 
			
		||||
        width: sizes.segmentOuter
 | 
			
		||||
    })
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const segmentInnerStyle = feature => new Style({
 | 
			
		||||
    stroke: new Stroke({
 | 
			
		||||
        color: feature.get('colors')[0],
 | 
			
		||||
        width: sizes.segmentInner,
 | 
			
		||||
    }),
 | 
			
		||||
        color: feature.get("colors")[0],
 | 
			
		||||
        width: sizes.segmentInner
 | 
			
		||||
    })
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const stopStyle = feature => new Style({
 | 
			
		||||
    image: new Circle({
 | 
			
		||||
        fill: new Fill({
 | 
			
		||||
            color: feature.get('colors')[0],
 | 
			
		||||
            color: feature.get("colors")[0]
 | 
			
		||||
        }),
 | 
			
		||||
        stroke: new Stroke({
 | 
			
		||||
            color: makeBorderColor(feature.get('colors')[0]),
 | 
			
		||||
            width: sizes.stopBorder,
 | 
			
		||||
            color: makeBorderColor(feature.get("colors")[0]),
 | 
			
		||||
            width: sizes.stopBorder
 | 
			
		||||
        }),
 | 
			
		||||
        radius: sizes.stopRadius,
 | 
			
		||||
    }),
 | 
			
		||||
        radius: sizes.stopRadius
 | 
			
		||||
    })
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const courseStyles = {};
 | 
			
		||||
 | 
			
		||||
const getCourseStyle = color =>
 | 
			
		||||
{
 | 
			
		||||
    if (!(color in courseStyles))
 | 
			
		||||
    {
 | 
			
		||||
        const icon = document.createElement('canvas');
 | 
			
		||||
const getCourseStyle = lineColor => {
 | 
			
		||||
    if (!(lineColor in courseStyles)) {
 | 
			
		||||
        const icon = window.document.createElement("canvas");
 | 
			
		||||
 | 
			
		||||
        const shapeSize = sizes.courseSize;
 | 
			
		||||
        const iconSize = sizes.courseSize + sizes.courseOuterBorder;
 | 
			
		||||
| 
						 | 
				
			
			@ -147,18 +136,17 @@ const getCourseStyle = color =>
 | 
			
		|||
        const cx = icon.width / 2;
 | 
			
		||||
        const cy = icon.height / 2;
 | 
			
		||||
 | 
			
		||||
        const iconCtx = icon.getContext('2d');
 | 
			
		||||
        const iconCtx = icon.getContext("2d");
 | 
			
		||||
 | 
			
		||||
        for (let [color, size] of [
 | 
			
		||||
            [makeBorderColor(color), sizes.courseOuterBorder],
 | 
			
		||||
            [color, sizes.courseBorder],
 | 
			
		||||
            [makeCourseColor(color), sizes.courseInnerBorder]
 | 
			
		||||
        ])
 | 
			
		||||
        {
 | 
			
		||||
        for (const [color, size] of [
 | 
			
		||||
            [makeBorderColor(lineColor), sizes.courseOuterBorder],
 | 
			
		||||
            [lineColor, sizes.courseBorder],
 | 
			
		||||
            [makeCourseColor(lineColor), sizes.courseInnerBorder]
 | 
			
		||||
        ]) {
 | 
			
		||||
            iconCtx.fillStyle = color;
 | 
			
		||||
            iconCtx.strokeStyle = color;
 | 
			
		||||
            iconCtx.lineWidth = size;
 | 
			
		||||
            iconCtx.lineJoin = 'round';
 | 
			
		||||
            iconCtx.lineJoin = "round";
 | 
			
		||||
            iconCtx.miterLimit = 200000;
 | 
			
		||||
 | 
			
		||||
            iconCtx.beginPath();
 | 
			
		||||
| 
						 | 
				
			
			@ -170,34 +158,34 @@ const getCourseStyle = color =>
 | 
			
		|||
            iconCtx.fill();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        courseStyles[color] = new Style({
 | 
			
		||||
        courseStyles[lineColor] = new Style({
 | 
			
		||||
            image: new Icon({
 | 
			
		||||
                img: icon,
 | 
			
		||||
                imgSize: [icon.width, icon.height],
 | 
			
		||||
            }),
 | 
			
		||||
                imgSize: [icon.width, icon.height]
 | 
			
		||||
            })
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return courseStyles[color];
 | 
			
		||||
    return courseStyles[lineColor];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const createMap = target =>
 | 
			
		||||
{
 | 
			
		||||
const createMap = target => {
 | 
			
		||||
 | 
			
		||||
    // Map background
 | 
			
		||||
    const backgroundSource = new XYZSource({
 | 
			
		||||
        url: 'https://api.mapbox.com/' + [
 | 
			
		||||
            'styles', 'v1', 'mapbox', 'streets-v11',
 | 
			
		||||
            'tiles', '512', '{z}', '{x}', '{y}',
 | 
			
		||||
        ].join('/') + `?access_token=${mapboxToken}`,
 | 
			
		||||
        tileSize: [512, 512],
 | 
			
		||||
        url: `https://api.mapbox.com/${[
 | 
			
		||||
            "styles", "v1", "mapbox", "streets-v11",
 | 
			
		||||
            "tiles", "512", "{z}", "{x}", "{y}"
 | 
			
		||||
        ].join("/")}?access_token=${mapboxToken}`,
 | 
			
		||||
        tileSize: [512, 512]
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const backgroundLayer = new TileLayer({
 | 
			
		||||
        source: backgroundSource,
 | 
			
		||||
        source: backgroundSource
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Static data overlay
 | 
			
		||||
    const {segmentsSource, stopsSource} = makeDataSources();
 | 
			
		||||
    const { segmentsSource, stopsSource } = makeDataSources();
 | 
			
		||||
 | 
			
		||||
    const segmentsBorderLayer = new VectorLayer({
 | 
			
		||||
        source: segmentsSource,
 | 
			
		||||
| 
						 | 
				
			
			@ -205,7 +193,7 @@ const createMap = target =>
 | 
			
		|||
        style: segmentBorderStyle,
 | 
			
		||||
 | 
			
		||||
        updateWhileInteracting: true,
 | 
			
		||||
        updateWhileAnimating: true,
 | 
			
		||||
        updateWhileAnimating: true
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const segmentsInnerLayer = new VectorLayer({
 | 
			
		||||
| 
						 | 
				
			
			@ -214,7 +202,7 @@ const createMap = target =>
 | 
			
		|||
        style: segmentInnerStyle,
 | 
			
		||||
 | 
			
		||||
        updateWhileInteracting: true,
 | 
			
		||||
        updateWhileAnimating: true,
 | 
			
		||||
        updateWhileAnimating: true
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const stopsLayer = new VectorLayer({
 | 
			
		||||
| 
						 | 
				
			
			@ -224,7 +212,7 @@ const createMap = target =>
 | 
			
		|||
 | 
			
		||||
        minZoom: 13,
 | 
			
		||||
        updateWhileInteracting: true,
 | 
			
		||||
        updateWhileAnimating: true,
 | 
			
		||||
        updateWhileAnimating: true
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Setup map
 | 
			
		||||
| 
						 | 
				
			
			@ -232,7 +220,7 @@ const createMap = target =>
 | 
			
		|||
        center: proj.fromLonLat([3.88, 43.605]),
 | 
			
		||||
        zoom: 14,
 | 
			
		||||
        maxZoom: 22,
 | 
			
		||||
        constrainResolution: true,
 | 
			
		||||
        constrainResolution: true
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const map = new Map({
 | 
			
		||||
| 
						 | 
				
			
			@ -241,9 +229,9 @@ const createMap = target =>
 | 
			
		|||
            backgroundLayer,
 | 
			
		||||
            segmentsBorderLayer,
 | 
			
		||||
            segmentsInnerLayer,
 | 
			
		||||
            stopsLayer,
 | 
			
		||||
            stopsLayer
 | 
			
		||||
        ],
 | 
			
		||||
        view,
 | 
			
		||||
        view
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Run courses simulation
 | 
			
		||||
| 
						 | 
				
			
			@ -252,26 +240,25 @@ const createMap = target =>
 | 
			
		|||
    // Course on which the view is currently focused
 | 
			
		||||
    let focusedCourse = null;
 | 
			
		||||
 | 
			
		||||
    const startFocus = courseId =>
 | 
			
		||||
    {
 | 
			
		||||
        if (courseId in simulInstance.courses)
 | 
			
		||||
        {
 | 
			
		||||
    const startFocus = courseId => {
 | 
			
		||||
        if (courseId in simulInstance.courses) {
 | 
			
		||||
            const course = simulInstance.courses[courseId];
 | 
			
		||||
 | 
			
		||||
            view.animate({
 | 
			
		||||
                center: course.position,
 | 
			
		||||
                duration: 500,
 | 
			
		||||
            }, () => focusedCourse = courseId);
 | 
			
		||||
                duration: 500
 | 
			
		||||
            }, () => {
 | 
			
		||||
                focusedCourse = courseId;
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const stopFocus = () =>
 | 
			
		||||
    {
 | 
			
		||||
    const stopFocus = () => {
 | 
			
		||||
        focusedCourse = null;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // Draw courses directly on the map
 | 
			
		||||
    map.on('postcompose', ev =>
 | 
			
		||||
    {
 | 
			
		||||
    map.on("postcompose", ev => {
 | 
			
		||||
        simulInstance.update();
 | 
			
		||||
 | 
			
		||||
        // The normal way to access a layer’s vector context is through the
 | 
			
		||||
| 
						 | 
				
			
			@ -281,17 +268,16 @@ const createMap = target =>
 | 
			
		|||
        // if no stop is visible. This hack listens to the global `postcompose`
 | 
			
		||||
        // event, which is always triggered at every frame, and reconstructs
 | 
			
		||||
        // the stops layer’s vector context from internal variables
 | 
			
		||||
        if (stopsLayer.renderer_)
 | 
			
		||||
        {
 | 
			
		||||
        /* eslint-disable no-underscore-dangle */
 | 
			
		||||
        if (stopsLayer.renderer_) {
 | 
			
		||||
            ev.context = stopsLayer.renderer_.context;
 | 
			
		||||
            ev.inversePixelTransform
 | 
			
		||||
                = stopsLayer.renderer_.inversePixelTransform;
 | 
			
		||||
            ev.inversePixelTransform =
 | 
			
		||||
                stopsLayer.renderer_.inversePixelTransform;
 | 
			
		||||
            /* eslint-enable no-underscore-dangle */
 | 
			
		||||
 | 
			
		||||
            const ctx = getVectorContext(ev);
 | 
			
		||||
            let rotation = 0;
 | 
			
		||||
 | 
			
		||||
            for (let course of Object.values(simulInstance.courses))
 | 
			
		||||
            {
 | 
			
		||||
            for (const course of Object.values(simulInstance.courses)) {
 | 
			
		||||
                const color = network.lines[course.line].color;
 | 
			
		||||
                const style = getCourseStyle(color);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -299,10 +285,10 @@ const createMap = target =>
 | 
			
		|||
                ctx.setStyle(style);
 | 
			
		||||
 | 
			
		||||
                const point = new Point(course.position);
 | 
			
		||||
 | 
			
		||||
                ctx.drawGeometry(point);
 | 
			
		||||
 | 
			
		||||
                if (course.id === focusedCourse)
 | 
			
		||||
                {
 | 
			
		||||
                if (course.id === focusedCourse) {
 | 
			
		||||
                    view.setCenter(course.position);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
| 
						 | 
				
			
			@ -313,20 +299,17 @@ const createMap = target =>
 | 
			
		|||
 | 
			
		||||
    map.render();
 | 
			
		||||
 | 
			
		||||
    map.on('singleclick', ev =>
 | 
			
		||||
    {
 | 
			
		||||
    map.on("singleclick", ev => {
 | 
			
		||||
        const mousePixel = map.getPixelFromCoordinate(ev.coordinate);
 | 
			
		||||
        const maxDistance = sizes.courseSize + sizes.courseOuterBorder;
 | 
			
		||||
 | 
			
		||||
        for (let course of Object.values(simulInstance.courses))
 | 
			
		||||
        {
 | 
			
		||||
        for (const course of Object.values(simulInstance.courses)) {
 | 
			
		||||
            const coursePixel = map.getPixelFromCoordinate(course.position);
 | 
			
		||||
            const dx = mousePixel[0] - coursePixel[0];
 | 
			
		||||
            const dy = mousePixel[1] - coursePixel[1];
 | 
			
		||||
            const distance = dx * dx + dy * dy;
 | 
			
		||||
 | 
			
		||||
            if (distance <= maxDistance * maxDistance)
 | 
			
		||||
            {
 | 
			
		||||
            if (distance <= maxDistance * maxDistance) {
 | 
			
		||||
                startFocus(course.id);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,5 @@
 | 
			
		|||
{
 | 
			
		||||
    "env": {
 | 
			
		||||
        "commonjs": true
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
/**
 | 
			
		||||
 * @file
 | 
			
		||||
 * @fileoverview
 | 
			
		||||
 *
 | 
			
		||||
 * Extract static information about the TaM network from OpenStreetMap (OSM):
 | 
			
		||||
 * tram and bus lines, stops and routes.
 | 
			
		||||
| 
						 | 
				
			
			@ -12,25 +12,24 @@
 | 
			
		|||
 * the `script/update-network` script.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
const turfHelpers = require('@turf/helpers');
 | 
			
		||||
const turfLength = require('@turf/length').default;
 | 
			
		||||
const util = require('../util');
 | 
			
		||||
const osm = require('./sources/osm');
 | 
			
		||||
const tam = require('./sources/tam');
 | 
			
		||||
const turfHelpers = require("@turf/helpers");
 | 
			
		||||
const turfLength = require("@turf/length").default;
 | 
			
		||||
const util = require("../util");
 | 
			
		||||
const osm = require("./sources/osm");
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Fetch stops and lines of the network.
 | 
			
		||||
 *
 | 
			
		||||
 * @param lineRefs List of lines to fetch.
 | 
			
		||||
 * @return Object with a set of stops, segments and lines.
 | 
			
		||||
 * @param {string[]} lineRefs List of lines to fetch.
 | 
			
		||||
 * @returns {{stops: Object, lines: Object, segments: Object}} Set of stops,
 | 
			
		||||
 * segments and lines.
 | 
			
		||||
 */
 | 
			
		||||
const fetch = async (lineRefs) =>
 | 
			
		||||
{
 | 
			
		||||
const fetch = async lineRefs => {
 | 
			
		||||
 | 
			
		||||
    // Retrieve routes, ways and stops from OpenStreetMap
 | 
			
		||||
    const rawData = await osm.runQuery(`[out:json];
 | 
			
		||||
 | 
			
		||||
// Find the public transport line bearing the requested reference
 | 
			
		||||
relation[network="TaM"][type="route_master"][ref~"^(${lineRefs.join('|')})$"];
 | 
			
		||||
relation[network="TaM"][type="route_master"][ref~"^(${lineRefs.join("|")})$"];
 | 
			
		||||
 | 
			
		||||
// Recursively fetch routes, ways and stops inside the line
 | 
			
		||||
(._; >>;);
 | 
			
		||||
| 
						 | 
				
			
			@ -45,8 +44,7 @@ out body qt;
 | 
			
		|||
    const routeMasters = elementsList.filter(osm.isTransportLine);
 | 
			
		||||
 | 
			
		||||
    // Retrieved objects indexed by ID
 | 
			
		||||
    const elements = elementsList.reduce((prev, elt) =>
 | 
			
		||||
    {
 | 
			
		||||
    const elements = elementsList.reduce((prev, elt) => {
 | 
			
		||||
        prev[elt.id] = elt;
 | 
			
		||||
        return prev;
 | 
			
		||||
    }, {});
 | 
			
		||||
| 
						 | 
				
			
			@ -60,46 +58,39 @@ out body qt;
 | 
			
		|||
    // All segments leading from one stop to another
 | 
			
		||||
    const segments = {};
 | 
			
		||||
 | 
			
		||||
    for (let routeMaster of routeMasters)
 | 
			
		||||
    {
 | 
			
		||||
    for (const routeMaster of routeMasters) {
 | 
			
		||||
        const lineRef = routeMaster.tags.ref;
 | 
			
		||||
        const color = routeMaster.tags.colour || '#000000';
 | 
			
		||||
        const color = routeMaster.tags.colour || "#000000";
 | 
			
		||||
 | 
			
		||||
        // Extract all routes for the given line
 | 
			
		||||
        const routes = [];
 | 
			
		||||
 | 
			
		||||
        for (let [routeRef, {ref: routeId}] of routeMaster.members.entries())
 | 
			
		||||
        {
 | 
			
		||||
        for (const [routeRef, data] of routeMaster.members.entries()) {
 | 
			
		||||
            const routeId = data.ref;
 | 
			
		||||
            const route = elements[routeId];
 | 
			
		||||
            const {from, via, to, name} = route.tags;
 | 
			
		||||
            const state = route.tags.state || 'normal';
 | 
			
		||||
            const { from, via, to, name } = route.tags;
 | 
			
		||||
            const state = route.tags.state || "normal";
 | 
			
		||||
 | 
			
		||||
            // Add missing stops to the global stops object
 | 
			
		||||
            for (let {ref, role} of route.members)
 | 
			
		||||
            {
 | 
			
		||||
                if (role === 'stop')
 | 
			
		||||
                {
 | 
			
		||||
            for (const { ref, role } of route.members) {
 | 
			
		||||
                if (role === "stop") {
 | 
			
		||||
                    const stop = elements[ref];
 | 
			
		||||
 | 
			
		||||
                    if (!('ref' in stop.tags))
 | 
			
		||||
                    {
 | 
			
		||||
                    if (!("ref" in stop.tags)) {
 | 
			
		||||
                        throw new Error(`Stop ${stop.id}
 | 
			
		||||
(${osm.viewNode(stop.id)}) on line ${route.tags.name} is missing
 | 
			
		||||
a “ref” tag`);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if (!(stop.tags.ref in stops))
 | 
			
		||||
                    {
 | 
			
		||||
                    if (!(stop.tags.ref in stops)) {
 | 
			
		||||
                        stops[stop.tags.ref] = turfHelpers.point([
 | 
			
		||||
                            stop.lon,
 | 
			
		||||
                            stop.lat
 | 
			
		||||
                        ], {
 | 
			
		||||
                            name: stop.tags.name,
 | 
			
		||||
                            routes: [[lineRef, routeRef]],
 | 
			
		||||
                            routes: [[lineRef, routeRef]]
 | 
			
		||||
                        });
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                    } else {
 | 
			
		||||
                        stops[stop.tags.ref].properties.routes.push([
 | 
			
		||||
                            lineRef,
 | 
			
		||||
                            routeRef
 | 
			
		||||
| 
						 | 
				
			
			@ -111,21 +102,19 @@ a “ref” tag`);
 | 
			
		|||
            // Check that the route consists of a block of stops and platforms
 | 
			
		||||
            // followed by a block of routes as dictated by PTv2
 | 
			
		||||
            const relationPivot = route.members.findIndex(
 | 
			
		||||
                ({role}) => role === ''
 | 
			
		||||
                ({ role }) => role === ""
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            if (!route.members.slice(0, relationPivot).every(
 | 
			
		||||
                ({role}) => role === 'stop' || role === 'platform'
 | 
			
		||||
            ))
 | 
			
		||||
            {
 | 
			
		||||
                ({ role }) => role === "stop" || role === "platform"
 | 
			
		||||
            )) {
 | 
			
		||||
                throw new Error(`Members with invalid roles in between stops
 | 
			
		||||
of ${name}`);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!route.members.slice(relationPivot).every(
 | 
			
		||||
                ({role}) => role === ''
 | 
			
		||||
            ))
 | 
			
		||||
            {
 | 
			
		||||
                ({ role }) => role === ""
 | 
			
		||||
            )) {
 | 
			
		||||
                throw new Error(`Members with invalid roles inside the path
 | 
			
		||||
of ${name}`);
 | 
			
		||||
            }
 | 
			
		||||
| 
						 | 
				
			
			@ -134,22 +123,21 @@ of ${name}`);
 | 
			
		|||
            // order as per PTv2 and to be traversed in order by the sequence
 | 
			
		||||
            // of ways extracted below
 | 
			
		||||
            const lineStops = route.members.slice(0, relationPivot)
 | 
			
		||||
                .filter(({role}) => role === 'stop')
 | 
			
		||||
                .map(({ref}) => ref);
 | 
			
		||||
                .filter(({ role }) => role === "stop")
 | 
			
		||||
                .map(({ ref }) => ref);
 | 
			
		||||
 | 
			
		||||
            // List of ways making up the route’s path through its stops
 | 
			
		||||
            // with each way connected to the next through a single point
 | 
			
		||||
            const ways = route.members.slice(relationPivot)
 | 
			
		||||
                .map(({ref}) => ref);
 | 
			
		||||
                .map(({ ref }) => ref);
 | 
			
		||||
 | 
			
		||||
            // Merge all used ways in a single path
 | 
			
		||||
            let path = [];
 | 
			
		||||
            let currentNode = lineStops[0];
 | 
			
		||||
 | 
			
		||||
            for (let wayIndex = 0; wayIndex < ways.length; wayIndex += 1)
 | 
			
		||||
            {
 | 
			
		||||
            for (let wayIndex = 0; wayIndex < ways.length; wayIndex += 1) {
 | 
			
		||||
                const way = elements[ways[wayIndex]];
 | 
			
		||||
                const {nodes: wayNodes, tags: wayTags} = way;
 | 
			
		||||
                const { nodes: wayNodes } = way;
 | 
			
		||||
                const wayNodesSet = new Set(wayNodes);
 | 
			
		||||
 | 
			
		||||
                const curNodeIndex = wayNodes.indexOf(currentNode);
 | 
			
		||||
| 
						 | 
				
			
			@ -159,13 +147,11 @@ of ${name}`);
 | 
			
		|||
                let nextNode = null;
 | 
			
		||||
                let nextNodeIndex = null;
 | 
			
		||||
 | 
			
		||||
                if (wayIndex + 1 < ways.length)
 | 
			
		||||
                {
 | 
			
		||||
                if (wayIndex + 1 < ways.length) {
 | 
			
		||||
                    const nextNodeCandidates = elements[ways[wayIndex + 1]]
 | 
			
		||||
                        .nodes.filter(node => wayNodesSet.has(node));
 | 
			
		||||
 | 
			
		||||
                    if (nextNodeCandidates.length !== 1)
 | 
			
		||||
                    {
 | 
			
		||||
                    if (nextNodeCandidates.length !== 1) {
 | 
			
		||||
                        throw new Error(`There should be exactly one point
 | 
			
		||||
connecting way n°${wayIndex} and way n°${wayIndex + 1} in ${name},
 | 
			
		||||
but there are ${nextNodeCandidates.length}`);
 | 
			
		||||
| 
						 | 
				
			
			@ -173,24 +159,20 @@ but there are ${nextNodeCandidates.length}`);
 | 
			
		|||
 | 
			
		||||
                    nextNode = nextNodeCandidates[0];
 | 
			
		||||
                    nextNodeIndex = wayNodes.indexOf(nextNode);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                } else {
 | 
			
		||||
                    nextNodeIndex = wayNodes.length;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (curNodeIndex < nextNodeIndex)
 | 
			
		||||
                {
 | 
			
		||||
                if (curNodeIndex < nextNodeIndex) {
 | 
			
		||||
 | 
			
		||||
                    // Use the way in its normal direction
 | 
			
		||||
                    path = path.concat(
 | 
			
		||||
                        wayNodes.slice(curNodeIndex, nextNodeIndex)
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                } else {
 | 
			
		||||
 | 
			
		||||
                    // Use the way in the reverse direction
 | 
			
		||||
                    if (osm.isOneWay(way))
 | 
			
		||||
                    {
 | 
			
		||||
                    if (osm.isOneWay(way)) {
 | 
			
		||||
                        throw new Error(`Way n°${wayIndex} in
 | 
			
		||||
${name} is one-way and cannot be used in reverse.`);
 | 
			
		||||
                    }
 | 
			
		||||
| 
						 | 
				
			
			@ -205,8 +187,7 @@ ${name} is one-way and cannot be used in reverse.`);
 | 
			
		|||
            }
 | 
			
		||||
 | 
			
		||||
            // Split the path into segments between stops
 | 
			
		||||
            for (let stopIdx = 0; stopIdx + 1 < lineStops.length; ++stopIdx)
 | 
			
		||||
            {
 | 
			
		||||
            for (let stopIdx = 0; stopIdx + 1 < lineStops.length; ++stopIdx) {
 | 
			
		||||
                const begin = elements[lineStops[stopIdx]].tags.ref;
 | 
			
		||||
                const beginIdx = path.indexOf(lineStops[stopIdx]);
 | 
			
		||||
                const end = elements[lineStops[stopIdx + 1]].tags.ref;
 | 
			
		||||
| 
						 | 
				
			
			@ -218,29 +199,28 @@ ${name} is one-way and cannot be used in reverse.`);
 | 
			
		|||
                const id = `${begin}-${end}`;
 | 
			
		||||
                const nodesIds = path.slice(beginIdx, endIdx);
 | 
			
		||||
 | 
			
		||||
                if (id in segments)
 | 
			
		||||
                {
 | 
			
		||||
                if (id in segments) {
 | 
			
		||||
                    if (!util.arraysEqual(
 | 
			
		||||
                        nodesIds,
 | 
			
		||||
                        segments[id].properties.nodesIds
 | 
			
		||||
                    ))
 | 
			
		||||
                    {
 | 
			
		||||
                    )) {
 | 
			
		||||
                        throw new Error(`Segment ${id} is defined as a
 | 
			
		||||
different sequence of nodes in two or more lines.`);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    segments[id].properties.routes.push([lineRef, routeRef]);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    segments[id] = turfHelpers.lineString(nodesIds.map(id => [
 | 
			
		||||
                        elements[id].lon,
 | 
			
		||||
                        elements[id].lat
 | 
			
		||||
                    ]), {
 | 
			
		||||
                } else {
 | 
			
		||||
                    segments[id] = turfHelpers.lineString(nodesIds.map(
 | 
			
		||||
                        nodeId => [
 | 
			
		||||
                            elements[nodeId].lon,
 | 
			
		||||
                            elements[nodeId].lat
 | 
			
		||||
                        ]
 | 
			
		||||
                    ), {
 | 
			
		||||
 | 
			
		||||
                        // Keep track of the original sequence of nodes to
 | 
			
		||||
                        // compare with duplicates
 | 
			
		||||
                        nodesIds,
 | 
			
		||||
                        routes: [[lineRef, routeRef]],
 | 
			
		||||
                        routes: [[lineRef, routeRef]]
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                    segments[id].properties.length = (
 | 
			
		||||
| 
						 | 
				
			
			@ -249,24 +229,26 @@ different sequence of nodes in two or more lines.`);
 | 
			
		|||
            }
 | 
			
		||||
 | 
			
		||||
            routes.push({
 | 
			
		||||
                from, via, to,
 | 
			
		||||
                name, state,
 | 
			
		||||
                from,
 | 
			
		||||
                via,
 | 
			
		||||
                to,
 | 
			
		||||
                name,
 | 
			
		||||
                state
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        lines[lineRef] = {
 | 
			
		||||
            color,
 | 
			
		||||
            routes,
 | 
			
		||||
            routes
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Remove OSM nodes from segments that were only used for checking validity
 | 
			
		||||
    for (let segment of Object.values(segments))
 | 
			
		||||
    {
 | 
			
		||||
    for (const segment of Object.values(segments)) {
 | 
			
		||||
        delete segment.properties.nodesIds;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return {stops, lines, segments};
 | 
			
		||||
    return { stops, lines, segments };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
exports.fetch = fetch;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,5 @@
 | 
			
		|||
const tam = require('./sources/tam');
 | 
			
		||||
const util = require('../util');
 | 
			
		||||
const network = require('./network.json');
 | 
			
		||||
const tam = require("./sources/tam");
 | 
			
		||||
const network = require("./network.json");
 | 
			
		||||
 | 
			
		||||
// Time at which the course data needs to be updated next
 | 
			
		||||
let nextUpdate = null;
 | 
			
		||||
| 
						 | 
				
			
			@ -8,99 +7,78 @@ let nextUpdate = null;
 | 
			
		|||
// Current information about courses
 | 
			
		||||
let currentCourses = null;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Information about the course of a vehicle.
 | 
			
		||||
 * @typedef {Object} Course
 | 
			
		||||
 * @property {string} id Unique identifier for this course.
 | 
			
		||||
 * @property {string} line Transport line number.
 | 
			
		||||
 * @property {string} finalStop Final stop to which the course is headed.
 | 
			
		||||
 * @property {Object.<string,number>} nextPassings Next stations to which
 | 
			
		||||
 * the vehicle will stop, associated to the passing timestamp.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Fetch real-time information about active courses in the TaM network.
 | 
			
		||||
 *
 | 
			
		||||
 * New data will only be fetched from the TaM server once every minute,
 | 
			
		||||
 * otherwise pulling from the in-memory cache.
 | 
			
		||||
 *
 | 
			
		||||
 * The following information is provided for each active course:
 | 
			
		||||
 *
 | 
			
		||||
 * - `id`: Unique identifier for the course.
 | 
			
		||||
 * - `line`: Line number.
 | 
			
		||||
 * - `finalStop`: The final stop to which the course is headed.
 | 
			
		||||
 * - `nextPassings`: Next passings of the vehicle, as a dictionary associating
 | 
			
		||||
 *   each next stop to the passing timestamp.
 | 
			
		||||
 *
 | 
			
		||||
 * @return Mapping from active course IDs to information about each course.
 | 
			
		||||
 * @returns {Object.<string,Course>} Mapping from active course IDs to
 | 
			
		||||
 * information about each course.
 | 
			
		||||
 */
 | 
			
		||||
const getCourses = () => new Promise((res, rej) =>
 | 
			
		||||
{
 | 
			
		||||
    if (nextUpdate !== null && Date.now() < nextUpdate)
 | 
			
		||||
    {
 | 
			
		||||
        res(currentCourses);
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
const fetch = async() => {
 | 
			
		||||
    if (nextUpdate === null || Date.now() >= nextUpdate) {
 | 
			
		||||
        const courses = {};
 | 
			
		||||
        const passings = tam.fetchRealtime();
 | 
			
		||||
        const timing = (await passings.next()).value;
 | 
			
		||||
 | 
			
		||||
    const courses = {};
 | 
			
		||||
    let lastUpdate = null;
 | 
			
		||||
        nextUpdate = timing.nextUpdate;
 | 
			
		||||
 | 
			
		||||
    tam.fetchRealtime((err, entry) =>
 | 
			
		||||
    {
 | 
			
		||||
        if (err)
 | 
			
		||||
        {
 | 
			
		||||
            rej(err);
 | 
			
		||||
            return;
 | 
			
		||||
        // Aggregate passings relative to the same course
 | 
			
		||||
        for await (const passing of passings) {
 | 
			
		||||
            const {
 | 
			
		||||
                course: id,
 | 
			
		||||
                routeShortName: line,
 | 
			
		||||
                stopId,
 | 
			
		||||
                destArCode: finalStop
 | 
			
		||||
            } = passing;
 | 
			
		||||
 | 
			
		||||
            const arrivalTime = (
 | 
			
		||||
                timing.lastUpdate +
 | 
			
		||||
                parseInt(passing.delaySec, 10) * 1000
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            if (!(id in courses)) {
 | 
			
		||||
                courses[id] = {
 | 
			
		||||
                    id,
 | 
			
		||||
                    line,
 | 
			
		||||
                    finalStop,
 | 
			
		||||
                    nextPassings: { [stopId]: arrivalTime }
 | 
			
		||||
                };
 | 
			
		||||
            } else {
 | 
			
		||||
                courses[id].nextPassings[stopId] = arrivalTime;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!util.isObject(entry))
 | 
			
		||||
        {
 | 
			
		||||
            // Filter courses to only keep those referring to known data
 | 
			
		||||
            for (let courseId of Object.keys(courses))
 | 
			
		||||
            {
 | 
			
		||||
                const course = courses[courseId];
 | 
			
		||||
        // Filter courses to only keep those referring to known data
 | 
			
		||||
        for (const courseId of Object.keys(courses)) {
 | 
			
		||||
            const course = courses[courseId];
 | 
			
		||||
 | 
			
		||||
                if (!(course.line in network.lines))
 | 
			
		||||
                {
 | 
			
		||||
                    delete courses[courseId];
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    for (let stopId of Object.keys(course.nextPassings))
 | 
			
		||||
                    {
 | 
			
		||||
                        if (!(stopId in network.stops))
 | 
			
		||||
                        {
 | 
			
		||||
                            delete courses[courseId];
 | 
			
		||||
                            break;
 | 
			
		||||
                        }
 | 
			
		||||
            if (!(course.line in network.lines)) {
 | 
			
		||||
                delete courses[courseId];
 | 
			
		||||
            } else {
 | 
			
		||||
                for (const stopId of Object.keys(course.nextPassings)) {
 | 
			
		||||
                    if (!(stopId in network.stops)) {
 | 
			
		||||
                        delete courses[courseId];
 | 
			
		||||
                        break;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            currentCourses = courses;
 | 
			
		||||
            res(currentCourses);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ('lastUpdate' in entry)
 | 
			
		||||
        {
 | 
			
		||||
            // Metadata header
 | 
			
		||||
            lastUpdate = entry.lastUpdate;
 | 
			
		||||
            nextUpdate = entry.nextUpdate;
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        currentCourses = courses;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        const {
 | 
			
		||||
            course: id,
 | 
			
		||||
            routeShortName: line,
 | 
			
		||||
            stopId,
 | 
			
		||||
            destArCode: finalStop,
 | 
			
		||||
        } = entry;
 | 
			
		||||
    return currentCourses;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
        const arrivalTime = lastUpdate + parseInt(entry.delaySec, 10) * 1000;
 | 
			
		||||
 | 
			
		||||
        if (!(id in courses))
 | 
			
		||||
        {
 | 
			
		||||
            courses[id] = {
 | 
			
		||||
                id, line, finalStop,
 | 
			
		||||
                nextPassings: {[stopId]: arrivalTime},
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            courses[id].nextPassings[stopId] = arrivalTime;
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
exports.getCourses = getCourses;
 | 
			
		||||
exports.fetch = fetch;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,14 +1,12 @@
 | 
			
		|||
const axios = require('axios');
 | 
			
		||||
const turfAlong = require('@turf/along').default;
 | 
			
		||||
const turfProjection = require('@turf/projection');
 | 
			
		||||
const network = require('./network.json');
 | 
			
		||||
const axios = require("axios");
 | 
			
		||||
const turfAlong = require("@turf/along").default;
 | 
			
		||||
const turfProjection = require("@turf/projection");
 | 
			
		||||
const network = require("./network.json");
 | 
			
		||||
 | 
			
		||||
const server = 'http://localhost:4321';
 | 
			
		||||
const server = "http://localhost:4321";
 | 
			
		||||
 | 
			
		||||
class Course
 | 
			
		||||
{
 | 
			
		||||
    constructor(data)
 | 
			
		||||
    {
 | 
			
		||||
class Course {
 | 
			
		||||
    constructor(data) {
 | 
			
		||||
        this.id = data.id;
 | 
			
		||||
        this.passings = {};
 | 
			
		||||
        this.state = null;
 | 
			
		||||
| 
						 | 
				
			
			@ -29,18 +27,15 @@ class Course
 | 
			
		|||
        this.history = [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get currentSegment()
 | 
			
		||||
    {
 | 
			
		||||
        if (this.state !== 'moving')
 | 
			
		||||
        {
 | 
			
		||||
            return undefined;
 | 
			
		||||
    get currentSegment() {
 | 
			
		||||
        if (this.state !== "moving") {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return network.segments[`${this.departureStop}-${this.arrivalStop}`];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    updateData(data)
 | 
			
		||||
    {
 | 
			
		||||
    updateData(data) {
 | 
			
		||||
        this.line = data.line;
 | 
			
		||||
        this.finalStop = data.finalStop;
 | 
			
		||||
        Object.assign(this.passings, data.nextPassings);
 | 
			
		||||
| 
						 | 
				
			
			@ -48,104 +43,84 @@ class Course
 | 
			
		|||
        const now = Date.now();
 | 
			
		||||
 | 
			
		||||
        // Make sure we’re on the right `stopped`/`moving` state
 | 
			
		||||
        if (this.state === null)
 | 
			
		||||
        {
 | 
			
		||||
        if (this.state === null) {
 | 
			
		||||
            let previousStop = null;
 | 
			
		||||
            let departureTime = 0;
 | 
			
		||||
 | 
			
		||||
            let nextStop = null;
 | 
			
		||||
            let arrivalTime = Infinity;
 | 
			
		||||
 | 
			
		||||
            for (let [stopId, time] of Object.entries(this.passings))
 | 
			
		||||
            {
 | 
			
		||||
                if (time > now && time < arrivalTime)
 | 
			
		||||
                {
 | 
			
		||||
            for (const [stopId, time] of Object.entries(this.passings)) {
 | 
			
		||||
                if (time > now && time < arrivalTime) {
 | 
			
		||||
                    nextStop = stopId;
 | 
			
		||||
                    arrivalTime = time;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (time < now && time > departureTime)
 | 
			
		||||
                {
 | 
			
		||||
                if (time < now && time > departureTime) {
 | 
			
		||||
                    previousStop = stopId;
 | 
			
		||||
                    departureTime = time;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (nextStop === null)
 | 
			
		||||
            {
 | 
			
		||||
            if (nextStop === null) {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (previousStop === null)
 | 
			
		||||
            {
 | 
			
		||||
            if (previousStop === null) {
 | 
			
		||||
                // Teleport to the first known stop
 | 
			
		||||
                this.arriveToStop(nextStop);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
            } else {
 | 
			
		||||
 | 
			
		||||
                // Teleport to the first known segment
 | 
			
		||||
                this.arriveToStop(previousStop);
 | 
			
		||||
                this.moveToStop(nextStop, arrivalTime);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        else if (this.state === 'moving')
 | 
			
		||||
        {
 | 
			
		||||
            // Should already be at the next stop
 | 
			
		||||
            if (this.passings[this.arrivalStop] <= now)
 | 
			
		||||
            {
 | 
			
		||||
        } else if (this.state === "moving") {
 | 
			
		||||
            if (this.passings[this.arrivalStop] <= now) {
 | 
			
		||||
                // Should already be at the next stop
 | 
			
		||||
                this.arriveToStop(this.arrivalStop);
 | 
			
		||||
            }
 | 
			
		||||
            // On the right track, update the arrival time
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
            } else {
 | 
			
		||||
                // On the right track, update the arrival time
 | 
			
		||||
                this.arrivalTime = this.passings[this.arrivalStop];
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        else // this.state === 'stopped'
 | 
			
		||||
        {
 | 
			
		||||
        } else {
 | 
			
		||||
            // (this.state === 'stopped')
 | 
			
		||||
            // Try moving to the next stop
 | 
			
		||||
            let nextStop = null;
 | 
			
		||||
            let arrivalTime = Infinity;
 | 
			
		||||
 | 
			
		||||
            for (let [stopId, time] of Object.entries(this.passings))
 | 
			
		||||
            {
 | 
			
		||||
                if (time > now && time < arrivalTime)
 | 
			
		||||
                {
 | 
			
		||||
            for (const [stopId, time] of Object.entries(this.passings)) {
 | 
			
		||||
                if (time > now && time < arrivalTime) {
 | 
			
		||||
                    nextStop = stopId;
 | 
			
		||||
                    arrivalTime = time;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (nextStop === null)
 | 
			
		||||
            {
 | 
			
		||||
            if (nextStop === null) {
 | 
			
		||||
                // This course is finished
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (nextStop !== this.currentStop)
 | 
			
		||||
            {
 | 
			
		||||
            if (nextStop !== this.currentStop) {
 | 
			
		||||
                this.moveToStop(nextStop, arrivalTime);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.state === 'moving')
 | 
			
		||||
        {
 | 
			
		||||
        if (this.state === "moving") {
 | 
			
		||||
            this.speed = this.computeTheoreticalSpeed();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    tick(time)
 | 
			
		||||
    {
 | 
			
		||||
        if (this.state === 'moving')
 | 
			
		||||
        {
 | 
			
		||||
    tick(time) {
 | 
			
		||||
        if (this.state === "moving") {
 | 
			
		||||
 | 
			
		||||
            // Integrate current speed in travelled distance
 | 
			
		||||
            this.traveledDistance += this.speed * time;
 | 
			
		||||
            const segment = this.currentSegment;
 | 
			
		||||
 | 
			
		||||
            if (this.traveledDistance >= segment.properties.length)
 | 
			
		||||
            {
 | 
			
		||||
            if (this.traveledDistance >= segment.properties.length) {
 | 
			
		||||
                this.arriveToStop(this.arrivalStop);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
| 
						 | 
				
			
			@ -164,7 +139,7 @@ class Course
 | 
			
		|||
 | 
			
		||||
            this.angle = Math.atan2(
 | 
			
		||||
                positions[0][1] - positions[2][1],
 | 
			
		||||
                positions[2][0] - positions[0][0],
 | 
			
		||||
                positions[2][0] - positions[0][0]
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            this.position = positions[1];
 | 
			
		||||
| 
						 | 
				
			
			@ -173,42 +148,40 @@ class Course
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Transition this course to a state where it has arrived to a stop.
 | 
			
		||||
     *
 | 
			
		||||
     * @param stop Identifier for the stop to which the course arrives.
 | 
			
		||||
     * @param {string} stop Identifier for the stop to which
 | 
			
		||||
     * the course arrives.
 | 
			
		||||
     * @returns {undefined}
 | 
			
		||||
     */
 | 
			
		||||
    arriveToStop(stop)
 | 
			
		||||
    {
 | 
			
		||||
        this.state = 'stopped';
 | 
			
		||||
    arriveToStop(stop) {
 | 
			
		||||
        this.state = "stopped";
 | 
			
		||||
        this.currentStop = stop;
 | 
			
		||||
        this.position = (
 | 
			
		||||
            turfProjection.toMercator(network.stops[stop])
 | 
			
		||||
                .geometry.coordinates);
 | 
			
		||||
        this.history.push(['arriveToStop', stop]);
 | 
			
		||||
        this.history.push(["arriveToStop", stop]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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.
 | 
			
		||||
     * @param {string} stop Next stop for this course.
 | 
			
		||||
     * @param {number} arrivalTime Planned arrival time to that stop.
 | 
			
		||||
     * @returns {undefined}
 | 
			
		||||
     */
 | 
			
		||||
    moveToStop(stop, arrivalTime)
 | 
			
		||||
    {
 | 
			
		||||
        if (!(`${this.currentStop}-${stop}` in network.segments))
 | 
			
		||||
        {
 | 
			
		||||
    moveToStop(stop, arrivalTime) {
 | 
			
		||||
        if (!(`${this.currentStop}-${stop}` in network.segments)) {
 | 
			
		||||
            console.warn(`Course ${this.id} cannot go from stop
 | 
			
		||||
${this.currentStop} to stop ${stop}. Teleporting to ${stop}`);
 | 
			
		||||
            this.arriveToStop(stop);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.state = 'moving';
 | 
			
		||||
        this.state = "moving";
 | 
			
		||||
        this.departureStop = this.currentStop;
 | 
			
		||||
        this.arrivalStop = stop;
 | 
			
		||||
        this.arrivalTime = arrivalTime;
 | 
			
		||||
        this.traveledDistance = 0;
 | 
			
		||||
        this.speed = 0;
 | 
			
		||||
        this.history.push(['moveToStop', stop, arrivalTime]);
 | 
			
		||||
        this.history.push(["moveToStop", stop, arrivalTime]);
 | 
			
		||||
 | 
			
		||||
        console.info(`Course ${this.id} leaving stop ${this.currentStop} \
 | 
			
		||||
with initial speed ${this.computeTheoreticalSpeed() * 3600} km/h`);
 | 
			
		||||
| 
						 | 
				
			
			@ -216,11 +189,10 @@ with initial speed ${this.computeTheoreticalSpeed() * 3600} km/h`);
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Compute the speed that needs to be maintained to arrive on time.
 | 
			
		||||
     * @returns {number} Speed in meters per millisecond.
 | 
			
		||||
     */
 | 
			
		||||
    computeTheoreticalSpeed()
 | 
			
		||||
    {
 | 
			
		||||
        if (this.state !== 'moving')
 | 
			
		||||
        {
 | 
			
		||||
    computeTheoreticalSpeed() {
 | 
			
		||||
        if (this.state !== "moving") {
 | 
			
		||||
            return 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -230,47 +202,34 @@ with initial speed ${this.computeTheoreticalSpeed() * 3600} km/h`);
 | 
			
		|||
            segment.properties.length - this.traveledDistance
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        if (remainingDistance <= 0)
 | 
			
		||||
        {
 | 
			
		||||
        if (remainingDistance <= 0) {
 | 
			
		||||
            return 0;
 | 
			
		||||
        }
 | 
			
		||||
        else if (remainingTime <= 0)
 | 
			
		||||
        {
 | 
			
		||||
        if (remainingTime <= 0) {
 | 
			
		||||
            // We’re late, go to maximum speed
 | 
			
		||||
            return 50 / 3600; // 50 km/h
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            return remainingDistance / remainingTime;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return remainingDistance / remainingTime;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const updateData = async (courses) =>
 | 
			
		||||
{
 | 
			
		||||
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)
 | 
			
		||||
        {
 | 
			
		||||
            if (!courses[id].updateData(data))
 | 
			
		||||
            {
 | 
			
		||||
    for (const [id, data] of Object.entries(dataset)) {
 | 
			
		||||
        if (id in courses) {
 | 
			
		||||
            if (!courses[id].updateData(data)) {
 | 
			
		||||
                console.info(`Course ${id} is finished.`);
 | 
			
		||||
                delete courses[id];
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
        } else {
 | 
			
		||||
            const newCourse = new Course(data);
 | 
			
		||||
 | 
			
		||||
            if (!newCourse.updateData(data))
 | 
			
		||||
            {
 | 
			
		||||
            if (!newCourse.updateData(data)) {
 | 
			
		||||
                console.info(`Ignoring course ${id} which is outdated.`);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
            } else {
 | 
			
		||||
                console.info(`Course ${id} starting.`);
 | 
			
		||||
                courses[id] = newCourse;
 | 
			
		||||
            }
 | 
			
		||||
| 
						 | 
				
			
			@ -278,45 +237,39 @@ const updateData = async (courses) =>
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    // Remove stale courses
 | 
			
		||||
    for (let id of Object.keys(courses))
 | 
			
		||||
    {
 | 
			
		||||
        if (!(id in dataset))
 | 
			
		||||
        {
 | 
			
		||||
    for (const id of Object.keys(courses)) {
 | 
			
		||||
        if (!(id in dataset)) {
 | 
			
		||||
            delete courses[id];
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const tick = (courses, time) =>
 | 
			
		||||
{
 | 
			
		||||
    for (let course of Object.values(courses))
 | 
			
		||||
    {
 | 
			
		||||
const tick = (courses, time) => {
 | 
			
		||||
    for (const course of Object.values(courses)) {
 | 
			
		||||
        course.tick(time);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const start = () =>
 | 
			
		||||
{
 | 
			
		||||
const start = () => {
 | 
			
		||||
    const courses = {};
 | 
			
		||||
    let lastFrame = null;
 | 
			
		||||
    let lastUpdate = null;
 | 
			
		||||
 | 
			
		||||
    const update = () =>
 | 
			
		||||
    {
 | 
			
		||||
    const update = () => {
 | 
			
		||||
        const now = Date.now();
 | 
			
		||||
 | 
			
		||||
        if (lastUpdate === null || lastUpdate + 5000 <= now)
 | 
			
		||||
        {
 | 
			
		||||
        if (lastUpdate === null || lastUpdate + 5000 <= now) {
 | 
			
		||||
            lastUpdate = now;
 | 
			
		||||
            updateData(courses);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const time = lastFrame === null ? 0 : now - lastFrame;
 | 
			
		||||
 | 
			
		||||
        lastFrame = now;
 | 
			
		||||
        tick(courses, time);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return {courses, update};
 | 
			
		||||
    return { courses, update };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
exports.start = start;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,77 +1,55 @@
 | 
			
		|||
/**
 | 
			
		||||
 * @file
 | 
			
		||||
 * @fileoverview
 | 
			
		||||
 *
 | 
			
		||||
 * Interface with the OpenStreetMap collaborative mapping database.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
const axios = require('axios');
 | 
			
		||||
const {isObject} = require('../../util');
 | 
			
		||||
const axios = require("axios");
 | 
			
		||||
const { isObject } = require("../../util");
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Submit a query to an Overpass endpoint.
 | 
			
		||||
 *
 | 
			
		||||
 * See <https://wiki.openstreetmap.org/wiki/Overpass_API/Overpass_QL> for more
 | 
			
		||||
 * See <https://wiki.osm.org/Overpass_API/Overpass_QL> for more
 | 
			
		||||
 * information on the Overpass Query Language (Overpass QL).
 | 
			
		||||
 *
 | 
			
		||||
 * @async
 | 
			
		||||
 * @param query Query to send.
 | 
			
		||||
 * @param [endpoint] Overpass endpoint to use.
 | 
			
		||||
 * @return Results returned by the endpoint. If JSON output is requested in
 | 
			
		||||
 * the query, the result will automatically be parsed into a JS object.
 | 
			
		||||
 * @param {string} query Query to send.
 | 
			
		||||
 * @param {string} [endpoint] Overpass endpoint to use.
 | 
			
		||||
 * @returns {string|Object} Results returned by the endpoint. If JSON output
 | 
			
		||||
 * is requested in the query, the result will automatically be parsed into
 | 
			
		||||
 * a JS object.
 | 
			
		||||
 */
 | 
			
		||||
const runQuery = (
 | 
			
		||||
    query,
 | 
			
		||||
    endpoint = 'https://lz4.overpass-api.de/api/interpreter'
 | 
			
		||||
    endpoint = "https://lz4.overpass-api.de/api/interpreter"
 | 
			
		||||
) => (
 | 
			
		||||
    axios.post(endpoint, 'data=' + query)
 | 
			
		||||
    .then(res => res.data)
 | 
			
		||||
    axios.post(endpoint, `data=${query}`)
 | 
			
		||||
        .then(res => res.data)
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
exports.runQuery = runQuery;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Create a link to add tags into JOSM.
 | 
			
		||||
 *
 | 
			
		||||
 * The JOSM remote control must be activated and JOSM must be running for this
 | 
			
		||||
 * link to work. See <https://wiki.openstreetmap.org/wiki/JOSM/RemoteControl>.
 | 
			
		||||
 *
 | 
			
		||||
 * @param id Identifier for the object to add the tags to.
 | 
			
		||||
 * @param tags List of tags to add, in the `key=value` format.
 | 
			
		||||
 * @return Link for remotely adding the tags.
 | 
			
		||||
 */
 | 
			
		||||
const addTagsToNode = (id, tags) => (
 | 
			
		||||
    'http://127.0.0.1:8111/load_object?' + [
 | 
			
		||||
        `objects=n${id}`,
 | 
			
		||||
        'new_layer=false',
 | 
			
		||||
        'addtags=' + tags.join('%7C'),
 | 
			
		||||
    ].join('&')
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
exports.addTagsToNode = addTagsToNode;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Create a link to view a node.
 | 
			
		||||
 *
 | 
			
		||||
 * @param id Identifier for the node to view.
 | 
			
		||||
 * @return Link to view this node on the OSM website.
 | 
			
		||||
 * @param {string|number} id Identifier for the node to view.
 | 
			
		||||
 * @returns {string} Link to view this node on the OSM website.
 | 
			
		||||
 */
 | 
			
		||||
const viewNode = id => `https://www.openstreetmap.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.
 | 
			
		||||
 *
 | 
			
		||||
 * See <https://wiki.openstreetmap.org/wiki/Key:oneway> for details.
 | 
			
		||||
 *
 | 
			
		||||
 * @param tags Set of tags of the way.
 | 
			
		||||
 * @return True iff. the way is one-way.
 | 
			
		||||
 * See <https://wiki.osm.org/Key:oneway> for details.
 | 
			
		||||
 * @param {Object} obj OSM way object.
 | 
			
		||||
 * @returns {boolean} Whether the way is one-way.
 | 
			
		||||
 */
 | 
			
		||||
const isOneWay = object => (
 | 
			
		||||
    object.type === 'way'
 | 
			
		||||
    && isObject(object.tags)
 | 
			
		||||
    && (object.tags.oneway === 'yes' || object.tags.junction === 'roundabout'
 | 
			
		||||
        || object.tags.highway === 'motorway')
 | 
			
		||||
const isOneWay = obj => (
 | 
			
		||||
    obj.type === "way" &&
 | 
			
		||||
    isObject(obj.tags) &&
 | 
			
		||||
    (obj.tags.oneway === "yes" || obj.tags.junction === "roundabout" ||
 | 
			
		||||
        obj.tags.highway === "motorway")
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
exports.isOneWay = isOneWay;
 | 
			
		||||
| 
						 | 
				
			
			@ -79,16 +57,15 @@ exports.isOneWay = isOneWay;
 | 
			
		|||
/**
 | 
			
		||||
 * Determine if an OSM object is a public transport line (route master).
 | 
			
		||||
 *
 | 
			
		||||
 * See <https://wiki.openstreetmap.org/wiki/Relation:route_master>
 | 
			
		||||
 * and <https://wiki.openstreetmap.org/wiki/Public_transport#Route_Master_relations>.
 | 
			
		||||
 *
 | 
			
		||||
 * @param object OSM object.
 | 
			
		||||
 * @return True iff. the relation is a public transport line.
 | 
			
		||||
 * See <https://wiki.osm.org/Relation:route_master>
 | 
			
		||||
 * and <https://wiki.osm.org/Public_transport#Route_Master_relations>.
 | 
			
		||||
 * @param {Object} obj OSM relation object.
 | 
			
		||||
 * @returns {boolean} Whether the relation is a public transport line.
 | 
			
		||||
 */
 | 
			
		||||
const isTransportLine = object => (
 | 
			
		||||
    object.type === 'relation'
 | 
			
		||||
    && isObject(object.tags)
 | 
			
		||||
    && object.tags.type === 'route_master'
 | 
			
		||||
const isTransportLine = obj => (
 | 
			
		||||
    obj.type === "relation" &&
 | 
			
		||||
    isObject(obj.tags) &&
 | 
			
		||||
    obj.tags.type === "route_master"
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
exports.isTransportLine = isTransportLine;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,123 +1,104 @@
 | 
			
		|||
const unzip = require('unzip-stream');
 | 
			
		||||
const csv = require('csv-parse');
 | 
			
		||||
const axios = require('axios');
 | 
			
		||||
const csv = require("csv-parse");
 | 
			
		||||
const axios = require("axios");
 | 
			
		||||
const { snakeToCamelCase, unzipFile } = require("../../util");
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Process a CSV stream to extract passings.
 | 
			
		||||
 * Data available for each passing of a vehicle at a station.
 | 
			
		||||
 *
 | 
			
		||||
 * @private
 | 
			
		||||
 * @param csvStream Stream containing CSV data.
 | 
			
		||||
 * @param callback See fetchRealtime for a description of the callback.
 | 
			
		||||
 * See also <http://data.montpellier3m.fr/node/10733/download>.
 | 
			
		||||
 * @typedef {Object} Passing
 | 
			
		||||
 * @property {string} course Identifier of the overall trip of the same vehicle
 | 
			
		||||
 * from one end of the route to another (unique for the day).
 | 
			
		||||
 * @property {string} stopCode Unused internal stop identifier.
 | 
			
		||||
 * @property {string} stopId Unique network identifier for the station at
 | 
			
		||||
 * which the vehicle will pass (same id as in GTFS).
 | 
			
		||||
 * @property {string} routeShortName Transport line number.
 | 
			
		||||
 * @property {string} tripHeadsign Name of the final stop of this trip.
 | 
			
		||||
 * @property {string} directionId Route identifier inside the line.
 | 
			
		||||
 * @property {string} departureTime Theoretical time at which the
 | 
			
		||||
 * vehicle will depart the stop (HH:MM:SS format).
 | 
			
		||||
 * @property {string} isTheorical (sic) Whether the arrival time is only
 | 
			
		||||
 * a theoretical information.
 | 
			
		||||
 * @property {string} delaySec Number of seconds before the vehicle arrives
 | 
			
		||||
 * at the station.
 | 
			
		||||
 * @property {string} destArCode Unique network identifier for the final
 | 
			
		||||
 * stop of this trip.
 | 
			
		||||
 */
 | 
			
		||||
const processTamPassingStream = (csvStream, callback) =>
 | 
			
		||||
{
 | 
			
		||||
    const parser = csv({
 | 
			
		||||
        delimiter: ';',
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const rowStream = csvStream.pipe(parser);
 | 
			
		||||
 | 
			
		||||
    rowStream.on('readable', () =>
 | 
			
		||||
    {
 | 
			
		||||
        let row;
 | 
			
		||||
 | 
			
		||||
        while ((row = rowStream.read()))
 | 
			
		||||
        {
 | 
			
		||||
            if (row.length === 0 || row[0] === 'course')
 | 
			
		||||
            {
 | 
			
		||||
                // Ignore les lignes invalides et l’en-tête
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            callback(null, {
 | 
			
		||||
                course: row[0],
 | 
			
		||||
                stopCode: row[1],
 | 
			
		||||
                stopId: row[2],
 | 
			
		||||
                stopName: row[3],
 | 
			
		||||
                routeShortName: row[4],
 | 
			
		||||
                tripHeadsign: row[5],
 | 
			
		||||
                directionId: row[6],
 | 
			
		||||
                departureTime: row[7],
 | 
			
		||||
                isTheorical: row[8],
 | 
			
		||||
                delaySec: row[9],
 | 
			
		||||
                destArCode: row[10],
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    rowStream.on('end', () => callback(null, null));
 | 
			
		||||
    rowStream.on('error', err => callback(err));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const tamRealtimeEndpoint = 'http://data.montpellier3m.fr/node/10732/download';
 | 
			
		||||
const realtimeEndpoint = "http://data.montpellier3m.fr/node/10732/download";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Fetch realtime passings across the network.
 | 
			
		||||
 *
 | 
			
		||||
 * The callback always receives two arguments. If an error occurs, the first
 | 
			
		||||
 * argument will contain an object with information about the error. Otherwise,
 | 
			
		||||
 * it will be null and the second argument will be the payload.
 | 
			
		||||
 *
 | 
			
		||||
 * The first call will provide metadata, specifically the time at which
 | 
			
		||||
 * the data that follows was last updated (`lastUpdate`) and the time at
 | 
			
		||||
 * which it will be updated next (`nextUpdate`).
 | 
			
		||||
 *
 | 
			
		||||
 * Following calls will provide each passing of the dataset individually,
 | 
			
		||||
 * and will be closed with a call where both arguments are null.
 | 
			
		||||
 *
 | 
			
		||||
 * @param callback Called for each passing during parsing.
 | 
			
		||||
 * Fetch real time passings of vehicles across the network.
 | 
			
		||||
 * @yields {{{lastUpdate: number, nextUpdate: number}|Passing}} First value
 | 
			
		||||
 * is an object containing the time of last update and the time of next
 | 
			
		||||
 * update of this information. Next values are informations about each vehicle
 | 
			
		||||
 * passing.
 | 
			
		||||
 */
 | 
			
		||||
const fetchRealtime = callback =>
 | 
			
		||||
{
 | 
			
		||||
    axios.get(tamRealtimeEndpoint, {
 | 
			
		||||
        responseType: 'stream'
 | 
			
		||||
    }).then(res =>
 | 
			
		||||
    {
 | 
			
		||||
        const lastUpdate = new Date(res.headers['last-modified']).getTime();
 | 
			
		||||
const fetchRealtime = async function *() {
 | 
			
		||||
    const res = await axios.get(realtimeEndpoint, {
 | 
			
		||||
        responseType: "stream"
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
        // Data is advertised as being updated every minute. Add a small
 | 
			
		||||
        // margin to account for potential delays
 | 
			
		||||
        const nextUpdate = lastUpdate + 65 * 1000;
 | 
			
		||||
    const lastUpdate = new Date(res.headers["last-modified"]).getTime();
 | 
			
		||||
    const nextUpdate = lastUpdate + 65 * 1000;
 | 
			
		||||
 | 
			
		||||
        callback(null, {lastUpdate, nextUpdate});
 | 
			
		||||
        processTamPassingStream(res.data, callback);
 | 
			
		||||
    }).catch(err => callback(err));
 | 
			
		||||
    yield { lastUpdate, nextUpdate };
 | 
			
		||||
 | 
			
		||||
    const parser = res.data.pipe(csv({
 | 
			
		||||
        delimiter: ";",
 | 
			
		||||
        columns: header => header.map(snakeToCamelCase)
 | 
			
		||||
    }));
 | 
			
		||||
 | 
			
		||||
    for await (const passing of parser) {
 | 
			
		||||
        yield passing;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
exports.fetchRealtime = fetchRealtime;
 | 
			
		||||
 | 
			
		||||
const tamTheoreticalEndpoint =
 | 
			
		||||
    'http://data.montpellier3m.fr/node/10731/download';
 | 
			
		||||
const tamTheoreticalFileName = 'offre_du_jour.csv';
 | 
			
		||||
const theoreticalEndpoint = "http://data.montpellier3m.fr/node/10731/download";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Fetch theoretical passings for the current day across the network.
 | 
			
		||||
 *
 | 
			
		||||
 * @param callback Called for each passing during parsing. First argument will
 | 
			
		||||
 * be non-null only if an error occurred. Second argument will contain passings
 | 
			
		||||
 * or be null if the end was reached.
 | 
			
		||||
 * @yields {{{lastUpdate: number, nextUpdate: number}|Passing}} First value
 | 
			
		||||
 * is an object containing the time of last update and the time of next
 | 
			
		||||
 * update of this information. Next values are informations about each vehicle
 | 
			
		||||
 * passing.
 | 
			
		||||
 */
 | 
			
		||||
const fetchTheoretical = callback =>
 | 
			
		||||
{
 | 
			
		||||
    axios.get(tamTheoreticalEndpoint, {
 | 
			
		||||
        responseType: 'stream'
 | 
			
		||||
    }).then(res =>
 | 
			
		||||
    {
 | 
			
		||||
        const fileStream = res.data.pipe(unzip.Parse());
 | 
			
		||||
 | 
			
		||||
        fileStream.on('entry', entry =>
 | 
			
		||||
        {
 | 
			
		||||
            if (entry.type !== 'File' || entry.path !== tamTheoreticalFileName)
 | 
			
		||||
            {
 | 
			
		||||
                entry.autodrain();
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            processTamPassingStream(entry, callback);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        fileStream.on('error', err => callback(err));
 | 
			
		||||
const fetchTheoretical = async function *() {
 | 
			
		||||
    const res = await axios.get(theoreticalEndpoint, {
 | 
			
		||||
        responseType: "stream"
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const lastUpdate = new Date();
 | 
			
		||||
 | 
			
		||||
    if (lastUpdate.getHours() < 4) {
 | 
			
		||||
        lastUpdate.setDate(lastUpdate.getDate() - 1);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    lastUpdate.setHours(4);
 | 
			
		||||
    lastUpdate.setMinutes(0);
 | 
			
		||||
    lastUpdate.setSeconds(0);
 | 
			
		||||
    lastUpdate.setMilliseconds(0);
 | 
			
		||||
 | 
			
		||||
    const nextUpdate = new Date(lastUpdate);
 | 
			
		||||
 | 
			
		||||
    nextUpdate.setDate(nextUpdate.getDate() + 1);
 | 
			
		||||
 | 
			
		||||
    yield {
 | 
			
		||||
        lastUpdate: lastUpdate.getTime(),
 | 
			
		||||
        nextUpdate: nextUpdate.getTime()
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const stream = await unzipFile(res.data, "offre_du_jour.csv");
 | 
			
		||||
    const parser = stream.pipe(csv({
 | 
			
		||||
        delimiter: ";",
 | 
			
		||||
        columns: header => header.map(snakeToCamelCase)
 | 
			
		||||
    }));
 | 
			
		||||
 | 
			
		||||
    for await (const passing of parser) {
 | 
			
		||||
        yield passing;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
exports.fetchTheoretical = fetchTheoretical;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										115
									
								
								src/util.js
								
								
								
								
							
							
						
						
									
										115
									
								
								src/util.js
								
								
								
								
							| 
						 | 
				
			
			@ -1,86 +1,65 @@
 | 
			
		|||
/**
 | 
			
		||||
 * Choose between singular or plural form based on the number of elements.
 | 
			
		||||
 *
 | 
			
		||||
 * @example
 | 
			
		||||
 * > choosePlural(1, 'example', '.s')
 | 
			
		||||
 * 'example'
 | 
			
		||||
 * > choosePlural(4, 'example', '.s')
 | 
			
		||||
 * 'examples'
 | 
			
		||||
 * > choosePlural(0, 'radius', 'radii')
 | 
			
		||||
 * 'radii'
 | 
			
		||||
 *
 | 
			
		||||
 * @param count Number of elements.
 | 
			
		||||
 * @param singular Singular form.
 | 
			
		||||
 * @param plural Plural form. An initial dot will be replaced by `singular`.
 | 
			
		||||
 * @return Appropriate form.
 | 
			
		||||
 */
 | 
			
		||||
const choosePlural = (count, singular, plural) =>
 | 
			
		||||
{
 | 
			
		||||
    if (count === 1)
 | 
			
		||||
    {
 | 
			
		||||
        return singular;
 | 
			
		||||
    }
 | 
			
		||||
    else
 | 
			
		||||
    {
 | 
			
		||||
        return plural.replace(/^\./, singular);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
exports.choosePlural = choosePlural;
 | 
			
		||||
const unzip = require("unzip-stream");
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Join elements with the given separator and a special separator for the last
 | 
			
		||||
 * element.
 | 
			
		||||
 *
 | 
			
		||||
 * @example
 | 
			
		||||
 * > joinSentence(['apple', 'orange', 'banana'], ', ', ' and ')
 | 
			
		||||
 * 'apple, orange and banana'
 | 
			
		||||
 * > joinSentence(['apple', 'banana'], ', ', ' and ')
 | 
			
		||||
 * 'apple and banana'
 | 
			
		||||
 * > joinSentence(['banana'], ', ', ' and ')
 | 
			
		||||
 * 'banana'
 | 
			
		||||
 *
 | 
			
		||||
 * @param array Sequence of strings to join.
 | 
			
		||||
 * @param separator Separator for all elements but the last one.
 | 
			
		||||
 * @param lastSeparator Separator for the last element.
 | 
			
		||||
 * @return Joined string.
 | 
			
		||||
 * Convert a snake-cased string to a camel-cased one.
 | 
			
		||||
 * @param {string} str Original string.
 | 
			
		||||
 * @returns {string} Transformed string.
 | 
			
		||||
 */
 | 
			
		||||
const joinSentence = (array, separator, lastSeparator) =>
 | 
			
		||||
{
 | 
			
		||||
    if (array.length <= 2)
 | 
			
		||||
    {
 | 
			
		||||
        return array.join(lastSeparator);
 | 
			
		||||
    }
 | 
			
		||||
const snakeToCamelCase = str => str.replace(/([-_][a-z])/gu, group =>
 | 
			
		||||
    group.toUpperCase().replace("-", "").replace("_", ""));
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        array.slice(0, -1).join(separator)
 | 
			
		||||
            + lastSeparator
 | 
			
		||||
            + array[array.length - 1]
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
exports.joinSentence = joinSentence;
 | 
			
		||||
exports.snakeToCamelCase = snakeToCamelCase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Check if a value is a JS object.
 | 
			
		||||
 *
 | 
			
		||||
 * @param value Value to check.
 | 
			
		||||
 * @return True iff. `value` is a JS object.
 | 
			
		||||
 * @param {*} value Value to check.
 | 
			
		||||
 * @returns {boolean} Whether `value` is a JS object.
 | 
			
		||||
 */
 | 
			
		||||
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.
 | 
			
		||||
 *
 | 
			
		||||
 * @param array1 First array.
 | 
			
		||||
 * @param array2 Second array.
 | 
			
		||||
 * @return True iff. the two arrays are equal.
 | 
			
		||||
 * @param {Array} array1 First array.
 | 
			
		||||
 * @param {Array} array2 Second array.
 | 
			
		||||
 * @returns {boolean} Whether the two arrays are equal.
 | 
			
		||||
 */
 | 
			
		||||
const arraysEqual = (array1, array2) => (
 | 
			
		||||
    array1.length === array2.length
 | 
			
		||||
    && array1.every((elt1, index) => elt1 === array2[index])
 | 
			
		||||
    array1.length === array2.length &&
 | 
			
		||||
    array1.every((elt1, index) => elt1 === array2[index])
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
exports.arraysEqual = arraysEqual;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Find a file in a zipped stream and unzip it.
 | 
			
		||||
 * @param {stream.Readable} data Input zipped stream.
 | 
			
		||||
 * @param {string} fileName Name of the file to find.
 | 
			
		||||
 * @returns {Promise.<stream.Readable>} Stream of the unzipped file.
 | 
			
		||||
 */
 | 
			
		||||
const unzipFile = (data, fileName) => new Promise((res, rej) => {
 | 
			
		||||
    // eslint-disable-next-line new-cap
 | 
			
		||||
    const stream = data.pipe(unzip.Parse());
 | 
			
		||||
    let found = false;
 | 
			
		||||
 | 
			
		||||
    stream.on("entry", entry => {
 | 
			
		||||
        if (entry.type !== "File" || entry.path !== fileName) {
 | 
			
		||||
            entry.autodrain();
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        found = true;
 | 
			
		||||
        res(entry);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    stream.on("end", () => {
 | 
			
		||||
        if (!found) {
 | 
			
		||||
            rej(new Error(`File ${fileName} not found in archive`));
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    stream.on("error", err => rej(err));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
exports.unzipFile = unzipFile;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue