tracktracker/back/data/network.js

255 lines
8.1 KiB
JavaScript
Raw Normal View History

const requestp = require('request-promise-native');
const geolib = require('geolib');
const {makeCached} = require('../util');
const OVERPASS_ENDPOINT = 'https://lz4.overpass-api.de/api/interpreter';
const fetch = makeCached(async (lineRefs) =>
{
// 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~"^(${lineRefs.join('|')})$"];
// Recursively fetch routes, ways and stops inside the line
(._; >>;);
out body qt;
`});
// List of retrieved objects
const elementsList = JSON.parse(rawData).elements;
// List of retrieved lines
const routeMasters = elementsList.filter(elt =>
elt.tags && elt.tags.type === 'route_master'
);
// Retrieved objects indexed by ID
const elements = elementsList.reduce((prev, elt) =>
{
prev[elt.id] = elt;
return prev;
}, {});
// Result object containing all stops
const stops = {};
// Result object containing all segments between stops
const segments = {};
// Result object containing all lines
const lines = {};
for (let routeMaster of routeMasters)
{
const lineRef = routeMaster.tags.ref;
const color = routeMaster.tags.colour || '#000000';
// Extract all routes for the given line
const rawRoutes = routeMaster.members.map(({ref}) => elements[ref]);
// Add missing stops to the result object
for (let route of rawRoutes)
{
for (let {ref, role} of route.members)
{
if (role === 'stop')
{
const stop = elements[ref];
if (!('ref' in stop.tags))
{
console.warn(`Stop ${stop.id} is missing a “ref” tag`);
continue;
}
if (!(stop.tags.ref in stops))
{
stops[stop.tags.ref] = {
lat: stop.lat,
lon: stop.lon,
name: stop.tags.name,
lines: [lineRef],
};
}
else
{
stops[stop.tags.ref].lines.push(lineRef);
}
}
}
}
// Add missing segments between stops
for (let route of rawRoutes)
{
const {from, to} = route.tags;
const stops = route.members
.filter(({role}) => role === 'stop')
.map(({ref}) => elements[ref])
.filter(stop => 'ref' in stop.tags)
.map(stop => ({
id: stop.id,
lat: stop.lat,
lon: stop.lon,
ref: stop.tags.ref,
name: stop.tags.name,
}));
const ways = route.members
.filter(({role}) => role === '')
.map(({ref}) => ref);
// Construct a graph with all connected nodes
const nodeNeighbors = new Map();
for (let id of ways)
{
const {type, nodes, tags} = elements[id];
const isOneWay = (
tags.oneway === 'yes'
|| tags.junction === 'roundabout'
);
const canGoBackward = (
!isOneWay
|| parseInt(tags['lanes:psv:backward'], 10) > 0
);
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());
}
nodeNeighbors.get(previousNode).add(node);
if (canGoBackward)
{
nodeNeighbors.get(node).add(previousNode);
}
previousNode = node;
}
}
}
// Find way from first stop through the end using DFS
const numberOfStops = stops.length;
let currentStopIndex = 0;
while (currentStopIndex + 1 < numberOfStops)
{
const currentStop = stops[currentStopIndex];
const nextStop = stops[currentStopIndex + 1];
const segmentId = `${currentStop.ref}-${nextStop.ref}`;
if (!(segmentId in segments))
{
const visitedEdges = new Set();
const stack = [{
currentNode: currentStop.id,
segment: [currentStop.id],
}];
let found = false;
while (stack.length !== 0)
{
const {currentNode, segment} = stack.pop();
if (currentNode === nextStop.id)
{
// Arrived at next stop
segments[segmentId] = {
nodes: segment.map(id =>
{
const {lat, lon} = elements[id];
return {lat, lon};
}),
length: geolib.getPathLength(segment.map(id =>
{
const {lat, lon} = elements[id];
return {latitude: lat, longitude: lon};
})),
lines: [lineRef],
};
found = true;
break;
}
const neighbors = nodeNeighbors.get(currentNode) || [];
for (let nextNode of neighbors)
{
const edge = `${currentNode}-${nextNode}`;
if (!visitedEdges.has(edge))
{
visitedEdges.add(edge);
stack.push({
currentNode: nextNode,
segment: segment.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}`);
}
}
else
{
segments[segmentId].lines.push(lineRef);
}
++currentStopIndex;
}
}
// Construct line objects
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}) => elements[ref])
.filter(stop => 'ref' in stop.tags)
.map(stop => stop.tags.ref)
}));
lines[lineRef] = {
color,
routes
};
}
return {stops, segments, lines};
});
exports.fetch = fetch;