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