gistools/proj/project/
krovak.rs

1use crate::proj::{
2    CZECH, CoordinateStep, KROVAK, KROVAK_MODIFIED, KROVAK_MODIFIED_NORTH_ORIENTED,
3    KROVAK_NORTH_ORIENTED, LATITUDE_OF_PROJECTION_CENTRE, LONGITUDE_OF_ORIGIN,
4    LONGITUDE_OF_PROJECTION_CENTRE, Proj, ProjValue, ProjectCoordinates,
5    SCALE_FACTOR_AT_NATURAL_ORIGIN, TransformCoordinates,
6};
7use alloc::rc::Rc;
8use core::{
9    cell::RefCell,
10    f64::consts::{FRAC_PI_2, FRAC_PI_4},
11};
12use libm::{asin, atan, atan2, cos, fabs, pow, sin, sqrt, tan};
13// ****************************************************************************
14// A description of the (forward) projection is found in:
15//
16//      Bohuslav Veverka,
17//
18//      KROVAK’S PROJECTION AND ITS USE FOR THE
19//      CZECH REPUBLIC AND THE SLOVAK REPUBLIC,
20//
21//      50 years of the Research Institute of
22//      and the Slovak Republic Geodesy, Topography and Cartography
23//
24// which can be found via the Wayback Machine:
25//
26//      https://web.archive.org/web/20150216143806/https://www.vugtk.cz/odis/sborniky/sb2005/Sbornik_50_let_VUGTK/Part_1-Scientific_Contribution/16-Veverka.pdf
27//
28// Further info, including the inverse projection, is given by EPSG:
29//
30//      Guidance Note 7 part 2
31//      Coordinate Conversions and Transformations including Formulas
32//
33//      http://www.iogp.org/pubs/373-07-2.pdf
34//
35// Variable names in this file mostly follows what is used in the
36// paper by Veverka.
37//
38// According to EPSG the full Krovak projection method should have
39// the following parameters.  Within PROJ the azimuth, and pseudo
40// standard parallel are hardcoded in the algorithm and can't be
41// altered from outside. The others all have defaults to match the
42// common usage with Krovak projection.
43//
44//      lat_0 = latitude of centre of the projection
45//
46//      lon_0 = longitude of centre of the projection
47//
48//      ** = azimuth (true) of the centre line passing through the
49//           centre of the projection
50//
51//      ** = latitude of pseudo standard parallel
52//
53//      k  = scale factor on the pseudo standard parallel
54//
55//      x_0 = False Easting of the centre of the projection at the
56//            apex of the cone
57//
58//      y_0 = False Northing of the centre of the projection at
59//            the apex of the cone
60//
61// **************************************************************************
62
63const EPS: f64 = 1e-15;
64const UQ: f64 = 1.04216856380474; // DU(2, 59, 42, 42.69689)
65const S0: f64 = 1.37008346281555; // Latitude of pseudo standard parallel 78deg 30'00" N
66// Not sure at all of the appropriate number for MAX_ITER...
67const MAX_ITER: usize = 100;
68
69/// Krovak variable data
70#[derive(Debug, Default, Clone, PartialEq)]
71pub struct KrovakData {
72    alpha: f64,
73    k: f64,
74    n: f64,
75    rho0: f64,
76    ad: f64,
77    // true, in default mode. false when using +czech
78    easting_northing: bool,
79    modified: bool,
80}
81
82const X0: f64 = 1089000.0;
83const Y0: f64 = 654000.0;
84const C1: f64 = 2.946529277E-02;
85const C2: f64 = 2.515965696E-02;
86const C3: f64 = 1.193845912E-07;
87const C4: f64 = -4.668270147E-07;
88const C5: f64 = 9.233980362E-12;
89const C6: f64 = 1.523735715E-12;
90const C7: f64 = 1.696780024E-18;
91const C8: f64 = 4.408314235E-18;
92const C9: f64 = -8.331083518E-24;
93const C10: f64 = -3.689471323E-24;
94
95/// Correction terms to be applied to regular Krovak to obtain Modified Krovak.
96/// Note that x_r is a Southing in metres and y_r a Westing in metres,
97/// and output (d_x, d_y) is a corrective term in (Southing, Westing) in metres
98///
99/// Reference:
100/// <https://www.cuzk.cz/Zememerictvi/Geodeticke-zaklady-na-uzemi-CR/GNSS/Nova-realizace-systemu-ETRS89-v-CR/Metodika-prevodu-ETRF2000-vs-S-JTSK-var2(101208).aspx>
101fn mod_krovak_compute_dx_dy(xr: f64, yr: f64) -> (f64, f64) {
102    let x_r2 = xr * xr;
103    let y_r2 = yr * yr;
104    let x_r4 = x_r2 * x_r2;
105    let y_r4 = y_r2 * y_r2;
106
107    let d_x = C1 + C3 * xr - C4 * yr - 2. * C6 * xr * yr
108        + C5 * (x_r2 - y_r2)
109        + C7 * xr * (x_r2 - 3. * y_r2)
110        - C8 * yr * (3. * x_r2 - y_r2)
111        + 4. * C9 * xr * yr * (x_r2 - y_r2)
112        + C10 * (x_r4 + y_r4 - 6. * x_r2 * y_r2);
113    let d_y = C2
114        + C3 * yr
115        + C4 * xr
116        + 2. * C5 * xr * yr
117        + C6 * (x_r2 - y_r2)
118        + C8 * xr * (x_r2 - 3. * y_r2)
119        + C7 * yr * (3. * x_r2 - y_r2)
120        - 4. * C10 * xr * yr * (x_r2 - y_r2)
121        + C9 * (x_r4 + y_r4 - 6. * x_r2 * y_r2);
122
123    (d_x, d_y)
124}
125
126// static PJ *krovak_setup(PJ *P, bool modified) {
127fn krovak_setup(proj: &mut Proj, modified: bool) -> KrovakData {
128    let mut data = KrovakData::default();
129
130    // we want Bessel as fixed ellipsoid
131    proj.a = 6377397.155;
132    proj.es = 0.006674372230614;
133    proj.e = sqrt(proj.es);
134
135    let lat_0 =
136        proj.params.get(&LATITUDE_OF_PROJECTION_CENTRE).unwrap_or(&ProjValue::default()).f64();
137    proj.phi0 = if lat_0 == 0. { 0.863937979737193 } else { lat_0 };
138
139    // if center long is not set use 42d30'E of Ferro - 17d40' for Ferro
140    // that will correspond to using longitudes relative to greenwich
141    // as input and output, instead of lat/long relative to Ferro
142    let lon_0 = proj
143        .params
144        .get(&LONGITUDE_OF_ORIGIN)
145        .or_else(|| proj.params.get(&LONGITUDE_OF_PROJECTION_CENTRE))
146        .unwrap_or(&ProjValue::default())
147        .f64();
148    proj.lam0 = if lon_0 == 0. { 0.7417649320975901 - 0.308341501185665 } else { lon_0 };
149
150    // if scale not set default to 0.9999
151    let k = proj.params.get(&SCALE_FACTOR_AT_NATURAL_ORIGIN).unwrap_or(&ProjValue::default()).f64();
152    proj.k0 = if k == 0. { 0.9999 } else { k };
153
154    data.modified = modified;
155
156    data.easting_northing = true;
157    if proj.params.contains_key(&CZECH) {
158        data.easting_northing = false;
159    }
160
161    // Set up shared parameters between forward and inverse
162    data.alpha = sqrt(1. + (proj.es * pow(cos(proj.phi0), 4.)) / (1. - proj.es));
163    let u0 = asin(sin(proj.phi0) / data.alpha);
164    let g = pow(
165        (1. + proj.e * sin(proj.phi0)) / (1. - proj.e * sin(proj.phi0)),
166        data.alpha * proj.e / 2.,
167    );
168    let tan_half_phi0_plus_pi_4 = tan(proj.phi0 / 2. + FRAC_PI_4);
169    if tan_half_phi0_plus_pi_4 == 0.0 {
170        panic!("Invalid value for lat_0: lat_0 + PI/4 should be different from 0");
171    }
172    data.k = tan(u0 / 2. + FRAC_PI_4) / pow(tan_half_phi0_plus_pi_4, data.alpha) * g;
173    let n0 = sqrt(1. - proj.es) / (1. - proj.es * pow(sin(proj.phi0), 2.));
174    data.n = sin(S0);
175    data.rho0 = proj.k0 * n0 / tan(S0);
176    data.ad = FRAC_PI_2 - UQ;
177
178    data
179}
180
181/// Krovak Projection
182///
183/// See [`KrovakBaseProjection`] for full documentation
184pub type KrovakProjection = KrovakBaseProjection<KROVAK, false>;
185/// Krovak North Oriented Projection
186///
187/// See [`KrovakBaseProjection`] for full documentation
188pub type KrovakNorthOrientedProjection = KrovakBaseProjection<KROVAK_NORTH_ORIENTED, false>;
189/// Krovak Modified Projection
190///
191/// See [`KrovakBaseProjection`] for full documentation
192pub type KrovakModifiedProjection = KrovakBaseProjection<KROVAK_MODIFIED, true>;
193/// Krovak Modified North Oriented Projection
194///
195/// See [`KrovakBaseProjection`] for full documentation
196pub type KrovakModifiedNorthOrientedProjection =
197    KrovakBaseProjection<KROVAK_MODIFIED_NORTH_ORIENTED, true>;
198
199/// # Krovak
200///
201/// **Classification**: Conformal Conical
202///
203/// **Available forms**: Forward and inverse, spherical and ellipsoidal
204///
205/// **Defined area**: Global, but more accurate around Czech Republic and Slovakia
206///
207/// **Alias**: krovak
208///
209/// **Domain**: 2D
210///
211/// **Input type**: Geodetic coordinates
212///
213/// **Output type**: Projected coordinates
214///
215/// ## Projection String
216/// ```ini
217/// +proj=krovak
218/// ```
219///
220/// By default, coordinates in the forward direction are output in easting, northing,
221/// and negative in the Czech Republic and Slovakia, with absolute value of
222/// easting/westing being smaller than absolute value of northing/southing.
223///
224/// See also `mod_krovak` for a variation of Krovak used with the S-JTSK/05 datum
225/// in the Czech Republic.
226///
227/// ## Required Parameters
228/// - None, all parameters are optional for this projection.
229///
230/// ## Optional Parameters
231/// - `+czech`: Reverses the sign of the output coordinates for use in the Czech Republic and Slovakia (positive values become westing and southing).
232/// - `+lon_0`: Longitude of projection center. Defaults to `24°50'` (24.8333).
233/// - `+lat_0`: Latitude of projection center. Defaults to `49.5`.
234/// - `+k_0`: Scale factor. Defaults to `0.9999`.
235/// - `+x_0`: False easting. Defaults to `0`.
236/// - `+y_0`: False northing. Defaults to `0`.
237///
238/// ## Notes
239/// - The latitude of the pseudo standard parallel is hardcoded to `78.5°`.
240/// - The ellipsoid used is Bessel by default.
241/// - Before PROJ 9.4, using custom `x_0` or `y_0` without the `+czech` switch resulted in incorrect values.
242///
243/// ![Krovak](https://github.com/Open-S2/gis-tools/blob/master/assets/proj4/projections/images/krovak.png?raw=true)
244#[derive(Debug, Clone, PartialEq)]
245pub struct KrovakBaseProjection<const C: i64, const E: bool> {
246    proj: Rc<RefCell<Proj>>,
247    store: RefCell<KrovakData>,
248}
249impl<const C: i64, const E: bool> ProjectCoordinates for KrovakBaseProjection<C, E> {
250    fn code(&self) -> i64 {
251        C
252    }
253    fn name(&self) -> &'static str {
254        "Krovak"
255    }
256    fn names() -> &'static [&'static str] {
257        &["Krovak", "Modified Krovak"]
258    }
259}
260impl<const C: i64, const E: bool> CoordinateStep for KrovakBaseProjection<C, E> {
261    fn new(proj: Rc<RefCell<Proj>>) -> Self {
262        let store = krovak_setup(&mut proj.borrow_mut(), E);
263        KrovakBaseProjection { proj, store: store.into() }
264    }
265    fn forward<P: TransformCoordinates>(&self, p: &mut P) {
266        krovak_e_forward(&self.store.borrow(), &self.proj.borrow(), p);
267    }
268    fn inverse<P: TransformCoordinates>(&self, p: &mut P) {
269        krovak_e_inverse(&self.store.borrow(), &self.proj.borrow(), p);
270    }
271}
272
273/// Krovak Ellipsoidal forward project
274pub fn krovak_e_forward<P: TransformCoordinates>(krovak: &KrovakData, proj: &Proj, p: &mut P) {
275    let mut x;
276    let mut y;
277
278    let gfi = pow(
279        (1. + proj.e * sin(p.phi())) / (1. - proj.e * sin(p.phi())),
280        krovak.alpha * proj.e / 2.,
281    );
282
283    let u =
284        2. * (atan(krovak.k * pow(tan(p.phi() / 2. + FRAC_PI_4), krovak.alpha) / gfi) - FRAC_PI_4);
285    let deltav = -p.lam() * krovak.alpha;
286
287    let s = asin(cos(krovak.ad) * sin(u) + sin(krovak.ad) * cos(u) * cos(deltav));
288    let cos_s = cos(s);
289    if cos_s < 1e-12 {
290        x = 0.;
291        y = 0.;
292        p.set_x(x);
293        p.set_y(y);
294        return;
295    }
296    let d = asin(cos(u) * sin(deltav) / cos_s);
297
298    let eps = krovak.n * d;
299    let rho = krovak.rho0 * pow(tan(S0 / 2. + FRAC_PI_4), krovak.n)
300        / pow(tan(s / 2. + FRAC_PI_4), krovak.n);
301
302    x = rho * cos(eps);
303    y = rho * sin(eps);
304
305    // At this point, x is a southing and y is a westing
306
307    if krovak.modified {
308        let xp = x;
309        let yp = y;
310
311        // Reduced X and Y
312        let xr = xp * proj.a - X0;
313        let yr = yp * proj.a - Y0;
314
315        let (dx, dy) = mod_krovak_compute_dx_dy(xr, yr);
316
317        x = xp - dx / proj.a;
318        y = yp - dy / proj.a;
319    }
320
321    // PROJ always return values in (easting, northing) (default mode)
322    // or (westing, southing) (+czech mode), so swap X/Y
323    core::mem::swap(&mut x, &mut y);
324
325    if krovak.easting_northing {
326        // The default non-Czech convention uses easting, northing, so we have
327        // to reverse the sign of the coordinates. But to do so, we have to take
328        // into account the false easting/northing.
329        x = -x - 2. * proj.x0 / proj.a;
330        y = -y - 2. * proj.y0 / proj.a;
331    }
332
333    p.set_x(x);
334    p.set_y(y);
335}
336
337/// Krovak Ellipsoidal inverse project
338pub fn krovak_e_inverse<P: TransformCoordinates>(krovak: &KrovakData, proj: &Proj, p: &mut P) {
339    let mut x = p.x();
340    let mut y = p.y();
341
342    if krovak.easting_northing {
343        // The default non-Czech convention uses easting, northing, so we have
344        // to reverse the sign of the coordinates. But to do so, we have to take
345        // into account the false easting/northing.
346        y = -y - 2. * proj.x0 / proj.a;
347        x = -x - 2. * proj.y0 / proj.a;
348    }
349
350    core::mem::swap(&mut x, &mut y);
351
352    if krovak.modified {
353        // Note: in EPSG guidance node 7-2, below x_r/y_r/d_x/d_y are actually
354        // x_r'/y_r'/d_x'/d_y'
355        let x_r = x * proj.a - X0;
356        let y_r = y * proj.a - Y0;
357
358        let (d_x, d_y) = mod_krovak_compute_dx_dy(x_r, y_r);
359
360        x += d_x / proj.a;
361        y += d_y / proj.a;
362    }
363
364    let rho = sqrt(x * x + y * y);
365    let eps = atan2(y, x);
366
367    let d = eps / sin(S0);
368    let s = if rho == 0.0 {
369        FRAC_PI_2
370    } else {
371        2. * (atan(pow(krovak.rho0 / rho, 1. / krovak.n) * tan(S0 / 2. + FRAC_PI_4)) - FRAC_PI_4)
372    };
373
374    let u = asin(cos(krovak.ad) * sin(s) - sin(krovak.ad) * cos(s) * cos(d));
375    let deltav = asin(cos(s) * sin(d) / cos(u));
376
377    p.set_lam(proj.lam0 - deltav / krovak.alpha);
378
379    // ITERATION FOR p.phi
380    let mut fi1 = u;
381
382    let mut i = MAX_ITER;
383    while i > 0 {
384        p.set_phi(
385            2. * (atan(
386                pow(krovak.k, -1. / krovak.alpha)
387                    * pow(tan(u / 2. + FRAC_PI_4), 1. / krovak.alpha)
388                    * pow((1. + proj.e * sin(fi1)) / (1. - proj.e * sin(fi1)), proj.e / 2.),
389            ) - FRAC_PI_4),
390        );
391
392        if fabs(fi1 - p.phi()) < EPS {
393            break;
394        }
395        fi1 = p.phi();
396        i -= 1;
397    }
398    if i == 0 {
399        panic!("Coordinate outside projection domain")
400    }
401
402    p.set_lam(p.lam() - proj.lam0);
403}