2020-07-16 23:41:05 +00:00
|
|
|
|
/**
|
|
|
|
|
* @file
|
2020-07-17 10:13:25 +00:00
|
|
|
|
*
|
2020-07-16 23:41:05 +00:00
|
|
|
|
* 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.
|
2020-07-17 10:13:25 +00:00
|
|
|
|
*
|
|
|
|
|
* 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.
|
2020-07-16 23:41:05 +00:00
|
|
|
|
*/
|
|
|
|
|
|
2020-07-17 21:48:32 +00:00
|
|
|
|
const geolib = require('geolib');
|
|
|
|
|
const util = require('../util');
|
2020-07-17 10:13:25 +00:00
|
|
|
|
const osm = require('./sources/osm');
|
|
|
|
|
const tam = require('./sources/tam');
|
2020-01-14 23:19:26 +00:00
|
|
|
|
|
|
|
|
|
/**
|
2020-07-16 23:41:05 +00:00
|
|
|
|
* Fetch stops and lines of the network.
|
2020-01-14 23:19:26 +00:00
|
|
|
|
*
|
|
|
|
|
* @param lineRefs List of lines to fetch.
|
|
|
|
|
* @return Object with a set of stops, segments and lines.
|
|
|
|
|
*/
|
|
|
|
|
const fetch = async (lineRefs) =>
|
2020-01-14 13:08:08 +00:00
|
|
|
|
{
|
|
|
|
|
// Retrieve routes, ways and stops from OpenStreetMap
|
2020-07-17 10:13:25 +00:00
|
|
|
|
const rawData = await osm.runQuery(`[out:json];
|
2020-01-14 13:08:08 +00:00
|
|
|
|
|
|
|
|
|
// Find the public transport line bearing the requested reference
|
|
|
|
|
relation[network="TaM"][type="route_master"][ref~"^(${lineRefs.join('|')})$"];
|
|
|
|
|
|
|
|
|
|
// Recursively fetch routes, ways and stops inside the line
|
|
|
|
|
(._; >>;);
|
|
|
|
|
|
|
|
|
|
out body qt;
|
2020-01-14 23:19:26 +00:00
|
|
|
|
`);
|
|
|
|
|
|
2020-01-14 13:08:08 +00:00
|
|
|
|
// List of retrieved objects
|
2020-07-16 22:16:54 +00:00
|
|
|
|
const elementsList = rawData.elements;
|
2020-01-14 13:08:08 +00:00
|
|
|
|
|
|
|
|
|
// List of retrieved lines
|
2020-07-17 10:13:25 +00:00
|
|
|
|
const routeMasters = elementsList.filter(osm.isTransportLine);
|
2020-01-14 13:08:08 +00:00
|
|
|
|
|
|
|
|
|
// Retrieved objects indexed by ID
|
|
|
|
|
const elements = elementsList.reduce((prev, elt) =>
|
|
|
|
|
{
|
|
|
|
|
prev[elt.id] = elt;
|
|
|
|
|
return prev;
|
|
|
|
|
}, {});
|
|
|
|
|
|
2020-07-16 20:56:39 +00:00
|
|
|
|
// All stops in the network
|
2020-01-14 13:08:08 +00:00
|
|
|
|
const stops = {};
|
|
|
|
|
|
2020-07-16 20:56:39 +00:00
|
|
|
|
// All transport lines of the network
|
2020-01-14 13:08:08 +00:00
|
|
|
|
const lines = {};
|
|
|
|
|
|
2020-07-17 21:48:32 +00:00
|
|
|
|
// All segments leading from one stop to another
|
|
|
|
|
const segments = {};
|
|
|
|
|
|
2020-01-14 13:08:08 +00:00
|
|
|
|
for (let routeMaster of routeMasters)
|
|
|
|
|
{
|
|
|
|
|
const lineRef = routeMaster.tags.ref;
|
|
|
|
|
const color = routeMaster.tags.colour || '#000000';
|
|
|
|
|
|
|
|
|
|
// Extract all routes for the given line
|
2020-07-22 21:10:43 +00:00
|
|
|
|
const routes = [];
|
2020-01-14 13:08:08 +00:00
|
|
|
|
|
2020-07-22 21:10:43 +00:00
|
|
|
|
for (let [routeRef, {ref: routeId}] of routeMaster.members.entries())
|
2020-01-14 13:08:08 +00:00
|
|
|
|
{
|
2020-07-22 21:10:43 +00:00
|
|
|
|
const route = elements[routeId];
|
|
|
|
|
const {from, to, name} = route.tags;
|
|
|
|
|
const state = route.tags.state || 'normal';
|
|
|
|
|
|
|
|
|
|
// Add missing stops to the global stops object
|
2020-01-14 13:08:08 +00:00
|
|
|
|
for (let {ref, role} of route.members)
|
|
|
|
|
{
|
|
|
|
|
if (role === 'stop')
|
|
|
|
|
{
|
|
|
|
|
const stop = elements[ref];
|
|
|
|
|
|
|
|
|
|
if (!('ref' in stop.tags))
|
|
|
|
|
{
|
2020-07-22 21:10:43 +00:00
|
|
|
|
throw new Error(`Stop ${stop.id}
|
|
|
|
|
(${osm.viewNode(stop.id)}) on line ${route.tags.name} is missing
|
|
|
|
|
a “ref” tag`);
|
2020-01-14 13:08:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!(stop.tags.ref in stops))
|
|
|
|
|
{
|
|
|
|
|
stops[stop.tags.ref] = {
|
|
|
|
|
lat: stop.lat,
|
|
|
|
|
lon: stop.lon,
|
|
|
|
|
name: stop.tags.name,
|
2020-07-22 21:10:43 +00:00
|
|
|
|
routes: [[lineRef, routeRef]],
|
2020-01-14 13:08:08 +00:00
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2020-07-22 21:10:43 +00:00
|
|
|
|
stops[stop.tags.ref].routes.push([lineRef, routeRef]);
|
2020-01-14 13:08:08 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-16 20:56:39 +00:00
|
|
|
|
// 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 === ''
|
|
|
|
|
);
|
2020-01-14 13:08:08 +00:00
|
|
|
|
|
2020-07-16 20:56:39 +00:00
|
|
|
|
if (!route.members.slice(0, relationPivot).every(
|
|
|
|
|
({role}) => role === 'stop' || role === 'platform'
|
|
|
|
|
))
|
2020-01-14 13:08:08 +00:00
|
|
|
|
{
|
2020-07-16 20:56:39 +00:00
|
|
|
|
throw new Error(`Members with invalid roles in between stops
|
2020-07-17 10:13:25 +00:00
|
|
|
|
of ${name}`);
|
2020-01-14 13:08:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-07-16 20:56:39 +00:00
|
|
|
|
if (!route.members.slice(relationPivot).every(
|
|
|
|
|
({role}) => role === ''
|
|
|
|
|
))
|
2020-01-14 13:08:08 +00:00
|
|
|
|
{
|
2020-07-16 20:56:39 +00:00
|
|
|
|
throw new Error(`Members with invalid roles inside the path
|
2020-07-17 10:13:25 +00:00
|
|
|
|
of ${name}`);
|
2020-07-16 20:56:39 +00:00
|
|
|
|
}
|
2020-01-14 13:08:08 +00:00
|
|
|
|
|
2020-07-16 20:56:39 +00:00
|
|
|
|
// List of stops in the route, expected to be in the timetable
|
|
|
|
|
// order as per PTv2 and to be traversed in order by the sequence
|
|
|
|
|
// of ways extracted below
|
2020-07-22 21:10:43 +00:00
|
|
|
|
const lineStops = route.members.slice(0, relationPivot)
|
2020-07-16 20:56:39 +00:00
|
|
|
|
.filter(({role}) => role === 'stop')
|
|
|
|
|
.map(({ref}) => ref);
|
2020-01-14 13:08:08 +00:00
|
|
|
|
|
2020-07-16 20:56:39 +00:00
|
|
|
|
// 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);
|
2020-01-14 13:08:08 +00:00
|
|
|
|
|
2020-07-16 20:56:39 +00:00
|
|
|
|
// Merge all used ways in a single path
|
|
|
|
|
let path = [];
|
2020-07-22 21:10:43 +00:00
|
|
|
|
let currentNode = lineStops[0];
|
2020-01-14 13:08:08 +00:00
|
|
|
|
|
2020-07-16 20:56:39 +00:00
|
|
|
|
for (let wayIndex = 0; wayIndex < ways.length; wayIndex += 1)
|
|
|
|
|
{
|
2020-07-17 10:13:25 +00:00
|
|
|
|
const way = elements[ways[wayIndex]];
|
|
|
|
|
const {nodes: wayNodes, tags: wayTags} = way;
|
2020-07-16 20:56:39 +00:00
|
|
|
|
const wayNodesSet = new Set(wayNodes);
|
2020-01-14 13:08:08 +00:00
|
|
|
|
|
2020-07-16 20:56:39 +00:00
|
|
|
|
const curNodeIndex = wayNodes.indexOf(currentNode);
|
2020-01-14 13:08:08 +00:00
|
|
|
|
|
2020-07-16 20:56:39 +00:00
|
|
|
|
// If not the last way, find a connection point to the next way
|
|
|
|
|
// (there should be exactly one)
|
|
|
|
|
let nextNode = null;
|
|
|
|
|
let nextNodeIndex = null;
|
2020-01-14 13:08:08 +00:00
|
|
|
|
|
2020-07-16 20:56:39 +00:00
|
|
|
|
if (wayIndex + 1 < ways.length)
|
|
|
|
|
{
|
|
|
|
|
const nextNodeCandidates = elements[ways[wayIndex + 1]]
|
|
|
|
|
.nodes.filter(node => wayNodesSet.has(node));
|
2020-01-14 13:08:08 +00:00
|
|
|
|
|
2020-07-16 20:56:39 +00:00
|
|
|
|
if (nextNodeCandidates.length !== 1)
|
2020-01-14 13:08:08 +00:00
|
|
|
|
{
|
2020-07-16 20:56:39 +00:00
|
|
|
|
throw new Error(`There should be exactly one point
|
2020-07-17 10:13:25 +00:00
|
|
|
|
connecting way n°${wayIndex} and way n°${wayIndex + 1} in ${name},
|
2020-07-16 20:56:39 +00:00
|
|
|
|
but there are ${nextNodeCandidates.length}`);
|
2020-01-14 13:08:08 +00:00
|
|
|
|
}
|
2020-07-16 20:56:39 +00:00
|
|
|
|
|
|
|
|
|
nextNode = nextNodeCandidates[0];
|
|
|
|
|
nextNodeIndex = wayNodes.indexOf(nextNode);
|
2020-01-14 13:08:08 +00:00
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2020-07-16 20:56:39 +00:00
|
|
|
|
nextNodeIndex = wayNodes.length;
|
2020-01-14 13:08:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-07-16 20:56:39 +00:00
|
|
|
|
if (curNodeIndex < nextNodeIndex)
|
|
|
|
|
{
|
|
|
|
|
// Use the way in its normal direction
|
|
|
|
|
path = path.concat(
|
|
|
|
|
wayNodes.slice(curNodeIndex, nextNodeIndex)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// Use the way in the reverse direction
|
2020-07-17 10:13:25 +00:00
|
|
|
|
if (osm.isOneWay(way))
|
2020-07-16 20:56:39 +00:00
|
|
|
|
{
|
|
|
|
|
throw new Error(`Way n°${wayIndex} in
|
2020-07-17 10:13:25 +00:00
|
|
|
|
${name} is one-way and cannot be used in reverse.`);
|
2020-07-16 20:56:39 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
path = path.concat(
|
|
|
|
|
wayNodes.slice(nextNodeIndex + 1, curNodeIndex + 1)
|
2020-07-16 22:16:54 +00:00
|
|
|
|
.reverse()
|
2020-07-16 20:56:39 +00:00
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
currentNode = nextNode;
|
2020-01-14 13:08:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-07-16 20:56:39 +00:00
|
|
|
|
// Split the path into segments between stops
|
2020-07-22 21:10:43 +00:00
|
|
|
|
for (let stopIdx = 0; stopIdx + 1 < lineStops.length; ++stopIdx)
|
2020-07-16 20:56:39 +00:00
|
|
|
|
{
|
2020-07-22 21:10:43 +00:00
|
|
|
|
const begin = elements[lineStops[stopIdx]].tags.ref;
|
|
|
|
|
const end = elements[lineStops[stopIdx + 1]].tags.ref;
|
2020-07-17 21:48:32 +00:00
|
|
|
|
|
|
|
|
|
const id = `${begin}-${end}`;
|
2020-07-24 23:02:07 +00:00
|
|
|
|
const nodesIds = path.slice(
|
2020-07-22 21:10:43 +00:00
|
|
|
|
path.indexOf(lineStops[stopIdx]),
|
|
|
|
|
path.indexOf(lineStops[stopIdx + 1]) + 1,
|
2020-07-17 21:48:32 +00:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (id in segments)
|
|
|
|
|
{
|
2020-07-24 23:02:07 +00:00
|
|
|
|
if (!util.arraysEqual(nodesIds, segments[id].nodesIds))
|
2020-07-17 21:48:32 +00:00
|
|
|
|
{
|
|
|
|
|
throw new Error(`Segment ${id} is defined as a
|
|
|
|
|
different sequence of nodes in two or more lines.`);
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-22 21:10:43 +00:00
|
|
|
|
segments[id].routes.push([lineRef, routeRef]);
|
2020-07-17 21:48:32 +00:00
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2020-07-24 23:02:07 +00:00
|
|
|
|
const nodes = nodesIds.map(id => ({
|
2020-07-17 21:48:32 +00:00
|
|
|
|
lat: elements[id].lat,
|
|
|
|
|
lon: elements[id].lon
|
|
|
|
|
}));
|
|
|
|
|
|
2020-07-24 23:02:07 +00:00
|
|
|
|
if (nodes.length)
|
2020-07-17 21:48:32 +00:00
|
|
|
|
{
|
2020-07-24 23:02:07 +00:00
|
|
|
|
// Augment each node with the distance to the start
|
|
|
|
|
nodes[0].distance = 0;
|
2020-07-17 21:48:32 +00:00
|
|
|
|
|
2020-07-24 23:02:07 +00:00
|
|
|
|
for (let i = 1; i < nodes.length; ++i)
|
2020-07-17 21:48:32 +00:00
|
|
|
|
{
|
2020-07-24 23:02:07 +00:00
|
|
|
|
nodes[i].distance = geolib.getPreciseDistance(
|
|
|
|
|
nodes[i - 1],
|
|
|
|
|
nodes[i],
|
|
|
|
|
) + nodes[i - 1].distance;
|
2020-07-17 21:48:32 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
segments[id] = {
|
|
|
|
|
// Keep track of the original sequence of nodes to
|
|
|
|
|
// compare with duplicates
|
2020-07-24 23:02:07 +00:00
|
|
|
|
nodesIds,
|
2020-07-17 21:48:32 +00:00
|
|
|
|
nodes,
|
2020-07-22 21:10:43 +00:00
|
|
|
|
routes: [[lineRef, routeRef]],
|
2020-07-17 21:48:32 +00:00
|
|
|
|
};
|
|
|
|
|
}
|
2020-07-16 20:56:39 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
routes.push({
|
2020-07-22 17:02:43 +00:00
|
|
|
|
from, to,
|
|
|
|
|
name, state,
|
2020-07-22 21:10:43 +00:00
|
|
|
|
stops: lineStops.map(id => elements[id].tags.ref),
|
2020-07-16 20:56:39 +00:00
|
|
|
|
});
|
|
|
|
|
}
|
2020-01-14 13:08:08 +00:00
|
|
|
|
|
|
|
|
|
lines[lineRef] = {
|
|
|
|
|
color,
|
2020-01-15 23:34:47 +00:00
|
|
|
|
routes,
|
2020-01-14 13:08:08 +00:00
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-17 21:48:32 +00:00
|
|
|
|
// Remove OSM nodes from segments that were only used for checking validity
|
|
|
|
|
for (let segment of Object.values(segments))
|
|
|
|
|
{
|
2020-07-24 23:02:07 +00:00
|
|
|
|
delete segment.nodesIds;
|
2020-07-17 21:48:32 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {stops, lines, segments};
|
2020-01-14 23:19:26 +00:00
|
|
|
|
};
|
2020-01-14 13:08:08 +00:00
|
|
|
|
|
|
|
|
|
exports.fetch = fetch;
|