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;
|
).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.
|
* Fetch stops, segments and lines in the network.
|
||||||
*
|
*
|
||||||
|
@ -162,13 +173,10 @@ out body qt;
|
||||||
return prev;
|
return prev;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
// Result object containing all stops
|
// All stops in the network
|
||||||
const stops = {};
|
const stops = {};
|
||||||
|
|
||||||
// Result object containing all segments between stops
|
// All transport lines of the network
|
||||||
const segments = {};
|
|
||||||
|
|
||||||
// Result object containing all lines
|
|
||||||
const lines = {};
|
const lines = {};
|
||||||
|
|
||||||
for (let routeMaster of routeMasters)
|
for (let routeMaster of routeMasters)
|
||||||
|
@ -234,7 +242,7 @@ URI: ${osmViewNode}/${stop.id}
|
||||||
|
|
||||||
if (candidates.length === 0)
|
if (candidates.length === 0)
|
||||||
{
|
{
|
||||||
console.warn('No candidate found in TaM data.');
|
console.warn('No candidate found in TaM data.');
|
||||||
}
|
}
|
||||||
else
|
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)
|
for (let route of rawRoutes)
|
||||||
{
|
{
|
||||||
const {from, to} = route.tags;
|
const {from, to} = route.tags;
|
||||||
|
|
||||||
const stops = route.members
|
// Human-readable description of the route for errors
|
||||||
.filter(({role}) => role === 'stop')
|
const routeDescription = `line ${lineRef}’s route from \
|
||||||
.map(({ref}) => elements[ref])
|
“${route.tags.from}” to “${route.tags.to}”`;
|
||||||
.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
|
// Check that the route consists of a block of stops and platforms
|
||||||
.filter(({role}) => role === '')
|
// 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);
|
.map(({ref}) => ref);
|
||||||
|
|
||||||
// Construct a graph with all connected nodes
|
// List of ways making up the route’s path through its stops
|
||||||
const nodeNeighbors = new Map();
|
// 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 = (
|
const curNodeIndex = wayNodes.indexOf(currentNode);
|
||||||
tags.oneway === 'yes'
|
|
||||||
|| tags.junction === 'roundabout'
|
|
||||||
);
|
|
||||||
|
|
||||||
const canGoBackward = (
|
// If not the last way, find a connection point to the next way
|
||||||
!isOneWay
|
// (there should be exactly one)
|
||||||
|| parseInt(tags['lanes:psv:backward'], 10) > 0
|
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))
|
nextNode = nextNodeCandidates[0];
|
||||||
{
|
nextNodeIndex = wayNodes.indexOf(nextNode);
|
||||||
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}”`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
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
|
path = path.concat(
|
||||||
const routes = rawRoutes.map(route => ({
|
wayNodes.slice(nextNodeIndex + 1, curNodeIndex + 1)
|
||||||
from: route.tags.from,
|
.reverse()
|
||||||
to: route.tags.to,
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Retrieve each stop’s information (stop order in the relation is
|
currentNode = nextNode;
|
||||||
// assumed to reflect reality)
|
}
|
||||||
stops: route.members
|
|
||||||
.filter(({role}) => role === 'stop')
|
// Split the path into segments between stops
|
||||||
.map(({ref}) => elements[ref])
|
const segments = [];
|
||||||
.filter(stop => 'ref' in stop.tags)
|
|
||||||
.map(stop => stop.tags.ref),
|
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] = {
|
lines[lineRef] = {
|
||||||
color,
|
color,
|
||||||
|
@ -436,7 +415,7 @@ ${joinSentence(Array.from(candidate.directions), ', ', ' or ')}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {stops, segments, lines};
|
return {stops, lines};
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.fetch = fetch;
|
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;
|
return value;
|
||||||
},
|
},
|
||||||
' '
|
4
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
})();
|
})();
|
||||||
|
|
Loading…
Reference in New Issue