polyline.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  1. /* eslint-disable no-constructor-return */
  2. import { Line } from './line';
  3. import { Point } from './point';
  4. import { Rectangle } from './rectangle';
  5. import { Geometry } from './geometry';
  6. export class Polyline extends Geometry {
  7. get start() {
  8. return this.points[0] || null;
  9. }
  10. get end() {
  11. return this.points[this.points.length - 1] || null;
  12. }
  13. constructor(points) {
  14. super();
  15. if (points != null) {
  16. if (typeof points === 'string') {
  17. return Polyline.parse(points);
  18. }
  19. this.points = points.map((p) => Point.create(p));
  20. }
  21. else {
  22. this.points = [];
  23. }
  24. }
  25. scale(sx, sy, origin = new Point()) {
  26. this.points.forEach((p) => p.scale(sx, sy, origin));
  27. return this;
  28. }
  29. rotate(angle, origin) {
  30. this.points.forEach((p) => p.rotate(angle, origin));
  31. return this;
  32. }
  33. translate(dx, dy) {
  34. const t = Point.create(dx, dy);
  35. this.points.forEach((p) => p.translate(t.x, t.y));
  36. return this;
  37. }
  38. round(precision = 0) {
  39. this.points.forEach((p) => p.round(precision));
  40. return this;
  41. }
  42. bbox() {
  43. if (this.points.length === 0) {
  44. return new Rectangle();
  45. }
  46. let x1 = Infinity;
  47. let x2 = -Infinity;
  48. let y1 = Infinity;
  49. let y2 = -Infinity;
  50. const points = this.points;
  51. for (let i = 0, ii = points.length; i < ii; i += 1) {
  52. const point = points[i];
  53. const x = point.x;
  54. const y = point.y;
  55. if (x < x1)
  56. x1 = x;
  57. if (x > x2)
  58. x2 = x;
  59. if (y < y1)
  60. y1 = y;
  61. if (y > y2)
  62. y2 = y;
  63. }
  64. return new Rectangle(x1, y1, x2 - x1, y2 - y1);
  65. }
  66. closestPoint(p) {
  67. const cpLength = this.closestPointLength(p);
  68. return this.pointAtLength(cpLength);
  69. }
  70. closestPointLength(p) {
  71. const points = this.points;
  72. const count = points.length;
  73. if (count === 0 || count === 1) {
  74. return 0;
  75. }
  76. let length = 0;
  77. let cpLength = 0;
  78. let minSqrDistance = Infinity;
  79. for (let i = 0, ii = count - 1; i < ii; i += 1) {
  80. const line = new Line(points[i], points[i + 1]);
  81. const lineLength = line.length();
  82. const cpNormalizedLength = line.closestPointNormalizedLength(p);
  83. const cp = line.pointAt(cpNormalizedLength);
  84. const sqrDistance = cp.squaredDistance(p);
  85. if (sqrDistance < minSqrDistance) {
  86. minSqrDistance = sqrDistance;
  87. cpLength = length + cpNormalizedLength * lineLength;
  88. }
  89. length += lineLength;
  90. }
  91. return cpLength;
  92. }
  93. closestPointNormalizedLength(p) {
  94. const length = this.length();
  95. if (length === 0) {
  96. return 0;
  97. }
  98. const cpLength = this.closestPointLength(p);
  99. return cpLength / length;
  100. }
  101. closestPointTangent(p) {
  102. const cpLength = this.closestPointLength(p);
  103. return this.tangentAtLength(cpLength);
  104. }
  105. containsPoint(p) {
  106. if (this.points.length === 0) {
  107. return false;
  108. }
  109. const ref = Point.clone(p);
  110. const x = ref.x;
  111. const y = ref.y;
  112. const points = this.points;
  113. const count = points.length;
  114. let startIndex = count - 1;
  115. let intersectionCount = 0;
  116. for (let endIndex = 0; endIndex < count; endIndex += 1) {
  117. const start = points[startIndex];
  118. const end = points[endIndex];
  119. if (ref.equals(start)) {
  120. return true;
  121. }
  122. const segment = new Line(start, end);
  123. if (segment.containsPoint(p)) {
  124. return true;
  125. }
  126. // do we have an intersection?
  127. if ((y <= start.y && y > end.y) || (y > start.y && y <= end.y)) {
  128. // this conditional branch IS NOT entered when `segment` is collinear/coincident with `ray`
  129. // (when `y === start.y === end.y`)
  130. // this conditional branch IS entered when `segment` touches `ray` at only one point
  131. // (e.g. when `y === start.y !== end.y`)
  132. // since this branch is entered again for the following segment, the two touches cancel out
  133. const xDifference = start.x - x > end.x - x ? start.x - x : end.x - x;
  134. if (xDifference >= 0) {
  135. // segment lies at least partially to the right of `p`
  136. const rayEnd = new Point(x + xDifference, y); // right
  137. const ray = new Line(p, rayEnd);
  138. if (segment.intersectsWithLine(ray)) {
  139. // an intersection was detected to the right of `p`
  140. intersectionCount += 1;
  141. }
  142. } // else: `segment` lies completely to the left of `p` (i.e. no intersection to the right)
  143. }
  144. // move to check the next polyline segment
  145. startIndex = endIndex;
  146. }
  147. // returns `true` for odd numbers of intersections (even-odd algorithm)
  148. return intersectionCount % 2 === 1;
  149. }
  150. intersectsWithLine(line) {
  151. const intersections = [];
  152. for (let i = 0, n = this.points.length - 1; i < n; i += 1) {
  153. const a = this.points[i];
  154. const b = this.points[i + 1];
  155. const int = line.intersectsWithLine(new Line(a, b));
  156. if (int) {
  157. intersections.push(int);
  158. }
  159. }
  160. return intersections.length > 0 ? intersections : null;
  161. }
  162. isDifferentiable() {
  163. for (let i = 0, ii = this.points.length - 1; i < ii; i += 1) {
  164. const a = this.points[i];
  165. const b = this.points[i + 1];
  166. const line = new Line(a, b);
  167. if (line.isDifferentiable()) {
  168. return true;
  169. }
  170. }
  171. return false;
  172. }
  173. length() {
  174. let len = 0;
  175. for (let i = 0, ii = this.points.length - 1; i < ii; i += 1) {
  176. const a = this.points[i];
  177. const b = this.points[i + 1];
  178. len += a.distance(b);
  179. }
  180. return len;
  181. }
  182. pointAt(ratio) {
  183. const points = this.points;
  184. const count = points.length;
  185. if (count === 0) {
  186. return null;
  187. }
  188. if (count === 1) {
  189. return points[0].clone();
  190. }
  191. if (ratio <= 0) {
  192. return points[0].clone();
  193. }
  194. if (ratio >= 1) {
  195. return points[count - 1].clone();
  196. }
  197. const total = this.length();
  198. const length = total * ratio;
  199. return this.pointAtLength(length);
  200. }
  201. pointAtLength(length) {
  202. const points = this.points;
  203. const count = points.length;
  204. if (count === 0) {
  205. return null;
  206. }
  207. if (count === 1) {
  208. return points[0].clone();
  209. }
  210. let fromStart = true;
  211. if (length < 0) {
  212. fromStart = false;
  213. length = -length; // eslint-disable-line
  214. }
  215. let tmp = 0;
  216. for (let i = 0, ii = count - 1; i < ii; i += 1) {
  217. const index = fromStart ? i : ii - 1 - i;
  218. const a = points[index];
  219. const b = points[index + 1];
  220. const l = new Line(a, b);
  221. const d = a.distance(b);
  222. if (length <= tmp + d) {
  223. return l.pointAtLength((fromStart ? 1 : -1) * (length - tmp));
  224. }
  225. tmp += d;
  226. }
  227. const lastPoint = fromStart ? points[count - 1] : points[0];
  228. return lastPoint.clone();
  229. }
  230. tangentAt(ratio) {
  231. const points = this.points;
  232. const count = points.length;
  233. if (count === 0 || count === 1) {
  234. return null;
  235. }
  236. if (ratio < 0) {
  237. ratio = 0; // eslint-disable-line
  238. }
  239. if (ratio > 1) {
  240. ratio = 1; // eslint-disable-line
  241. }
  242. const total = this.length();
  243. const length = total * ratio;
  244. return this.tangentAtLength(length);
  245. }
  246. tangentAtLength(length) {
  247. const points = this.points;
  248. const count = points.length;
  249. if (count === 0 || count === 1) {
  250. return null;
  251. }
  252. let fromStart = true;
  253. if (length < 0) {
  254. fromStart = false;
  255. length = -length; // eslint-disable-line
  256. }
  257. let lastValidLine;
  258. let tmp = 0;
  259. for (let i = 0, ii = count - 1; i < ii; i += 1) {
  260. const index = fromStart ? i : ii - 1 - i;
  261. const a = points[index];
  262. const b = points[index + 1];
  263. const l = new Line(a, b);
  264. const d = a.distance(b);
  265. if (l.isDifferentiable()) {
  266. // has a tangent line (line length is not 0)
  267. if (length <= tmp + d) {
  268. return l.tangentAtLength((fromStart ? 1 : -1) * (length - tmp));
  269. }
  270. lastValidLine = l;
  271. }
  272. tmp += d;
  273. }
  274. if (lastValidLine) {
  275. const ratio = fromStart ? 1 : 0;
  276. return lastValidLine.tangentAt(ratio);
  277. }
  278. return null;
  279. }
  280. simplify(
  281. // TODO: Accept startIndex and endIndex to specify where to start and end simplification
  282. options = {}) {
  283. const points = this.points;
  284. // we need at least 3 points
  285. if (points.length < 3) {
  286. return this;
  287. }
  288. const threshold = options.threshold || 0;
  289. // start at the beginning of the polyline and go forward
  290. let currentIndex = 0;
  291. // we need at least one intermediate point (3 points) in every iteration
  292. // as soon as that stops being true, we know we reached the end of the polyline
  293. while (points[currentIndex + 2]) {
  294. const firstIndex = currentIndex;
  295. const middleIndex = currentIndex + 1;
  296. const lastIndex = currentIndex + 2;
  297. const firstPoint = points[firstIndex];
  298. const middlePoint = points[middleIndex];
  299. const lastPoint = points[lastIndex];
  300. const chord = new Line(firstPoint, lastPoint); // = connection between first and last point
  301. const closestPoint = chord.closestPoint(middlePoint); // = closest point on chord from middle point
  302. const closestPointDistance = closestPoint.distance(middlePoint);
  303. if (closestPointDistance <= threshold) {
  304. // middle point is close enough to the chord = simplify
  305. // 1) remove middle point:
  306. points.splice(middleIndex, 1);
  307. // 2) in next iteration, investigate the newly-created triplet of points
  308. // - do not change `currentIndex`
  309. // = (first point stays, point after removed point becomes middle point)
  310. }
  311. else {
  312. // middle point is far from the chord
  313. // 1) preserve middle point
  314. // 2) in next iteration, move `currentIndex` by one step:
  315. currentIndex += 1;
  316. // = (point after first point becomes first point)
  317. }
  318. }
  319. // `points` array was modified in-place
  320. return this;
  321. }
  322. toHull() {
  323. const points = this.points;
  324. const count = points.length;
  325. if (count === 0) {
  326. return new Polyline();
  327. }
  328. // Step 1: find the starting point -- point with
  329. // the lowest y (if equality, highest x).
  330. let startPoint = points[0];
  331. for (let i = 1; i < count; i += 1) {
  332. if (points[i].y < startPoint.y) {
  333. startPoint = points[i];
  334. }
  335. else if (points[i].y === startPoint.y && points[i].x > startPoint.x) {
  336. startPoint = points[i];
  337. }
  338. }
  339. // Step 2: sort the list of points by angle between line
  340. // from start point to current point and the x-axis (theta).
  341. // Step 2a: create the point records = [point, originalIndex, angle]
  342. const sortedRecords = [];
  343. for (let i = 0; i < count; i += 1) {
  344. let angle = startPoint.theta(points[i]);
  345. if (angle === 0) {
  346. // Give highest angle to start point.
  347. // The start point will end up at end of sorted list.
  348. // The start point will end up at beginning of hull points list.
  349. angle = 360;
  350. }
  351. sortedRecords.push([points[i], i, angle]);
  352. }
  353. // Step 2b: sort the list in place
  354. sortedRecords.sort((record1, record2) => {
  355. let ret = record1[2] - record2[2];
  356. if (ret === 0) {
  357. ret = record2[1] - record1[1];
  358. }
  359. return ret;
  360. });
  361. // Step 2c: duplicate start record from the top of
  362. // the stack to the bottom of the stack.
  363. if (sortedRecords.length > 2) {
  364. const startPoint = sortedRecords[sortedRecords.length - 1];
  365. sortedRecords.unshift(startPoint);
  366. }
  367. // Step 3
  368. // ------
  369. // Step 3a: go through sorted points in order and find those with
  370. // right turns, and we want to get our results in clockwise order.
  371. // Dictionary of points with left turns - cannot be on the hull.
  372. const insidePoints = {};
  373. // Stack of records with right turns - hull point candidates.
  374. const hullRecords = [];
  375. const getKey = (record) => `${record[0].toString()}@${record[1]}`;
  376. while (sortedRecords.length !== 0) {
  377. const currentRecord = sortedRecords.pop();
  378. const currentPoint = currentRecord[0];
  379. // Check if point has already been discarded.
  380. if (insidePoints[getKey(currentRecord)]) {
  381. continue;
  382. }
  383. let correctTurnFound = false;
  384. while (!correctTurnFound) {
  385. if (hullRecords.length < 2) {
  386. // Not enough points for comparison, just add current point.
  387. hullRecords.push(currentRecord);
  388. correctTurnFound = true;
  389. }
  390. else {
  391. const lastHullRecord = hullRecords.pop();
  392. const lastHullPoint = lastHullRecord[0];
  393. const secondLastHullRecord = hullRecords.pop();
  394. const secondLastHullPoint = secondLastHullRecord[0];
  395. const crossProduct = secondLastHullPoint.cross(lastHullPoint, currentPoint);
  396. if (crossProduct < 0) {
  397. // Found a right turn.
  398. hullRecords.push(secondLastHullRecord);
  399. hullRecords.push(lastHullRecord);
  400. hullRecords.push(currentRecord);
  401. correctTurnFound = true;
  402. }
  403. else if (crossProduct === 0) {
  404. // the three points are collinear
  405. // three options:
  406. // there may be a 180 or 0 degree angle at lastHullPoint
  407. // or two of the three points are coincident
  408. // we have to take rounding errors into account
  409. const THRESHOLD = 1e-10;
  410. const angleBetween = lastHullPoint.angleBetween(secondLastHullPoint, currentPoint);
  411. if (Math.abs(angleBetween - 180) < THRESHOLD) {
  412. // rouding around 180 to 180
  413. // if the cross product is 0 because the angle is 180 degrees
  414. // discard last hull point (add to insidePoints)
  415. // insidePoints.unshift(lastHullPoint);
  416. insidePoints[getKey(lastHullRecord)] = lastHullPoint;
  417. // reenter second-to-last hull point (will be last at next iter)
  418. hullRecords.push(secondLastHullRecord);
  419. // do not do anything with current point
  420. // correct turn not found
  421. }
  422. else if (lastHullPoint.equals(currentPoint) ||
  423. secondLastHullPoint.equals(lastHullPoint)) {
  424. // if the cross product is 0 because two points are the same
  425. // discard last hull point (add to insidePoints)
  426. // insidePoints.unshift(lastHullPoint);
  427. insidePoints[getKey(lastHullRecord)] = lastHullPoint;
  428. // reenter second-to-last hull point (will be last at next iter)
  429. hullRecords.push(secondLastHullRecord);
  430. // do not do anything with current point
  431. // correct turn not found
  432. }
  433. else if (Math.abs(((angleBetween + 1) % 360) - 1) < THRESHOLD) {
  434. // rounding around 0 and 360 to 0
  435. // if the cross product is 0 because the angle is 0 degrees
  436. // remove last hull point from hull BUT do not discard it
  437. // reenter second-to-last hull point (will be last at next iter)
  438. hullRecords.push(secondLastHullRecord);
  439. // put last hull point back into the sorted point records list
  440. sortedRecords.push(lastHullRecord);
  441. // we are switching the order of the 0deg and 180deg points
  442. // correct turn not found
  443. }
  444. }
  445. else {
  446. // found a left turn
  447. // discard last hull point (add to insidePoints)
  448. // insidePoints.unshift(lastHullPoint);
  449. insidePoints[getKey(lastHullRecord)] = lastHullPoint;
  450. // reenter second-to-last hull point (will be last at next iter of loop)
  451. hullRecords.push(secondLastHullRecord);
  452. // do not do anything with current point
  453. // correct turn not found
  454. }
  455. }
  456. }
  457. }
  458. // At this point, hullPointRecords contains the output points in clockwise order
  459. // the points start with lowest-y,highest-x startPoint, and end at the same point
  460. // Step 3b: remove duplicated startPointRecord from the end of the array
  461. if (hullRecords.length > 2) {
  462. hullRecords.pop();
  463. }
  464. // Step 4: find the lowest originalIndex record and put it at the beginning of hull
  465. let lowestHullIndex; // the lowest originalIndex on the hull
  466. let indexOfLowestHullIndexRecord = -1; // the index of the record with lowestHullIndex
  467. for (let i = 0, n = hullRecords.length; i < n; i += 1) {
  468. const currentHullIndex = hullRecords[i][1];
  469. if (lowestHullIndex === undefined || currentHullIndex < lowestHullIndex) {
  470. lowestHullIndex = currentHullIndex;
  471. indexOfLowestHullIndexRecord = i;
  472. }
  473. }
  474. let hullPointRecordsReordered = [];
  475. if (indexOfLowestHullIndexRecord > 0) {
  476. const newFirstChunk = hullRecords.slice(indexOfLowestHullIndexRecord);
  477. const newSecondChunk = hullRecords.slice(0, indexOfLowestHullIndexRecord);
  478. hullPointRecordsReordered = newFirstChunk.concat(newSecondChunk);
  479. }
  480. else {
  481. hullPointRecordsReordered = hullRecords;
  482. }
  483. const hullPoints = [];
  484. for (let i = 0, n = hullPointRecordsReordered.length; i < n; i += 1) {
  485. hullPoints.push(hullPointRecordsReordered[i][0]);
  486. }
  487. return new Polyline(hullPoints);
  488. }
  489. equals(p) {
  490. if (p == null) {
  491. return false;
  492. }
  493. if (p.points.length !== this.points.length) {
  494. return false;
  495. }
  496. return p.points.every((a, i) => a.equals(this.points[i]));
  497. }
  498. clone() {
  499. return new Polyline(this.points.map((p) => p.clone()));
  500. }
  501. toJSON() {
  502. return this.points.map((p) => p.toJSON());
  503. }
  504. serialize() {
  505. return this.points.map((p) => `${p.serialize()}`).join(' ');
  506. }
  507. }
  508. (function (Polyline) {
  509. function isPolyline(instance) {
  510. return instance != null && instance instanceof Polyline;
  511. }
  512. Polyline.isPolyline = isPolyline;
  513. })(Polyline || (Polyline = {}));
  514. (function (Polyline) {
  515. function parse(svgString) {
  516. const str = svgString.trim();
  517. if (str === '') {
  518. return new Polyline();
  519. }
  520. const points = [];
  521. const coords = str.split(/\s*,\s*|\s+/);
  522. for (let i = 0, ii = coords.length; i < ii; i += 2) {
  523. points.push({ x: +coords[i], y: +coords[i + 1] });
  524. }
  525. return new Polyline(points);
  526. }
  527. Polyline.parse = parse;
  528. })(Polyline || (Polyline = {}));
  529. //# sourceMappingURL=polyline.js.map