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 }