tracktracker/back/data/network.js

255 lines
8.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;