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   import java.util.Objects;
7   
8   import org.djutils.base.AngleUtil;
9   import org.djutils.draw.Directed3d;
10  import org.djutils.draw.DrawRuntimeException;
11  import org.djutils.exceptions.Throw;
12  
13  /**
14   * DirectedPoint3d.java.
15   * <p>
16   * Copyright (c) 2023-2024 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
17   * for project information <a href="https://djutils.org" target="_blank"> https://djutils.org</a>. The DJUTILS project is
18   * distributed under a three-clause BSD-style license, which can be found at
19   * <a href="https://djutils.org/docs/license.html" target="_blank"> https://djutils.org/docs/license.html</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 DirectedPoint3d extends Point3d implements Directed3d<DirectedPoint3d>
25  {
26      /** */
27      private static final long serialVersionUID = 20200828L;
28  
29      /** The direction as rotation around the x-axis. */
30      @SuppressWarnings("checkstyle:visibilitymodifier")
31      public final double dirY;
32  
33      /** The direction as rotation from the positive z-axis towards the x-y plane. */
34      @SuppressWarnings("checkstyle:visibilitymodifier")
35      public final double dirZ;
36  
37      /**
38       * Create a new DirectedPoint3d with x, y, and z coordinates and orientation dirX,dirY,dirZ.
39       * @param x double; the x coordinate
40       * @param y double; the y coordinate
41       * @param z double; the z coordinate
42       * @param dirY double; the complement of the slope
43       * @param dirZ double; the counter-clockwise rotation around the point in radians
44       * @throws IllegalArgumentException when x, y, z, dirX, dirY, or dirZ is NaN
45       */
46      public DirectedPoint3d(final double x, final double y, final double z, final double dirY, final double dirZ)
47              throws IllegalArgumentException
48      {
49          super(x, y, z);
50          Throw.when(Double.isNaN(dirY) || Double.isNaN(dirZ), IllegalArgumentException.class,
51                  "dirY and dirZ must be numbers (not NaN)");
52          this.dirZ = dirZ;
53          this.dirY = dirY;
54      }
55  
56      /**
57       * Create a new DirectedPoint3d with x, y, and z coordinates in a double[] and direction dirY,dirZ.
58       * @param xyz double[]; the x, y and z coordinates
59       * @param dirY double; the complement of the slope
60       * @param dirZ double; the counter-clockwise rotation around the point in radians
61       * @throws NullPointerException when <cite>xyx</cite> is null
62       * @throws IllegalArgumentException when the length of the <cite>xyz</cite> array is not 3, or contains a NaN value, or
63       *             <cite>dirY</cite>, or <cite>dirZ</cite> is NaN
64       */
65      public DirectedPoint3d(final double[] xyz, final double dirY, final double dirZ)
66              throws NullPointerException, IllegalArgumentException
67      {
68          super(xyz);
69          Throw.when(Double.isNaN(dirY) || Double.isNaN(dirZ), IllegalArgumentException.class,
70                  "dirY and dirZ must be numbers (not NaN)");
71          this.dirY = dirY;
72          this.dirZ = dirZ;
73      }
74  
75      /**
76       * Create a new DirectedPoint3d from another Point3d and and direction dirY,dirZ.
77       * @param point Point3d; the point from which this OrientedPoint3d will be instantiated
78       * @param dirY double; the complement of the slope
79       * @param dirZ double; the counter-clockwise rotation around the point in radians
80       * @throws IllegalArgumentException when dirX, dirY, or dirZ is NaN
81       */
82      public DirectedPoint3d(final Point3d point, final double dirY, final double dirZ) throws IllegalArgumentException
83      {
84          this(point.x, point.y, point.z, dirY, dirZ);
85      }
86  
87      /**
88       * Construct a new DirectedPoint3d from three coordinates and the coordinates of a point that the direction goes through.
89       * @param x double; the x coordinate of the new DirectedPoint
90       * @param y double; the y coordinate of the new DirectedPoint
91       * @param z double; the z coordinate of the new DirectedPoint
92       * @param throughX double; the x-coordinate of a point that the direction goes through
93       * @param throughY double; the y-coordinate of a point that the direction goes through
94       * @param throughZ double; the z-coordinate of a point that the direction goes through
95       * @throws DrawRuntimeException when <cite>throughX</cite> == <cite>x</cite> and <cite>throughY</cite> == <cite>y</cite> and
96       *             <cite>throughZ</cite> == <cite>z</cite>, or any through-value is NaN
97       */
98      public DirectedPoint3d(final double x, final double y, final double z, final double throughX, final double throughY,
99              final double throughZ) throws DrawRuntimeException
100     {
101         this(x, y, z, buildDirectionVector(throughX - x, throughY - y, throughZ - z));
102     }
103 
104     /**
105      * Build the direction vector.
106      * @param dX double; x difference
107      * @param dY double; y difference
108      * @param dZ double; z difference
109      * @return double[]; a two-element array containing dirY and dirZ
110      */
111     private static double[] buildDirectionVector(final double dX, final double dY, final double dZ)
112     {
113         Throw.when(0 == dX && 0 == dY && 0 == dZ, IllegalArgumentException.class, "Through point may not be equal to point");
114         return new double[] {Math.atan2(Math.hypot(dX, dY), dZ), Math.atan2(dY, dX)};
115     }
116 
117     /**
118      * Construct a new DirectedPoint3d form a Point3d and the coordinates that the direction goes through.
119      * @param point Point3d; the point
120      * @param throughX double; the x coordinate of a point that the direction goes through
121      * @param throughY double; the y coordinate of a point that the direction goes through
122      * @param throughZ double; the z coordinate of a point that the direction goes through
123      * @throws DrawRuntimeException when <cite>throughX</cite> == <cite>point.x</cite> and <cite>throughY</cite> ==
124      *             <cite>point.y</cite> and <cite>throughZ</cite> == <cite>point.z</cite>, or any through-value is NaN
125      */
126     public DirectedPoint3d(final Point3d point, final double throughX, final double throughY, final double throughZ)
127             throws DrawRuntimeException
128     {
129         this(Throw.whenNull(point, "point").x, point.y, point.z, throughX, throughY, throughZ);
130     }
131 
132     /**
133      * Verify that a double array is not null, has two elements.
134      * @param orientation double[]; the array to check
135      * @return double; the first element of the argument
136      * @throws NullPointerException when <code>orientation</code> is null
137      * @throws IllegalArgumentException when the length of the <code>orientation</code> array is not 2
138      */
139     private static double checkOrientationVector(final double[] orientation)
140             throws NullPointerException, IllegalArgumentException
141     {
142         Throw.when(orientation.length != 2, IllegalArgumentException.class, "length of orientation array must be 2");
143         return orientation[0];
144     }
145 
146     /**
147      * Create a new DirectedPoint3d with x, y and z coordinates and orientation specified using a double array of two elements
148      * (containing dirY,dirZ in that order).
149      * @param x double; the x coordinate
150      * @param y double; the y coordinate
151      * @param z double; the z coordinate
152      * @param orientation double[]; the two direction values (dirY and dirZ) in a double array containing dirY and dirZ in that
153      *            order. DirY is the rotation from the positive z-axis to the direction. DirZ is the angle from the positive
154      *            x-axis to the projection of the direction in the x-y-plane.
155      * @throws NullPointerException when <code>orientation</code> is null
156      * @throws IllegalArgumentException when the length of the <code>direction</code> array is not 2, or contains a NaN value
157      */
158     public DirectedPoint3d(final double x, final double y, final double z, final double[] orientation)
159             throws NullPointerException, IllegalArgumentException
160     {
161         this(x, y, z, checkOrientationVector(orientation), orientation[1]);
162     }
163 
164     /**
165      * Create a new OrientedPoint3d from x, y and z coordinates packed in a double array of three elements and a direction
166      * specified using a double array of two elements.
167      * @param xyz double[]; the <cite>x</cite>, <cite>y</cite> and <cite>z</cite> coordinates in that order
168      * @param orientation double[]; the two orientation angles <cite>dirY</cite> and <cite>dirZ</cite> in that order
169      * @throws NullPointerException when <cite>xyx</cite> or <cite>orientation</cite> is null
170      * @throws IllegalArgumentException when the length of the <cite>xyx</cite> array is not 3 or the length of the
171      *             <cite>orientation</cite> array is not 2
172      */
173     public DirectedPoint3d(final double[] xyz, final double[] orientation) throws NullPointerException, IllegalArgumentException
174     {
175         this(xyz, checkOrientationVector(orientation), orientation[1]);
176     }
177 
178     /**
179      * Construct a new DirectedPoint3d from x, y and z coordinates and a point that the direction goes through.
180      * @param x double; the x coordinate of the new DirectedPoint3d
181      * @param y double; the y coordinate of the new DirectedPoint3d
182      * @param z double; the z coordinate of the new DirectedPoint3d
183      * @param throughPoint Point3d; a point that the direction goes through
184      * @throws NullPointerException when throughPoint is null
185      * @throws DrawRuntimeException when throughPoint is exactly at (x, y, z)
186      */
187     public DirectedPoint3d(final double x, final double y, final double z, final Point3d throughPoint)
188             throws NullPointerException, DrawRuntimeException
189     {
190         this(x, y, z, Throw.whenNull(throughPoint, "througPoint").x, throughPoint.y, throughPoint.z);
191     }
192 
193     /**
194      * Construct a new DirectedPoint3d.
195      * @param point Point3d; the location of the new DirectedPoint3d
196      * @param throughPoint Point3d; another point that the direction goes through
197      * @throws NullPointerException when point is null or throughPoint is null
198      * @throws DrawRuntimeException when throughPoint is exactly at point
199      */
200     public DirectedPoint3d(final Point3d point, final Point3d throughPoint) throws NullPointerException, DrawRuntimeException
201     {
202         this(Throw.whenNull(point, "point").x, point.y, point.z, Throw.whenNull(throughPoint, "throughPoint").x, throughPoint.y,
203                 throughPoint.z);
204     }
205 
206     @Override
207     public DirectedPoint3d translate(final double dx, final double dy) throws IllegalArgumentException
208     {
209         Throw.when(Double.isNaN(dx) || Double.isNaN(dy), IllegalArgumentException.class, "translation may not contain NaN");
210         return new DirectedPoint3d(this.x + dx, this.y + dy, this.z, this.dirY, this.dirZ);
211     }
212 
213     @Override
214     public DirectedPoint3d translate(final double dx, final double dy, final double dz) throws IllegalArgumentException
215     {
216         Throw.when(Double.isNaN(dx) || Double.isNaN(dy) || Double.isNaN(dz), IllegalArgumentException.class,
217                 "Translation may not contain NaN");
218         return new DirectedPoint3d(this.x + dx, this.y + dy, this.z + dz, this.dirY, this.dirZ);
219     }
220 
221     @Override
222     public DirectedPoint3d scale(final double factor) throws IllegalArgumentException
223     {
224         return new DirectedPoint3d(this.x * factor, this.y * factor, this.z * factor, this.dirY, this.dirZ);
225     }
226 
227     @Override
228     public DirectedPoint3d neg()
229     {
230         return new DirectedPoint3d(-this.x, -this.y, -this.z, AngleUtil.normalizeAroundZero(this.dirY + Math.PI),
231                 AngleUtil.normalizeAroundZero(this.dirZ + Math.PI));
232     }
233 
234     @Override
235     public DirectedPoint3d abs()
236     {
237         return new DirectedPoint3d(Math.abs(this.x), Math.abs(this.y), Math.abs(this.z), this.dirY, this.dirZ);
238     }
239 
240     @Override
241     public DirectedPoint3d normalize() throws DrawRuntimeException
242     {
243         double length = Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
244         Throw.when(length == 0.0, DrawRuntimeException.class, "cannot normalize (0.0, 0.0, 0.0)");
245         return new DirectedPoint3d(this.x / length, this.y / length, this.z / length, this.dirY, this.dirZ);
246     }
247 
248     /**
249      * Interpolate towards another DirectedPoint3d with a fraction. It is allowed for fraction to be less than zero or larger
250      * than 1. In that case the interpolation turns into an extrapolation. DirY and dirZ are interpolated/extrapolated using the
251      * interpolateShortest method.
252      * @param otherPoint OrientedPoint3d; the other point
253      * @param fraction double; the factor for interpolation towards the other point. When &lt;code&gt;fraction&lt;/code&gt; is
254      *            between 0 and 1, it is an interpolation, otherwise an extrapolation. If <code>fraction</code> is 0;
255      *            <code>this</code> Point is returned; if <code>fraction</code> is 1, the <code>otherPoint</code> is returned
256      * @return DirectedPoint3d; a new OrientedPoint3d at the requested fraction
257      * @throws NullPointerException when otherPoint is null
258      * @throws IllegalArgumentException when fraction is NaN
259      */
260     public DirectedPoint3d interpolate(final DirectedPoint3d otherPoint, final double fraction)
261             throws NullPointerException, IllegalArgumentException
262     {
263         Throw.whenNull(otherPoint, "otherPoint");
264         Throw.when(Double.isNaN(fraction), IllegalArgumentException.class, "fraction must be a number (not NaN)");
265         if (0.0 == fraction)
266         {
267             return this;
268         }
269         if (1.0 == fraction)
270         {
271             return otherPoint;
272         }
273         return new DirectedPoint3d((1.0 - fraction) * this.x + fraction * otherPoint.x,
274                 (1.0 - fraction) * this.y + fraction * otherPoint.y, (1.0 - fraction) * this.z + fraction * otherPoint.z,
275                 AngleUtil.interpolateShortest(this.dirY, otherPoint.dirY, fraction),
276                 AngleUtil.interpolateShortest(this.dirZ, otherPoint.dirZ, fraction));
277     }
278 
279     /**
280      * Return a new DirectedPoint3d with an in-place rotation around the z-axis by the provided rotateZ. The resulting rotation
281      * will be normalized between -&pi; and &pi;.
282      * @param rotateZ double; the rotation around the z-axis
283      * @return DirectedPoint3d; a new point with the same coordinates, dirX and dirY and modified dirZ
284      * @throws IllegalArgumentException when rotateZ is NaN
285      */
286     public DirectedPoint3d rotate(final double rotateZ) throws IllegalArgumentException
287     {
288         Throw.when(Double.isNaN(rotateZ), IllegalArgumentException.class, "rotateZ must be a number (not NaN)");
289         return new DirectedPoint3d(this.x, this.y, this.z, this.dirY, AngleUtil.normalizeAroundZero(this.dirZ + rotateZ));
290     }
291 
292     /**
293      * Return a new OrientedPoint3d point with an in-place rotation by the provided rotateY, and rotateZ. The resulting
294      * rotations will be normalized between -&pi; and &pi;.
295      * @param rotateY double; the rotation around the y-axis
296      * @param rotateZ double; the rotation around the z-axis
297      * @return DirectedPoint3d; a new point with the same coordinates and applied rotations
298      * @throws IllegalArgumentException when any of the rotations is NaN
299      */
300     public DirectedPoint3d rotate(final double rotateY, final double rotateZ) throws IllegalArgumentException
301     {
302         Throw.when(Double.isNaN(rotateY) || Double.isNaN(rotateZ), IllegalArgumentException.class,
303                 "rotateY and rotateZ must be a numbers (not NaN)");
304         return new DirectedPoint3d(this.x, this.y, this.z, AngleUtil.normalizeAroundZero(this.dirY + rotateY),
305                 AngleUtil.normalizeAroundZero(this.dirZ + rotateZ));
306     }
307 
308     @Override
309     public double getDirZ()
310     {
311         return this.dirZ;
312     }
313 
314     @Override
315     public double getDirY()
316     {
317         return this.dirY;
318     }
319 
320     @Override
321     public Iterator<? extends DirectedPoint3d> getPoints()
322     {
323         return Arrays.stream(new DirectedPoint3d[] {this}).iterator();
324     }
325 
326     @Override
327     public String toString()
328     {
329         return toString("%f", false);
330     }
331 
332     @Override
333     public String toString(final String doubleFormat, final boolean doNotIncludeClassName)
334     {
335         String format = String.format("%1$s[x=%2$s, y=%2$s, z=%2%s, dirY=%2$s, dirZ=%2$s]",
336                 doNotIncludeClassName ? "" : "DirectedPoint3d ", doubleFormat);
337         return String.format(Locale.US, format, this.x, this.y, this.z, this.dirY, this.dirZ);
338     }
339 
340     @Override
341     public boolean epsilonEquals(final DirectedPoint3d other, final double epsilonCoordinate, final double epsilonRotation)
342             throws NullPointerException, IllegalArgumentException
343     {
344         Throw.whenNull(other, "other");
345         Throw.when(epsilonCoordinate < 0 || epsilonRotation < 0, IllegalArgumentException.class,
346                 "epsilonCoordinate and epsilonRotation may not be negative");
347         Throw.when(Double.isNaN(epsilonCoordinate) || Double.isNaN(epsilonRotation), IllegalArgumentException.class,
348                 "epsilonCoordinate and epsilonRotation may not be NaN");
349         if (Math.abs(this.x - other.x) > epsilonCoordinate)
350         {
351             return false;
352         }
353         if (Math.abs(this.y - other.y) > epsilonCoordinate)
354         {
355             return false;
356         }
357         if (Math.abs(this.z - other.z) > epsilonCoordinate)
358         {
359             return false;
360         }
361         if (Math.abs(AngleUtil.normalizeAroundZero(this.dirZ - other.dirZ)) > epsilonRotation)
362         {
363             return false;
364         }
365         if (Math.abs(AngleUtil.normalizeAroundZero(this.dirY - other.dirY)) > epsilonRotation)
366         {
367             return false;
368         }
369         return true;
370     }
371 
372     @Override
373     public int hashCode()
374     {
375         final int prime = 31;
376         int result = super.hashCode();
377         result = prime * result + Objects.hash(this.dirZ, this.dirY);
378         return result;
379     }
380 
381     @Override
382     @SuppressWarnings("checkstyle:needbraces")
383     public boolean equals(final Object obj)
384     {
385         if (this == obj)
386             return true;
387         if (!super.equals(obj))
388             return false;
389         if (getClass() != obj.getClass())
390             return false;
391         DirectedPoint3d other = (DirectedPoint3d) obj;
392         return Double.doubleToLongBits(this.dirZ) == Double.doubleToLongBits(other.dirZ)
393                 && Double.doubleToLongBits(this.dirY) == Double.doubleToLongBits(other.dirY);
394     }
395 
396 }