hull.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. /*
  2. (c) 2014, Andrey Geonya
  3. Hull.js, a JavaScript library for concave hull generation by set of points.
  4. https://github.com/AndreyGeonya/hull
  5. */
  6. //'use strict';
  7. //var intersect = require('./intersect.js');
  8. //var grid = require('./grid.js');
  9. include("intersect.js");
  10. include("grid.js");
  11. function _sortByX(pointset) {
  12. return pointset.sort(function(a, b) {
  13. if (a[0] == b[0]) {
  14. return a[1] - b[1];
  15. } else {
  16. return a[0] - b[0];
  17. }
  18. });
  19. }
  20. function _getMaxY(pointset) {
  21. var maxY = -Infinity;
  22. for (var i = pointset.length - 1; i >= 0; i--) {
  23. if (pointset[i][1] > maxY) {
  24. maxY = pointset[i][1];
  25. }
  26. }
  27. return maxY;
  28. }
  29. function _upperTangent(pointset) {
  30. var lower = [];
  31. for (var l = 0; l < pointset.length; l++) {
  32. while (lower.length >= 2 && (_cross(lower[lower.length - 2], lower[lower.length - 1], pointset[l]) <= 0)) {
  33. lower.pop();
  34. }
  35. lower.push(pointset[l]);
  36. }
  37. lower.pop();
  38. return lower;
  39. }
  40. function _lowerTangent(pointset) {
  41. var reversed = pointset.reverse(),
  42. upper = [];
  43. for (var u = 0; u < reversed.length; u++) {
  44. while (upper.length >= 2 && (_cross(upper[upper.length - 2], upper[upper.length - 1], reversed[u]) <= 0)) {
  45. upper.pop();
  46. }
  47. upper.push(reversed[u]);
  48. }
  49. upper.pop();
  50. return upper;
  51. }
  52. function _cross(o, a, b) {
  53. return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
  54. }
  55. function _sqLength(a, b) {
  56. return Math.pow(b[0] - a[0], 2) + Math.pow(b[1] - a[1], 2);
  57. }
  58. function _cos(o, a, b) {
  59. var aShifted = [a[0] - o[0], a[1] - o[1]],
  60. bShifted = [b[0] - o[0], b[1] - o[1]],
  61. sqALen = _sqLength(o, a),
  62. sqBLen = _sqLength(o, b),
  63. dot = aShifted[0] * bShifted[0] + aShifted[1] * bShifted[1];
  64. return dot / Math.sqrt(sqALen * sqBLen);
  65. }
  66. function _intersect(segment, pointset) {
  67. for (var i = 0; i < pointset.length - 1; i++) {
  68. var seg = [pointset[i], pointset[i + 1]];
  69. if (segment[0][0] === seg[0][0] && segment[0][1] === seg[0][1] ||
  70. segment[0][0] === seg[1][0] && segment[0][1] === seg[1][1]) {
  71. continue;
  72. }
  73. if (intersect(segment, seg)) {
  74. return true;
  75. }
  76. }
  77. return false;
  78. }
  79. function _bBoxAround(edge, boxSize) {
  80. var minX, maxX, minY, maxY;
  81. if (edge[0][0] < edge[1][0]) {
  82. minX = edge[0][0] - boxSize;
  83. maxX = edge[1][0] + boxSize;
  84. } else {
  85. minX = edge[1][0] - boxSize;
  86. maxX = edge[0][0] + boxSize;
  87. }
  88. if (edge[0][1] < edge[1][1]) {
  89. minY = edge[0][1] - boxSize;
  90. maxY = edge[1][1] + boxSize;
  91. } else {
  92. minY = edge[1][1] - boxSize;
  93. maxY = edge[0][1] + boxSize;
  94. }
  95. return [
  96. minX, minY, // tl
  97. maxX, maxY // br
  98. ];
  99. }
  100. function _midPoint(edge, innerPoints, convex) {
  101. var point = null,
  102. angle1Cos = MAX_CONCAVE_ANGLE_COS,
  103. angle2Cos = MAX_CONCAVE_ANGLE_COS,
  104. a1Cos, a2Cos;
  105. for (var i = 0; i < innerPoints.length; i++) {
  106. a1Cos = _cos(edge[0], edge[1], innerPoints[i]);
  107. a2Cos = _cos(edge[1], edge[0], innerPoints[i]);
  108. if (a1Cos > angle1Cos && a2Cos > angle2Cos &&
  109. !_intersect([edge[0], innerPoints[i]], convex) &&
  110. !_intersect([edge[1], innerPoints[i]], convex)) {
  111. angle1Cos = a1Cos;
  112. angle2Cos = a2Cos;
  113. point = innerPoints[i];
  114. }
  115. }
  116. return point;
  117. }
  118. function _concave(convex, maxSqEdgeLen, maxSearchBBoxSize, grid) {
  119. var edge,
  120. border,
  121. bBoxSize,
  122. midPoint,
  123. bBoxAround,
  124. midPointInserted = false;
  125. for (var i = 0; i < convex.length - 1; i++) {
  126. edge = [convex[i], convex[i + 1]];
  127. if (_sqLength(edge[0], edge[1]) < maxSqEdgeLen) { continue; }
  128. border = 0;
  129. bBoxSize = MIN_SEARCH_BBOX_SIZE;
  130. bBoxAround = _bBoxAround(edge, bBoxSize);
  131. do {
  132. bBoxAround = grid.addBorder2Bbox(bBoxAround, border);
  133. bBoxSize = bBoxAround[2] - bBoxAround[0];
  134. midPoint = _midPoint(edge, grid.rangePoints(bBoxAround), convex);
  135. border++;
  136. } while (midPoint === null && maxSearchBBoxSize > bBoxSize);
  137. if (midPoint !== null) {
  138. convex.splice(i + 1, 0, midPoint);
  139. grid.removePoint(midPoint);
  140. midPointInserted = true;
  141. }
  142. }
  143. if (midPointInserted) {
  144. return _concave(convex, maxSqEdgeLen, maxSearchBBoxSize, grid);
  145. }
  146. return convex;
  147. }
  148. function hull(pointset, concavity) {
  149. var lower, upper, convex,
  150. innerPoints,
  151. maxSearchBBoxSize,
  152. maxEdgeLen = concavity || 20;
  153. if (pointset.length < 4) {
  154. return pointset;
  155. }
  156. pointset = _sortByX(pointset);
  157. upper = _upperTangent(pointset);
  158. lower = _lowerTangent(pointset);
  159. convex = lower.concat(upper);
  160. convex.push(pointset[0]);
  161. maxSearchBBoxSize = Math.max(pointset[pointset.length - 1][0], _getMaxY(convex)) * MAX_SEARCH_BBOX_SIZE_PERCENT;
  162. innerPoints = pointset.filter(function(pt) {
  163. return convex.indexOf(pt) < 0;
  164. });
  165. return _concave(convex, Math.pow(maxEdgeLen, 2), maxSearchBBoxSize, grid(innerPoints));
  166. }
  167. var MAX_CONCAVE_ANGLE_COS = Math.cos(90 / (180 / Math.PI)); // angle = 90 deg
  168. var MIN_SEARCH_BBOX_SIZE = 5;
  169. var MAX_SEARCH_BBOX_SIZE_PERCENT = 0.8;
  170. //module.exports = hull;