Compare commits
No commits in common. "05d7faa72532365b0e2d4a3239a19a156cab4f0e" and "a51a80a4b3019ab278096c25a7b72b842a49e460" have entirely different histories.
05d7faa725
...
a51a80a4b3
|
@ -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, it’s certainly
|
// If there is a standard segment linking both stops, it’s certainly
|
||||||
|
@ -58,8 +53,8 @@ const isPenultimateStop = (stop, finalStop) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there is another stop in the way, it can’t be either
|
// If there is another stop in the way, it can’t 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 can’t jump between rails at railway crossings
|
// Make sure we can’t 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,22 +390,29 @@ 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])
|
||||||
|
);
|
||||||
|
|
||||||
|
let removedDeadEnds = true;
|
||||||
|
const nodesToCompress = {};
|
||||||
|
|
||||||
|
while (removedDeadEnds) {
|
||||||
|
// Identify nodes to be compressed
|
||||||
for (const nodeId in navigation) {
|
for (const nodeId in navigation) {
|
||||||
if (stopsSet.has(nodeId)) {
|
if (nodeId in stopsReverse) {
|
||||||
// Keep stop nodes
|
// Keep stop nodes
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const entries = navigationReverse[nodeId];
|
const entries = new Set(Object.keys(navigationReverse[nodeId]));
|
||||||
const exits = new Set(Object.keys(navigation[nodeId]));
|
const exits = new Set(Object.keys(navigation[nodeId]));
|
||||||
|
|
||||||
// Keep split nodes, i.e. nodes with at least two exit nodes
|
// Keep split nodes, i.e. nodes with at least two exit nodes
|
||||||
|
@ -455,34 +462,26 @@ const findCompressibleNodes = (stopsSet, navigation, navigationReverse) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compress all other nodes
|
// Compress all other nodes
|
||||||
compressible.add(nodeId);
|
nodesToCompress[nodeId] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return compressible;
|
// Find nodes that cannot be used to directly link up two kept nodes
|
||||||
};
|
const usedNodes = {};
|
||||||
|
|
||||||
/**
|
|
||||||
* 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) {
|
for (const beginId in navigation) {
|
||||||
if (compressible.has(beginId)) {
|
if (beginId in nodesToCompress) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
usedNodes[beginId] = true;
|
||||||
|
|
||||||
|
// 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;
|
||||||
}
|
}
|
||||||
|
@ -492,103 +491,57 @@ const removeDeadEnds = (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)) {
|
||||||
let hasSuccessor = false;
|
let trackback = parent[endId];
|
||||||
|
|
||||||
for (const succId in end) {
|
|
||||||
if (succId !== parent[endId]) {
|
|
||||||
parent[succId] = endId;
|
|
||||||
stack.push(succId);
|
|
||||||
hasSuccessor = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasSuccessor) {
|
|
||||||
// Remove the dead-end path
|
|
||||||
let trackback = 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];
|
||||||
}
|
}
|
||||||
|
|
||||||
didRemove = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
} else {
|
||||||
// Remove the dead-end path
|
for (const succId in end) {
|
||||||
let trackback = endId;
|
if (succId !== parent[endId]) {
|
||||||
|
parent[succId] = endId;
|
||||||
while (trackback !== beginId) {
|
stack.push(succId);
|
||||||
navigationReverse[trackback].delete(parent[trackback]);
|
}
|
||||||
delete navigation[parent[trackback]][trackback];
|
|
||||||
trackback = parent[trackback];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
didRemove = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return didRemove;
|
// Remove dead-end nodes
|
||||||
};
|
removedDeadEnds = false;
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 usedNodes)) {
|
||||||
|
for (const neighborId in navigation[beginId]) {
|
||||||
|
delete navigationReverse[neighborId][beginId];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const neighborId in navigationReverse[beginId]) {
|
||||||
|
delete navigation[neighborId][beginId];
|
||||||
|
}
|
||||||
|
|
||||||
|
delete navigation[beginId];
|
||||||
|
delete navigationReverse[beginId];
|
||||||
|
removedDeadEnds = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform node compression
|
||||||
|
for (const beginId in navigation) {
|
||||||
|
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 we’re 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);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
2422
src/tam/network.json
2422
src/tam/network.json
File diff suppressed because it is too large
Load Diff
|
@ -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;
|
||||||
|
|
Loading…
Reference in New Issue