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