tracktracker/back/data.js

185 lines
5.1 KiB
JavaScript
Raw Normal View History

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 stops 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)
{
const {type, nodes} = elements[id];
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))
{
nodeNeighbors.set(node, new Set([previousNode]));
}
else
{
nodeNeighbors.get(node).add(previousNode);
}
nodeNeighbors.get(previousNode).add(node);
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;