gistools/geometry/s1/
angle.rs

1use crate::{
2    geometry::{LonLat, S2Point},
3    space::EARTH_RADIUS,
4};
5use core::{
6    cmp::Ordering,
7    f64::consts::TAU,
8    ops::{Add, Deref, Div, Mul, Neg, Rem, RemAssign, Sub},
9};
10use libm::{fabs, fmod};
11use s2json::MValueCompatible;
12
13/// This class represents a one-dimensional angle (as opposed to a
14/// two-dimensional solid angle).  It has methods for converting angles to
15/// or from radians, degrees, and the E5/E6/E7 representations (i.e. degrees
16/// multiplied by 1e5/1e6/1e7 and rounded to the nearest integer).
17///
18/// The internal representation is a double-precision value in radians, so
19/// conversion to and from radians is exact.  Conversions between E5, E6, E7,
20/// and Degrees are not always exact; for example, Degrees(3.1) is different
21/// from E6(3100000) or E7(310000000).  However, the following properties are
22/// guaranteed for any integer "n", provided that "n" is in the input range of
23/// both functions:
24///
25/// $$     Degrees(n) == E6(1000000 * n) $$
26/// $$     Degrees(n) == E7(10000000 * n) $$
27/// $$          E6(n) == E7(10 * n) $$
28///
29/// The corresponding properties are *not* true for E5, so if you use E5 then
30/// don't test for exact equality when comparing to other formats such as
31/// Degrees or E7.
32///
33/// The following conversions between degrees and radians are exact:
34///
35/// $$         Degrees(180) == Radians(M_PI); $$
36/// $$      Degrees(45 * k) == Radians(k * M_PI / 4)  for k == 0..8 $$
37///
38/// These identities also hold when the arguments are scaled up or down by any
39/// power of 2.  Some similar identities are also true, for example,
40/// Degrees(60) == Radians(M_PI / 3), but be aware that this type of identity
41/// does not hold in general.  For example, Degrees(3) != Radians(M_PI / 60).
42///
43/// Similarly, the conversion to radians means that Angle::Degrees(x).degrees()
44/// does not always equal "x".  For example,
45///
46/// $$       S1Angle::Degrees(45 * k).degrees() == 45 * k      for k == 0..8 $$
47///   but
48/// $$       S1Angle::Degrees(60).degrees() != 60. $$
49///
50/// This means that when testing for equality, you should allow for numerical
51/// errors (EXPECT_DOUBLE_EQ) or convert to discrete E5/E6/E7 values first.
52///
53/// CAVEAT: All of the above properties depend on "double" being the usual
54/// 64-bit IEEE 754 type (which is true on almost all modern platforms).
55///
56/// This class is intended to be copied by value as desired.  It uses
57/// the default copy constructor and assignment operator.
58///
59/// ## Usage
60///
61/// Methods that are available:
62/// - [`S1Angle::new`]: Create an angle
63/// - [`S1Angle::infinity`]: Return an angle representing infinity
64/// - [`S1Angle::from_degrees`]: Convert an angle in degrees to an angle in radians
65/// - [`S1Angle::to_degrees`]: Convert an angle in radians to an angle in degrees
66/// - [`S1Angle::to_e5`]: Convert an angle in radians to an angle in degrees
67/// - [`S1Angle::to_e6`]: Convert an angle in radians to an angle in degrees
68/// - [`S1Angle::to_e7`]: Convert an angle in radians to an angle in degrees
69/// - [`S1Angle::from_s2points`]: Convert two S2Point points to an angle
70/// - [`S1Angle::from_lon_lat`]: Convert two S2LatLng points to an angle
71/// - [`S1Angle::to_meters`]: Convert an angle in radians to an angle in meters
72/// - [`S1Angle::from_meters`]: Convert an angle in meters to an angle in radians
73/// - [`S1Angle::to_km`]: Convert an angle in radians to an angle in kilometers
74/// - [`S1Angle::from_km`]: Convert an angle in kilometers to an angle in radians
75/// - [`S1Angle::e5`]: Build an angle in E5 format.
76/// - [`S1Angle::e6`]: Build an angle in E6 format.
77/// - [`S1Angle::e7`]: Build an angle in E7 format.
78/// - [`S1Angle::normalize`]: Normalize this angle to the range (-180, 180] degrees.
79/// - [`S1Angle::modulo`]: Returns the remainder when dividing by `modulus`
80#[derive(Copy, Clone, Default, Debug)]
81#[repr(C)]
82pub struct S1Angle {
83    /// Angle in radians
84    pub radians: f64,
85}
86impl S1Angle {
87    /// Creates an S1Angle from a value in radians.
88    pub fn new(radians: f64) -> Self {
89        Self { radians }
90    }
91
92    /// Creates an S1Angle with an infinite value.
93    pub fn infinity() -> Self {
94        Self { radians: f64::INFINITY }
95    }
96
97    /// Creates an S1Angle from a value in degrees, converting it to radians.
98    pub fn from_degrees(degrees: f64) -> Self {
99        Self { radians: degrees.to_radians() }
100    }
101
102    /// Returns the angle in degrees.
103    pub fn to_degrees(&self) -> f64 {
104        (**self).to_degrees()
105    }
106
107    /// build an angle in E5 format.
108    pub fn to_e5(e5_: f64) -> Self {
109        Self::from_degrees(e5_ * 1e-5)
110    }
111
112    /// build an angle in E6 format.
113    pub fn to_e6(e6_: f64) -> Self {
114        Self::from_degrees(e6_ * 1e-6)
115    }
116
117    /// build an angle in E7 format.
118    pub fn to_e7(e7_: f64) -> Self {
119        Self::from_degrees(e7_ * 1e-7)
120    }
121
122    /// Return the angle between two points, which is also equal to the distance
123    /// between these points on the unit sphere.  The points do not need to be
124    /// normalized.  This function has a maximum error of 3.25 * DBL_EPSILON (or
125    /// 2.5 * DBL_EPSILON for angles up to 1 radian). If either point is
126    /// zero-length (e.g. an uninitialized S2Point), or almost zero-length, the
127    /// resulting angle will be zero.
128    pub fn from_s2points(a: &S2Point, b: &S2Point) -> Self {
129        a.angle(b).into()
130    }
131
132    /// Like the constructor above, but return the angle (i.e., distance) between
133    /// two S2LatLng points.  This function has about 15 digits of accuracy for
134    /// small distances but only about 8 digits of accuracy as the distance
135    /// approaches 180 degrees (i.e., nearly-antipodal points).
136    pub fn from_lon_lat<M1: MValueCompatible, M2: MValueCompatible>(
137        a: &LonLat<M1>,
138        b: &LonLat<M2>,
139    ) -> Self {
140        a.get_distance(b).into()
141    }
142
143    /// Convert an angle in radians to an angle in meters
144    /// If no radius is specified, the Earth's radius is used.
145    pub fn to_meters(&self, radius: Option<f64>) -> f64 {
146        let radius = radius.unwrap_or(EARTH_RADIUS);
147        **self * radius
148    }
149
150    /// Convert an angle in meters to an angle in radians
151    /// If no radius is specified, the Earth's radius is used.
152    pub fn from_meters(angle: f64, radius: Option<f64>) -> Self {
153        let radius = radius.unwrap_or(EARTH_RADIUS);
154        (angle / radius).into()
155    }
156
157    /// Convert an angle in radians to an angle in kilometers
158    /// If no radius is specified, the Earth's radius is used.
159    pub fn to_km(&self, radius: Option<f64>) -> f64 {
160        let radius = radius.unwrap_or(EARTH_RADIUS);
161        **self * radius / 1_000.0
162    }
163
164    /// Convert an angle in kilometers to an angle in radians
165    /// If no radius is specified, the Earth's radius is used.
166    pub fn from_km(angle: f64, radius: Option<f64>) -> Self {
167        let radius = radius.unwrap_or(EARTH_RADIUS);
168        ((angle * 1_000.0) / radius).into()
169    }
170
171    // Note that the E5, E6, and E7 conversion involve two multiplications rather
172    // than one.  This is mainly for backwards compatibility (changing this would
173    // break many tests), but it does have the nice side effect that conversions
174    // between Degrees, E6, and E7 are exact when the arguments are integers.
175
176    /// Build an angle in E5 format.
177    pub fn e5(&self) -> f64 {
178        self.to_degrees() * 1e5
179    }
180
181    /// Build an angle in E6 format.
182    pub fn e6(&self) -> f64 {
183        self.to_degrees() * 1e6
184    }
185
186    /// Build an angle in E7 format.
187    pub fn e7(&self) -> f64 {
188        self.to_degrees() * 1e7
189    }
190
191    /// Normalize this angle to the range (-180, 180] degrees.
192    pub fn normalize(&self) -> Self {
193        *self % TAU
194    }
195
196    /// Returns the remainder when dividing by `modulus`
197    pub fn modulo(&self, modulus: f64) -> Self {
198        Self { radians: fmod(**self, fabs(modulus)) }
199    }
200}
201impl From<f64> for S1Angle {
202    fn from(radians: f64) -> S1Angle {
203        S1Angle { radians }
204    }
205}
206impl Deref for S1Angle {
207    type Target = f64;
208    fn deref(&self) -> &Self::Target {
209        &self.radians
210    }
211}
212
213impl Add for S1Angle {
214    type Output = Self;
215    fn add(self, rhs: Self) -> Self {
216        (self.radians + rhs.radians).into()
217    }
218}
219impl Add<f64> for S1Angle {
220    type Output = Self;
221    fn add(self, rhs: f64) -> Self {
222        (self.radians + rhs).into()
223    }
224}
225impl Sub for S1Angle {
226    type Output = Self;
227    fn sub(self, rhs: Self) -> Self {
228        (self.radians - rhs.radians).into()
229    }
230}
231impl Sub<f64> for S1Angle {
232    type Output = Self;
233    /// Subtracts a value from the radians of the chord angle.
234    fn sub(self, rhs: f64) -> Self {
235        (self.radians - rhs).into()
236    }
237}
238impl Mul for S1Angle {
239    type Output = Self;
240    fn mul(self, rhs: Self) -> Self {
241        (self.radians * rhs.radians).into()
242    }
243}
244impl Mul<f64> for S1Angle {
245    type Output = Self;
246    fn mul(self, rhs: f64) -> Self {
247        (self.radians * rhs).into()
248    }
249}
250impl Div for S1Angle {
251    type Output = Self;
252    fn div(self, rhs: Self) -> Self {
253        (self.radians / rhs.radians).into()
254    }
255}
256impl Div<f64> for S1Angle {
257    type Output = Self;
258    fn div(self, rhs: f64) -> Self {
259        (self.radians / rhs).into()
260    }
261}
262impl Neg for S1Angle {
263    type Output = Self;
264    fn neg(self) -> Self {
265        (-self.radians).into()
266    }
267}
268impl Rem<f64> for S1Angle {
269    type Output = Self;
270    fn rem(self, modulus: f64) -> Self::Output {
271        self.modulo(modulus)
272    }
273}
274impl RemAssign<f64> for S1Angle {
275    fn rem_assign(&mut self, modulus: f64) {
276        self.radians = fmod(self.radians, fabs(modulus));
277    }
278}
279
280impl PartialOrd<S1Angle> for S1Angle {
281    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
282        Some(self.cmp(other))
283    }
284}
285// Implement PartialOrd for comparison between S1Angle and f64
286impl PartialOrd<f64> for S1Angle {
287    fn partial_cmp(&self, other: &f64) -> Option<Ordering> {
288        self.radians.partial_cmp(other)
289    }
290}
291impl Ord for S1Angle {
292    fn cmp(&self, other: &Self) -> Ordering {
293        self.radians.partial_cmp(&other.radians).unwrap_or(Ordering::Equal)
294    }
295}
296impl PartialEq<S1Angle> for S1Angle {
297    fn eq(&self, other: &Self) -> bool {
298        self.radians == other.radians
299    }
300}
301impl PartialEq<f64> for S1Angle {
302    fn eq(&self, other: &f64) -> bool {
303        self.radians == *other
304    }
305}
306impl Eq for S1Angle {}