1 /**
2    S1Angle represents a one-dimensional angle (as opposed to a two-dimensional solid angle).
3 
4    Copyright: 2005 Google Inc. All Rights Reserved.
5 
6    License:
7    Licensed under the Apache License, Version 2.0 (the "License");
8    you may not use this file except in compliance with the License.
9    You may obtain a copy of the License at
10 
11    $(LINK http://www.apache.org/licenses/LICENSE-2.0)
12 
13    Unless required by applicable law or agreed to in writing, software
14    distributed under the License is distributed on an "AS-IS" BASIS,
15    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16    See the License for the specific language governing permissions and
17    limitations under the License.
18 
19    Authors: ericv@google.com (Eric Veach), madric@gmail.com (Vijay Nayar)
20 */
21 module s2.s1angle;
22 
23 import s2.s2point;
24 import s2.util.math.mathutil;
25 import s2.util.math.vector;
26 import s2.s2latlng;
27 import math = std.math;
28 import format = std.format;
29 
30 /**
31  * This class represents a one-dimensional angle (as opposed to a
32  * two-dimensional solid angle).  It has methods for converting angles to
33  * or from radians, degrees, and the E5/E6/E7 representations (i.e. degrees
34  * multiplied by 1e5/1e6/1e7 and rounded to the nearest integer).
35  *
36  * The internal representation is a double-precision value in radians, so
37  * conversion to and from radians is exact.  Conversions between E5, E6, E7,
38  * and Degrees are not always exact; for example, Degrees(3.1) is different
39  * from E6(3100000) or E7(310000000).  However, the following properties are
40  * guaranteed for any integer "n", provided that "n" is in the input range of
41  * both functions:
42  * ---
43  *     Degrees(n) == E6(1000000 * n)
44  *     Degrees(n) == E7(10000000 * n)
45  *          E6(n) == E7(10 * n)
46  * ---
47  * The corresponding properties are *not* true for E5, so if you use E5 then
48  * don't test for exact equality when comparing to other formats such as
49  * Degrees or E7.
50  *
51  * The following conversions between degrees and radians are exact:
52  * ---
53  *          Degrees(180) == Radians(M_PI)
54  *       Degrees(45 * k) == Radians(k * M_PI / 4)  for k == 0..8
55  * ---
56  * These identities also hold when the arguments are scaled up or down by any
57  * power of 2.  Some similar identities are also true, for example,
58  * Degrees(60) == Radians(M_PI / 3), but be aware that this type of identity
59  * does not hold in general.  For example, Degrees(3) != Radians(M_PI / 60).
60  *
61  * Similarly, the conversion to radians means that Angle::Degrees(x).degrees()
62  * does not always equal "x".  For example,
63  *
64  *         `S1Angle.degrees(45 * k).degrees() == 45 * k`      for k == 0..8
65  *   but       `S1Angle.degrees(60).degrees() != 60`.
66  *
67  * This means that when testing for equality, you should allow for numerical
68  * errors (EXPECT_DOUBLE_EQ) or convert to discrete E5/E6/E7 values first.
69  *
70  * Caveat: All of the above properties depend on "double" being the usual
71  * 64-bit IEEE 754 type (which is true on almost all modern platforms).
72  *
73  * This class is intended to be copied by value as desired.  It uses
74  * the default copy constructor and assignment operator.
75  */
76 struct S1Angle {
77 private:
78   /// The default constructor yields a zero angle.  This is useful for STL
79   /// containers and class methods with output arguments.
80   double _radians = 0;
81 
82   this(double radians) {
83     _radians = radians;
84   }
85 
86 public:
87   /// These methods construct S1Angle objects from their measure in radians or degrees.
88   static S1Angle fromRadians(double radians) {
89     return S1Angle(radians);
90   }
91 
92   static S1Angle fromDegrees(double degrees) {
93     return S1Angle((M_PI / 180) * degrees);
94   }
95 
96   static S1Angle fromE5(int e5) {
97     return S1Angle.fromDegrees(1e-5 * e5);
98   }
99 
100   static S1Angle fromE6(int e6) {
101     return S1Angle.fromDegrees(1e-6 * e6);
102   }
103 
104   static S1Angle fromE7(int e7) {
105     return S1Angle.fromDegrees(1e-7 * e7);
106   }
107 
108   // Convenience functions -- to use when args have been fixed32s in protos.
109   //
110   // The arguments are cast into int, so very large unsigned values
111   // are treated as negative numbers.
112   static S1Angle fromUnsignedE6(uint e6) {
113     return S1Angle.fromE6(cast(int) e6);
114   }
115 
116   static S1Angle fromUnsignedE7(uint e7) {
117     return S1Angle.fromE7(cast(int) e7);
118   }
119 
120   // Return an angle larger than any finite angle.
121   static S1Angle infinity() {
122     return S1Angle(double.infinity);
123   }
124 
125   /// A explicit shorthand for the default constructor.
126   static S1Angle zero() {
127     return S1Angle(0);
128   }
129 
130   /**
131      Return the angle between two points, which is also equal to the distance
132      between these points on the unit sphere.  The points do not need to be
133      normalized.  This function has a maximum error of 3.25 * DBL_EPSILON (or
134      2.5 * double.epsilon for angles up to 1 radian).
135   */
136   this(in S2Point x, in S2Point y) {
137     _radians = x.angle(y);
138   }
139 
140   /**
141      Like the constructor above, but return the angle (i.e., distance) between
142      two S2LatLng points.  This function has about 15 digits of accuracy for
143      small distances but only about 8 digits of accuracy as the distance
144      approaches 180 degrees (i.e., nearly-antipodal points).
145   */
146   this(in S2LatLng x, in S2LatLng y) {
147     _radians = x.getDistance(y).radians();
148   }
149 
150   double radians() const {
151     return _radians;
152   }
153 
154   double degrees() const {
155     return (180 / M_PI) * _radians;
156   }
157 
158   // Note that the E5, E6, and E7 conversion involve two multiplications rather
159   // than one.  This is mainly for backwards compatibility (changing this would
160   // break many tests), but it does have the nice side effect that conversions
161   // between Degrees, E6, and E7 are exact when the arguments are integers.
162 
163   int e5() const {
164     return cast(int) math.lround(1e5 * degrees());
165   }
166 
167   int e6() const {
168     return cast(int) math.lround(1e6 * degrees());
169   }
170 
171   int e7() const {
172     return cast(int) math.lround(1e7 * degrees());
173   }
174 
175   /// Returns the absolute value of an angle.
176   S1Angle abs() const {
177     return S1Angle(math.fabs(_radians));
178   }
179 
180 
181   /// Comparison operators.
182   bool opEquals(in S1Angle y) const {
183     return radians() == y.radians();
184   }
185 
186   int opCmp(in S1Angle y) const {
187     if (radians() == y.radians()) {
188       return 0;
189     }
190     return radians() > y.radians() ? 1 : -1;
191   }
192 
193   /// Implement negation.
194   S1Angle opUnary(string op)() const
195   if (op == "-") {
196     return S1Angle(-radians());
197   }
198 
199   /// Simple arithmetic operators for manipulating S1Angles.
200   S1Angle opOpAssign(string op)(in S1Angle v) {
201     mixin("_radians " ~ op ~ "= v.radians();");
202     return this;
203   }
204 
205   S1Angle opOpAssign(string op)(in double v) {
206     mixin("_radians " ~ op ~ "= v;");
207     return this;
208   }
209 
210   S1Angle opBinary(string op)(in S1Angle v) const {
211     return mixin("S1Angle(radians() " ~ op ~ " v.radians())");
212   }
213 
214   S1Angle opBinary(string op)(in double v) const {
215     return mixin("S1Angle(radians() " ~ op ~ " v)");
216   }
217 
218   S1Angle opBinaryRight(string op)(double v) const {
219     return mixin("S1Angle(v " ~ op ~ " radians())");
220   }
221 
222   /// Returns the angle normalized to the range \(-180, 180] degrees.
223   S1Angle normalized() const {
224     auto a = S1Angle(_radians);
225     a.normalize();
226     return a;
227   }
228 
229   /// Normalizes this angle to the range \(-180, 180] degrees.
230   void normalize() {
231     _radians = math.remainder(_radians, 2.0 * M_PI);
232     if (_radians <= -M_PI) {
233       _radians = M_PI;
234     }
235   }
236 
237   string toString() const {
238     return format.format("%.7f", degrees());
239   }
240 }
241 
242 /// Trigonmetric functions (not necessary but slightly more convenient).
243 double sin(in S1Angle a) {
244   return math.sin(a.radians());
245 }
246 
247 double cos(in S1Angle a) {
248   return math.cos(a.radians());
249 }
250 
251 double tan(in S1Angle a) {
252   return math.tan(a.radians());
253 }