1 package org.djutils.math;
2
3 /**
4 * AngleUtil has some base methods to deal with angles, such as normalization between -PI and PI, and between 0 and 2*PI. <br>
5 * <br>
6 * Copyright (c) 2020-2025 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
7 * for project information <a href="https://djutils.org" target="_blank"> https://djutils.org</a>. The DJUTILS project is
8 * distributed under a three-clause BSD-style license, which can be found at
9 * <a href="https://djutils.org/docs/license.html" target="_blank"> https://djutils.org/docs/license.html</a>. <br>
10 * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
11 * @author <a href="https://www.tudelft.nl/pknoppers">Peter Knoppers</a>
12 */
13 public final class AngleUtil
14 {
15 /** Utility constructor. */
16 private AngleUtil()
17 {
18 // Utility constructor
19 }
20
21 /** The value 2π. */
22 public static final double PI2 = 2.0 * Math.PI;
23
24 /**
25 * Normalize an angle (in radians) in a 2π wide interval around π, resulting in angles in the 0 to 2π interval. An
26 * angle value of NaN or Infinity returns NaN.
27 * @param angle the angle in radians to normalize
28 * @return the normalized angle in radians
29 */
30 public static double normalizeAroundPi(final double angle)
31 {
32 return angle - PI2 * Math.floor(angle / PI2);
33 }
34
35 /**
36 * Normalize an angle (in radians) in a 2π wide interval around 0, resulting in angles in the -π to π interval. An
37 * angle value of NaN or Infinity returns NaN.
38 * @param angle the angle in radians to normalize
39 * @return the normalized angle in radians
40 */
41 public static double normalizeAroundZero(final double angle)
42 {
43 return angle + PI2 * Math.floor((-angle + Math.PI) / PI2);
44 }
45
46 /**
47 * Return whether two angles (in radians) are less than epsilon apart. The method returns true, for instance when
48 * epsilonEquals(-Math.PI, Math.PI, 1E-6) is called, since the -PI and +PI angles have the same angle.
49 * @param angle1 the first angle in radians
50 * @param angle2 the second angle in radians
51 * @param epsilon the precision
52 * @return whether the two angles are less than epsilon apart
53 */
54 public static boolean epsilonEquals(final double angle1, final double angle2, final double epsilon)
55 {
56 if (Math.abs(angle1 - angle2) < epsilon)
57 {
58 return true;
59 }
60 double diff = Math.abs(normalizeAroundZero(angle2) - normalizeAroundZero(angle1));
61 return diff < epsilon || Math.abs(PI2 - diff) < epsilon;
62 }
63
64 /**
65 * Interpolate between two angles that will first be normalized between 0 and 2π, and take the interpolated angle when
66 * going clockwise from the first angle to the second angle. The fraction indicates where the angle between angle1 and
67 * angle2 will be. A fraction of 0.0 returns angle1, a fraction of 1.0 returns angle2, and a fraction of 0.5 returns the
68 * shortest angle halfway. A fraction of less than 0 or larger than 1 can be used for extrapolation. The resulting angle is
69 * normalized between -π and π. This means that the halfway interpolation between 0 and 2π and between 2π and 0
70 * are both 0, since the two angles are the same. Likewise, the halfway interpolation between -π and π is +/- π
71 * where the sign depends on rounding.
72 * @param angle1 the first angle in radians
73 * @param angle2 the second angle in radians
74 * @param fraction the fraction for interpolation; 0.5 is halfway
75 * @return the normalized angle between angle1 and angle2
76 */
77 public static double interpolateClockwise(final double angle1, final double angle2, final double fraction)
78 {
79 double a1 = normalizeAroundPi(angle1);
80 double a2 = normalizeAroundPi(angle2);
81 if (a2 < a1)
82 {
83 a2 += PI2;
84 }
85 return normalizeAroundZero((1.0 - fraction) * a1 + fraction * a2);
86 }
87
88 /**
89 * Interpolate between two angles taking the <i>shortest way</i>.
90 * @param angle1 the first angle in radians
91 * @param angle2 the second angle in radians
92 * @param fraction the fraction for interpolation; 0.5 is halfway, 0.0 returns angle1, 1.0 returns angle2.
93 * @return The interpolated angle, normalized around zero. If any input angle is not normalized around zero the
94 * result will still be normalized, and the exact equality at fraction is 0.0 or 1.0 may not hold. When the
95 * difference between angle1 and angle2 is very close to an odd multiple of PI, the <i>shortest way</i> between
96 * those angles is ill-defined. The result of this method reflects this (fundamental) problem.
97 */
98 public static double interpolateShortest(final double angle1, final double angle2, final double fraction)
99 {
100 double result;
101 if (fraction == 1.0)
102 {
103 result = angle2; // Make sure that the promise of the exact result holds
104 }
105 else
106 {
107 double difference = angle2 - angle1;
108 if (difference < -Math.PI)
109 {
110 difference = difference - PI2 * Math.ceil((difference - Math.PI) / PI2);
111 }
112 if (difference > Math.PI)
113 {
114 difference = difference - PI2 * Math.floor((difference + Math.PI) / PI2);
115 }
116 result = angle1 + fraction * difference;
117 }
118 return normalizeAroundZero(result);
119 }
120
121 }