123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589 |
- import bbox from '@turf/bbox';
- import booleanPointInPolygon from '@turf/boolean-point-in-polygon';
- import distance from '@turf/distance';
- import scale from '@turf/transform-scale';
- import cleanCoords from '@turf/clean-coords';
- import bboxPolygon from '@turf/bbox-polygon';
- import { getCoord, getType, getGeom } from '@turf/invariant';
- import { isObject, featureCollection, isNumber, point, feature, lineString } from '@turf/helpers';
- // 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 pathTo(node) {
- var curr = node,
- path = [];
- while (curr.parent) {
- path.unshift(curr);
- curr = curr.parent;
- }
- return path;
- }
- function getHeap() {
- return new BinaryHeap(function (node) {
- return node.f;
- });
- }
- /**
- * Astar
- * @private
- */
- var astar = {
- /**
- * Perform an A* Search on a graph given a start and end node.
- *
- * @private
- * @memberof astar
- * @param {Graph} graph Graph
- * @param {GridNode} start Start
- * @param {GridNode} end End
- * @param {Object} [options] 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).
- * @returns {Object} Search
- */
- search: function (graph, start, end, options) {
- graph.cleanDirty();
- options = options || {};
- var heuristic = options.heuristic || astar.heuristics.manhattan,
- closest = options.closest || false;
- var openHeap = getHeap(),
- closestNode = start; // set the start node to be the closest if required
- start.h = heuristic(start, end);
- openHeap.push(start);
- while (openHeap.size() > 0) {
- // Grab the lowest f(x) to process next. Heap keeps this sorted for us.
- var currentNode = openHeap.pop();
- // 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.
- var neighbors = graph.neighbors(currentNode);
- for (var i = 0, il = neighbors.length; i < il; ++i) {
- var 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.
- var gScore = currentNode.g + neighbor.getCost(currentNode),
- 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);
- 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) {
- var d1 = Math.abs(pos1.x - pos0.x);
- var d2 = Math.abs(pos1.y - pos0.y);
- return d1 + d2;
- },
- diagonal: function (pos0, pos1) {
- var D = 1;
- var D2 = Math.sqrt(2);
- var d1 = Math.abs(pos1.x - pos0.x);
- var d2 = Math.abs(pos1.y - pos0.y);
- return D * (d1 + d2) + (D2 - 2 * D) * Math.min(d1, d2);
- },
- },
- cleanNode: function (node) {
- node.f = 0;
- node.g = 0;
- node.h = 0;
- node.visited = false;
- node.closed = false;
- node.parent = null;
- },
- };
- /**
- * A graph memory structure
- *
- * @private
- * @param {Array} gridIn 2D array of input weights
- * @param {Object} [options] Options
- * @param {boolean} [options.diagonal] Specifies whether diagonal moves are allowed
- * @returns {void} Graph
- */
- function Graph(gridIn, options) {
- options = options || {};
- this.nodes = [];
- this.diagonal = !!options.diagonal;
- this.grid = [];
- for (var x = 0; x < gridIn.length; x++) {
- this.grid[x] = [];
- for (var y = 0, row = gridIn[x]; y < row.length; y++) {
- var node = new GridNode(x, y, row[y]);
- this.grid[x][y] = node;
- this.nodes.push(node);
- }
- }
- this.init();
- }
- Graph.prototype.init = function () {
- this.dirtyNodes = [];
- for (var i = 0; i < this.nodes.length; i++) {
- astar.cleanNode(this.nodes[i]);
- }
- };
- Graph.prototype.cleanDirty = function () {
- for (var 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) {
- var ret = [],
- x = node.x,
- y = node.y,
- 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 () {
- var graphString = [],
- nodes = this.grid, // when using grid
- rowDebug,
- row,
- y,
- l;
- for (var x = 0, len = nodes.length; x < len; x++) {
- rowDebug = [];
- row = nodes[x];
- for (y = 0, l = row.length; y < l; 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.
- var result = this.content[0];
- // Get the element at the end of the array.
- var 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) {
- var i = this.content.indexOf(node);
- // When it is found, the process seen in 'pop' is repeated
- // to fill up the hole.
- var 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.
- var 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.
- var parentN = ((n + 1) >> 1) - 1,
- 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.
- var length = this.content.length,
- element = this.content[n],
- elemScore = this.scoreFunction(element);
- while (true) {
- // Compute the indices of the child elements.
- var child2N = (n + 1) << 1,
- child1N = child2N - 1;
- // This is used to store the new position of the element, if any.
- var swap = null,
- child1Score;
- // If the first child exists (is inside the array)...
- if (child1N < length) {
- // Look it up and compute its score.
- var 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) {
- var child2 = this.content[child2N],
- 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;
- }
- }
- },
- };
- /**
- * Returns the shortest {@link LineString|path} from {@link Point|start} to {@link Point|end} without colliding with
- * any {@link Feature} in {@link FeatureCollection<Polygon>| obstacles}
- *
- * @name shortestPath
- * @param {Coord} start point
- * @param {Coord} end point
- * @param {Object} [options={}] optional parameters
- * @param {Geometry|Feature|FeatureCollection<Polygon>} [options.obstacles] areas which path cannot travel
- * @param {number} [options.minDistance] minimum distance between shortest path and obstacles
- * @param {string} [options.units='kilometers'] unit in which resolution & minimum distance will be expressed in; it can be degrees, radians, miles, kilometers, ...
- * @param {number} [options.resolution=100] distance between matrix points on which the path will be calculated
- * @returns {Feature<LineString>} shortest path between start and end
- * @example
- * var start = [-5, -6];
- * var end = [9, -6];
- * var options = {
- * obstacles: turf.polygon([[[0, -7], [5, -7], [5, -3], [0, -3], [0, -7]]])
- * };
- *
- * var path = turf.shortestPath(start, end, options);
- *
- * //addToMap
- * var addToMap = [start, end, options.obstacles, path];
- */
- function shortestPath(start, end, options) {
- // Optional parameters
- options = options || {};
- if (!isObject(options)) throw new Error("options is invalid");
- var resolution = options.resolution;
- var minDistance = options.minDistance;
- var obstacles = options.obstacles || featureCollection([]);
- // validation
- if (!start) throw new Error("start is required");
- if (!end) throw new Error("end is required");
- if ((resolution && !isNumber(resolution)) || resolution <= 0)
- throw new Error("options.resolution must be a number, greater than 0");
- if (minDistance)
- throw new Error("options.minDistance is not yet implemented");
- // Normalize Inputs
- var startCoord = getCoord(start);
- var endCoord = getCoord(end);
- start = point(startCoord);
- end = point(endCoord);
- // Handle obstacles
- switch (getType(obstacles)) {
- case "FeatureCollection":
- if (obstacles.features.length === 0)
- return lineString([startCoord, endCoord]);
- break;
- case "Polygon":
- obstacles = featureCollection([feature(getGeom(obstacles))]);
- break;
- default:
- throw new Error("invalid obstacles");
- }
- // define path grid area
- var collection = obstacles;
- collection.features.push(start);
- collection.features.push(end);
- var box = bbox(scale(bboxPolygon(bbox(collection)), 1.15)); // extend 15%
- if (!resolution) {
- var width = distance([box[0], box[1]], [box[2], box[1]], options);
- resolution = width / 100;
- }
- collection.features.pop();
- collection.features.pop();
- var west = box[0];
- var south = box[1];
- var east = box[2];
- var north = box[3];
- var xFraction = resolution / distance([west, south], [east, south], options);
- var cellWidth = xFraction * (east - west);
- var yFraction = resolution / distance([west, south], [west, north], options);
- var cellHeight = yFraction * (north - south);
- var bboxHorizontalSide = east - west;
- var bboxVerticalSide = north - south;
- var columns = Math.floor(bboxHorizontalSide / cellWidth);
- var rows = Math.floor(bboxVerticalSide / cellHeight);
- // adjust origin of the grid
- var deltaX = (bboxHorizontalSide - columns * cellWidth) / 2;
- var deltaY = (bboxVerticalSide - rows * cellHeight) / 2;
- // loop through points only once to speed up process
- // define matrix grid for A-star algorithm
- var pointMatrix = [];
- var matrix = [];
- var closestToStart = [];
- var closestToEnd = [];
- var minDistStart = Infinity;
- var minDistEnd = Infinity;
- var currentY = north - deltaY;
- var r = 0;
- while (currentY >= south) {
- // var currentY = south + deltaY;
- var matrixRow = [];
- var pointMatrixRow = [];
- var currentX = west + deltaX;
- var c = 0;
- while (currentX <= east) {
- var pt = point([currentX, currentY]);
- var isInsideObstacle = isInside(pt, obstacles);
- // feed obstacles matrix
- matrixRow.push(isInsideObstacle ? 0 : 1); // with javascript-astar
- // matrixRow.push(isInsideObstacle ? 1 : 0); // with astar-andrea
- // map point's coords
- pointMatrixRow.push(currentX + "|" + currentY);
- // set closest points
- var distStart = distance(pt, start);
- // if (distStart < minDistStart) {
- if (!isInsideObstacle && distStart < minDistStart) {
- minDistStart = distStart;
- closestToStart = { x: c, y: r };
- }
- var distEnd = distance(pt, end);
- // if (distEnd < minDistEnd) {
- if (!isInsideObstacle && distEnd < minDistEnd) {
- minDistEnd = distEnd;
- closestToEnd = { x: c, y: r };
- }
- currentX += cellWidth;
- c++;
- }
- matrix.push(matrixRow);
- pointMatrix.push(pointMatrixRow);
- currentY -= cellHeight;
- r++;
- }
- // find path on matrix grid
- // javascript-astar ----------------------
- var graph = new Graph(matrix, { diagonal: true });
- var startOnMatrix = graph.grid[closestToStart.y][closestToStart.x];
- var endOnMatrix = graph.grid[closestToEnd.y][closestToEnd.x];
- var result = astar.search(graph, startOnMatrix, endOnMatrix);
- var path = [startCoord];
- result.forEach(function (coord) {
- var coords = pointMatrix[coord.x][coord.y].split("|");
- path.push([+coords[0], +coords[1]]); // make sure coords are numbers
- });
- path.push(endCoord);
- // ---------------------------------------
- // astar-andrea ------------------------
- // var result = aStar(matrix, [closestToStart.x, closestToStart.y], [closestToEnd.x, closestToEnd.y], 'DiagonalFree');
- // var path = [start.geometry.coordinates];
- // result.forEach(function (coord) {
- // var coords = pointMatrix[coord[1]][coord[0]].split('|');
- // path.push([+coords[0], +coords[1]]); // make sure coords are numbers
- // });
- // path.push(end.geometry.coordinates);
- // ---------------------------------------
- return cleanCoords(lineString(path));
- }
- /**
- * Checks if Point is inside any of the Polygons
- *
- * @private
- * @param {Feature<Point>} pt to check
- * @param {FeatureCollection<Polygon>} polygons features
- * @returns {boolean} if inside or not
- */
- function isInside(pt, polygons) {
- for (var i = 0; i < polygons.features.length; i++) {
- if (booleanPointInPolygon(pt, polygons.features[i])) {
- return true;
- }
- }
- return false;
- }
- export default shortestPath;
|