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 {}