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-2021 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 Point3dl#Point3d">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 Point3doint3d">Point3d>Point3d closestPointOnSegment(final Point3doint3d">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 &lt;
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      *            &gt; 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 &lt; 0.0 is subject to lowLimitHandling. A result &gt; 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 Point3dl#Point3d">Point3d3d">Point3d closestPointOnLine(final Point3dl#Point3d">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../../../../org/djutils/draw/point/Point3d.html#Point3d">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 }