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