2020-01-13 11:20:35 +00:00
|
|
|
|
const requestp = require('request-promise-native');
|
|
|
|
|
|
|
|
|
|
const {makeCached} = require('./util');
|
|
|
|
|
|
|
|
|
|
const OVERPASS_ENDPOINT = 'https://lz4.overpass-api.de/api/interpreter';
|
|
|
|
|
|
|
|
|
|
const fetchLineData = makeCached(async (lineRef) =>
|
|
|
|
|
{
|
|
|
|
|
// Retrieve routes, ways and stops from OpenStreetMap
|
|
|
|
|
const rawData = await requestp.post(OVERPASS_ENDPOINT, {form: `\
|
|
|
|
|
data=[out:json];
|
|
|
|
|
|
|
|
|
|
// Find the public transport line bearing the requested reference
|
|
|
|
|
relation[network="TaM"][type="route_master"][ref="${lineRef}"];
|
|
|
|
|
|
|
|
|
|
// Recursively fetch routes, ways and stops inside the line
|
|
|
|
|
(._; >>;);
|
|
|
|
|
|
|
|
|
|
out body qt;
|
|
|
|
|
`});
|
|
|
|
|
|
|
|
|
|
const elementsList = JSON.parse(rawData).elements;
|
|
|
|
|
|
|
|
|
|
// Extract all routes for the given line
|
|
|
|
|
const rawRoutes = elementsList.filter(elt =>
|
|
|
|
|
elt.type === 'relation'
|
|
|
|
|
&& elt.tags.type === 'route'
|
|
|
|
|
&& elt.tags.ref === lineRef
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// If no route is found, assume the line does not exist
|
|
|
|
|
if (rawRoutes.length === 0)
|
|
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Index retrieved objects by their ID
|
|
|
|
|
const elements = elementsList.reduce((prev, elt) =>
|
|
|
|
|
{
|
|
|
|
|
prev[elt.id] = elt;
|
|
|
|
|
return prev;
|
|
|
|
|
}, {});
|
|
|
|
|
|
|
|
|
|
const color = rawRoutes[0].tags.colour || '#000000';
|
|
|
|
|
|
|
|
|
|
// Extract stops for each route of the line
|
|
|
|
|
const routes = rawRoutes.map(route => ({
|
|
|
|
|
from: route.tags.from,
|
|
|
|
|
to: route.tags.to,
|
|
|
|
|
|
|
|
|
|
// Retrieve each stop’s information (stop order in the relation is
|
|
|
|
|
// assumed to reflect reality)
|
|
|
|
|
stops: route.members
|
|
|
|
|
.filter(({role}) => role === 'stop')
|
|
|
|
|
.map(({ref}) => {
|
|
|
|
|
const elt = elements[ref];
|
|
|
|
|
return {
|
|
|
|
|
id: ref,
|
|
|
|
|
lat: elt.lat,
|
|
|
|
|
lon: elt.lon,
|
|
|
|
|
name: elt.tags.name,
|
|
|
|
|
};
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
// List of ways making up the route (raw)
|
|
|
|
|
rawWays: route.members
|
|
|
|
|
.filter(({role}) => role === '')
|
|
|
|
|
.map(({ref}) => ref)
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
// Process ways in each route to sort and merge them
|
|
|
|
|
for (let route of routes)
|
|
|
|
|
{
|
|
|
|
|
const {from, to, stops, rawWays} = route;
|
|
|
|
|
|
|
|
|
|
// Construct a graph with all connected nodes
|
|
|
|
|
const nodeNeighbors = new Map();
|
|
|
|
|
|
|
|
|
|
for (let id of rawWays)
|
|
|
|
|
{
|
2020-01-13 14:01:07 +00:00
|
|
|
|
const {type, nodes, tags} = elements[id];
|
|
|
|
|
const canGoBackward = (
|
|
|
|
|
tags.oneway !== 'yes'
|
|
|
|
|
|| parseInt(tags['lanes:psv:backward'], 10) > 0
|
|
|
|
|
);
|
2020-01-13 11:20:35 +00:00
|
|
|
|
|
|
|
|
|
if (type === 'way' && nodes.length >= 1)
|
|
|
|
|
{
|
|
|
|
|
let previousNode = nodes[0];
|
|
|
|
|
|
|
|
|
|
if (!nodeNeighbors.has(previousNode))
|
|
|
|
|
{
|
|
|
|
|
nodeNeighbors.set(previousNode, new Set());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (let node of nodes.slice(1))
|
|
|
|
|
{
|
|
|
|
|
if (!nodeNeighbors.has(node))
|
|
|
|
|
{
|
2020-01-13 14:01:07 +00:00
|
|
|
|
nodeNeighbors.set(node, new Set());
|
2020-01-13 11:20:35 +00:00
|
|
|
|
}
|
2020-01-13 14:01:07 +00:00
|
|
|
|
|
|
|
|
|
nodeNeighbors.get(previousNode).add(node);
|
|
|
|
|
|
|
|
|
|
if (canGoBackward)
|
2020-01-13 11:20:35 +00:00
|
|
|
|
{
|
|
|
|
|
nodeNeighbors.get(node).add(previousNode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
previousNode = node;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Find way from first stop through the end using DFS
|
|
|
|
|
const numberOfStops = stops.length;
|
|
|
|
|
const ways = [];
|
|
|
|
|
|
|
|
|
|
let currentStopIndex = 0;
|
|
|
|
|
|
|
|
|
|
while (currentStopIndex + 1 < numberOfStops)
|
|
|
|
|
{
|
|
|
|
|
const currentStop = stops[currentStopIndex];
|
|
|
|
|
const nextStop = stops[currentStopIndex + 1];
|
|
|
|
|
|
|
|
|
|
const visited = new Set();
|
|
|
|
|
const stack = [{
|
|
|
|
|
currentNode: currentStop.id,
|
|
|
|
|
way: [currentStop.id],
|
|
|
|
|
}];
|
|
|
|
|
|
|
|
|
|
let found = false;
|
|
|
|
|
|
|
|
|
|
while (stack.length !== 0)
|
|
|
|
|
{
|
|
|
|
|
const {currentNode, way} = stack.pop();
|
|
|
|
|
visited.add(currentNode);
|
|
|
|
|
|
|
|
|
|
if (currentNode === nextStop.id)
|
|
|
|
|
{
|
|
|
|
|
// Arrived at next stop
|
|
|
|
|
ways.push(way);
|
|
|
|
|
found = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const neighbors = nodeNeighbors.get(currentNode) || [];
|
|
|
|
|
|
|
|
|
|
for (let nextNode of neighbors)
|
|
|
|
|
{
|
|
|
|
|
if (!visited.has(nextNode))
|
|
|
|
|
{
|
|
|
|
|
stack.push({
|
|
|
|
|
currentNode: nextNode,
|
|
|
|
|
way: way.concat([nextNode]),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!found)
|
|
|
|
|
{
|
|
|
|
|
throw new Error(`No way between stop “${currentStop.name}” \
|
|
|
|
|
(${currentStop.id}) and stop “${nextStop.name}” (${nextStop.id}) on line \
|
|
|
|
|
${lineRef}’s route from “${from}” to “${to}”`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
++currentStopIndex;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Only keep geo coordinates for each way node
|
|
|
|
|
route.ways = ways.map(way =>
|
|
|
|
|
way.map(id =>
|
|
|
|
|
{
|
|
|
|
|
const node = elements[id];
|
|
|
|
|
return {lat: node.lat, lon: node.lon};
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
delete route.rawWays;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
ref: lineRef,
|
|
|
|
|
color,
|
|
|
|
|
routes
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
exports.fetchLineData = fetchLineData;
|