Compare commits

..

3 Commits

7 changed files with 500 additions and 237933 deletions

View File

@ -1,21 +1,20 @@
const {choosePlural, joinSentence} = require('../util');
const {queryOverpass, fetchTamTheoretical} = require('./endpoints');
const osmViewNode = 'https://www.openstreetmap.org/node';
/**
* Create a link to remotely add tags into JOSM.
* @file
*
* @param objectId Identifier for the object to add the tags to.
* @param tags Tags to add.
* @return Link for remotely adding the tags.
* 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 josmAddTagToNode = (objectId, tags) =>
'http://127.0.0.1:8111/load_object?' + [
`objects=n${objectId}`,
'new_layer=false',
'addtags=' + tags.join('%7C'),
].join('&');
const {choosePlural, joinSentence} = require('../util');
const osm = require('./sources/osm');
const tam = require('./sources/tam');
/**
* Use theoretical passings data to guess which lines use which stops in which
@ -31,7 +30,7 @@ const fetchStopsRefAssociations = () => new Promise((res, rej) =>
{
const stops = {};
fetchTamTheoretical((err, row) =>
tam.fetchTheoretical((err, row) =>
{
if (err)
{
@ -123,18 +122,7 @@ const matchStopNames = (fullName, abbrName) =>
};
/**
* 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, segments and lines in the network.
* Fetch stops and lines of the network.
*
* @param lineRefs List of lines to fetch.
* @return Object with a set of stops, segments and lines.
@ -142,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('|')})$"];
@ -160,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) =>
@ -199,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
@ -253,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])}
`);
}
}
@ -283,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
@ -300,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(
@ -308,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
@ -329,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);
@ -348,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}`);
}
@ -370,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(
@ -392,7 +374,7 @@ ${routeDescription} is one-way and cannot be used in reverse.`);
{
segments.push(path.slice(
path.indexOf(stops[stopIndex]),
path.indexOf(stops[stopIndex + 1] + 1),
path.indexOf(stops[stopIndex + 1]) + 1,
).map(id => ({
lat: elements[id].lat,
lon: elements[id].lon
@ -400,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),
});

File diff suppressed because it is too large Load Diff

89
back/data/sources/osm.js Normal file
View File

@ -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 <https://wiki.openstreetmap.org/wiki/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.
*/
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 <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.
*/
const viewNode = id => `https://www.openstreetmap.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.
*/
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 <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.
*/
const isTransportLine = object =>
object.type === 'relation'
&& isObject(object.tags)
&& object.tags.type === 'route_master';
exports.isTransportLine = isTransportLine;

View File

@ -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;

View File

@ -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 JSobject.
*/
const isObject = value => value !== null && typeof value === 'object';
exports.isObject = isObject;

90
package-lock.json generated
View File

@ -1907,6 +1907,16 @@
"qs": "6.7.0",
"raw-body": "2.4.0",
"type-is": "~1.6.17"
},
"dependencies": {
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
}
}
},
"boolbase": {
@ -3060,9 +3070,9 @@
}
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"requires": {
"ms": "2.0.0"
}
@ -3679,6 +3689,15 @@
"to-regex": "^3.0.1"
},
"dependencies": {
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dev": true,
"requires": {
"ms": "2.0.0"
}
},
"define-property": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
@ -3734,6 +3753,16 @@
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"dependencies": {
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
}
}
},
"extend": {
@ -3977,6 +4006,16 @@
"parseurl": "~1.3.3",
"statuses": "~1.5.0",
"unpipe": "~1.0.0"
},
"dependencies": {
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
}
}
},
"flat-cache": {
@ -4002,16 +4041,6 @@
"integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
"requires": {
"debug": "=3.1.0"
},
"dependencies": {
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"requires": {
"ms": "2.0.0"
}
}
}
},
"for-in": {
@ -7303,6 +7332,21 @@
"statuses": "~1.5.0"
},
"dependencies": {
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
},
"dependencies": {
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
}
}
},
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
@ -7441,6 +7485,15 @@
"use": "^3.1.0"
},
"dependencies": {
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dev": true,
"requires": {
"ms": "2.0.0"
}
},
"define-property": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
@ -8147,6 +8200,17 @@
"dev": true,
"requires": {
"debug": "^2.2.0"
},
"dependencies": {
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dev": true,
"requires": {
"ms": "2.0.0"
}
}
}
},
"unicode-canonical-property-names-ecmascript": {