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 stop’s 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;