index.js 18 KB

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