gistools/proj/project/
aeqd.rs

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// /******************************************************************************
14//  * Project:  PROJ.4
15//  * Purpose:  Implementation of the aeqd (Azimuthal Equidistant) projection.
16//  * Author:   Gerald Evenden
17//  *
18//  ******************************************************************************
19//  * Copyright (c) 1995, Gerald Evenden
20//  *
21//  * Permission is hereby granted, free of charge, to any person obtaining a
22//  * copy of this software and associated documentation files (the "Software"),
23//  * to deal in the Software without restriction, including without limitation
24//  * the rights to use, copy, modify, merge, publish, distribute, sublicense,
25//  * and/or sell copies of the Software, and to permit persons to whom the
26//  * Software is furnished to do so, subject to the following conditions:
27//  *
28//  * The above copyright notice and this permission notice shall be included
29//  * in all copies or substantial portions of the Software.
30//  *
31//  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
32//  * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
33//  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
34//  * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
35//  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
36//  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
37//  * DEALINGS IN THE SOFTWARE.
38//  *****************************************************************************/
39/// Azimuthal Equidistant Variables
40#[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// TODO: https://epsg.org/coord-operation-method_9831/Guam-Projection.html
57// Guam is an EPSG code, so we should add the code and also set the guam flag in the constructor or JSON parsing?
58
59/// # Azimuthal Equidistant Projection
60///
61/// **Classification**: Azimuthal
62///
63/// **Available forms**: Forward and inverse, spherical and ellipsoidal
64///
65/// **Defined area**: Global
66///
67/// **Alias**: `aeqd`
68///
69/// **Domain**: 2D
70///
71/// **Input type**: Geodetic coordinates
72///
73/// **Output type**: Projected coordinates
74///
75/// ## Projection String
76/// ```ini
77/// +proj=aeqd
78/// ```
79///
80/// ## Required Parameters
81/// None
82///
83/// ## Optional Parameters
84/// - `guam`: Use Guam ellipsoidal formulas (accurate near Guam: $λ ≈ 144.5°$, $φ ≈ 13.5°$)
85/// - `lat0`: Latitude of origin
86/// - `lon0`: Longitude of origin
87/// - `x0`: False easting
88/// - `y0`: False northing
89/// - `ellps`: Ellipsoid name
90/// - `R`: Radius of sphere
91///
92/// ![Azimuthal Equidistant Projection](https://github.com/Open-S2/gis-tools/blob/master/assets/proj4/projections/images/aeqd.png?raw=true)
93#[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
183/// Guam Ellipsoidal forward project
184pub 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
196/// Azimuthal Equidistant Ellipsoidal forward project
197pub 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
242/// Azimuthal Equidistant Spheroidal forward project
243pub 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
308/// Guam Ellipsoidal inverse project
309pub 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
323/// Azimuthal Equidistant Ellipsoidal inverse project
324pub 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(); // Clockwise from north
337        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        // Polar
345        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
353/// Azimuthal Equidistant Spheroidal inverse project
354pub 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}