1 // Copyright 2013 Google Inc. All Rights Reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS-IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Original author: ericv@google.com (Eric Veach) 16 // Converted to D: madric@gmail.com (Vijay Nayar) 17 18 module s2.s1chord_angle; 19 20 import s2.s1angle; 21 import s2.s2point; 22 import s2pointutil = s2.s2pointutil; 23 import algorithm = std.algorithm; 24 import math = std.math; 25 26 /** 27 * S1ChordAngle represents the angle subtended by a chord (i.e., the straight 28 * line segment connecting two points on the sphere). Its representation 29 * makes it very efficient for computing and comparing distances, but unlike 30 * S1Angle it is only capable of representing angles between 0 and Pi radians. 31 * Generally, S1ChordAngle should only be used in loops where many angles need 32 * to be calculated and compared. Otherwise it is simpler to use S1Angle. 33 * 34 * S1ChordAngle also loses some accuracy as the angle approaches Pi radians. 35 * Specifically, the representation of (Pi - x) radians has an error of about 36 * (1e-15 / x), with a maximum error of about 2e-8 radians (about 13cm on the 37 * Earth's surface). For comparison, for angles up to 90 degrees (10000km) 38 * the worst-case representation error is about 2e-16 radians (1 nanonmeter), 39 * which is about the same as S1Angle. 40 * 41 * This class is intended to be copied by value as desired. It uses 42 * the default copy constructor and assignment operator. 43 */ 44 struct S1ChordAngle { 45 private: 46 static immutable double MAX_LENGTH2 = 4.0; 47 48 // S1ChordAngles are represented by the squared chord length, which can 49 // range from 0 to 4. Infinity() uses an infinite squared length. 50 this(double length2) 51 in { 52 assert(isValid()); 53 } do { 54 _length2 = length2; 55 } 56 57 double _length2 = 0; 58 59 public: 60 /// Construct the S1ChordAngle corresponding to the distance between the two 61 /// given points. The points must be unit length. 62 this(in S2Point x, in S2Point y) 63 in { 64 assert(s2pointutil.isUnitLength(x)); 65 assert(s2pointutil.isUnitLength(y)); 66 } out { 67 isValid(); 68 } do { 69 // The distance may slightly exceed MAX_LENGTH2 due to roundoff errors. 70 // The maximum error in the result is 2 * DBL_EPSILON * length2_. 71 _length2 = algorithm.min(MAX_LENGTH2, (x - y).norm2()); 72 } 73 74 this(in S1ChordAngle chordAngle) { 75 _length2 = chordAngle.length2(); 76 } 77 78 /** 79 * Conversion from an S1Angle. Angles outside the range [0, Pi] are handled 80 * as follows: Infinity() is mapped to Infinity(), negative angles are 81 * mapped to Negative(), and finite angles larger than Pi are mapped to 82 * Straight(). 83 * 84 * Note that this operation is relatively expensive and should be avoided. 85 * To use S1ChordAngle effectively, you should structure your code so that 86 * input arguments are converted to S1ChordAngles at the beginning of your 87 * algorithm, and results are converted back to S1Angles only at the end. 88 */ 89 this(in S1Angle angle) 90 out { 91 assert(isValid(), toString() ~ " is invalid."); 92 } do { 93 if (angle.radians() < 0) { 94 this = negative(); 95 } else if (angle == S1Angle.infinity()) { 96 this = infinity(); 97 } else { 98 // The chord length is 2 * sin(angle / 2). 99 double length = 2 * math.sin(0.5 * algorithm.min( 100 M_PI, 101 math.isFinite(angle.radians()) ? angle.radians() : double.max)); 102 _length2 = length * length; 103 } 104 } 105 106 107 /// Return the zero chord angle. 108 static S1ChordAngle zero() { 109 return S1ChordAngle(0); 110 } 111 112 /// Return a chord angle of 90 degrees (a "right angle"). 113 static S1ChordAngle right() { 114 return S1ChordAngle(2); 115 } 116 117 /// Return a chord angle of 180 degrees (a "straight angle"). This is the 118 /// maximum finite chord angle. 119 static S1ChordAngle straight() { 120 return S1ChordAngle(4); 121 } 122 123 /** 124 * Return a chord angle larger than any finite chord angle. The only valid 125 * operations on Infinity() are comparisons, S1Angle conversions, and 126 * Successor() / Predecessor(). 127 */ 128 static S1ChordAngle infinity() { 129 return S1ChordAngle(double.infinity); 130 } 131 132 /** 133 * Return a chord angle smaller than Zero(). The only valid operations on 134 * Negative() are comparisons, S1Angle conversions, and Successor() / 135 * Predecessor(). 136 */ 137 static S1ChordAngle negative() { 138 return S1ChordAngle(-1); 139 } 140 141 /// Convenience methods implemented by converting from an S1Angle. 142 static S1ChordAngle fromRadians(double radians) { 143 return S1ChordAngle(S1Angle.fromRadians(radians)); 144 } 145 146 static S1ChordAngle fromDegrees(double degrees) { 147 return S1ChordAngle(S1Angle.fromDegrees(degrees)); 148 } 149 150 S1ChordAngle fromE5(int e5) { 151 return S1ChordAngle(S1Angle.fromE5(e5)); 152 } 153 154 S1ChordAngle fromE6(int e6) { 155 return S1ChordAngle(S1Angle.fromE6(e6)); 156 } 157 158 S1ChordAngle fromE7(int e7) { 159 return S1ChordAngle(S1Angle.fromE7(e7)); 160 } 161 162 /** 163 * Construct an S1ChordAngle that is an upper bound on the given S1Angle, 164 * i.e. such that FastUpperBoundFrom(x).ToAngle() >= x. Unlike the S1Angle 165 * constructor above, this method is very fast, and the bound is accurate to 166 * within 1% for distances up to about 3100km on the Earth's surface. 167 */ 168 static S1ChordAngle fastUpperBoundFrom(in S1Angle angle) { 169 // This method uses the distance along the surface of the sphere as an upper 170 // bound on the distance through the sphere's interior. 171 return S1ChordAngle.fromLength2(angle.radians() * angle.radians()); 172 } 173 174 /** 175 * Construct an S1ChordAngle from the squared chord length. Note that the 176 * argument is automatically clamped to a maximum of 4.0 to handle possible 177 * roundoff errors. The argument must be non-negative. 178 */ 179 static S1ChordAngle fromLength2(double length2) { 180 return S1ChordAngle(algorithm.min(4.0, length2)); 181 } 182 183 /** 184 * Converts to an S1Angle. 185 * 186 * Infinity() is converted to S1Angle::Infinity(), and Negative() is 187 * converted to an unspecified negative S1Angle. 188 * 189 * Note that the conversion uses trigonometric functions and therefore 190 * should be avoided in inner loops. 191 */ 192 S1Angle toS1Angle() const { 193 if (isNegative()) { 194 return S1Angle.fromRadians(-1); 195 } 196 if (isInfinity()) { 197 return S1Angle.infinity(); 198 } 199 return S1Angle.fromRadians(2 * math.asin(0.5 * math.sqrt(_length2))); 200 } 201 202 /** 203 * Convenience methods implemented by calling ToAngle() first. Note that 204 * because of the S1Angle conversion these methods are relatively expensive 205 * (despite their lowercase names), so the results should be cached if they 206 * are needed inside loops. 207 */ 208 double radians() const { 209 return toS1Angle().radians(); 210 } 211 212 double degrees() const { 213 return toS1Angle().degrees(); 214 } 215 216 int e5() const { 217 return toS1Angle().e5(); 218 } 219 220 int e6() const { 221 return toS1Angle().e6(); 222 } 223 224 int e7() const { 225 return toS1Angle().e7(); 226 } 227 228 // All operators and functions are declared here so that we can put them all 229 // in one place. (The compound assignment operators must be put here.) 230 231 // Comparison operators. 232 bool opEquals(in S1ChordAngle x) const { 233 return length2() == x.length2(); 234 } 235 236 int opCmp(in S1ChordAngle x) const { 237 if (length2() == x.length2()) { 238 return 0; 239 } 240 return length2() < x.length2() ? -1 : 1; 241 } 242 243 /// Comparison predicates. 244 bool isZero() const { 245 return _length2 == 0; 246 } 247 248 bool isNegative() const { 249 // TODO(ericv): Consider stricter check here -- only allow Negative(). 250 return _length2 < 0; 251 } 252 253 bool isInfinity() const { 254 return _length2 == double.infinity; 255 } 256 257 /// Negative or infinity. 258 bool isSpecial() const { 259 return isNegative() || isInfinity(); 260 } 261 262 /** 263 * Only addition and subtraction of S1ChordAngles is supported. These 264 * methods add or subtract the corresponding S1Angles, and clamp the result 265 * to the range [0, Pi]. Both arguments must be non-negative and 266 * non-infinite. 267 * 268 * REQUIRES: !a.is_special() && !b.is_special() 269 */ 270 S1ChordAngle opBinary(string op)(S1ChordAngle b) const 271 if (op == "+") 272 in { 273 assert(!isSpecial()); 274 assert(!b.isSpecial()); 275 } do { 276 // Note that this method is much more efficient than converting the chord 277 // angles to S1Angles and adding those. It requires only one square root 278 // plus a few additions and multiplications. 279 280 // Optimization for the common case where "b" is an error tolerance 281 // parameter that happens to be set to zero. 282 double a2 = length2(); 283 double b2 = b.length2(); 284 if (b2 == 0) { 285 return S1ChordAngle(this); 286 } 287 288 // Clamp the angle sum to at most 180 degrees. 289 if (a2 + b2 >= MAX_LENGTH2) { 290 return S1ChordAngle.straight(); 291 } 292 293 // Let "a" and "b" be the (non-squared) chord lengths, and let c = a+b. 294 // Let A, B, and C be the corresponding half-angles (a = 2*sin(A), etc). 295 // Then the formula below can be derived from c = 2 * sin(A+B) and the 296 // relationships sin(A+B) = sin(A)*cos(B) + sin(B)*cos(A) 297 // cos(X) = sqrt(1 - sin^2(X)) . 298 double x = a2 * (1 - 0.25 * b2); // is_valid() => non-negative 299 double y = b2 * (1 - 0.25 * a2); // is_valid() => non-negative 300 return S1ChordAngle(algorithm.min(MAX_LENGTH2, x + y + 2 * math.sqrt(x * y))); 301 } 302 303 S1ChordAngle opBinary(string op)(S1ChordAngle b) const 304 if (op == "-") 305 in { 306 assert(!isSpecial()); 307 assert(!b.isSpecial()); 308 } do { 309 // See comments in opBinary!"+"(). 310 double a2 = length2(), b2 = b.length2(); 311 if (b2 == 0) { 312 return S1ChordAngle(this); 313 } 314 if (a2 <= b2) { 315 return S1ChordAngle.zero(); 316 } 317 double x = a2 * (1 - 0.25 * b2); 318 double y = b2 * (1 - 0.25 * a2); 319 return S1ChordAngle(algorithm.max(0.0, x + y - 2 * math.sqrt(x * y))); 320 } 321 322 S1ChordAngle opOpAssign(string op)(S1ChordAngle a) 323 if (op == "+" || op == "-") { 324 return this = mixin("this " ~ op ~ "a"); 325 } 326 327 /// Trigonmetric functions. It is more accurate and efficient to call these 328 /// rather than first converting to an S1Angle. 329 double sin() const { 330 return math.sqrt(sin2()); 331 } 332 333 double cos() const 334 in { 335 assert(!isSpecial()); 336 } do { 337 // cos(2*A) = cos^2(A) - sin^2(A) = 1 - 2*sin^2(A) 338 return 1 - 0.5 * length2(); 339 } 340 341 double tan() const { 342 return sin() / cos(); 343 } 344 345 /// Returns sin(a)**2, but computed more efficiently. 346 double sin2() const 347 in { 348 assert(!isSpecial()); 349 } do { 350 // Let "a" be the (non-squared) chord length, and let A be the corresponding 351 // half-angle (a = 2*sin(A)). The formula below can be derived from: 352 // sin(2*A) = 2 * sin(A) * cos(A) 353 // cos^2(A) = 1 - sin^2(A) 354 // This is much faster than converting to an angle and computing its sine. 355 return length2() * (1 - 0.25 * length2()); 356 } 357 358 359 /// The squared length of the chord. (Most clients will not need this.) 360 @property 361 double length2() const { 362 return _length2; 363 } 364 365 /** 366 * Returns the smallest representable S1ChordAngle larger than this object. 367 * This can be used to convert a "<" comparison to a "<=" comparison. For 368 * example: 369 * 370 * S2ClosestEdgeQuery query(...); 371 * S1ChordAngle limit = ...; 372 * if (query.IsDistanceLess(target, limit.Successor())) { 373 * // Distance to "target" is less than or equal to "limit". 374 * } 375 * 376 * Note the following special cases: 377 * Negative().Successor() == Zero() 378 * Straight().Successor() == Infinity() 379 * Infinity().Successor() == Infinity() 380 */ 381 S1ChordAngle successor() const { 382 if (_length2 >= MAX_LENGTH2) { 383 return infinity(); 384 } 385 if (_length2 < 0.0) { 386 return zero(); 387 } 388 return S1ChordAngle(math.nextafter(_length2, 10.0)); 389 } 390 391 /** 392 * Like Successor(), but returns the largest representable S1ChordAngle less 393 * than this object. 394 * 395 * Note the following special cases: 396 * Infinity().Predecessor() == Straight() 397 * Zero().Predecessor() == Negative() 398 * Negative().Predecessor() == Negative() 399 */ 400 S1ChordAngle predecessor() const { 401 if (_length2 <= 0.0) { 402 return negative(); 403 } 404 if (_length2 > MAX_LENGTH2) { 405 return straight(); 406 } 407 return S1ChordAngle(math.nextafter(_length2, -10.0)); 408 } 409 410 411 /** 412 * Returns a new S1ChordAngle that has been adjusted by the given error 413 * bound (which can be positive or negative). "error" should be the value 414 * returned by one of the error bound methods below. For example: 415 * S1ChordAngle a(x, y); 416 * S1ChordAngle a1 = a.PlusError(a.GetS2PointConstructorMaxError()); 417 */ 418 S1ChordAngle plusError(double error) const { 419 // If angle is Negative() or Infinity(), don't change it. 420 // Otherwise clamp it to the valid range. 421 if (isSpecial()) { 422 return this; 423 } 424 return S1ChordAngle(algorithm.max(0.0, algorithm.min(MAX_LENGTH2, _length2 + error))); 425 } 426 427 /** 428 * Return the maximum error in length2() for the S1ChordAngle(x, y) 429 * constructor, assuming that "x" and "y" are normalized to within the 430 * bounds guaranteed by S2Point::Normalize(). (The error is defined with 431 * respect to the true distance after the points are projected to lie 432 * exactly on the sphere.) 433 */ 434 double getS2PointConstructorMaxError() const { 435 // There is a relative error of 2.5 * DBL_EPSILON when computing the squared 436 // distance, plus a relative error of 2 * DBL_EPSILON and an absolute error 437 // of (16 * DBL_EPSILON**2) because the lengths of the input points may 438 // differ from 1 by up to (2 * DBL_EPSILON) each. (This is the maximum 439 // length error in S2Point::Normalize.) 440 return 4.5 * double.epsilon * _length2 + 16 * double.epsilon * double.epsilon; 441 } 442 443 /// Return the maximum error in length2() for the S1Angle constructor. 444 double getS1AngleConstructorMaxError() const { 445 // Assuming that an accurate math library is being used, the sin() call and 446 // the multiply each have a relative error of 0.5 * DBL_EPSILON. 447 return double.epsilon * _length2; 448 } 449 450 /// Return true if the internal representation is valid. Negative() and 451 /// Infinity() are both considered valid. 452 bool isValid() const { 453 return (_length2 >= 0 && _length2 <= MAX_LENGTH2) || isSpecial(); 454 } 455 456 string toString() const { 457 return toS1Angle().toString(); 458 } 459 }