point.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. import { GeometryUtil } from './util';
  2. import { Angle } from './angle';
  3. import { Geometry } from './geometry';
  4. export class Point extends Geometry {
  5. constructor(x, y) {
  6. super();
  7. this.x = x == null ? 0 : x;
  8. this.y = y == null ? 0 : y;
  9. }
  10. /**
  11. * Rounds the point to the given precision.
  12. */
  13. round(precision = 0) {
  14. this.x = GeometryUtil.round(this.x, precision);
  15. this.y = GeometryUtil.round(this.y, precision);
  16. return this;
  17. }
  18. add(x, y) {
  19. const p = Point.create(x, y);
  20. this.x += p.x;
  21. this.y += p.y;
  22. return this;
  23. }
  24. update(x, y) {
  25. const p = Point.create(x, y);
  26. this.x = p.x;
  27. this.y = p.y;
  28. return this;
  29. }
  30. translate(dx, dy) {
  31. const t = Point.create(dx, dy);
  32. this.x += t.x;
  33. this.y += t.y;
  34. return this;
  35. }
  36. /**
  37. * Rotate the point by `degree` around `center`.
  38. */
  39. rotate(degree, center) {
  40. const p = Point.rotate(this, degree, center);
  41. this.x = p.x;
  42. this.y = p.y;
  43. return this;
  44. }
  45. /**
  46. * Scale point by `sx` and `sy` around the given `origin`. If origin is
  47. * not specified, the point is scaled around `0, 0`.
  48. */
  49. scale(sx, sy, origin = new Point()) {
  50. const ref = Point.create(origin);
  51. this.x = ref.x + sx * (this.x - ref.x);
  52. this.y = ref.y + sy * (this.y - ref.y);
  53. return this;
  54. }
  55. /**
  56. * Chooses the point closest to this point from among `points`. If `points`
  57. * is an empty array, `null` is returned.
  58. */
  59. closest(points) {
  60. if (points.length === 1) {
  61. return Point.create(points[0]);
  62. }
  63. let ret = null;
  64. let min = Infinity;
  65. points.forEach((p) => {
  66. const dist = this.squaredDistance(p);
  67. if (dist < min) {
  68. ret = p;
  69. min = dist;
  70. }
  71. });
  72. return ret ? Point.create(ret) : null;
  73. }
  74. /**
  75. * Returns the distance between the point and another point `p`.
  76. */
  77. distance(p) {
  78. return Math.sqrt(this.squaredDistance(p));
  79. }
  80. /**
  81. * Returns the squared distance between the point and another point `p`.
  82. *
  83. * Useful for distance comparisons in which real distance is not necessary
  84. * (saves one `Math.sqrt()` operation).
  85. */
  86. squaredDistance(p) {
  87. const ref = Point.create(p);
  88. const dx = this.x - ref.x;
  89. const dy = this.y - ref.y;
  90. return dx * dx + dy * dy;
  91. }
  92. manhattanDistance(p) {
  93. const ref = Point.create(p);
  94. return Math.abs(ref.x - this.x) + Math.abs(ref.y - this.y);
  95. }
  96. /**
  97. * Returns the magnitude of the point vector.
  98. *
  99. * @see http://en.wikipedia.org/wiki/Magnitude_(mathematics)
  100. */
  101. magnitude() {
  102. return Math.sqrt(this.x * this.x + this.y * this.y) || 0.01;
  103. }
  104. /**
  105. * Returns the angle(in degrees) between vector from this point to `p` and
  106. * the x-axis.
  107. */
  108. theta(p = new Point()) {
  109. const ref = Point.create(p);
  110. const y = -(ref.y - this.y); // invert the y-axis.
  111. const x = ref.x - this.x;
  112. let rad = Math.atan2(y, x);
  113. // Correction for III. and IV. quadrant.
  114. if (rad < 0) {
  115. rad = 2 * Math.PI + rad;
  116. }
  117. return (180 * rad) / Math.PI;
  118. }
  119. /**
  120. * Returns the angle(in degrees) between vector from this point to `p1` and
  121. * the vector from this point to `p2`.
  122. *
  123. * The ordering of points `p1` and `p2` is important.
  124. *
  125. * The function returns a value between `0` and `180` when the angle (in the
  126. * direction from `p1` to `p2`) is clockwise, and a value between `180` and
  127. * `360` when the angle is counterclockwise.
  128. *
  129. * Returns `NaN` if either of the points `p1` and `p2` is equal with this point.
  130. */
  131. angleBetween(p1, p2) {
  132. if (this.equals(p1) || this.equals(p2)) {
  133. return NaN;
  134. }
  135. let angle = this.theta(p2) - this.theta(p1);
  136. if (angle < 0) {
  137. angle += 360;
  138. }
  139. return angle;
  140. }
  141. /**
  142. * Returns the angle(in degrees) between the line from `(0,0)` and this point
  143. * and the line from `(0,0)` to `p`.
  144. *
  145. * The function returns a value between `0` and `180` when the angle (in the
  146. * direction from this point to `p`) is clockwise, and a value between `180`
  147. * and `360` when the angle is counterclockwise. Returns `NaN` if called from
  148. * point `(0,0)` or if `p` is `(0,0)`.
  149. */
  150. vectorAngle(p) {
  151. const zero = new Point(0, 0);
  152. return zero.angleBetween(this, p);
  153. }
  154. /**
  155. * Converts rectangular to polar coordinates.
  156. */
  157. toPolar(origin) {
  158. this.update(Point.toPolar(this, origin));
  159. return this;
  160. }
  161. /**
  162. * Returns the change in angle(in degrees) that is the result of moving the
  163. * point from its previous position to its current position.
  164. *
  165. * More specifically, this function computes the angle between the line from
  166. * the ref point to the previous position of this point(i.e. current position
  167. * `-dx`, `-dy`) and the line from the `ref` point to the current position of
  168. * this point.
  169. *
  170. * The function returns a positive value between `0` and `180` when the angle
  171. * (in the direction from previous position of this point to its current
  172. * position) is clockwise, and a negative value between `0` and `-180` when
  173. * the angle is counterclockwise.
  174. *
  175. * The function returns `0` if the previous and current positions of this
  176. * point are the same (i.e. both `dx` and `dy` are `0`).
  177. */
  178. changeInAngle(dx, dy, ref = new Point()) {
  179. // Revert the translation and measure the change in angle around x-axis.
  180. return this.clone().translate(-dx, -dy).theta(ref) - this.theta(ref);
  181. }
  182. /**
  183. * If the point lies outside the rectangle `rect`, adjust the point so that
  184. * it becomes the nearest point on the boundary of `rect`.
  185. */
  186. adhereToRect(rect) {
  187. if (!GeometryUtil.containsPoint(rect, this)) {
  188. this.x = Math.min(Math.max(this.x, rect.x), rect.x + rect.width);
  189. this.y = Math.min(Math.max(this.y, rect.y), rect.y + rect.height);
  190. }
  191. return this;
  192. }
  193. /**
  194. * Returns the bearing(cardinal direction) between me and the given point.
  195. *
  196. * @see https://en.wikipedia.org/wiki/Cardinal_direction
  197. */
  198. bearing(p) {
  199. const ref = Point.create(p);
  200. const lat1 = Angle.toRad(this.y);
  201. const lat2 = Angle.toRad(ref.y);
  202. const lon1 = this.x;
  203. const lon2 = ref.x;
  204. const dLon = Angle.toRad(lon2 - lon1);
  205. const y = Math.sin(dLon) * Math.cos(lat2);
  206. const x = Math.cos(lat1) * Math.sin(lat2) -
  207. Math.sin(lat1) * Math.cos(lat2) * Math.cos(dLon);
  208. const brng = Angle.toDeg(Math.atan2(y, x));
  209. const bearings = ['NE', 'E', 'SE', 'S', 'SW', 'W', 'NW', 'N'];
  210. let index = brng - 22.5;
  211. if (index < 0) {
  212. index += 360;
  213. }
  214. index = parseInt((index / 45), 10);
  215. return bearings[index];
  216. }
  217. /**
  218. * Returns the cross product of the vector from me to `p1` and the vector
  219. * from me to `p2`.
  220. *
  221. * The left-hand rule is used because the coordinate system is left-handed.
  222. */
  223. cross(p1, p2) {
  224. if (p1 != null && p2 != null) {
  225. const a = Point.create(p1);
  226. const b = Point.create(p2);
  227. return (b.x - this.x) * (a.y - this.y) - (b.y - this.y) * (a.x - this.x);
  228. }
  229. return NaN;
  230. }
  231. /**
  232. * Returns the dot product of this point with given other point.
  233. */
  234. dot(p) {
  235. const ref = Point.create(p);
  236. return this.x * ref.x + this.y * ref.y;
  237. }
  238. diff(dx, dy) {
  239. if (typeof dx === 'number') {
  240. return new Point(this.x - dx, this.y - dy);
  241. }
  242. const p = Point.create(dx);
  243. return new Point(this.x - p.x, this.y - p.y);
  244. }
  245. /**
  246. * Returns an interpolation between me and point `p` for a parametert in
  247. * the closed interval `[0, 1]`.
  248. */
  249. lerp(p, t) {
  250. const ref = Point.create(p);
  251. return new Point((1 - t) * this.x + t * ref.x, (1 - t) * this.y + t * ref.y);
  252. }
  253. /**
  254. * Normalize the point vector, scale the line segment between `(0, 0)`
  255. * and the point in order for it to have the given length. If length is
  256. * not specified, it is considered to be `1`; in that case, a unit vector
  257. * is computed.
  258. */
  259. normalize(length = 1) {
  260. const scale = length / this.magnitude();
  261. return this.scale(scale, scale);
  262. }
  263. /**
  264. * Moves this point along the line starting from `ref` to this point by a
  265. * certain `distance`.
  266. */
  267. move(ref, distance) {
  268. const p = Point.create(ref);
  269. const rad = Angle.toRad(p.theta(this));
  270. return this.translate(Math.cos(rad) * distance, -Math.sin(rad) * distance);
  271. }
  272. /**
  273. * Returns a point that is the reflection of me with the center of inversion
  274. * in `ref` point.
  275. */
  276. reflection(ref) {
  277. return Point.create(ref).move(this, this.distance(ref));
  278. }
  279. snapToGrid(gx, gy) {
  280. this.x = GeometryUtil.snapToGrid(this.x, gx);
  281. this.y = GeometryUtil.snapToGrid(this.y, gy == null ? gx : gy);
  282. return this;
  283. }
  284. equals(p) {
  285. const ref = Point.create(p);
  286. return ref != null && ref.x === this.x && ref.y === this.y;
  287. }
  288. clone() {
  289. return Point.clone(this);
  290. }
  291. /**
  292. * Returns the point as a simple JSON object. For example: `{ x: 0, y: 0 }`.
  293. */
  294. toJSON() {
  295. return Point.toJSON(this);
  296. }
  297. serialize() {
  298. return `${this.x} ${this.y}`;
  299. }
  300. }
  301. (function (Point) {
  302. function isPoint(instance) {
  303. return instance != null && instance instanceof Point;
  304. }
  305. Point.isPoint = isPoint;
  306. })(Point || (Point = {}));
  307. (function (Point) {
  308. function isPointLike(p) {
  309. return (p != null &&
  310. typeof p === 'object' &&
  311. typeof p.x === 'number' &&
  312. typeof p.y === 'number');
  313. }
  314. Point.isPointLike = isPointLike;
  315. function isPointData(p) {
  316. return (p != null &&
  317. Array.isArray(p) &&
  318. p.length === 2 &&
  319. typeof p[0] === 'number' &&
  320. typeof p[1] === 'number');
  321. }
  322. Point.isPointData = isPointData;
  323. })(Point || (Point = {}));
  324. (function (Point) {
  325. function create(x, y) {
  326. if (x == null || typeof x === 'number') {
  327. return new Point(x, y);
  328. }
  329. return clone(x);
  330. }
  331. Point.create = create;
  332. function clone(p) {
  333. if (Point.isPoint(p)) {
  334. return new Point(p.x, p.y);
  335. }
  336. if (Array.isArray(p)) {
  337. return new Point(p[0], p[1]);
  338. }
  339. return new Point(p.x, p.y);
  340. }
  341. Point.clone = clone;
  342. function toJSON(p) {
  343. if (Point.isPoint(p)) {
  344. return { x: p.x, y: p.y };
  345. }
  346. if (Array.isArray(p)) {
  347. return { x: p[0], y: p[1] };
  348. }
  349. return { x: p.x, y: p.y };
  350. }
  351. Point.toJSON = toJSON;
  352. /**
  353. * Returns a new Point object from the given polar coordinates.
  354. * @see http://en.wikipedia.org/wiki/Polar_coordinate_system
  355. */
  356. function fromPolar(r, rad, origin = new Point()) {
  357. let x = Math.abs(r * Math.cos(rad));
  358. let y = Math.abs(r * Math.sin(rad));
  359. const org = clone(origin);
  360. const deg = Angle.normalize(Angle.toDeg(rad));
  361. if (deg < 90) {
  362. y = -y;
  363. }
  364. else if (deg < 180) {
  365. x = -x;
  366. y = -y;
  367. }
  368. else if (deg < 270) {
  369. x = -x;
  370. }
  371. return new Point(org.x + x, org.y + y);
  372. }
  373. Point.fromPolar = fromPolar;
  374. /**
  375. * Converts rectangular to polar coordinates.
  376. */
  377. function toPolar(point, origin = new Point()) {
  378. const p = clone(point);
  379. const o = clone(origin);
  380. const dx = p.x - o.x;
  381. const dy = p.y - o.y;
  382. return new Point(Math.sqrt(dx * dx + dy * dy), // r
  383. Angle.toRad(o.theta(p)));
  384. }
  385. Point.toPolar = toPolar;
  386. function equals(p1, p2) {
  387. if (p1 === p2) {
  388. return true;
  389. }
  390. if (p1 != null && p2 != null) {
  391. return p1.x === p2.x && p1.y === p2.y;
  392. }
  393. return false;
  394. }
  395. Point.equals = equals;
  396. function equalPoints(p1, p2) {
  397. if ((p1 == null && p2 != null) ||
  398. (p1 != null && p2 == null) ||
  399. (p1 != null && p2 != null && p1.length !== p2.length)) {
  400. return false;
  401. }
  402. if (p1 != null && p2 != null) {
  403. for (let i = 0, ii = p1.length; i < ii; i += 1) {
  404. if (!equals(p1[i], p2[i])) {
  405. return false;
  406. }
  407. }
  408. }
  409. return true;
  410. }
  411. Point.equalPoints = equalPoints;
  412. /**
  413. * Returns a point with random coordinates that fall within the range
  414. * `[x1, x2]` and `[y1, y2]`.
  415. */
  416. function random(x1, x2, y1, y2) {
  417. return new Point(GeometryUtil.random(x1, x2), GeometryUtil.random(y1, y2));
  418. }
  419. Point.random = random;
  420. function rotate(point, angle, center) {
  421. const rad = Angle.toRad(Angle.normalize(-angle));
  422. const sin = Math.sin(rad);
  423. const cos = Math.cos(rad);
  424. return rotateEx(point, cos, sin, center);
  425. }
  426. Point.rotate = rotate;
  427. function rotateEx(point, cos, sin, center = new Point()) {
  428. const source = clone(point);
  429. const origin = clone(center);
  430. const dx = source.x - origin.x;
  431. const dy = source.y - origin.y;
  432. const x1 = dx * cos - dy * sin;
  433. const y1 = dy * cos + dx * sin;
  434. return new Point(x1 + origin.x, y1 + origin.y);
  435. }
  436. Point.rotateEx = rotateEx;
  437. })(Point || (Point = {}));
  438. //# sourceMappingURL=point.js.map