gistools/proj/project/
tmerc.rs

1use crate::proj::{
2    ALGO, APPROX, AuxLat, CoordinateStep, EPS10, Proj, ProjValue, ProjectCoordinates, SOUTH,
3    TRANSVERSE_MERCATOR, TRANSVERSE_MERCATOR_SOUTH_ORIENTATED, TransformCoordinates, ZONE, adjlon,
4    auxlat_coeffs, auxlat_convert, auxlat_convert_mid, enfn, inv_mlfn, mlfn, rectifying_radius,
5};
6use alloc::{rc::Rc, vec::Vec};
7use core::{
8    cell::RefCell,
9    f64::consts::{FRAC_PI_2, PI},
10};
11use libm::{
12    acos, asin, asinh, atan2, copysign, cos, exp, fabs, floor, hypot, log, round, sin, sinh, sqrt,
13};
14
15//                   Transverse Mercator implementations
16//
17// In this file two transverse mercator implementations are found. One of Gerald
18// Evenden/John Snyder origin and one of Knud Poder/Karsten Engsager origin. The
19// former is regarded as "approximate" in the following and the latter is
20// "exact". This word choice has been made to distinguish between the two
21// algorithms, where the Evenden/Snyder implementation is the faster, less
22// accurate implementation and the Poder/Engsager algorithm is a slightly
23// slower, but more accurate implementation.
24
25/// Poder/Engsager if far from central meridian, otherwise Evenden/Snyder
26#[derive(Debug, Default, Clone, PartialEq)]
27pub enum TMercAlgo {
28    /// Auto
29    Auto,
30    /// Evenden/Snyder
31    EvendenSnyder,
32    /// Poder/Engsager
33    #[default]
34    PoderEngsager,
35}
36
37/// Approximate: Evenden/Snyder
38#[derive(Debug, Default, Clone, PartialEq)]
39pub struct EvendenSnyder {
40    esp: f64,
41    ml0: f64,
42    en: Vec<f64>,
43}
44
45/// More exact: Poder/Engsager
46#[derive(Debug, Default, Clone, PartialEq)]
47pub struct PoderEngsager {
48    /// Merid. quad., scaled to the projection
49    qn: f64,
50    /// Radius vector in polar coord. systems
51    zb: f64,
52    /// Constants for Gauss -> Geo lat
53    cgb: [f64; 6],
54    /// Constants for Geo lat -> Gauss
55    cbg: [f64; 6],
56    /// Constants for transv. merc. -> geo
57    utg: [f64; 6],
58    /// Constants for geo -> transv. merc.
59    gtu: [f64; 6],
60}
61
62/// Transverse Mercator Data
63#[derive(Debug, Default, Clone, PartialEq)]
64pub struct TmercData {
65    approx: EvendenSnyder,
66    exact: PoderEngsager,
67}
68
69/// Transverse Mercator Mode
70#[derive(Debug, Default, Clone, PartialEq)]
71pub enum TMercMode {
72    /// Spherical
73    #[default]
74    Spherical,
75    /// Approximate Ellipsoidal
76    ApproxEllipsoidal,
77    /// Exact Ellipsoidal
78    ExactEllipsoidal,
79    /// Auto Ellipsoidal
80    AutoEllipsoidal,
81}
82
83// Constants for "approximate" transverse mercator
84const FC1: f64 = 1.0;
85const FC2: f64 = 0.5;
86const FC3: f64 = 0.166_666_666_666_666_66;
87const FC4: f64 = 0.083_333_333_333_333_33;
88const FC5: f64 = 0.05;
89const FC6: f64 = 0.033_333_333_333_333_33;
90const FC7: f64 = 0.023_809_523_809_523_808;
91const FC8: f64 = 0.017_857_142_857_142_856;
92
93/// Constant for "exact" transverse mercator */
94const PROJ_ETMERC_ORDER: i32 = 6;
95
96/// Approximate Transverse Mercator functions
97pub fn tmerc_approx_e_fwd<P: TransformCoordinates>(tmerc: &mut TmercData, proj: &Proj, p: &mut P) {
98    let evenden_snyder = &tmerc.approx;
99    // Fail if our longitude is more than 90 degrees from the
100    // central meridian since the results are essentially garbage.
101    // Is error -20 really an appropriate return value?
102    //
103    //  http://trac.osgeo.org/proj/ticket/5
104    if p.lam() < -FRAC_PI_2 || p.lam() > FRAC_PI_2 {
105        panic!("Longitude out of range");
106    }
107
108    let sinphi = sin(p.phi());
109    let cosphi = cos(p.phi());
110    let mut t = if fabs(cosphi) > 1e-10 { sinphi / cosphi } else { 0. };
111    t *= t;
112    let mut al = cosphi * p.lam();
113    let als = al * al;
114    al /= sqrt(1. - proj.es * sinphi * sinphi);
115    let n = evenden_snyder.esp * cosphi * cosphi;
116    p.set_x(
117        proj.k0
118            * al
119            * (FC1
120                + FC3
121                    * als
122                    * (1. - t
123                        + n
124                        + FC5
125                            * als
126                            * (5.
127                                + t * (t - 18.)
128                                + n * (14. - 58. * t)
129                                + FC7 * als * (61. + t * (t * (179. - t) - 479.))))),
130    );
131    p.set_y(
132        proj.k0
133            * (mlfn(p.phi(), sinphi, cosphi, &evenden_snyder.en) - evenden_snyder.ml0
134                + sinphi
135                    * al
136                    * p.lam()
137                    * FC2
138                    * (1.
139                        + FC4
140                            * als
141                            * (5. - t
142                                + n * (9. + 4. * n)
143                                + FC6
144                                    * als
145                                    * (61.
146                                        + t * (t - 58.)
147                                        + n * (270. - 330. * t)
148                                        + FC8 * als * (1385. + t * (t * (543. - t) - 3111.)))))),
149    );
150}
151
152/// Spherical Transverse Mercator forward project
153pub fn tmerc_spherical_fwd<P: TransformCoordinates>(tmerc: &mut TmercData, proj: &Proj, p: &mut P) {
154    let evenden_snyder = &tmerc.approx;
155
156    let cosphi = cos(p.phi());
157    let mut b = cosphi * sin(p.lam());
158    if fabs(fabs(b) - 1.) <= EPS10 {
159        panic!("Coordinate outside projection domain");
160    }
161
162    let x = evenden_snyder.ml0 * log((1. + b) / (1. - b));
163    let mut y = cosphi * cos(p.lam()) / sqrt(1. - b * b);
164
165    b = fabs(y);
166    if cosphi == 1. && (p.lam() < -FRAC_PI_2 || p.lam() > FRAC_PI_2) {
167        // Helps to be able to roundtrip |longitudes| > 90 at lat=0
168        // We could also map to -M_PI ...
169        y = PI;
170    } else if b >= 1. {
171        if (b - 1.) > EPS10 {
172            panic!("Coordinate outside projection domain");
173        } else {
174            y = 0.;
175        }
176    } else {
177        y = acos(y);
178    }
179
180    if p.phi() < 0. {
181        y = -y;
182    }
183    y = evenden_snyder.esp * (y - proj.phi0);
184
185    p.set_x(x);
186    p.set_y(y);
187}
188
189/// Approximate Transverse Mercator inverse project
190pub fn tmerc_approx_e_inv<P: TransformCoordinates>(tmerc: &mut TmercData, proj: &Proj, p: &mut P) {
191    let evenden_snyder = &tmerc.approx;
192
193    let mut phi = inv_mlfn(evenden_snyder.ml0 + p.y() / proj.k0, &evenden_snyder.en);
194    let lam: f64;
195    if fabs(phi) >= FRAC_PI_2 {
196        phi = if p.y() < 0. { -FRAC_PI_2 } else { FRAC_PI_2 };
197        lam = 0.;
198    } else {
199        let sinphi = sin(phi);
200        let cosphi = cos(phi);
201        let mut t = if fabs(cosphi) > 1e-10 { sinphi / cosphi } else { 0. };
202        let n = evenden_snyder.esp * cosphi * cosphi;
203        let mut con = 1. - proj.es * sinphi * sinphi;
204        let d = p.x() * sqrt(con) / proj.k0;
205        con *= t;
206        t *= t;
207        let ds = d * d;
208        phi -= (con * ds / (1. - proj.es))
209            * FC2
210            * (1.
211                - ds * FC4
212                    * (5. + t * (3. - 9. * n) + n * (1. - 4. * n)
213                        - ds * FC6
214                            * (61. + t * (90. - 252. * n + 45. * t) + 46. * n
215                                - ds * FC8 * (1385. + t * (3633. + t * (4095. + 1575. * t))))));
216        lam = d
217            * (FC1
218                - ds * FC3
219                    * (1. + 2. * t + n
220                        - ds * FC5
221                            * (5. + t * (28. + 24. * t + 8. * n) + 6. * n
222                                - ds * FC7 * (61. + t * (662. + t * (1320. + 720. * t))))))
223            / cosphi;
224    }
225    p.set_phi(phi);
226    p.set_lam(lam);
227}
228
229/// Spherical Transverse Mercator inverse project
230pub fn tmerc_spherical_inv<P: TransformCoordinates>(tmerc: &mut TmercData, proj: &Proj, p: &mut P) {
231    let evenden_snyder = &tmerc.approx;
232    let mut h = exp(p.x() / evenden_snyder.esp);
233    if h == 0. {
234        panic!("Coordinate outside projection domain");
235    }
236    let g = 0.5 * (h - 1. / h);
237    // D, as in equation 8-8 of USGS "Map Projections - A Working Manual"
238    let d = proj.phi0 + p.y() / evenden_snyder.esp;
239    h = cos(d);
240    p.set_phi(asin(sqrt((1. - h * h) / (1. + g * g))));
241    // Make sure that phi is on the correct hemisphere when false northing is used
242    p.set_phi(copysign(p.phi(), d));
243    p.set_lam(if g != 0.0 || h != 0.0 { atan2(g, h) } else { 0. });
244}
245
246fn setup_approx(tmerc: &mut TmercData, proj: &Proj) {
247    let evenden_snyder = &mut tmerc.approx;
248
249    if proj.es != 0.0 {
250        evenden_snyder.en = enfn(proj.n);
251        if evenden_snyder.en.is_empty() {
252            panic!("Projection setup failed");
253        }
254
255        evenden_snyder.ml0 = mlfn(proj.phi0, sin(proj.phi0), cos(proj.phi0), &evenden_snyder.en);
256        evenden_snyder.esp = proj.es / (1. - proj.es);
257    } else {
258        evenden_snyder.esp = proj.k0;
259        evenden_snyder.ml0 = 0.5 * evenden_snyder.esp;
260    }
261}
262
263//                  Exact Transverse Mercator functions
264//
265//
266// The code in this file is largly based upon procedures:
267//
268// Written by: Knud Poder and Karsten Engsager
269//
270// Based on math from: R.Koenig and K.H. Weise, "Mathematische
271// Grundlagen der hoeheren Geodaesie und Kartographie,
272// Springer-Verlag, Berlin/Goettingen" Heidelberg, 1951.
273//
274// Modified and used here by permission of Reference Networks
275// Division, Kort og Matrikelstyrelsen (KMS), Copenhagen, Denmark
276
277/// Complex Clenshaw summation
278#[inline]
279fn clen_s(
280    a: &[f64],
281    sin_arg_r: f64,
282    cos_arg_r: f64,
283    sinh_arg_i: f64,
284    cosh_arg_i: f64,
285) -> (f64, f64) {
286    let mut r: f64;
287    let mut i: f64;
288    let mut hr: f64;
289    let mut hr1: f64 = 0.0;
290    let mut hr2: f64;
291    let mut hi: f64 = 0.0;
292    let mut hi1: f64 = 0.0;
293    let mut hi2: f64;
294
295    // arguments
296    let mut p = a.len();
297    r = 2.0 * cos_arg_r * cosh_arg_i;
298    i = -2.0 * sin_arg_r * sinh_arg_i;
299
300    // summation loop
301    p -= 1;
302    hr = a[p];
303    while p > 0 {
304        p -= 1;
305        hr2 = hr1;
306        hi2 = hi1;
307        hr1 = hr;
308        hi1 = hi;
309        hr = -hr2 + r * hr1 - i * hi1 + a[p];
310        hi = -hi2 + i * hr1 + r * hi1;
311    }
312
313    r = sin_arg_r * cosh_arg_i;
314    i = cos_arg_r * sinh_arg_i;
315    let real = r * hr - i * hi;
316    let imag = r * hi + i * hr;
317    (real, imag)
318}
319
320/// Transverse Mercator Ellipsoidal forward project
321pub fn tmerc_exact_e_fwd<P: TransformCoordinates>(tmerc: &mut TmercData, p: &mut P) {
322    let poder_engsager = &tmerc.exact;
323
324    // ell. LAT, LNG -> Gaussian LAT, LNG
325    let mut cn = auxlat_convert(p.phi(), &poder_engsager.cbg, PROJ_ETMERC_ORDER);
326    // Gaussian LAT, LNG -> compl. sph. LAT
327    let sin_cn = sin(cn);
328    let cos_cn = cos(cn);
329    let sin_ce = sin(p.lam());
330    let cos_ce = cos(p.lam());
331
332    let cos_cn_cos_ce = cos_cn * cos_ce;
333    cn = atan2(sin_cn, cos_cn_cos_ce);
334
335    let inv_denom_tan_ce = 1. / hypot(sin_cn, cos_cn_cos_ce);
336    let tan_ce = sin_ce * cos_cn * inv_denom_tan_ce;
337    // Variant of the above: found not to be measurably faster
338    // let sin_ce_cos_cn = sin_ce * cos_cn;
339    // let denom = sqrt(1. - sin_ce_cos_cn * sin_ce_cos_cn);
340    // let tan_ce = sin_ce_cos_cn / denom;
341
342    // compl. sph. N, E -> ell. norm. N, E
343    let mut ce = asinh(tan_ce); /* Replaces: Ce  = log(tan(FORTPI + Ce*0.5)); */
344    //  Non-optimized version:
345    //  let sin_arg_r  = sin(2*Cn);
346    //  let cos_arg_r  = cos(2*Cn);
347    //
348    //  Given:
349    //      sin(2 * Cn) = 2 sin(Cn) cos(Cn)
350    //          sin(atan(y)) = y / sqrt(1 + y^2)
351    //          cos(atan(y)) = 1 / sqrt(1 + y^2)
352    //      ==> sin(2 * Cn) = 2 tan_Cn / (1 + tan_Cn^2)
353    //
354    //      cos(2 * Cn) = 2cos^2(Cn) - 1
355    //                  = 2 / (1 + tan_Cn^2) - 1
356    let two_inv_denom_tan_ce = 2. * inv_denom_tan_ce;
357    let two_inv_denom_tan_ce_square = two_inv_denom_tan_ce * inv_denom_tan_ce;
358    let tmp_r = cos_cn_cos_ce * two_inv_denom_tan_ce_square;
359    let sin_arg_r = sin_cn * tmp_r;
360    let cos_arg_r = cos_cn_cos_ce * tmp_r - 1.;
361
362    //  Non-optimized version:
363    //  let sinh_arg_i = sinh(2*Ce);
364    //  let cosh_arg_i = cosh(2*Ce);
365    //
366    //  Given
367    //      sinh(2 * Ce) = 2 sinh(Ce) cosh(Ce)
368    //          sinh(asinh(y)) = y
369    //          cosh(asinh(y)) = sqrt(1 + y^2)
370    //      ==> sinh(2 * Ce) = 2 tan_ce sqrt(1 + tan_ce^2)
371    //
372    //      cosh(2 * Ce) = 2cosh^2(Ce) - 1
373    //                   = 2 * (1 + tan_ce^2) - 1
374    //
375    // and 1+tan_ce^2 = 1 + sin_ce^2 * cos_cn^2 / (sin_cn^2 + cos_cn^2 *
376    // cos_ce^2) = (sin_cn^2 + cos_cn^2 * cos_ce^2 + sin_ce^2 * cos_cn^2) /
377    // (sin_cn^2 + cos_cn^2 * cos_ce^2) = 1. / (sin_cn^2 + cos_cn^2 * cos_ce^2)
378    // = inv_denom_tan_ce^2
379    //
380    let sinh_arg_i = tan_ce * two_inv_denom_tan_ce;
381    let cosh_arg_i = two_inv_denom_tan_ce_square - 1.;
382    let (d_cn, d_ce) = clen_s(&poder_engsager.gtu, sin_arg_r, cos_arg_r, sinh_arg_i, cosh_arg_i);
383    cn += d_cn;
384    ce += d_ce;
385    if fabs(ce) <= 2.623395162778 {
386        // Northing
387        p.set_y(poder_engsager.qn * cn + poder_engsager.zb);
388        // Easting
389        p.set_x(poder_engsager.qn * ce);
390    } else {
391        panic!("Coordinate outside projection domain");
392    }
393}
394
395/// Transverse Mercator Ellipsoidal inverse project
396pub fn tmerc_exact_e_inv<P: TransformCoordinates>(tmerc: &mut TmercData, p: &mut P) {
397    let poder_engsager = &tmerc.exact;
398
399    // normalize N, E
400    let mut cn = (p.y() - poder_engsager.zb) / poder_engsager.qn;
401    let mut ce = p.x() / poder_engsager.qn;
402
403    if fabs(ce) <= 2.623395162778 {
404        /* 150 degrees */
405        /* norm. N, E -> compl. sph. LAT, LNG */
406        let sin_arg_r = sin(2. * cn);
407        let cos_arg_r = cos(2. * cn);
408
409        // let sinh_arg_i = sinh(2*Ce);
410        // let cosh_arg_i = cosh(2*Ce);
411        let exp_2_ce = exp(2. * ce);
412        let half_inv_exp_2_ce = 0.5 / exp_2_ce;
413        let sinh_arg_i = 0.5 * exp_2_ce - half_inv_exp_2_ce;
414        let cosh_arg_i = 0.5 * exp_2_ce + half_inv_exp_2_ce;
415        let (d_cn, d_ce) =
416            clen_s(&poder_engsager.utg, sin_arg_r, cos_arg_r, sinh_arg_i, cosh_arg_i);
417        cn += d_cn;
418        ce += d_ce;
419
420        /* compl. sph. LAT -> Gaussian LAT, LNG */
421        let sin_cn = sin(cn);
422        let cos_cn = cos(cn);
423
424        // #if 0
425        //         // Non-optimized version:
426        //         double sin_ce, cos_ce;
427        //         Ce = atan (sinh (Ce));  // Replaces: Ce = 2*(atan(exp(Ce)) - FORTPI);
428        //         sin_ce = sin (Ce);
429        //         cos_ce = cos (Ce);
430        //         Ce     = atan2 (sin_ce, cos_ce*cos_cn);
431        //         Cn     = atan2 (sin_cn*cos_ce,  hypot (sin_ce, cos_ce*cos_cn));
432        // #else
433        /*
434         *      One can divide both member of Ce = atan2(...) by cos_ce, which
435         * gives: Ce     = atan2 (tan_ce, cos_cn) = atan2(sinh(Ce), cos_cn)
436         *
437         *      and the same for Cn = atan2(...)
438         *      Cn     = atan2 (sin_cn, hypot (sin_ce, cos_ce*cos_cn)/cos_ce)
439         *             = atan2 (sin_cn, hypot (sin_ce/cos_ce, cos_cn))
440         *             = atan2 (sin_cn, hypot (tan_ce, cos_cn))
441         *             = atan2 (sin_cn, hypot (sinh_ce, cos_cn))
442         */
443        let sinh_ce = sinh(ce);
444        ce = atan2(sinh_ce, cos_cn);
445        let modulus_ce = hypot(sinh_ce, cos_cn);
446        let rr = hypot(sin_cn, modulus_ce);
447        cn = atan2(sin_cn, modulus_ce);
448        // #endif
449
450        // Gaussian LAT, LNG -> ell. LAT, LNG
451        p.set_phi(auxlat_convert_mid(
452            cn,
453            sin_cn / rr,
454            modulus_ce / rr,
455            &poder_engsager.cgb,
456            PROJ_ETMERC_ORDER,
457        ));
458        p.set_lam(ce);
459    } else {
460        panic!("Coordinate outside projection domain");
461    }
462}
463
464fn setup_exact(tmerc: &mut TmercData, proj: &Proj) {
465    let poder_engsager = &mut tmerc.exact;
466    assert!(proj.es > 0., "Eccentricity must be greater than zero");
467    assert!(PROJ_ETMERC_ORDER == AuxLat::ORDER as i32, "Inconsistent orders etmerc vs auxorder");
468    // third flattening
469    let n = proj.n;
470
471    // N.B., Engsager and Poder terminology (simplifying a little here...)
472    //   geodetic coordinates = geographic latitude
473    //   Soldner sphere + complex gaussian coordinates = conformal latitude
474    //   transverse Mercator coordinates = rectifying latitude
475
476    // COEF. OF TRIG SERIES GEO <-> GAUSS
477    // cgb := Gaussian -> Geodetic, KW p190 - 191 (61) - (62)
478    // cbg := Geodetic -> Gaussian, KW p186 - 187 (51) - (52)
479    // PROJ_ETMERC_ORDER = 6th degree : Engsager and Poder: ICC2007
480    auxlat_coeffs(n, AuxLat::CONFORMAL, AuxLat::GEOGRAPHIC, &mut poder_engsager.cgb);
481    auxlat_coeffs(n, AuxLat::GEOGRAPHIC, AuxLat::CONFORMAL, &mut poder_engsager.cbg);
482    // Constants of the projections
483    // Transverse Mercator (UTM, ITM, etc)
484    // Norm. mer. quad, K&W p.50 (96), p.19 (38b), p.5 (2)
485    poder_engsager.qn = proj.k0 * rectifying_radius(n);
486    // coef of trig series
487    // utg := ell. N, E -> sph. N, E,  KW p194 (65)
488    // gtu := sph. N, E -> ell. N, E,  KW p196 (69)
489    auxlat_coeffs(n, AuxLat::RECTIFYING, AuxLat::CONFORMAL, &mut poder_engsager.utg);
490    auxlat_coeffs(n, AuxLat::CONFORMAL, AuxLat::RECTIFYING, &mut poder_engsager.gtu);
491    // Gaussian latitude value of the origin latitude
492    let z = auxlat_convert(proj.phi0, &poder_engsager.cbg, PROJ_ETMERC_ORDER);
493
494    // Origin northing minus true northing at the origin latitude
495    // i.e. true northing = N - proj.zb
496    poder_engsager.zb =
497        -poder_engsager.qn * auxlat_convert(z, &poder_engsager.gtu, PROJ_ETMERC_ORDER);
498}
499
500/// Transverse Mercator Auto forward project
501pub fn tmerc_auto_e_fwd<P: TransformCoordinates>(tmerc: &mut TmercData, proj: &Proj, p: &mut P) {
502    if fabs(p.lam()) > 3.0_f64.to_radians() {
503        tmerc_exact_e_fwd(tmerc, p);
504    } else {
505        tmerc_approx_e_fwd(tmerc, proj, p);
506    }
507}
508
509/// Transverse Mercator Auto inverse project
510pub fn tmerc_auto_e_inv<P: TransformCoordinates>(tmerc: &mut TmercData, proj: &Proj, p: &mut P) {
511    // static PJ_LP tmerc_auto_e_inv(PJ_XY xy, PJ *P) {
512    // For k = 1 and long = 3 (from central meridian),
513    // At lat = 0, we get x ~= 0.052, y = 0
514    // At lat = 90, we get x = 0, y ~= 1.57 }
515    // And the shape of this x=f(y) frontier curve is very very roughly a
516    // parabola. Hence:
517    if fabs(p.x()) > 0.053 - 0.022 * p.y() * p.y() {
518        tmerc_exact_e_inv(tmerc, p);
519    } else {
520        tmerc_approx_e_inv(tmerc, proj, p);
521    }
522}
523
524fn get_algo_from_params(proj: &Proj, algo: &mut TMercAlgo) -> bool {
525    let approx = proj.params.get(&APPROX).unwrap_or(&ProjValue::default()).bool();
526    if approx {
527        *algo = TMercAlgo::EvendenSnyder;
528        return true;
529    }
530
531    let algo_str = proj.params.get(&ALGO).unwrap_or(&ProjValue::default()).string();
532    if !algo_str.is_empty() {
533        if algo_str == "evenden_snyder" {
534            *algo = TMercAlgo::EvendenSnyder;
535            return true;
536        }
537        if algo_str == "poder_engsager" {
538            *algo = TMercAlgo::PoderEngsager;
539            return true;
540        }
541        if algo_str == "auto" {
542            *algo = TMercAlgo::Auto;
543            // Don't return so that we can run a later validity check
544        } else {
545            panic!("unknown value for +algo");
546        }
547    }
548
549    // We haven't worked on the criterion on inverse transformation
550    // when phi0 != 0 or if k0 is not close to 1 or for very oblate
551    // ellipsoid (es > 0.1 is ~ rf < 200)
552    if *algo == TMercAlgo::Auto && (proj.es > 0.1 || proj.phi0 != 0. || fabs(proj.k0 - 1.) > 0.01) {
553        *algo = TMercAlgo::PoderEngsager;
554    }
555
556    true
557}
558
559fn setup(proj: &mut Proj, e_alg: &mut TMercAlgo) -> (TmercData, TMercMode) {
560    let mut tmerc = TmercData::default();
561
562    if proj.es == 0. {
563        *e_alg = TMercAlgo::EvendenSnyder;
564    }
565    let mode = match *e_alg {
566        TMercAlgo::EvendenSnyder => {
567            setup_approx(&mut tmerc, proj);
568            if proj.es == 0. { TMercMode::Spherical } else { TMercMode::ApproxEllipsoidal }
569        }
570        TMercAlgo::PoderEngsager => {
571            setup_exact(&mut tmerc, proj);
572            TMercMode::ExactEllipsoidal
573        }
574        TMercAlgo::Auto => {
575            setup_approx(&mut tmerc, proj);
576            setup_exact(&mut tmerc, proj);
577            TMercMode::AutoEllipsoidal
578        }
579    };
580
581    (tmerc, mode)
582}
583
584/// Transverse Mercator
585///
586/// See [`TransverseMercatorBaseProjection`] for full documentation.
587pub type TransverseMercatorProjection = TransverseMercatorBaseProjection<TRANSVERSE_MERCATOR>;
588/// Transverse Mercator (South Oriented)
589///
590/// See [`TransverseMercatorBaseProjection`] for full documentation.
591pub type TransverseMercatorSouthOrientedProjection =
592    TransverseMercatorBaseProjection<TRANSVERSE_MERCATOR_SOUTH_ORIENTATED>;
593
594/// # Transverse Mercator
595///
596/// **Classification**: Transverse and oblique cylindrical
597///
598/// **Available forms**: Forward and inverse, spherical and ellipsoidal
599///
600/// **Defined area**: Global, with full accuracy within 3900 km of the central meridian
601///
602/// **Alias**: tmerc
603///
604/// **Domain**: 2D
605///
606/// **Input type**: Geodetic coordinates
607///
608/// **Output type**: Projected coordinates
609///
610/// ## Projection String
611/// ```ini
612/// +proj=tmerc
613/// ```
614///
615/// ## Required Parameters
616/// - `+lon_0`: Longitude of the central meridian.
617///
618/// ## Optional Parameters
619/// - `+approx`: Use the faster Evenden-Snyder algorithm, less accurate beyond 3°.
620/// - `+algo`: Select algorithm from "auto", "evenden_snyder", or "poder_engsager".
621/// - `+lat_0`: Latitude of origin.
622/// - `+k_0`: Scale factor on the central meridian.
623/// - `+x_0`: False easting.
624/// - `+y_0`: False northing.
625///
626/// ## Notes
627/// - exact transverse mercator only exists in ellipsoidal form,
628///   use approximate version if +a sphere is requested
629///
630/// ![Transverse Mercator](https://github.com/Open-S2/gis-tools/blob/master/assets/proj4/projections/images/tmerc.png?raw=true)
631#[derive(Debug, Clone, PartialEq)]
632pub struct TransverseMercatorBaseProjection<const C: i64> {
633    proj: Rc<RefCell<Proj>>,
634    mode: TMercMode,
635    store: RefCell<TmercData>,
636    algo: RefCell<TMercAlgo>,
637}
638impl<const C: i64> ProjectCoordinates for TransverseMercatorBaseProjection<C> {
639    fn code(&self) -> i64 {
640        C
641    }
642    fn name(&self) -> &'static str {
643        "Transverse Mercator"
644    }
645    fn names() -> &'static [&'static str] {
646        &[
647            "Transverse Mercator",
648            "Transverse_Mercator",
649            "Transverse Mercator (South Oriented)",
650            "tmerc",
651        ]
652    }
653}
654impl<const C: i64> CoordinateStep for TransverseMercatorBaseProjection<C> {
655    fn new(proj: Rc<RefCell<Proj>>) -> Self {
656        let mut algo = TMercAlgo::default();
657        get_algo_from_params(&proj.borrow(), &mut algo);
658        let (store, mode) = setup(&mut proj.borrow_mut(), &mut algo);
659        TransverseMercatorBaseProjection { proj, mode, store: store.into(), algo: algo.into() }
660    }
661    fn forward<P: TransformCoordinates>(&self, p: &mut P) {
662        match self.mode {
663            TMercMode::Spherical => {
664                tmerc_spherical_fwd(&mut self.store.borrow_mut(), &self.proj.borrow(), p)
665            }
666            TMercMode::ApproxEllipsoidal => {
667                tmerc_approx_e_fwd(&mut self.store.borrow_mut(), &self.proj.borrow(), p)
668            }
669            TMercMode::ExactEllipsoidal => tmerc_exact_e_fwd(&mut self.store.borrow_mut(), p),
670            TMercMode::AutoEllipsoidal => {
671                tmerc_auto_e_fwd(&mut self.store.borrow_mut(), &self.proj.borrow(), p)
672            }
673        }
674    }
675    fn inverse<P: TransformCoordinates>(&self, p: &mut P) {
676        match self.mode {
677            TMercMode::Spherical => {
678                tmerc_spherical_inv(&mut self.store.borrow_mut(), &self.proj.borrow(), p)
679            }
680            TMercMode::ApproxEllipsoidal => {
681                tmerc_approx_e_inv(&mut self.store.borrow_mut(), &self.proj.borrow(), p)
682            }
683            TMercMode::ExactEllipsoidal => tmerc_exact_e_inv(&mut self.store.borrow_mut(), p),
684            TMercMode::AutoEllipsoidal => {
685                tmerc_auto_e_inv(&mut self.store.borrow_mut(), &self.proj.borrow(), p)
686            }
687        }
688    }
689}
690
691/// # Extended Transverse Mercator
692///
693/// **Classification**: Transverse and oblique cylindrical
694///
695/// **Available forms**: Forward and inverse, spherical and ellipsoidal
696///
697/// **Defined area**: Global, with full accuracy within 3900 km of the central meridian
698///
699/// **Alias**: etmerc
700///
701/// **Domain**: 2D
702///
703/// **Input type**: Geodetic coordinates
704///
705/// **Output type**: Projected coordinates
706///
707/// ## Projection String
708/// ```ini
709/// +proj=etmerc
710/// ```
711///
712/// ## Required Parameters
713/// - `+lon_0`: Longitude of the central meridian.
714///
715/// ## Optional Parameters
716/// - `+approx`: Use the faster Evenden-Snyder algorithm, less accurate beyond 3°.
717/// - `+algo`: Select algorithm from "auto", "evenden_snyder", or "poder_engsager".
718/// - `+lat_0`: Latitude of origin.
719/// - `+k_0`: Scale factor on the central meridian.
720/// - `+x_0`: False easting.
721/// - `+y_0`: False northing.
722///
723/// ![ExtendedTransverseMercator](https://github.com/Open-S2/gis-tools/blob/master/assets/proj4/projections/images/tmerc.png?raw=true)
724#[derive(Debug, Clone, PartialEq)]
725pub struct ExtendedTransverseMercatorProjection {
726    proj: Rc<RefCell<Proj>>,
727    mode: TMercMode,
728    store: RefCell<TmercData>,
729    algo: RefCell<TMercAlgo>,
730}
731impl ProjectCoordinates for ExtendedTransverseMercatorProjection {
732    fn code(&self) -> i64 {
733        -1
734    }
735    fn name(&self) -> &'static str {
736        "Extended Transverse Mercator"
737    }
738    fn names() -> &'static [&'static str] {
739        &["Extended Transverse Mercator", "Extended_Transverse_Mercator", "etmerc"]
740    }
741}
742impl CoordinateStep for ExtendedTransverseMercatorProjection {
743    fn new(proj: Rc<RefCell<Proj>>) -> Self {
744        if proj.borrow().es == 0.0 {
745            panic!("Invalid value for eccentricity: it should not be zero");
746        }
747        let mut algo = TMercAlgo::PoderEngsager;
748        get_algo_from_params(&proj.borrow(), &mut algo);
749        let (store, mode) = setup(&mut proj.borrow_mut(), &mut algo);
750        ExtendedTransverseMercatorProjection { proj, mode, store: store.into(), algo: algo.into() }
751    }
752    fn forward<P: TransformCoordinates>(&self, p: &mut P) {
753        match self.mode {
754            TMercMode::Spherical => {
755                tmerc_spherical_fwd(&mut self.store.borrow_mut(), &self.proj.borrow(), p)
756            }
757            TMercMode::ApproxEllipsoidal => {
758                tmerc_approx_e_fwd(&mut self.store.borrow_mut(), &self.proj.borrow(), p)
759            }
760            TMercMode::ExactEllipsoidal => tmerc_exact_e_fwd(&mut self.store.borrow_mut(), p),
761            TMercMode::AutoEllipsoidal => {
762                tmerc_auto_e_fwd(&mut self.store.borrow_mut(), &self.proj.borrow(), p)
763            }
764        }
765    }
766    fn inverse<P: TransformCoordinates>(&self, p: &mut P) {
767        match self.mode {
768            TMercMode::Spherical => {
769                tmerc_spherical_inv(&mut self.store.borrow_mut(), &self.proj.borrow(), p)
770            }
771            TMercMode::ApproxEllipsoidal => {
772                tmerc_approx_e_inv(&mut self.store.borrow_mut(), &self.proj.borrow(), p)
773            }
774            TMercMode::ExactEllipsoidal => tmerc_exact_e_inv(&mut self.store.borrow_mut(), p),
775            TMercMode::AutoEllipsoidal => {
776                tmerc_auto_e_inv(&mut self.store.borrow_mut(), &self.proj.borrow(), p)
777            }
778        }
779    }
780}
781
782/// # Universal Transverse Mercator (UTM)
783///
784/// **Classification**: Transverse cylindrical, conformal
785///
786/// **Available forms**: Forward and inverse, ellipsoidal only
787///
788/// **Defined area**: Within the used zone, but transformations of coordinates in adjacent zones can be accurate
789///
790/// **Alias**: utm
791///
792/// **Domain**: 2D
793///
794/// **Input type**: Geodetic coordinates
795///
796/// **Output type**: Projected coordinates
797///
798/// ## Projection String
799/// ```ini
800/// +proj=utm
801/// ```
802///
803/// ## Required Parameters
804/// - `+zone=<value>`: Select which UTM zone to use. Can be a value between 1-60.
805///
806/// ## Optional Parameters
807/// - `+south`: Add this flag when using the UTM on the southern hemisphere.
808/// - `+approx`: Use a faster, less accurate algorithm for the Transverse Mercator. (added in PROJ 6.0.0)
809/// - `+algo=auto/evenden_snyder/poder_engsager`: Selects the algorithm to use. Defaults to `poder_engsager`. (added in PROJ 7.1)
810/// - `+ellps=<value>`
811///
812/// ## Usage Examples
813///
814/// Convert geodetic coordinates to UTM Zone 32 on the northern hemisphere:
815/// ```bash
816/// $ echo 12 56 | proj +proj=utm +zone=32
817/// 687071.44       6210141.33
818/// ```
819///
820/// Convert geodetic coordinates to UTM Zone 59 on the southern hemisphere:
821/// ```bash
822/// $ echo 174 -44 | proj +proj=utm +zone=59 +south
823/// 740526.32       5123750.87
824/// ```
825///
826/// Show the relationship of UTM to TM:
827/// ```bash
828/// $ echo 121 24 | proj +proj=utm +lon_0=123 | proj -I +proj=tmerc +lon_0=123 +x_0=500000 +k=0.9996
829/// 121dE 24dN
830/// ```
831///
832/// ## Notes
833/// - UTM uses the Poder/Engsager implementation for the underlying projection
834/// - UNLESS +approx is set in which case the Evenden/Snyder implementation is used
835///
836/// ## Further Reading
837/// - [Wikipedia](https://en.wikipedia.org/wiki/Universal_Transverse_Mercator_coordinate_system)
838///
839/// ![Universal Transverse Mercator (UTM) zones](https://github.com/Open-S2/gis-tools/blob/master/assets/proj4/projections/images/utm.png?raw=true)
840#[derive(Debug, Clone, PartialEq)]
841pub struct UniversalTransverseMercatorProjection {
842    proj: Rc<RefCell<Proj>>,
843    mode: TMercMode,
844    store: RefCell<TmercData>,
845    algo: RefCell<TMercAlgo>,
846}
847impl ProjectCoordinates for UniversalTransverseMercatorProjection {
848    fn code(&self) -> i64 {
849        -1
850    }
851    fn name(&self) -> &'static str {
852        "Universal Transverse Mercator"
853    }
854    fn names() -> &'static [&'static str] {
855        &["Universal Transverse Mercator", "Universal Transverse Mercator (UTM)", "utm"]
856    }
857}
858impl CoordinateStep for UniversalTransverseMercatorProjection {
859    fn new(proj: Rc<RefCell<Proj>>) -> Self {
860        {
861            let proj = &mut proj.borrow_mut();
862            if proj.es == 0.0 {
863                panic!("Invalid value for eccentricity: it should not be zero");
864            }
865
866            if proj.lam0 < -1000.0 || proj.lam0 > 1000.0 {
867                panic!("Invalid value for lon_0");
868            }
869
870            if proj.params.contains_key(&SOUTH) {
871                proj.y0 = 10000000.
872            } else {
873                proj.y0 = 0.
874            }
875            proj.x0 = 500000.;
876            // zone input ?
877            let zone = if let Some(zone) = proj.params.get(&ZONE) {
878                let mut zone = zone.i64();
879                if zone > 0 && zone <= 60 {
880                    zone -= 1;
881                } else {
882                    panic!("Invalid value for zone");
883                }
884                zone
885            } else {
886                // nearest central meridian input
887                let mut zone = round(floor((adjlon(proj.lam0) + PI) * 30. / PI)) as i64;
888                if zone < 0 {
889                    zone = 0;
890                } else if zone >= 60 {
891                    zone = 59;
892                }
893                zone
894            };
895            proj.lam0 = ((zone as f64) + 0.5) * PI / 30. - PI;
896            proj.k0 = 0.9996;
897            proj.phi0 = 0.;
898        }
899        let mut algo = TMercAlgo::PoderEngsager;
900        get_algo_from_params(&proj.borrow(), &mut algo);
901        let (store, mode) = setup(&mut proj.borrow_mut(), &mut algo);
902        UniversalTransverseMercatorProjection { proj, mode, store: store.into(), algo: algo.into() }
903    }
904    fn forward<P: TransformCoordinates>(&self, p: &mut P) {
905        match self.mode {
906            TMercMode::Spherical => {
907                tmerc_spherical_fwd(&mut self.store.borrow_mut(), &self.proj.borrow(), p)
908            }
909            TMercMode::ApproxEllipsoidal => {
910                tmerc_approx_e_fwd(&mut self.store.borrow_mut(), &self.proj.borrow(), p)
911            }
912            TMercMode::ExactEllipsoidal => tmerc_exact_e_fwd(&mut self.store.borrow_mut(), p),
913            TMercMode::AutoEllipsoidal => {
914                tmerc_auto_e_fwd(&mut self.store.borrow_mut(), &self.proj.borrow(), p)
915            }
916        }
917    }
918    fn inverse<P: TransformCoordinates>(&self, p: &mut P) {
919        match self.mode {
920            TMercMode::Spherical => {
921                tmerc_spherical_inv(&mut self.store.borrow_mut(), &self.proj.borrow(), p)
922            }
923            TMercMode::ApproxEllipsoidal => {
924                tmerc_approx_e_inv(&mut self.store.borrow_mut(), &self.proj.borrow(), p)
925            }
926            TMercMode::ExactEllipsoidal => tmerc_exact_e_inv(&mut self.store.borrow_mut(), p),
927            TMercMode::AutoEllipsoidal => {
928                tmerc_auto_e_inv(&mut self.store.borrow_mut(), &self.proj.borrow(), p)
929            }
930        }
931    }
932}