View Javadoc
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-2024 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 from x, y and z coordinates provided as double arguments.
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 or z 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 Point3d from x, y and z coordinates provided as values in a double array.
58       * @param xyz double[]; the x, y and z coordinates
59       * @throws NullPointerException when xyz is null
60       * @throws IllegalArgumentException when the length 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"))[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 Point2d; a java.awt.geom.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");
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 from x, y obtained from a AWT Point2D and double 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");
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     @Override
114     public final double getX()
115     {
116         return this.x;
117     }
118 
119     @Override
120     public final double getY()
121     {
122         return this.y;
123     }
124 
125     /**
126      * Return the z-coordinate.
127      * @return double; the z-coordinate
128      */
129     public final double getZ()
130     {
131         return this.z;
132     }
133 
134     @Override
135     public double distanceSquared(final Point3d otherPoint) throws NullPointerException
136     {
137         Throw.whenNull(otherPoint, "otherPoint");
138         double dx = this.x - otherPoint.x;
139         double dy = this.y - otherPoint.y;
140         double dz = this.z - otherPoint.z;
141         return dx * dx + dy * dy + dz * dz;
142     }
143 
144     @Override
145     public double distance(final Point3d otherPoint) throws NullPointerException
146     {
147         Throw.whenNull(otherPoint, "otherPoint");
148         return Math.sqrt(distanceSquared(otherPoint));
149     }
150 
151     @Override
152     public int size()
153     {
154         return 1;
155     }
156 
157     @Override
158     public Iterator<? extends Point3d> getPoints()
159     {
160         return Arrays.stream(new Point3d[] {this}).iterator();
161     }
162 
163     @Override
164     public Point2d project() throws DrawRuntimeException
165     {
166         return new Point2d(this.x, this.y);
167     }
168 
169     /**
170      * Return a new Point3d with a translation by the provided dx and dy.
171      * @param dx double; the x translation
172      * @param dy double; the y translation
173      * @return Point3D; a new point with the translated coordinates
174      * @throws IllegalArgumentException when dx, or dy is NaN
175      */
176     public Point3d translate(final double dx, final double dy) throws IllegalArgumentException
177     {
178         Throw.when(Double.isNaN(dx) || Double.isNaN(dy), IllegalArgumentException.class,
179                 "Translation must be number (not NaN)");
180         return new Point3d(this.x + dx, this.y + dy, this.z);
181     }
182 
183     /**
184      * Return a new Point3d with a translation by the provided dx, dy and dz.
185      * @param dx double; the x translation
186      * @param dy double; the y translation
187      * @param dz double; the z translation
188      * @return Point3d; a new point with the translated coordinates
189      * @throws IllegalArgumentException when dx, dy, or dz is NaN
190      */
191     public Point3d translate(final double dx, final double dy, final double dz) throws IllegalArgumentException
192     {
193         Throw.when(Double.isNaN(dx) || Double.isNaN(dy) || Double.isNaN(dz), IllegalArgumentException.class,
194                 "dx, dy and dz must be numbers (not NaN)");
195         return new Point3d(this.x + dx, this.y + dy, this.z + dz);
196     }
197 
198     @Override
199     public Point3d scale(final double factor) throws IllegalArgumentException
200     {
201         Throw.when(Double.isNaN(factor), IllegalArgumentException.class, "factor must be a number (not NaN)");
202         return new Point3d(this.x * factor, this.y * factor, this.z * factor);
203     }
204 
205     @Override
206     public Point3d neg()
207     {
208         return scale(-1.0);
209     }
210 
211     @Override
212     public Point3d abs()
213     {
214         return new Point3d(Math.abs(this.x), Math.abs(this.y), Math.abs(this.z));
215     }
216 
217     @Override
218     public Point3d normalize() throws DrawRuntimeException
219     {
220         double length = Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
221         Throw.when(length == 0.0, DrawRuntimeException.class, "cannot normalize (0.0, 0.0, 0.0)");
222         return this.scale(1.0 / length);
223     }
224 
225     @Override
226     public Point3d interpolate(final Point3d point, final double fraction)
227     {
228         Throw.whenNull(point, "point");
229         Throw.when(Double.isNaN(fraction), IllegalArgumentException.class, "fraction must be a number (not NaN)");
230         return new Point3d((1.0 - fraction) * this.x + fraction * point.x, (1.0 - fraction) * this.y + fraction * point.y,
231                 (1.0 - fraction) * this.z + fraction * point.z);
232 
233     }
234 
235     @Override
236     public boolean epsilonEquals(final Point3d otherPoint, final double epsilon)
237     {
238         Throw.whenNull(otherPoint, "otherPoint");
239         if (Math.abs(this.x - otherPoint.x) > epsilon)
240         {
241             return false;
242         }
243         if (Math.abs(this.y - otherPoint.y) > epsilon)
244         {
245             return false;
246         }
247         if (Math.abs(this.z - otherPoint.z) > epsilon)
248         {
249             return false;
250         }
251         return true;
252     }
253 
254     @Override
255     public Bounds3d getBounds()
256     {
257         return new Bounds3d(this);
258     }
259 
260     @Override
261     public final Point3d closestPointOnSegment(final Point3d segmentPoint1, final Point3d segmentPoint2)
262     {
263         Throw.whenNull(segmentPoint1, "segmentPoint1");
264         Throw.whenNull(segmentPoint2, "segmentPoint2");
265         return closestPointOnSegment(segmentPoint1.x, segmentPoint1.y, segmentPoint1.z, segmentPoint2.x, segmentPoint2.y,
266                 segmentPoint2.z);
267     }
268 
269     /**
270      * Compute the closest point on a line with optional limiting of the result on either end.
271      * @param p1X double; the x coordinate of the first point on the line
272      * @param p1Y double; the y coordinate of the first point on the line
273      * @param p1Z double; the z coordinate of the first point on the line
274      * @param p2X double; the x coordinate of the second point on the line
275      * @param p2Y double; the y coordinate of the second point on the line
276      * @param p2Z double; the z coordinate of the second point on the line
277      * @param lowLimitHandling Boolean; controls handling of results that lie before the first point of the line. If null; this
278      *            method returns null; else if true; this method returns (p1X,p1Y); else (lowLimitHandling is false); this
279      *            method will return the closest point on the line
280      * @param highLimitHandling Boolean; controls the handling of results that lie beyond the second point of the line. If null;
281      *            this method returns null; else if true; this method returns (p2X,p2Y); else (highLimitHandling is false); this
282      *            method will return the closest point on the line
283      * @return Point3d; the closest point on the line after applying the indicated limit handling; so the result can be null
284      * @throws DrawRuntimeException when any of the arguments is NaN
285      */
286     @SuppressWarnings("checkstyle:parameternumber")
287     public Point3d closestPointOnLine(final double p1X, final double p1Y, final double p1Z, final double p2X, final double p2Y,
288             final double p2Z, final Boolean lowLimitHandling, final Boolean highLimitHandling) throws DrawRuntimeException
289     {
290         double fraction = fractionalPositionOnLine(p1X, p1Y, p1Z, p2X, p2Y, p2Z, lowLimitHandling, highLimitHandling);
291         if (Double.isNaN(fraction))
292         {
293             return null;
294         }
295         if (fraction == 1.0)
296         {
297             return new Point3d(p2X, p2Y, p2Z); // Maximize precision in case fraction == 1.0
298         }
299         return new Point3d(p1X + fraction * (p2X - p1X), p1Y + fraction * (p2Y - p1Y), p1Z + fraction * (p2Z - p1Z));
300     }
301 
302     /**
303      * Compute the fractional position of the closest point on a line with optional limiting of the result on either end. If the
304      * line has length 0; this method returns 0.0.
305      * @param p1X double; the x coordinate of the first point on the line
306      * @param p1Y double; the y coordinate of the first point on the line
307      * @param p1Z double; the z coordinate of the first point on the line
308      * @param p2X double; the x coordinate of the second point on the line
309      * @param p2Y double; the y coordinate of the second point on the line
310      * @param p2Z double; the z coordinate of the second point on the line
311      * @param lowLimitHandling Boolean; controls handling of results that lie before the first point of the line. If null; this
312      *            method returns NaN; else if true; this method returns 0.0; else (lowLimitHandling is false); this results &lt;
313      *            0.0 are returned
314      * @param highLimitHandling Boolean; controls the handling of results that lie beyond the second point of the line. If null;
315      *            this method returns NaN; else if true; this method returns 1.0; else (highLimitHandling is false); results
316      *            &gt; 1.0 are returned
317      * @return double; the fractional position of the closest point on the line. Results within the range 0.0 .. 1.0 are always
318      *         returned as is.. A result &lt; 0.0 is subject to lowLimitHandling. A result &gt; 1.0 is subject to
319      *         highLimitHandling
320      * @throws DrawRuntimeException when any of the arguments is NaN
321      */
322     @SuppressWarnings("checkstyle:parameternumber")
323     public double fractionalPositionOnLine(final double p1X, final double p1Y, final double p1Z, final double p2X,
324             final double p2Y, final double p2Z, final Boolean lowLimitHandling, final Boolean highLimitHandling)
325             throws DrawRuntimeException
326     {
327         double dX = p2X - p1X;
328         double dY = p2Y - p1Y;
329         double dZ = p2Z - p1Z;
330         Throw.when(Double.isNaN(dX) || Double.isNaN(dY) || Double.isNaN(dZ), DrawRuntimeException.class,
331                 "NaN values not permitted");
332         if (0 == dX && 0 == dY && 0 == dZ)
333         {
334             return 0.0;
335         }
336         double fraction = ((this.x - p1X) * dX + (this.y - p1Y) * dY + (this.z - p1Z) * dZ) / (dX * dX + dY * dY + dZ * dZ);
337         if (fraction < 0.0)
338         {
339             if (lowLimitHandling == null)
340             {
341                 return Double.NaN;
342             }
343             if (lowLimitHandling)
344             {
345                 fraction = 0.0;
346             }
347         }
348         else if (fraction > 1.0)
349         {
350             if (highLimitHandling == null)
351             {
352                 return Double.NaN;
353             }
354             if (highLimitHandling)
355             {
356                 fraction = 1.0;
357             }
358         }
359         return fraction;
360     }
361 
362     /**
363      * Project a point on a line segment. If the the projected points lies outside the line segment, the nearest end point of
364      * the line segment is returned. Otherwise the returned point lies between the end points of the line segment. <br>
365      * Adapted from <a href="http://paulbourke.net/geometry/pointlineplane/DistancePoint.java">example code provided by Paul
366      * Bourke</a>.
367      * @param p1X double; the x coordinate of the start point of the line segment
368      * @param p1Y double; the y coordinate of the start point of the line segment
369      * @param p1Z double; the z coordinate of the start point of the line segment
370      * @param p2X double; the x coordinate of the end point of the line segment
371      * @param p2Y double; the y coordinate of the end point of the line segment
372      * @param p2Z double; the y coordinate of the end point of the line segment
373      * @return P; either <cite>segmentPoint1</cite>, or <cite>segmentPoint2</cite> or a new Point2d that lies somewhere in
374      *         between those two.
375      * @throws DrawRuntimeException when any of the parameters is NaN
376      */
377     public final Point3d closestPointOnSegment(final double p1X, final double p1Y, final double p1Z, final double p2X,
378             final double p2Y, final double p2Z) throws DrawRuntimeException
379     {
380         return closestPointOnLine(p1X, p1Y, p1Z, p2X, p2Y, p2Z, true, true);
381     }
382 
383     @Override
384     public final Point3d closestPointOnLine(final Point3d linePoint1, final Point3d linePoint2)
385             throws NullPointerException, DrawRuntimeException
386     {
387         Throw.whenNull(linePoint1, "linePoint1");
388         Throw.whenNull(linePoint2, "linePoint2");
389         return closestPointOnLine(linePoint1.x, linePoint1.y, linePoint1.z, linePoint2.x, linePoint2.y, linePoint2.z);
390     }
391 
392     /**
393      * Project a point on a line. <br>
394      * Adapted from <a href="http://paulbourke.net/geometry/pointlineplane/DistancePoint.java">example code provided by Paul
395      * Bourke</a>.
396      * @param p1X double; the x coordinate of a point of the line
397      * @param p1Y double; the y coordinate of a point of the line
398      * @param p1Z double; the z coordinate of a point on the line
399      * @param p2X double; the x coordinate of another point on the line
400      * @param p2Y double; the y coordinate of another point on the line
401      * @param p2Z double; the z coordinate of another point on the line
402      * @return Point3d; a point on the line that goes through the points
403      * @throws DrawRuntimeException when the points on the line are identical
404      */
405     public final Point3d closestPointOnLine(final double p1X, final double p1Y, final double p1Z, final double p2X,
406             final double p2Y, final double p2Z) throws DrawRuntimeException
407     {
408         Throw.when(p1X == p2X && p1Y == p2Y && p1Z == p2Z, DrawRuntimeException.class, "degenerate line not allowed");
409         return closestPointOnLine(p1X, p1Y, p1Z, p2X, p2Y, p2Z, false, false);
410     }
411 
412     /**
413      * Return the direction of the point in radians with respect to the origin, ignoring the z-coordinate.
414      * @return double; the direction of the projection of the point in the x-y plane with respect to the origin, in radians
415      */
416     final double horizontalDirection()
417     {
418         return Math.atan2(this.y, this.x);
419     }
420 
421     /**
422      * Return the direction to another point, in radians, ignoring the z-coordinate.
423      * @param otherPoint Point3d; the other point
424      * @return double; the direction of the projection of the point in the x-y plane to another point, in radians
425      * @throws NullPointerException when <code>point</code> is null
426      */
427     final double horizontalDirection(final Point3d otherPoint) throws NullPointerException
428     {
429         Throw.whenNull(otherPoint, "otherPoint");
430         return Math.atan2(otherPoint.y - this.y, otherPoint.x - this.x);
431     }
432 
433     /**
434      * Return the direction with respect to the Z axis to another point, in radians.
435      * @param otherPoint Point3d; the other point
436      * @return double; the direction with respect to the Z axis to another point, in radians
437      * @throws NullPointerException when <code>point</code> is null
438      */
439     final double verticalDirection(final Point3d otherPoint) throws NullPointerException
440     {
441         Throw.whenNull(otherPoint, "otherPoint");
442         return Math.atan2(Math.hypot(otherPoint.y - this.y, otherPoint.x - this.x), otherPoint.z - this.z);
443     }
444 
445     /**
446      * Return the squared distance between the coordinates of this point and the provided point, ignoring the z-coordinate.
447      * @param otherPoint Point3d; the other point
448      * @return double; the squared distance between this point and the other point, ignoring the z-coordinate
449      * @throws NullPointerException when point is null
450      */
451     final double horizontalDistanceSquared(final Point3d otherPoint)
452     {
453         Throw.whenNull(otherPoint, "otherPoint");
454         double dx = this.x - otherPoint.x;
455         double dy = this.y - otherPoint.y;
456         return dx * dx + dy * dy;
457     }
458 
459     /**
460      * Return the Euclidean distance between this point and the provided point, ignoring the z-coordinate.
461      * @param otherPoint Point3d; the other point
462      * @return double; the Euclidean distance between this point and the other point, ignoring the z-coordinate
463      * @throws NullPointerException when point is null
464      */
465     final double horizontalDistance(final Point3d otherPoint)
466     {
467         return Math.sqrt(horizontalDistanceSquared(otherPoint));
468     }
469 
470     @Override
471     @SuppressWarnings("checkstyle:designforextension")
472     public String toString()
473     {
474         return toString("%f");
475     }
476 
477     @Override
478     public String toString(final String doubleFormat, final boolean doNotIncludeClassName)
479     {
480         String format = String.format("%1$s[x=%2$s, y=%2$s, z=%2$s]", doNotIncludeClassName ? "" : "Point3d ", doubleFormat);
481         return String.format(Locale.US, format, this.x, this.y, this.z);
482     }
483 
484     @Override
485     public int hashCode()
486     {
487         final int prime = 31;
488         int result = 1;
489         long temp;
490         temp = Double.doubleToLongBits(this.x);
491         result = prime * result + (int) (temp ^ (temp >>> 32));
492         temp = Double.doubleToLongBits(this.y);
493         result = prime * result + (int) (temp ^ (temp >>> 32));
494         temp = Double.doubleToLongBits(this.z);
495         result = prime * result + (int) (temp ^ (temp >>> 32));
496         return result;
497     }
498 
499     @SuppressWarnings("checkstyle:needbraces")
500     @Override
501     public boolean equals(final Object obj)
502     {
503         if (this == obj)
504             return true;
505         if (obj == null)
506             return false;
507         if (getClass() != obj.getClass())
508             return false;
509         Point3d other = (Point3d) obj;
510         if (Double.doubleToLongBits(this.x) != Double.doubleToLongBits(other.x))
511             return false;
512         if (Double.doubleToLongBits(this.y) != Double.doubleToLongBits(other.y))
513             return false;
514         if (Double.doubleToLongBits(this.z) != Double.doubleToLongBits(other.z))
515             return false;
516         return true;
517     }
518 
519 }