Use PTv2 standard instead of DFS for finding segments

This commit is contained in:
Mattéo Delabre 2020-07-16 22:56:39 +02:00
parent 783f29ce68
commit dd008d7ee2
Signed by: matteo
GPG Key ID: AE3FBD02DC583ABB
3 changed files with 264935 additions and 26874 deletions

View File

@ -124,6 +124,17 @@ const matchStopNames = (fullName, abbrName) =>
).length;
};
/**
* Determine if an OSM way is oneway or not.
*
* @param tags Set of tags of the way.
* @return True iff. the way is oneway.
*/
const isOneWay = tags =>
('oneway' in tags && tags.oneway === 'yes')
|| ('junction' in tags && tags.junction === 'roundabout')
|| ('highway' in tags && tags.highway === 'motorway');
/**
* Fetch stops, segments and lines in the network.
*
@ -162,13 +173,10 @@ out body qt;
return prev;
}, {});
// Result object containing all stops
// All stops in the network
const stops = {};
// Result object containing all segments between stops
const segments = {};
// Result object containing all lines
// All transport lines of the network
const lines = {};
for (let routeMaster of routeMasters)
@ -234,7 +242,7 @@ URI: ${osmViewNode}/${stop.id}
if (candidates.length === 0)
{
console.warn('No candidate found in TaMdata.');
console.warn('No candidate found in TaM data.');
}
else
{
@ -272,171 +280,142 @@ ${joinSentence(Array.from(candidate.directions), ', ', ' or ')}
}
}
// Add missing segments between stops
// Reconstruct the lines route from stop to stop
const routes = [];
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,
}));
// Human-readable description of the route for errors
const routeDescription = `line ${lineRef}s route from \
${route.tags.from} to ${route.tags.to}`;
const ways = route.members
.filter(({role}) => role === '')
.map(({ref}) => ref);
// Check that the route consists of a block of stops and platforms
// followed by a block of routes as dictated by PTv2
const relationPivot = route.members.findIndex(
({role}) => role === ''
);
// Construct a graph with all connected nodes
const nodeNeighbors = new Map();
for (let id of ways)
if (!route.members.slice(0, relationPivot).every(
({role}) => role === 'stop' || role === 'platform'
))
{
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;
}
}
throw new Error(`Members with invalid roles in between stops
of ${routeDescription}`);
}
// Find way from first stop through the end using DFS
const numberOfStops = stops.length;
let currentStopIndex = 0;
while (currentStopIndex + 1 < numberOfStops)
if (!route.members.slice(relationPivot).every(
({role}) => role === ''
))
{
const currentStop = stops[currentStopIndex];
const nextStop = stops[currentStopIndex + 1];
const segmentId = `${currentStop.ref}-${nextStop.ref}`;
throw new Error(`Members with invalid roles inside the path
of ${routeDescription}`);
}
if (!(segmentId in segments))
// List of stops in the route, expected to be in the timetable
// order as per PTv2 and to be traversed in order by the sequence
// of ways extracted below
const stops = route.members.slice(0, relationPivot)
.filter(({role}) => role === 'stop')
.map(({ref}) => ref);
// List of ways making up the routes path through its stops
// with each way connected to the next through a single point
const ways = route.members.slice(relationPivot)
.map(({ref}) => ref);
// Merge all used ways in a single path
let path = [];
let currentNode = stops[0];
for (let wayIndex = 0; wayIndex < ways.length; wayIndex += 1)
{
const {nodes: wayNodes, tags: wayTags}
= elements[ways[wayIndex]];
const wayNodesSet = new Set(wayNodes);
const curNodeIndex = wayNodes.indexOf(currentNode);
// If not the last way, find a connection point to the next way
// (there should be exactly one)
let nextNode = null;
let nextNodeIndex = null;
if (wayIndex + 1 < ways.length)
{
const visitedEdges = new Set();
const stack = [{
currentNode: currentStop.id,
segment: [currentStop.id],
}];
const nextNodeCandidates = elements[ways[wayIndex + 1]]
.nodes.filter(node => wayNodesSet.has(node));
let found = false;
while (stack.length !== 0)
if (nextNodeCandidates.length !== 1)
{
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: new Set([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]),
});
}
}
throw new Error(`There should be exactly one point
connecting way n°${wayIndex} and way n°${wayIndex + 1} in ${routeDescription},
but there are ${nextNodeCandidates.length}`);
}
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}`);
}
nextNode = nextNodeCandidates[0];
nextNodeIndex = wayNodes.indexOf(nextNode);
}
else
{
segments[segmentId].lines.add(lineRef);
nextNodeIndex = wayNodes.length;
}
++currentStopIndex;
if (curNodeIndex < nextNodeIndex)
{
// Use the way in its normal direction
path = path.concat(
wayNodes.slice(curNodeIndex, nextNodeIndex)
);
}
else
{
// Use the way in the reverse direction
if (isOneWay(wayTags))
{
throw new Error(`Way n°${wayIndex} in
${routeDescription} is one-way and cannot be used in reverse.`);
}
path = path.concat(
wayNodes.slice(nextNodeIndex + 1, curNodeIndex + 1)
.reverse()
);
}
currentNode = nextNode;
}
// Split the path into segments between stops
const segments = [];
for (let stopIndex = 0; stopIndex + 1 < stops.length; ++stopIndex)
{
segments.push(path.slice(
path.indexOf(stops[stopIndex]),
path.indexOf(stops[stopIndex + 1] + 1),
).map(id => ({
lat: elements[id].lat,
lon: elements[id].lon,
})));
}
routes.push({
from, to,
name: route.tags.name,
segments,
stops: stops.map(id => elements[id].tags.ref),
});
}
// 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};
return {stops, lines};
};
exports.fetch = fetch;

File diff suppressed because it is too large Load Diff

View File

@ -23,7 +23,7 @@ const fs = require('fs');
return value;
},
' '
4
)
);
})();