From 944207ed8a4d202bd791b057598eaaaf5b9ae112 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matt=C3=A9o=20Delabre?= Date: Fri, 17 Jul 2020 12:13:25 +0200 Subject: [PATCH] Split data/endpoints.js into osm and tam --- back/data/network.js | 70 +++++------------ back/data/sources/osm.js | 89 ++++++++++++++++++++++ back/data/{endpoints.js => sources/tam.js} | 26 ++----- back/util.js | 10 +++ 4 files changed, 125 insertions(+), 70 deletions(-) create mode 100644 back/data/sources/osm.js rename back/data/{endpoints.js => sources/tam.js} (81%) diff --git a/back/data/network.js b/back/data/network.js index 72e04db..567ef5a 100644 --- a/back/data/network.js +++ b/back/data/network.js @@ -1,30 +1,20 @@ /** * @file + * * Extract static information about the TaM network from OpenStreetMap (OSM): * tram and bus lines, stops and routes. * * Functions in this file also report and offer to correct errors that may * occur in OSM data. + * + * Because of the static nature of this data, it is cached in a + * version-controlled file `network.json` next to this file. To update it, use + * the `script/update-network` script. */ const {choosePlural, joinSentence} = require('../util'); -const {queryOverpass, fetchTamTheoretical} = require('./endpoints'); - -const osmViewNode = 'https://www.openstreetmap.org/node'; - -/** - * Create a link to add tags into JOSM. - * - * @param objectId Identifier for the object to add the tags to. - * @param tags Tags to add. - * @return Link for remotely adding the tags. - */ -const josmAddTagToNode = (objectId, tags) => - 'http://127.0.0.1:8111/load_object?' + [ - `objects=n${objectId}`, - 'new_layer=false', - 'addtags=' + tags.join('%7C'), - ].join('&'); +const osm = require('./sources/osm'); +const tam = require('./sources/tam'); /** * Use theoretical passings data to guess which lines use which stops in which @@ -40,7 +30,7 @@ const fetchStopsRefAssociations = () => new Promise((res, rej) => { const stops = {}; - fetchTamTheoretical((err, row) => + tam.fetchTheoretical((err, row) => { if (err) { @@ -131,17 +121,6 @@ const matchStopNames = (fullName, abbrName) => ).length; }; -/** - * Determine if an OSM way is oneway or not. - * - * @param tags Set of tags of the way. - * @return True iff. the way is oneway. - */ -const isOneWay = tags => - ('oneway' in tags && tags.oneway === 'yes') - || ('junction' in tags && tags.junction === 'roundabout') - || ('highway' in tags && tags.highway === 'motorway'); - /** * Fetch stops and lines of the network. * @@ -151,7 +130,7 @@ const isOneWay = tags => const fetch = async (lineRefs) => { // Retrieve routes, ways and stops from OpenStreetMap - const rawData = await queryOverpass(`[out:json]; + 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('|')})$"]; @@ -169,9 +148,7 @@ out body qt; const elementsList = rawData.elements; // List of retrieved lines - const routeMasters = elementsList.filter(elt => - elt.tags && elt.tags.type === 'route_master' - ); + const routeMasters = elementsList.filter(osm.isTransportLine); // Retrieved objects indexed by ID const elements = elementsList.reduce((prev, elt) => @@ -208,7 +185,7 @@ out body qt; console.warn(`Stop ${stop.id} is missing a “ref” tag Name: ${stop.tags.name} Part of line: ${route.tags.name} -URI: ${osmViewNode}/${stop.id} +URI: ${osm.viewNode(stop.id)} `); // Try to identify stops matching this stop in the @@ -262,7 +239,7 @@ URI: ${osmViewNode}/${stop.id} ${choosePlural(candidate.lines.length, 'line', '.s')} \ ${joinSentence(Array.from(candidate.lines), ', ', ' and ')} going to \ ${joinSentence(Array.from(candidate.directions), ', ', ' or ')} - Apply in JOSM: ${josmAddTagToNode(stop.id, ['ref=' + candidate.stopRef])} + Apply in JOSM: ${osm.addTagsToNode(stop.id, ['ref=' + candidate.stopRef])} `); } } @@ -292,11 +269,7 @@ ${joinSentence(Array.from(candidate.directions), ', ', ' or ')} for (let route of rawRoutes) { - const {from, to} = route.tags; - - // Human-readable description of the route for errors - const routeDescription = `line ${lineRef}’s route from \ -“${route.tags.from}” to “${route.tags.to}”`; + const {from, to, name} = route.tags; // Check that the route consists of a block of stops and platforms // followed by a block of routes as dictated by PTv2 @@ -309,7 +282,7 @@ ${joinSentence(Array.from(candidate.directions), ', ', ' or ')} )) { throw new Error(`Members with invalid roles in between stops -of ${routeDescription}`); +of ${name}`); } if (!route.members.slice(relationPivot).every( @@ -317,7 +290,7 @@ of ${routeDescription}`); )) { throw new Error(`Members with invalid roles inside the path -of ${routeDescription}`); +of ${name}`); } // List of stops in the route, expected to be in the timetable @@ -338,8 +311,8 @@ of ${routeDescription}`); for (let wayIndex = 0; wayIndex < ways.length; wayIndex += 1) { - const {nodes: wayNodes, tags: wayTags} - = elements[ways[wayIndex]]; + const way = elements[ways[wayIndex]]; + const {nodes: wayNodes, tags: wayTags} = way; const wayNodesSet = new Set(wayNodes); const curNodeIndex = wayNodes.indexOf(currentNode); @@ -357,7 +330,7 @@ of ${routeDescription}`); if (nextNodeCandidates.length !== 1) { throw new Error(`There should be exactly one point -connecting way n°${wayIndex} and way n°${wayIndex + 1} in ${routeDescription}, +connecting way n°${wayIndex} and way n°${wayIndex + 1} in ${name}, but there are ${nextNodeCandidates.length}`); } @@ -379,10 +352,10 @@ but there are ${nextNodeCandidates.length}`); else { // Use the way in the reverse direction - if (isOneWay(wayTags)) + if (osm.isOneWay(way)) { throw new Error(`Way n°${wayIndex} in -${routeDescription} is one-way and cannot be used in reverse.`); +${name} is one-way and cannot be used in reverse.`); } path = path.concat( @@ -409,8 +382,7 @@ ${routeDescription} is one-way and cannot be used in reverse.`); } routes.push({ - from, to, - name: route.tags.name, + from, to, name, segments, stops: stops.map(id => elements[id].tags.ref), }); diff --git a/back/data/sources/osm.js b/back/data/sources/osm.js new file mode 100644 index 0000000..a4aff03 --- /dev/null +++ b/back/data/sources/osm.js @@ -0,0 +1,89 @@ +/** + * @file + * + * Interface with the OpenStreetMap collaborative mapping database. + */ + +const axios = require('axios'); +const {isObject} = require('../../util'); + +/** + * Submit a query to an Overpass endpoint. + * + * See 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. + */ +const runQuery = ( + query, + endpoint = 'https://lz4.overpass-api.de/api/interpreter' +) => 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 . + * + * @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. + */ +const viewNode = id => `https://www.openstreetmap.org/node/${id}`; + +exports.viewNode = viewNode; + +/** + * Determine if an OSM way is one-way or not. + * + * See for details. + * + * @param tags Set of tags of the way. + * @return True iff. 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'); + +exports.isOneWay = isOneWay; + +/** + * Determine if an OSM object is a public transport line (route master). + * + * See + * and . + * + * @param object OSM object. + * @return True iff. the relation is a public transport line. + */ +const isTransportLine = object => + object.type === 'relation' + && isObject(object.tags) + && object.tags.type === 'route_master'; + +exports.isTransportLine = isTransportLine; diff --git a/back/data/endpoints.js b/back/data/sources/tam.js similarity index 81% rename from back/data/endpoints.js rename to back/data/sources/tam.js index cdde2e6..2870991 100644 --- a/back/data/endpoints.js +++ b/back/data/sources/tam.js @@ -2,28 +2,12 @@ const unzip = require('unzip-stream'); const csv = require('csv-parse'); const axios = require('axios'); -const overpassEndpoint = 'https://lz4.overpass-api.de/api/interpreter'; - -/** - * Submit an Overpass query. - * - * @async - * @param query Query in Overpass QL. - * @return Results as provided by the endpoint. - */ -const queryOverpass = query => axios.post( - overpassEndpoint, - 'data=' + query -).then(res => res.data); - -exports.queryOverpass = queryOverpass; - /** * Process a CSV stream to extract passings. * * @private * @param csvStream Stream containing CSV data. - * @param callback See fetchTamRealtime for a description of the callback. + * @param callback See fetchRealtime for a description of the callback. */ const processTamPassingStream = (csvStream, callback) => { @@ -74,14 +58,14 @@ const tamRealtimeEndpoint = 'http://data.montpellier3m.fr/node/10732/download'; * be non-null only if an error occurred. Second argument will contain passings * or be null if the end was reached. */ -const fetchTamRealtime = (callback) => +const fetchRealtime = (callback) => { axios.get(tamRealtimeEndpoint, { responseType: 'stream' }).then(res => processTamPassingStream(res.data, callback)); }; -exports.fetchTamRealtime = fetchTamRealtime; +exports.fetchRealtime = fetchRealtime; const tamTheoreticalEndpoint = 'http://data.montpellier3m.fr/node/10731/download'; @@ -94,7 +78,7 @@ const tamTheoreticalFileName = 'offre_du_jour.csv'; * be non-null only if an error occurred. Second argument will contain passings * or be null if the end was reached. */ -const fetchTamTheoretical = (callback) => +const fetchTheoretical = (callback) => { axios.get(tamTheoreticalEndpoint, { responseType: 'stream' @@ -117,4 +101,4 @@ const fetchTamTheoretical = (callback) => }); }; -exports.fetchTamTheoretical = fetchTamTheoretical; +exports.fetchTheoretical = fetchTheoretical; diff --git a/back/util.js b/back/util.js index 8c718b8..c463fa3 100644 --- a/back/util.js +++ b/back/util.js @@ -58,3 +58,13 @@ const joinSentence = (array, separator, lastSeparator) => }; exports.joinSentence = joinSentence; + +/** + * Check if a value is a JS object. + * + * @param value Value to check. + * @return True iff. `value` is a JS object. + */ +const isObject = value => value !== null && typeof value === 'object'; + +exports.isObject = isObject;