GeometryCircles.js 11 KB

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