mappers/projections/
modified_azimuthal_equidistant.rs1use float_cmp::approx_eq;
32
33use crate::{
34 Projection, ProjectionError,
35 ellipsoids::Ellipsoid,
36 errors::{ensure_finite, ensure_within_range, unpack_required_parameter},
37};
38
39#[cfg(feature = "tracing")]
40use tracing::instrument;
41
42#[derive(Copy, Clone, PartialEq, PartialOrd, Debug)]
44pub struct ModifiedAzimuthalEquidistant {
45 lon_0: f64,
46 lat_0: f64,
47 n_1: f64,
48 g: f64,
49 ellps: Ellipsoid,
50}
51
52impl ModifiedAzimuthalEquidistant {
53 #[must_use]
58 pub fn builder() -> ModifiedAzimuthalEquidistantBuilder {
59 ModifiedAzimuthalEquidistantBuilder::default()
60 }
61}
62
63#[derive(Copy, Clone, PartialEq, PartialOrd, Debug)]
67pub struct ModifiedAzimuthalEquidistantBuilder {
68 ref_lon: Option<f64>,
69 ref_lat: Option<f64>,
70 ellipsoid: Ellipsoid,
71}
72
73impl Default for ModifiedAzimuthalEquidistantBuilder {
74 fn default() -> Self {
75 Self {
76 ref_lon: None,
77 ref_lat: None,
78 ellipsoid: Ellipsoid::WGS84,
79 }
80 }
81}
82
83impl ModifiedAzimuthalEquidistantBuilder {
84 pub const fn ref_lonlat(&mut self, lon: f64, lat: f64) -> &mut Self {
86 self.ref_lon = Some(lon);
87 self.ref_lat = Some(lat);
88 self
89 }
90
91 pub const fn ellipsoid(&mut self, ellps: Ellipsoid) -> &mut Self {
93 self.ellipsoid = ellps;
94 self
95 }
96
97 pub fn initialize_projection(&self) -> Result<ModifiedAzimuthalEquidistant, ProjectionError> {
113 let ref_lon = unpack_required_parameter!(self, ref_lon);
114 let ref_lat = unpack_required_parameter!(self, ref_lat);
115 let ellps = self.ellipsoid;
116 ensure_finite!(ref_lon, ref_lat);
117
118 ensure_within_range!(ref_lon, -180.0..180.0);
119 ensure_within_range!(ref_lat, -90.0..90.0);
120
121 let lon_0 = ref_lon.to_radians();
122 let lat_0 = ref_lat.to_radians();
123
124 let n_1 = ellps.A / ellps.E.powi(2).mul_add(-(lat_0.sin()).powi(2), 1.0).sqrt();
125 let g = ellps.E * lat_0.sin() / ellps.E.mul_add(-ellps.E, 1.0).sqrt();
126
127 Ok(ModifiedAzimuthalEquidistant {
128 lon_0,
129 lat_0,
130 n_1,
131 g,
132 ellps,
133 })
134 }
135}
136
137impl Projection for ModifiedAzimuthalEquidistant {
138 #[inline]
139 #[allow(clippy::many_single_char_names)]
140 #[cfg_attr(feature = "tracing", instrument(level = "trace"))]
141 fn project_unchecked(&self, lon: f64, lat: f64) -> (f64, f64) {
142 let lon = lon.to_radians();
143 let lat = lat.to_radians();
144
145 let n = self.ellps.A / self.ellps.E.powi(2).mul_add(-(lat.sin()).powi(2), 1.0).sqrt();
146
147 let psi = self.ellps.E.mul_add(-self.ellps.E, 1.0).mul_add(lat.tan(), (self.ellps.E.powi(2) * self.n_1 * self.lat_0.sin()) / (n * lat.cos()))
148 .atan();
149
150 let az = ((lon - self.lon_0).sin())
151 .atan2(self.lat_0.cos().mul_add(psi.tan(), -(self.lat_0.sin() * (lon - self.lon_0).cos())));
152
153 let s = if approx_eq!(f64, az.sin(), 0.0) {
154 self.lat_0.cos().mul_add(psi.sin(), -(self.lat_0.sin() * psi.cos()))
155 .asin()
156 .abs()
157 * az.cos().signum()
158 } else {
159 (((lon - self.lon_0).sin() * psi.cos()) / (az.sin())).asin()
160 };
161
162 let h = self.ellps.E * self.lat_0.cos() * az.cos() / self.ellps.E.mul_add(-self.ellps.E, 1.0).sqrt();
163
164 let c = (self.n_1 * s)
165 * ((s.powi(5) / 48.0) * self.g).mul_add(-h, (s.powi(4) / 120.0).mul_add(h.powi(2).mul_add(7.0f64.mul_add(-h.powi(2), 4.0), -(3.0 * self.g.powi(2) * 7.0f64.mul_add(-h.powi(2), 1.0))), ((s.powi(3) / 8.0) * self.g * h).mul_add(2.0f64.mul_add(-h.powi(2), 1.0), 1.0 - (s.powi(2) * h.powi(2) * h.mul_add(-h, 1.0) / 6.0))));
166
167 let x = c * az.sin();
168 let y = c * az.cos();
169
170 (x, y)
171 }
172
173 #[inline]
174 #[cfg_attr(feature = "tracing", instrument(level = "trace"))]
175 fn inverse_project_unchecked(&self, x: f64, y: f64) -> (f64, f64) {
176 let c = x.hypot(y);
177 let az = x.atan2(y);
178
179 let big_a =
180 -self.ellps.E * self.ellps.E * ((self.lat_0.cos()).powi(2)) * ((az.cos()).powi(2))
181 / self.ellps.E.mul_add(-self.ellps.E, 1.0);
182 let big_b = 3.0
183 * self.ellps.E
184 * self.ellps.E
185 * (1.0 - big_a)
186 * self.lat_0.sin()
187 * self.lat_0.cos()
188 * az.cos()
189 / self.ellps.E.mul_add(-self.ellps.E, 1.0);
190 let big_d = c / self.n_1;
191 let big_e = big_d
192 - (big_a * (1.0 + big_a) * big_d.powi(3) / 6.0)
193 - (big_b * 3.0f64.mul_add(big_a, 1.0) * big_d.powi(4) / 24.0);
194 let big_f = 1.0 - (big_a * big_e * big_e / 2.0) - (big_b * big_e.powi(3) / 6.0);
195
196 let psi =
197 self.lat_0.sin().mul_add(big_e.cos(), self.lat_0.cos() * big_e.sin() * az.cos()).asin();
198
199 let lon = self.lon_0 + (az.sin() * big_e.sin() / psi.cos()).asin();
200 let lat = ((1.0 - (self.ellps.E * self.ellps.E * big_f * self.lat_0.sin() / psi.sin()))
201 * psi.tan()
202 / self.ellps.E.mul_add(-self.ellps.E, 1.0))
203 .atan();
204
205 let lon = lon.to_degrees();
206 let lat = lat.to_degrees();
207
208 (lon, lat)
209 }
210}