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