Compare commits

..

No commits in common. "05d7faa72532365b0e2d4a3239a19a156cab4f0e" and "a51a80a4b3019ab278096c25a7b72b842a49e460" have entirely different histories.

4 changed files with 1683 additions and 1180 deletions

View File

@ -37,11 +37,6 @@ const parseTime = (time, reference) => {
return result; return result;
}; };
/** List of OSM nodes that are stops. */
const stopsSet = new Set(
Object.values(network.stops).map(stop => stop.properties.node)
);
/** Guess whether a stop comes directly before another one. */ /** Guess whether a stop comes directly before another one. */
const isPenultimateStop = (stop, finalStop) => { const isPenultimateStop = (stop, finalStop) => {
// If there is a standard segment linking both stops, its certainly // If there is a standard segment linking both stops, its certainly
@ -58,8 +53,8 @@ const isPenultimateStop = (stop, finalStop) => {
} }
// If there is another stop in the way, it cant be either // If there is another stop in the way, it cant be either
for (const nodeId of route.slice(1, -1)) { for (const node of route.slice(1, -1)) {
if (stopsSet.has(nodeId)) { if (node in network.stops) {
return false; return false;
} }
} }

View File

@ -340,7 +340,7 @@ different sequence of nodes in two or more lines.`);
* Create a graph for navigating between stops. * Create a graph for navigating between stops.
* @param {Array.<Object>} elementsList List of nodes retrieved from OSM. * @param {Array.<Object>} elementsList List of nodes retrieved from OSM.
* @param {Object.<string,Object>} elementsById OSM nodes indexed by their ID. * @param {Object.<string,Object>} elementsById OSM nodes indexed by their ID.
* @return {Object} Resulting graph and reverse arcs. * @return {Object.<string,Object.<string,NavigationEdge>} Resulting graph.
*/ */
const createNavigationGraph = (elementsList, elementsById) => { const createNavigationGraph = (elementsList, elementsById) => {
const navigation = {}; const navigation = {};
@ -350,7 +350,7 @@ const createNavigationGraph = (elementsList, elementsById) => {
for (const obj of elementsList) { for (const obj of elementsList) {
if (obj.type === "node") { if (obj.type === "node") {
navigation[obj.id] = {}; navigation[obj.id] = {};
navigationReverse[obj.id] = new Set(); navigationReverse[obj.id] = {};
} }
} }
@ -360,27 +360,27 @@ const createNavigationGraph = (elementsList, elementsById) => {
const oneWay = osm.isOneWay(obj); const oneWay = osm.isOneWay(obj);
for (let i = 0; i + 1 < obj.nodes.length; ++i) { for (let i = 0; i + 1 < obj.nodes.length; ++i) {
const from = obj.nodes[i].toString(); const from = obj.nodes[i];
let to = obj.nodes[i + 1].toString(); let to = obj.nodes[i + 1];
let path = [from, to]; let path = [from.toString(), to.toString()];
// Make sure we cant jump between rails at railway crossings // Make sure we cant jump between rails at railway crossings
if (i + 2 < obj.nodes.length if (i + 2 < obj.nodes.length
&& osm.isRailwayCrossing(elementsById[to])) { && osm.isRailwayCrossing(elementsById[to])) {
const next = obj.nodes[i + 2].toString(); const next = obj.nodes[i + 2];
path = [from, to, next]; path = [from.toString(), to.toString(), next.toString()];
to = next; to = next;
i += 1; i += 1;
} }
navigation[from][to] = path; navigation[from][to] = path;
navigationReverse[to].add(from); navigationReverse[to][from] = true;
if (!oneWay) { if (!oneWay) {
const reversePath = [...path]; const reversePath = [...path];
reversePath.reverse(); reversePath.reverse();
navigation[to][from] = reversePath; navigation[to][from] = reversePath;
navigationReverse[from].add(to); navigationReverse[from][to] = true;
} }
} }
} }
@ -390,205 +390,158 @@ const createNavigationGraph = (elementsList, elementsById) => {
}; };
/** /**
* Identify intermediate nodes of the navigation graph that can be simplified. * Remove and relink nodes that connect only two nodes or less.
* @param {Set.<string>} stopsSet OSM IDs of stop nodes. * @param {Object.<string,Stop>} stops List of stops.
* @param {Navigation} navigation Input navigation graph. * @param {Navigation} navigation Input navigation graph.
* @param {Object.<string,Set.<string>>} navigationReverse Reverse arcs. * @param {Object.<string,Object.<string,boolean>>} navigationReverse
* @return {Set.<string>} Set of compressible nodes. * Backward edges of the navigation graph.
*/ */
const findCompressibleNodes = (stopsSet, navigation, navigationReverse) => { const compressNavigationGraph = (stops, navigation, navigationReverse) => {
const compressible = new Set(); const stopsReverse = Object.fromEntries(
Object.entries(stops).map(([id, stop]) => [stop.properties.node, id])
);
for (const nodeId in navigation) { let removedDeadEnds = true;
if (stopsSet.has(nodeId)) { const nodesToCompress = {};
// Keep stop nodes
continue;
}
const entries = navigationReverse[nodeId]; while (removedDeadEnds) {
const exits = new Set(Object.keys(navigation[nodeId])); // Identify nodes to be compressed
for (const nodeId in navigation) {
// Keep split nodes, i.e. nodes with at least two exit nodes if (nodeId in stopsReverse) {
// and one entry node that are all distinct from each other // Keep stop nodes
if (entries.size >= 1) {
if (exits.size >= 3) {
continue; continue;
} }
let isSplit = false; const entries = new Set(Object.keys(navigationReverse[nodeId]));
const exits = new Set(Object.keys(navigation[nodeId]));
if (exits.size === 2) { // Keep split nodes, i.e. nodes with at least two exit nodes
for (const entry of entries) { // and one entry node that are all distinct from each other
if (!exits.has(entry)) { if (entries.size >= 1) {
isSplit = true; if (exits.size >= 3) {
break; continue;
}
} }
}
if (isSplit) { let isSplit = false;
continue;
}
}
// Keep junction nodes, i.e. nodes with at least two entry nodes if (exits.size === 2) {
// and one exit node that are all distinct from each other for (const entry of entries) {
if (exits.size >= 1) { if (!exits.has(entry)) {
if (entries.size >= 3) { isSplit = true;
continue; break;
} }
let isJunction = false;
if (entries.size === 2) {
for (const exit of exits) {
if (!entries.has(exit)) {
isJunction = true;
break;
}
}
}
if (isJunction) {
continue;
}
}
// Compress all other nodes
compressible.add(nodeId);
}
return compressible;
};
/**
* Remove nodes that are not used to link up two kept nodes.
* @param {Navigation} navigation Input navigation graph.
* @param {Object.<string,Set.<string>>} navigationReverse Reverse arcs.
* @param {Set.<string>} compressible Set of nodes that will not be kept.
* @return {boolean} True if some dead-ends were removed.
*/
const removeDeadEnds = (navigation, navigationReverse, compressible) => {
let didRemove = false;
// Find dead-ends starting from kept nodes
for (const beginId in navigation) {
if (compressible.has(beginId)) {
continue;
}
const begin = navigation[beginId];
const stack = [];
const parent = {[beginId]: beginId};
for (const succId in begin) {
if (compressible.has(succId)) {
stack.push(succId);
parent[succId] = beginId;
}
}
while (stack.length > 0) {
const endId = stack.pop();
const end = navigation[endId];
if (compressible.has(endId)) {
let hasSuccessor = false;
for (const succId in end) {
if (succId !== parent[endId]) {
parent[succId] = endId;
stack.push(succId);
hasSuccessor = true;
} }
} }
if (!hasSuccessor) { if (isSplit) {
// Remove the dead-end path continue;
let trackback = endId; }
}
// Keep junction nodes, i.e. nodes with at least two entry nodes
// and one exit node that are all distinct from each other
if (exits.size >= 1) {
if (entries.size >= 3) {
continue;
}
let isJunction = false;
if (entries.size === 2) {
for (const exit of exits) {
if (!entries.has(exit)) {
isJunction = true;
break;
}
}
}
if (isJunction) {
continue;
}
}
// Compress all other nodes
nodesToCompress[nodeId] = true;
}
// Find nodes that cannot be used to directly link up two kept nodes
const usedNodes = {};
for (const beginId in navigation) {
if (beginId in nodesToCompress) {
continue;
}
usedNodes[beginId] = true;
// Start a DFS from each node to be kept
const begin = navigation[beginId];
const stack = [];
const parent = {[beginId]: beginId};
for (const succId in begin) {
if (succId in nodesToCompress) {
stack.push(succId);
parent[succId] = beginId;
}
}
while (stack.length > 0) {
const endId = stack.pop();
const end = navigation[endId];
if (!(endId in nodesToCompress)) {
let trackback = parent[endId];
while (trackback !== beginId) { while (trackback !== beginId) {
navigationReverse[trackback].delete(parent[trackback]); usedNodes[trackback] = true;
delete navigation[parent[trackback]][trackback];
trackback = parent[trackback]; trackback = parent[trackback];
} }
} else {
didRemove = true; for (const succId in end) {
} if (succId !== parent[endId]) {
} parent[succId] = endId;
} stack.push(succId);
} }
// Find dead-ends starting from compressible source nodes
for (const beginId in navigation) {
if (!compressible.has(beginId)) {
continue;
}
if (navigationReverse[beginId].size > 0) {
continue;
}
const begin = navigation[beginId];
const stack = [];
const parent = {[beginId]: beginId};
for (const succId in begin) {
stack.push(succId);
parent[succId] = beginId;
}
while (stack.length > 0) {
const endId = stack.pop();
const end = navigation[endId];
if (compressible.has(endId)) {
for (const succId in end) {
if (succId !== parent[endId]) {
parent[succId] = endId;
stack.push(succId);
} }
} }
} else { }
// Remove the dead-end path }
let trackback = endId;
while (trackback !== beginId) { // Remove dead-end nodes
navigationReverse[trackback].delete(parent[trackback]); removedDeadEnds = false;
delete navigation[parent[trackback]][trackback];
trackback = parent[trackback]; for (const beginId in navigation) {
if (!(beginId in usedNodes)) {
for (const neighborId in navigation[beginId]) {
delete navigationReverse[neighborId][beginId];
} }
didRemove = true; for (const neighborId in navigationReverse[beginId]) {
delete navigation[neighborId][beginId];
}
delete navigation[beginId];
delete navigationReverse[beginId];
removedDeadEnds = true;
} }
} }
} }
return didRemove; // Perform node compression
};
/**
* Compress the given set of nodes.
* @param {Navigation} navigation Input navigation graph.
* @param {Object.<string,Set.<string>>} navigationReverse Reverse arcs.
* @param {Set.<string>} compressible Set of nodes to compress.
* @return {boolean} True if some nodes were compressed.
*/
const removeCompressibleNodes = (navigation, navigationReverse, compressible) => {
let didCompress = false;
for (const beginId in navigation) { for (const beginId in navigation) {
if (compressible.has(beginId)) { if (beginId in nodesToCompress) {
continue; continue;
} }
// Start a DFS from each kept node // Start a DFS from each node to be kept
const begin = navigation[beginId]; const begin = navigation[beginId];
const stack = []; const stack = [];
const parent = {[beginId]: beginId}; const parent = {[beginId]: beginId};
for (const succId in begin) { for (const succId in begin) {
if (compressible.has(succId)) { if (succId in nodesToCompress) {
stack.push(succId); stack.push(succId);
parent[succId] = beginId; parent[succId] = beginId;
} }
@ -598,35 +551,44 @@ const removeCompressibleNodes = (navigation, navigationReverse, compressible) =>
const endId = stack.pop(); const endId = stack.pop();
const end = navigation[endId]; const end = navigation[endId];
if (!compressible.has(endId)) { if (!(endId in nodesToCompress)) {
// Found another kept node // Found another kept node
// Collect and remove intermediate path // Collect and remove intermediate nodes
let path = []; const reversePath = [endId];
let trackback = endId; let trackback = parent[endId];
let oneWay = !(trackback in end);
do { while (trackback !== beginId) {
const segment = [...navigation[parent[trackback]][trackback]]; reversePath.push(trackback);
segment.reverse(); oneWay = oneWay || !(parent[trackback] in navigation[trackback]);
path = path.concat(segment.slice(0, -1));
navigationReverse[trackback].delete(parent[trackback]);
delete navigation[parent[trackback]][trackback];
delete navigation[trackback];
delete navigationReverse[trackback];
trackback = parent[trackback]; trackback = parent[trackback];
} while (trackback !== beginId);
// Make sure not to add loops if were compressing a cycle
if (endId !== beginId) {
path.push(beginId);
path.reverse();
begin[endId] = path;
navigationReverse[endId].add(beginId);
} }
didCompress = true; reversePath.push(beginId);
const forwardPath = [...reversePath];
forwardPath.reverse();
// Create edges to link both nodes directly
delete begin[forwardPath[1]];
delete navigationReverse[endId][reversePath[1]];
delete end[reversePath[1]];
delete navigationReverse[beginId][forwardPath[1]];
if (!(endId in begin)) {
begin[endId] = forwardPath;
navigationReverse[endId][beginId] = true;
}
if (!oneWay && !(beginId in end)) {
end[beginId] = reversePath;
navigationReverse[beginId][endId] = true;
}
} else { } else {
// Continue the traversal down compressible nodes // Continue the traversal down unused nodes
let isFirst = true; let isFirst = true;
for (const succId in end) { for (const succId in end) {
@ -644,57 +606,6 @@ non-junction node ${endId}`);
} }
} }
} }
return didCompress;
};
/**
* Find nodes in the graph that have no exits nor entries and remove them.
* @param {Navigation} navigation Input navigation graph.
* @param {Object.<string,Set.<string>>} navigationReverse Reverse arcs.
*/
const cleanUpIsolatedNodes = (navigation, navigationReverse) => {
for (const nodeId in navigation) {
if (
Object.keys(navigation[nodeId]).length === 0
&& navigationReverse[nodeId].size === 0
) {
delete navigation[nodeId];
delete navigationReverse[nodeId];
}
}
};
/**
* Remove and relink nodes that connect only two nodes or less.
* @param {Object.<string,Stop>} stops List of stops.
* @param {Navigation} navigation Input navigation graph.
* @param {Object.<string,Set.<string>>} navigationReverse Reverse arcs.
*/
const compressNavigationGraph = (stops, navigation, navigationReverse) => {
const stopsSet = new Set(
Object.values(stops).map(stop => stop.properties.node)
);
let compressible = null;
let didCompress = true;
while (didCompress) {
let didRemove = true;
while (didRemove) {
compressible = findCompressibleNodes(
stopsSet, navigation, navigationReverse
);
didRemove = removeDeadEnds(
navigation, navigationReverse, compressible
);
}
didCompress = removeCompressibleNodes(
navigation, navigationReverse, compressible
);
cleanUpIsolatedNodes(navigation, navigationReverse);
}
}; };
/** /**

File diff suppressed because it is too large Load Diff

View File

@ -16,23 +16,14 @@ for (const [beginId, begin] of Object.entries(network.navigation)) {
} }
/** /**
* Get the OSM ID of a stop. * Find the shortest path of nodes linking two nodes or stops.
* @param {string} stopId Network ID of the stop. * @param {string} from ID of the starting stop or node.
* @return {string} OSM ID of the stop. * @param {string} to ID of the ending stop or node.
* @return {Array.<string>} If possible, list of nodes joining `from` to `to`.
*/ */
export const stopToOSM = stopId => network.stops[stopId].properties.node; export const findPath = (from, to) => {
/**
* Find the shortest path of nodes linking two stops.
* @param {string} fromStop Network ID of the starting stop.
* @param {string} toStop Network ID of the ending stop.
* @return {Array.<string>} If it exists, a path of nodes joining the two stops.
*/
export const findPath = (fromStop, toStop) => {
try { try {
return dijkstra.find_path( return dijkstra.find_path(graph, from, to);
graph, stopToOSM(fromStop), stopToOSM(toStop)
);
} catch (err) { } catch (err) {
return null; return null;
} }
@ -40,12 +31,12 @@ export const findPath = (fromStop, toStop) => {
/** /**
* Find the shortest segment linking two nodes or stops. * Find the shortest segment linking two nodes or stops.
* @param {string} fromStop Network ID of the starting stop. * @param {string} from ID of the starting stop or node.
* @param {string} toStop Network ID of the ending stop. * @param {string} to ID of the ending stop or node.
* @return {LineString?} If it exists, a segment joining the two stops. * @return {LineString?} If it exists, a segment linking the two nodes.
*/ */
export const findSegment = (fromStop, toStop) => { export const findSegment = (from, to) => {
const path = findPath(fromStop, toStop); const path = findPath(from, to);
if (path === null) { if (path === null) {
return null; return null;