1use crate::proj::{
2 AZIMUTHAL_EQUIDISTANT, CoordinateStep, EPS10, GUAM, GeodGeodesic, Proj, ProjMethod, ProjMode,
3 ProjectCoordinates, TransformCoordinates, aasin, enfn, geod_direct, geod_init, geod_inverse,
4 inv_mlfn, mlfn,
5};
6use alloc::{rc::Rc, vec::Vec};
7use core::{
8 cell::RefCell,
9 f64::consts::{FRAC_PI_2, PI},
10};
11use libm::{acos, atan2, cos, fabs, hypot, sin, sqrt, tan};
12
13#[derive(Debug, Default, Clone, PartialEq)]
41pub struct AeqdData {
42 sinph0: f64,
43 cosph0: f64,
44 en: Vec<f64>,
45 m1: f64,
46 n1: f64,
47 mp: f64,
48 he: f64,
49 _g: f64,
50 mode: ProjMode,
51 g: GeodGeodesic,
52}
53
54const TOL: f64 = 1e-14;
55
56#[derive(Debug, Clone, PartialEq)]
94pub struct AzimuthalEquidistantProjection {
95 proj: Rc<RefCell<Proj>>,
96 store: RefCell<AeqdData>,
97 method: ProjMethod,
98 guam: bool,
99}
100impl ProjectCoordinates for AzimuthalEquidistantProjection {
101 fn code(&self) -> i64 {
102 AZIMUTHAL_EQUIDISTANT
103 }
104 fn name(&self) -> &'static str {
105 "Azimuthal Equidistant"
106 }
107 fn names() -> &'static [&'static str] {
108 &["Azimuthal Equidistant", "Azimuthal_Equidistant", "aeqd", "guam"]
109 }
110}
111impl CoordinateStep for AzimuthalEquidistantProjection {
112 fn new(proj: Rc<RefCell<Proj>>) -> Self {
113 let mut store = AeqdData::default();
114 let mut method = ProjMethod::Spheroidal;
115 let mut guam = false;
116 {
117 let proj = &mut proj.borrow_mut();
118
119 geod_init(&mut store.g, 1., proj.f);
120
121 if fabs(fabs(proj.phi0) - FRAC_PI_2) < EPS10 {
122 store.mode = if proj.phi0 < 0. { ProjMode::SPole } else { ProjMode::NPole };
123 store.sinph0 = if proj.phi0 < 0. { -1. } else { 1. };
124 store.cosph0 = 0.;
125 } else if fabs(proj.phi0) < EPS10 {
126 store.mode = ProjMode::Equit;
127 store.sinph0 = 0.;
128 store.cosph0 = 1.;
129 } else {
130 store.mode = ProjMode::Obliq;
131 store.sinph0 = sin(proj.phi0);
132 store.cosph0 = cos(proj.phi0);
133 }
134 if proj.es == 0.0 {
135 method = ProjMethod::Spheroidal;
136 } else {
137 store.en = enfn(proj.n);
138 if proj.params.contains_key(&GUAM) {
139 store.m1 = mlfn(proj.phi0, store.sinph0, store.cosph0, &store.en);
140 guam = true;
141 } else {
142 match store.mode {
143 ProjMode::NPole => {
144 store.mp = mlfn(FRAC_PI_2, 1., 0., &store.en);
145 }
146 ProjMode::SPole => {
147 store.mp = mlfn(-FRAC_PI_2, -1., 0., &store.en);
148 }
149 ProjMode::Equit | ProjMode::Obliq => {
150 store.n1 = 1. / sqrt(1. - proj.es * store.sinph0 * store.sinph0);
151 store.he = proj.e / sqrt(proj.one_es);
152 store._g = store.sinph0 * store.he;
153 store.he *= store.cosph0;
154 }
155 }
156 method = ProjMethod::Ellipsoidal;
157 }
158 }
159 }
160
161 AzimuthalEquidistantProjection { proj, store: store.into(), method, guam }
162 }
163 fn forward<P: TransformCoordinates>(&self, p: &mut P) {
164 if self.guam {
165 e_guam_fwd(&self.store.borrow(), &self.proj.borrow(), p);
166 } else if self.method == ProjMethod::Ellipsoidal {
167 aeqd_e_forward(&mut self.store.borrow_mut(), &self.proj.borrow(), p);
168 } else {
169 aeqd_s_forward(&mut self.store.borrow_mut(), &self.proj.borrow(), p);
170 }
171 }
172 fn inverse<P: TransformCoordinates>(&self, p: &mut P) {
173 if self.guam {
174 e_guam_inv(&self.store.borrow(), &self.proj.borrow(), p);
175 } else if self.method == ProjMethod::Ellipsoidal {
176 aeqd_e_inverse(&mut self.store.borrow_mut(), &self.proj.borrow(), p);
177 } else {
178 aeqd_s_inverse(&self.store.borrow(), &self.proj.borrow(), p);
179 }
180 }
181}
182
183pub fn e_guam_fwd<P: TransformCoordinates>(aeqd: &AeqdData, proj: &Proj, p: &mut P) {
185 let cosphi = cos(p.phi());
186 let sinphi = sin(p.phi());
187 let t = 1. / sqrt(1. - proj.es * sinphi * sinphi);
188 let x = p.lam() * cosphi * t;
189 let y = mlfn(p.phi(), sinphi, cosphi, &aeqd.en) - aeqd.m1
190 + 0.5 * p.lam() * p.lam() * cosphi * sinphi * t;
191
192 p.set_x(x);
193 p.set_y(y);
194}
195
196pub fn aeqd_e_forward<P: TransformCoordinates>(aeqd: &mut AeqdData, proj: &Proj, p: &mut P) {
198 let x;
199 let y;
200 let mut coslam = cos(p.lam());
201 match aeqd.mode {
202 ProjMode::NPole => {
203 coslam = -coslam;
204 let cosphi = cos(p.phi());
205 let sinphi = sin(p.phi());
206 let rho = fabs(aeqd.mp - mlfn(p.phi(), sinphi, cosphi, &aeqd.en));
207 x = rho * sin(p.lam());
208 y = rho * coslam;
209 }
210 ProjMode::SPole => {
211 let cosphi = cos(p.phi());
212 let sinphi = sin(p.phi());
213 let rho = fabs(aeqd.mp - mlfn(p.phi(), sinphi, cosphi, &aeqd.en));
214 x = rho * sin(p.lam());
215 y = rho * coslam;
216 }
217 ProjMode::Equit | ProjMode::Obliq => {
218 if fabs(p.lam()) < EPS10 && fabs(p.phi() - proj.phi0) < EPS10 {
219 x = 0.;
220 y = 0.;
221 } else {
222 let lat1 = proj.phi0.to_degrees();
223 let lon1 = 0.;
224 let lat2 = p.phi().to_degrees();
225 let lon2 = p.lam().to_degrees();
226
227 let mut s12 = 0.0;
228 let mut azi1: f64 = 0.;
229 let mut azi2 = 0.;
230 geod_inverse(&mut aeqd.g, lat1, lon1, lat2, lon2, &mut s12, &mut azi1, &mut azi2);
231 azi1 = azi1.to_radians();
232 x = s12 * sin(azi1);
233 y = s12 * cos(azi1);
234 }
235 }
236 }
237
238 p.set_x(x);
239 p.set_y(y);
240}
241
242pub fn aeqd_s_forward<P: TransformCoordinates>(aeqd: &mut AeqdData, proj: &Proj, p: &mut P) {
244 let x;
245 let mut y;
246 if aeqd.mode == ProjMode::Equit {
247 let cosphi = cos(p.phi());
248 let sinphi = sin(p.phi());
249 let coslam = cos(p.lam());
250 let sinlam = sin(p.lam());
251
252 y = cosphi * coslam;
253
254 if fabs(fabs(y) - 1.) < TOL {
255 if y < 0. {
256 panic!("Coordinate outside projection domain");
257 } else {
258 aeqd_e_forward(aeqd, proj, p);
259 return;
260 }
261 } else {
262 y = acos(y);
263 y /= sin(y);
264 x = y * cosphi * sinlam;
265 y *= sinphi;
266 }
267 } else if aeqd.mode == ProjMode::Obliq {
268 let cosphi = cos(p.phi());
269 let sinphi = sin(p.phi());
270 let coslam = cos(p.lam());
271 let sinlam = sin(p.lam());
272 let cosphi_x_coslam = cosphi * coslam;
273
274 y = aeqd.sinph0 * sinphi + aeqd.cosph0 * cosphi_x_coslam;
275
276 if fabs(fabs(y) - 1.) < TOL {
277 if y < 0. {
278 panic!("Coordinate outside projection domain");
279 } else {
280 aeqd_e_forward(aeqd, proj, p);
281 return;
282 }
283 } else {
284 y = acos(y);
285 y /= sin(y);
286 x = y * cosphi * sinlam;
287 y *= aeqd.cosph0 * sinphi - aeqd.sinph0 * cosphi_x_coslam;
288 }
289 } else {
290 let mut coslam = cos(p.lam());
291 let sinlam = sin(p.lam());
292 if aeqd.mode == ProjMode::NPole {
293 p.set_phi(-p.phi());
294 coslam = -coslam;
295 }
296 if fabs(p.phi() - FRAC_PI_2) < EPS10 {
297 panic!("Coordinate outside projection domain");
298 }
299 y = FRAC_PI_2 + p.phi();
300 x = y * sinlam;
301 y *= coslam;
302 }
303
304 p.set_x(x);
305 p.set_y(y);
306}
307
308pub fn e_guam_inv<P: TransformCoordinates>(aeqd: &AeqdData, proj: &Proj, p: &mut P) {
310 let x = p.x();
311 let y = p.y();
312 let x2 = 0.5 * x * x;
313 let mut t = 0.;
314 p.set_phi(proj.phi0);
315 for _ in 0..3 {
316 t = proj.e * sin(p.phi());
317 t = sqrt(1. - t * t);
318 p.set_phi(inv_mlfn(aeqd.m1 + y - x2 * tan(p.phi()) * t, &aeqd.en));
319 }
320 p.set_lam(x * t / cos(p.phi()));
321}
322
323pub fn aeqd_e_inverse<P: TransformCoordinates>(aeqd: &mut AeqdData, proj: &Proj, p: &mut P) {
325 let x = p.x();
326 let y = p.y();
327 let s12 = hypot(x, y);
328 if s12 < EPS10 {
329 p.set_phi(proj.phi0);
330 p.set_lam(0.);
331 return;
332 }
333 if aeqd.mode == ProjMode::Obliq || aeqd.mode == ProjMode::Equit {
334 let lat1 = proj.phi0.to_degrees();
335 let lon1 = 0.;
336 let azi1 = atan2(x, y).to_degrees(); let mut lat2 = 0.;
338 let mut lon2 = 0.;
339 let mut azi2 = 0.;
340 geod_direct(&mut aeqd.g, lat1, lon1, azi1, s12, &mut lat2, &mut lon2, &mut azi2);
341 p.set_phi(lat2.to_radians());
342 p.set_lam(lon2.to_radians());
343 } else {
344 p.set_phi(inv_mlfn(
346 if aeqd.mode == ProjMode::NPole { aeqd.mp - s12 } else { aeqd.mp + s12 },
347 &aeqd.en,
348 ));
349 p.set_lam(atan2(x, if aeqd.mode == ProjMode::NPole { -y } else { y }));
350 }
351}
352
353pub fn aeqd_s_inverse<P: TransformCoordinates>(aeqd: &AeqdData, proj: &Proj, p: &mut P) {
355 let mut x = p.x();
356 let mut y = p.y();
357 let lam;
358 let phi;
359 let mut c_rh = hypot(x, y);
360 if c_rh > PI {
361 if c_rh - EPS10 > PI {
362 panic!("Coordinate outside projection domain");
363 }
364 c_rh = PI;
365 } else if c_rh < EPS10 {
366 phi = proj.phi0;
367 lam = 0.;
368 p.set_phi(phi);
369 p.set_lam(lam);
370 return;
371 }
372 if aeqd.mode == ProjMode::Obliq || aeqd.mode == ProjMode::Equit {
373 let sinc = sin(c_rh);
374 let cosc = cos(c_rh);
375 if aeqd.mode == ProjMode::Equit {
376 phi = aasin(y * sinc / c_rh);
377 x *= sinc;
378 y = cosc * c_rh;
379 } else {
380 phi = aasin(cosc * aeqd.sinph0 + y * sinc * aeqd.cosph0 / c_rh);
381 y = (cosc - aeqd.sinph0 * sin(phi)) * c_rh;
382 x *= sinc * aeqd.cosph0;
383 }
384 lam = if y == 0. { 0. } else { atan2(x, y) };
385 } else if aeqd.mode == ProjMode::NPole {
386 phi = FRAC_PI_2 - c_rh;
387 lam = atan2(x, -y);
388 } else {
389 phi = c_rh - FRAC_PI_2;
390 lam = atan2(x, y);
391 }
392
393 p.set_lam(lam);
394 p.set_phi(phi);
395}