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;