diff --git a/PathFinding.js-0.4.17.zip b/PathFinding.js-0.4.17.zip new file mode 100644 index 0000000..65db859 Binary files /dev/null and b/PathFinding.js-0.4.17.zip differ diff --git a/cytoscape.js-3.29.2.zip b/cytoscape.js-3.29.2.zip deleted file mode 100644 index 70af607..0000000 Binary files a/cytoscape.js-3.29.2.zip and /dev/null differ diff --git a/src/js/pathFinder/PathFinding.js b/src/js/pathFinder/PathFinding.js new file mode 100644 index 0000000..5435d02 --- /dev/null +++ b/src/js/pathFinder/PathFinding.js @@ -0,0 +1,18 @@ +module.exports = { + 'Heap' : require('heap'), + 'Node' : require('./core/Node'), + 'Grid' : require('./core/Grid'), + 'Util' : require('./core/Util'), + 'DiagonalMovement' : require('./core/DiagonalMovement'), + 'Heuristic' : require('./core/Heuristic'), + 'AStarFinder' : require('./finders/AStarFinder'), + 'BestFirstFinder' : require('./finders/BestFirstFinder'), + 'BreadthFirstFinder' : require('./finders/BreadthFirstFinder'), + 'DijkstraFinder' : require('./finders/DijkstraFinder'), + 'BiAStarFinder' : require('./finders/BiAStarFinder'), + 'BiBestFirstFinder' : require('./finders/BiBestFirstFinder'), + 'BiBreadthFirstFinder' : require('./finders/BiBreadthFirstFinder'), + 'BiDijkstraFinder' : require('./finders/BiDijkstraFinder'), + 'IDAStarFinder' : require('./finders/IDAStarFinder'), + 'JumpPointFinder' : require('./finders/JumpPointFinder'), +}; diff --git a/src/js/pathFinder/banner b/src/js/pathFinder/banner new file mode 100644 index 0000000..2e5e81c --- /dev/null +++ b/src/js/pathFinder/banner @@ -0,0 +1,9 @@ +/** + * ____ _ _ _____ _ _ _ _ + * | _ \ __ _| |_| |__ | ___(_)_ __ __| (_)_ __ __ _ (_)___ + * | |_) / _` | __| '_ \| |_ | | '_ \ / _` | | '_ \ / _` | | / __| + * | __/ (_| | |_| | | | _| | | | | | (_| | | | | | (_| |_ | \__ \ + * |_| \__,_|\__|_| |_|_| |_|_| |_|\__,_|_|_| |_|\__, (_)/ |___/ + * |___/ |__/ + * https://github.com/qiao/PathFinding.js + */ diff --git a/src/js/pathFinder/core/DiagonalMovement.js b/src/js/pathFinder/core/DiagonalMovement.js new file mode 100644 index 0000000..3cb3734 --- /dev/null +++ b/src/js/pathFinder/core/DiagonalMovement.js @@ -0,0 +1,8 @@ +var DiagonalMovement = { + Always: 1, + Never: 2, + IfAtMostOneObstacle: 3, + OnlyWhenNoObstacles: 4 +}; + +module.exports = DiagonalMovement; \ No newline at end of file diff --git a/src/js/pathFinder/core/Grid.js b/src/js/pathFinder/core/Grid.js new file mode 100644 index 0000000..31ae6ee --- /dev/null +++ b/src/js/pathFinder/core/Grid.js @@ -0,0 +1,247 @@ +var Node = require('./Node'); +var DiagonalMovement = require('./DiagonalMovement'); + +/** + * The Grid class, which serves as the encapsulation of the layout of the nodes. + * @constructor + * @param {number|Array.>} width_or_matrix Number of columns of the grid, or matrix + * @param {number} height Number of rows of the grid. + * @param {Array.>} [matrix] - A 0-1 matrix + * representing the walkable status of the nodes(0 or false for walkable). + * If the matrix is not supplied, all the nodes will be walkable. */ +function Grid(width_or_matrix, height, matrix) { + var width; + + if (typeof width_or_matrix !== 'object') { + width = width_or_matrix; + } else { + height = width_or_matrix.length; + width = width_or_matrix[0].length; + matrix = width_or_matrix; + } + + /** + * The number of columns of the grid. + * @type number + */ + this.width = width; + /** + * The number of rows of the grid. + * @type number + */ + this.height = height; + + /** + * A 2D array of nodes. + */ + this.nodes = this._buildNodes(width, height, matrix); +} + +/** + * Build and return the nodes. + * @private + * @param {number} width + * @param {number} height + * @param {Array.>} [matrix] - A 0-1 matrix representing + * the walkable status of the nodes. + * @see Grid + */ +Grid.prototype._buildNodes = function(width, height, matrix) { + var i, j, + nodes = new Array(height), + row; + + for (i = 0; i < height; ++i) { + nodes[i] = new Array(width); + for (j = 0; j < width; ++j) { + nodes[i][j] = new Node(j, i); + } + } + + + if (matrix === undefined) { + return nodes; + } + + if (matrix.length !== height || matrix[0].length !== width) { + throw new Error('Matrix size does not fit'); + } + + for (i = 0; i < height; ++i) { + for (j = 0; j < width; ++j) { + if (matrix[i][j]) { + // 0, false, null will be walkable + // while others will be un-walkable + nodes[i][j].walkable = false; + } + } + } + + return nodes; +}; + + +Grid.prototype.getNodeAt = function(x, y) { + return this.nodes[y][x]; +}; + + +/** + * Determine whether the node at the given position is walkable. + * (Also returns false if the position is outside the grid.) + * @param {number} x - The x coordinate of the node. + * @param {number} y - The y coordinate of the node. + * @return {boolean} - The walkability of the node. + */ +Grid.prototype.isWalkableAt = function(x, y) { + return this.isInside(x, y) && this.nodes[y][x].walkable; +}; + + +/** + * Determine whether the position is inside the grid. + * XXX: `grid.isInside(x, y)` is wierd to read. + * It should be `(x, y) is inside grid`, but I failed to find a better + * name for this method. + * @param {number} x + * @param {number} y + * @return {boolean} + */ +Grid.prototype.isInside = function(x, y) { + return (x >= 0 && x < this.width) && (y >= 0 && y < this.height); +}; + + +/** + * Set whether the node on the given position is walkable. + * NOTE: throws exception if the coordinate is not inside the grid. + * @param {number} x - The x coordinate of the node. + * @param {number} y - The y coordinate of the node. + * @param {boolean} walkable - Whether the position is walkable. + */ +Grid.prototype.setWalkableAt = function(x, y, walkable) { + this.nodes[y][x].walkable = walkable; +}; + + +/** + * Get the neighbors of the given node. + * + * offsets diagonalOffsets: + * +---+---+---+ +---+---+---+ + * | | 0 | | | 0 | | 1 | + * +---+---+---+ +---+---+---+ + * | 3 | | 1 | | | | | + * +---+---+---+ +---+---+---+ + * | | 2 | | | 3 | | 2 | + * +---+---+---+ +---+---+---+ + * + * When allowDiagonal is true, if offsets[i] is valid, then + * diagonalOffsets[i] and + * diagonalOffsets[(i + 1) % 4] is valid. + * @param {Node} node + * @param {DiagonalMovement} diagonalMovement + */ +Grid.prototype.getNeighbors = function(node, diagonalMovement) { + var x = node.x, + y = node.y, + neighbors = [], + s0 = false, d0 = false, + s1 = false, d1 = false, + s2 = false, d2 = false, + s3 = false, d3 = false, + nodes = this.nodes; + + // ↑ + if (this.isWalkableAt(x, y - 1)) { + neighbors.push(nodes[y - 1][x]); + s0 = true; + } + // → + if (this.isWalkableAt(x + 1, y)) { + neighbors.push(nodes[y][x + 1]); + s1 = true; + } + // ↓ + if (this.isWalkableAt(x, y + 1)) { + neighbors.push(nodes[y + 1][x]); + s2 = true; + } + // ← + if (this.isWalkableAt(x - 1, y)) { + neighbors.push(nodes[y][x - 1]); + s3 = true; + } + + if (diagonalMovement === DiagonalMovement.Never) { + return neighbors; + } + + if (diagonalMovement === DiagonalMovement.OnlyWhenNoObstacles) { + d0 = s3 && s0; + d1 = s0 && s1; + d2 = s1 && s2; + d3 = s2 && s3; + } else if (diagonalMovement === DiagonalMovement.IfAtMostOneObstacle) { + d0 = s3 || s0; + d1 = s0 || s1; + d2 = s1 || s2; + d3 = s2 || s3; + } else if (diagonalMovement === DiagonalMovement.Always) { + d0 = true; + d1 = true; + d2 = true; + d3 = true; + } else { + throw new Error('Incorrect value of diagonalMovement'); + } + + // ↖ + if (d0 && this.isWalkableAt(x - 1, y - 1)) { + neighbors.push(nodes[y - 1][x - 1]); + } + // ↗ + if (d1 && this.isWalkableAt(x + 1, y - 1)) { + neighbors.push(nodes[y - 1][x + 1]); + } + // ↘ + if (d2 && this.isWalkableAt(x + 1, y + 1)) { + neighbors.push(nodes[y + 1][x + 1]); + } + // ↙ + if (d3 && this.isWalkableAt(x - 1, y + 1)) { + neighbors.push(nodes[y + 1][x - 1]); + } + + return neighbors; +}; + + +/** + * Get a clone of this grid. + * @return {Grid} Cloned grid. + */ +Grid.prototype.clone = function() { + var i, j, + + width = this.width, + height = this.height, + thisNodes = this.nodes, + + newGrid = new Grid(width, height), + newNodes = new Array(height), + row; + + for (i = 0; i < height; ++i) { + newNodes[i] = new Array(width); + for (j = 0; j < width; ++j) { + newNodes[i][j] = new Node(j, i, thisNodes[i][j].walkable); + } + } + + newGrid.nodes = newNodes; + + return newGrid; +}; + +module.exports = Grid; diff --git a/src/js/pathFinder/core/Heuristic.js b/src/js/pathFinder/core/Heuristic.js new file mode 100644 index 0000000..4f0af32 --- /dev/null +++ b/src/js/pathFinder/core/Heuristic.js @@ -0,0 +1,48 @@ +/** + * @namespace PF.Heuristic + * @description A collection of heuristic functions. + */ +module.exports = { + + /** + * Manhattan distance. + * @param {number} dx - Difference in x. + * @param {number} dy - Difference in y. + * @return {number} dx + dy + */ + manhattan: function(dx, dy) { + return dx + dy; + }, + + /** + * Euclidean distance. + * @param {number} dx - Difference in x. + * @param {number} dy - Difference in y. + * @return {number} sqrt(dx * dx + dy * dy) + */ + euclidean: function(dx, dy) { + return Math.sqrt(dx * dx + dy * dy); + }, + + /** + * Octile distance. + * @param {number} dx - Difference in x. + * @param {number} dy - Difference in y. + * @return {number} sqrt(dx * dx + dy * dy) for grids + */ + octile: function(dx, dy) { + var F = Math.SQRT2 - 1; + return (dx < dy) ? F * dx + dy : F * dy + dx; + }, + + /** + * Chebyshev distance. + * @param {number} dx - Difference in x. + * @param {number} dy - Difference in y. + * @return {number} max(dx, dy) + */ + chebyshev: function(dx, dy) { + return Math.max(dx, dy); + } + +}; diff --git a/src/js/pathFinder/core/Node.js b/src/js/pathFinder/core/Node.js new file mode 100644 index 0000000..8df8ba5 --- /dev/null +++ b/src/js/pathFinder/core/Node.js @@ -0,0 +1,28 @@ +/** + * A node in grid. + * This class holds some basic information about a node and custom + * attributes may be added, depending on the algorithms' needs. + * @constructor + * @param {number} x - The x coordinate of the node on the grid. + * @param {number} y - The y coordinate of the node on the grid. + * @param {boolean} [walkable] - Whether this node is walkable. + */ +function Node(x, y, walkable) { + /** + * The x coordinate of the node on the grid. + * @type number + */ + this.x = x; + /** + * The y coordinate of the node on the grid. + * @type number + */ + this.y = y; + /** + * Whether this node can be walked through. + * @type boolean + */ + this.walkable = (walkable === undefined ? true : walkable); +} + +module.exports = Node; diff --git a/src/js/pathFinder/core/Util.js b/src/js/pathFinder/core/Util.js new file mode 100644 index 0000000..615f5dd --- /dev/null +++ b/src/js/pathFinder/core/Util.js @@ -0,0 +1,246 @@ +/** + * Backtrace according to the parent records and return the path. + * (including both start and end nodes) + * @param {Node} node End node + * @return {Array.>} the path + */ +function backtrace(node) { + var path = [[node.x, node.y]]; + while (node.parent) { + node = node.parent; + path.push([node.x, node.y]); + } + return path.reverse(); +} +exports.backtrace = backtrace; + +/** + * Backtrace from start and end node, and return the path. + * (including both start and end nodes) + * @param {Node} + * @param {Node} + */ +function biBacktrace(nodeA, nodeB) { + var pathA = backtrace(nodeA), + pathB = backtrace(nodeB); + return pathA.concat(pathB.reverse()); +} +exports.biBacktrace = biBacktrace; + +/** + * Compute the length of the path. + * @param {Array.>} path The path + * @return {number} The length of the path + */ +function pathLength(path) { + var i, sum = 0, a, b, dx, dy; + for (i = 1; i < path.length; ++i) { + a = path[i - 1]; + b = path[i]; + dx = a[0] - b[0]; + dy = a[1] - b[1]; + sum += Math.sqrt(dx * dx + dy * dy); + } + return sum; +} +exports.pathLength = pathLength; + + +/** + * Given the start and end coordinates, return all the coordinates lying + * on the line formed by these coordinates, based on Bresenham's algorithm. + * http://en.wikipedia.org/wiki/Bresenham's_line_algorithm#Simplification + * @param {number} x0 Start x coordinate + * @param {number} y0 Start y coordinate + * @param {number} x1 End x coordinate + * @param {number} y1 End y coordinate + * @return {Array.>} The coordinates on the line + */ +function interpolate(x0, y0, x1, y1) { + var abs = Math.abs, + line = [], + sx, sy, dx, dy, err, e2; + + dx = abs(x1 - x0); + dy = abs(y1 - y0); + + sx = (x0 < x1) ? 1 : -1; + sy = (y0 < y1) ? 1 : -1; + + err = dx - dy; + + while (true) { + line.push([x0, y0]); + + if (x0 === x1 && y0 === y1) { + break; + } + + e2 = 2 * err; + if (e2 > -dy) { + err = err - dy; + x0 = x0 + sx; + } + if (e2 < dx) { + err = err + dx; + y0 = y0 + sy; + } + } + + return line; +} +exports.interpolate = interpolate; + + +/** + * Given a compressed path, return a new path that has all the segments + * in it interpolated. + * @param {Array.>} path The path + * @return {Array.>} expanded path + */ +function expandPath(path) { + var expanded = [], + len = path.length, + coord0, coord1, + interpolated, + interpolatedLen, + i, j; + + if (len < 2) { + return expanded; + } + + for (i = 0; i < len - 1; ++i) { + coord0 = path[i]; + coord1 = path[i + 1]; + + interpolated = interpolate(coord0[0], coord0[1], coord1[0], coord1[1]); + interpolatedLen = interpolated.length; + for (j = 0; j < interpolatedLen - 1; ++j) { + expanded.push(interpolated[j]); + } + } + expanded.push(path[len - 1]); + + return expanded; +} +exports.expandPath = expandPath; + + +/** + * Smoothen the give path. + * The original path will not be modified; a new path will be returned. + * @param {PF.Grid} grid + * @param {Array.>} path The path + */ +function smoothenPath(grid, path) { + var len = path.length, + x0 = path[0][0], // path start x + y0 = path[0][1], // path start y + x1 = path[len - 1][0], // path end x + y1 = path[len - 1][1], // path end y + sx, sy, // current start coordinate + ex, ey, // current end coordinate + newPath, + i, j, coord, line, testCoord, blocked; + + sx = x0; + sy = y0; + newPath = [[sx, sy]]; + + for (i = 2; i < len; ++i) { + coord = path[i]; + ex = coord[0]; + ey = coord[1]; + line = interpolate(sx, sy, ex, ey); + + blocked = false; + for (j = 1; j < line.length; ++j) { + testCoord = line[j]; + + if (!grid.isWalkableAt(testCoord[0], testCoord[1])) { + blocked = true; + break; + } + } + if (blocked) { + lastValidCoord = path[i - 1]; + newPath.push(lastValidCoord); + sx = lastValidCoord[0]; + sy = lastValidCoord[1]; + } + } + newPath.push([x1, y1]); + + return newPath; +} +exports.smoothenPath = smoothenPath; + + +/** + * Compress a path, remove redundant nodes without altering the shape + * The original path is not modified + * @param {Array.>} path The path + * @return {Array.>} The compressed path + */ +function compressPath(path) { + + // nothing to compress + if(path.length < 3) { + return path; + } + + var compressed = [], + sx = path[0][0], // start x + sy = path[0][1], // start y + px = path[1][0], // second point x + py = path[1][1], // second point y + dx = px - sx, // direction between the two points + dy = py - sy, // direction between the two points + lx, ly, + ldx, ldy, + sq, i; + + // normalize the direction + sq = Math.sqrt(dx*dx + dy*dy); + dx /= sq; + dy /= sq; + + // start the new path + compressed.push([sx,sy]); + + for(i = 2; i < path.length; i++) { + + // store the last point + lx = px; + ly = py; + + // store the last direction + ldx = dx; + ldy = dy; + + // next point + px = path[i][0]; + py = path[i][1]; + + // next direction + dx = px - lx; + dy = py - ly; + + // normalize + sq = Math.sqrt(dx*dx + dy*dy); + dx /= sq; + dy /= sq; + + // if the direction has changed, store the point + if ( dx !== ldx || dy !== ldy ) { + compressed.push([lx,ly]); + } + } + + // store the last point + compressed.push([px,py]); + + return compressed; +} +exports.compressPath = compressPath; diff --git a/src/js/pathFinder/finders/AStarFinder.js b/src/js/pathFinder/finders/AStarFinder.js new file mode 100644 index 0000000..c0a1b81 --- /dev/null +++ b/src/js/pathFinder/finders/AStarFinder.js @@ -0,0 +1,125 @@ +var Heap = require('heap'); +var Util = require('../core/Util'); +var Heuristic = require('../core/Heuristic'); +var DiagonalMovement = require('../core/DiagonalMovement'); + +/** + * A* path-finder. + * based upon https://github.com/bgrins/javascript-astar + * @constructor + * @param {object} opt + * @param {boolean} opt.allowDiagonal Whether diagonal movement is allowed. Deprecated, use diagonalMovement instead. + * @param {boolean} opt.dontCrossCorners Disallow diagonal movement touching block corners. Deprecated, use diagonalMovement instead. + * @param {DiagonalMovement} opt.diagonalMovement Allowed diagonal movement. + * @param {function} opt.heuristic Heuristic function to estimate the distance + * (defaults to manhattan). + * @param {integer} opt.weight Weight to apply to the heuristic to allow for suboptimal paths, + * in order to speed up the search. + */ +function AStarFinder(opt) { + opt = opt || {}; + this.allowDiagonal = opt.allowDiagonal; + this.dontCrossCorners = opt.dontCrossCorners; + this.heuristic = opt.heuristic || Heuristic.manhattan; + this.weight = opt.weight || 1; + this.diagonalMovement = opt.diagonalMovement; + + if (!this.diagonalMovement) { + if (!this.allowDiagonal) { + this.diagonalMovement = DiagonalMovement.Never; + } else { + if (this.dontCrossCorners) { + this.diagonalMovement = DiagonalMovement.OnlyWhenNoObstacles; + } else { + this.diagonalMovement = DiagonalMovement.IfAtMostOneObstacle; + } + } + } + + //When diagonal movement is allowed the manhattan heuristic is not admissible + //It should be octile instead + if (this.diagonalMovement === DiagonalMovement.Never) { + this.heuristic = opt.heuristic || Heuristic.manhattan; + } else { + this.heuristic = opt.heuristic || Heuristic.octile; + } +} + +/** + * Find and return the the path. + * @return {Array.<[number, number]>} The path, including both start and + * end positions. + */ +AStarFinder.prototype.findPath = function(startX, startY, endX, endY, grid) { + var openList = new Heap(function(nodeA, nodeB) { + return nodeA.f - nodeB.f; + }), + startNode = grid.getNodeAt(startX, startY), + endNode = grid.getNodeAt(endX, endY), + heuristic = this.heuristic, + diagonalMovement = this.diagonalMovement, + weight = this.weight, + abs = Math.abs, SQRT2 = Math.SQRT2, + node, neighbors, neighbor, i, l, x, y, ng; + + // set the `g` and `f` value of the start node to be 0 + startNode.g = 0; + startNode.f = 0; + + // push the start node into the open list + openList.push(startNode); + startNode.opened = true; + + // while the open list is not empty + while (!openList.empty()) { + // pop the position of node which has the minimum `f` value. + node = openList.pop(); + node.closed = true; + + // if reached the end position, construct the path and return it + if (node === endNode) { + return Util.backtrace(endNode); + } + + // get neigbours of the current node + neighbors = grid.getNeighbors(node, diagonalMovement); + for (i = 0, l = neighbors.length; i < l; ++i) { + neighbor = neighbors[i]; + + if (neighbor.closed) { + continue; + } + + x = neighbor.x; + y = neighbor.y; + + // get the distance between current node and the neighbor + // and calculate the next g score + ng = node.g + ((x - node.x === 0 || y - node.y === 0) ? 1 : SQRT2); + + // check if the neighbor has not been inspected yet, or + // can be reached with smaller cost from the current node + if (!neighbor.opened || ng < neighbor.g) { + neighbor.g = ng; + neighbor.h = neighbor.h || weight * heuristic(abs(x - endX), abs(y - endY)); + neighbor.f = neighbor.g + neighbor.h; + neighbor.parent = node; + + if (!neighbor.opened) { + openList.push(neighbor); + neighbor.opened = true; + } else { + // the neighbor can be reached with smaller cost. + // Since its f value has been updated, we have to + // update its position in the open list + openList.updateItem(neighbor); + } + } + } // end for each neighbor + } // end while not open list empty + + // fail to find the path + return []; +}; + +module.exports = AStarFinder; diff --git a/src/js/pathFinder/finders/BestFirstFinder.js b/src/js/pathFinder/finders/BestFirstFinder.js new file mode 100644 index 0000000..04af65d --- /dev/null +++ b/src/js/pathFinder/finders/BestFirstFinder.js @@ -0,0 +1,26 @@ +var AStarFinder = require('./AStarFinder'); + +/** + * Best-First-Search path-finder. + * @constructor + * @extends AStarFinder + * @param {object} opt + * @param {boolean} opt.allowDiagonal Whether diagonal movement is allowed. Deprecated, use diagonalMovement instead. + * @param {boolean} opt.dontCrossCorners Disallow diagonal movement touching block corners. Deprecated, use diagonalMovement instead. + * @param {DiagonalMovement} opt.diagonalMovement Allowed diagonal movement. + * @param {function} opt.heuristic Heuristic function to estimate the distance + * (defaults to manhattan). + */ +function BestFirstFinder(opt) { + AStarFinder.call(this, opt); + + var orig = this.heuristic; + this.heuristic = function(dx, dy) { + return orig(dx, dy) * 1000000; + }; +} + +BestFirstFinder.prototype = new AStarFinder(); +BestFirstFinder.prototype.constructor = BestFirstFinder; + +module.exports = BestFirstFinder; diff --git a/src/js/pathFinder/finders/BiAStarFinder.js b/src/js/pathFinder/finders/BiAStarFinder.js new file mode 100644 index 0000000..4915947 --- /dev/null +++ b/src/js/pathFinder/finders/BiAStarFinder.js @@ -0,0 +1,177 @@ +var Heap = require('heap'); +var Util = require('../core/Util'); +var Heuristic = require('../core/Heuristic'); +var DiagonalMovement = require('../core/DiagonalMovement'); + +/** + * A* path-finder. + * based upon https://github.com/bgrins/javascript-astar + * @constructor + * @param {object} opt + * @param {boolean} opt.allowDiagonal Whether diagonal movement is allowed. Deprecated, use diagonalMovement instead. + * @param {boolean} opt.dontCrossCorners Disallow diagonal movement touching block corners. Deprecated, use diagonalMovement instead. + * @param {DiagonalMovement} opt.diagonalMovement Allowed diagonal movement. + * @param {function} opt.heuristic Heuristic function to estimate the distance + * (defaults to manhattan). + * @param {integer} opt.weight Weight to apply to the heuristic to allow for suboptimal paths, + * in order to speed up the search. + */ +function BiAStarFinder(opt) { + opt = opt || {}; + this.allowDiagonal = opt.allowDiagonal; + this.dontCrossCorners = opt.dontCrossCorners; + this.diagonalMovement = opt.diagonalMovement; + this.heuristic = opt.heuristic || Heuristic.manhattan; + this.weight = opt.weight || 1; + + if (!this.diagonalMovement) { + if (!this.allowDiagonal) { + this.diagonalMovement = DiagonalMovement.Never; + } else { + if (this.dontCrossCorners) { + this.diagonalMovement = DiagonalMovement.OnlyWhenNoObstacles; + } else { + this.diagonalMovement = DiagonalMovement.IfAtMostOneObstacle; + } + } + } + + //When diagonal movement is allowed the manhattan heuristic is not admissible + //It should be octile instead + if (this.diagonalMovement === DiagonalMovement.Never) { + this.heuristic = opt.heuristic || Heuristic.manhattan; + } else { + this.heuristic = opt.heuristic || Heuristic.octile; + } +} + +/** + * Find and return the the path. + * @return {Array.<[number, number]>} The path, including both start and + * end positions. + */ +BiAStarFinder.prototype.findPath = function(startX, startY, endX, endY, grid) { + var cmp = function(nodeA, nodeB) { + return nodeA.f - nodeB.f; + }, + startOpenList = new Heap(cmp), + endOpenList = new Heap(cmp), + startNode = grid.getNodeAt(startX, startY), + endNode = grid.getNodeAt(endX, endY), + heuristic = this.heuristic, + diagonalMovement = this.diagonalMovement, + weight = this.weight, + abs = Math.abs, SQRT2 = Math.SQRT2, + node, neighbors, neighbor, i, l, x, y, ng, + BY_START = 1, BY_END = 2; + + // set the `g` and `f` value of the start node to be 0 + // and push it into the start open list + startNode.g = 0; + startNode.f = 0; + startOpenList.push(startNode); + startNode.opened = BY_START; + + // set the `g` and `f` value of the end node to be 0 + // and push it into the open open list + endNode.g = 0; + endNode.f = 0; + endOpenList.push(endNode); + endNode.opened = BY_END; + + // while both the open lists are not empty + while (!startOpenList.empty() && !endOpenList.empty()) { + + // pop the position of start node which has the minimum `f` value. + node = startOpenList.pop(); + node.closed = true; + + // get neigbours of the current node + neighbors = grid.getNeighbors(node, diagonalMovement); + for (i = 0, l = neighbors.length; i < l; ++i) { + neighbor = neighbors[i]; + + if (neighbor.closed) { + continue; + } + if (neighbor.opened === BY_END) { + return Util.biBacktrace(node, neighbor); + } + + x = neighbor.x; + y = neighbor.y; + + // get the distance between current node and the neighbor + // and calculate the next g score + ng = node.g + ((x - node.x === 0 || y - node.y === 0) ? 1 : SQRT2); + + // check if the neighbor has not been inspected yet, or + // can be reached with smaller cost from the current node + if (!neighbor.opened || ng < neighbor.g) { + neighbor.g = ng; + neighbor.h = neighbor.h || weight * heuristic(abs(x - endX), abs(y - endY)); + neighbor.f = neighbor.g + neighbor.h; + neighbor.parent = node; + + if (!neighbor.opened) { + startOpenList.push(neighbor); + neighbor.opened = BY_START; + } else { + // the neighbor can be reached with smaller cost. + // Since its f value has been updated, we have to + // update its position in the open list + startOpenList.updateItem(neighbor); + } + } + } // end for each neighbor + + + // pop the position of end node which has the minimum `f` value. + node = endOpenList.pop(); + node.closed = true; + + // get neigbours of the current node + neighbors = grid.getNeighbors(node, diagonalMovement); + for (i = 0, l = neighbors.length; i < l; ++i) { + neighbor = neighbors[i]; + + if (neighbor.closed) { + continue; + } + if (neighbor.opened === BY_START) { + return Util.biBacktrace(neighbor, node); + } + + x = neighbor.x; + y = neighbor.y; + + // get the distance between current node and the neighbor + // and calculate the next g score + ng = node.g + ((x - node.x === 0 || y - node.y === 0) ? 1 : SQRT2); + + // check if the neighbor has not been inspected yet, or + // can be reached with smaller cost from the current node + if (!neighbor.opened || ng < neighbor.g) { + neighbor.g = ng; + neighbor.h = neighbor.h || weight * heuristic(abs(x - startX), abs(y - startY)); + neighbor.f = neighbor.g + neighbor.h; + neighbor.parent = node; + + if (!neighbor.opened) { + endOpenList.push(neighbor); + neighbor.opened = BY_END; + } else { + // the neighbor can be reached with smaller cost. + // Since its f value has been updated, we have to + // update its position in the open list + endOpenList.updateItem(neighbor); + } + } + } // end for each neighbor + } // end while not open list empty + + // fail to find the path + return []; +}; + +module.exports = BiAStarFinder; diff --git a/src/js/pathFinder/finders/BiBestFirstFinder.js b/src/js/pathFinder/finders/BiBestFirstFinder.js new file mode 100644 index 0000000..326c191 --- /dev/null +++ b/src/js/pathFinder/finders/BiBestFirstFinder.js @@ -0,0 +1,26 @@ +var BiAStarFinder = require('./BiAStarFinder'); + +/** + * Bi-direcitional Best-First-Search path-finder. + * @constructor + * @extends BiAStarFinder + * @param {object} opt + * @param {boolean} opt.allowDiagonal Whether diagonal movement is allowed. Deprecated, use diagonalMovement instead. + * @param {boolean} opt.dontCrossCorners Disallow diagonal movement touching block corners. Deprecated, use diagonalMovement instead. + * @param {DiagonalMovement} opt.diagonalMovement Allowed diagonal movement. + * @param {function} opt.heuristic Heuristic function to estimate the distance + * (defaults to manhattan). + */ +function BiBestFirstFinder(opt) { + BiAStarFinder.call(this, opt); + + var orig = this.heuristic; + this.heuristic = function(dx, dy) { + return orig(dx, dy) * 1000000; + }; +} + +BiBestFirstFinder.prototype = new BiAStarFinder(); +BiBestFirstFinder.prototype.constructor = BiBestFirstFinder; + +module.exports = BiBestFirstFinder; diff --git a/src/js/pathFinder/finders/BiBreadthFirstFinder.js b/src/js/pathFinder/finders/BiBreadthFirstFinder.js new file mode 100644 index 0000000..168e05f --- /dev/null +++ b/src/js/pathFinder/finders/BiBreadthFirstFinder.js @@ -0,0 +1,113 @@ +var Util = require('../core/Util'); +var DiagonalMovement = require('../core/DiagonalMovement'); + +/** + * Bi-directional Breadth-First-Search path finder. + * @constructor + * @param {object} opt + * @param {boolean} opt.allowDiagonal Whether diagonal movement is allowed. Deprecated, use diagonalMovement instead. + * @param {boolean} opt.dontCrossCorners Disallow diagonal movement touching block corners. Deprecated, use diagonalMovement instead. + * @param {DiagonalMovement} opt.diagonalMovement Allowed diagonal movement. + */ +function BiBreadthFirstFinder(opt) { + opt = opt || {}; + this.allowDiagonal = opt.allowDiagonal; + this.dontCrossCorners = opt.dontCrossCorners; + this.diagonalMovement = opt.diagonalMovement; + + if (!this.diagonalMovement) { + if (!this.allowDiagonal) { + this.diagonalMovement = DiagonalMovement.Never; + } else { + if (this.dontCrossCorners) { + this.diagonalMovement = DiagonalMovement.OnlyWhenNoObstacles; + } else { + this.diagonalMovement = DiagonalMovement.IfAtMostOneObstacle; + } + } + } +} + + +/** + * Find and return the the path. + * @return {Array.<[number, number]>} The path, including both start and + * end positions. + */ +BiBreadthFirstFinder.prototype.findPath = function(startX, startY, endX, endY, grid) { + var startNode = grid.getNodeAt(startX, startY), + endNode = grid.getNodeAt(endX, endY), + startOpenList = [], endOpenList = [], + neighbors, neighbor, node, + diagonalMovement = this.diagonalMovement, + BY_START = 0, BY_END = 1, + i, l; + + // push the start and end nodes into the queues + startOpenList.push(startNode); + startNode.opened = true; + startNode.by = BY_START; + + endOpenList.push(endNode); + endNode.opened = true; + endNode.by = BY_END; + + // while both the queues are not empty + while (startOpenList.length && endOpenList.length) { + + // expand start open list + + node = startOpenList.shift(); + node.closed = true; + + neighbors = grid.getNeighbors(node, diagonalMovement); + for (i = 0, l = neighbors.length; i < l; ++i) { + neighbor = neighbors[i]; + + if (neighbor.closed) { + continue; + } + if (neighbor.opened) { + // if this node has been inspected by the reversed search, + // then a path is found. + if (neighbor.by === BY_END) { + return Util.biBacktrace(node, neighbor); + } + continue; + } + startOpenList.push(neighbor); + neighbor.parent = node; + neighbor.opened = true; + neighbor.by = BY_START; + } + + // expand end open list + + node = endOpenList.shift(); + node.closed = true; + + neighbors = grid.getNeighbors(node, diagonalMovement); + for (i = 0, l = neighbors.length; i < l; ++i) { + neighbor = neighbors[i]; + + if (neighbor.closed) { + continue; + } + if (neighbor.opened) { + if (neighbor.by === BY_START) { + return Util.biBacktrace(neighbor, node); + } + continue; + } + endOpenList.push(neighbor); + neighbor.parent = node; + neighbor.opened = true; + neighbor.by = BY_END; + } + } + + // fail to find the path + return []; +}; + +module.exports = BiBreadthFirstFinder; diff --git a/src/js/pathFinder/finders/BiDijkstraFinder.js b/src/js/pathFinder/finders/BiDijkstraFinder.js new file mode 100644 index 0000000..0e55fa6 --- /dev/null +++ b/src/js/pathFinder/finders/BiDijkstraFinder.js @@ -0,0 +1,22 @@ +var BiAStarFinder = require('./BiAStarFinder'); + +/** + * Bi-directional Dijkstra path-finder. + * @constructor + * @extends BiAStarFinder + * @param {object} opt + * @param {boolean} opt.allowDiagonal Whether diagonal movement is allowed. Deprecated, use diagonalMovement instead. + * @param {boolean} opt.dontCrossCorners Disallow diagonal movement touching block corners. Deprecated, use diagonalMovement instead. + * @param {DiagonalMovement} opt.diagonalMovement Allowed diagonal movement. + */ +function BiDijkstraFinder(opt) { + BiAStarFinder.call(this, opt); + this.heuristic = function(dx, dy) { + return 0; + }; +} + +BiDijkstraFinder.prototype = new BiAStarFinder(); +BiDijkstraFinder.prototype.constructor = BiDijkstraFinder; + +module.exports = BiDijkstraFinder; diff --git a/src/js/pathFinder/finders/BreadthFirstFinder.js b/src/js/pathFinder/finders/BreadthFirstFinder.js new file mode 100644 index 0000000..028093f --- /dev/null +++ b/src/js/pathFinder/finders/BreadthFirstFinder.js @@ -0,0 +1,77 @@ +var Util = require('../core/Util'); +var DiagonalMovement = require('../core/DiagonalMovement'); + +/** + * Breadth-First-Search path finder. + * @constructor + * @param {object} opt + * @param {boolean} opt.allowDiagonal Whether diagonal movement is allowed. Deprecated, use diagonalMovement instead. + * @param {boolean} opt.dontCrossCorners Disallow diagonal movement touching block corners. Deprecated, use diagonalMovement instead. + * @param {DiagonalMovement} opt.diagonalMovement Allowed diagonal movement. + */ +function BreadthFirstFinder(opt) { + opt = opt || {}; + this.allowDiagonal = opt.allowDiagonal; + this.dontCrossCorners = opt.dontCrossCorners; + this.diagonalMovement = opt.diagonalMovement; + + if (!this.diagonalMovement) { + if (!this.allowDiagonal) { + this.diagonalMovement = DiagonalMovement.Never; + } else { + if (this.dontCrossCorners) { + this.diagonalMovement = DiagonalMovement.OnlyWhenNoObstacles; + } else { + this.diagonalMovement = DiagonalMovement.IfAtMostOneObstacle; + } + } + } +} + +/** + * Find and return the the path. + * @return {Array.<[number, number]>} The path, including both start and + * end positions. + */ +BreadthFirstFinder.prototype.findPath = function(startX, startY, endX, endY, grid) { + var openList = [], + diagonalMovement = this.diagonalMovement, + startNode = grid.getNodeAt(startX, startY), + endNode = grid.getNodeAt(endX, endY), + neighbors, neighbor, node, i, l; + + // push the start pos into the queue + openList.push(startNode); + startNode.opened = true; + + // while the queue is not empty + while (openList.length) { + // take the front node from the queue + node = openList.shift(); + node.closed = true; + + // reached the end position + if (node === endNode) { + return Util.backtrace(endNode); + } + + neighbors = grid.getNeighbors(node, diagonalMovement); + for (i = 0, l = neighbors.length; i < l; ++i) { + neighbor = neighbors[i]; + + // skip this neighbor if it has been inspected before + if (neighbor.closed || neighbor.opened) { + continue; + } + + openList.push(neighbor); + neighbor.opened = true; + neighbor.parent = node; + } + } + + // fail to find the path + return []; +}; + +module.exports = BreadthFirstFinder; diff --git a/src/js/pathFinder/finders/DijkstraFinder.js b/src/js/pathFinder/finders/DijkstraFinder.js new file mode 100644 index 0000000..d29eeea --- /dev/null +++ b/src/js/pathFinder/finders/DijkstraFinder.js @@ -0,0 +1,22 @@ +var AStarFinder = require('./AStarFinder'); + +/** + * Dijkstra path-finder. + * @constructor + * @extends AStarFinder + * @param {object} opt + * @param {boolean} opt.allowDiagonal Whether diagonal movement is allowed. Deprecated, use diagonalMovement instead. + * @param {boolean} opt.dontCrossCorners Disallow diagonal movement touching block corners. Deprecated, use diagonalMovement instead. + * @param {DiagonalMovement} opt.diagonalMovement Allowed diagonal movement. + */ +function DijkstraFinder(opt) { + AStarFinder.call(this, opt); + this.heuristic = function(dx, dy) { + return 0; + }; +} + +DijkstraFinder.prototype = new AStarFinder(); +DijkstraFinder.prototype.constructor = DijkstraFinder; + +module.exports = DijkstraFinder; diff --git a/src/js/pathFinder/finders/IDAStarFinder.js b/src/js/pathFinder/finders/IDAStarFinder.js new file mode 100644 index 0000000..e40beaf --- /dev/null +++ b/src/js/pathFinder/finders/IDAStarFinder.js @@ -0,0 +1,208 @@ +var Util = require('../core/Util'); +var Heuristic = require('../core/Heuristic'); +var Node = require('../core/Node'); +var DiagonalMovement = require('../core/DiagonalMovement'); + +/** + * Iterative Deeping A Star (IDA*) path-finder. + * + * Recursion based on: + * http://www.apl.jhu.edu/~hall/AI-Programming/IDA-Star.html + * + * Path retracing based on: + * V. Nageshwara Rao, Vipin Kumar and K. Ramesh + * "A Parallel Implementation of Iterative-Deeping-A*", January 1987. + * ftp://ftp.cs.utexas.edu/.snapshot/hourly.1/pub/AI-Lab/tech-reports/UT-AI-TR-87-46.pdf + * + * @author Gerard Meier (www.gerardmeier.com) + * + * @constructor + * @param {object} opt + * @param {boolean} opt.allowDiagonal Whether diagonal movement is allowed. Deprecated, use diagonalMovement instead. + * @param {boolean} opt.dontCrossCorners Disallow diagonal movement touching block corners. Deprecated, use diagonalMovement instead. + * @param {DiagonalMovement} opt.diagonalMovement Allowed diagonal movement. + * @param {function} opt.heuristic Heuristic function to estimate the distance + * (defaults to manhattan). + * @param {integer} opt.weight Weight to apply to the heuristic to allow for suboptimal paths, + * in order to speed up the search. + * @param {object} opt.trackRecursion Whether to track recursion for statistical purposes. + * @param {object} opt.timeLimit Maximum execution time. Use <= 0 for infinite. + */ + +function IDAStarFinder(opt) { + opt = opt || {}; + this.allowDiagonal = opt.allowDiagonal; + this.dontCrossCorners = opt.dontCrossCorners; + this.diagonalMovement = opt.diagonalMovement; + this.heuristic = opt.heuristic || Heuristic.manhattan; + this.weight = opt.weight || 1; + this.trackRecursion = opt.trackRecursion || false; + this.timeLimit = opt.timeLimit || Infinity; // Default: no time limit. + + if (!this.diagonalMovement) { + if (!this.allowDiagonal) { + this.diagonalMovement = DiagonalMovement.Never; + } else { + if (this.dontCrossCorners) { + this.diagonalMovement = DiagonalMovement.OnlyWhenNoObstacles; + } else { + this.diagonalMovement = DiagonalMovement.IfAtMostOneObstacle; + } + } + } + + //When diagonal movement is allowed the manhattan heuristic is not admissible + //It should be octile instead + if (this.diagonalMovement === DiagonalMovement.Never) { + this.heuristic = opt.heuristic || Heuristic.manhattan; + } else { + this.heuristic = opt.heuristic || Heuristic.octile; + } +} + +/** + * Find and return the the path. When an empty array is returned, either + * no path is possible, or the maximum execution time is reached. + * + * @return {Array.<[number, number]>} The path, including both start and + * end positions. + */ +IDAStarFinder.prototype.findPath = function(startX, startY, endX, endY, grid) { + // Used for statistics: + var nodesVisited = 0; + + // Execution time limitation: + var startTime = new Date().getTime(); + + // Heuristic helper: + var h = function(a, b) { + return this.heuristic(Math.abs(b.x - a.x), Math.abs(b.y - a.y)); + }.bind(this); + + // Step cost from a to b: + var cost = function(a, b) { + return (a.x === b.x || a.y === b.y) ? 1 : Math.SQRT2; + }; + + /** + * IDA* search implementation. + * + * @param {Node} The node currently expanding from. + * @param {number} Cost to reach the given node. + * @param {number} Maximum search depth (cut-off value). + * @param {{Array.<[number, number]>}} The found route. + * @param {number} Recursion depth. + * + * @return {Object} either a number with the new optimal cut-off depth, + * or a valid node instance, in which case a path was found. + */ + var search = function(node, g, cutoff, route, depth) { + nodesVisited++; + + // Enforce timelimit: + if(this.timeLimit > 0 && new Date().getTime() - startTime > this.timeLimit * 1000) { + // Enforced as "path-not-found". + return Infinity; + } + + var f = g + h(node, end) * this.weight; + + // We've searched too deep for this iteration. + if(f > cutoff) { + return f; + } + + if(node == end) { + route[depth] = [node.x, node.y]; + return node; + } + + var min, t, k, neighbour; + + var neighbours = grid.getNeighbors(node, this.diagonalMovement); + + // Sort the neighbours, gives nicer paths. But, this deviates + // from the original algorithm - so I left it out. + //neighbours.sort(function(a, b){ + // return h(a, end) - h(b, end); + //}); + + + /*jshint -W084 *///Disable warning: Expected a conditional expression and instead saw an assignment + for(k = 0, min = Infinity; neighbour = neighbours[k]; ++k) { + /*jshint +W084 *///Enable warning: Expected a conditional expression and instead saw an assignment + if(this.trackRecursion) { + // Retain a copy for visualisation. Due to recursion, this + // node may be part of other paths too. + neighbour.retainCount = neighbour.retainCount + 1 || 1; + + if(neighbour.tested !== true) { + neighbour.tested = true; + } + } + + t = search(neighbour, g + cost(node, neighbour), cutoff, route, depth + 1); + + if(t instanceof Node) { + route[depth] = [node.x, node.y]; + + // For a typical A* linked list, this would work: + // neighbour.parent = node; + return t; + } + + // Decrement count, then determine whether it's actually closed. + if(this.trackRecursion && (--neighbour.retainCount) === 0) { + neighbour.tested = false; + } + + if(t < min) { + min = t; + } + } + + return min; + + }.bind(this); + + // Node instance lookups: + var start = grid.getNodeAt(startX, startY); + var end = grid.getNodeAt(endX, endY); + + // Initial search depth, given the typical heuristic contraints, + // there should be no cheaper route possible. + var cutOff = h(start, end); + + var j, route, t; + + // With an overflow protection. + for(j = 0; true; ++j) { + //console.log("Iteration: " + j + ", search cut-off value: " + cutOff + ", nodes visited thus far: " + nodesVisited + "."); + + route = []; + + // Search till cut-off depth: + t = search(start, 0, cutOff, route, 0); + + // Route not possible, or not found in time limit. + if(t === Infinity) { + return []; + } + + // If t is a node, it's also the end node. Route is now + // populated with a valid path to the end node. + if(t instanceof Node) { + //console.log("Finished at iteration: " + j + ", search cut-off value: " + cutOff + ", nodes visited: " + nodesVisited + "."); + return route; + } + + // Try again, this time with a deeper cut-off. The t score + // is the closest we got to the end node. + cutOff = t; + } + + // This _should_ never to be reached. + return []; +}; + +module.exports = IDAStarFinder; diff --git a/src/js/pathFinder/finders/JPFAlwaysMoveDiagonally.js b/src/js/pathFinder/finders/JPFAlwaysMoveDiagonally.js new file mode 100644 index 0000000..6a1621e --- /dev/null +++ b/src/js/pathFinder/finders/JPFAlwaysMoveDiagonally.js @@ -0,0 +1,149 @@ +/** + * @author imor / https://github.com/imor + */ +var JumpPointFinderBase = require('./JumpPointFinderBase'); +var DiagonalMovement = require('../core/DiagonalMovement'); + +/** + * Path finder using the Jump Point Search algorithm which always moves + * diagonally irrespective of the number of obstacles. + */ +function JPFAlwaysMoveDiagonally(opt) { + JumpPointFinderBase.call(this, opt); +} + +JPFAlwaysMoveDiagonally.prototype = new JumpPointFinderBase(); +JPFAlwaysMoveDiagonally.prototype.constructor = JPFAlwaysMoveDiagonally; + +/** + * Search recursively in the direction (parent -> child), stopping only when a + * jump point is found. + * @protected + * @return {Array.<[number, number]>} The x, y coordinate of the jump point + * found, or null if not found + */ +JPFAlwaysMoveDiagonally.prototype._jump = function(x, y, px, py) { + var grid = this.grid, + dx = x - px, dy = y - py; + + if (!grid.isWalkableAt(x, y)) { + return null; + } + + if(this.trackJumpRecursion === true) { + grid.getNodeAt(x, y).tested = true; + } + + if (grid.getNodeAt(x, y) === this.endNode) { + return [x, y]; + } + + // check for forced neighbors + // along the diagonal + if (dx !== 0 && dy !== 0) { + if ((grid.isWalkableAt(x - dx, y + dy) && !grid.isWalkableAt(x - dx, y)) || + (grid.isWalkableAt(x + dx, y - dy) && !grid.isWalkableAt(x, y - dy))) { + return [x, y]; + } + // when moving diagonally, must check for vertical/horizontal jump points + if (this._jump(x + dx, y, x, y) || this._jump(x, y + dy, x, y)) { + return [x, y]; + } + } + // horizontally/vertically + else { + if( dx !== 0 ) { // moving along x + if((grid.isWalkableAt(x + dx, y + 1) && !grid.isWalkableAt(x, y + 1)) || + (grid.isWalkableAt(x + dx, y - 1) && !grid.isWalkableAt(x, y - 1))) { + return [x, y]; + } + } + else { + if((grid.isWalkableAt(x + 1, y + dy) && !grid.isWalkableAt(x + 1, y)) || + (grid.isWalkableAt(x - 1, y + dy) && !grid.isWalkableAt(x - 1, y))) { + return [x, y]; + } + } + } + + return this._jump(x + dx, y + dy, x, y); +}; + +/** + * Find the neighbors for the given node. If the node has a parent, + * prune the neighbors based on the jump point search algorithm, otherwise + * return all available neighbors. + * @return {Array.<[number, number]>} The neighbors found. + */ +JPFAlwaysMoveDiagonally.prototype._findNeighbors = function(node) { + var parent = node.parent, + x = node.x, y = node.y, + grid = this.grid, + px, py, nx, ny, dx, dy, + neighbors = [], neighborNodes, neighborNode, i, l; + + // directed pruning: can ignore most neighbors, unless forced. + if (parent) { + px = parent.x; + py = parent.y; + // get the normalized direction of travel + dx = (x - px) / Math.max(Math.abs(x - px), 1); + dy = (y - py) / Math.max(Math.abs(y - py), 1); + + // search diagonally + if (dx !== 0 && dy !== 0) { + if (grid.isWalkableAt(x, y + dy)) { + neighbors.push([x, y + dy]); + } + if (grid.isWalkableAt(x + dx, y)) { + neighbors.push([x + dx, y]); + } + if (grid.isWalkableAt(x + dx, y + dy)) { + neighbors.push([x + dx, y + dy]); + } + if (!grid.isWalkableAt(x - dx, y)) { + neighbors.push([x - dx, y + dy]); + } + if (!grid.isWalkableAt(x, y - dy)) { + neighbors.push([x + dx, y - dy]); + } + } + // search horizontally/vertically + else { + if(dx === 0) { + if (grid.isWalkableAt(x, y + dy)) { + neighbors.push([x, y + dy]); + } + if (!grid.isWalkableAt(x + 1, y)) { + neighbors.push([x + 1, y + dy]); + } + if (!grid.isWalkableAt(x - 1, y)) { + neighbors.push([x - 1, y + dy]); + } + } + else { + if (grid.isWalkableAt(x + dx, y)) { + neighbors.push([x + dx, y]); + } + if (!grid.isWalkableAt(x, y + 1)) { + neighbors.push([x + dx, y + 1]); + } + if (!grid.isWalkableAt(x, y - 1)) { + neighbors.push([x + dx, y - 1]); + } + } + } + } + // return all neighbors + else { + neighborNodes = grid.getNeighbors(node, DiagonalMovement.Always); + for (i = 0, l = neighborNodes.length; i < l; ++i) { + neighborNode = neighborNodes[i]; + neighbors.push([neighborNode.x, neighborNode.y]); + } + } + + return neighbors; +}; + +module.exports = JPFAlwaysMoveDiagonally; diff --git a/src/js/pathFinder/finders/JPFMoveDiagonallyIfAtMostOneObstacle.js b/src/js/pathFinder/finders/JPFMoveDiagonallyIfAtMostOneObstacle.js new file mode 100644 index 0000000..a89adaa --- /dev/null +++ b/src/js/pathFinder/finders/JPFMoveDiagonallyIfAtMostOneObstacle.js @@ -0,0 +1,155 @@ +/** + * @author imor / https://github.com/imor + */ +var JumpPointFinderBase = require('./JumpPointFinderBase'); +var DiagonalMovement = require('../core/DiagonalMovement'); + +/** + * Path finder using the Jump Point Search algorithm which moves + * diagonally only when there is at most one obstacle. + */ +function JPFMoveDiagonallyIfAtMostOneObstacle(opt) { + JumpPointFinderBase.call(this, opt); +} + +JPFMoveDiagonallyIfAtMostOneObstacle.prototype = new JumpPointFinderBase(); +JPFMoveDiagonallyIfAtMostOneObstacle.prototype.constructor = JPFMoveDiagonallyIfAtMostOneObstacle; + +/** + * Search recursively in the direction (parent -> child), stopping only when a + * jump point is found. + * @protected + * @return {Array.<[number, number]>} The x, y coordinate of the jump point + * found, or null if not found + */ +JPFMoveDiagonallyIfAtMostOneObstacle.prototype._jump = function(x, y, px, py) { + var grid = this.grid, + dx = x - px, dy = y - py; + + if (!grid.isWalkableAt(x, y)) { + return null; + } + + if(this.trackJumpRecursion === true) { + grid.getNodeAt(x, y).tested = true; + } + + if (grid.getNodeAt(x, y) === this.endNode) { + return [x, y]; + } + + // check for forced neighbors + // along the diagonal + if (dx !== 0 && dy !== 0) { + if ((grid.isWalkableAt(x - dx, y + dy) && !grid.isWalkableAt(x - dx, y)) || + (grid.isWalkableAt(x + dx, y - dy) && !grid.isWalkableAt(x, y - dy))) { + return [x, y]; + } + // when moving diagonally, must check for vertical/horizontal jump points + if (this._jump(x + dx, y, x, y) || this._jump(x, y + dy, x, y)) { + return [x, y]; + } + } + // horizontally/vertically + else { + if( dx !== 0 ) { // moving along x + if((grid.isWalkableAt(x + dx, y + 1) && !grid.isWalkableAt(x, y + 1)) || + (grid.isWalkableAt(x + dx, y - 1) && !grid.isWalkableAt(x, y - 1))) { + return [x, y]; + } + } + else { + if((grid.isWalkableAt(x + 1, y + dy) && !grid.isWalkableAt(x + 1, y)) || + (grid.isWalkableAt(x - 1, y + dy) && !grid.isWalkableAt(x - 1, y))) { + return [x, y]; + } + } + } + + // moving diagonally, must make sure one of the vertical/horizontal + // neighbors is open to allow the path + if (grid.isWalkableAt(x + dx, y) || grid.isWalkableAt(x, y + dy)) { + return this._jump(x + dx, y + dy, x, y); + } else { + return null; + } +}; + +/** + * Find the neighbors for the given node. If the node has a parent, + * prune the neighbors based on the jump point search algorithm, otherwise + * return all available neighbors. + * @return {Array.<[number, number]>} The neighbors found. + */ +JPFMoveDiagonallyIfAtMostOneObstacle.prototype._findNeighbors = function(node) { + var parent = node.parent, + x = node.x, y = node.y, + grid = this.grid, + px, py, nx, ny, dx, dy, + neighbors = [], neighborNodes, neighborNode, i, l; + + // directed pruning: can ignore most neighbors, unless forced. + if (parent) { + px = parent.x; + py = parent.y; + // get the normalized direction of travel + dx = (x - px) / Math.max(Math.abs(x - px), 1); + dy = (y - py) / Math.max(Math.abs(y - py), 1); + + // search diagonally + if (dx !== 0 && dy !== 0) { + if (grid.isWalkableAt(x, y + dy)) { + neighbors.push([x, y + dy]); + } + if (grid.isWalkableAt(x + dx, y)) { + neighbors.push([x + dx, y]); + } + if (grid.isWalkableAt(x, y + dy) || grid.isWalkableAt(x + dx, y)) { + neighbors.push([x + dx, y + dy]); + } + if (!grid.isWalkableAt(x - dx, y) && grid.isWalkableAt(x, y + dy)) { + neighbors.push([x - dx, y + dy]); + } + if (!grid.isWalkableAt(x, y - dy) && grid.isWalkableAt(x + dx, y)) { + neighbors.push([x + dx, y - dy]); + } + } + // search horizontally/vertically + else { + if(dx === 0) { + if (grid.isWalkableAt(x, y + dy)) { + neighbors.push([x, y + dy]); + if (!grid.isWalkableAt(x + 1, y)) { + neighbors.push([x + 1, y + dy]); + } + if (!grid.isWalkableAt(x - 1, y)) { + neighbors.push([x - 1, y + dy]); + } + } + } + else { + if (grid.isWalkableAt(x + dx, y)) { + neighbors.push([x + dx, y]); + if (!grid.isWalkableAt(x, y + 1)) { + neighbors.push([x + dx, y + 1]); + } + if (!grid.isWalkableAt(x, y - 1)) { + neighbors.push([x + dx, y - 1]); + } + } + } + } + } + // return all neighbors + else { + neighborNodes = grid.getNeighbors(node, DiagonalMovement.IfAtMostOneObstacle); + for (i = 0, l = neighborNodes.length; i < l; ++i) { + neighborNode = neighborNodes[i]; + neighbors.push([neighborNode.x, neighborNode.y]); + } + } + + return neighbors; +}; + +module.exports = JPFMoveDiagonallyIfAtMostOneObstacle; diff --git a/src/js/pathFinder/finders/JPFMoveDiagonallyIfNoObstacles.js b/src/js/pathFinder/finders/JPFMoveDiagonallyIfNoObstacles.js new file mode 100644 index 0000000..b22360d --- /dev/null +++ b/src/js/pathFinder/finders/JPFMoveDiagonallyIfNoObstacles.js @@ -0,0 +1,174 @@ +/** + * @author imor / https://github.com/imor + */ +var JumpPointFinderBase = require('./JumpPointFinderBase'); +var DiagonalMovement = require('../core/DiagonalMovement'); + +/** + * Path finder using the Jump Point Search algorithm which moves + * diagonally only when there are no obstacles. + */ +function JPFMoveDiagonallyIfNoObstacles(opt) { + JumpPointFinderBase.call(this, opt); +} + +JPFMoveDiagonallyIfNoObstacles.prototype = new JumpPointFinderBase(); +JPFMoveDiagonallyIfNoObstacles.prototype.constructor = JPFMoveDiagonallyIfNoObstacles; + +/** + * Search recursively in the direction (parent -> child), stopping only when a + * jump point is found. + * @protected + * @return {Array.<[number, number]>} The x, y coordinate of the jump point + * found, or null if not found + */ +JPFMoveDiagonallyIfNoObstacles.prototype._jump = function(x, y, px, py) { + var grid = this.grid, + dx = x - px, dy = y - py; + + if (!grid.isWalkableAt(x, y)) { + return null; + } + + if(this.trackJumpRecursion === true) { + grid.getNodeAt(x, y).tested = true; + } + + if (grid.getNodeAt(x, y) === this.endNode) { + return [x, y]; + } + + // check for forced neighbors + // along the diagonal + if (dx !== 0 && dy !== 0) { + // if ((grid.isWalkableAt(x - dx, y + dy) && !grid.isWalkableAt(x - dx, y)) || + // (grid.isWalkableAt(x + dx, y - dy) && !grid.isWalkableAt(x, y - dy))) { + // return [x, y]; + // } + // when moving diagonally, must check for vertical/horizontal jump points + if (this._jump(x + dx, y, x, y) || this._jump(x, y + dy, x, y)) { + return [x, y]; + } + } + // horizontally/vertically + else { + if (dx !== 0) { + if ((grid.isWalkableAt(x, y - 1) && !grid.isWalkableAt(x - dx, y - 1)) || + (grid.isWalkableAt(x, y + 1) && !grid.isWalkableAt(x - dx, y + 1))) { + return [x, y]; + } + } + else if (dy !== 0) { + if ((grid.isWalkableAt(x - 1, y) && !grid.isWalkableAt(x - 1, y - dy)) || + (grid.isWalkableAt(x + 1, y) && !grid.isWalkableAt(x + 1, y - dy))) { + return [x, y]; + } + // When moving vertically, must check for horizontal jump points + // if (this._jump(x + 1, y, x, y) || this._jump(x - 1, y, x, y)) { + // return [x, y]; + // } + } + } + + // moving diagonally, must make sure one of the vertical/horizontal + // neighbors is open to allow the path + if (grid.isWalkableAt(x + dx, y) && grid.isWalkableAt(x, y + dy)) { + return this._jump(x + dx, y + dy, x, y); + } else { + return null; + } +}; + +/** + * Find the neighbors for the given node. If the node has a parent, + * prune the neighbors based on the jump point search algorithm, otherwise + * return all available neighbors. + * @return {Array.<[number, number]>} The neighbors found. + */ +JPFMoveDiagonallyIfNoObstacles.prototype._findNeighbors = function(node) { + var parent = node.parent, + x = node.x, y = node.y, + grid = this.grid, + px, py, nx, ny, dx, dy, + neighbors = [], neighborNodes, neighborNode, i, l; + + // directed pruning: can ignore most neighbors, unless forced. + if (parent) { + px = parent.x; + py = parent.y; + // get the normalized direction of travel + dx = (x - px) / Math.max(Math.abs(x - px), 1); + dy = (y - py) / Math.max(Math.abs(y - py), 1); + + // search diagonally + if (dx !== 0 && dy !== 0) { + if (grid.isWalkableAt(x, y + dy)) { + neighbors.push([x, y + dy]); + } + if (grid.isWalkableAt(x + dx, y)) { + neighbors.push([x + dx, y]); + } + if (grid.isWalkableAt(x, y + dy) && grid.isWalkableAt(x + dx, y)) { + neighbors.push([x + dx, y + dy]); + } + } + // search horizontally/vertically + else { + var isNextWalkable; + if (dx !== 0) { + isNextWalkable = grid.isWalkableAt(x + dx, y); + var isTopWalkable = grid.isWalkableAt(x, y + 1); + var isBottomWalkable = grid.isWalkableAt(x, y - 1); + + if (isNextWalkable) { + neighbors.push([x + dx, y]); + if (isTopWalkable) { + neighbors.push([x + dx, y + 1]); + } + if (isBottomWalkable) { + neighbors.push([x + dx, y - 1]); + } + } + if (isTopWalkable) { + neighbors.push([x, y + 1]); + } + if (isBottomWalkable) { + neighbors.push([x, y - 1]); + } + } + else if (dy !== 0) { + isNextWalkable = grid.isWalkableAt(x, y + dy); + var isRightWalkable = grid.isWalkableAt(x + 1, y); + var isLeftWalkable = grid.isWalkableAt(x - 1, y); + + if (isNextWalkable) { + neighbors.push([x, y + dy]); + if (isRightWalkable) { + neighbors.push([x + 1, y + dy]); + } + if (isLeftWalkable) { + neighbors.push([x - 1, y + dy]); + } + } + if (isRightWalkable) { + neighbors.push([x + 1, y]); + } + if (isLeftWalkable) { + neighbors.push([x - 1, y]); + } + } + } + } + // return all neighbors + else { + neighborNodes = grid.getNeighbors(node, DiagonalMovement.OnlyWhenNoObstacles); + for (i = 0, l = neighborNodes.length; i < l; ++i) { + neighborNode = neighborNodes[i]; + neighbors.push([neighborNode.x, neighborNode.y]); + } + } + + return neighbors; +}; + +module.exports = JPFMoveDiagonallyIfNoObstacles; diff --git a/src/js/pathFinder/finders/JPFNeverMoveDiagonally.js b/src/js/pathFinder/finders/JPFNeverMoveDiagonally.js new file mode 100644 index 0000000..a006556 --- /dev/null +++ b/src/js/pathFinder/finders/JPFNeverMoveDiagonally.js @@ -0,0 +1,120 @@ +/** + * @author imor / https://github.com/imor + */ +var JumpPointFinderBase = require('./JumpPointFinderBase'); +var DiagonalMovement = require('../core/DiagonalMovement'); + +/** + * Path finder using the Jump Point Search algorithm allowing only horizontal + * or vertical movements. + */ +function JPFNeverMoveDiagonally(opt) { + JumpPointFinderBase.call(this, opt); +} + +JPFNeverMoveDiagonally.prototype = new JumpPointFinderBase(); +JPFNeverMoveDiagonally.prototype.constructor = JPFNeverMoveDiagonally; + +/** + * Search recursively in the direction (parent -> child), stopping only when a + * jump point is found. + * @protected + * @return {Array.<[number, number]>} The x, y coordinate of the jump point + * found, or null if not found + */ +JPFNeverMoveDiagonally.prototype._jump = function(x, y, px, py) { + var grid = this.grid, + dx = x - px, dy = y - py; + + if (!grid.isWalkableAt(x, y)) { + return null; + } + + if(this.trackJumpRecursion === true) { + grid.getNodeAt(x, y).tested = true; + } + + if (grid.getNodeAt(x, y) === this.endNode) { + return [x, y]; + } + + if (dx !== 0) { + if ((grid.isWalkableAt(x, y - 1) && !grid.isWalkableAt(x - dx, y - 1)) || + (grid.isWalkableAt(x, y + 1) && !grid.isWalkableAt(x - dx, y + 1))) { + return [x, y]; + } + } + else if (dy !== 0) { + if ((grid.isWalkableAt(x - 1, y) && !grid.isWalkableAt(x - 1, y - dy)) || + (grid.isWalkableAt(x + 1, y) && !grid.isWalkableAt(x + 1, y - dy))) { + return [x, y]; + } + //When moving vertically, must check for horizontal jump points + if (this._jump(x + 1, y, x, y) || this._jump(x - 1, y, x, y)) { + return [x, y]; + } + } + else { + throw new Error("Only horizontal and vertical movements are allowed"); + } + + return this._jump(x + dx, y + dy, x, y); +}; + +/** + * Find the neighbors for the given node. If the node has a parent, + * prune the neighbors based on the jump point search algorithm, otherwise + * return all available neighbors. + * @return {Array.<[number, number]>} The neighbors found. + */ +JPFNeverMoveDiagonally.prototype._findNeighbors = function(node) { + var parent = node.parent, + x = node.x, y = node.y, + grid = this.grid, + px, py, nx, ny, dx, dy, + neighbors = [], neighborNodes, neighborNode, i, l; + + // directed pruning: can ignore most neighbors, unless forced. + if (parent) { + px = parent.x; + py = parent.y; + // get the normalized direction of travel + dx = (x - px) / Math.max(Math.abs(x - px), 1); + dy = (y - py) / Math.max(Math.abs(y - py), 1); + + if (dx !== 0) { + if (grid.isWalkableAt(x, y - 1)) { + neighbors.push([x, y - 1]); + } + if (grid.isWalkableAt(x, y + 1)) { + neighbors.push([x, y + 1]); + } + if (grid.isWalkableAt(x + dx, y)) { + neighbors.push([x + dx, y]); + } + } + else if (dy !== 0) { + if (grid.isWalkableAt(x - 1, y)) { + neighbors.push([x - 1, y]); + } + if (grid.isWalkableAt(x + 1, y)) { + neighbors.push([x + 1, y]); + } + if (grid.isWalkableAt(x, y + dy)) { + neighbors.push([x, y + dy]); + } + } + } + // return all neighbors + else { + neighborNodes = grid.getNeighbors(node, DiagonalMovement.Never); + for (i = 0, l = neighborNodes.length; i < l; ++i) { + neighborNode = neighborNodes[i]; + neighbors.push([neighborNode.x, neighborNode.y]); + } + } + + return neighbors; +}; + +module.exports = JPFNeverMoveDiagonally; diff --git a/src/js/pathFinder/finders/JumpPointFinder.js b/src/js/pathFinder/finders/JumpPointFinder.js new file mode 100644 index 0000000..24ab1d7 --- /dev/null +++ b/src/js/pathFinder/finders/JumpPointFinder.js @@ -0,0 +1,31 @@ +/** + * @author aniero / https://github.com/aniero + */ +var DiagonalMovement = require('../core/DiagonalMovement'); +var JPFNeverMoveDiagonally = require('./JPFNeverMoveDiagonally'); +var JPFAlwaysMoveDiagonally = require('./JPFAlwaysMoveDiagonally'); +var JPFMoveDiagonallyIfNoObstacles = require('./JPFMoveDiagonallyIfNoObstacles'); +var JPFMoveDiagonallyIfAtMostOneObstacle = require('./JPFMoveDiagonallyIfAtMostOneObstacle'); + +/** + * Path finder using the Jump Point Search algorithm + * @param {object} opt + * @param {function} opt.heuristic Heuristic function to estimate the distance + * (defaults to manhattan). + * @param {DiagonalMovement} opt.diagonalMovement Condition under which diagonal + * movement will be allowed. + */ +function JumpPointFinder(opt) { + opt = opt || {}; + if (opt.diagonalMovement === DiagonalMovement.Never) { + return new JPFNeverMoveDiagonally(opt); + } else if (opt.diagonalMovement === DiagonalMovement.Always) { + return new JPFAlwaysMoveDiagonally(opt); + } else if (opt.diagonalMovement === DiagonalMovement.OnlyWhenNoObstacles) { + return new JPFMoveDiagonallyIfNoObstacles(opt); + } else { + return new JPFMoveDiagonallyIfAtMostOneObstacle(opt); + } +} + +module.exports = JumpPointFinder; diff --git a/src/js/pathFinder/finders/JumpPointFinderBase.js b/src/js/pathFinder/finders/JumpPointFinderBase.js new file mode 100644 index 0000000..18396ed --- /dev/null +++ b/src/js/pathFinder/finders/JumpPointFinderBase.js @@ -0,0 +1,114 @@ +/** + * @author imor / https://github.com/imor + */ +var Heap = require('heap'); +var Util = require('../core/Util'); +var Heuristic = require('../core/Heuristic'); +var DiagonalMovement = require('../core/DiagonalMovement'); + +/** + * Base class for the Jump Point Search algorithm + * @param {object} opt + * @param {function} opt.heuristic Heuristic function to estimate the distance + * (defaults to manhattan). + */ +function JumpPointFinderBase(opt) { + opt = opt || {}; + this.heuristic = opt.heuristic || Heuristic.manhattan; + this.trackJumpRecursion = opt.trackJumpRecursion || false; +} + +/** + * Find and return the path. + * @return {Array.<[number, number]>} The path, including both start and + * end positions. + */ +JumpPointFinderBase.prototype.findPath = function(startX, startY, endX, endY, grid) { + var openList = this.openList = new Heap(function(nodeA, nodeB) { + return nodeA.f - nodeB.f; + }), + startNode = this.startNode = grid.getNodeAt(startX, startY), + endNode = this.endNode = grid.getNodeAt(endX, endY), node; + + this.grid = grid; + + + // set the `g` and `f` value of the start node to be 0 + startNode.g = 0; + startNode.f = 0; + + // push the start node into the open list + openList.push(startNode); + startNode.opened = true; + + // while the open list is not empty + while (!openList.empty()) { + // pop the position of node which has the minimum `f` value. + node = openList.pop(); + node.closed = true; + + if (node === endNode) { + return Util.expandPath(Util.backtrace(endNode)); + } + + this._identifySuccessors(node); + } + + // fail to find the path + return []; +}; + +/** + * Identify successors for the given node. Runs a jump point search in the + * direction of each available neighbor, adding any points found to the open + * list. + * @protected + */ +JumpPointFinderBase.prototype._identifySuccessors = function(node) { + var grid = this.grid, + heuristic = this.heuristic, + openList = this.openList, + endX = this.endNode.x, + endY = this.endNode.y, + neighbors, neighbor, + jumpPoint, i, l, + x = node.x, y = node.y, + jx, jy, dx, dy, d, ng, jumpNode, + abs = Math.abs, max = Math.max; + + neighbors = this._findNeighbors(node); + for(i = 0, l = neighbors.length; i < l; ++i) { + neighbor = neighbors[i]; + jumpPoint = this._jump(neighbor[0], neighbor[1], x, y); + if (jumpPoint) { + + jx = jumpPoint[0]; + jy = jumpPoint[1]; + jumpNode = grid.getNodeAt(jx, jy); + + if (jumpNode.closed) { + continue; + } + + // include distance, as parent may not be immediately adjacent: + d = Heuristic.octile(abs(jx - x), abs(jy - y)); + ng = node.g + d; // next `g` value + + if (!jumpNode.opened || ng < jumpNode.g) { + jumpNode.g = ng; + jumpNode.h = jumpNode.h || heuristic(abs(jx - endX), abs(jy - endY)); + jumpNode.f = jumpNode.g + jumpNode.h; + jumpNode.parent = node; + + if (!jumpNode.opened) { + openList.push(jumpNode); + jumpNode.opened = true; + } else { + openList.updateItem(jumpNode); + } + } + } + } +}; + +module.exports = JumpPointFinderBase;