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   import java.util.Objects;
8   
9   import org.djutils.base.AngleUtil;
10  import org.djutils.draw.Directed2d;
11  import org.djutils.draw.DrawRuntimeException;
12  import org.djutils.exceptions.Throw;
13  
14  /**
15   * DirectedPoint2d.java.
16   * <p>
17   * Copyright (c) 2023-2024 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
18   * for project information <a href="https://djutils.org" target="_blank"> https://djutils.org</a>. The DJUTILS project is
19   * distributed under a three-clause BSD-style license, which can be found at
20   * <a href="https://djutils.org/docs/license.html" target="_blank"> https://djutils.org/docs/license.html</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 DirectedPoint2d extends Point2d implements Directed2d<DirectedPoint2d>
26  {
27      /** */
28      private static final long serialVersionUID = 20200828L;
29  
30      /** The counter-clockwise rotation around the point in radians. */
31      @SuppressWarnings("checkstyle:visibilitymodifier")
32      public final double dirZ;
33  
34      /**
35       * Construct a new DirectedPoint2d with an x and y coordinate and a direction.
36       * @param x double; the x coordinate
37       * @param y double; the y coordinate
38       * @param dirZ double; the counter-clockwise rotation around the point in radians
39       * @throws IllegalArgumentException when any coordinate or dirZ is NaN
40       */
41      public DirectedPoint2d(final double x, final double y, final double dirZ) throws IllegalArgumentException
42      {
43          super(x, y);
44          Throw.when(Double.isNaN(dirZ), IllegalArgumentException.class, "dirZ must be a number (not NaN)");
45          this.dirZ = dirZ;
46      }
47  
48      /**
49       * Construct a new DirectedPoint2d from an x and y coordinates in a double[] and a direction.
50       * @param xy double[]; the <cite>x</cite> and <cite>y</cite> coordinates in that order
51       * @param dirZ double; the counter-clockwise rotation around the point in radians
52       * @throws NullPointerException when xy is null
53       * @throws IllegalArgumentException when the dimension of xy is not 2 or any value in xy is NaN or rotZ is NaN
54       */
55      public DirectedPoint2d(final double[] xy, final double dirZ) throws IllegalArgumentException
56      {
57          super(xy);
58          Throw.when(Double.isNaN(dirZ), IllegalArgumentException.class, "dirZ must be a number (not NaN)");
59          this.dirZ = dirZ;
60      }
61  
62      /**
63       * Construct a new DirectedPoint2d from an AWT Point2D and a direction.
64       * @param point Point2D; an AWT Point2D
65       * @param dirZ double; the counter-clockwise rotation around the point in radians
66       * @throws IllegalArgumentException when any coordinate in point is NaN, or rotZ is NaN
67       */
68      public DirectedPoint2d(final Point2D point, final double dirZ) throws IllegalArgumentException
69      {
70          super(point);
71          Throw.when(Double.isNaN(dirZ), IllegalArgumentException.class, "dirZ must be a number (not NaN)");
72          this.dirZ = dirZ;
73      }
74  
75      /**
76       * Construct a new DirectedPoint2d from a Point2d and a direction.
77       * @param point Point2d; a point (with or without orientation)
78       * @param dirZ double; the counter-clockwise rotation around the point in radians
79       * @throws IllegalArgumentException when rotZ is NaN
80       */
81      public DirectedPoint2d(final Point2d point, final double dirZ) throws IllegalArgumentException
82      {
83          this(point.x, point.y, dirZ);
84      }
85  
86      /**
87       * Construct a new DirectedPoint2d from two coordinates and the coordinates of a point that the direction goes through.
88       * @param x double; the x coordinate of the of the new DirectedPoint
89       * @param y double; the y coordinate of the of the new DirectedPoint
90       * @param throughX double; the x-coordinate of a point that the direction goes through
91       * @param throughY double; the y-coordinate of a point that the direction goes through
92       * @throws DrawRuntimeException when <cite>throughX</cite> == <cite>x</cite> and <cite>throughY</cite> == <cite>y</cite>, or
93       *             any through-value is NaN
94       */
95      public DirectedPoint2d(final double x, final double y, final double throughX, final double throughY)
96              throws DrawRuntimeException
97      {
98          this(x, y, buildDirection(throughX - x, throughY - y));
99      }
100 
101     /**
102      * Build the direction.
103      * @param dX double; x difference
104      * @param dY double; y difference
105      * @return double; the computed value of dirZ
106      */
107     private static double buildDirection(final double dX, final double dY)
108     {
109         Throw.when(0 == dX && 0 == dY, IllegalArgumentException.class, "Through point may not be equal to point");
110         return Math.atan2(dY, dX);
111     }
112 
113     /**
114      * Construct a new DirectedPoint2d from a Point2d and the coordinates of a point that the direction goes through.
115      * @param point Point2d; the point
116      * @param throughX double; the x coordinate of a point that the direction goes through
117      * @param throughY double; the y coordinate of a point that the direction goes through
118      * @throws NullPointerException when point is null
119      * @throws DrawRuntimeException when <cite>throughX</cite> == <cite>point.x</cite> and <cite>throughY</cite> ==
120      *             <cite>point.y</cite>, or any through-value is NaN
121      */
122     public DirectedPoint2d(final Point2d point, final double throughX, final double throughY)
123             throws NullPointerException, DrawRuntimeException
124     {
125         this(Throw.whenNull(point, "point").x, point.y, throughX, throughY);
126     }
127 
128     @Override
129     public DirectedPoint2d translate(final double dx, final double dy) throws IllegalArgumentException
130     {
131         Throw.when(Double.isNaN(dx) || Double.isNaN(dy), IllegalArgumentException.class, "translation may not be NaN");
132         return new DirectedPoint2d(this.x + dx, this.y + dy, this.dirZ);
133     }
134 
135     @Override
136     public DirectedPoint3d translate(final double dx, final double dy, final double z) throws IllegalArgumentException
137     {
138         return new DirectedPoint3d(this.x + dx, this.y + dy, z, 0, this.dirZ);
139     }
140 
141     @Override
142     public DirectedPoint2d scale(final double factor) throws IllegalArgumentException
143     {
144         Throw.when(Double.isNaN(factor), IllegalArgumentException.class, "factor must be a number (not NaN)");
145         return new DirectedPoint2d(this.x * factor, this.y * factor, this.dirZ);
146     }
147 
148     @Override
149     public DirectedPoint2d neg()
150     {
151         return new DirectedPoint2d(-this.x, -this.y, AngleUtil.normalizeAroundZero(this.dirZ + Math.PI));
152     }
153 
154     @Override
155     public DirectedPoint2d abs()
156     {
157         return new DirectedPoint2d(Math.abs(this.x), Math.abs(this.y), this.dirZ);
158     }
159 
160     @Override
161     public DirectedPoint2d normalize()
162     {
163         double length = Math.sqrt(this.x * this.x + this.y * this.y);
164         Throw.when(length == 0.0, DrawRuntimeException.class, "cannot normalize (0.0, 0.0)");
165         return this.scale(1.0 / length);
166     }
167 
168     /**
169      * Interpolate towards another DirectedPoint2d with a fraction. It is allowed for fraction to be less than zero or larger
170      * than 1. In that case the interpolation turns into an extrapolation. DirZ is interpolated using the
171      * AngleUtil.interpolateShortest method.
172      * @param otherPoint DirectedPoint2d; the other point
173      * @param fraction double; the factor for interpolation towards the other point. When &lt;code&gt;fraction&lt;/code&gt; is
174      *            between 0 and 1, it is an interpolation, otherwise an extrapolation. If <code>fraction</code> is 0;
175      *            <code>this</code> Point is returned; if <code>fraction</code> is 1, the <code>otherPoint</code> is returned
176      * @return DirectedPoint2d; a new OrientedPoint2d at the requested fraction
177      * @throws NullPointerException when otherPoint is null
178      * @throws IllegalArgumentException when fraction is NaN
179      */
180     public DirectedPoint2d interpolate(final DirectedPoint2d otherPoint, final double fraction)
181             throws NullPointerException, IllegalArgumentException
182     {
183         Throw.whenNull(otherPoint, "otherPoint");
184         Throw.when(Double.isNaN(fraction), IllegalArgumentException.class, "fraction must be a number (not NaN)");
185         if (0.0 == fraction)
186         {
187             return this;
188         }
189         if (1.0 == fraction)
190         {
191             return otherPoint;
192         }
193         return new DirectedPoint2d((1.0 - fraction) * this.x + fraction * otherPoint.x,
194                 (1.0 - fraction) * this.y + fraction * otherPoint.y,
195                 AngleUtil.interpolateShortest(this.dirZ, otherPoint.dirZ, fraction));
196     }
197 
198     /**
199      * Return a new DirectedPoint2d with an in-place rotation around the z-axis by the provided rotateZ. The resulting rotation
200      * is normalized between -&pi; and &pi;.
201      * @param rotateZ double; the rotation around the z-axis
202      * @return DirectedPoint2d; a new point with the same coordinates and applied rotation
203      * @throws IllegalArgumentException when deltaRotZ is NaN
204      */
205     public DirectedPoint2d rotate(final double rotateZ) throws IllegalArgumentException
206     {
207         Throw.when(Double.isNaN(rotateZ), IllegalArgumentException.class, "deltaDirZ must be a number (not NaN)");
208         return new DirectedPoint2d(this.x, this.y, AngleUtil.normalizeAroundZero(this.dirZ + rotateZ));
209     }
210     
211     @Override
212     public double getDirZ()
213     {
214         return this.dirZ;
215     }
216 
217     @Override
218     public Iterator<? extends DirectedPoint2d> getPoints()
219     {
220         return Arrays.stream(new DirectedPoint2d[] {this}).iterator();
221     }
222 
223     @Override
224     public String toString()
225     {
226         return toString("%f", false);
227     }
228 
229     @Override
230     public String toString(final String doubleFormat, final boolean doNotIncludeClassName)
231     {
232         String format =
233                 String.format("%1$s[x=%2$s, y=%2$s, dirZ=%2$s]", doNotIncludeClassName ? "" : "DirectedPoint2d ", doubleFormat);
234         return String.format(Locale.US, format, this.x, this.y, this.dirZ);
235     }
236 
237     @Override
238     public boolean epsilonEquals(final DirectedPoint2d other, final double epsilonCoordinate, final double epsilonRotation)
239             throws NullPointerException, IllegalArgumentException
240     {
241         Throw.whenNull(other, "other");
242         Throw.when(epsilonCoordinate < 0 || epsilonRotation < 0, IllegalArgumentException.class,
243                 "epsilonCoordinate and epsilongRotation may not be negative");
244         Throw.when(Double.isNaN(epsilonCoordinate) || Double.isNaN(epsilonRotation), IllegalArgumentException.class,
245                 "epsilonCoordinate and epsilongRotation may not be NaN");
246         if (Math.abs(this.x - other.x) > epsilonCoordinate)
247         {
248             return false;
249         }
250         if (Math.abs(this.y - other.y) > epsilonCoordinate)
251         {
252             return false;
253         }
254         if (Math.abs(AngleUtil.normalizeAroundZero(this.dirZ - other.dirZ)) > epsilonRotation)
255         {
256             return false;
257         }
258         return true;
259     }
260 
261     @Override
262     public int hashCode()
263     {
264         final int prime = 31;
265         int result = super.hashCode();
266         result = prime * result + Objects.hash(this.dirZ);
267         return result;
268     }
269 
270     @Override
271     @SuppressWarnings("checkstyle:needbraces")
272     public boolean equals(final Object obj)
273     {
274         if (this == obj)
275             return true;
276         if (!super.equals(obj))
277             return false;
278         if (getClass() != obj.getClass())
279             return false;
280         DirectedPoint2d other = (DirectedPoint2d) obj;
281         return Double.doubleToLongBits(this.dirZ) == Double.doubleToLongBits(other.dirZ);
282     }
283 
284 }