index.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589
  1. import bbox from '@turf/bbox';
  2. import booleanPointInPolygon from '@turf/boolean-point-in-polygon';
  3. import distance from '@turf/distance';
  4. import scale from '@turf/transform-scale';
  5. import cleanCoords from '@turf/clean-coords';
  6. import bboxPolygon from '@turf/bbox-polygon';
  7. import { getCoord, getType, getGeom } from '@turf/invariant';
  8. import { isObject, featureCollection, isNumber, point, feature, lineString } from '@turf/helpers';
  9. // javascript-astar 0.4.1
  10. // http://github.com/bgrins/javascript-astar
  11. // Freely distributable under the MIT License.
  12. // Implements the astar search algorithm in javascript using a Binary Heap.
  13. // Includes Binary Heap (with modifications) from Marijn Haverbeke.
  14. // http://eloquentjavascript.net/appendix2.html
  15. function pathTo(node) {
  16. var curr = node,
  17. path = [];
  18. while (curr.parent) {
  19. path.unshift(curr);
  20. curr = curr.parent;
  21. }
  22. return path;
  23. }
  24. function getHeap() {
  25. return new BinaryHeap(function (node) {
  26. return node.f;
  27. });
  28. }
  29. /**
  30. * Astar
  31. * @private
  32. */
  33. var astar = {
  34. /**
  35. * Perform an A* Search on a graph given a start and end node.
  36. *
  37. * @private
  38. * @memberof astar
  39. * @param {Graph} graph Graph
  40. * @param {GridNode} start Start
  41. * @param {GridNode} end End
  42. * @param {Object} [options] Options
  43. * @param {bool} [options.closest] Specifies whether to return the path to the closest node if the target is unreachable.
  44. * @param {Function} [options.heuristic] Heuristic function (see astar.heuristics).
  45. * @returns {Object} Search
  46. */
  47. search: function (graph, start, end, options) {
  48. graph.cleanDirty();
  49. options = options || {};
  50. var heuristic = options.heuristic || astar.heuristics.manhattan,
  51. closest = options.closest || false;
  52. var openHeap = getHeap(),
  53. closestNode = start; // set the start node to be the closest if required
  54. start.h = heuristic(start, end);
  55. openHeap.push(start);
  56. while (openHeap.size() > 0) {
  57. // Grab the lowest f(x) to process next. Heap keeps this sorted for us.
  58. var currentNode = openHeap.pop();
  59. // End case -- result has been found, return the traced path.
  60. if (currentNode === end) {
  61. return pathTo(currentNode);
  62. }
  63. // Normal case -- move currentNode from open to closed, process each of its neighbors.
  64. currentNode.closed = true;
  65. // Find all neighbors for the current node.
  66. var neighbors = graph.neighbors(currentNode);
  67. for (var i = 0, il = neighbors.length; i < il; ++i) {
  68. var neighbor = neighbors[i];
  69. if (neighbor.closed || neighbor.isWall()) {
  70. // Not a valid node to process, skip to next neighbor.
  71. continue;
  72. }
  73. // The g score is the shortest distance from start to current node.
  74. // We need to check if the path we have arrived at this neighbor is the shortest one we have seen yet.
  75. var gScore = currentNode.g + neighbor.getCost(currentNode),
  76. beenVisited = neighbor.visited;
  77. if (!beenVisited || gScore < neighbor.g) {
  78. // Found an optimal (so far) path to this node. Take score for node to see how good it is.
  79. neighbor.visited = true;
  80. neighbor.parent = currentNode;
  81. neighbor.h = neighbor.h || heuristic(neighbor, end);
  82. neighbor.g = gScore;
  83. neighbor.f = neighbor.g + neighbor.h;
  84. graph.markDirty(neighbor);
  85. if (closest) {
  86. // If the neighbour is closer than the current closestNode or if it's equally close but has
  87. // a cheaper path than the current closest node then it becomes the closest node
  88. if (
  89. neighbor.h < closestNode.h ||
  90. (neighbor.h === closestNode.h && neighbor.g < closestNode.g)
  91. ) {
  92. closestNode = neighbor;
  93. }
  94. }
  95. if (!beenVisited) {
  96. // Pushing to heap will put it in proper place based on the 'f' value.
  97. openHeap.push(neighbor);
  98. } else {
  99. // Already seen the node, but since it has been rescored we need to reorder it in the heap
  100. openHeap.rescoreElement(neighbor);
  101. }
  102. }
  103. }
  104. }
  105. if (closest) {
  106. return pathTo(closestNode);
  107. }
  108. // No result was found - empty array signifies failure to find path.
  109. return [];
  110. },
  111. // See list of heuristics: http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html
  112. heuristics: {
  113. manhattan: function (pos0, pos1) {
  114. var d1 = Math.abs(pos1.x - pos0.x);
  115. var d2 = Math.abs(pos1.y - pos0.y);
  116. return d1 + d2;
  117. },
  118. diagonal: function (pos0, pos1) {
  119. var D = 1;
  120. var D2 = Math.sqrt(2);
  121. var d1 = Math.abs(pos1.x - pos0.x);
  122. var d2 = Math.abs(pos1.y - pos0.y);
  123. return D * (d1 + d2) + (D2 - 2 * D) * Math.min(d1, d2);
  124. },
  125. },
  126. cleanNode: function (node) {
  127. node.f = 0;
  128. node.g = 0;
  129. node.h = 0;
  130. node.visited = false;
  131. node.closed = false;
  132. node.parent = null;
  133. },
  134. };
  135. /**
  136. * A graph memory structure
  137. *
  138. * @private
  139. * @param {Array} gridIn 2D array of input weights
  140. * @param {Object} [options] Options
  141. * @param {boolean} [options.diagonal] Specifies whether diagonal moves are allowed
  142. * @returns {void} Graph
  143. */
  144. function Graph(gridIn, options) {
  145. options = options || {};
  146. this.nodes = [];
  147. this.diagonal = !!options.diagonal;
  148. this.grid = [];
  149. for (var x = 0; x < gridIn.length; x++) {
  150. this.grid[x] = [];
  151. for (var y = 0, row = gridIn[x]; y < row.length; y++) {
  152. var node = new GridNode(x, y, row[y]);
  153. this.grid[x][y] = node;
  154. this.nodes.push(node);
  155. }
  156. }
  157. this.init();
  158. }
  159. Graph.prototype.init = function () {
  160. this.dirtyNodes = [];
  161. for (var i = 0; i < this.nodes.length; i++) {
  162. astar.cleanNode(this.nodes[i]);
  163. }
  164. };
  165. Graph.prototype.cleanDirty = function () {
  166. for (var i = 0; i < this.dirtyNodes.length; i++) {
  167. astar.cleanNode(this.dirtyNodes[i]);
  168. }
  169. this.dirtyNodes = [];
  170. };
  171. Graph.prototype.markDirty = function (node) {
  172. this.dirtyNodes.push(node);
  173. };
  174. Graph.prototype.neighbors = function (node) {
  175. var ret = [],
  176. x = node.x,
  177. y = node.y,
  178. grid = this.grid;
  179. // West
  180. if (grid[x - 1] && grid[x - 1][y]) {
  181. ret.push(grid[x - 1][y]);
  182. }
  183. // East
  184. if (grid[x + 1] && grid[x + 1][y]) {
  185. ret.push(grid[x + 1][y]);
  186. }
  187. // South
  188. if (grid[x] && grid[x][y - 1]) {
  189. ret.push(grid[x][y - 1]);
  190. }
  191. // North
  192. if (grid[x] && grid[x][y + 1]) {
  193. ret.push(grid[x][y + 1]);
  194. }
  195. if (this.diagonal) {
  196. // Southwest
  197. if (grid[x - 1] && grid[x - 1][y - 1]) {
  198. ret.push(grid[x - 1][y - 1]);
  199. }
  200. // Southeast
  201. if (grid[x + 1] && grid[x + 1][y - 1]) {
  202. ret.push(grid[x + 1][y - 1]);
  203. }
  204. // Northwest
  205. if (grid[x - 1] && grid[x - 1][y + 1]) {
  206. ret.push(grid[x - 1][y + 1]);
  207. }
  208. // Northeast
  209. if (grid[x + 1] && grid[x + 1][y + 1]) {
  210. ret.push(grid[x + 1][y + 1]);
  211. }
  212. }
  213. return ret;
  214. };
  215. Graph.prototype.toString = function () {
  216. var graphString = [],
  217. nodes = this.grid, // when using grid
  218. rowDebug,
  219. row,
  220. y,
  221. l;
  222. for (var x = 0, len = nodes.length; x < len; x++) {
  223. rowDebug = [];
  224. row = nodes[x];
  225. for (y = 0, l = row.length; y < l; y++) {
  226. rowDebug.push(row[y].weight);
  227. }
  228. graphString.push(rowDebug.join(" "));
  229. }
  230. return graphString.join("\n");
  231. };
  232. function GridNode(x, y, weight) {
  233. this.x = x;
  234. this.y = y;
  235. this.weight = weight;
  236. }
  237. GridNode.prototype.toString = function () {
  238. return "[" + this.x + " " + this.y + "]";
  239. };
  240. GridNode.prototype.getCost = function (fromNeighbor) {
  241. // Take diagonal weight into consideration.
  242. if (fromNeighbor && fromNeighbor.x !== this.x && fromNeighbor.y !== this.y) {
  243. return this.weight * 1.41421;
  244. }
  245. return this.weight;
  246. };
  247. GridNode.prototype.isWall = function () {
  248. return this.weight === 0;
  249. };
  250. function BinaryHeap(scoreFunction) {
  251. this.content = [];
  252. this.scoreFunction = scoreFunction;
  253. }
  254. BinaryHeap.prototype = {
  255. push: function (element) {
  256. // Add the new element to the end of the array.
  257. this.content.push(element);
  258. // Allow it to sink down.
  259. this.sinkDown(this.content.length - 1);
  260. },
  261. pop: function () {
  262. // Store the first element so we can return it later.
  263. var result = this.content[0];
  264. // Get the element at the end of the array.
  265. var end = this.content.pop();
  266. // If there are any elements left, put the end element at the
  267. // start, and let it bubble up.
  268. if (this.content.length > 0) {
  269. this.content[0] = end;
  270. this.bubbleUp(0);
  271. }
  272. return result;
  273. },
  274. remove: function (node) {
  275. var i = this.content.indexOf(node);
  276. // When it is found, the process seen in 'pop' is repeated
  277. // to fill up the hole.
  278. var end = this.content.pop();
  279. if (i !== this.content.length - 1) {
  280. this.content[i] = end;
  281. if (this.scoreFunction(end) < this.scoreFunction(node)) {
  282. this.sinkDown(i);
  283. } else {
  284. this.bubbleUp(i);
  285. }
  286. }
  287. },
  288. size: function () {
  289. return this.content.length;
  290. },
  291. rescoreElement: function (node) {
  292. this.sinkDown(this.content.indexOf(node));
  293. },
  294. sinkDown: function (n) {
  295. // Fetch the element that has to be sunk.
  296. var element = this.content[n];
  297. // When at 0, an element can not sink any further.
  298. while (n > 0) {
  299. // Compute the parent element's index, and fetch it.
  300. var parentN = ((n + 1) >> 1) - 1,
  301. parent = this.content[parentN];
  302. // Swap the elements if the parent is greater.
  303. if (this.scoreFunction(element) < this.scoreFunction(parent)) {
  304. this.content[parentN] = element;
  305. this.content[n] = parent;
  306. // Update 'n' to continue at the new position.
  307. n = parentN;
  308. // Found a parent that is less, no need to sink any further.
  309. } else {
  310. break;
  311. }
  312. }
  313. },
  314. bubbleUp: function (n) {
  315. // Look up the target element and its score.
  316. var length = this.content.length,
  317. element = this.content[n],
  318. elemScore = this.scoreFunction(element);
  319. while (true) {
  320. // Compute the indices of the child elements.
  321. var child2N = (n + 1) << 1,
  322. child1N = child2N - 1;
  323. // This is used to store the new position of the element, if any.
  324. var swap = null,
  325. child1Score;
  326. // If the first child exists (is inside the array)...
  327. if (child1N < length) {
  328. // Look it up and compute its score.
  329. var child1 = this.content[child1N];
  330. child1Score = this.scoreFunction(child1);
  331. // If the score is less than our element's, we need to swap.
  332. if (child1Score < elemScore) {
  333. swap = child1N;
  334. }
  335. }
  336. // Do the same checks for the other child.
  337. if (child2N < length) {
  338. var child2 = this.content[child2N],
  339. child2Score = this.scoreFunction(child2);
  340. if (child2Score < (swap === null ? elemScore : child1Score)) {
  341. swap = child2N;
  342. }
  343. }
  344. // If the element needs to be moved, swap it, and continue.
  345. if (swap !== null) {
  346. this.content[n] = this.content[swap];
  347. this.content[swap] = element;
  348. n = swap;
  349. // Otherwise, we are done.
  350. } else {
  351. break;
  352. }
  353. }
  354. },
  355. };
  356. /**
  357. * Returns the shortest {@link LineString|path} from {@link Point|start} to {@link Point|end} without colliding with
  358. * any {@link Feature} in {@link FeatureCollection<Polygon>| obstacles}
  359. *
  360. * @name shortestPath
  361. * @param {Coord} start point
  362. * @param {Coord} end point
  363. * @param {Object} [options={}] optional parameters
  364. * @param {Geometry|Feature|FeatureCollection<Polygon>} [options.obstacles] areas which path cannot travel
  365. * @param {number} [options.minDistance] minimum distance between shortest path and obstacles
  366. * @param {string} [options.units='kilometers'] unit in which resolution & minimum distance will be expressed in; it can be degrees, radians, miles, kilometers, ...
  367. * @param {number} [options.resolution=100] distance between matrix points on which the path will be calculated
  368. * @returns {Feature<LineString>} shortest path between start and end
  369. * @example
  370. * var start = [-5, -6];
  371. * var end = [9, -6];
  372. * var options = {
  373. * obstacles: turf.polygon([[[0, -7], [5, -7], [5, -3], [0, -3], [0, -7]]])
  374. * };
  375. *
  376. * var path = turf.shortestPath(start, end, options);
  377. *
  378. * //addToMap
  379. * var addToMap = [start, end, options.obstacles, path];
  380. */
  381. function shortestPath(start, end, options) {
  382. // Optional parameters
  383. options = options || {};
  384. if (!isObject(options)) throw new Error("options is invalid");
  385. var resolution = options.resolution;
  386. var minDistance = options.minDistance;
  387. var obstacles = options.obstacles || featureCollection([]);
  388. // validation
  389. if (!start) throw new Error("start is required");
  390. if (!end) throw new Error("end is required");
  391. if ((resolution && !isNumber(resolution)) || resolution <= 0)
  392. throw new Error("options.resolution must be a number, greater than 0");
  393. if (minDistance)
  394. throw new Error("options.minDistance is not yet implemented");
  395. // Normalize Inputs
  396. var startCoord = getCoord(start);
  397. var endCoord = getCoord(end);
  398. start = point(startCoord);
  399. end = point(endCoord);
  400. // Handle obstacles
  401. switch (getType(obstacles)) {
  402. case "FeatureCollection":
  403. if (obstacles.features.length === 0)
  404. return lineString([startCoord, endCoord]);
  405. break;
  406. case "Polygon":
  407. obstacles = featureCollection([feature(getGeom(obstacles))]);
  408. break;
  409. default:
  410. throw new Error("invalid obstacles");
  411. }
  412. // define path grid area
  413. var collection = obstacles;
  414. collection.features.push(start);
  415. collection.features.push(end);
  416. var box = bbox(scale(bboxPolygon(bbox(collection)), 1.15)); // extend 15%
  417. if (!resolution) {
  418. var width = distance([box[0], box[1]], [box[2], box[1]], options);
  419. resolution = width / 100;
  420. }
  421. collection.features.pop();
  422. collection.features.pop();
  423. var west = box[0];
  424. var south = box[1];
  425. var east = box[2];
  426. var north = box[3];
  427. var xFraction = resolution / distance([west, south], [east, south], options);
  428. var cellWidth = xFraction * (east - west);
  429. var yFraction = resolution / distance([west, south], [west, north], options);
  430. var cellHeight = yFraction * (north - south);
  431. var bboxHorizontalSide = east - west;
  432. var bboxVerticalSide = north - south;
  433. var columns = Math.floor(bboxHorizontalSide / cellWidth);
  434. var rows = Math.floor(bboxVerticalSide / cellHeight);
  435. // adjust origin of the grid
  436. var deltaX = (bboxHorizontalSide - columns * cellWidth) / 2;
  437. var deltaY = (bboxVerticalSide - rows * cellHeight) / 2;
  438. // loop through points only once to speed up process
  439. // define matrix grid for A-star algorithm
  440. var pointMatrix = [];
  441. var matrix = [];
  442. var closestToStart = [];
  443. var closestToEnd = [];
  444. var minDistStart = Infinity;
  445. var minDistEnd = Infinity;
  446. var currentY = north - deltaY;
  447. var r = 0;
  448. while (currentY >= south) {
  449. // var currentY = south + deltaY;
  450. var matrixRow = [];
  451. var pointMatrixRow = [];
  452. var currentX = west + deltaX;
  453. var c = 0;
  454. while (currentX <= east) {
  455. var pt = point([currentX, currentY]);
  456. var isInsideObstacle = isInside(pt, obstacles);
  457. // feed obstacles matrix
  458. matrixRow.push(isInsideObstacle ? 0 : 1); // with javascript-astar
  459. // matrixRow.push(isInsideObstacle ? 1 : 0); // with astar-andrea
  460. // map point's coords
  461. pointMatrixRow.push(currentX + "|" + currentY);
  462. // set closest points
  463. var distStart = distance(pt, start);
  464. // if (distStart < minDistStart) {
  465. if (!isInsideObstacle && distStart < minDistStart) {
  466. minDistStart = distStart;
  467. closestToStart = { x: c, y: r };
  468. }
  469. var distEnd = distance(pt, end);
  470. // if (distEnd < minDistEnd) {
  471. if (!isInsideObstacle && distEnd < minDistEnd) {
  472. minDistEnd = distEnd;
  473. closestToEnd = { x: c, y: r };
  474. }
  475. currentX += cellWidth;
  476. c++;
  477. }
  478. matrix.push(matrixRow);
  479. pointMatrix.push(pointMatrixRow);
  480. currentY -= cellHeight;
  481. r++;
  482. }
  483. // find path on matrix grid
  484. // javascript-astar ----------------------
  485. var graph = new Graph(matrix, { diagonal: true });
  486. var startOnMatrix = graph.grid[closestToStart.y][closestToStart.x];
  487. var endOnMatrix = graph.grid[closestToEnd.y][closestToEnd.x];
  488. var result = astar.search(graph, startOnMatrix, endOnMatrix);
  489. var path = [startCoord];
  490. result.forEach(function (coord) {
  491. var coords = pointMatrix[coord.x][coord.y].split("|");
  492. path.push([+coords[0], +coords[1]]); // make sure coords are numbers
  493. });
  494. path.push(endCoord);
  495. // ---------------------------------------
  496. // astar-andrea ------------------------
  497. // var result = aStar(matrix, [closestToStart.x, closestToStart.y], [closestToEnd.x, closestToEnd.y], 'DiagonalFree');
  498. // var path = [start.geometry.coordinates];
  499. // result.forEach(function (coord) {
  500. // var coords = pointMatrix[coord[1]][coord[0]].split('|');
  501. // path.push([+coords[0], +coords[1]]); // make sure coords are numbers
  502. // });
  503. // path.push(end.geometry.coordinates);
  504. // ---------------------------------------
  505. return cleanCoords(lineString(path));
  506. }
  507. /**
  508. * Checks if Point is inside any of the Polygons
  509. *
  510. * @private
  511. * @param {Feature<Point>} pt to check
  512. * @param {FeatureCollection<Polygon>} polygons features
  513. * @returns {boolean} if inside or not
  514. */
  515. function isInside(pt, polygons) {
  516. for (var i = 0; i < polygons.features.length; i++) {
  517. if (booleanPointInPolygon(pt, polygons.features[i])) {
  518. return true;
  519. }
  520. }
  521. return false;
  522. }
  523. export default shortestPath;