gistools/proj/project/
laea.rs

1use crate::proj::{
2    CoordinateStep, EPS10, LAMBERT_AZIMUTHAL_EQUAL_AREA, LAMBERT_AZIMUTHAL_EQUAL_AREA_SPHERICAL,
3    Proj, ProjMethod, ProjMode, ProjectCoordinates, TransformCoordinates, authalic_lat,
4    authalic_lat_compute_coeffs, authalic_lat_inverse, authalic_lat_q,
5};
6use alloc::{rc::Rc, vec::Vec};
7use core::{
8    cell::RefCell,
9    f64::consts::{FRAC_PI_2, FRAC_PI_4},
10};
11use libm::{asin, atan2, cos, fabs, hypot, sin, sqrt};
12
13/// Lambert Azimuthal Equal Area Variables
14#[derive(Debug, Default, Clone, PartialEq)]
15pub struct LaeaData {
16    sinb1: f64,
17    cosb1: f64,
18    xmf: f64,
19    ymf: f64,
20    mmf: f64,
21    qp: f64,
22    dd: f64,
23    rq: f64,
24    apa: Vec<f64>,
25    mode: ProjMode,
26}
27
28/// Lambert Azimuthal Equal Area Projection
29///
30/// See [`LambertAzimuthalEqualAreaBase`] for full documentation.
31pub type LambertAzimuthalEqualAreaProjection =
32    LambertAzimuthalEqualAreaBase<LAMBERT_AZIMUTHAL_EQUAL_AREA>;
33/// Lambert Azimuthal Equal Area (Spherical) Projection
34///
35/// See [`LambertAzimuthalEqualAreaBase`] for full documentation.
36pub type LambertAzimuthalEqualAreaSphericalProjection =
37    LambertAzimuthalEqualAreaBase<LAMBERT_AZIMUTHAL_EQUAL_AREA_SPHERICAL>;
38
39/// # Lambert Azimuthal Equal Area
40///
41/// **Classification**: Azimuthal
42///
43/// **Available forms**: Forward and inverse, spherical and ellipsoidal
44///
45/// **Defined area**: Global
46///
47/// **Alias**: laea
48///
49/// **Domain**: 2D
50///
51/// **Input type**: Geodetic coordinates
52///
53/// **Output type**: Projected coordinates
54///
55/// ## Projection String
56/// ```ini
57/// +proj=laea
58/// ```
59///
60/// ## Required Parameters
61/// - None, all parameters are optional for this projection.
62///
63/// ## Optional Parameters
64/// - `+lon_0`: Longitude of projection center. Defaults to `0`.
65/// - `+lat_0`: Latitude of projection center. Defaults to `0`.
66/// - `+ellps`: Ellipsoid. Defaults to `WGS84`.
67/// - `+R`: Radius of the sphere.
68/// - `+x_0`: False easting. Defaults to `0`.
69/// - `+y_0`: False northing. Defaults to `0`.
70///
71/// Reference
72/// "New Equal-Area Map Projections for Noncircular Regions", John P. Snyder,
73/// The American Cartographer, Vol 15, No. 4, October 1988, pp. 341-355.
74///
75/// ![Lambert Azimuthal Equal Area](https://github.com/Open-S2/gis-tools/blob/master/assets/proj4/projections/images/laea.png?raw=true)
76#[derive(Debug, Clone, PartialEq)]
77pub struct LambertAzimuthalEqualAreaBase<const C: i64> {
78    proj: Rc<RefCell<Proj>>,
79    store: RefCell<LaeaData>,
80    method: ProjMethod,
81}
82impl<const C: i64> ProjectCoordinates for LambertAzimuthalEqualAreaBase<C> {
83    fn code(&self) -> i64 {
84        C
85    }
86    fn name(&self) -> &'static str {
87        "Lambert Azimuthal Equal Area"
88    }
89    fn names() -> &'static [&'static str] {
90        &[
91            "Lambert Azimuthal Equal Area",
92            "Lambert_Azimuthal_Equal_Area",
93            "Lambert Azimuthal Equal Area (Spherical)",
94            "laea",
95        ]
96    }
97}
98impl<const C: i64> CoordinateStep for LambertAzimuthalEqualAreaBase<C> {
99    fn new(proj: Rc<RefCell<Proj>>) -> Self {
100        let mut store = LaeaData::default();
101        let method: ProjMethod;
102        {
103            let proj = &mut proj.borrow_mut();
104
105            let t = fabs(proj.phi0);
106            if t > FRAC_PI_2 + EPS10 {
107                panic!("Invalid value for lat_0: |lat_0| should be <= 90°");
108            }
109            if fabs(t - FRAC_PI_2) < EPS10 {
110                store.mode = if proj.phi0 < 0. { ProjMode::SPole } else { ProjMode::NPole };
111            } else if fabs(t) < EPS10 {
112                store.mode = ProjMode::Equit;
113            } else {
114                store.mode = ProjMode::Obliq;
115            }
116            method = if proj.es != 0.0 {
117                proj.e = sqrt(proj.es);
118                store.qp = authalic_lat_q(1.0, proj);
119                store.mmf = 0.5 / (1. - proj.es);
120                store.apa = authalic_lat_compute_coeffs(proj.n);
121                match store.mode {
122                    ProjMode::NPole | ProjMode::SPole => {
123                        store.dd = 1.;
124                    }
125                    ProjMode::Equit => {
126                        store.rq = sqrt(0.5 * store.qp);
127                        store.dd = 1. / store.rq;
128                        store.xmf = 1.;
129                        store.ymf = 0.5 * store.qp;
130                    }
131                    ProjMode::Obliq => {
132                        store.rq = sqrt(0.5 * store.qp);
133                        let sinphi = sin(proj.phi0);
134                        let cosphi = cos(proj.phi0);
135                        let b1 =
136                            authalic_lat(proj.phi0, sinphi, cosphi, &store.apa, proj, store.qp);
137                        store.sinb1 = sin(b1);
138                        store.cosb1 = cos(b1);
139                        store.dd = cos(proj.phi0)
140                            / (sqrt(1. - proj.es * sinphi * sinphi) * store.rq * store.cosb1);
141                        store.xmf = store.rq;
142                        store.ymf = store.xmf / store.dd;
143                        store.xmf *= store.dd;
144                    }
145                }
146                ProjMethod::Ellipsoidal
147            } else {
148                if store.mode == ProjMode::Obliq {
149                    store.sinb1 = sin(proj.phi0);
150                    store.cosb1 = cos(proj.phi0);
151                }
152                ProjMethod::Spheroidal
153            }
154        }
155
156        LambertAzimuthalEqualAreaBase { proj, store: store.into(), method }
157    }
158    fn forward<P: TransformCoordinates>(&self, p: &mut P) {
159        if self.method == ProjMethod::Spheroidal {
160            laea_s_forward(&self.store.borrow(), &self.proj.borrow(), p);
161        } else {
162            laea_e_forward(&self.store.borrow(), &self.proj.borrow(), p);
163        }
164    }
165    fn inverse<P: TransformCoordinates>(&self, p: &mut P) {
166        if self.method == ProjMethod::Spheroidal {
167            laea_s_inverse(&self.store.borrow(), &self.proj.borrow(), p);
168        } else {
169            laea_e_inverse(&self.store.borrow(), &self.proj.borrow(), p);
170        }
171    }
172}
173
174/// Equidistant Conic Ellipsoidal forward project
175pub fn laea_e_forward<P: TransformCoordinates>(laea: &LaeaData, proj: &Proj, p: &mut P) {
176    let coslam = cos(p.lam());
177    let sinlam = sin(p.lam());
178    let sinphi = sin(p.phi());
179    let cosphi = cos(p.phi());
180    let mut sinb = 0.0;
181    let mut cosb = 0.0;
182    let xi = authalic_lat(p.phi(), sinphi, cosphi, &laea.apa, proj, laea.qp);
183    let mut q = sin(xi) * laea.qp;
184    let mut b;
185
186    if laea.mode == ProjMode::Obliq || laea.mode == ProjMode::Equit {
187        sinb = sin(xi);
188        cosb = cos(xi);
189    }
190
191    match laea.mode {
192        ProjMode::Obliq => {
193            b = 1. + laea.sinb1 * sinb + laea.cosb1 * cosb * coslam;
194        }
195        ProjMode::Equit => {
196            b = 1. + cosb * coslam;
197        }
198        ProjMode::NPole => {
199            b = FRAC_PI_2 + p.phi();
200            q = laea.qp - q;
201        }
202        ProjMode::SPole => {
203            b = p.phi() - FRAC_PI_2;
204            q += laea.qp;
205        }
206    }
207    if fabs(b) < EPS10 {
208        panic!("Coordinate outside projection domain");
209    }
210
211    match laea.mode {
212        ProjMode::Obliq => {
213            b = sqrt(2. / b);
214            p.set_y(laea.ymf * b * (laea.cosb1 * sinb - laea.sinb1 * cosb * coslam));
215            p.set_x(laea.xmf * b * cosb * sinlam);
216        }
217        ProjMode::Equit => {
218            b = sqrt(2. / (1. + cosb * coslam));
219            p.set_y(b * sinb * laea.ymf);
220            p.set_x(laea.xmf * b * cosb * sinlam);
221        }
222        ProjMode::NPole | ProjMode::SPole => {
223            if q >= 1e-15 {
224                b = sqrt(q);
225                p.set_x(b * sinlam);
226                p.set_y(coslam * (if laea.mode == ProjMode::SPole { b } else { -b }));
227            } else {
228                p.set_x(0.);
229                p.set_y(0.);
230            }
231        }
232    }
233}
234
235/// Equidistant Conic Spheroidal forward project
236pub fn laea_s_forward<P: TransformCoordinates>(laea: &LaeaData, proj: &Proj, p: &mut P) {
237    let sinphi = sin(p.phi());
238    let cosphi = cos(p.phi());
239    let mut coslam = cos(p.lam());
240    let x;
241    let mut y;
242    match laea.mode {
243        ProjMode::Equit => {
244            y = 1. + cosphi * coslam;
245            if y <= EPS10 {
246                panic!("Coordinate outside projection domain");
247            }
248            y = sqrt(2. / y);
249            x = y * cosphi * sin(p.lam());
250            y *= if laea.mode == ProjMode::Equit {
251                sinphi
252            } else {
253                laea.cosb1 * sinphi - laea.sinb1 * cosphi * coslam
254            };
255        }
256        ProjMode::Obliq => {
257            y = 1. + laea.sinb1 * sinphi + laea.cosb1 * cosphi * coslam;
258            if y <= EPS10 {
259                panic!("Coordinate outside projection domain");
260            }
261            y = sqrt(2. / y);
262            x = y * cosphi * sin(p.lam());
263            y *= if laea.mode == ProjMode::Equit {
264                sinphi
265            } else {
266                laea.cosb1 * sinphi - laea.sinb1 * cosphi * coslam
267            };
268        }
269        ProjMode::NPole => {
270            coslam = -coslam;
271            if fabs(p.phi() + proj.phi0) < EPS10 {
272                panic!("Coordinate outside projection domain");
273            }
274            y = FRAC_PI_4 - p.phi() * 0.5;
275            y = 2. * (if laea.mode == ProjMode::SPole { cos(y) } else { sin(y) });
276            x = y * sin(p.lam());
277            y *= coslam;
278        }
279        ProjMode::SPole => {
280            if fabs(p.phi() + proj.phi0) < EPS10 {
281                panic!("Coordinate outside projection domain");
282            }
283            y = FRAC_PI_4 - p.phi() * 0.5;
284            y = 2. * (if laea.mode == ProjMode::SPole { cos(y) } else { sin(y) });
285            x = y * sin(p.lam());
286            y *= coslam;
287        }
288    }
289    p.set_x(x);
290    p.set_y(y);
291}
292
293/// Equidistant Conic Ellipsoidal inverse project
294pub fn laea_e_inverse<P: TransformCoordinates>(laea: &LaeaData, proj: &Proj, p: &mut P) {
295    // static PJ_LP laea_e_inverse(PJ_XY xy, PJ *P) { /* Ellipsoidal, inverse */
296    //     PJ_LP lp = {0.0, 0.0};
297    //     struct pj_laea_data *Q = static_cast<struct pj_laea_data *>(proj.opaque);
298    //     double c_ce, s_ce, q, rho, ab = 0.0;
299    let mut x = p.x();
300    let mut y = p.y();
301    let mut ab;
302
303    match laea.mode {
304        ProjMode::Equit | ProjMode::Obliq => {
305            x /= laea.dd;
306            y *= laea.dd;
307            let rho = hypot(x, y);
308            if rho < EPS10 {
309                p.set_lam(0.);
310                p.set_phi(proj.phi0);
311                return;
312            }
313            let asin_argument = 0.5 * rho / laea.rq;
314            if asin_argument > 1. {
315                // proj_errno_set(P, PROJ_ERR_COORD_TRANSFM_OUTSIDE_PROJECTION_DOMAIN);
316                // return lp;
317                panic!("Coordinate outside projection domain");
318            }
319            let s_ce = 2. * asin(asin_argument);
320            let c_ce = cos(s_ce);
321            let s_ce = sin(s_ce);
322            x *= s_ce;
323            if laea.mode == ProjMode::Obliq {
324                ab = c_ce * laea.sinb1 + y * s_ce * laea.cosb1 / rho;
325                y = rho * laea.cosb1 * c_ce - y * laea.sinb1 * s_ce;
326            } else {
327                ab = y * s_ce / rho;
328                y = rho * c_ce;
329            }
330        }
331        ProjMode::NPole => {
332            y = -y;
333            let q = x * x + y * y;
334            if q == 0.0 {
335                p.set_lam(0.);
336                p.set_phi(proj.phi0);
337                return;
338            }
339            ab = 1. - q / laea.qp;
340            if laea.mode == ProjMode::SPole {
341                ab = -ab;
342            }
343        }
344        ProjMode::SPole => {
345            let q = x * x + y * y;
346            if q == 0.0 {
347                p.set_lam(0.);
348                p.set_phi(proj.phi0);
349                return;
350            }
351            ab = 1. - q / laea.qp;
352            if laea.mode == ProjMode::SPole {
353                ab = -ab;
354            }
355        }
356    }
357    p.set_lam(atan2(x, y));
358    p.set_phi(authalic_lat_inverse(asin(ab), &laea.apa, proj, laea.qp));
359}
360
361/// Equidistant Conic Spheroidal inverse project
362pub fn laea_s_inverse<P: TransformCoordinates>(laea: &LaeaData, proj: &Proj, p: &mut P) {
363    // static PJ_LP laea_s_inverse(PJ_XY xy, PJ *P) { /* Spheroidal, inverse */
364    //     PJ_LP lp = {0.0, 0.0};
365    //     struct pj_laea_data *Q = static_cast<struct pj_laea_data *>(proj.opaque);
366    //     double cosz = 0.0, rh, sinz = 0.0;
367
368    let mut cosz = 0.0;
369    let mut sinz = 0.0;
370    let mut x = p.x();
371    let mut y = p.y();
372    let rh = hypot(x, y);
373    let mut phi = rh * 0.5;
374    if phi > 1. {
375        // proj_errno_set(P, PROJ_ERR_COORD_TRANSFM_OUTSIDE_PROJECTION_DOMAIN);
376        // return lp;
377        panic!("Coordinate outside projection domain");
378    }
379    phi = 2. * asin(phi);
380    if laea.mode == ProjMode::Obliq || laea.mode == ProjMode::Equit {
381        sinz = sin(phi);
382        cosz = cos(phi);
383    }
384    match laea.mode {
385        ProjMode::Equit => {
386            phi = if fabs(rh) <= EPS10 { 0. } else { asin(y * sinz / rh) };
387            x *= sinz;
388            y = cosz * rh;
389        }
390        ProjMode::Obliq => {
391            phi = if fabs(rh) <= EPS10 {
392                proj.phi0
393            } else {
394                asin(cosz * laea.sinb1 + y * sinz * laea.cosb1 / rh)
395            };
396            x *= sinz * laea.cosb1;
397            y = (cosz - sin(phi) * laea.sinb1) * rh;
398        }
399        ProjMode::NPole => {
400            y = -y;
401            phi = FRAC_PI_2 - phi;
402        }
403        ProjMode::SPole => {
404            phi -= FRAC_PI_2;
405        }
406    }
407    let lam = if y == 0. && (laea.mode == ProjMode::Equit || laea.mode == ProjMode::Obliq) {
408        0.
409    } else {
410        atan2(x, y)
411    };
412
413    p.set_lam(lam);
414    p.set_phi(phi);
415}