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