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.Space3d;
11  import org.djutils.draw.bounds.Bounds3d;
12  import org.djutils.exceptions.Throw;
13  
14  /**
15   * A Point3d is an immutable point with an x, y, and z coordinate, stored with double precision. It differs from many Point
16   * implementations by being immutable.
17   * <p>
18   * Copyright (c) 2020-2021 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
19   * BSD-style license. See <a href="https://djutils.org/docs/current/djutils/licenses.html">DJUTILS License</a>.
20   * </p>
21   * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
22   * @author <a href="https://www.tudelft.nl/pknoppers">Peter Knoppers</a>
23   */
24  public class Point3d implements Drawable3d, Point<Point3d, Space3d>
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 with just an x, y and z coordinate, stored with double precision.
43       * @param x double; the x coordinate
44       * @param y double; the y coordinate
45       * @param z double; the z coordinate
46       * @throws IllegalArgumentException when x or y is NaN
47       */
48      public Point3d(final double x, final double y, final double z)
49      {
50          Throw.when(Double.isNaN(x) || Double.isNaN(y) || Double.isNaN(z), IllegalArgumentException.class,
51                  "Coordinate must be a number (not NaN)");
52          this.x = x;
53          this.y = y;
54          this.z = z;
55      }
56  
57      /**
58       * Create a new Point with just an x, y and z coordinate, stored with double precision.
59       * @param xyz double[]; the x, y and z coordinate
60       * @throws NullPointerException when xyz is null
61       * @throws IllegalArgumentException when the dimension of xyz is not 3, or a coordinate is NaN
62       */
63      public Point3d(final double[] xyz) throws NullPointerException, IllegalArgumentException
64      {
65          this(checkLengthIsThree(Throw.whenNull(xyz, "xyz-point cannot be null"))[0], xyz[1], xyz[2]);
66      }
67  
68      /**
69       * Create an immutable point with just three values, x, y and z, stored with double precision from a Point2d and z.
70       * @param point Point2d; a Point2d
71       * @param z double; the z coordinate
72       * @throws NullPointerException when point is null
73       * @throws IllegalArgumentException when z is NaN
74       */
75      public Point3d(final Point2d point, final double z) throws NullPointerException, IllegalArgumentException
76      {
77          Throw.whenNull(point, "point cannot be null");
78          Throw.when(Double.isNaN(z), IllegalArgumentException.class, "Coordinate must be a number (not NaN)");
79          this.x = point.x;
80          this.y = point.y;
81          this.z = z;
82      }
83  
84      /**
85       * Create an immutable point with just three values, x, y and z, stored with double precision from an AWT Point2D and z.
86       * @param point Point2D; an AWT Point2D
87       * @param z double; the z coordinate
88       * @throws NullPointerException when point is null
89       * @throws IllegalArgumentException when point has a NaN coordinate, or z is NaN
90       */
91      public Point3d(final Point2D point, final double z) throws NullPointerException, IllegalArgumentException
92      {
93          Throw.whenNull(point, "point cannot be null");
94          Throw.when(Double.isNaN(point.getX()) || Double.isNaN(point.getY()), IllegalArgumentException.class,
95                  "Coordinate must be a number (not NaN)");
96          Throw.when(Double.isNaN(z), IllegalArgumentException.class, "Coordinate must be a number (not NaN)");
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 double[]; the provided array
105      * @return double[]; the provided array
106      * @throws IllegalArgumentException when length of xyz is not three
107      */
108     private static double[] checkLengthIsThree(final double[] xyz) throws IllegalArgumentException
109     {
110         Throw.when(xyz.length != 3, IllegalArgumentException.class, "Length of xy-array must be 2");
111         return xyz;
112     }
113 
114     /** {@inheritDoc} */
115     @Override
116     public final double getX()
117     {
118         return this.x;
119     }
120 
121     /** {@inheritDoc} */
122     @Override
123     public final double getY()
124     {
125         return this.y;
126     }
127 
128     /**
129      * Return the z-coordinate.
130      * @return double; the z-coordinate
131      */
132     public final double getZ()
133     {
134         return this.z;
135     }
136 
137     /** {@inheritDoc} */
138     @Override
139     public double distanceSquared(final Point3d otherPoint) throws NullPointerException
140     {
141         Throw.whenNull(otherPoint, "point cannot be null");
142         double dx = this.x - otherPoint.x;
143         double dy = this.y - otherPoint.y;
144         double dz = this.z - otherPoint.z;
145         return dx * dx + dy * dy + dz * dz;
146     }
147 
148     /** {@inheritDoc} */
149     @Override
150     public double distance(final Point3d otherPoint) throws NullPointerException
151     {
152         Throw.whenNull(otherPoint, "point cannot be null");
153         return Math.sqrt(distanceSquared(otherPoint));
154     }
155 
156     /** {@inheritDoc} */
157     @Override
158     public int size()
159     {
160         return 1;
161     }
162 
163     /** {@inheritDoc} */
164     @Override
165     public Iterator<? extends Point3d> getPoints()
166     {
167         return Arrays.stream(new Point3d[] {this}).iterator();
168     }
169 
170     /** {@inheritDoc} */
171     @Override
172     public Point2d project() throws DrawRuntimeException
173     {
174         return new Point2d(this.x, this.y);
175     }
176 
177     /**
178      * Return a new Point with a translation by the provided dx and dy.
179      * @param dx double; the horizontal translation
180      * @param dy double; the vertical translation
181      * @return Point3D; a new point with the translated coordinates
182      * @throws IllegalArgumentException when dx, or dy is NaN
183      */
184     public Point3d translate(final double dx, final double dy) throws IllegalArgumentException
185     {
186         Throw.when(Double.isNaN(dx) || Double.isNaN(dy), IllegalArgumentException.class,
187                 "Translation must be number (not NaN)");
188         return new Point3d(this.x + dx, this.y + dy, this.z);
189     }
190 
191     /**
192      * Return a new Point3d with a translation by the provided dx, dy and dz.
193      * @param dx double; the x translation
194      * @param dy double; the y translation
195      * @param dz double; the z translation
196      * @return Point3d; a new point with the translated coordinates
197      * @throws IllegalArgumentException when dx, dy, or dz is NaN
198      */
199     public Point3d translate(final double dx, final double dy, final double dz) throws IllegalArgumentException
200     {
201         Throw.when(Double.isNaN(dx) || Double.isNaN(dy) || Double.isNaN(dz), IllegalArgumentException.class,
202                 "dx, dy and dz must be numbers (not NaN)");
203         return new Point3d(this.x + dx, this.y + dy, this.z + dz);
204     }
205 
206     /** {@inheritDoc} */
207     @Override
208     public Point3d scale(final double factor) throws IllegalArgumentException
209     {
210         Throw.when(Double.isNaN(factor), IllegalArgumentException.class, "factor must be a number (not NaN)");
211         return new Point3d(this.x * factor, this.y * factor, this.z * factor);
212     }
213 
214     /** {@inheritDoc} */
215     @Override
216     public Point3d neg()
217     {
218         return scale(-1.0);
219     }
220 
221     /** {@inheritDoc} */
222     @Override
223     public Point3d abs()
224     {
225         return new Point3d(Math.abs(this.x), Math.abs(this.y), Math.abs(this.z));
226     }
227 
228     /** {@inheritDoc} */
229     @Override
230     public Point3d normalize() throws DrawRuntimeException
231     {
232         double length = Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
233         Throw.when(length == 0.0, DrawRuntimeException.class, "cannot normalize (0.0, 0.0, 0.0)");
234         return this.scale(1.0 / length);
235     }
236 
237     /** {@inheritDoc} */
238     @Override
239     public Point3dl#Point3d">Point3d interpolate(final Point3d point, final double fraction)
240     {
241         Throw.whenNull(point, "point cannot be null");
242         Throw.when(Double.isNaN(fraction), IllegalArgumentException.class, "fraction must be a number (not NaN)");
243         return new Point3d((1.0 - fraction) * this.x + fraction * point.x, (1.0 - fraction) * this.y + fraction * point.y,
244                 (1.0 - fraction) * this.z + fraction * point.z);
245 
246     }
247 
248     /** {@inheritDoc} */
249     @Override
250     public boolean epsilonEquals(final Point3d other, final double epsilon)
251     {
252         Throw.whenNull(other, "other point cannot be null");
253         if (Math.abs(this.x - other.x) > epsilon)
254         {
255             return false;
256         }
257         if (Math.abs(this.y - other.y) > epsilon)
258         {
259             return false;
260         }
261         if (Math.abs(this.z - other.z) > epsilon)
262         {
263             return false;
264         }
265         return true;
266     }
267 
268     /** {@inheritDoc} */
269     @Override
270     public Bounds3d getBounds()
271     {
272         return new Bounds3d(this);
273     }
274 
275     /** {@inheritDoc} */
276     @Override
277     public final Point3doint3d">Point3d>Point3d closestPointOnSegment(final Point3doint3d">Point3d segmentPoint1, final Point3d segmentPoint2)
278     {
279         Throw.whenNull(segmentPoint1, "segmentPoint1 may not be null");
280         Throw.whenNull(segmentPoint2, "segmentPoint2 may not be null");
281         return closestPointOnSegment(segmentPoint1.x, segmentPoint1.y, segmentPoint1.z, segmentPoint2.x, segmentPoint2.y,
282                 segmentPoint2.z);
283     }
284 
285     /**
286      * Compute the closest point on a line with optional limiting of the result on either end.
287      * @param p1X double; the x coordinate of the first point on the line
288      * @param p1Y double; the y coordinate of the first point on the line
289      * @param p1Z double; the z coordinate of the first point on the line
290      * @param p2X double; the x coordinate of the second point on the line
291      * @param p2Y double; the y coordinate of the second point on the line
292      * @param p2Z double; the z coordinate of the second point on the line
293      * @param lowLimitHandling Boolean; controls handling of results that lie before the first point of the line. If null; this
294      *            method returns null; else if true; this method returns (p1X,p1Y); else (lowLimitHandling is false); this
295      *            method will return the closest point on the line
296      * @param highLimitHandling Boolean; controls the handling of results that lie beyond the second point of the line. If null;
297      *            this method returns null; else if true; this method returns (p2X,p2Y); else (highLimitHandling is false); this
298      *            method will return the closest point on the line
299      * @return Point3d; the closest point on the line after applying the indicated limit handling; so the result can be null
300      * @throws DrawRuntimeException when any of the arguments is NaN
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) throws DrawRuntimeException
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 double; the x coordinate of the first point on the line
322      * @param p1Y double; the y coordinate of the first point on the line
323      * @param p1Z double; the z coordinate of the first point on the line
324      * @param p2X double; the x coordinate of the second point on the line
325      * @param p2Y double; the y coordinate of the second point on the line
326      * @param p2Z double; the z coordinate of the second point on the line
327      * @param lowLimitHandling Boolean; controls handling of results that lie before the first point of the line. If null; this
328      *            method returns NaN; else if true; this method returns 0.0; else (lowLimitHandling is false); this results &lt;
329      *            0.0 are returned
330      * @param highLimitHandling Boolean; controls the handling of results that lie beyond the second point of the line. If null;
331      *            this method returns NaN; else if true; this method returns 1.0; else (highLimitHandling is false); results
332      *            &gt; 1.0 are returned
333      * @return double; the fractional position of the closest point on the line. Results within the range 0.0 .. 1.0 are always
334      *         returned as is.. A result &lt; 0.0 is subject to lowLimitHandling. A result &gt; 1.0 is subject to
335      *         highLimitHandling
336      * @throws DrawRuntimeException when any of the arguments is NaN
337      */
338     @SuppressWarnings("checkstyle:parameternumber")
339     public double fractionalPositionOnLine(final double p1X, final double p1Y, final double p1Z, final double p2X,
340             final double p2Y, final double p2Z, final Boolean lowLimitHandling, final Boolean highLimitHandling)
341             throws DrawRuntimeException
342     {
343         double dX = p2X - p1X;
344         double dY = p2Y - p1Y;
345         double dZ = p2Z - p1Z;
346         Throw.when(Double.isNaN(dX) || Double.isNaN(dY) || Double.isNaN(dZ), DrawRuntimeException.class,
347                 "NaN values not permitted");
348         if (0 == dX && 0 == dY && 0 == dZ)
349         {
350             return 0.0;
351         }
352         double fraction = ((this.x - p1X) * dX + (this.y - p1Y) * dY + (this.z - p1Z) * dZ) / (dX * dX + dY * dY + dZ * dZ);
353         if (fraction < 0.0)
354         {
355             if (lowLimitHandling == null)
356             {
357                 return Double.NaN;
358             }
359             if (lowLimitHandling)
360             {
361                 fraction = 0.0;
362             }
363         }
364         else if (fraction > 1.0)
365         {
366             if (highLimitHandling == null)
367             {
368                 return Double.NaN;
369             }
370             if (highLimitHandling)
371             {
372                 fraction = 1.0;
373             }
374         }
375         return fraction;
376     }
377 
378     /**
379      * Project a point on a line segment. If the the projected points lies outside the line segment, the nearest end point of
380      * the line segment is returned. Otherwise the returned point lies between the end points of the line segment. <br>
381      * Adapted from <a href="http://paulbourke.net/geometry/pointlineplane/DistancePoint.java">example code provided by Paul
382      * Bourke</a>.
383      * @param p1X double; the x coordinate of the start point of the line segment
384      * @param p1Y double; the y coordinate of the start point of the line segment
385      * @param p1Z double; the z coordinate of the start point of the line segment
386      * @param p2X double; the x coordinate of the end point of the line segment
387      * @param p2Y double; the y coordinate of the end point of the line segment
388      * @param p2Z double; the y coordinate of the end point of the line segment
389      * @return P; either <cite>segmentPoint1</cite>, or <cite>segmentPoint2</cite> or a new Point2d that lies somewhere in
390      *         between those two.
391      * @throws DrawRuntimeException when any of the parameters is NaN
392      */
393     public final Point3d closestPointOnSegment(final double p1X, final double p1Y, final double p1Z, final double p2X,
394             final double p2Y, final double p2Z) throws DrawRuntimeException
395     {
396         return closestPointOnLine(p1X, p1Y, p1Z, p2X, p2Y, p2Z, true, true);
397     }
398 
399     /** {@inheritDoc} */
400     @Override
401     public final Point3dl#Point3d">Point3d3d">Point3d closestPointOnLine(final Point3dl#Point3d">Point3d linePoint1, final Point3d linePoint2)
402             throws NullPointerException, DrawRuntimeException
403     {
404         Throw.whenNull(linePoint1, "linePoint1 may not be null");
405         Throw.whenNull(linePoint2, "linePoint2 may not be null");
406         return closestPointOnLine(linePoint1.x, linePoint1.y, linePoint1.z, linePoint2.x, linePoint2.y, linePoint2.z);
407     }
408 
409     /**
410      * Project a point on a line. <br>
411      * Adapted from <a href="http://paulbourke.net/geometry/pointlineplane/DistancePoint.java">example code provided by Paul
412      * Bourke</a>.
413      * @param p1X double; the x coordinate of a point of the line
414      * @param p1Y double; the y coordinate of a point of the line
415      * @param p1Z double; the z coordinate of a point on the line
416      * @param p2X double; the x coordinate of another point on the line
417      * @param p2Y double; the y coordinate of another point on the line
418      * @param p2Z double; the z coordinate of another point on the line
419      * @return Point3d; a point on the line that goes through the points
420      * @throws DrawRuntimeException when the points on the line are identical
421      */
422     public final Point3d closestPointOnLine(final double p1X, final double p1Y, final double p1Z, final double p2X,
423             final double p2Y, final double p2Z) throws DrawRuntimeException
424     {
425         Throw.when(p1X == p2X && p1Y == p2Y && p1Z == p2Z, DrawRuntimeException.class, "degenerate line not allowed");
426         return closestPointOnLine(p1X, p1Y, p1Z, p2X, p2Y, p2Z, false, false);
427     }
428 
429     /**
430      * Return the direction of the point in radians with respect to the origin, ignoring the z-coordinate.
431      * @return double; the direction of the projection of the point in the x-y plane with respect to the origin, in radians
432      */
433     final double horizontalDirection()
434     {
435         return Math.atan2(this.y, this.x);
436     }
437 
438     /**
439      * Return the direction to another point, in radians, ignoring the z-coordinate.
440      * @param point Point3d; the other point
441      * @return double; the direction of the projection of the point in the x-y plane to another point, in radians
442      * @throws NullPointerException when <code>point</code> is null
443      */
444     final double horizontalDirection(final Point3d point) throws NullPointerException
445     {
446         Throw.whenNull(point, "point cannot be null");
447         return Math.atan2(point.y - this.y, point.x - this.x);
448     }
449 
450     /**
451      * Return the direction with respect to the Z axis to another point, in radians.
452      * @param point Point3d; the other point
453      * @return double; the direction with respect to the Z axis to another point, in radians
454      * @throws NullPointerException when <code>point</code> is null
455      */
456     final double verticalDirection(final Point3d point) throws NullPointerException
457     {
458         Throw.whenNull(point, "point cannot be null");
459         return Math.atan2(Math.hypot(point.y - this.y, point.x - this.x), point.z - this.z);
460     }
461 
462     /**
463      * Return the squared distance between the coordinates of this point and the provided point, ignoring the z-coordinate.
464      * @param point Point3d; the other point
465      * @return double; the squared distance between this point and the other point, ignoring the z-coordinate
466      * @throws NullPointerException when point is null
467      */
468     final double horizontalDistanceSquared(final Point3d point)
469     {
470         Throw.whenNull(point, "point cannot be null");
471         double dx = this.x - point.x;
472         double dy = this.y - point.y;
473         return dx * dx + dy * dy;
474     }
475 
476     /**
477      * Return the Euclidean distance between this point and the provided point, ignoring the z-coordinate.
478      * @param point Point3d; the other point
479      * @return double; the Euclidean distance between this point and the other point, ignoring the z-coordinate
480      * @throws NullPointerException when point is null
481      */
482     final double horizontalDistance(final Point3d point)
483     {
484         return Math.sqrt(horizontalDistanceSquared(point));
485     }
486 
487     /** {@inheritDoc} */
488     @Override
489     @SuppressWarnings("checkstyle:designforextension")
490     public String toString()
491     {
492         return toString("%f");
493     }
494 
495     /** {@inheritDoc} */
496     @Override
497     public String toString(final String doubleFormat, final boolean doNotIncludeClassName)
498     {
499         String format = String.format("%1$s[x=%2$s, y=%2$s, z=%2$s]", doNotIncludeClassName ? "" : "Point3d ", doubleFormat);
500         return String.format(Locale.US, format, this.x, this.y, this.z);
501     }
502 
503     /** {@inheritDoc} */
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     /** {@inheritDoc} */
520     @SuppressWarnings("checkstyle:needbraces")
521     @Override
522     public boolean equals(final Object obj)
523     {
524         if (this == obj)
525             return true;
526         if (obj == null)
527             return false;
528         if (getClass() != obj.getClass())
529             return false;
530         Point3d../../../../org/djutils/draw/point/Point3d.html#Point3d">Point3d other = (Point3d) obj;
531         if (Double.doubleToLongBits(this.x) != Double.doubleToLongBits(other.x))
532             return false;
533         if (Double.doubleToLongBits(this.y) != Double.doubleToLongBits(other.y))
534             return false;
535         if (Double.doubleToLongBits(this.z) != Double.doubleToLongBits(other.z))
536             return false;
537         return true;
538     }
539 
540 }