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