|
- // javascript-astar 0.4.1
- // http://github.com/bgrins/javascript-astar
- // Freely distributable under the MIT License.
- // Implements the astar search algorithm in javascript using a Binary Heap.
- // Includes Binary Heap (with modifications) from Marijn Haverbeke.
- // http://eloquentjavascript.net/appendix2.html
- (function (definition) {
- /* global module, define */
- if (typeof module === 'object' && typeof module.exports === 'object')
- module.exports = definition();
- else if (typeof define === 'function' && define.amd)
- define([], definition);
- else {
- let exports = definition();
- window.astar = exports.astar;
- window.Graph = exports.Graph;
- }
- })(function () {
- function pathTo (node) {
- let curr = node;
- let path = [];
- while (curr.parent) {
- path.unshift(curr);
- curr = curr.parent;
- }
- return path;
- }
-
- function getHeap () {
- return new BinaryHeap(function (node) {
- return node.f;
- });
- }
-
- let astar = {
- /**
- * Perform an A* Search on a graph given a start and end node.
- * @param {Graph} graph
- * @param {GridNode} start
- * @param {GridNode} end
- * @param {Object} [options]
- * @param {bool} [options.closest] Specifies whether to return the
- path to the closest node if the target is unreachable.
- * @param {Function} [options.heuristic] Heuristic function (see
- * astar.heuristics).
- */
- search: function (graph, start, end, options) {
- start = graph.grid[start.x][start.y] || start;
- end = graph.grid[end.x][end.y] || end;
-
- graph.cleanDirty();
- options = options || {};
- let heuristic = options.heuristic || astar.heuristics.manhattan;
- let closest = options.closest || false;
- let distance = options.distance;
-
- if (distance)
- heuristic = astar.heuristics.manhattanDistance;
-
- let openHeap = getHeap();
- let closestNode = start; // set the start node to be the closest if required
-
- start.h = heuristic(start, end, distance);
- graph.markDirty(start);
-
- openHeap.push(start);
-
- while (openHeap.size() > 0) {
- // Grab the lowest f(x) to process next. Heap keeps this sorted for us.
- let currentNode = openHeap.pop();
-
- let onWall = !currentNode.isWall || currentNode.isWall();
-
- if (!onWall) {
- if (distance) {
- if (currentNode.h === distance)
- return pathTo(currentNode);
- } else {
- // End case -- result has been found, return the traced path.
- if (currentNode === end)
- return pathTo(currentNode);
- }
- }
-
- // Normal case -- move currentNode from open to closed, process each of its neighbors.
- currentNode.closed = true;
-
- // Find all neighbors for the current node.
- let neighbors = graph.neighbors(currentNode);
-
- for (let i = 0, il = neighbors.length; i < il; ++i) {
- let neighbor = neighbors[i];
-
- if (neighbor.closed || neighbor.isWall()) {
- // Not a valid node to process, skip to next neighbor.
- continue;
- }
-
- // The g score is the shortest distance from start to current node.
- // We need to check if the path we have arrived at this neighbor is the shortest one we have seen yet.
- let gScore = currentNode.g + neighbor.getCost(currentNode);
- let beenVisited = neighbor.visited;
-
- if (!beenVisited || gScore < neighbor.g) {
- // Found an optimal (so far) path to this node. Take score for node to see how good it is.
- neighbor.visited = true;
- neighbor.parent = currentNode;
- neighbor.h = neighbor.h || heuristic(neighbor, end, distance);
- neighbor.g = gScore;
- neighbor.f = neighbor.g + neighbor.h;
- graph.markDirty(neighbor);
- if (closest) {
- // If the neighbour is closer than the current closestNode or if it's equally close but has
- // a cheaper path than the current closest node then it becomes the closest node
- if (neighbor.h < closestNode.h || (neighbor.h === closestNode.h && neighbor.g < closestNode.g))
- closestNode = neighbor;
- }
-
- if (!beenVisited) {
- // Pushing to heap will put it in proper place based on the 'f' value.
- openHeap.push(neighbor);
- } else {
- // Already seen the node, but since it has been rescored we need to reorder it in the heap
- openHeap.rescoreElement(neighbor);
- }
- }
- }
- }
-
- if (closest)
- return pathTo(closestNode);
-
- // No result was found - empty array signifies failure to find path.
- return [];
- },
- // See list of heuristics: http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html
- heuristics: {
- manhattan: function (pos0, pos1) {
- let d1 = Math.abs(pos1.x - pos0.x);
- let d2 = Math.abs(pos1.y - pos0.y);
- return Math.max(d1, d2);
- },
- manhattanDistance: function (pos0, pos1, distance) {
- let d1 = Math.abs(pos1.x - pos0.x);
- let d2 = Math.abs(pos1.y - pos0.y);
- return Math.abs(distance - Math.max(d1, d2)) + 1;
- },
- diagonal: function (pos0, pos1) {
- let D = 1;
- let D2 = Math.sqrt(2);
- let d1 = Math.abs(pos1.x - pos0.x);
- let d2 = Math.abs(pos1.y - pos0.y);
- return (D * (d1 + d2)) + ((D2 - (2 * D)) * Math.min(d1, d2));
- }
- },
- cleanNode: function (node) {
- if (!node)
- return;
- node.f = 0;
- node.g = 0;
- node.h = 0;
- node.visited = false;
- node.closed = false;
- node.parent = null;
- }
- };
-
- /**
- * A graph memory structure
- * @param {Array} gridIn 2D array of input weights
- * @param {Object} [options]
- * @param {bool} [options.diagonal] Specifies whether diagonal moves are allowed
- */
- function Graph (gridIn, options) {
- options = options || {};
- this.nodes = [];
- this.diagonal = !!options.diagonal;
- this.grid = [];
-
- for (let x = 0; x < gridIn.length; x++) {
- this.grid[x] = [];
-
- for (let y = 0, row = gridIn[x]; y < row.length; y++) {
- if (!row[y]) {
- node = new GridNode(x, y, row[y] ? 0 : 1);
- this.grid[x][y] = node;
- this.nodes.push(node);
- }
- }
- }
- this.init();
- }
-
- Graph.prototype.init = function () {
- this.dirtyNodes = [];
- for (let i = 0; i < this.nodes.length; i++)
- astar.cleanNode(this.nodes[i]);
- };
-
- Graph.prototype.cleanDirty = function () {
- for (let i = 0; i < this.dirtyNodes.length; i++)
- astar.cleanNode(this.dirtyNodes[i]);
-
- this.dirtyNodes = [];
- };
-
- Graph.prototype.markDirty = function (node) {
- this.dirtyNodes.push(node);
- };
-
- Graph.prototype.neighbors = function (node) {
- let ret = [];
- let x = node.x;
- let y = node.y;
- let grid = this.grid;
-
- // West
- if (grid[x - 1] && grid[x - 1][y])
- ret.push(grid[x - 1][y]);
-
- // East
- if (grid[x + 1] && grid[x + 1][y])
- ret.push(grid[x + 1][y]);
-
- // South
- if (grid[x] && grid[x][y - 1])
- ret.push(grid[x][y - 1]);
-
- // North
- if (grid[x] && grid[x][y + 1])
- ret.push(grid[x][y + 1]);
-
- if (this.diagonal) {
- // Southwest
- if (grid[x - 1] && grid[x - 1][y - 1])
- ret.push(grid[x - 1][y - 1]);
-
- // Southeast
- if (grid[x + 1] && grid[x + 1][y - 1])
- ret.push(grid[x + 1][y - 1]);
-
- // Northwest
- if (grid[x - 1] && grid[x - 1][y + 1])
- ret.push(grid[x - 1][y + 1]);
-
- // Northeast
- if (grid[x + 1] && grid[x + 1][y + 1])
- ret.push(grid[x + 1][y + 1]);
- }
-
- return ret;
- };
-
- Graph.prototype.toString = function () {
- let graphString = [];
- let nodes = this.grid;
- for (let x = 0; x < nodes.length; x++) {
- let rowDebug = [];
- let row = nodes[x];
- for (let y = 0; y < row.length; y++)
- rowDebug.push(row[y].weight);
-
- graphString.push(rowDebug.join(' '));
- }
- return graphString.join('\n');
- };
-
- function GridNode (x, y, weight) {
- this.x = x;
- this.y = y;
- this.weight = weight;
- }
-
- GridNode.prototype.toString = function () {
- return '[' + this.x + ' ' + this.y + ']';
- };
-
- GridNode.prototype.getCost = function (fromNeighbor) {
- // Take diagonal weight into consideration.
- if (fromNeighbor && fromNeighbor.x !== this.x && fromNeighbor.y !== this.y)
- return this.weight * 1.41421;
-
- return this.weight;
- };
-
- GridNode.prototype.isWall = function () {
- return this.weight === 0;
- };
-
- function BinaryHeap (scoreFunction) {
- this.content = [];
- this.scoreFunction = scoreFunction;
- }
-
- BinaryHeap.prototype = {
- push: function (element) {
- // Add the new element to the end of the array.
- this.content.push(element);
-
- // Allow it to sink down.
- this.sinkDown(this.content.length - 1);
- },
- pop: function () {
- // Store the first element so we can return it later.
- let result = this.content[0];
- // Get the element at the end of the array.
- let end = this.content.pop();
- // If there are any elements left, put the end element at the
- // start, and let it bubble up.
- if (this.content.length > 0) {
- this.content[0] = end;
- this.bubbleUp(0);
- }
- return result;
- },
- remove: function (node) {
- let i = this.content.indexOf(node);
-
- // When it is found, the process seen in 'pop' is repeated
- // to fill up the hole.
- let end = this.content.pop();
-
- if (i !== this.content.length - 1) {
- this.content[i] = end;
-
- if (this.scoreFunction(end) < this.scoreFunction(node))
- this.sinkDown(i);
- else
- this.bubbleUp(i);
- }
- },
- size: function () {
- return this.content.length;
- },
- rescoreElement: function (node) {
- this.sinkDown(this.content.indexOf(node));
- },
- sinkDown: function (n) {
- // Fetch the element that has to be sunk.
- let element = this.content[n];
-
- // When at 0, an element can not sink any further.
- while (n > 0) {
- // Compute the parent element's index, and fetch it.
- let parentN = ((n + 1) >> 1) - 1;
- let parent = this.content[parentN];
- // Swap the elements if the parent is greater.
- if (this.scoreFunction(element) < this.scoreFunction(parent)) {
- this.content[parentN] = element;
- this.content[n] = parent;
- // Update 'n' to continue at the new position.
- n = parentN;
- }
- // Found a parent that is less, no need to sink any further.
- else
- break;
- }
- },
- bubbleUp: function (n) {
- // Look up the target element and its score.
- let length = this.content.length;
- let element = this.content[n];
- let elemScore = this.scoreFunction(element);
-
- while (true) {
- // Compute the indices of the child elements.
- let child2N = (n + 1) << 1;
- let child1N = child2N - 1;
- // This is used to store the new position of the element, if any.
- let swap = null;
- let child1Score;
- // If the first child exists (is inside the array)...
- if (child1N < length) {
- // Look it up and compute its score.
- let child1 = this.content[child1N];
- child1Score = this.scoreFunction(child1);
-
- // If the score is less than our element's, we need to swap.
- if (child1Score < elemScore)
- swap = child1N;
- }
-
- // Do the same checks for the other child.
- if (child2N < length) {
- let child2 = this.content[child2N];
- let child2Score = this.scoreFunction(child2);
- if (child2Score < (swap === null ? elemScore : child1Score))
- swap = child2N;
- }
-
- // If the element needs to be moved, swap it, and continue.
- if (swap !== null) {
- this.content[n] = this.content[swap];
- this.content[swap] = element;
- n = swap;
- }
- // Otherwise, we are done.
- else
- break;
- }
- }
- };
-
- return {
- astar: astar,
- Graph: Graph,
- gridNode: GridNode
- };
- });
|