View Javadoc
1   package org.djutils.draw.line;
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.Drawable3d;
10  import org.djutils.draw.Space3d;
11  import org.djutils.draw.bounds.Bounds3d;
12  import org.djutils.draw.point.Point3d;
13  import org.djutils.exceptions.Throw;
14  
15  /**
16   * Ray3d is a half-line; it has one end point with non-infinite coordinates; the other end point is infinitely far away.
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 Ray3d extends Point3d implements Drawable3d, Ray<Ray3d, Point3d, Space3d>
25  {
26      /** */
27      private static final long serialVersionUID = 20210119L;
28  
29      /** Phi; the angle from the positive X axis direction in radians. */
30      @SuppressWarnings("checkstyle:visibilitymodifier")
31      public final double phi;
32  
33      /** Theta; the angle from the positive Z axis direction in radians. */
34      @SuppressWarnings("checkstyle:visibilitymodifier")
35      public final double theta;
36  
37      /**
38       * Construct a new Ray3d.
39       * @param x double; the x coordinate of the finite end point of the ray
40       * @param y double; the y coordinate of the finite end point of the ray
41       * @param z double; the z coordinate of the finite end point of the ray
42       * @param phi double; the angle from the positive X axis direction in radians.
43       * @param theta double; the angle from the positive Z axis direction in radians
44       * @throws DrawRuntimeException when phi is NaN
45       */
46      public Ray3d(final double x, final double y, final double z, final double phi, final double theta)
47              throws DrawRuntimeException
48      {
49          super(x, y, z);
50          Throw.when(Double.isNaN(phi) || Double.isNaN(theta), DrawRuntimeException.class, "phi and theta may not be NaN");
51          this.phi = phi;
52          this.theta = theta;
53      }
54  
55      /**
56       * Construct a new Ray3d.
57       * @param point Point3d; the finite end point of the ray
58       * @param phi double; the angle from the positive X axis direction in radians.
59       * @param theta double; the angle from the positive Z axis direction in radians
60       * @throws NullPointerException when point is null
61       * @throws DrawRuntimeException when phi is NaN, or theta is NaN
62       */
63      public Ray3d(final Point3d point, final double phi, final double theta) throws NullPointerException, DrawRuntimeException
64      {
65          this(Throw.whenNull(point, "point may not be null").x, point.y, point.z, phi, theta);
66      }
67  
68      /**
69       * Construct a new Ray3d.
70       * @param x double; the x coordinate of the finite end point of the ray
71       * @param y double; the y coordinate of the finite end point of the ray
72       * @param z double; the z coordinate of the finite end point of the ray
73       * @param throughX double; the x coordinate of another point on the ray
74       * @param throughY double; the y coordinate of another point on the ray
75       * @param throughZ double; the z coordinate of another point on the ray
76       * @throws DrawRuntimeException when throughX == x and throughY == y
77       */
78      public Ray3d(final double x, final double y, final double z, final double throughX, final double throughY,
79              final double throughZ) throws DrawRuntimeException
80      {
81          super(x, y, z);
82          Throw.when(throughX == x && throughY == y && throughZ == z, DrawRuntimeException.class,
83                  "the coordinates of the through point must differ from (x, y, z)");
84          this.phi = Math.atan2(throughY - y, throughX - x);
85          this.theta = Math.atan2(Math.hypot(throughX - x, throughY - y), throughZ - z);
86      }
87  
88      /**
89       * Construct a new Ray3d.
90       * @param point Point3d; the finite end point of the ray
91       * @param throughX double; the x coordinate of another point on the ray
92       * @param throughY double; the y coordinate of another point on the ray
93       * @param throughZ double; the z coordinate of another point on the ray
94       * @throws NullPointerException when point is null
95       * @throws DrawRuntimeException when throughX == point.x and throughY == point.y
96       */
97      public Ray3d(final Point3d point, final double throughX, final double throughY, final double throughZ)
98              throws NullPointerException, DrawRuntimeException
99      {
100         this(Throw.whenNull(point, "point may not be null").x, point.y, point.z, throughX, throughY, throughZ);
101     }
102 
103     /**
104      * Construct a new Ray3d.
105      * @param x double; the x coordinate of the finite end point of the ray
106      * @param y double; the y coordinate of the finite end point of the ray
107      * @param z double; the z coordinate of the finite end point of the ray
108      * @param throughPoint Point3d; another point on the ray
109      * @throws NullPointerException when throughPoint is null
110      * @throws DrawRuntimeException when throughPoint is exactly at (x, y)
111      */
112     public Ray3d(final double x, final double y, final double z, final Point3d throughPoint)
113             throws NullPointerException, DrawRuntimeException
114     {
115         this(x, y, z, Throw.whenNull(throughPoint, "througPoint may not be null").x, throughPoint.y, throughPoint.z);
116     }
117 
118     /**
119      * Construct a new Ray3d.
120      * @param point Point3d; the finite end point of the ray
121      * @param throughPoint Point3d; another point on the ray
122      * @throws NullPointerException when point is null or throughPoint is null
123      * @throws DrawRuntimeException when throughPoint is exactly at point
124      */
125     public Ray3d(final Point3dd.html#Point3d">Point3d point, final Point3d throughPoint) throws NullPointerException, DrawRuntimeException
126     {
127         this(Throw.whenNull(point, "point may not be null").x, point.y, point.z,
128                 Throw.whenNull(throughPoint, "throughPoint may not be null").x, throughPoint.y, throughPoint.z);
129     }
130 
131     /** {@inheritDoc} */
132     @Override
133     public final double getPhi()
134     {
135         return this.phi;
136     }
137 
138     /**
139      * Retrieve the angle from the positive Z axis direction in radians.
140      * @return double; the angle from the positive Z axis direction in radians
141      */
142     public final double getTheta()
143     {
144         return this.theta;
145     }
146 
147     /** {@inheritDoc} */
148     @Override
149     public Point3d getEndPoint()
150     {
151         return new Point3d(this.x, this.y, this.z);
152     }
153 
154     /** {@inheritDoc} */
155     @Override
156     public int size()
157     {
158         return 2;
159     }
160 
161     /** {@inheritDoc} */
162     @Override
163     public Iterator<Point3d> getPoints()
164     {
165         double sinPhi = Math.sin(this.phi);
166         double cosPhi = Math.cos(this.phi);
167         double sinTheta = Math.sin(this.theta);
168         double cosTheta = Math.cos(this.theta);
169         Point3doint3d.html#Point3d">Point3d.html#Point3d">Point3d[] array = new Point3doint3d.html#Point3d">Point3d[] { new Point3d(this.x, this.y, this.z),
170                 new Point3d(cosPhi * sinTheta == 0 ? this.x : cosPhi * sinTheta * Double.POSITIVE_INFINITY,
171                         cosPhi * sinPhi == 0 ? this.y : cosPhi * sinPhi * Double.POSITIVE_INFINITY,
172                         cosTheta == 0 ? this.z : cosTheta * Double.POSITIVE_INFINITY) };
173         return Arrays.stream(array).iterator();
174     }
175 
176     /** {@inheritDoc} */
177     @Override
178     public Bounds3d getBounds()
179     {
180         double sinPhi = Math.sin(this.phi);
181         double cosPhi = Math.cos(this.phi);
182         double sinTheta = Math.sin(this.theta);
183         double cosTheta = Math.cos(this.theta);
184         return new Bounds3d(cosPhi * sinTheta >= 0 ? this.x : Double.NEGATIVE_INFINITY,
185                 cosPhi * sinTheta <= 0 ? this.x : Double.POSITIVE_INFINITY,
186                 sinPhi * sinTheta >= 0 ? this.y : Double.NEGATIVE_INFINITY,
187                 sinPhi * sinTheta <= 0 ? this.y : Double.POSITIVE_INFINITY, cosTheta >= 0 ? this.z : Double.NEGATIVE_INFINITY,
188                 cosTheta <= 0 ? this.z : Double.POSITIVE_INFINITY);
189     }
190 
191     /** {@inheritDoc} */
192     @Override
193     public Ray3d neg()
194     {
195         return new Ray3d(-this.x, -this.y, -this.z, this.phi + Math.PI, this.theta + Math.PI);
196     }
197 
198     /** {@inheritDoc} */
199     @Override
200     public Ray3d flip()
201     {
202         return new Ray3d(this.x, this.y, this.z, this.phi + Math.PI, Math.PI - this.theta);
203     }
204 
205     /** {@inheritDoc} */
206     @Override
207     public Ray3d getLocationExtended(final double position) throws DrawRuntimeException
208     {
209         Throw.when(Double.isNaN(position) || Double.isInfinite(position), DrawRuntimeException.class,
210                 "position must be finite");
211         double sinTheta = Math.sin(this.theta);
212         double dX = Math.cos(this.phi) * sinTheta;
213         double dY = Math.sin(this.phi) * sinTheta;
214         double dZ = Math.cos(this.theta);
215         return new Ray3d(this.x + dX * position, this.y + dY * position, this.z + dZ * position, this.phi, this.theta);
216     }
217 
218     /** {@inheritDoc} */
219     @Override
220     public Point3dt3d">Point3d closestPointOnRay(final Point3d point) throws NullPointerException
221     {
222         Throw.whenNull(point, "point may not be null");
223         double sinTheta = Math.sin(this.theta);
224         return point.closestPointOnLine(this.x, this.y, this.z, this.x + Math.cos(this.phi) * sinTheta,
225                 this.y + Math.sin(this.phi) * sinTheta, this.z + Math.cos(this.theta), true, false);
226     }
227 
228     /** {@inheritDoc} */
229     @Override
230     public Point3dt3d">Point3d projectOrthogonal(final Point3d point) throws NullPointerException
231     {
232         Throw.whenNull(point, "point may not be null");
233         double sinTheta = Math.sin(this.theta);
234         return point.closestPointOnLine(this.x, this.y, this.z, this.x + Math.cos(this.phi) * sinTheta,
235                 this.y + Math.sin(this.phi) * sinTheta, this.z + Math.cos(this.theta), null, false);
236     }
237 
238     /** {@inheritDoc} */
239     @Override
240     public Point3dnt3d projectOrthogonalExtended(final Point3d point)
241     {
242         Throw.whenNull(point, "point may not be null");
243         double sinTheta = Math.sin(this.theta);
244         return point.closestPointOnLine(getX(), getY(), getZ(), getX() + Math.cos(this.phi) * sinTheta,
245                 getY() + Math.sin(this.phi) * sinTheta, getZ() + Math.cos(this.theta), false, false);
246     }
247 
248     /** {@inheritDoc} */
249     @Override
250     public double projectOrthogonalFractional(final Point3d point) throws NullPointerException
251     {
252         Throw.whenNull(point, "point may not be null");
253         double sinTheta = Math.sin(this.theta);
254         return point.fractionalPositionOnLine(this.x, this.y, this.z, this.x + Math.cos(this.phi) * sinTheta,
255                 this.y + Math.sin(this.phi) * sinTheta, this.z + Math.cos(this.theta), null, false);
256     }
257 
258     /** {@inheritDoc} */
259     @Override
260     public double projectOrthogonalFractionalExtended(final Point3d point) throws NullPointerException
261     {
262         Throw.whenNull(point, "point may not be null");
263         double sinTheta = Math.sin(this.theta);
264         return point.fractionalPositionOnLine(getX(), getY(), getZ(), getX() + Math.cos(this.phi) * sinTheta,
265                 getY() + Math.sin(this.phi) * sinTheta, getZ() + Math.cos(this.theta), false, false);
266     }
267 
268     /** {@inheritDoc} */
269     @Override
270     public boolean epsilonEquals(final Ray3d other, final double epsilonCoordinate, final double epsilonRotation)
271             throws NullPointerException, IllegalArgumentException
272     {
273         Throw.whenNull(other, "other point may not be null");
274         Throw.when(
275                 Double.isNaN(epsilonCoordinate) || epsilonCoordinate < 0 || Double.isNaN(epsilonRotation)
276                         || epsilonRotation < 0,
277                 IllegalArgumentException.class, "epsilon values may not be negative and may not be NaN");
278         if (Math.abs(this.x - other.x) > epsilonCoordinate)
279         {
280             return false;
281         }
282         if (Math.abs(this.y - other.y) > epsilonCoordinate)
283         {
284             return false;
285         }
286         if (Math.abs(this.z - other.z) > epsilonCoordinate)
287         {
288             return false;
289         }
290         if ((Math.abs(AngleUtil.normalizeAroundZero(this.phi - other.phi)) > epsilonRotation
291                 || Math.abs(AngleUtil.normalizeAroundZero(this.theta - other.theta)) > epsilonRotation)
292                 && (Math.abs(AngleUtil.normalizeAroundZero(Math.PI + this.phi - other.phi)) > epsilonRotation
293                         || Math.abs(AngleUtil.normalizeAroundZero(Math.PI - this.theta - other.theta)) > epsilonRotation))
294         {
295             return false;
296         }
297         return true;
298         // FIXME this method should return true if the same angle is approximated with inverted theta and phi off by PI
299     }
300 
301     /** {@inheritDoc} */
302     @Override
303     public String toString()
304     {
305         return toString("%f", false);
306     }
307 
308     /** {@inheritDoc} */
309     @Override
310     public String toString(final String doubleFormat, final boolean doNotIncludeClassName)
311     {
312         String format = String.format("%1$s[x=%2$s, y=%2$s, z=%2$s, phi=%2$s, theta=%2$s]", doNotIncludeClassName ? "" : "Ray3d ",
313                 doubleFormat);
314         return String.format(Locale.US, format, this.x, this.y, this.z, this.phi, this.theta);
315     }
316 
317     /** {@inheritDoc} */
318     @Override
319     public int hashCode()
320     {
321         final int prime = 31;
322         int result = super.hashCode();
323         long temp;
324         temp = Double.doubleToLongBits(this.phi);
325         result = prime * result + (int) (temp ^ (temp >>> 32));
326         temp = Double.doubleToLongBits(this.theta);
327         result = prime * result + (int) (temp ^ (temp >>> 32));
328         return result;
329     }
330 
331     /** {@inheritDoc} */
332     @Override
333     @SuppressWarnings("checkstyle:needbraces")
334     public boolean equals(final Object obj)
335     {
336         if (this == obj)
337             return true;
338         if (!super.equals(obj))
339             return false;
340         if (getClass() != obj.getClass())
341             return false;
342         Ray3d="../../../../org/djutils/draw/line/Ray3d.html#Ray3d">Ray3d other = (Ray3d) obj;
343         if (Double.doubleToLongBits(this.phi) != Double.doubleToLongBits(other.phi))
344             return false;
345         if (Double.doubleToLongBits(this.theta) != Double.doubleToLongBits(other.theta))
346             return false;
347         return true;
348     }
349 
350 }