LineAttachmentUtil.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. var sqrt = Math.sqrt,
  2. min = Math.min,
  3. max = Math.max,
  4. abs = Math.abs;
  5. /**
  6. * Calculate the square (power to two) of a number.
  7. *
  8. * @param {number} n
  9. *
  10. * @return {number}
  11. */
  12. function sq(n) {
  13. return Math.pow(n, 2);
  14. }
  15. /**
  16. * Get distance between two points.
  17. *
  18. * @param {Point} p1
  19. * @param {Point} p2
  20. *
  21. * @return {number}
  22. */
  23. function getDistance(p1, p2) {
  24. return sqrt(sq(p1.x - p2.x) + sq(p1.y - p2.y));
  25. }
  26. /**
  27. * Return the attachment of the given point on the specified line.
  28. *
  29. * The attachment is either a bendpoint (attached to the given point)
  30. * or segment (attached to a location on a line segment) attachment:
  31. *
  32. * ```javascript
  33. * var pointAttachment = {
  34. * type: 'bendpoint',
  35. * bendpointIndex: 3,
  36. * position: { x: 10, y: 10 } // the attach point on the line
  37. * };
  38. *
  39. * var segmentAttachment = {
  40. * type: 'segment',
  41. * segmentIndex: 2,
  42. * relativeLocation: 0.31, // attach point location between 0 (at start) and 1 (at end)
  43. * position: { x: 10, y: 10 } // the attach point on the line
  44. * };
  45. * ```
  46. *
  47. * @param {Point} point
  48. * @param {Array<Point>} line
  49. *
  50. * @return {Object} attachment
  51. */
  52. export function getAttachment(point, line) {
  53. var idx = 0,
  54. segmentStart,
  55. segmentEnd,
  56. segmentStartDistance,
  57. segmentEndDistance,
  58. attachmentPosition,
  59. minDistance,
  60. intersections,
  61. attachment,
  62. attachmentDistance,
  63. closestAttachmentDistance,
  64. closestAttachment;
  65. for (idx = 0; idx < line.length - 1; idx++) {
  66. segmentStart = line[idx];
  67. segmentEnd = line[idx + 1];
  68. if (pointsEqual(segmentStart, segmentEnd)) {
  69. intersections = [ segmentStart ];
  70. } else {
  71. segmentStartDistance = getDistance(point, segmentStart);
  72. segmentEndDistance = getDistance(point, segmentEnd);
  73. minDistance = min(segmentStartDistance, segmentEndDistance);
  74. intersections = getCircleSegmentIntersections(segmentStart, segmentEnd, point, minDistance);
  75. }
  76. if (intersections.length < 1) {
  77. throw new Error('expected between [1, 2] circle -> line intersections');
  78. }
  79. // one intersection -> bendpoint attachment
  80. if (intersections.length === 1) {
  81. attachment = {
  82. type: 'bendpoint',
  83. position: intersections[0],
  84. segmentIndex: idx,
  85. bendpointIndex: pointsEqual(segmentStart, intersections[0]) ? idx : idx + 1
  86. };
  87. }
  88. // two intersections -> segment attachment
  89. if (intersections.length === 2) {
  90. attachmentPosition = mid(intersections[0], intersections[1]);
  91. attachment = {
  92. type: 'segment',
  93. position: attachmentPosition,
  94. segmentIndex: idx,
  95. relativeLocation: getDistance(segmentStart, attachmentPosition) / getDistance(segmentStart, segmentEnd)
  96. };
  97. }
  98. attachmentDistance = getDistance(attachment.position, point);
  99. if (!closestAttachment || closestAttachmentDistance > attachmentDistance) {
  100. closestAttachment = attachment;
  101. closestAttachmentDistance = attachmentDistance;
  102. }
  103. }
  104. return closestAttachment;
  105. }
  106. /**
  107. * Gets the intersection between a circle and a line segment.
  108. *
  109. * @param {Point} s1 segment start
  110. * @param {Point} s2 segment end
  111. * @param {Point} cc circle center
  112. * @param {number} cr circle radius
  113. *
  114. * @return {Array<Point>} intersections
  115. */
  116. function getCircleSegmentIntersections(s1, s2, cc, cr) {
  117. var baX = s2.x - s1.x;
  118. var baY = s2.y - s1.y;
  119. var caX = cc.x - s1.x;
  120. var caY = cc.y - s1.y;
  121. var a = baX * baX + baY * baY;
  122. var bBy2 = baX * caX + baY * caY;
  123. var c = caX * caX + caY * caY - cr * cr;
  124. var pBy2 = bBy2 / a;
  125. var q = c / a;
  126. var disc = pBy2 * pBy2 - q;
  127. // check against negative value to work around
  128. // negative, very close to zero results (-4e-15)
  129. // being produced in some environments
  130. if (disc < 0 && disc > -0.000001) {
  131. disc = 0;
  132. }
  133. if (disc < 0) {
  134. return [];
  135. }
  136. // if disc == 0 ... dealt with later
  137. var tmpSqrt = sqrt(disc);
  138. var abScalingFactor1 = -pBy2 + tmpSqrt;
  139. var abScalingFactor2 = -pBy2 - tmpSqrt;
  140. var i1 = {
  141. x: s1.x - baX * abScalingFactor1,
  142. y: s1.y - baY * abScalingFactor1
  143. };
  144. if (disc === 0) { // abScalingFactor1 == abScalingFactor2
  145. return [ i1 ];
  146. }
  147. var i2 = {
  148. x: s1.x - baX * abScalingFactor2,
  149. y: s1.y - baY * abScalingFactor2
  150. };
  151. // return only points on line segment
  152. return [ i1, i2 ].filter(function(p) {
  153. return isPointInSegment(p, s1, s2);
  154. });
  155. }
  156. function isPointInSegment(p, segmentStart, segmentEnd) {
  157. return (
  158. fenced(p.x, segmentStart.x, segmentEnd.x) &&
  159. fenced(p.y, segmentStart.y, segmentEnd.y)
  160. );
  161. }
  162. function fenced(n, rangeStart, rangeEnd) {
  163. // use matching threshold to work around
  164. // precision errors in intersection computation
  165. return (
  166. n >= min(rangeStart, rangeEnd) - EQUAL_THRESHOLD &&
  167. n <= max(rangeStart, rangeEnd) + EQUAL_THRESHOLD
  168. );
  169. }
  170. /**
  171. * Calculate mid of two points.
  172. *
  173. * @param {Point} p1
  174. * @param {Point} p2
  175. *
  176. * @return {Point}
  177. */
  178. function mid(p1, p2) {
  179. return {
  180. x: (p1.x + p2.x) / 2,
  181. y: (p1.y + p2.y) / 2
  182. };
  183. }
  184. var EQUAL_THRESHOLD = 0.1;
  185. function pointsEqual(p1, p2) {
  186. return (
  187. abs(p1.x - p2.x) <= EQUAL_THRESHOLD &&
  188. abs(p1.y - p2.y) <= EQUAL_THRESHOLD
  189. );
  190. }