mirror of
https://github.com/anvaka/ngraph.path.git
synced 2026-01-18 15:13:12 +00:00
1043 lines
30 KiB
JavaScript
1043 lines
30 KiB
JavaScript
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.ngraphPath = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
|
|
/**
|
|
* Based on https://github.com/mourner/tinyqueue
|
|
* Copyright (c) 2017, Vladimir Agafonkin https://github.com/mourner/tinyqueue/blob/master/LICENSE
|
|
*
|
|
* Adapted for PathFinding needs by @anvaka
|
|
* Copyright (c) 2017, Andrei Kashcha
|
|
*/
|
|
module.exports = NodeHeap;
|
|
|
|
function NodeHeap(data, options) {
|
|
if (!(this instanceof NodeHeap)) return new NodeHeap(data, options);
|
|
|
|
if (!Array.isArray(data)) {
|
|
// assume first argument is our config object;
|
|
options = data;
|
|
data = [];
|
|
}
|
|
|
|
options = options || {};
|
|
|
|
this.data = data || [];
|
|
this.length = this.data.length;
|
|
this.compare = options.compare || defaultCompare;
|
|
this.setNodeId = options.setNodeId || noop;
|
|
|
|
if (this.length > 0) {
|
|
for (var i = (this.length >> 1); i >= 0; i--) this._down(i);
|
|
}
|
|
|
|
if (options.setNodeId) {
|
|
for (var i = 0; i < this.length; ++i) {
|
|
this.setNodeId(this.data[i], i);
|
|
}
|
|
}
|
|
}
|
|
|
|
function noop() {}
|
|
|
|
function defaultCompare(a, b) {
|
|
return a - b;
|
|
}
|
|
|
|
NodeHeap.prototype = {
|
|
|
|
push: function (item) {
|
|
this.data.push(item);
|
|
this.setNodeId(item, this.length);
|
|
this.length++;
|
|
this._up(this.length - 1);
|
|
},
|
|
|
|
pop: function () {
|
|
if (this.length === 0) return undefined;
|
|
|
|
var top = this.data[0];
|
|
this.length--;
|
|
|
|
if (this.length > 0) {
|
|
this.data[0] = this.data[this.length];
|
|
this.setNodeId(this.data[0], 0);
|
|
this._down(0);
|
|
}
|
|
this.data.pop();
|
|
|
|
return top;
|
|
},
|
|
|
|
peek: function () {
|
|
return this.data[0];
|
|
},
|
|
|
|
updateItem: function (pos) {
|
|
this._down(pos);
|
|
this._up(pos);
|
|
},
|
|
|
|
_up: function (pos) {
|
|
var data = this.data;
|
|
var compare = this.compare;
|
|
var setNodeId = this.setNodeId;
|
|
var item = data[pos];
|
|
|
|
while (pos > 0) {
|
|
var parent = (pos - 1) >> 1;
|
|
var current = data[parent];
|
|
if (compare(item, current) >= 0) break;
|
|
data[pos] = current;
|
|
|
|
setNodeId(current, pos);
|
|
pos = parent;
|
|
}
|
|
|
|
data[pos] = item;
|
|
setNodeId(item, pos);
|
|
},
|
|
|
|
_down: function (pos) {
|
|
var data = this.data;
|
|
var compare = this.compare;
|
|
var halfLength = this.length >> 1;
|
|
var item = data[pos];
|
|
var setNodeId = this.setNodeId;
|
|
|
|
while (pos < halfLength) {
|
|
var left = (pos << 1) + 1;
|
|
var right = left + 1;
|
|
var best = data[left];
|
|
|
|
if (right < this.length && compare(data[right], best) < 0) {
|
|
left = right;
|
|
best = data[right];
|
|
}
|
|
if (compare(best, item) >= 0) break;
|
|
|
|
data[pos] = best;
|
|
setNodeId(best, pos);
|
|
pos = left;
|
|
}
|
|
|
|
data[pos] = item;
|
|
setNodeId(item, pos);
|
|
}
|
|
};
|
|
},{}],2:[function(require,module,exports){
|
|
/**
|
|
* Performs suboptimal, greed A Star path finding.
|
|
* This finder does not necessary finds the shortest path. The path
|
|
* that it finds is very close to the shortest one. It is very fast though.
|
|
*/
|
|
module.exports = aStarBi;
|
|
|
|
var NodeHeap = require('./NodeHeap');
|
|
var makeSearchStatePool = require('./makeSearchStatePool');
|
|
var heuristics = require('./heuristics');
|
|
var defaultSettings = require('./defaultSettings');
|
|
|
|
var BY_FROM = 1;
|
|
var BY_TO = 2;
|
|
var NO_PATH = defaultSettings.NO_PATH;
|
|
|
|
module.exports.l2 = heuristics.l2;
|
|
module.exports.l1 = heuristics.l1;
|
|
|
|
/**
|
|
* Creates a new instance of pathfinder. A pathfinder has just one method:
|
|
* `find(fromId, toId)`, it may be extended in future.
|
|
*
|
|
* NOTE: Algorithm implemented in this code DOES NOT find optimal path.
|
|
* Yet the path that it finds is always near optimal, and it finds it very fast.
|
|
*
|
|
* @param {ngraph.graph} graph instance. See https://github.com/anvaka/ngraph.graph
|
|
*
|
|
* @param {Object} options that configures search
|
|
* @param {Function(a, b)} options.heuristic - a function that returns estimated distance between
|
|
* nodes `a` and `b`. Defaults function returns 0, which makes this search equivalent to Dijkstra search.
|
|
* @param {Function(a, b)} options.distance - a function that returns actual distance between two
|
|
* nodes `a` and `b`. By default this is set to return graph-theoretical distance (always 1);
|
|
* @param {Boolean} options.oriented - whether graph should be considered oriented or not.
|
|
*
|
|
* @returns {Object} A pathfinder with single method `find()`.
|
|
*/
|
|
function aStarBi(graph, options) {
|
|
options = options || {};
|
|
// whether traversal should be considered over oriented graph.
|
|
var oriented = options.oriented;
|
|
|
|
var heuristic = options.heuristic;
|
|
if (!heuristic) heuristic = defaultSettings.heuristic;
|
|
|
|
var distance = options.distance;
|
|
if (!distance) distance = defaultSettings.distance;
|
|
var pool = makeSearchStatePool();
|
|
|
|
return {
|
|
find: find
|
|
};
|
|
|
|
function find(fromId, toId) {
|
|
// Not sure if we should return NO_PATH or throw. Throw seem to be more
|
|
// helpful to debug errors. So, throwing.
|
|
var from = graph.getNode(fromId);
|
|
if (!from) throw new Error('fromId is not defined in this graph: ' + fromId);
|
|
var to = graph.getNode(toId);
|
|
if (!to) throw new Error('toId is not defined in this graph: ' + toId);
|
|
|
|
if (from === to) return [from]; // trivial case.
|
|
|
|
pool.reset();
|
|
|
|
var callVisitor = oriented ? orientedVisitor : nonOrientedVisitor;
|
|
|
|
// Maps nodeId to NodeSearchState.
|
|
var nodeState = new Map();
|
|
|
|
var openSetFrom = new NodeHeap({
|
|
compare: defaultSettings.compareFScore,
|
|
setNodeId: defaultSettings.setHeapIndex
|
|
});
|
|
|
|
var openSetTo = new NodeHeap({
|
|
compare: defaultSettings.compareFScore,
|
|
setNodeId: defaultSettings.setHeapIndex
|
|
});
|
|
|
|
|
|
var startNode = pool.createNewState(from);
|
|
nodeState.set(fromId, startNode);
|
|
|
|
// For the first node, fScore is completely heuristic.
|
|
startNode.fScore = heuristic(from, to);
|
|
// The cost of going from start to start is zero.
|
|
startNode.distanceToSource = 0;
|
|
openSetFrom.push(startNode);
|
|
startNode.open = BY_FROM;
|
|
|
|
var endNode = pool.createNewState(to);
|
|
endNode.fScore = heuristic(to, from);
|
|
endNode.distanceToSource = 0;
|
|
openSetTo.push(endNode);
|
|
endNode.open = BY_TO;
|
|
|
|
// Cost of the best solution found so far. Used for accurate termination
|
|
var lMin = Number.POSITIVE_INFINITY;
|
|
var minFrom;
|
|
var minTo;
|
|
|
|
var currentSet = openSetFrom;
|
|
var currentOpener = BY_FROM;
|
|
|
|
while (openSetFrom.length > 0 && openSetTo.length > 0) {
|
|
if (openSetFrom.length < openSetTo.length) {
|
|
// we pick a set with less elements
|
|
currentOpener = BY_FROM;
|
|
currentSet = openSetFrom;
|
|
} else {
|
|
currentOpener = BY_TO;
|
|
currentSet = openSetTo;
|
|
}
|
|
|
|
var current = currentSet.pop();
|
|
|
|
// no need to visit this node anymore
|
|
current.closed = true;
|
|
|
|
if (current.distanceToSource > lMin) continue;
|
|
|
|
graph.forEachLinkedNode(current.node.id, callVisitor);
|
|
|
|
if (minFrom && minTo) {
|
|
// This is not necessary the best path, but we are so greedy that we
|
|
// can't resist:
|
|
return reconstructBiDirectionalPath(minFrom, minTo);
|
|
}
|
|
}
|
|
|
|
return NO_PATH; // No path.
|
|
|
|
function nonOrientedVisitor(otherNode, link) {
|
|
return visitNode(otherNode, link, current);
|
|
}
|
|
|
|
function orientedVisitor(otherNode, link) {
|
|
// For oritned graphs we need to reverse graph, when traveling
|
|
// backwards. So, we use non-oriented ngraph's traversal, and
|
|
// filter link orientation here.
|
|
if (currentOpener === BY_FROM) {
|
|
if (link.fromId === current.node.id) return visitNode(otherNode, link, current)
|
|
} else if (currentOpener === BY_TO) {
|
|
if (link.toId === current.node.id) return visitNode(otherNode, link, current);
|
|
}
|
|
}
|
|
|
|
function canExit(currentNode) {
|
|
var opener = currentNode.open
|
|
if (opener && opener !== currentOpener) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function reconstructBiDirectionalPath(a, b) {
|
|
var pathOfNodes = [];
|
|
var aParent = a;
|
|
while(aParent) {
|
|
pathOfNodes.push(aParent.node);
|
|
aParent = aParent.parent;
|
|
}
|
|
var bParent = b;
|
|
while (bParent) {
|
|
pathOfNodes.unshift(bParent.node);
|
|
bParent = bParent.parent
|
|
}
|
|
return pathOfNodes;
|
|
}
|
|
|
|
function visitNode(otherNode, link, cameFrom) {
|
|
var otherSearchState = nodeState.get(otherNode.id);
|
|
if (!otherSearchState) {
|
|
otherSearchState = pool.createNewState(otherNode);
|
|
nodeState.set(otherNode.id, otherSearchState);
|
|
}
|
|
|
|
if (otherSearchState.closed) {
|
|
// Already processed this node.
|
|
return;
|
|
}
|
|
|
|
if (canExit(otherSearchState, cameFrom)) {
|
|
// this node was opened by alternative opener. The sets intersect now,
|
|
// we found an optimal path, that goes through *this* node. However, there
|
|
// is no guarantee that this is the global optimal solution path.
|
|
|
|
var potentialLMin = otherSearchState.distanceToSource + cameFrom.distanceToSource;
|
|
if (potentialLMin < lMin) {
|
|
minFrom = otherSearchState;
|
|
minTo = cameFrom
|
|
lMin = potentialLMin;
|
|
}
|
|
// we are done with this node.
|
|
return;
|
|
}
|
|
|
|
var tentativeDistance = cameFrom.distanceToSource + distance(otherSearchState.node, cameFrom.node, link);
|
|
|
|
if (tentativeDistance >= otherSearchState.distanceToSource) {
|
|
// This would only make our path longer. Ignore this route.
|
|
return;
|
|
}
|
|
|
|
// Choose target based on current working set:
|
|
var target = (currentOpener === BY_FROM) ? to : from;
|
|
var newFScore = tentativeDistance + heuristic(otherSearchState.node, target);
|
|
if (newFScore >= lMin) {
|
|
// this can't be optimal path, as we have already found a shorter path.
|
|
return;
|
|
}
|
|
otherSearchState.fScore = newFScore;
|
|
|
|
if (otherSearchState.open === 0) {
|
|
// Remember this node in the current set
|
|
currentSet.push(otherSearchState);
|
|
currentSet.updateItem(otherSearchState.heapIndex);
|
|
|
|
otherSearchState.open = currentOpener;
|
|
}
|
|
|
|
// bingo! we found shorter path:
|
|
otherSearchState.parent = cameFrom;
|
|
otherSearchState.distanceToSource = tentativeDistance;
|
|
}
|
|
}
|
|
}
|
|
|
|
},{"./NodeHeap":1,"./defaultSettings":4,"./heuristics":5,"./makeSearchStatePool":6}],3:[function(require,module,exports){
|
|
/**
|
|
* Performs a uni-directional A Star search on graph.
|
|
*
|
|
* We will try to minimize f(n) = g(n) + h(n), where
|
|
* g(n) is actual distance from source node to `n`, and
|
|
* h(n) is heuristic distance from `n` to target node.
|
|
*/
|
|
module.exports = aStarPathSearch;
|
|
|
|
var NodeHeap = require('./NodeHeap');
|
|
var makeSearchStatePool = require('./makeSearchStatePool');
|
|
var heuristics = require('./heuristics');
|
|
var defaultSettings = require('./defaultSettings.js');
|
|
|
|
var NO_PATH = defaultSettings.NO_PATH;
|
|
|
|
module.exports.l2 = heuristics.l2;
|
|
module.exports.l1 = heuristics.l1;
|
|
|
|
/**
|
|
* Creates a new instance of pathfinder. A pathfinder has just one method:
|
|
* `find(fromId, toId)`, it may be extended in future.
|
|
*
|
|
* @param {ngraph.graph} graph instance. See https://github.com/anvaka/ngraph.graph
|
|
* @param {Object} options that configures search
|
|
* @param {Function(a, b)} options.heuristic - a function that returns estimated distance between
|
|
* nodes `a` and `b`. This function should never overestimate actual distance between two
|
|
* nodes (otherwise the found path will not be the shortest). Defaults function returns 0,
|
|
* which makes this search equivalent to Dijkstra search.
|
|
* @param {Function(a, b)} options.distance - a function that returns actual distance between two
|
|
* nodes `a` and `b`. By default this is set to return graph-theoretical distance (always 1);
|
|
* @param {Boolean} options.oriented - whether graph should be considered oriented or not.
|
|
*
|
|
* @returns {Object} A pathfinder with single method `find()`.
|
|
*/
|
|
function aStarPathSearch(graph, options) {
|
|
options = options || {};
|
|
// whether traversal should be considered over oriented graph.
|
|
var oriented = options.oriented;
|
|
|
|
var heuristic = options.heuristic;
|
|
if (!heuristic) heuristic = defaultSettings.heuristic;
|
|
|
|
var distance = options.distance;
|
|
if (!distance) distance = defaultSettings.distance;
|
|
var pool = makeSearchStatePool();
|
|
|
|
return {
|
|
/**
|
|
* Finds a path between node `fromId` and `toId`.
|
|
* @returns {Array} of nodes between `toId` and `fromId`. Empty array is returned
|
|
* if no path is found.
|
|
*/
|
|
find: find
|
|
};
|
|
|
|
function find(fromId, toId) {
|
|
var from = graph.getNode(fromId);
|
|
if (!from) throw new Error('fromId is not defined in this graph: ' + fromId);
|
|
var to = graph.getNode(toId);
|
|
if (!to) throw new Error('toId is not defined in this graph: ' + toId);
|
|
pool.reset();
|
|
|
|
// Maps nodeId to NodeSearchState.
|
|
var nodeState = new Map();
|
|
|
|
// the nodes that we still need to evaluate
|
|
var openSet = new NodeHeap({
|
|
compare: defaultSettings.compareFScore,
|
|
setNodeId: defaultSettings.setHeapIndex
|
|
});
|
|
|
|
var startNode = pool.createNewState(from);
|
|
nodeState.set(fromId, startNode);
|
|
|
|
// For the first node, fScore is completely heuristic.
|
|
startNode.fScore = heuristic(from, to);
|
|
|
|
// The cost of going from start to start is zero.
|
|
startNode.distanceToSource = 0;
|
|
openSet.push(startNode);
|
|
startNode.open = 1;
|
|
|
|
var cameFrom;
|
|
|
|
while (openSet.length > 0) {
|
|
cameFrom = openSet.pop();
|
|
if (goalReached(cameFrom, to)) return reconstructPath(cameFrom);
|
|
|
|
// no need to visit this node anymore
|
|
cameFrom.closed = true;
|
|
graph.forEachLinkedNode(cameFrom.node.id, visitNeighbour, oriented);
|
|
}
|
|
|
|
// If we got here, then there is no path.
|
|
return NO_PATH;
|
|
|
|
function visitNeighbour(otherNode, link) {
|
|
var otherSearchState = nodeState.get(otherNode.id);
|
|
if (!otherSearchState) {
|
|
otherSearchState = pool.createNewState(otherNode);
|
|
nodeState.set(otherNode.id, otherSearchState);
|
|
}
|
|
|
|
if (otherSearchState.closed) {
|
|
// Already processed this node.
|
|
return;
|
|
}
|
|
if (otherSearchState.open === 0) {
|
|
// Remember this node.
|
|
openSet.push(otherSearchState);
|
|
otherSearchState.open = 1;
|
|
}
|
|
|
|
var tentativeDistance = cameFrom.distanceToSource + distance(otherNode, cameFrom.node, link);
|
|
if (tentativeDistance >= otherSearchState.distanceToSource) {
|
|
// This would only make our path longer. Ignore this route.
|
|
return;
|
|
}
|
|
|
|
// bingo! we found shorter path:
|
|
otherSearchState.parent = cameFrom;
|
|
otherSearchState.distanceToSource = tentativeDistance;
|
|
otherSearchState.fScore = tentativeDistance + heuristic(otherSearchState.node, to);
|
|
|
|
openSet.updateItem(otherSearchState.heapIndex);
|
|
}
|
|
}
|
|
}
|
|
|
|
function goalReached(searchState, targetNode) {
|
|
return searchState.node === targetNode;
|
|
}
|
|
|
|
function reconstructPath(searchState) {
|
|
var path = [searchState.node];
|
|
var parent = searchState.parent;
|
|
|
|
while (parent) {
|
|
path.push(parent.node);
|
|
parent = parent.parent;
|
|
}
|
|
|
|
return path;
|
|
}
|
|
|
|
},{"./NodeHeap":1,"./defaultSettings.js":4,"./heuristics":5,"./makeSearchStatePool":6}],4:[function(require,module,exports){
|
|
// We reuse instance of array, but we trie to freeze it as well,
|
|
// so that consumers don't modify it. Maybe it's a bad idea.
|
|
var NO_PATH = [];
|
|
if (typeof Object.freeze === 'function') Object.freeze(NO_PATH);
|
|
|
|
module.exports = {
|
|
// Path search settings
|
|
heuristic: blindHeuristic,
|
|
distance: constantDistance,
|
|
compareFScore: compareFScore,
|
|
NO_PATH: NO_PATH,
|
|
|
|
// heap settings
|
|
setHeapIndex: setHeapIndex,
|
|
|
|
// nba:
|
|
setH1: setH1,
|
|
setH2: setH2,
|
|
compareF1Score: compareF1Score,
|
|
compareF2Score: compareF2Score,
|
|
}
|
|
|
|
function blindHeuristic(/* a, b */) {
|
|
// blind heuristic makes this search equal to plain Dijkstra path search.
|
|
return 0;
|
|
}
|
|
|
|
function constantDistance(/* a, b */) {
|
|
return 1;
|
|
}
|
|
|
|
function compareFScore(a, b) {
|
|
var result = a.fScore - b.fScore;
|
|
// TODO: Can I improve speed with smarter ties-breaking?
|
|
// I tried distanceToSource, but it didn't seem to have much effect
|
|
return result;
|
|
}
|
|
|
|
function setHeapIndex(nodeSearchState, heapIndex) {
|
|
nodeSearchState.heapIndex = heapIndex;
|
|
}
|
|
|
|
function compareF1Score(a, b) {
|
|
return a.f1 - b.f1;
|
|
}
|
|
|
|
function compareF2Score(a, b) {
|
|
return a.f2 - b.f2;
|
|
}
|
|
|
|
function setH1(node, heapIndex) {
|
|
node.h1 = heapIndex;
|
|
}
|
|
|
|
function setH2(node, heapIndex) {
|
|
node.h2 = heapIndex;
|
|
}
|
|
},{}],5:[function(require,module,exports){
|
|
module.exports = {
|
|
l2: l2,
|
|
l1: l1
|
|
};
|
|
|
|
/**
|
|
* Euclid distance (l2 norm);
|
|
*
|
|
* @param {*} a
|
|
* @param {*} b
|
|
*/
|
|
function l2(a, b) {
|
|
var dx = a.x - b.x;
|
|
var dy = a.y - b.y;
|
|
return Math.sqrt(dx * dx + dy * dy);
|
|
}
|
|
|
|
/**
|
|
* Manhattan distance (l1 norm);
|
|
* @param {*} a
|
|
* @param {*} b
|
|
*/
|
|
function l1(a, b) {
|
|
var dx = a.x - b.x;
|
|
var dy = a.y - b.y;
|
|
return Math.abs(dx) + Math.abs(dy);
|
|
}
|
|
|
|
},{}],6:[function(require,module,exports){
|
|
/**
|
|
* This class represents a single search node in the exploration tree for
|
|
* A* algorithm.
|
|
*
|
|
* @param {Object} node original node in the graph
|
|
*/
|
|
function NodeSearchState(node) {
|
|
this.node = node;
|
|
|
|
// How we came to this node?
|
|
this.parent = null;
|
|
|
|
this.closed = false;
|
|
this.open = 0;
|
|
|
|
this.distanceToSource = Number.POSITIVE_INFINITY;
|
|
// the f(n) = g(n) + h(n) value
|
|
this.fScore = Number.POSITIVE_INFINITY;
|
|
|
|
// used to reconstruct heap when fScore is updated.
|
|
this.heapIndex = -1;
|
|
};
|
|
|
|
function makeSearchStatePool() {
|
|
var currentInCache = 0;
|
|
var nodeCache = [];
|
|
|
|
return {
|
|
createNewState: createNewState,
|
|
reset: reset
|
|
};
|
|
|
|
function reset() {
|
|
currentInCache = 0;
|
|
}
|
|
|
|
function createNewState(node) {
|
|
var cached = nodeCache[currentInCache];
|
|
if (cached) {
|
|
// TODO: This almost duplicates constructor code. Not sure if
|
|
// it would impact performance if I move this code into a function
|
|
cached.node = node;
|
|
// How we came to this node?
|
|
cached.parent = null;
|
|
|
|
cached.closed = false;
|
|
cached.open = 0;
|
|
|
|
cached.distanceToSource = Number.POSITIVE_INFINITY;
|
|
// the f(n) = g(n) + h(n) value
|
|
cached.fScore = Number.POSITIVE_INFINITY;
|
|
|
|
// used to reconstruct heap when fScore is updated.
|
|
cached.heapIndex = -1;
|
|
|
|
} else {
|
|
cached = new NodeSearchState(node);
|
|
nodeCache[currentInCache] = cached;
|
|
}
|
|
currentInCache++;
|
|
return cached;
|
|
}
|
|
}
|
|
module.exports = makeSearchStatePool;
|
|
},{}],7:[function(require,module,exports){
|
|
module.exports = nba;
|
|
|
|
var NodeHeap = require('../NodeHeap');
|
|
var heuristics = require('../heuristics');
|
|
var defaultSettings = require('../defaultSettings.js');
|
|
var makeNBASearchStatePool = require('./makeNBASearchStatePool.js');
|
|
|
|
var NO_PATH = defaultSettings.NO_PATH;
|
|
|
|
module.exports.l2 = heuristics.l2;
|
|
module.exports.l1 = heuristics.l1;
|
|
|
|
/**
|
|
* Creates a new instance of pathfinder. A pathfinder has just one method:
|
|
* `find(fromId, toId)`.
|
|
*
|
|
* This is implementation of the NBA* algorithm described in
|
|
*
|
|
* "Yet another bidirectional algorithm for shortest paths" paper by Wim Pijls and Henk Post
|
|
*
|
|
* The paper is available here: https://repub.eur.nl/pub/16100/ei2009-10.pdf
|
|
*
|
|
* @param {ngraph.graph} graph instance. See https://github.com/anvaka/ngraph.graph
|
|
* @param {Object} options that configures search
|
|
* @param {Function(a, b)} options.heuristic - a function that returns estimated distance between
|
|
* nodes `a` and `b`. This function should never overestimate actual distance between two
|
|
* nodes (otherwise the found path will not be the shortest). Defaults function returns 0,
|
|
* which makes this search equivalent to Dijkstra search.
|
|
* @param {Function(a, b)} options.distance - a function that returns actual distance between two
|
|
* nodes `a` and `b`. By default this is set to return graph-theoretical distance (always 1);
|
|
*
|
|
* @returns {Object} A pathfinder with single method `find()`.
|
|
*/
|
|
function nba(graph, options) {
|
|
options = options || {};
|
|
// whether traversal should be considered over oriented graph.
|
|
var oriented = options.oriented;
|
|
var quitFast = options.quitFast;
|
|
|
|
var heuristic = options.heuristic;
|
|
if (!heuristic) heuristic = defaultSettings.heuristic;
|
|
|
|
var distance = options.distance;
|
|
if (!distance) distance = defaultSettings.distance;
|
|
|
|
// During stress tests I noticed that garbage collection was one of the heaviest
|
|
// contributors to the algorithm's speed. So I'm using an object pool to recycle nodes.
|
|
var pool = makeNBASearchStatePool();
|
|
|
|
return {
|
|
/**
|
|
* Finds a path between node `fromId` and `toId`.
|
|
* @returns {Array} of nodes between `toId` and `fromId`. Empty array is returned
|
|
* if no path is found.
|
|
*/
|
|
find: find
|
|
};
|
|
|
|
function find(fromId, toId) {
|
|
// I must apologize for the code duplication. This was the easiest way for me to
|
|
// implement the algorithm fast.
|
|
var from = graph.getNode(fromId);
|
|
if (!from) throw new Error('fromId is not defined in this graph: ' + fromId);
|
|
var to = graph.getNode(toId);
|
|
if (!to) throw new Error('toId is not defined in this graph: ' + toId);
|
|
|
|
pool.reset();
|
|
|
|
// I must also apologize for somewhat cryptic names. The NBA* is bi-directional
|
|
// search algorithm, which means it runs two searches in parallel. One is called
|
|
// forward search and it runs from source node to target, while the other one
|
|
// (backward search) runs from target to source.
|
|
|
|
// Everywhere where you see `1` it means it's for the forward search. `2` is for
|
|
// backward search.
|
|
|
|
// For oriented graph path finding, we need to reverse the graph, so that
|
|
// backward search visits correct link. Obviously we don't want to duplicate
|
|
// the graph, instead we always traverse the graph as non-oriented, and filter
|
|
// edges in `visitN1Oriented/visitN2Oritented`
|
|
var forwardVisitor = oriented ? visitN1Oriented : visitN1;
|
|
var reverseVisitor = oriented ? visitN2Oriented : visitN2;
|
|
|
|
// Maps nodeId to NBASearchState.
|
|
var nodeState = new Map();
|
|
|
|
// These two heaps store nodes by their underestimated values.
|
|
var open1Set = new NodeHeap({
|
|
compare: defaultSettings.compareF1Score,
|
|
setNodeId: defaultSettings.setH1
|
|
});
|
|
var open2Set = new NodeHeap({
|
|
compare: defaultSettings.compareF2Score,
|
|
setNodeId: defaultSettings.setH2
|
|
});
|
|
|
|
// This is where both searches will meet.
|
|
var minNode;
|
|
|
|
// The smallest path length seen so far is stored here:
|
|
var lMin = Number.POSITIVE_INFINITY;
|
|
|
|
// We start by putting start/end nodes to the corresponding heaps
|
|
// If variable names like `f1`, `g1` are too confusing, please refer
|
|
// to makeNBASearchStatePool.js file, which has detailed description.
|
|
var startNode = pool.createNewState(from);
|
|
nodeState.set(fromId, startNode);
|
|
startNode.g1 = 0;
|
|
var f1 = heuristic(from, to);
|
|
startNode.f1 = f1;
|
|
open1Set.push(startNode);
|
|
|
|
var endNode = pool.createNewState(to);
|
|
nodeState.set(toId, endNode);
|
|
endNode.g2 = 0;
|
|
var f2 = f1; // they should agree originally
|
|
endNode.f2 = f2;
|
|
open2Set.push(endNode)
|
|
|
|
// the `cameFrom` variable is accessed by both searches, so that we can store parents.
|
|
var cameFrom;
|
|
|
|
// this is the main algorithm loop:
|
|
while (open2Set.length && open1Set.length) {
|
|
if (open1Set.length < open2Set.length) {
|
|
forwardSearch();
|
|
} else {
|
|
reverseSearch();
|
|
}
|
|
|
|
if (quitFast && minNode) break;
|
|
}
|
|
|
|
var path = reconstructPath(minNode);
|
|
return path; // the public API is over
|
|
|
|
function forwardSearch() {
|
|
cameFrom = open1Set.pop();
|
|
if (cameFrom.closed) {
|
|
return;
|
|
}
|
|
|
|
cameFrom.closed = true;
|
|
|
|
if (cameFrom.f1 < lMin && (cameFrom.g1 + f2 - heuristic(from, cameFrom.node)) < lMin) {
|
|
graph.forEachLinkedNode(cameFrom.node.id, forwardVisitor);
|
|
}
|
|
|
|
if (open1Set.length > 0) {
|
|
// this will be used in reverse search
|
|
f1 = open1Set.peek().f1;
|
|
}
|
|
}
|
|
|
|
function reverseSearch() {
|
|
cameFrom = open2Set.pop();
|
|
if (cameFrom.closed) {
|
|
return;
|
|
}
|
|
cameFrom.closed = true;
|
|
|
|
if (cameFrom.f2 < lMin && (cameFrom.g2 + f1 - heuristic(cameFrom.node, to)) < lMin) {
|
|
graph.forEachLinkedNode(cameFrom.node.id, reverseVisitor);
|
|
}
|
|
|
|
if (open2Set.length > 0) {
|
|
// this will be used in forward search
|
|
f2 = open2Set.peek().f2;
|
|
}
|
|
}
|
|
|
|
function visitN1(otherNode, link) {
|
|
var otherSearchState = nodeState.get(otherNode.id);
|
|
if (!otherSearchState) {
|
|
otherSearchState = pool.createNewState(otherNode);
|
|
nodeState.set(otherNode.id, otherSearchState);
|
|
}
|
|
|
|
if (otherSearchState.closed) return;
|
|
|
|
var tentativeDistance = cameFrom.g1 + distance(cameFrom.node, otherNode, link);
|
|
|
|
if (tentativeDistance < otherSearchState.g1) {
|
|
otherSearchState.g1 = tentativeDistance;
|
|
otherSearchState.f1 = tentativeDistance + heuristic(otherSearchState.node, to);
|
|
otherSearchState.p1 = cameFrom;
|
|
if (otherSearchState.h1 < 0) {
|
|
open1Set.push(otherSearchState);
|
|
} else {
|
|
open1Set.updateItem(otherSearchState.h1);
|
|
}
|
|
}
|
|
var potentialMin = otherSearchState.g1 + otherSearchState.g2;
|
|
if (potentialMin < lMin) {
|
|
lMin = potentialMin;
|
|
minNode = otherSearchState;
|
|
}
|
|
}
|
|
|
|
function visitN2(otherNode, link) {
|
|
var otherSearchState = nodeState.get(otherNode.id);
|
|
if (!otherSearchState) {
|
|
otherSearchState = pool.createNewState(otherNode);
|
|
nodeState.set(otherNode.id, otherSearchState);
|
|
}
|
|
|
|
if (otherSearchState.closed) return;
|
|
|
|
var tentativeDistance = cameFrom.g2 + distance(cameFrom.node, otherNode, link);
|
|
|
|
if (tentativeDistance < otherSearchState.g2) {
|
|
otherSearchState.g2 = tentativeDistance;
|
|
otherSearchState.f2 = tentativeDistance + heuristic(from, otherSearchState.node);
|
|
otherSearchState.p2 = cameFrom;
|
|
if (otherSearchState.h2 < 0) {
|
|
open2Set.push(otherSearchState);
|
|
} else {
|
|
open2Set.updateItem(otherSearchState.h2);
|
|
}
|
|
}
|
|
var potentialMin = otherSearchState.g1 + otherSearchState.g2;
|
|
if (potentialMin < lMin) {
|
|
lMin = potentialMin;
|
|
minNode = otherSearchState;
|
|
}
|
|
}
|
|
|
|
function visitN2Oriented(otherNode, link) {
|
|
// we are going backwards, graph needs to be reversed.
|
|
if (link.toId === cameFrom.node.id) return visitN2(otherNode, link);
|
|
}
|
|
function visitN1Oriented(otherNode, link) {
|
|
// this is forward direction, so we should be coming FROM:
|
|
if (link.fromId === cameFrom.node.id) return visitN1(otherNode, link);
|
|
}
|
|
}
|
|
}
|
|
|
|
function reconstructPath(searchState) {
|
|
if (!searchState) return NO_PATH;
|
|
|
|
var path = [searchState.node];
|
|
var parent = searchState.p1;
|
|
|
|
while (parent) {
|
|
path.push(parent.node);
|
|
parent = parent.p1;
|
|
}
|
|
|
|
var child = searchState.p2;
|
|
|
|
while (child) {
|
|
path.unshift(child.node);
|
|
child = child.p2;
|
|
}
|
|
return path;
|
|
}
|
|
|
|
},{"../NodeHeap":1,"../defaultSettings.js":4,"../heuristics":5,"./makeNBASearchStatePool.js":8}],8:[function(require,module,exports){
|
|
module.exports = makeNBASearchStatePool;
|
|
|
|
/**
|
|
* Creates new instance of NBASearchState. The instance stores information
|
|
* about search state, and is used by NBA* algorithm.
|
|
*
|
|
* @param {Object} node - original graph node
|
|
*/
|
|
function NBASearchState(node) {
|
|
/**
|
|
* Original graph node.
|
|
*/
|
|
this.node = node;
|
|
|
|
/**
|
|
* Parent of this node in forward search
|
|
*/
|
|
this.p1 = null;
|
|
|
|
/**
|
|
* Parent of this node in reverse search
|
|
*/
|
|
this.p2 = null;
|
|
|
|
/**
|
|
* If this is set to true, then the node was already processed
|
|
* and we should not touch it anymore.
|
|
*/
|
|
this.closed = false;
|
|
|
|
/**
|
|
* Actual distance from this node to its parent in forward search
|
|
*/
|
|
this.g1 = Number.POSITIVE_INFINITY;
|
|
|
|
/**
|
|
* Actual distance from this node to its parent in reverse search
|
|
*/
|
|
this.g2 = Number.POSITIVE_INFINITY;
|
|
|
|
|
|
/**
|
|
* Underestimated distance from this node to the path-finding source.
|
|
*/
|
|
this.f1 = Number.POSITIVE_INFINITY;
|
|
|
|
/**
|
|
* Underestimated distance from this node to the path-finding target.
|
|
*/
|
|
this.f2 = Number.POSITIVE_INFINITY;
|
|
|
|
// used to reconstruct heap when fScore is updated. TODO: do I need them both?
|
|
|
|
/**
|
|
* Index of this node in the forward heap.
|
|
*/
|
|
this.h1 = -1;
|
|
|
|
/**
|
|
* Index of this node in the reverse heap.
|
|
*/
|
|
this.h2 = -1;
|
|
}
|
|
|
|
/**
|
|
* As path-finding is memory-intensive process, we want to reduce pressure on
|
|
* garbage collector. This class helps us to recycle path-finding nodes and significantly
|
|
* reduces the search time (~20% faster than without it).
|
|
*/
|
|
function makeNBASearchStatePool() {
|
|
var currentInCache = 0;
|
|
var nodeCache = [];
|
|
|
|
return {
|
|
/**
|
|
* Creates a new NBASearchState instance
|
|
*/
|
|
createNewState: createNewState,
|
|
|
|
/**
|
|
* Marks all created instances available for recycling.
|
|
*/
|
|
reset: reset
|
|
};
|
|
|
|
function reset() {
|
|
currentInCache = 0;
|
|
}
|
|
|
|
function createNewState(node) {
|
|
var cached = nodeCache[currentInCache];
|
|
if (cached) {
|
|
// TODO: This almost duplicates constructor code. Not sure if
|
|
// it would impact performance if I move this code into a function
|
|
cached.node = node;
|
|
|
|
// How we came to this node?
|
|
cached.p1 = null;
|
|
cached.p2 = null;
|
|
|
|
cached.closed = false;
|
|
|
|
cached.g1 = Number.POSITIVE_INFINITY;
|
|
cached.g2 = Number.POSITIVE_INFINITY;
|
|
cached.f1 = Number.POSITIVE_INFINITY;
|
|
cached.f2 = Number.POSITIVE_INFINITY;
|
|
|
|
// used to reconstruct heap when fScore is updated.
|
|
cached.h1 = -1;
|
|
cached.h2 = -1;
|
|
} else {
|
|
cached = new NBASearchState(node);
|
|
nodeCache[currentInCache] = cached;
|
|
}
|
|
currentInCache++;
|
|
return cached;
|
|
}
|
|
}
|
|
|
|
},{}],9:[function(require,module,exports){
|
|
module.exports = {
|
|
aStar: require('./a-star/a-star.js'),
|
|
aGreedy: require('./a-star/a-greedy-star'),
|
|
nba: require('./a-star/nba/index.js'),
|
|
}
|
|
|
|
},{"./a-star/a-greedy-star":2,"./a-star/a-star.js":3,"./a-star/nba/index.js":7}]},{},[9])(9)
|
|
});
|