View Javadoc
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.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-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 DrawRuntimeException
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 DrawRuntimeException
225     {
226         double length = Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
227         Throw.when(length == 0.0, DrawRuntimeException.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 getBounds()
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 &pi; /
270      *         2; etc.). If 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
296      *            <code>null</code>; this method returns <code>null</code>; else if <code>true</code>; this method returns
297      *            (p1X,p1Y); else (lowLimitHandling is <code>false</code>); this method will return the closest point on the
298      *            line
299      * @param highLimitHandling controls the handling of results that lie beyond the second point of the line. If
300      *            <code>null</code>; this method returns <code>null</code>; else if <code>true</code>; this method returns
301      *            (p2X,p2Y); else (highLimitHandling is <code>false</code>); this method will return the closest point on the
302      *            line
303      * @return the closest point on the line after applying the indicated limit handling; so the result can be
304      *         <code>null</code>
305      * @throws ArithmeticException when any of the arguments is <code>NaN</code>
306      */
307     @SuppressWarnings("checkstyle:parameternumber")
308     public Point3d closestPointOnLine(final double p1X, final double p1Y, final double p1Z, final double p2X, final double p2Y,
309             final double p2Z, final Boolean lowLimitHandling, final Boolean highLimitHandling)
310     {
311         double fraction = fractionalPositionOnLine(p1X, p1Y, p1Z, p2X, p2Y, p2Z, lowLimitHandling, highLimitHandling);
312         if (Double.isNaN(fraction))
313         {
314             return null;
315         }
316         if (fraction == 1.0)
317         {
318             return new Point3d(p2X, p2Y, p2Z); // Maximize precision in case fraction == 1.0
319         }
320         return new Point3d(p1X + fraction * (p2X - p1X), p1Y + fraction * (p2Y - p1Y), p1Z + fraction * (p2Z - p1Z));
321     }
322 
323     /**
324      * Compute the fractional position of the closest point on a line with optional limiting of the result on either end. If the
325      * line has length 0; this method returns 0.0.
326      * @param p1X the x coordinate of the first point on the line
327      * @param p1Y the y coordinate of the first point on the line
328      * @param p1Z the z coordinate of the first point on the line
329      * @param p2X the x coordinate of the second point on the line
330      * @param p2Y the y coordinate of the second point on the line
331      * @param p2Z the z coordinate of the second point on the line
332      * @param lowLimitHandling controls handling of results that lie before the first point of the line. If null; this
333      *            method returns <code>NaN</code>; else if <code>true</code>; this method returns 0.0; else (lowLimitHandling is
334      *            false); this results &lt; 0.0 are returned
335      * @param highLimitHandling controls the handling of results that lie beyond the second point of the line. If null;
336      *            this method returns <code>NaN</code>; else if <code>true</code>; this method returns 1.0; else
337      *            (highLimitHandling is <code>false</code>); results &gt; 1.0 are returned
338      * @return the fractional position of the closest point on the line. Results within the range 0.0 .. 1.0 are always
339      *         returned as is.. A result &lt; 0.0 is subject to lowLimitHandling. A result &gt; 1.0 is subject to
340      *         highLimitHandling
341      * @throws DrawRuntimeException when any of the arguments is <code>NaN</code>
342      */
343     @SuppressWarnings("checkstyle:parameternumber")
344     public double fractionalPositionOnLine(final double p1X, final double p1Y, final double p1Z, final double p2X,
345             final double p2Y, final double p2Z, final Boolean lowLimitHandling, final Boolean highLimitHandling)
346     {
347         double dX = p2X - p1X;
348         double dY = p2Y - p1Y;
349         double dZ = p2Z - p1Z;
350         Throw.whenNaN(dX, "dX");
351         Throw.whenNaN(dY, "dY");
352         Throw.whenNaN(dZ, "dZ");
353         if (0 == dX && 0 == dY && 0 == dZ)
354         {
355             return 0.0;
356         }
357         double fraction = ((this.x - p1X) * dX + (this.y - p1Y) * dY + (this.z - p1Z) * dZ) / (dX * dX + dY * dY + dZ * dZ);
358         if (fraction < 0.0)
359         {
360             if (lowLimitHandling == null)
361             {
362                 return Double.NaN;
363             }
364             if (lowLimitHandling)
365             {
366                 fraction = 0.0;
367             }
368         }
369         else if (fraction > 1.0)
370         {
371             if (highLimitHandling == null)
372             {
373                 return Double.NaN;
374             }
375             if (highLimitHandling)
376             {
377                 fraction = 1.0;
378             }
379         }
380         return fraction;
381     }
382 
383     /**
384      * Project a point on a line segment. If the the projected points lies outside the line segment, the nearest end point of
385      * the line segment is returned. Otherwise the returned point lies between the end points of the line segment. <br>
386      * Adapted from <a href="http://paulbourke.net/geometry/pointlineplane/DistancePoint.java">example code provided by Paul
387      * Bourke</a>.
388      * @param p1X the x coordinate of the start point of the line segment
389      * @param p1Y the y coordinate of the start point of the line segment
390      * @param p1Z the z coordinate of the start point of the line segment
391      * @param p2X the x coordinate of the end point of the line segment
392      * @param p2Y the y coordinate of the end point of the line segment
393      * @param p2Z the y coordinate of the end point of the line segment
394      * @return either <code>segmentPoint1</code>, or <code>segmentPoint2</code> or a new Point2d that lies somewhere in
395      *         between those two.
396      * @throws ArithmeticException when any of the parameters is <code>NaN</code>
397      */
398     public final Point3d closestPointOnSegment(final double p1X, final double p1Y, final double p1Z, final double p2X,
399             final double p2Y, final double p2Z)
400     {
401         return closestPointOnLine(p1X, p1Y, p1Z, p2X, p2Y, p2Z, true, true);
402     }
403 
404     @Override
405     public final Point3d closestPointOnLine(final Point3d linePoint1, final Point3d linePoint2)
406     {
407         Throw.whenNull(linePoint1, "linePoint1");
408         Throw.whenNull(linePoint2, "linePoint2");
409         return closestPointOnLine(linePoint1.x, linePoint1.y, linePoint1.z, linePoint2.x, linePoint2.y, linePoint2.z);
410     }
411 
412     /**
413      * Project a point on a line. <br>
414      * Adapted from <a href="http://paulbourke.net/geometry/pointlineplane/DistancePoint.java">example code provided by Paul
415      * Bourke</a>.
416      * @param p1X the x coordinate of a point of the line
417      * @param p1Y the y coordinate of a point of the line
418      * @param p1Z the z coordinate of a point on the line
419      * @param p2X the x coordinate of another point on the line
420      * @param p2Y the y coordinate of another point on the line
421      * @param p2Z the z coordinate of another point on the line
422      * @return a point on the line that goes through the points
423      * @throws DrawRuntimeException when the points on the line are identical
424      */
425     public final Point3d closestPointOnLine(final double p1X, final double p1Y, final double p1Z, final double p2X,
426             final double p2Y, final double p2Z)
427     {
428         Throw.when(p1X == p2X && p1Y == p2Y && p1Z == p2Z, DrawRuntimeException.class, "degenerate line not allowed");
429         return closestPointOnLine(p1X, p1Y, p1Z, p2X, p2Y, p2Z, false, false);
430     }
431 
432     /**
433      * Return the direction of the point in radians with respect to the origin, ignoring the z-coordinate.
434      * @return the direction of the projection of the point in the x-y plane with respect to the origin, in radians
435      */
436     final double horizontalDirection()
437     {
438         return Math.atan2(this.y, this.x);
439     }
440 
441     /**
442      * Return the direction to another point, in radians, ignoring the z-coordinate.
443      * @param otherPoint the other point
444      * @return the direction of the projection of the point in the x-y plane to another point, in radians
445      * @throws NullPointerException when <code>otherPoint</code> is <code>null</code>
446      */
447     final double horizontalDirection(final Point3d otherPoint)
448     {
449         Throw.whenNull(otherPoint, "otherPoint");
450         return Math.atan2(otherPoint.y - this.y, otherPoint.x - this.x);
451     }
452 
453     /**
454      * Return the direction with respect to the Z axis to another point, in radians.
455      * @param otherPoint the other point
456      * @return the direction with respect to the Z axis to another point, in radians
457      * @throws NullPointerException when <code>otherPoint</code> is <code>null</code>
458      */
459     final double verticalDirection(final Point3d otherPoint)
460     {
461         Throw.whenNull(otherPoint, "otherPoint");
462         return Math.atan2(Math.hypot(otherPoint.y - this.y, otherPoint.x - this.x), otherPoint.z - this.z);
463     }
464 
465     /**
466      * Return the squared distance between the coordinates of this point and the provided point, ignoring the z-coordinate.
467      * @param otherPoint the other point
468      * @return the squared distance between this point and the other point, ignoring the z-coordinate
469      * @throws NullPointerException when <code>otherPoint</code> is <code>null</code>
470      */
471     final double horizontalDistanceSquared(final Point3d otherPoint)
472     {
473         Throw.whenNull(otherPoint, "otherPoint");
474         double dx = this.x - otherPoint.x;
475         double dy = this.y - otherPoint.y;
476         return dx * dx + dy * dy;
477     }
478 
479     /**
480      * Return the Euclidean distance between this point and the provided point, ignoring the z-coordinate.
481      * @param otherPoint the other point
482      * @return the Euclidean distance between this point and the other point, ignoring the z-coordinate
483      * @throws NullPointerException when <code>otherPoint</code> is <code>null</code>
484      */
485     final double horizontalDistance(final Point3d otherPoint)
486     {
487         return Math.sqrt(horizontalDistanceSquared(otherPoint));
488     }
489 
490     @Override
491     @SuppressWarnings("checkstyle:designforextension")
492     public String toString()
493     {
494         return toString("%f");
495     }
496 
497     @Override
498     public String toString(final String doubleFormat, final boolean doNotIncludeClassName)
499     {
500         String format = String.format("%1$s[x=%2$s, y=%2$s, z=%2$s]", doNotIncludeClassName ? "" : "Point3d ", doubleFormat);
501         return String.format(Locale.US, format, this.x, this.y, this.z);
502     }
503 
504     @Override
505     public int hashCode()
506     {
507         final int prime = 31;
508         int result = 1;
509         long temp;
510         temp = Double.doubleToLongBits(this.x);
511         result = prime * result + (int) (temp ^ (temp >>> 32));
512         temp = Double.doubleToLongBits(this.y);
513         result = prime * result + (int) (temp ^ (temp >>> 32));
514         temp = Double.doubleToLongBits(this.z);
515         result = prime * result + (int) (temp ^ (temp >>> 32));
516         return result;
517     }
518 
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 }