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 }