gistools/geometry/s1/
chord_angle.rs

1use crate::geometry::{S1Angle, S2Point};
2use core::{
3    cmp::Ordering,
4    f64::consts::PI,
5    ops::{Add, Deref, Div, Mul, Neg, Rem, RemAssign, Sub},
6};
7use libm::{asin, fabs, fmin, fmod, sin, sqrt};
8
9/// The Maximum allowed squared chord length.
10pub const K_MAX_LENGTH_2: f64 = 4.0;
11
12/// S1ChordAngle represents the angle subtended by a chord (i.e., the straight
13/// line segment connecting two points on the sphere).  Its representation
14/// makes it very efficient for computing and comparing distances, but unlike
15/// S1Angle it is only capable of representing angles between 0 and Pi radians.
16/// S1ChordAngle is intended for applications where many angles need to be
17/// computed and compared, otherwise it is simpler to use S1Angle.
18///
19/// S1ChordAngle also loses some accuracy as the angle approaches Pi radians.
20/// There are several different ways to measure this error, including the
21/// representational error (i.e., how accurately S1ChordAngle can represent
22/// angles near Pi radians), the conversion error (i.e., how much precision is
23/// lost when an S1Angle is converted to an S1ChordAngle), and the measurement
24/// error (i.e., how accurate the S1ChordAngle(a, b) constructor is when the
25/// points A and B are separated by angles close to Pi radians).  All of these
26/// errors differ by a small constant factor.
27///
28/// For the measurement error (which is the largest of these errors and also
29/// the most important in practice), let the angle between A and B be (Pi - x)
30/// radians, i.e. A and B are within "x" radians of being antipodal.  The
31/// corresponding chord length is
32///
33/// $$    r = 2 * sin((Pi - x) / 2) = 2 * cos(x / 2) . $$
34///
35/// For values of x not close to Pi the relative error in the squared chord
36/// length is at most 4.5 * DBL_EPSILON (see GetS2PointConstructorMaxError).
37/// The relative error in "r" is thus at most 2.25 * DBL_EPSILON ~= 5e-16.  To
38/// convert this error into an equivalent angle, we have
39///
40/// $$   |dr / dx| = sin(x / 2) $$
41///
42/// and therefore
43///
44/// $$   |dx| = dr / sin(x / 2) $$
45/// $$        = 5e-16 * (2 * cos(x / 2)) / sin(x / 2) $$
46/// $$        = 1e-15 / tan(x / 2) $$
47///
48/// The maximum error is attained when
49///
50/// $$   x  = |dx| $$
51/// $$      = 1e-15 / tan(x / 2) $$
52/// $$     ~= 1e-15 / (x / 2) $$
53/// $$     ~= sqrt(2e-15) $$
54///
55/// In summary, the measurement error for an angle (Pi - x) is at most
56///
57/// $$   dx  = min(1e-15 / tan(x / 2), sqrt(2e-15)) $$
58/// $$     (~= min(2e-15 / x, sqrt(2e-15)) when x is small). $$
59///
60/// On the Earth's surface (assuming a radius of 6371km), this corresponds to
61/// the following worst-case measurement errors:
62/// ```ini
63///     Accuracy:             Unless antipodal to within:
64///     ---------             ---------------------------
65///     6.4 nanometers        10,000 km (90 degrees)
66///     1 micrometer          81.2 kilometers
67///     1 millimeter          81.2 meters
68///     1 centimeter          8.12 meters
69///     28.5 centimeters      28.5 centimeters
70/// ```
71/// The representational and conversion errors referred to earlier are somewhat
72/// smaller than this.  For example, maximum distance between adjacent
73/// representable S1ChordAngle values is only 13.5 cm rather than 28.5 cm.  To
74/// see this, observe that the closest representable value to r^2 = 4 is
75/// r^2 =  4 * (1 - DBL_EPSILON / 2).  Thus r = 2 * (1 - DBL_EPSILON / 4) and
76/// the angle between these two representable values is
77///
78/// $$   x  = 2 * acos(r / 2) $$
79/// $$      = 2 * acos(1 - DBL_EPSILON / 4) $$
80/// $$     ~= 2 * asin(sqrt(DBL_EPSILON / 2) $$
81/// $$     ~= sqrt(2 * DBL_EPSILON) $$
82/// $$     ~= 2.1e-8 $$
83///
84/// which is 13.5 cm on the Earth's surface.
85///
86/// The worst case rounding error occurs when the value halfway between these
87/// two representable values is rounded up to 4.  This halfway value is
88/// r^2 = (4 * (1 - DBL_EPSILON / 4)), thus r = 2 * (1 - DBL_EPSILON / 8) and
89/// the worst case rounding error is
90///
91/// $$   x  = 2 * acos(r / 2) $$
92/// $$      = 2 * acos(1 - DBL_EPSILON / 8) $$
93/// $$     ~= 2 * asin(sqrt(DBL_EPSILON / 4) $$
94/// $$     ~= sqrt(DBL_EPSILON) $$
95/// $$     ~= 1.5e-8 $$
96///
97/// which is 9.5 cm on the Earth's surface.
98///
99/// This class is intended to be copied by value as desired.  It uses
100/// the default copy constructor and assignment operator.
101///
102/// ## Usage
103///
104/// Methods that are available:
105/// - [`S1ChordAngle::new`]: Create a new S1ChordAngle
106/// - [`S1ChordAngle::zero`]: Returns the zero S1ChordAngle
107/// - [`S1ChordAngle::infinity`]: Returns the infinite S1ChordAngle
108/// - [`S1ChordAngle::from_angle`]: Conversion from an S1Angle
109/// - [`S1ChordAngle::from_degrees`]: Construct an S1ChordAngle from an angle in degrees
110/// - [`S1ChordAngle::from_length2`]: Construct the S1ChordAngle corresponding to the given length
111/// - [`S1ChordAngle::from_s2_points`]: Construct the S1ChordAngle corresponding to the distance between the two given points
112/// - [`S1ChordAngle::right_angle`]: Return a right angle
113/// - [`S1ChordAngle::straight_angle`]: Return a chord angle of 180 degrees (a "straight angle")
114/// - [`S1ChordAngle::negative_angle`]: Construct an S1ChordAngle that is a lower bound on the given S1Angle
115/// - [`S1ChordAngle::fast_upper_bound_from`]: Construct an S1ChordAngle that is an upper bound on the given S1Angle
116/// - [`S1ChordAngle::is_special`]: Returns true if the angle is special
117/// - [`S1ChordAngle::to_angle`]: Convert to an S1Angle
118/// - [`S1ChordAngle::to_meters`]: Convert to meters. If no radius is specified, the Earth's radius is used.
119/// - [`S1ChordAngle::from_meters`]: Convert from meters. If no radius is specified, the Earth's radius is used.
120/// - [`S1ChordAngle::to_km`]: Convert to kilometers. If no radius is specified, the Earth's radius is used.
121/// - [`S1ChordAngle::from_km`]: Convert from kilometers. If no radius is specified, the Earth's radius is used.
122/// - [`S1ChordAngle::chord_angle_sin`]: apply a sine function on a ChordAngle
123/// - [`S1ChordAngle::chord_angle_cos`]: apply a cosine function on a ChordAngle
124/// - [`S1ChordAngle::chord_angle_tan`]: apply a tangent function on a ChordAngle
125/// - [`S1ChordAngle::chord_angle_sin2`]: Returns sin(a)^2
126/// - [`S1ChordAngle::modulo`]: Returns the remainder when dividing by modulus
127#[derive(Copy, Clone, Default, Debug)]
128#[repr(C)]
129pub struct S1ChordAngle {
130    /// The squared length of the corresponding S1Chord.
131    pub length2: f64,
132}
133impl S1ChordAngle {
134    /// Creates a new S1ChordAngle.
135    pub fn new(length2: f64) -> Self {
136        S1ChordAngle { length2 }
137    }
138
139    /// Returns the zero S1ChordAngle.
140    pub fn zero() -> Self {
141        S1ChordAngle { length2: 0.0 }
142    }
143
144    /// Returns the infinite S1ChordAngle.
145    pub fn infinity() -> Self {
146        S1ChordAngle { length2: f64::INFINITY }
147    }
148
149    ///  Conversion from an S1Angle.  Angles outside the range [0, Pi] are handled
150    ///  as follows: Infinity() is mapped to Infinity(), negative angles are
151    ///  mapped to Negative(), and finite angles larger than Pi are mapped to
152    ///  Straight().
153    ///
154    ///  Note that this operation is relatively expensive and should be avoided.
155    ///  To use S1ChordAngle effectively, you should structure your code so that
156    ///  input arguments are converted to S1ChordAngles at the beginning of your
157    ///  algorithm, and results are converted back to S1Angles only at the end.
158    ///
159    ///  S1ChordAngles are represented by the squared chord length, which can
160    ///  range from 0 to 4.  Infinity() uses an infinite squared length.
161    ///
162    /// ## Parameters
163    /// - `angle` An angle in radians.
164    ///
165    /// ## Returns
166    /// The corresponding ChordAngle.
167    pub fn from_angle(angle: S1Angle) -> Self {
168        let radians: f64 = angle.radians;
169        if radians < 0. {
170            (-1.).into()
171        } else if radians == f64::INFINITY {
172            f64::INFINITY.into()
173        } else {
174            // The chord length is 2 * sin(angle / 2).
175            let length = 2.0 * sin(0.5 * fmin(PI, radians));
176            (length * length).into()
177        }
178    }
179
180    /// Construct an S1ChordAngle from an angle in degrees.
181    pub fn from_degrees(degrees: f64) -> Self {
182        S1Angle::from_degrees(degrees).into()
183    }
184
185    /// Construct an S1ChordAngle from the squared chord length.  Note that the
186    /// argument is automatically clamped to a maximum of 4.0 to handle possible
187    /// roundoff errors. The argument must be non-negative.
188    pub fn from_length2(length2_: f64) -> Self {
189        fmin(K_MAX_LENGTH_2, length2_).into()
190    }
191
192    /// Construct the S1ChordAngle corresponding to the distance between the two
193    /// given points. The points must be unit length.
194    pub fn from_s2_points(a: &S2Point, b: &S2Point) -> Self {
195        // The squared distance may slightly exceed 4.0 due to roundoff errors.
196        // The maximum error in the result is 2 * DBL_EPSILON * length2_.
197        fmin(K_MAX_LENGTH_2, (*a - *b).norm2()).into()
198    }
199
200    /// Return a chord angle of 90 degrees (a "right angle").
201    pub fn right_angle() -> Self {
202        2.0.into()
203    }
204
205    /// Return a chord angle of 180 degrees (a "straight angle").  This is the
206    /// maximum finite chord angle.
207    pub fn straight_angle() -> Self {
208        K_MAX_LENGTH_2.into()
209    }
210
211    // /**
212    /// Return a chord angle smaller than Zero().  The only valid operations on
213    /// Negative() are comparisons, S1Angle conversions, and successor() / predecessor().
214    pub fn negative_angle() -> Self {
215        (-1.).into()
216    }
217
218    /// Construct an S1ChordAngle that is an upper bound on the given S1Angle.
219    /// i.i. such that FastUpperBoundFrom(x).toAngle() >= x. Unlike the S1Angle
220    /// constructor above, this method is very fast, and the bound is accurate to
221    /// within 1% for distances up to about 3100km on the Earth's surface.
222    pub fn fast_upper_bound_from(angle: S1Angle) -> Self {
223        // This method uses the distance along the surface of the sphere as an upper
224        // bound on the distance through the sphere's interior.
225        Self::from_length2((*angle) * (*angle))
226    }
227
228    /// Convenience function to test if a ChordAngle is special.
229    pub fn is_special(&self) -> bool {
230        *self < 0. || *self == f64::INFINITY
231    }
232
233    /// Convert to an S1Angle.
234    /// Infinity() is converted to S1Angle.Infinity(), and Negative() is
235    /// converted to an unspecified negative S1Angle.
236    ///
237    /// Note that the conversion uses trigonometric functions and therefore
238    /// should be avoided in inner loops.
239    pub fn to_angle(&self) -> S1Angle {
240        let length2 = self.length2;
241        if length2 < 0. {
242            (-1.0).into()
243        } else if length2 == f64::INFINITY {
244            f64::INFINITY.into()
245        } else {
246            (2. * asin(0.5 * sqrt(length2))).into()
247        }
248    }
249
250    /// Convert to meters.
251    /// If no radius is specified, the Earth's radius is used.
252    pub fn to_meters(&self, radius: Option<f64>) -> f64 {
253        self.to_angle().to_meters(radius)
254    }
255
256    /// Convert from meters.
257    /// If no radius is specified, the Earth's radius is used.
258    pub fn from_meters(meters: f64, radius: Option<f64>) -> Self {
259        S1Angle::from_meters(meters, radius).into()
260    }
261
262    /// Convert to kilometers.
263    /// If no radius is specified, the Earth's radius is used.
264    pub fn to_km(&self, radius: Option<f64>) -> f64 {
265        self.to_angle().to_km(radius)
266    }
267
268    /// Convert from kilometers.
269    /// If no radius is specified, the Earth's radius is used.
270    pub fn from_km(km: f64, radius: Option<f64>) -> Self {
271        S1Angle::from_km(km, radius).into()
272    }
273
274    // Trigonmetric functions.  It is more accurate and efficient to call these
275    // rather than first converting to an S1Angle.
276
277    /// apply a sine function on a ChordAngle
278    pub fn chord_angle_sin(&self) -> f64 {
279        sqrt(self.chord_angle_sin2())
280    }
281
282    /// apply a cosine function on a ChordAngle
283    pub fn chord_angle_cos(&self) -> f64 {
284        // cos(2*A) = cos^2(A) - sin^2(A) = 1 - 2*sin^2(A)
285        1.0 - 0.5 * (**self)
286    }
287
288    /// apply a tangent function on a ChordAngle
289    pub fn chord_angle_tan(&self) -> f64 {
290        self.chord_angle_sin() / self.chord_angle_cos()
291    }
292
293    /// Returns sin(a)^2, but computed more efficiently.
294    pub fn chord_angle_sin2(&self) -> f64 {
295        // Let "a" be the (non-squared) chord length, and let A be the corresponding
296        // half-angle (a = 2*sin(A)).  The formula below can be derived from:
297        //   sin(2*A) = 2 * sin(A) * cos(A)
298        //   cos^2(A) = 1 - sin^2(A)
299        // This is much faster than converting to an angle and computing its sine.
300        (**self) * (1.0 - 0.25 * (**self))
301    }
302
303    /// Returns the remainder when dividing by `modulus`
304    pub fn modulo(&self, modulus: f64) -> Self {
305        Self { length2: fmod(**self, fabs(modulus)) }
306    }
307}
308impl From<f64> for S1ChordAngle {
309    fn from(length2: f64) -> S1ChordAngle {
310        S1ChordAngle { length2 }
311    }
312}
313impl From<S1Angle> for S1ChordAngle {
314    fn from(angle: S1Angle) -> S1ChordAngle {
315        S1ChordAngle::from_angle(angle)
316    }
317}
318impl From<S1ChordAngle> for S1Angle {
319    fn from(c_angle: S1ChordAngle) -> S1Angle {
320        c_angle.to_angle()
321    }
322}
323impl Deref for S1ChordAngle {
324    type Target = f64;
325    fn deref(&self) -> &Self::Target {
326        &self.length2
327    }
328}
329
330impl Add for S1ChordAngle {
331    type Output = Self;
332    fn add(self, rhs: Self) -> Self {
333        (self.length2 + rhs.length2).into()
334    }
335}
336impl Add<f64> for S1ChordAngle {
337    type Output = Self;
338    fn add(self, rhs: f64) -> Self {
339        (self.length2 + rhs).into()
340    }
341}
342impl Sub for S1ChordAngle {
343    type Output = Self;
344    fn sub(self, rhs: Self) -> Self {
345        (self.length2 - rhs.length2).into()
346    }
347}
348impl Sub<f64> for S1ChordAngle {
349    type Output = Self;
350    /// Subtracts a value from the length2 of the chord angle.
351    fn sub(self, rhs: f64) -> Self {
352        (self.length2 - rhs).into()
353    }
354}
355impl Mul for S1ChordAngle {
356    type Output = Self;
357    fn mul(self, rhs: Self) -> Self {
358        (self.length2 * rhs.length2).into()
359    }
360}
361impl Mul<f64> for S1ChordAngle {
362    type Output = Self;
363    fn mul(self, rhs: f64) -> Self {
364        (self.length2 * rhs).into()
365    }
366}
367impl Div for S1ChordAngle {
368    type Output = Self;
369    fn div(self, rhs: Self) -> Self {
370        (self.length2 / rhs.length2).into()
371    }
372}
373impl Div<f64> for S1ChordAngle {
374    type Output = Self;
375    fn div(self, rhs: f64) -> Self {
376        (self.length2 / rhs).into()
377    }
378}
379impl Neg for S1ChordAngle {
380    type Output = Self;
381    fn neg(self) -> Self {
382        (-self.length2).into()
383    }
384}
385impl Rem<f64> for S1ChordAngle {
386    type Output = Self;
387    fn rem(self, modulus: f64) -> Self::Output {
388        self.modulo(modulus)
389    }
390}
391impl RemAssign<f64> for S1ChordAngle {
392    fn rem_assign(&mut self, modulus: f64) {
393        self.length2 = fmod(self.length2, fabs(modulus));
394    }
395}
396
397// equalities
398impl PartialOrd<S1ChordAngle> for S1ChordAngle {
399    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
400        Some(self.cmp(other))
401    }
402}
403impl PartialOrd<f64> for S1ChordAngle {
404    fn partial_cmp(&self, other: &f64) -> Option<Ordering> {
405        self.length2.partial_cmp(other)
406    }
407}
408impl Ord for S1ChordAngle {
409    fn cmp(&self, other: &Self) -> Ordering {
410        self.length2.partial_cmp(&other.length2).unwrap_or(Ordering::Equal)
411    }
412}
413impl PartialEq<S1ChordAngle> for S1ChordAngle {
414    fn eq(&self, other: &Self) -> bool {
415        self.length2 == other.length2
416    }
417}
418impl PartialEq<f64> for S1ChordAngle {
419    fn eq(&self, other: &f64) -> bool {
420        self.length2 == *other
421    }
422}
423impl Eq for S1ChordAngle {}