1 package org.djutils.draw.point;
2
3 import java.awt.geom.Point2D;
4 import java.util.ArrayList;
5 import java.util.Arrays;
6 import java.util.Iterator;
7 import java.util.List;
8 import java.util.Locale;
9
10 import org.djutils.draw.DrawRuntimeException;
11 import org.djutils.draw.Drawable2d;
12 import org.djutils.draw.bounds.Bounds2d;
13 import org.djutils.exceptions.Throw;
14
15
16
17
18
19
20
21
22
23
24
25 public class Point2d implements Drawable2d, Point<Point2d>
26 {
27
28 private static final long serialVersionUID = 20201201L;
29
30
31 @SuppressWarnings("checkstyle:visibilitymodifier")
32 public final double x;
33
34
35 @SuppressWarnings("checkstyle:visibilitymodifier")
36 public final double y;
37
38
39
40
41
42
43
44 public Point2d(final double x, final double y) throws IllegalArgumentException
45 {
46 Throw.when(Double.isNaN(x) || Double.isNaN(y), IllegalArgumentException.class, "Coordinate must be a number (not NaN)");
47 this.x = x;
48 this.y = y;
49 }
50
51
52
53
54
55
56
57 public Point2d(final double[] xy) throws NullPointerException, IllegalArgumentException
58 {
59 this(checkLengthIsTwo(Throw.whenNull(xy, "xy-point cannot be null"))[0], xy[1]);
60 }
61
62
63
64
65
66
67
68 public Point2d(final Point2D point) throws NullPointerException, IllegalArgumentException
69 {
70 Throw.whenNull(point, "point cannot be null");
71 Throw.when(Double.isNaN(point.getX()) || Double.isNaN(point.getY()), IllegalArgumentException.class,
72 "Coordinate must be a number (not NaN)");
73 this.x = point.getX();
74 this.y = point.getY();
75 }
76
77
78
79
80
81
82
83 private static double[] checkLengthIsTwo(final double[] xy) throws IllegalArgumentException
84 {
85 Throw.when(xy.length != 2, IllegalArgumentException.class, "Length of xy-array must be 2");
86 return xy;
87 }
88
89
90 @Override
91 public final double getX()
92 {
93 return this.x;
94 }
95
96
97 @Override
98 public final double getY()
99 {
100 return this.y;
101 }
102
103
104 @Override
105 public double distance(final Point2d otherPoint)
106 {
107 Throw.whenNull(otherPoint, "point cannot be null");
108 return Math.hypot(otherPoint.x - this.x, otherPoint.y - this.y);
109 }
110
111
112 @Override
113 public double distanceSquared(final Point2d otherPoint) throws NullPointerException
114 {
115 Throw.whenNull(otherPoint, "point cannot be null");
116 double dx = this.x - otherPoint.x;
117 double dy = this.y - otherPoint.y;
118 return dx * dx + dy * dy;
119 }
120
121
122 @Override
123 public int size()
124 {
125 return 1;
126 }
127
128
129 @Override
130 public Iterator<? extends Point2d> getPoints()
131 {
132 return Arrays.stream(new Point2d[] { this }).iterator();
133 }
134
135
136
137
138
139
140
141
142 public Point2d translate(final double dx, final double dy)
143 {
144 Throw.when(Double.isNaN(dx) || Double.isNaN(dy), IllegalArgumentException.class, "translation may not be NaN");
145 return new Point2d(this.x + dx, this.y + dy);
146 }
147
148
149
150
151
152
153
154
155
156
157 public Point3d translate(final double dx, final double dy, final double dz)
158 {
159 Throw.when(Double.isNaN(dx) || Double.isNaN(dy) || Double.isNaN(dz), IllegalArgumentException.class,
160 "translation may not be NaN");
161 return new Point3d(this.x + dx, this.y + dy, dz);
162 }
163
164
165 @Override
166 public Point2d scale(final double factor)
167 {
168 Throw.when(Double.isNaN(factor), IllegalArgumentException.class, "factor must be a number (not NaN)");
169 return new Point2d(this.x * factor, this.y * factor);
170 }
171
172
173 @Override
174 public Point2d neg()
175 {
176 return scale(-1.0);
177 }
178
179
180 @Override
181 public Point2d abs()
182 {
183 return new Point2d(Math.abs(this.x), Math.abs(this.y));
184 }
185
186
187 @Override
188 public Point2d normalize() throws DrawRuntimeException
189 {
190 double length = Math.sqrt(this.x * this.x + this.y * this.y);
191 Throw.when(length == 0.0, DrawRuntimeException.class, "cannot normalize (0.0, 0.0)");
192 return this.scale(1.0 / length);
193 }
194
195
196 @Override
197 public Point2d interpolate(final Point2d point, final double fraction)
198 {
199 Throw.whenNull(point, "point cannot be null");
200 Throw.when(Double.isNaN(fraction), IllegalArgumentException.class, "fraction must be a number (not NaN)");
201 return new Point2d((1.0 - fraction) * this.x + fraction * point.x, (1.0 - fraction) * this.y + fraction * point.y);
202 }
203
204
205 @Override
206 public boolean epsilonEquals(final Point2d other, final double epsilon)
207 {
208 Throw.whenNull(other, "other point cannot be null");
209 if (Math.abs(this.x - other.x) > epsilon)
210 {
211 return false;
212 }
213 if (Math.abs(this.y - other.y) > epsilon)
214 {
215 return false;
216 }
217 return true;
218 }
219
220
221 @Override
222 public Bounds2d getBounds()
223 {
224 return new Bounds2d(this);
225 }
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245 @SuppressWarnings("checkstyle:parameternumber")
246 public static Point2d intersectionOfLines(final double line1P1X, final double line1P1Y, final double line1P2X,
247 final double line1P2Y, final boolean lowLimitLine1, final boolean highLimitLine1, final double line2P1X,
248 final double line2P1Y, final double line2P2X, final double line2P2Y, final boolean lowLimitLine2,
249 final boolean highLimitLine2) throws DrawRuntimeException
250 {
251 double line1DX = line1P2X - line1P1X;
252 double line1DY = line1P2Y - line1P1Y;
253 double l2p1x = line2P1X - line1P1X;
254 double l2p1y = line2P1Y - line1P1Y;
255 double l2p2x = line2P2X - line1P1X;
256 double l2p2y = line2P2Y - line1P1Y;
257 double denominator = (l2p2y - l2p1y) * line1DX - (l2p2x - l2p1x) * line1DY;
258 Throw.when(Double.isNaN(denominator), DrawRuntimeException.class, "NaN value not permitted");
259 if (denominator == 0.0)
260 {
261 return null;
262 }
263 double uA = ((l2p2x - l2p1x) * (-l2p1y) - (l2p2y - l2p1y) * (-l2p1x)) / denominator;
264
265 if (uA < 0.0 && lowLimitLine1 || uA > 1.0 && highLimitLine1)
266 {
267 return null;
268 }
269 double uB = (line1DY * l2p1x - line1DX * l2p1y) / denominator;
270
271 if (uB < 0.0 && lowLimitLine2 || uB > 1.0 && highLimitLine2)
272 {
273 return null;
274 }
275 if (uA == 1.0)
276 {
277 return new Point2d(line1P2X, line1P2Y);
278 }
279 if (uB == 0.0)
280 {
281 return new Point2d(line2P1X, line2P1Y);
282 }
283 if (uB == 1.0)
284 {
285 return new Point2d(line2P2X, line2P2Y);
286 }
287 return new Point2d(line1P1X + uA * line1DX, line1P1Y + uA * line1DY);
288 }
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304 @SuppressWarnings("checkstyle:parameternumber")
305 public static Point2d intersectionOfLines(final double l1P1X, final double l1P1Y, final double l1P2X, final double l1P2Y,
306 final double l2P1X, final double l2P1Y, final double l2P2X, final double l2P2Y) throws DrawRuntimeException
307 {
308 return intersectionOfLines(l1P1X, l1P1Y, l1P2X, l1P2Y, false, false, l2P1X, l2P1Y, l2P2X, l2P2Y, false, false);
309 }
310
311
312
313
314
315
316
317
318
319
320
321 public static Point2d intersectionOfLines(final Point2d line1P1, final Point2d line1P2, final Point2d line2P1,
322 final Point2d line2P2) throws NullPointerException
323 {
324 Throw.when(line1P1 == null || line1P2 == null || line2P1 == null || line2P2 == null, NullPointerException.class,
325 "Points may not be null");
326 return intersectionOfLines(line1P1.x, line1P1.y, line1P2.x, line1P2.y, false, false, line2P1.x, line2P1.y, line2P2.x,
327 line2P2.y, false, false);
328 }
329
330
331
332
333
334
335
336
337
338
339
340
341
342 public static Point2d intersectionOfLineSegments(final Point2d line1P1, final Point2d line1P2, final Point2d line2P1,
343 final Point2d line2P2) throws NullPointerException, DrawRuntimeException
344 {
345 Throw.when(line1P1 == null || line1P2 == null || line2P1 == null || line2P2 == null, NullPointerException.class,
346 "Points may not be null");
347 return intersectionOfLines(line1P1.x, line1P1.y, line1P2.x, line1P2.y, true, true, line2P1.x, line2P1.y, line2P2.x,
348 line2P2.y, true, true);
349 }
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365 @SuppressWarnings("checkstyle:parameternumber")
366 public static Point2d intersectionOfLineSegments(final double line1P1X, final double line1P1Y, final double line1P2X,
367 final double line1P2Y, final double line2P1X, final double line2P1Y, final double line2P2X, final double line2P2Y)
368 throws DrawRuntimeException
369 {
370 return intersectionOfLines(line1P1X, line1P1Y, line1P2X, line1P2Y, true, true, line2P1X, line2P1Y, line2P2X, line2P2Y,
371 true, true);
372 }
373
374
375 @Override
376 public Point2d closestPointOnSegment(final Point2d segmentPoint1, final Point2d segmentPoint2)
377 {
378 Throw.whenNull(segmentPoint1, "linePoint1 may not be null");
379 Throw.whenNull(segmentPoint2, "linePoint2 may not be null");
380 return closestPointOnSegment(segmentPoint1.x, segmentPoint1.y, segmentPoint2.x, segmentPoint2.y);
381 }
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398 public Point2d closestPointOnLine(final double p1X, final double p1Y, final double p2X, final double p2Y,
399 final Boolean lowLimitHandling, final Boolean highLimitHandling) throws DrawRuntimeException
400 {
401 double fraction = fractionalPositionOnLine(p1X, p1Y, p2X, p2Y, lowLimitHandling, highLimitHandling);
402 if (Double.isNaN(fraction))
403 {
404 return null;
405 }
406 if (fraction == 1.0)
407 {
408 return new Point2d(p2X, p2Y);
409 }
410 return new Point2d(p1X + fraction * (p2X - p1X), p1Y + fraction * (p2Y - p1Y));
411 }
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431 public double fractionalPositionOnLine(final double p1X, final double p1Y, final double p2X, final double p2Y,
432 final Boolean lowLimitHandling, final Boolean highLimitHandling) throws DrawRuntimeException
433 {
434 double dX = p2X - p1X;
435 double dY = p2Y - p1Y;
436 Throw.when(Double.isNaN(dX) || Double.isNaN(dY), DrawRuntimeException.class, "NaN values not permitted");
437 if (0 == dX && 0 == dY)
438 {
439 return 0.0;
440 }
441 double fraction = ((this.x - p1X) * dX + (this.y - p1Y) * dY) / (dX * dX + dY * dY);
442 if (fraction < 0.0)
443 {
444 if (lowLimitHandling == null)
445 {
446 return Double.NaN;
447 }
448 if (lowLimitHandling)
449 {
450 fraction = 0.0;
451 }
452 }
453 else if (fraction > 1.0)
454 {
455 if (highLimitHandling == null)
456 {
457 return Double.NaN;
458 }
459 if (highLimitHandling)
460 {
461 fraction = 1.0;
462 }
463 }
464 return fraction;
465 }
466
467
468
469
470
471
472
473
474
475
476
477
478
479 public final Point2d closestPointOnSegment(final double p1X, final double p1Y, final double p2X, final double p2Y)
480 {
481 return closestPointOnLine(p1X, p1Y, p2X, p2Y, true, true);
482 }
483
484
485 @Override
486 public Point2d closestPointOnLine(final Point2d linePoint1, final Point2d linePoint2)
487 throws NullPointerException, DrawRuntimeException
488 {
489 Throw.whenNull(linePoint1, "linePoint1 may not be null");
490 Throw.whenNull(linePoint2, "linePoint2 may not be null");
491 return closestPointOnLine(linePoint1.x, linePoint1.y, linePoint2.x, linePoint2.y);
492 }
493
494
495
496
497
498
499
500
501
502
503
504
505 public final Point2d closestPointOnLine(final double p1X, final double p1Y, final double p2X, final double p2Y)
506 throws DrawRuntimeException
507 {
508 Throw.when(p1X == p2X && p1Y == p2Y, DrawRuntimeException.class, "degenerate line not allowed");
509 return closestPointOnLine(p1X, p1Y, p2X, p2Y, false, false);
510 }
511
512
513
514
515
516
517
518
519
520
521
522
523
524 public static final List<Point2d> circleIntersections(final Point2d center1, final double radius1, final Point2d center2,
525 final double radius2) throws NullPointerException, DrawRuntimeException
526 {
527 Throw.whenNull(center1, "center1 may not be null");
528 Throw.whenNull(center2, "center2 may not be null");
529 Throw.when(radius1 < 0 || radius2 < 0, DrawRuntimeException.class, "radius may not be less than 0");
530 Throw.when(center1.equals(center2) && radius1 == radius2, DrawRuntimeException.class, "Circles must be different");
531 List<Point2d> result = new ArrayList<>();
532
533 double dX = center2.x - center1.x;
534 double dY = center2.y - center1.y;
535 double distance = Math.hypot(dX, dY);
536 if (distance > radius1 + radius2 || distance < Math.abs(radius1 - radius2))
537 {
538 return result;
539 }
540 double a = (radius1 * radius1 - radius2 * radius2 + distance * distance) / (2 * distance);
541
542 double x2 = center1.x + (dX * a / distance);
543 double y2 = center1.y + (dY * a / distance);
544
545 double h = Math.sqrt(radius1 * radius1 - a * a);
546
547 double rX = -dY * (h / distance);
548 double rY = dX * (h / distance);
549 result.add(new Point2d(x2 + rX, y2 + rY));
550 if (h > 0)
551 {
552
553 result.add(new Point2d(x2 - rX, y2 - rY));
554 }
555 return result;
556 }
557
558
559
560
561
562
563
564 public double directionTo(final Point2d otherPoint)
565 {
566 return Math.atan2(otherPoint.y - this.y, otherPoint.x - this.x);
567 }
568
569
570
571
572
573 public Point2D toPoint2D()
574 {
575 return new Point2D.Double(this.x, this.y);
576 }
577
578
579 @Override
580 public String toString()
581 {
582 return toString("%f");
583 }
584
585
586 @Override
587 public String toString(final String doubleFormat, final boolean doNotIncludeClassName)
588 {
589 String format = String.format("%1$s[x=%2$s, y=%2$s]", doNotIncludeClassName ? "" : "Point2d ", doubleFormat);
590 return String.format(Locale.US, format, this.x, this.y);
591 }
592
593
594 @Override
595 public int hashCode()
596 {
597 final int prime = 31;
598 int result = 1;
599 long temp;
600 temp = Double.doubleToLongBits(this.x);
601 result = prime * result + (int) (temp ^ (temp >>> 32));
602 temp = Double.doubleToLongBits(this.y);
603 result = prime * result + (int) (temp ^ (temp >>> 32));
604 return result;
605 }
606
607
608 @SuppressWarnings("checkstyle:needbraces")
609 @Override
610 public boolean equals(final Object obj)
611 {
612 if (this == obj)
613 return true;
614 if (obj == null)
615 return false;
616 if (getClass() != obj.getClass())
617 return false;
618 Point2d other = (Point2d) obj;
619 if (Double.doubleToLongBits(this.x) != Double.doubleToLongBits(other.x))
620 return false;
621 if (Double.doubleToLongBits(this.y) != Double.doubleToLongBits(other.y))
622 return false;
623 return true;
624 }
625
626 }