geometry_strategy/geographic/
direct_thomas.rs1use geometry_cs::Spheroid;
7
8use super::direct::DirectResult;
9
10#[cfg(feature = "std")]
11use super::direct::normalize_longitude;
12#[cfg(feature = "std")]
13use super::spheroid_calc::SpheroidCalc;
14
15#[derive(Debug, Clone, Copy)]
22pub struct ThomasDirect {
23 pub spheroid: Spheroid,
25 pub second_order: bool,
27}
28
29impl ThomasDirect {
30 pub const WGS84: Self = Self {
32 spheroid: Spheroid::WGS84,
33 second_order: true,
34 };
35
36 #[cfg(feature = "std")]
42 #[inline]
43 #[must_use]
44 #[allow(
45 clippy::float_cmp,
46 clippy::if_not_else,
47 clippy::many_single_char_names,
48 clippy::similar_names,
49 clippy::too_many_lines,
50 reason = "names follow Thomas's equations and the cited Boost implementation"
51 )]
52 pub fn apply(&self, lon1: f64, lat1: f64, distance: f64, azimuth12: f64) -> DirectResult {
53 let calc = SpheroidCalc::from(self.spheroid);
54 let a = calc.a;
55 let f = calc.f;
56 let one_minus_f = 1.0 - f;
57 let pi = core::f64::consts::PI;
58 let pi_half = core::f64::consts::FRAC_PI_2;
59
60 let mut azi12_alt = azimuth12;
61 let mut lat1_alt = lat1;
62 let alter_result = if azimuth12 > pi_half {
63 azi12_alt = pi - azimuth12;
64 lat1_alt = -lat1;
65 true
66 } else if azimuth12 < -pi_half {
67 azi12_alt = -pi - azimuth12;
68 lat1_alt = -lat1;
69 true
70 } else {
71 false
72 };
73
74 let theta1 = if lat1_alt == pi_half || lat1_alt == -pi_half {
75 lat1_alt
76 } else {
77 (one_minus_f * lat1_alt.tan()).atan()
78 };
79 let sin_theta1 = theta1.sin();
80 let cos_theta1 = theta1.cos();
81 let sin_a12 = azi12_alt.sin();
82 let cos_a12 = azi12_alt.cos();
83 let m = cos_theta1 * sin_a12;
84 let theta0 = m.acos();
85 let sin_theta0 = theta0.sin();
86 let n = cos_theta1 * cos_a12;
87 let c1 = f * m;
88 let c2 = f * (1.0 - m * m) / 4.0;
89 let (d_coefficient, p) = if self.second_order {
90 let d = (1.0 - c2) * (1.0 - c2 - c1 * m);
91 (d, c2 * (1.0 + c1 * m / 2.0) / d)
92 } else {
93 let d = 1.0 - 2.0 * c2 - c1 * m;
94 (d, c2 / d)
95 };
96
97 let cos_sigma1 = if sin_theta0 == 0.0 {
98 1.0
99 } else {
100 (sin_theta1 / sin_theta0).clamp(-1.0, 1.0)
101 };
102 let sigma1 = cos_sigma1.acos();
103 let d = distance / (a * d_coefficient);
104 let u = 2.0 * (sigma1 - d);
105 let cos_d = d.cos();
106 let sin_d = d.sin();
107 let cos_u = u.cos();
108 let sin_u = u.sin();
109 let w = 1.0 - 2.0 * p * cos_u;
110 let v = cos_u * cos_d - sin_u * sin_d;
111 let y = 2.0 * p * v * w * sin_d;
112 let mut d_sigma = d - y;
113 if self.second_order {
114 let x = c2 * c2 * sin_d * cos_d * (2.0 * v * v - 1.0);
115 d_sigma += x;
116 }
117 let sin_d_sigma = d_sigma.sin();
118 let cos_d_sigma = d_sigma.cos();
119
120 let mut reverse_azimuth = m.atan2(n * cos_d_sigma - sin_theta1 * sin_d_sigma);
121 if alter_result {
122 reverse_azimuth = if reverse_azimuth == 0.0 {
123 if azimuth12 >= 0.0 { pi } else { -pi }
124 } else if reverse_azimuth > 0.0 {
125 pi - reverse_azimuth
126 } else {
127 -pi - reverse_azimuth
128 };
129 }
130
131 let s_sigma = 2.0 * sigma1 - d_sigma;
132 let mut h = c1 * d_sigma;
133 if self.second_order {
134 h = h * (1.0 - c2) - c1 * c2 * sin_d_sigma * s_sigma.cos();
135 }
136 let d_eta = (sin_d_sigma * sin_a12)
137 .atan2(cos_theta1 * cos_d_sigma - sin_theta1 * sin_d_sigma * cos_a12);
138 let d_lambda = d_eta - h;
139 let mut lat2 = if m != 0.0 {
140 let sin_a21 = reverse_azimuth.sin();
141 let tan_theta2 = (sin_theta1 * cos_d_sigma + n * sin_d_sigma) * sin_a21 / m;
142 (tan_theta2 / one_minus_f).atan()
143 } else {
144 let sigma2 = s_sigma - sigma1;
145 let tan_theta2 = sigma2.cos() / sigma2.sin().abs();
146 (tan_theta2 / one_minus_f).atan()
147 };
148 if alter_result {
149 lat2 = -lat2;
150 }
151
152 DirectResult::solved(
153 lon1,
154 lat1,
155 azimuth12,
156 self.spheroid,
157 normalize_longitude(lon1 + d_lambda),
158 lat2,
159 reverse_azimuth,
160 )
161 }
162}
163
164impl Default for ThomasDirect {
165 #[inline]
166 fn default() -> Self {
167 Self::WGS84
168 }
169}