1 package org.djutils.draw.point;
2
3 import java.awt.geom.Point2D;
4 import java.util.Arrays;
5 import java.util.Iterator;
6 import java.util.Locale;
7
8 import org.djutils.draw.DrawRuntimeException;
9 import org.djutils.draw.Drawable3d;
10 import org.djutils.draw.bounds.Bounds3d;
11 import org.djutils.exceptions.Throw;
12
13 /**
14 * A Point3d is an immutable point with an x, y, and z coordinate, stored with double precision. It differs from many Point
15 * implementations by being immutable.
16 * <p>
17 * Copyright (c) 2020-2023 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
18 * BSD-style license. See <a href="https://djutils.org/docs/current/djutils/licenses.html">DJUTILS License</a>.
19 * </p>
20 * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
21 * @author <a href="https://www.tudelft.nl/pknoppers">Peter Knoppers</a>
22 */
23 public class Point3d implements Drawable3d, Point<Point3d>
24 {
25 /** */
26 private static final long serialVersionUID = 20201201L;
27
28 /** The x-coordinate. */
29 @SuppressWarnings("checkstyle:visibilitymodifier")
30 public final double x;
31
32 /** The y-coordinate. */
33 @SuppressWarnings("checkstyle:visibilitymodifier")
34 public final double y;
35
36 /** The z-coordinate. */
37 @SuppressWarnings("checkstyle:visibilitymodifier")
38 public final double z;
39
40 /**
41 * Create a new Point with just an x, y and z coordinate, stored with double precision.
42 * @param x double; the x coordinate
43 * @param y double; the y coordinate
44 * @param z double; the z coordinate
45 * @throws IllegalArgumentException when x or y is NaN
46 */
47 public Point3d(final double x, final double y, final double z)
48 {
49 Throw.when(Double.isNaN(x) || Double.isNaN(y) || Double.isNaN(z), IllegalArgumentException.class,
50 "Coordinate must be a number (not NaN)");
51 this.x = x;
52 this.y = y;
53 this.z = z;
54 }
55
56 /**
57 * Create a new Point with just an x, y and z coordinate, stored with double precision.
58 * @param xyz double[]; the x, y and z coordinate
59 * @throws NullPointerException when xyz is null
60 * @throws IllegalArgumentException when the dimension of xyz is not 3, or a coordinate is NaN
61 */
62 public Point3d(final double[] xyz) throws NullPointerException, IllegalArgumentException
63 {
64 this(checkLengthIsThree(Throw.whenNull(xyz, "xyz-point cannot be null"))[0], xyz[1], xyz[2]);
65 }
66
67 /**
68 * Create an immutable point with just three values, x, y and z, stored with double precision from a Point2d and z.
69 * @param point Point2d; a Point2d
70 * @param z double; the z coordinate
71 * @throws NullPointerException when point is null
72 * @throws IllegalArgumentException when z is NaN
73 */
74 public Point3d(final Point2d point, final double z) throws NullPointerException, IllegalArgumentException
75 {
76 Throw.whenNull(point, "point cannot be null");
77 Throw.when(Double.isNaN(z), IllegalArgumentException.class, "Coordinate must be a number (not NaN)");
78 this.x = point.x;
79 this.y = point.y;
80 this.z = z;
81 }
82
83 /**
84 * Create an immutable point with just three values, x, y and z, stored with double precision from an AWT Point2D and z.
85 * @param point Point2D; an AWT Point2D
86 * @param z double; the z coordinate
87 * @throws NullPointerException when point is null
88 * @throws IllegalArgumentException when point has a NaN coordinate, or z is NaN
89 */
90 public Point3d(final Point2D point, final double z) throws NullPointerException, IllegalArgumentException
91 {
92 Throw.whenNull(point, "point cannot be null");
93 Throw.when(Double.isNaN(point.getX()) || Double.isNaN(point.getY()), IllegalArgumentException.class,
94 "Coordinate must be a number (not NaN)");
95 Throw.when(Double.isNaN(z), IllegalArgumentException.class, "Coordinate must be a number (not NaN)");
96 this.x = point.getX();
97 this.y = point.getY();
98 this.z = z;
99 }
100
101 /**
102 * Throw an IllegalArgumentException if the length of the provided array is not three.
103 * @param xyz double[]; the provided array
104 * @return double[]; the provided array
105 * @throws IllegalArgumentException when length of xyz is not three
106 */
107 private static double[] checkLengthIsThree(final double[] xyz) throws IllegalArgumentException
108 {
109 Throw.when(xyz.length != 3, IllegalArgumentException.class, "Length of xy-array must be 2");
110 return xyz;
111 }
112
113 /** {@inheritDoc} */
114 @Override
115 public final double getX()
116 {
117 return this.x;
118 }
119
120 /** {@inheritDoc} */
121 @Override
122 public final double getY()
123 {
124 return this.y;
125 }
126
127 /**
128 * Return the z-coordinate.
129 * @return double; the z-coordinate
130 */
131 public final double getZ()
132 {
133 return this.z;
134 }
135
136 /** {@inheritDoc} */
137 @Override
138 public double distanceSquared(final Point3d otherPoint) throws NullPointerException
139 {
140 Throw.whenNull(otherPoint, "point cannot be null");
141 double dx = this.x - otherPoint.x;
142 double dy = this.y - otherPoint.y;
143 double dz = this.z - otherPoint.z;
144 return dx * dx + dy * dy + dz * dz;
145 }
146
147 /** {@inheritDoc} */
148 @Override
149 public double distance(final Point3d otherPoint) throws NullPointerException
150 {
151 Throw.whenNull(otherPoint, "point cannot be null");
152 return Math.sqrt(distanceSquared(otherPoint));
153 }
154
155 /** {@inheritDoc} */
156 @Override
157 public int size()
158 {
159 return 1;
160 }
161
162 /** {@inheritDoc} */
163 @Override
164 public Iterator<? extends Point3d> getPoints()
165 {
166 return Arrays.stream(new Point3d[] {this}).iterator();
167 }
168
169 /** {@inheritDoc} */
170 @Override
171 public Point2d project() throws DrawRuntimeException
172 {
173 return new Point2d(this.x, this.y);
174 }
175
176 /**
177 * Return a new Point with a translation by the provided dx and dy.
178 * @param dx double; the horizontal translation
179 * @param dy double; the vertical translation
180 * @return Point3D; a new point with the translated coordinates
181 * @throws IllegalArgumentException when dx, or dy is NaN
182 */
183 public Point3d translate(final double dx, final double dy) throws IllegalArgumentException
184 {
185 Throw.when(Double.isNaN(dx) || Double.isNaN(dy), IllegalArgumentException.class,
186 "Translation must be number (not NaN)");
187 return new Point3d(this.x + dx, this.y + dy, this.z);
188 }
189
190 /**
191 * Return a new Point3d with a translation by the provided dx, dy and dz.
192 * @param dx double; the x translation
193 * @param dy double; the y translation
194 * @param dz double; the z translation
195 * @return Point3d; a new point with the translated coordinates
196 * @throws IllegalArgumentException when dx, dy, or dz is NaN
197 */
198 public Point3d translate(final double dx, final double dy, final double dz) throws IllegalArgumentException
199 {
200 Throw.when(Double.isNaN(dx) || Double.isNaN(dy) || Double.isNaN(dz), IllegalArgumentException.class,
201 "dx, dy and dz must be numbers (not NaN)");
202 return new Point3d(this.x + dx, this.y + dy, this.z + dz);
203 }
204
205 /** {@inheritDoc} */
206 @Override
207 public Point3d scale(final double factor) throws IllegalArgumentException
208 {
209 Throw.when(Double.isNaN(factor), IllegalArgumentException.class, "factor must be a number (not NaN)");
210 return new Point3d(this.x * factor, this.y * factor, this.z * factor);
211 }
212
213 /** {@inheritDoc} */
214 @Override
215 public Point3d neg()
216 {
217 return scale(-1.0);
218 }
219
220 /** {@inheritDoc} */
221 @Override
222 public Point3d abs()
223 {
224 return new Point3d(Math.abs(this.x), Math.abs(this.y), Math.abs(this.z));
225 }
226
227 /** {@inheritDoc} */
228 @Override
229 public Point3d normalize() throws DrawRuntimeException
230 {
231 double length = Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
232 Throw.when(length == 0.0, DrawRuntimeException.class, "cannot normalize (0.0, 0.0, 0.0)");
233 return this.scale(1.0 / length);
234 }
235
236 /** {@inheritDoc} */
237 @Override
238 public Point3d interpolate(final Point3d point, final double fraction)
239 {
240 Throw.whenNull(point, "point cannot be null");
241 Throw.when(Double.isNaN(fraction), IllegalArgumentException.class, "fraction must be a number (not NaN)");
242 return new Point3d((1.0 - fraction) * this.x + fraction * point.x, (1.0 - fraction) * this.y + fraction * point.y,
243 (1.0 - fraction) * this.z + fraction * point.z);
244
245 }
246
247 /** {@inheritDoc} */
248 @Override
249 public boolean epsilonEquals(final Point3d other, final double epsilon)
250 {
251 Throw.whenNull(other, "other point cannot be null");
252 if (Math.abs(this.x - other.x) > epsilon)
253 {
254 return false;
255 }
256 if (Math.abs(this.y - other.y) > epsilon)
257 {
258 return false;
259 }
260 if (Math.abs(this.z - other.z) > epsilon)
261 {
262 return false;
263 }
264 return true;
265 }
266
267 /** {@inheritDoc} */
268 @Override
269 public Bounds3d getBounds()
270 {
271 return new Bounds3d(this);
272 }
273
274 /** {@inheritDoc} */
275 @Override
276 public final Point3d closestPointOnSegment(final Point3d segmentPoint1, final Point3d segmentPoint2)
277 {
278 Throw.whenNull(segmentPoint1, "segmentPoint1 may not be null");
279 Throw.whenNull(segmentPoint2, "segmentPoint2 may not be null");
280 return closestPointOnSegment(segmentPoint1.x, segmentPoint1.y, segmentPoint1.z, segmentPoint2.x, segmentPoint2.y,
281 segmentPoint2.z);
282 }
283
284 /**
285 * Compute the closest point on a line with optional limiting of the result on either end.
286 * @param p1X double; the x coordinate of the first point on the line
287 * @param p1Y double; the y coordinate of the first point on the line
288 * @param p1Z double; the z coordinate of the first point on the line
289 * @param p2X double; the x coordinate of the second point on the line
290 * @param p2Y double; the y coordinate of the second point on the line
291 * @param p2Z double; the z coordinate of the second point on the line
292 * @param lowLimitHandling Boolean; controls handling of results that lie before the first point of the line. If null; this
293 * method returns null; else if true; this method returns (p1X,p1Y); else (lowLimitHandling is false); this
294 * method will return the closest point on the line
295 * @param highLimitHandling Boolean; controls the handling of results that lie beyond the second point of the line. If null;
296 * this method returns null; else if true; this method returns (p2X,p2Y); else (highLimitHandling is false); this
297 * method will return the closest point on the line
298 * @return Point3d; the closest point on the line after applying the indicated limit handling; so the result can be null
299 * @throws DrawRuntimeException when any of the arguments is NaN
300 */
301 @SuppressWarnings("checkstyle:parameternumber")
302 public Point3d closestPointOnLine(final double p1X, final double p1Y, final double p1Z, final double p2X, final double p2Y,
303 final double p2Z, final Boolean lowLimitHandling, final Boolean highLimitHandling) throws DrawRuntimeException
304 {
305 double fraction = fractionalPositionOnLine(p1X, p1Y, p1Z, p2X, p2Y, p2Z, lowLimitHandling, highLimitHandling);
306 if (Double.isNaN(fraction))
307 {
308 return null;
309 }
310 if (fraction == 1.0)
311 {
312 return new Point3d(p2X, p2Y, p2Z); // Maximize precision in case fraction == 1.0
313 }
314 return new Point3d(p1X + fraction * (p2X - p1X), p1Y + fraction * (p2Y - p1Y), p1Z + fraction * (p2Z - p1Z));
315 }
316
317 /**
318 * Compute the fractional position of the closest point on a line with optional limiting of the result on either end. If the
319 * line has length 0; this method returns 0.0.
320 * @param p1X double; the x coordinate of the first point on the line
321 * @param p1Y double; the y coordinate of the first point on the line
322 * @param p1Z double; the z coordinate of the first point on the line
323 * @param p2X double; the x coordinate of the second point on the line
324 * @param p2Y double; the y coordinate of the second point on the line
325 * @param p2Z double; the z coordinate of the second point on the line
326 * @param lowLimitHandling Boolean; controls handling of results that lie before the first point of the line. If null; this
327 * method returns NaN; else if true; this method returns 0.0; else (lowLimitHandling is false); this results <
328 * 0.0 are returned
329 * @param highLimitHandling Boolean; controls the handling of results that lie beyond the second point of the line. If null;
330 * this method returns NaN; else if true; this method returns 1.0; else (highLimitHandling is false); results
331 * > 1.0 are returned
332 * @return double; the fractional position of the closest point on the line. Results within the range 0.0 .. 1.0 are always
333 * returned as is.. A result < 0.0 is subject to lowLimitHandling. A result > 1.0 is subject to
334 * highLimitHandling
335 * @throws DrawRuntimeException when any of the arguments is NaN
336 */
337 @SuppressWarnings("checkstyle:parameternumber")
338 public double fractionalPositionOnLine(final double p1X, final double p1Y, final double p1Z, final double p2X,
339 final double p2Y, final double p2Z, final Boolean lowLimitHandling, final Boolean highLimitHandling)
340 throws DrawRuntimeException
341 {
342 double dX = p2X - p1X;
343 double dY = p2Y - p1Y;
344 double dZ = p2Z - p1Z;
345 Throw.when(Double.isNaN(dX) || Double.isNaN(dY) || Double.isNaN(dZ), DrawRuntimeException.class,
346 "NaN values not permitted");
347 if (0 == dX && 0 == dY && 0 == dZ)
348 {
349 return 0.0;
350 }
351 double fraction = ((this.x - p1X) * dX + (this.y - p1Y) * dY + (this.z - p1Z) * dZ) / (dX * dX + dY * dY + dZ * dZ);
352 if (fraction < 0.0)
353 {
354 if (lowLimitHandling == null)
355 {
356 return Double.NaN;
357 }
358 if (lowLimitHandling)
359 {
360 fraction = 0.0;
361 }
362 }
363 else if (fraction > 1.0)
364 {
365 if (highLimitHandling == null)
366 {
367 return Double.NaN;
368 }
369 if (highLimitHandling)
370 {
371 fraction = 1.0;
372 }
373 }
374 return fraction;
375 }
376
377 /**
378 * Project a point on a line segment. If the the projected points lies outside the line segment, the nearest end point of
379 * the line segment is returned. Otherwise the returned point lies between the end points of the line segment. <br>
380 * Adapted from <a href="http://paulbourke.net/geometry/pointlineplane/DistancePoint.java">example code provided by Paul
381 * Bourke</a>.
382 * @param p1X double; the x coordinate of the start point of the line segment
383 * @param p1Y double; the y coordinate of the start point of the line segment
384 * @param p1Z double; the z coordinate of the start point of the line segment
385 * @param p2X double; the x coordinate of the end point of the line segment
386 * @param p2Y double; the y coordinate of the end point of the line segment
387 * @param p2Z double; the y coordinate of the end point of the line segment
388 * @return P; either <cite>segmentPoint1</cite>, or <cite>segmentPoint2</cite> or a new Point2d that lies somewhere in
389 * between those two.
390 * @throws DrawRuntimeException when any of the parameters is NaN
391 */
392 public final Point3d closestPointOnSegment(final double p1X, final double p1Y, final double p1Z, final double p2X,
393 final double p2Y, final double p2Z) throws DrawRuntimeException
394 {
395 return closestPointOnLine(p1X, p1Y, p1Z, p2X, p2Y, p2Z, true, true);
396 }
397
398 /** {@inheritDoc} */
399 @Override
400 public final Point3d closestPointOnLine(final Point3d linePoint1, final Point3d linePoint2)
401 throws NullPointerException, DrawRuntimeException
402 {
403 Throw.whenNull(linePoint1, "linePoint1 may not be null");
404 Throw.whenNull(linePoint2, "linePoint2 may not be null");
405 return closestPointOnLine(linePoint1.x, linePoint1.y, linePoint1.z, linePoint2.x, linePoint2.y, linePoint2.z);
406 }
407
408 /**
409 * Project a point on a line. <br>
410 * Adapted from <a href="http://paulbourke.net/geometry/pointlineplane/DistancePoint.java">example code provided by Paul
411 * Bourke</a>.
412 * @param p1X double; the x coordinate of a point of the line
413 * @param p1Y double; the y coordinate of a point of the line
414 * @param p1Z double; the z coordinate of a point on the line
415 * @param p2X double; the x coordinate of another point on the line
416 * @param p2Y double; the y coordinate of another point on the line
417 * @param p2Z double; the z coordinate of another point on the line
418 * @return Point3d; a point on the line that goes through the points
419 * @throws DrawRuntimeException when the points on the line are identical
420 */
421 public final Point3d closestPointOnLine(final double p1X, final double p1Y, final double p1Z, final double p2X,
422 final double p2Y, final double p2Z) throws DrawRuntimeException
423 {
424 Throw.when(p1X == p2X && p1Y == p2Y && p1Z == p2Z, DrawRuntimeException.class, "degenerate line not allowed");
425 return closestPointOnLine(p1X, p1Y, p1Z, p2X, p2Y, p2Z, false, false);
426 }
427
428 /**
429 * Return the direction of the point in radians with respect to the origin, ignoring the z-coordinate.
430 * @return double; the direction of the projection of the point in the x-y plane with respect to the origin, in radians
431 */
432 final double horizontalDirection()
433 {
434 return Math.atan2(this.y, this.x);
435 }
436
437 /**
438 * Return the direction to another point, in radians, ignoring the z-coordinate.
439 * @param point Point3d; the other point
440 * @return double; the direction of the projection of the point in the x-y plane to another point, in radians
441 * @throws NullPointerException when <code>point</code> is null
442 */
443 final double horizontalDirection(final Point3d point) throws NullPointerException
444 {
445 Throw.whenNull(point, "point cannot be null");
446 return Math.atan2(point.y - this.y, point.x - this.x);
447 }
448
449 /**
450 * Return the direction with respect to the Z axis to another point, in radians.
451 * @param point Point3d; the other point
452 * @return double; the direction with respect to the Z axis to another point, in radians
453 * @throws NullPointerException when <code>point</code> is null
454 */
455 final double verticalDirection(final Point3d point) throws NullPointerException
456 {
457 Throw.whenNull(point, "point cannot be null");
458 return Math.atan2(Math.hypot(point.y - this.y, point.x - this.x), point.z - this.z);
459 }
460
461 /**
462 * Return the squared distance between the coordinates of this point and the provided point, ignoring the z-coordinate.
463 * @param point Point3d; the other point
464 * @return double; the squared distance between this point and the other point, ignoring the z-coordinate
465 * @throws NullPointerException when point is null
466 */
467 final double horizontalDistanceSquared(final Point3d point)
468 {
469 Throw.whenNull(point, "point cannot be null");
470 double dx = this.x - point.x;
471 double dy = this.y - point.y;
472 return dx * dx + dy * dy;
473 }
474
475 /**
476 * Return the Euclidean distance between this point and the provided point, ignoring the z-coordinate.
477 * @param point Point3d; the other point
478 * @return double; the Euclidean distance between this point and the other point, ignoring the z-coordinate
479 * @throws NullPointerException when point is null
480 */
481 final double horizontalDistance(final Point3d point)
482 {
483 return Math.sqrt(horizontalDistanceSquared(point));
484 }
485
486 /** {@inheritDoc} */
487 @Override
488 @SuppressWarnings("checkstyle:designforextension")
489 public String toString()
490 {
491 return toString("%f");
492 }
493
494 /** {@inheritDoc} */
495 @Override
496 public String toString(final String doubleFormat, final boolean doNotIncludeClassName)
497 {
498 String format = String.format("%1$s[x=%2$s, y=%2$s, z=%2$s]", doNotIncludeClassName ? "" : "Point3d ", doubleFormat);
499 return String.format(Locale.US, format, this.x, this.y, this.z);
500 }
501
502 /** {@inheritDoc} */
503 @Override
504 public int hashCode()
505 {
506 final int prime = 31;
507 int result = 1;
508 long temp;
509 temp = Double.doubleToLongBits(this.x);
510 result = prime * result + (int) (temp ^ (temp >>> 32));
511 temp = Double.doubleToLongBits(this.y);
512 result = prime * result + (int) (temp ^ (temp >>> 32));
513 temp = Double.doubleToLongBits(this.z);
514 result = prime * result + (int) (temp ^ (temp >>> 32));
515 return result;
516 }
517
518 /** {@inheritDoc} */
519 @SuppressWarnings("checkstyle:needbraces")
520 @Override
521 public boolean equals(final Object obj)
522 {
523 if (this == obj)
524 return true;
525 if (obj == null)
526 return false;
527 if (getClass() != obj.getClass())
528 return false;
529 Point3d other = (Point3d) obj;
530 if (Double.doubleToLongBits(this.x) != Double.doubleToLongBits(other.x))
531 return false;
532 if (Double.doubleToLongBits(this.y) != Double.doubleToLongBits(other.y))
533 return false;
534 if (Double.doubleToLongBits(this.z) != Double.doubleToLongBits(other.z))
535 return false;
536 return true;
537 }
538
539 }