geometry-circles.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. import geometry from './geometry.js';
  2. var getAngleBetweenPoints = geometry.getAngleBetweenPoints,
  3. getCenterOfPoints = geometry.getCenterOfPoints,
  4. getDistanceBetweenPoints = geometry.getDistanceBetweenPoints;
  5. var round = function round(x, decimals) {
  6. var a = Math.pow(10, decimals);
  7. return Math.round(x * a) / a;
  8. };
  9. /**
  10. * Calculates the area of a circular segment based on the radius of the circle
  11. * and the height of the segment.
  12. * See http://mathworld.wolfram.com/CircularSegment.html
  13. *
  14. * @param {number} r The radius of the circle.
  15. * @param {number} h The height of the circular segment.
  16. * @returns {number} Returns the area of the circular segment.
  17. */
  18. var getCircularSegmentArea = function getCircularSegmentArea(r, h) {
  19. return r * r * Math.acos(1 - h / r) - (r - h) * Math.sqrt(h * (2 * r - h));
  20. };
  21. /**
  22. * Calculates the area of overlap between two circles based on their radiuses
  23. * and the distance between them.
  24. * See http://mathworld.wolfram.com/Circle-CircleIntersection.html
  25. *
  26. * @param {number} r1 Radius of the first circle.
  27. * @param {number} r2 Radius of the second circle.
  28. * @param {number} d The distance between the two circles.
  29. * @returns {number} Returns the area of overlap between the two circles.
  30. */
  31. var getOverlapBetweenCircles =
  32. function getOverlapBetweenCircles(r1, r2, d) {
  33. var overlap = 0;
  34. // If the distance is larger than the sum of the radiuses then the circles
  35. // does not overlap.
  36. if (d < r1 + r2) {
  37. var r1Square = r1 * r1,
  38. r2Square = r2 * r2;
  39. if (d <= Math.abs(r2 - r1)) {
  40. // If the circles are completely overlapping, then the overlap
  41. // equals the area of the smallest circle.
  42. overlap = Math.PI * Math.min(r1Square, r2Square);
  43. } else {
  44. // Height of first triangle segment.
  45. var d1 = (r1Square - r2Square + d * d) / (2 * d),
  46. // Height of second triangle segment.
  47. d2 = d - d1;
  48. overlap = (
  49. getCircularSegmentArea(r1, r1 - d1) +
  50. getCircularSegmentArea(r2, r2 - d2)
  51. );
  52. }
  53. // Round the result to two decimals.
  54. overlap = round(overlap, 14);
  55. }
  56. return overlap;
  57. };
  58. /**
  59. * Calculates the intersection points of two circles.
  60. *
  61. * NOTE: does not handle floating errors well.
  62. *
  63. * @param {object} c1 The first circle.s
  64. * @param {object} c2 The second sircle.
  65. * @returns {array} Returns the resulting intersection points.
  66. */
  67. var getCircleCircleIntersection =
  68. function getCircleCircleIntersection(c1, c2) {
  69. var d = getDistanceBetweenPoints(c1, c2),
  70. r1 = c1.r,
  71. r2 = c2.r,
  72. points = [];
  73. if (d < r1 + r2 && d > Math.abs(r1 - r2)) {
  74. // If the circles are overlapping, but not completely overlapping, then
  75. // it exists intersecting points.
  76. var r1Square = r1 * r1,
  77. r2Square = r2 * r2,
  78. // d^2 - r^2 + R^2 / 2d
  79. x = (r1Square - r2Square + d * d) / (2 * d),
  80. // y^2 = R^2 - x^2
  81. y = Math.sqrt(r1Square - x * x),
  82. x1 = c1.x,
  83. x2 = c2.x,
  84. y1 = c1.y,
  85. y2 = c2.y,
  86. x0 = x1 + x * (x2 - x1) / d,
  87. y0 = y1 + x * (y2 - y1) / d,
  88. rx = -(y2 - y1) * (y / d),
  89. ry = -(x2 - x1) * (y / d);
  90. points = [
  91. { x: round(x0 + rx, 14), y: round(y0 - ry, 14) },
  92. { x: round(x0 - rx, 14), y: round(y0 + ry, 14) }
  93. ];
  94. }
  95. return points;
  96. };
  97. /**
  98. * Calculates all the intersection points for between a list of circles.
  99. *
  100. * @param {array} circles The circles to calculate the points from.
  101. * @returns {array} Returns a list of intersection points.
  102. */
  103. var getCirclesIntersectionPoints = function getIntersectionPoints(circles) {
  104. return circles.reduce(function (points, c1, i, arr) {
  105. var additional = arr.slice(i + 1)
  106. .reduce(function (points, c2, j) {
  107. var indexes = [i, j + i + 1];
  108. return points.concat(
  109. getCircleCircleIntersection(c1, c2)
  110. .map(function (p) {
  111. p.indexes = indexes;
  112. return p;
  113. })
  114. );
  115. }, []);
  116. return points.concat(additional);
  117. }, []);
  118. };
  119. /**
  120. * Tests wether a point lies within a given circle.
  121. *
  122. * @param {object} point The point to test for.
  123. * @param {object} circle The circle to test if the point is within.
  124. * @returns {boolean} Returns true if the point is inside, false if outside.
  125. */
  126. var isPointInsideCircle = function isPointInsideCircle(point, circle) {
  127. return getDistanceBetweenPoints(point, circle) <= circle.r + 1e-10;
  128. };
  129. /**
  130. * Tests wether a point lies within a set of circles.
  131. *
  132. * @param {object} point The point to test.
  133. * @param {array} circles The list of circles to test against.
  134. * @returns {boolean} Returns true if the point is inside all the circles, false
  135. * if not.
  136. */
  137. var isPointInsideAllCircles = function isPointInsideAllCircles(point, circles) {
  138. return !circles.some(function (circle) {
  139. return !isPointInsideCircle(point, circle);
  140. });
  141. };
  142. /**
  143. * Tests wether a point lies outside a set of circles.
  144. *
  145. * TODO: add unit tests.
  146. *
  147. * @param {object} point The point to test.
  148. * @param {array} circles The list of circles to test against.
  149. * @returns {boolean} Returns true if the point is outside all the circles,
  150. * false if not.
  151. */
  152. var isPointOutsideAllCircles =
  153. function isPointOutsideAllCircles(point, circles) {
  154. return !circles.some(function (circle) {
  155. return isPointInsideCircle(point, circle);
  156. });
  157. };
  158. /**
  159. * Calculate the path for the area of overlap between a set of circles.
  160. *
  161. * TODO: handle cases with only 1 or 0 arcs.
  162. *
  163. * @param {array} circles List of circles to calculate area of.
  164. * @returns {string} Returns the path for the area of overlap. Returns an empty
  165. * string if there are no intersection between all the circles.
  166. */
  167. var getAreaOfIntersectionBetweenCircles =
  168. function getAreaOfIntersectionBetweenCircles(circles) {
  169. var intersectionPoints = getCirclesIntersectionPoints(circles)
  170. .filter(function (p) {
  171. return isPointInsideAllCircles(p, circles);
  172. }),
  173. result;
  174. if (intersectionPoints.length > 1) {
  175. // Calculate the center of the intersection points.
  176. var center = getCenterOfPoints(intersectionPoints);
  177. intersectionPoints = intersectionPoints
  178. // Calculate the angle between the center and the points.
  179. .map(function (p) {
  180. p.angle = getAngleBetweenPoints(center, p);
  181. return p;
  182. })
  183. // Sort the points by the angle to the center.
  184. .sort(function (a, b) {
  185. return b.angle - a.angle;
  186. });
  187. var startPoint = intersectionPoints[intersectionPoints.length - 1];
  188. var arcs = intersectionPoints
  189. .reduce(function (data, p1) {
  190. var startPoint = data.startPoint,
  191. midPoint = getCenterOfPoints([startPoint, p1]);
  192. // Calculate the arc from the intersection points and their
  193. // circles.
  194. var arc = p1.indexes
  195. // Filter out circles that are not included in both
  196. // intersection points.
  197. .filter(function (index) {
  198. return startPoint.indexes.indexOf(index) > -1;
  199. })
  200. // Iterate the circles of the intersection points and
  201. // calculate arcs.
  202. .reduce(function (arc, index) {
  203. var circle = circles[index],
  204. angle1 = getAngleBetweenPoints(circle, p1),
  205. angle2 = getAngleBetweenPoints(circle, startPoint),
  206. angleDiff = angle2 - angle1 +
  207. (angle2 < angle1 ? 2 * Math.PI : 0),
  208. angle = angle2 - angleDiff / 2,
  209. width = getDistanceBetweenPoints(
  210. midPoint,
  211. {
  212. x: circle.x + circle.r * Math.sin(angle),
  213. y: circle.y + circle.r * Math.cos(angle)
  214. }
  215. ),
  216. r = circle.r;
  217. // Width can sometimes become to large due to floating
  218. // point errors
  219. if (width > r * 2) {
  220. width = r * 2;
  221. }
  222. // Get the arc with the smallest width.
  223. if (!arc || arc.width > width) {
  224. arc = {
  225. r: r,
  226. largeArc: width > r ? 1 : 0,
  227. width: width,
  228. x: p1.x,
  229. y: p1.y
  230. };
  231. }
  232. // Return the chosen arc.
  233. return arc;
  234. }, null);
  235. // If we find an arc then add it to the list and update p2.
  236. if (arc) {
  237. var r = arc.r;
  238. data.arcs.push(
  239. ['A', r, r, 0, arc.largeArc, 1, arc.x, arc.y]
  240. );
  241. data.startPoint = p1;
  242. }
  243. return data;
  244. }, {
  245. startPoint: startPoint,
  246. arcs: []
  247. }).arcs;
  248. if (arcs.length === 0) {
  249. } else if (arcs.length === 1) {
  250. } else {
  251. arcs.unshift(['M', startPoint.x, startPoint.y]);
  252. result = {
  253. center: center,
  254. d: arcs
  255. };
  256. }
  257. }
  258. return result;
  259. };
  260. var geometryCircles = {
  261. getAreaOfIntersectionBetweenCircles: getAreaOfIntersectionBetweenCircles,
  262. getCircleCircleIntersection: getCircleCircleIntersection,
  263. getCirclesIntersectionPoints: getCirclesIntersectionPoints,
  264. getCircularSegmentArea: getCircularSegmentArea,
  265. getOverlapBetweenCircles: getOverlapBetweenCircles,
  266. isPointInsideCircle: isPointInsideCircle,
  267. isPointInsideAllCircles: isPointInsideAllCircles,
  268. isPointOutsideAllCircles: isPointOutsideAllCircles,
  269. round: round
  270. };
  271. export default geometryCircles;