GeometryCircles.js 11 KB

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