Use PTv2 standard instead of DFS for finding segments
This commit is contained in:
parent
783f29ce68
commit
dd008d7ee2
|
@ -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 TaM data.');
|
||||
console.warn('No candidate found in TaM data.');
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -272,163 +280,134 @@ ${joinSentence(Array.from(candidate.directions), ', ', ' or ')}
|
|||
}
|
||||
}
|
||||
|
||||
// Add missing segments between stops
|
||||
// Reconstruct the line’s 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 === '')
|
||||
// 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 === ''
|
||||
);
|
||||
|
||||
if (!route.members.slice(0, relationPivot).every(
|
||||
({role}) => role === 'stop' || role === 'platform'
|
||||
))
|
||||
{
|
||||
throw new Error(`Members with invalid roles in between stops
|
||||
of ${routeDescription}`);
|
||||
}
|
||||
|
||||
if (!route.members.slice(relationPivot).every(
|
||||
({role}) => role === ''
|
||||
))
|
||||
{
|
||||
throw new Error(`Members with invalid roles inside the path
|
||||
of ${routeDescription}`);
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
// Construct a graph with all connected nodes
|
||||
const nodeNeighbors = new Map();
|
||||
// List of ways making up the route’s path through its stops
|
||||
// with each way connected to the next through a single point
|
||||
const ways = route.members.slice(relationPivot)
|
||||
.map(({ref}) => ref);
|
||||
|
||||
for (let id of ways)
|
||||
// Merge all used ways in a single path
|
||||
let path = [];
|
||||
let currentNode = stops[0];
|
||||
|
||||
for (let wayIndex = 0; wayIndex < ways.length; wayIndex += 1)
|
||||
{
|
||||
const {type, nodes, tags} = elements[id];
|
||||
const {nodes: wayNodes, tags: wayTags}
|
||||
= elements[ways[wayIndex]];
|
||||
const wayNodesSet = new Set(wayNodes);
|
||||
|
||||
const isOneWay = (
|
||||
tags.oneway === 'yes'
|
||||
|| tags.junction === 'roundabout'
|
||||
);
|
||||
const curNodeIndex = wayNodes.indexOf(currentNode);
|
||||
|
||||
const canGoBackward = (
|
||||
!isOneWay
|
||||
|| parseInt(tags['lanes:psv:backward'], 10) > 0
|
||||
);
|
||||
// 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 (type === 'way' && nodes.length >= 1)
|
||||
if (wayIndex + 1 < ways.length)
|
||||
{
|
||||
let previousNode = nodes[0];
|
||||
const nextNodeCandidates = elements[ways[wayIndex + 1]]
|
||||
.nodes.filter(node => wayNodesSet.has(node));
|
||||
|
||||
if (!nodeNeighbors.has(previousNode))
|
||||
if (nextNodeCandidates.length !== 1)
|
||||
{
|
||||
nodeNeighbors.set(previousNode, new Set());
|
||||
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}`);
|
||||
}
|
||||
|
||||
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: 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]),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.`);
|
||||
}
|
||||
|
||||
// Construct line objects
|
||||
const routes = rawRoutes.map(route => ({
|
||||
from: route.tags.from,
|
||||
to: route.tags.to,
|
||||
path = path.concat(
|
||||
wayNodes.slice(nextNodeIndex + 1, curNodeIndex + 1)
|
||||
.reverse()
|
||||
);
|
||||
}
|
||||
|
||||
// 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),
|
||||
}));
|
||||
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),
|
||||
});
|
||||
}
|
||||
|
||||
lines[lineRef] = {
|
||||
color,
|
||||
|
@ -436,7 +415,7 @@ ${joinSentence(Array.from(candidate.directions), ', ', ' or ')}
|
|||
};
|
||||
}
|
||||
|
||||
return {stops, segments, lines};
|
||||
return {stops, lines};
|
||||
};
|
||||
|
||||
exports.fetch = fetch;
|
||||
|
|
261162
back/data/network.json
261162
back/data/network.json
File diff suppressed because it is too large
Load Diff
|
@ -23,7 +23,7 @@ const fs = require('fs');
|
|||
|
||||
return value;
|
||||
},
|
||||
' '
|
||||
4
|
||||
)
|
||||
);
|
||||
})();
|
||||
|
|
Loading…
Reference in New Issue