index.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. 'use strict';
  2. var RBush = require('rbush');
  3. var Queue = require('tinyqueue');
  4. var pointInPolygon = require('point-in-polygon');
  5. var orient = require('robust-predicates/umd/orient2d.min.js').orient2d;
  6. // Fix for require issue in webpack https://github.com/mapbox/concaveman/issues/18
  7. if (Queue.default) {
  8. Queue = Queue.default;
  9. }
  10. module.exports = concaveman;
  11. module.exports.default = concaveman;
  12. function concaveman(points, concavity, lengthThreshold) {
  13. // a relative measure of concavity; higher value means simpler hull
  14. concavity = Math.max(0, concavity === undefined ? 2 : concavity);
  15. // when a segment goes below this length threshold, it won't be drilled down further
  16. lengthThreshold = lengthThreshold || 0;
  17. // start with a convex hull of the points
  18. var hull = fastConvexHull(points);
  19. // index the points with an R-tree
  20. var tree = new RBush(16);
  21. tree.toBBox = function (a) {
  22. return {
  23. minX: a[0],
  24. minY: a[1],
  25. maxX: a[0],
  26. maxY: a[1]
  27. };
  28. };
  29. tree.compareMinX = function (a, b) { return a[0] - b[0]; };
  30. tree.compareMinY = function (a, b) { return a[1] - b[1]; };
  31. tree.load(points);
  32. // turn the convex hull into a linked list and populate the initial edge queue with the nodes
  33. var queue = [];
  34. for (var i = 0, last; i < hull.length; i++) {
  35. var p = hull[i];
  36. tree.remove(p);
  37. last = insertNode(p, last);
  38. queue.push(last);
  39. }
  40. // index the segments with an R-tree (for intersection checks)
  41. var segTree = new RBush(16);
  42. for (i = 0; i < queue.length; i++) segTree.insert(updateBBox(queue[i]));
  43. var sqConcavity = concavity * concavity;
  44. var sqLenThreshold = lengthThreshold * lengthThreshold;
  45. // process edges one by one
  46. while (queue.length) {
  47. var node = queue.shift();
  48. var a = node.p;
  49. var b = node.next.p;
  50. // skip the edge if it's already short enough
  51. var sqLen = getSqDist(a, b);
  52. if (sqLen < sqLenThreshold) continue;
  53. var maxSqLen = sqLen / sqConcavity;
  54. // find the best connection point for the current edge to flex inward to
  55. p = findCandidate(tree, node.prev.p, a, b, node.next.next.p, maxSqLen, segTree);
  56. // if we found a connection and it satisfies our concavity measure
  57. if (p && Math.min(getSqDist(p, a), getSqDist(p, b)) <= maxSqLen) {
  58. // connect the edge endpoints through this point and add 2 new edges to the queue
  59. queue.push(node);
  60. queue.push(insertNode(p, node));
  61. // update point and segment indexes
  62. tree.remove(p);
  63. segTree.remove(node);
  64. segTree.insert(updateBBox(node));
  65. segTree.insert(updateBBox(node.next));
  66. }
  67. }
  68. // convert the resulting hull linked list to an array of points
  69. node = last;
  70. var concave = [];
  71. do {
  72. concave.push(node.p);
  73. node = node.next;
  74. } while (node !== last);
  75. concave.push(node.p);
  76. return concave;
  77. }
  78. function findCandidate(tree, a, b, c, d, maxDist, segTree) {
  79. var queue = new Queue([], compareDist);
  80. var node = tree.data;
  81. // search through the point R-tree with a depth-first search using a priority queue
  82. // in the order of distance to the edge (b, c)
  83. while (node) {
  84. for (var i = 0; i < node.children.length; i++) {
  85. var child = node.children[i];
  86. var dist = node.leaf ? sqSegDist(child, b, c) : sqSegBoxDist(b, c, child);
  87. if (dist > maxDist) continue; // skip the node if it's farther than we ever need
  88. queue.push({
  89. node: child,
  90. dist: dist
  91. });
  92. }
  93. while (queue.length && !queue.peek().node.children) {
  94. var item = queue.pop();
  95. var p = item.node;
  96. // skip all points that are as close to adjacent edges (a,b) and (c,d),
  97. // and points that would introduce self-intersections when connected
  98. var d0 = sqSegDist(p, a, b);
  99. var d1 = sqSegDist(p, c, d);
  100. if (item.dist < d0 && item.dist < d1 &&
  101. noIntersections(b, p, segTree) &&
  102. noIntersections(c, p, segTree)) return p;
  103. }
  104. node = queue.pop();
  105. if (node) node = node.node;
  106. }
  107. return null;
  108. }
  109. function compareDist(a, b) {
  110. return a.dist - b.dist;
  111. }
  112. // square distance from a segment bounding box to the given one
  113. function sqSegBoxDist(a, b, bbox) {
  114. if (inside(a, bbox) || inside(b, bbox)) return 0;
  115. var d1 = sqSegSegDist(a[0], a[1], b[0], b[1], bbox.minX, bbox.minY, bbox.maxX, bbox.minY);
  116. if (d1 === 0) return 0;
  117. var d2 = sqSegSegDist(a[0], a[1], b[0], b[1], bbox.minX, bbox.minY, bbox.minX, bbox.maxY);
  118. if (d2 === 0) return 0;
  119. var d3 = sqSegSegDist(a[0], a[1], b[0], b[1], bbox.maxX, bbox.minY, bbox.maxX, bbox.maxY);
  120. if (d3 === 0) return 0;
  121. var d4 = sqSegSegDist(a[0], a[1], b[0], b[1], bbox.minX, bbox.maxY, bbox.maxX, bbox.maxY);
  122. if (d4 === 0) return 0;
  123. return Math.min(d1, d2, d3, d4);
  124. }
  125. function inside(a, bbox) {
  126. return a[0] >= bbox.minX &&
  127. a[0] <= bbox.maxX &&
  128. a[1] >= bbox.minY &&
  129. a[1] <= bbox.maxY;
  130. }
  131. // check if the edge (a,b) doesn't intersect any other edges
  132. function noIntersections(a, b, segTree) {
  133. var minX = Math.min(a[0], b[0]);
  134. var minY = Math.min(a[1], b[1]);
  135. var maxX = Math.max(a[0], b[0]);
  136. var maxY = Math.max(a[1], b[1]);
  137. var edges = segTree.search({minX: minX, minY: minY, maxX: maxX, maxY: maxY});
  138. for (var i = 0; i < edges.length; i++) {
  139. if (intersects(edges[i].p, edges[i].next.p, a, b)) return false;
  140. }
  141. return true;
  142. }
  143. function cross(p1, p2, p3) {
  144. return orient(p1[0], p1[1], p2[0], p2[1], p3[0], p3[1]);
  145. }
  146. // check if the edges (p1,q1) and (p2,q2) intersect
  147. function intersects(p1, q1, p2, q2) {
  148. return p1 !== q2 && q1 !== p2 &&
  149. cross(p1, q1, p2) > 0 !== cross(p1, q1, q2) > 0 &&
  150. cross(p2, q2, p1) > 0 !== cross(p2, q2, q1) > 0;
  151. }
  152. // update the bounding box of a node's edge
  153. function updateBBox(node) {
  154. var p1 = node.p;
  155. var p2 = node.next.p;
  156. node.minX = Math.min(p1[0], p2[0]);
  157. node.minY = Math.min(p1[1], p2[1]);
  158. node.maxX = Math.max(p1[0], p2[0]);
  159. node.maxY = Math.max(p1[1], p2[1]);
  160. return node;
  161. }
  162. // speed up convex hull by filtering out points inside quadrilateral formed by 4 extreme points
  163. function fastConvexHull(points) {
  164. var left = points[0];
  165. var top = points[0];
  166. var right = points[0];
  167. var bottom = points[0];
  168. // find the leftmost, rightmost, topmost and bottommost points
  169. for (var i = 0; i < points.length; i++) {
  170. var p = points[i];
  171. if (p[0] < left[0]) left = p;
  172. if (p[0] > right[0]) right = p;
  173. if (p[1] < top[1]) top = p;
  174. if (p[1] > bottom[1]) bottom = p;
  175. }
  176. // filter out points that are inside the resulting quadrilateral
  177. var cull = [left, top, right, bottom];
  178. var filtered = cull.slice();
  179. for (i = 0; i < points.length; i++) {
  180. if (!pointInPolygon(points[i], cull)) filtered.push(points[i]);
  181. }
  182. // get convex hull around the filtered points
  183. return convexHull(filtered);
  184. }
  185. // create a new node in a doubly linked list
  186. function insertNode(p, prev) {
  187. var node = {
  188. p: p,
  189. prev: null,
  190. next: null,
  191. minX: 0,
  192. minY: 0,
  193. maxX: 0,
  194. maxY: 0
  195. };
  196. if (!prev) {
  197. node.prev = node;
  198. node.next = node;
  199. } else {
  200. node.next = prev.next;
  201. node.prev = prev;
  202. prev.next.prev = node;
  203. prev.next = node;
  204. }
  205. return node;
  206. }
  207. // square distance between 2 points
  208. function getSqDist(p1, p2) {
  209. var dx = p1[0] - p2[0],
  210. dy = p1[1] - p2[1];
  211. return dx * dx + dy * dy;
  212. }
  213. // square distance from a point to a segment
  214. function sqSegDist(p, p1, p2) {
  215. var x = p1[0],
  216. y = p1[1],
  217. dx = p2[0] - x,
  218. dy = p2[1] - y;
  219. if (dx !== 0 || dy !== 0) {
  220. var t = ((p[0] - x) * dx + (p[1] - y) * dy) / (dx * dx + dy * dy);
  221. if (t > 1) {
  222. x = p2[0];
  223. y = p2[1];
  224. } else if (t > 0) {
  225. x += dx * t;
  226. y += dy * t;
  227. }
  228. }
  229. dx = p[0] - x;
  230. dy = p[1] - y;
  231. return dx * dx + dy * dy;
  232. }
  233. // segment to segment distance, ported from http://geomalgorithms.com/a07-_distance.html by Dan Sunday
  234. function sqSegSegDist(x0, y0, x1, y1, x2, y2, x3, y3) {
  235. var ux = x1 - x0;
  236. var uy = y1 - y0;
  237. var vx = x3 - x2;
  238. var vy = y3 - y2;
  239. var wx = x0 - x2;
  240. var wy = y0 - y2;
  241. var a = ux * ux + uy * uy;
  242. var b = ux * vx + uy * vy;
  243. var c = vx * vx + vy * vy;
  244. var d = ux * wx + uy * wy;
  245. var e = vx * wx + vy * wy;
  246. var D = a * c - b * b;
  247. var sc, sN, tc, tN;
  248. var sD = D;
  249. var tD = D;
  250. if (D === 0) {
  251. sN = 0;
  252. sD = 1;
  253. tN = e;
  254. tD = c;
  255. } else {
  256. sN = b * e - c * d;
  257. tN = a * e - b * d;
  258. if (sN < 0) {
  259. sN = 0;
  260. tN = e;
  261. tD = c;
  262. } else if (sN > sD) {
  263. sN = sD;
  264. tN = e + b;
  265. tD = c;
  266. }
  267. }
  268. if (tN < 0.0) {
  269. tN = 0.0;
  270. if (-d < 0.0) sN = 0.0;
  271. else if (-d > a) sN = sD;
  272. else {
  273. sN = -d;
  274. sD = a;
  275. }
  276. } else if (tN > tD) {
  277. tN = tD;
  278. if ((-d + b) < 0.0) sN = 0;
  279. else if (-d + b > a) sN = sD;
  280. else {
  281. sN = -d + b;
  282. sD = a;
  283. }
  284. }
  285. sc = sN === 0 ? 0 : sN / sD;
  286. tc = tN === 0 ? 0 : tN / tD;
  287. var cx = (1 - sc) * x0 + sc * x1;
  288. var cy = (1 - sc) * y0 + sc * y1;
  289. var cx2 = (1 - tc) * x2 + tc * x3;
  290. var cy2 = (1 - tc) * y2 + tc * y3;
  291. var dx = cx2 - cx;
  292. var dy = cy2 - cy;
  293. return dx * dx + dy * dy;
  294. }
  295. function compareByX(a, b) {
  296. return a[0] === b[0] ? a[1] - b[1] : a[0] - b[0];
  297. }
  298. function convexHull(points) {
  299. points.sort(compareByX);
  300. var lower = [];
  301. for (var i = 0; i < points.length; i++) {
  302. while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], points[i]) <= 0) {
  303. lower.pop();
  304. }
  305. lower.push(points[i]);
  306. }
  307. var upper = [];
  308. for (var ii = points.length - 1; ii >= 0; ii--) {
  309. while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], points[ii]) <= 0) {
  310. upper.pop();
  311. }
  312. upper.push(points[ii]);
  313. }
  314. upper.pop();
  315. lower.pop();
  316. return lower.concat(upper);
  317. }