255 lines
8.1 KiB
JavaScript
255 lines
8.1 KiB
JavaScript
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 stop’s 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;
|