classes/segment.js

  1. /**
  2. * Created by Alex Bol on 3/10/2017.
  3. */
  4. "use strict";
  5. module.exports = function (Flatten) {
  6. /**
  7. * Class representing a segment
  8. * @type {Segment}
  9. */
  10. Flatten.Segment = class Segment {
  11. /**
  12. *
  13. * @param {Point} ps - start point
  14. * @param {Point} pe - end point
  15. */
  16. constructor(...args) {
  17. /**
  18. * Start point
  19. * @type {Point}
  20. */
  21. this.ps = new Flatten.Point();
  22. /**
  23. * End Point
  24. * @type {Point}
  25. */
  26. this.pe = new Flatten.Point();
  27. if (args.length == 0) {
  28. return;
  29. }
  30. if (args.length == 1 && args[0] instanceof Array && args[0].length == 4) {
  31. let coords = args[0];
  32. this.ps = new Flatten.Point(coords[0], coords[1]);
  33. this.pe = new Flatten.Point(coords[2], coords[3]);
  34. return;
  35. }
  36. if (args.length == 1 && args[0] instanceof Object && args[0].name === "segment") {
  37. let {ps,pe} = args[0];
  38. this.ps = new Flatten.Point(ps.x, ps.y);
  39. this.pe = new Flatten.Point(pe.x, pe.y);
  40. return;
  41. }
  42. if (args.length == 2 && args[0] instanceof Flatten.Point && args[1] instanceof Flatten.Point) {
  43. this.ps = args[0].clone();
  44. this.pe = args[1].clone();
  45. return;
  46. }
  47. if (args.length == 4) {
  48. this.ps = new Flatten.Point(args[0], args[1]);
  49. this.pe = new Flatten.Point(args[2], args[3]);
  50. return;
  51. }
  52. throw Flatten.Errors.ILLEGAL_PARAMETERS;
  53. }
  54. /**
  55. * Method clone copies segment and returns a new instance
  56. * @returns {Segment}
  57. */
  58. clone() {
  59. return new Flatten.Segment(this.start, this.end);
  60. }
  61. /**
  62. * Start point
  63. * @returns {Point}
  64. */
  65. get start() {
  66. return this.ps;
  67. }
  68. /**
  69. * End point
  70. * @returns {Point}
  71. */
  72. get end() {
  73. return this.pe;
  74. }
  75. /**
  76. * Returns array of start and end point
  77. * @returns [Point,Point]
  78. */
  79. get vertices() {
  80. return [this.ps.clone(), this.pe.clone()];
  81. }
  82. /**
  83. * Length of a segment
  84. * @returns {number}
  85. */
  86. get length() {
  87. return this.start.distanceTo(this.end)[0];
  88. }
  89. /**
  90. * Slope of the line - angle to axe x in radians from 0 to 2PI
  91. * @returns {number}
  92. */
  93. get slope() {
  94. let vec = new Flatten.Vector(this.start, this.end);
  95. return vec.slope;
  96. }
  97. /**
  98. * Bounding box
  99. * @returns {Box}
  100. */
  101. get box() {
  102. return new Flatten.Box(
  103. Math.min(this.start.x, this.end.x),
  104. Math.min(this.start.y, this.end.y),
  105. Math.max(this.start.x, this.end.x),
  106. Math.max(this.start.y, this.end.y)
  107. )
  108. }
  109. /**
  110. * Returns true if equals to query segment, false otherwise
  111. * @param {Seg} seg - query segment
  112. * @returns {boolean}
  113. */
  114. equalTo(seg) {
  115. return this.ps.equalTo(seg.ps) && this.pe.equalTo(seg.pe);
  116. }
  117. /**
  118. * Returns true if segment contains point
  119. * @param {Point} pt Query point
  120. * @returns {boolean}
  121. */
  122. contains(pt) {
  123. return Flatten.Utils.EQ_0(this.distanceToPoint(pt));
  124. }
  125. /**
  126. * Returns array of intersection points between segment and other shape
  127. * @param {Shape} shape - Shape of the one of supported types <br/>
  128. * @returns {Point[]}
  129. */
  130. intersect(shape) {
  131. if (shape instanceof Flatten.Point) {
  132. return this.contains(shape) ? [shape] : [];
  133. }
  134. if (shape instanceof Flatten.Line) {
  135. return Segment.intersectSegment2Line(this, shape);
  136. }
  137. if (shape instanceof Flatten.Segment) {
  138. return Segment.intersectSegment2Segment(this, shape);
  139. }
  140. if (shape instanceof Flatten.Circle) {
  141. return Segment.intersectSegment2Circle(this, shape);
  142. }
  143. if (shape instanceof Flatten.Arc) {
  144. return Segment.intersectSegment2Arc(this, shape);
  145. }
  146. if (shape instanceof Flatten.Polygon) {
  147. return Flatten.Polygon.intersectShape2Polygon(this, shape);
  148. }
  149. }
  150. /**
  151. * Calculate distance and shortest segment from segment to shape and return as array [distance, shortest segment]
  152. * @param {Shape} shape Shape of the one of supported types Point, Line, Circle, Segment, Arc, Polygon or Planar Set
  153. * @returns {number} distance from segment to shape
  154. * @returns {Segment} shortest segment between segment and shape (started at segment, ended at shape)
  155. */
  156. distanceTo(shape) {
  157. let {Distance} = Flatten;
  158. if (shape instanceof Flatten.Point) {
  159. let [dist, shortest_segment] = Distance.point2segment(shape, this);
  160. shortest_segment = shortest_segment.reverse();
  161. return [dist, shortest_segment];
  162. }
  163. if (shape instanceof Flatten.Circle) {
  164. let [dist, shortest_segment] = Distance.segment2circle(this, shape);
  165. return [dist, shortest_segment];
  166. }
  167. if (shape instanceof Flatten.Line) {
  168. let [dist, shortest_segment] = Distance.segment2line(this, shape);
  169. return [dist, shortest_segment];
  170. }
  171. if (shape instanceof Flatten.Segment) {
  172. let [dist, shortest_segment] = Distance.segment2segment(this, shape);
  173. return [dist, shortest_segment];
  174. }
  175. if (shape instanceof Flatten.Arc) {
  176. let [dist, shortest_segment] = Distance.segment2arc(this, shape);
  177. return [dist, shortest_segment];
  178. }
  179. if (shape instanceof Flatten.Polygon) {
  180. let [dist, shortest_segment] = Distance.shape2polygon(this, shape);
  181. return [dist, shortest_segment];
  182. }
  183. if (shape instanceof Flatten.PlanarSet) {
  184. let [dist, shortest_segment] = Distance.shape2planarSet(this, shape);
  185. return [dist, shortest_segment];
  186. }
  187. }
  188. /**
  189. * Returns unit vector in the direction from start to end
  190. * @returns {Vector}
  191. */
  192. tangentInStart() {
  193. let vec = new Flatten.Vector(this.start, this.end);
  194. return vec.normalize();
  195. }
  196. /**
  197. * Return unit vector in the direction from end to start
  198. * @returns {Vector}
  199. */
  200. tangentInEnd() {
  201. let vec = new Flatten.Vector(this.end, this.start);
  202. return vec.normalize();
  203. }
  204. /**
  205. * Returns new segment with swapped start and end points
  206. * @returns {Segment}
  207. */
  208. reverse() {
  209. return new Segment(this.end, this.start);
  210. }
  211. /**
  212. * When point belongs to segment, return array of two segments split by given point,
  213. * if point is inside segment. Returns clone of this segment if query point is incident
  214. * to start or end point of the segment. Returns empty array if point does not belong to segment
  215. * @param {Point} pt Query point
  216. * @returns {Segment[]}
  217. */
  218. split(pt) {
  219. if (!this.contains(pt))
  220. return [];
  221. if (this.start.equalTo(this.end))
  222. return [this.clone()];
  223. if (this.start.equalTo(pt) || this.end.equalTo(pt))
  224. return [this];
  225. return [
  226. new Flatten.Segment(this.start, pt),
  227. new Flatten.Segment(pt, this.end)
  228. ]
  229. }
  230. /**
  231. * Return middle point of the segment
  232. * @returns {Point}
  233. */
  234. middle() {
  235. return new Flatten.Point((this.start.x + this.end.x)/2, (this.start.y + this.end.y)/2);
  236. }
  237. distanceToPoint(pt) {
  238. let [dist, ...rest] = Flatten.Distance.point2segment(pt, this);
  239. return dist;
  240. };
  241. definiteIntegral(ymin = 0.0) {
  242. let dx = this.end.x - this.start.x;
  243. let dy1 = this.start.y - ymin;
  244. let dy2 = this.end.y - ymin;
  245. return ( dx * (dy1 + dy2) / 2 );
  246. }
  247. /**
  248. * Returns new segment translated by vector vec
  249. * @param {Vector} vec
  250. * @returns {Segment}
  251. */
  252. translate(...args) {
  253. return new Segment(this.ps.translate(...args), this.pe.translate(...args));
  254. }
  255. /**
  256. * Return new segment rotated by given angle around given point
  257. * If point omitted, rotate around origin (0,0)
  258. * Positive value of angle defines rotation counter clockwise, negative - clockwise
  259. * @param {number} angle - rotation angle in radians
  260. * @param {Point} center - center point, default is (0,0)
  261. * @returns {Segment}
  262. */
  263. rotate(angle = 0, center = new Flatten.Point()) {
  264. let m = new Flatten.Matrix();
  265. m = m.translate(center.x, center.y).rotate(angle).translate(-center.x, -center.y);
  266. return this.transform(m);
  267. }
  268. /**
  269. * Return new segment transformed using affine transformation matrix
  270. * @param {Matrix} matrix - affine transformation matrix
  271. * @returns {Segment} - transformed segment
  272. */
  273. transform(matrix = new Flatten.Matrix()) {
  274. return new Segment(this.ps.transform(matrix), this.pe.transform(matrix))
  275. }
  276. /**
  277. * Returns true if segment start is equal to segment end up to DP_TOL
  278. * @returns {boolean}
  279. */
  280. isZeroLength() {
  281. return this.ps.equalTo(this.pe)
  282. }
  283. static intersectSegment2Line(seg, line) {
  284. let ip = [];
  285. // Boundary cases
  286. if (seg.ps.on(line)) {
  287. ip.push(seg.ps);
  288. }
  289. // If both ends lay on line, return two intersection points
  290. if (seg.pe.on(line) && !seg.isZeroLength()) {
  291. ip.push(seg.pe);
  292. }
  293. if (ip.length > 0) {
  294. return ip; // done, intersection found
  295. }
  296. // If zero-length segment and nothing found, return no intersections
  297. if (seg.isZeroLength()) {
  298. return ip;
  299. }
  300. // Not a boundary case, check if both points are on the same side and
  301. // hence there is no intersection
  302. if (seg.ps.leftTo(line) && seg.pe.leftTo(line) ||
  303. !seg.ps.leftTo(line) && !seg.pe.leftTo(line)) {
  304. return ip;
  305. }
  306. // Calculate intersection between lines
  307. let line1 = new Flatten.Line(seg.ps, seg.pe);
  308. return line1.intersect(line);
  309. }
  310. static intersectSegment2Segment(seg1, seg2) {
  311. let ip = [];
  312. // quick reject
  313. if (seg1.box.not_intersect(seg2.box)) {
  314. return ip;
  315. }
  316. // Special case of seg1 zero length
  317. if (seg1.isZeroLength()) {
  318. if (seg1.ps.on(seg2)) {
  319. ip.push(seg1.ps);
  320. }
  321. return ip;
  322. }
  323. // Special case of seg2 zero length
  324. if (seg2.isZeroLength()) {
  325. if (seg2.ps.on(seg1)) {
  326. ip.push(seg2.ps);
  327. }
  328. return ip;
  329. }
  330. // Neither seg1 nor seg2 is zero length
  331. let line1 = new Flatten.Line(seg1.ps, seg1.pe);
  332. let line2 = new Flatten.Line(seg2.ps, seg2.pe);
  333. // Check overlapping between segments in case of incidence
  334. // If segments touching, add one point. If overlapping, add two points
  335. if (line1.incidentTo(line2)) {
  336. if (seg1.ps.on(seg2)) {
  337. ip.push(seg1.ps);
  338. }
  339. if (seg1.pe.on(seg2)) {
  340. ip.push(seg1.pe);
  341. }
  342. if (seg2.ps.on(seg1) && !seg2.ps.equalTo(seg1.ps) && !seg2.ps.equalTo(seg1.pe)) {
  343. ip.push(seg2.ps);
  344. }
  345. if (seg2.pe.on(seg1) && !seg2.pe.equalTo(seg1.ps) && !seg2.pe.equalTo(seg1.pe)) {
  346. ip.push(seg2.pe);
  347. }
  348. }
  349. else { /* not incident - parallel or intersect */
  350. // Calculate intersection between lines
  351. let new_ip = line1.intersect(line2);
  352. if (new_ip.length > 0 && new_ip[0].on(seg1) && new_ip[0].on(seg2)) {
  353. ip.push(new_ip[0]);
  354. }
  355. }
  356. return ip;
  357. }
  358. static intersectSegment2Circle(segment, circle) {
  359. let ips = [];
  360. if (segment.box.not_intersect(circle.box)) {
  361. return ips;
  362. }
  363. // Special case of zero length segment
  364. if (segment.isZeroLength()) {
  365. let [dist,shortest_segment] = segment.ps.distanceTo(circle.pc);
  366. if (Flatten.Utils.EQ(dist, circle.r)) {
  367. ips.push(segment.ps);
  368. }
  369. return ips;
  370. }
  371. // Non zero-length segment
  372. let line = new Flatten.Line(segment.ps, segment.pe);
  373. let ips_tmp = line.intersect(circle);
  374. for (let ip of ips_tmp) {
  375. if (ip.on(segment)) {
  376. ips.push(ip);
  377. }
  378. }
  379. return ips;
  380. }
  381. static intersectSegment2Arc(segment, arc) {
  382. let ip = [];
  383. if (segment.box.not_intersect(arc.box)) {
  384. return ip;
  385. }
  386. // Special case of zero-length segment
  387. if (segment.isZeroLength()) {
  388. if (segment.ps.on(arc)) {
  389. ip.push(segment.ps);
  390. }
  391. return ip;
  392. }
  393. // Non-zero length segment
  394. let line = new Flatten.Line(segment.ps, segment.pe);
  395. let circle = new Flatten.Circle(arc.pc, arc.r);
  396. let ip_tmp = line.intersect(circle);
  397. for (let pt of ip_tmp) {
  398. if (pt.on(segment) && pt.on(arc)) {
  399. ip.push(pt);
  400. }
  401. }
  402. return ip;
  403. }
  404. /**
  405. * Return string to draw segment in svg
  406. * @param {Object} attrs - an object with attributes for svg path element,
  407. * like "stroke", "strokeWidth" <br/>
  408. * Defaults are stroke:"black", strokeWidth:"1"
  409. * @returns {string}
  410. */
  411. svg(attrs = {}) {
  412. let {stroke, strokeWidth, id, className} = attrs;
  413. // let rest_str = Object.keys(rest).reduce( (acc, key) => acc += ` ${key}="${rest[key]}"`, "");
  414. let id_str = (id && id.length > 0) ? `id="${id}"` : "";
  415. let class_str = (className && className.length > 0) ? `class="${className}"` : "";
  416. return `\n<line x1="${this.start.x}" y1="${this.start.y}" x2="${this.end.x}" y2="${this.end.y}" stroke="${stroke || "black"}" stroke-width="${strokeWidth || 1}" ${id_str} ${class_str} />`;
  417. }
  418. /**
  419. * This method returns an object that defines how data will be
  420. * serialized when called JSON.stringify() method
  421. * @returns {Object}
  422. */
  423. toJSON() {
  424. return Object.assign({},this,{name:"segment"});
  425. }
  426. };
  427. /**
  428. * Shortcut method to create new segment
  429. */
  430. Flatten.segment = (...args) => new Flatten.Segment(...args);
  431. };