gistools/proj/transform/
transformer.rs

1use super::TransformCoordinates;
2use crate::proj::{
3    Direction, IoUnits, ProjJSON, ProjectionTransform, Step, adjlon, check_not_wgs84,
4    datum_transform, geocentric_latitude,
5};
6use alloc::{collections::BTreeMap, fmt::Debug, string::String};
7use core::f64::consts::FRAC_PI_2;
8
9/// # PROJ Transformer
10///
11/// ## Description
12///
13/// A Transformer class contains all projections necessary for converting coordinates from one
14/// projection to another. This is a modular class that can be extended to add new projections
15/// as needed to reduce code size and improve performance.
16/// Both forward and inverse projections are default set to wgs84.
17///
18/// See the NadGridStore
19///
20/// ## Usage
21///
22/// Utilizes the [`TransformCoordinates`] trait
23///
24/// ### Useful methods
25/// - [`Transformer::forward`]: Given a point in the source projection, return the point in the destination projection
26/// - [`Transformer::forward_mut`]: Given a point in the source projection, return the point in the destination projection
27/// - [`Transformer::inverse`]: Given a point in the destination projection, return the point in the source projection
28/// - [`Transformer::inverse_mut`]: Given a mutable point in the destination projection, return the point in the source projection
29/// - [`Transformer::set_source`]: Set the source projection
30/// - [`Transformer::set_destination`]: Set the destination projection
31/// - [`Transformer::insert_epsg_code`]: Insert an EPSG code definition
32///
33/// ### Example
34///
35/// ```rust
36/// use gistools::proj::{Coords, Transformer};
37///
38/// let mut transformer = Transformer::new();
39/// // Insert a source or destination as needed
40/// // transformer.set_source("WKT_STRING...".into());
41///
42/// let mut point = Coords::new_xy(0., 0.);
43/// transformer.forward_mut(&mut point);
44/// assert_eq!(point, Coords::new_xy(0., 0.));
45/// ```
46#[derive(Debug, Default, Clone, PartialEq)]
47pub struct Transformer {
48    epsgs: BTreeMap<String, String>,
49    src: ProjectionTransform,
50    dest: ProjectionTransform,
51}
52impl Transformer {
53    /// Create a new Transformer
54    #[allow(clippy::new_without_default)]
55    pub fn new() -> Self {
56        Self {
57            epsgs: BTreeMap::new(),
58            src: ProjectionTransform::wgs84(),
59            dest: ProjectionTransform::wgs84(),
60        }
61    }
62    /// Move forward from source projection to destination projection
63    pub fn forward<P: TransformCoordinates + Debug>(&self, p: &P) -> P {
64        let mut res = p.clone();
65        transform_point(&self.src, &self.dest, &mut res);
66        res
67    }
68    /// Move forward from source projection to destination projection in place
69    pub fn forward_mut<P: TransformCoordinates + Debug>(&self, p: &mut P) {
70        transform_point(&self.src, &self.dest, p);
71    }
72    /// Move backward from destination projection to source projection
73    pub fn inverse<P: TransformCoordinates + Debug>(&self, p: &P) -> P {
74        let mut res = p.clone();
75        transform_point(&self.dest, &self.src, &mut res);
76        res
77    }
78    /// Move backward from destination projection to source projection in place
79    pub fn inverse_mut<P: TransformCoordinates + Debug>(&self, p: &mut P) {
80        transform_point(&self.dest, &self.src, p);
81    }
82
83    /// Insert an EPSG code definition
84    /// ```rust
85    /// use gistools::proj::Transformer;
86    ///
87    /// let mut transformer = Transformer::new();
88    /// transformer.insert_epsg_code("4326".to_string(), "WKT_STRING".to_string());
89    /// ```
90    ///
91    /// ## Parameters
92    /// - `code`: EPSG code to insert e.g. "4326": "WKT_STRING"
93    /// - `value`: the EPSG definition which is either a WKT string object or proj4 encoded string
94    pub fn insert_epsg_code(&mut self, code: String, value: String) {
95        self.epsgs.insert(code, value);
96    }
97
98    /// Set the source projection
99    ///
100    /// ## Parameters
101    /// - `code`: can be a name or a json/wkt coded definition
102    pub fn set_source(&mut self, code: String) {
103        self.src = self.build_transformer(code);
104    }
105
106    /// Set the source projection
107    ///
108    /// ## Parameters
109    /// - `def`: transform definition
110    pub fn set_source_def(&mut self, def: ProjectionTransform) {
111        self.src = def;
112    }
113
114    /// Set the destination projection
115    ///
116    /// ## Parameters
117    /// - `code`: can be a name or a coded definition
118    pub fn set_destination(&mut self, code: String) {
119        self.dest = self.build_transformer(code);
120    }
121
122    /// Set the source projection
123    ///
124    /// ## Parameters
125    /// - `def`: transform definition
126    pub fn set_destination_def(&mut self, def: ProjectionTransform) {
127        self.dest = def;
128    }
129
130    /// Build a ProjectionTransform
131    ///
132    /// ## Parameters
133    /// - `code`: can be a WKT object or proj4 encoded string
134    ///
135    /// ## Returns
136    /// A ready to use ProjectionTransform
137    fn build_transformer(&mut self, mut code: String) -> ProjectionTransform {
138        if let Some(epsg) = self.epsgs.get(&code) {
139            code = epsg.clone();
140        }
141        // TRY JSON
142        if let Ok(json) = serde_json::from_str::<ProjJSON>(&code) {
143            return json.to_projection_transform();
144        }
145        // TRY WKT
146        ProjJSON::parse_wkt(&code).to_projection_transform()
147    }
148
149    /// Get access to an epsg code
150    pub fn get_epsg_code(&self, code: String) -> Option<String> {
151        self.epsgs.get(&code).cloned()
152    }
153}
154
155/// Transforms a point from one projection to another
156///
157/// ## Parameters
158/// - `src`: source projection
159/// - `dest`: destination projection
160/// - `point`: point to mutate
161pub fn transform_point<P: TransformCoordinates + Debug>(
162    src: &ProjectionTransform,
163    dest: &ProjectionTransform,
164    point: &mut P,
165) {
166    // Check if there are any steps
167    if src == dest || (Step::same_step(&src.method, &dest.method)) {
168        return;
169    }
170    let has_z = point.has_z();
171
172    // TODO: We ideally drop this when cart and cart_wgs84 are merged
173    // STEP 0: If the datums are incompatible, be sure to use the intermediate wgs84 transform
174    if check_not_wgs84(src, dest) {
175        transform_point(src, &ProjectionTransform::wgs84(), point);
176    }
177
178    // STEP 1 INVERSE
179    transform_inv(src, point);
180    // STEP 2: MID-POINT. Convert datums if needed, and if possible.
181    datum_transform(point, &src.proj.borrow(), &dest.proj.borrow());
182    // STEP 3: FORWARD
183    transform_fwd(dest, point);
184
185    if !has_z {
186        point.set_z(0.0);
187    }
188}
189
190fn transform_inv<P: TransformCoordinates + Debug>(
191    proj_trans: &ProjectionTransform,
192    coords: &mut P,
193) {
194    transform_inv_prepare(proj_trans, coords);
195    proj_trans.method.inverse(coords);
196    transform_inv_finalize(proj_trans, coords);
197}
198
199fn transform_inv_prepare<P: TransformCoordinates + Debug>(
200    proj_trans: &ProjectionTransform,
201    coords: &mut P,
202) {
203    let proj = &proj_trans.proj.borrow();
204    // The helmert datum shift will choke unless it gets a sensible 4D coordinate
205    if f64::INFINITY == coords.z() && proj_trans.helmert.is_some() {
206        coords.set_z(0.0);
207    }
208    if f64::INFINITY == coords.t() && proj_trans.helmert.is_some() {
209        coords.set_t(0.0);
210    }
211
212    if let Some(axisswap) = &proj_trans.axisswap {
213        axisswap.method.inverse(coords);
214    }
215
216    // Handle remaining possible input types
217    match proj.right {
218        // de-scale and de-offset
219        IoUnits::CARTESIAN => {
220            let to_meter = proj.to_meter;
221            coords.set_x(coords.x() * to_meter);
222            coords.set_y(coords.y() * to_meter);
223            coords.set_z(coords.z() * to_meter);
224            if proj.is_geocent
225                && let Some(cart) = &proj_trans.cart
226            {
227                transform_inv(cart, coords);
228            }
229        }
230
231        IoUnits::PROJECTED | IoUnits::CLASSIC => {
232            let to_meter = proj.to_meter;
233            let vto_meter = proj.vto_meter;
234            coords.set_x(coords.x() * to_meter - proj.x0);
235            // 440720.0 - 400000
236            coords.set_y(coords.y() * to_meter - proj.y0);
237            coords.set_z(coords.z() * vto_meter - proj.z0);
238            if proj.right == IoUnits::PROJECTED {
239                return;
240            }
241
242            // Classic proj.4 functions expect plane coordinates in units of the semimajor axis
243            // Multiplying by ra, rather than dividing by a because the CalCOFI projection
244            // stomps on a and hence (apparently) depends on this to roundtrip correctly
245            // (CalCOFI avoids further scaling by stomping - but a better solution is possible)
246            if proj.ra != 0. {
247                coords.set_x(coords.x() * proj.ra);
248                coords.set_y(coords.y() * proj.ra);
249            }
250        }
251
252        IoUnits::RADIANS => {
253            coords.set_z(coords.z() * proj.vto_meter - proj.z0);
254        }
255        _ => {}
256    }
257}
258
259fn transform_inv_finalize<P: TransformCoordinates + Debug>(
260    proj_trans: &ProjectionTransform,
261    coords: &mut P,
262) {
263    let proj = &proj_trans.proj.borrow();
264    if proj.left == IoUnits::RADIANS {
265        // Distance from central meridian, taking system zero meridian into account
266        coords.set_lam(coords.lam() + proj.from_greenwich + proj.lam0);
267
268        // adjust longitude to central meridian
269        if !proj.over {
270            coords.set_lam(adjlon(coords.lam()));
271        }
272
273        if let Some(vgridshift) = &proj_trans.vgridshift {
274            // Go geometric from orthometric
275            transform_inv(vgridshift, coords);
276        }
277        if let Some(hgridshift) = &proj_trans.hgridshift {
278            // Go geometric from orthometric
279            transform_fwd(hgridshift, coords);
280        } else if (proj_trans.cart_wgs84.is_some() && proj_trans.cart.is_some())
281            || proj_trans.helmert.is_some()
282        {
283            // Go cartesian in local frame
284            if let Some(cart) = &proj_trans.cart {
285                transform_fwd(cart, coords);
286            }
287            // Step into WGS84
288            if let Some(helmert) = &proj_trans.helmert {
289                transform_fwd(helmert, coords);
290            }
291            // Go back to angular using WGS84 ellps
292            if let Some(cart_wgs84) = &proj_trans.cart_wgs84 {
293                transform_inv(cart_wgs84, coords);
294            }
295        }
296
297        // If input latitude was geocentrical, convert back to geocentrical
298        if proj.geoc {
299            geocentric_latitude(proj, Direction::FWD, coords);
300        }
301    }
302}
303
304fn transform_fwd<P: TransformCoordinates + Debug>(
305    proj_trans: &ProjectionTransform,
306    coords: &mut P,
307) {
308    transform_fwd_prepare(proj_trans, coords);
309    proj_trans.method.forward(coords);
310    transform_fwd_finalize(proj_trans, coords);
311}
312
313fn transform_fwd_prepare<P: TransformCoordinates + Debug>(
314    proj_trans: &ProjectionTransform,
315    coords: &mut P,
316) {
317    let proj = &proj_trans.proj.borrow();
318    // The helmert datum shift will choke unless it gets a sensible 4D coordinate
319    if coords.z() == f64::INFINITY && proj_trans.helmert.is_some() {
320        coords.set_z(0.0);
321    }
322    if coords.t() == f64::INFINITY && proj_trans.helmert.is_some() {
323        coords.set_t(0.0);
324    }
325
326    // Check validity of angular input coordinates
327    if proj.left == IoUnits::RADIANS {
328        // check for latitude or longitude over-range
329        let t = (if coords.phi() < 0. { -coords.phi() } else { coords.phi() }) - FRAC_PI_2;
330        if t > 1e-12 || coords.lam() > 10. || coords.lam() < -10. {
331            panic!("Invalid latitude");
332        }
333        // Clamp latitude to -90..90 degree range
334        if coords.phi() > FRAC_PI_2 {
335            coords.set_phi(FRAC_PI_2);
336        }
337        if coords.phi() < -FRAC_PI_2 {
338            coords.set_phi(-FRAC_PI_2);
339        }
340        // If input latitude is geocentrical, convert to geographical */
341        if proj.geoc {
342            geocentric_latitude(proj, Direction::INV, coords);
343        }
344        // Ensure longitude is in the -pi:pi range
345        if !proj.over {
346            coords.set_lam(adjlon(coords.lam()));
347        }
348
349        if let Some(hgridshift) = &proj_trans.hgridshift {
350            transform_inv(hgridshift, coords);
351        } else if (proj_trans.cart_wgs84.is_some() && proj_trans.cart.is_some())
352            || proj_trans.helmert.is_some()
353        {
354            // Go cartesian in local frame
355            if let Some(cart_wgs84) = &proj_trans.cart_wgs84 {
356                transform_fwd(cart_wgs84, coords);
357            }
358            // Step into WGS84
359            if let Some(helmert) = &proj_trans.helmert {
360                transform_inv(helmert, coords);
361            }
362            // Go back to angular using WGS84 ellps
363            if let Some(cart) = &proj_trans.cart {
364                transform_inv(cart, coords);
365            }
366        }
367        // Go orthometric from geometric
368        if let Some(vgridshift) = &proj_trans.vgridshift {
369            transform_fwd(vgridshift, coords);
370        }
371        // Distance from central meridian, taking system zero meridian into account
372        coords.set_lam((coords.lam() - proj.from_greenwich) - proj.lam0);
373
374        // Ensure longitude is in the -pi:pi range
375        if !proj.over {
376            coords.set_lam(adjlon(coords.lam()));
377        }
378
379        return;
380    }
381
382    // We do not support gridshifts on cartesian input
383    if proj.left == IoUnits::CARTESIAN
384        && let Some(helmert) = &proj_trans.helmert
385    {
386        transform_inv(helmert, coords);
387    }
388}
389
390fn transform_fwd_finalize<P: TransformCoordinates + Debug>(
391    proj_trans: &ProjectionTransform,
392    coords: &mut P,
393) {
394    let proj = &proj_trans.proj.borrow();
395    match proj.right {
396        // Handle false eastings/northings and non-metric linear units
397        IoUnits::CARTESIAN => {
398            if proj.is_geocent
399                && let Some(cart) = &proj_trans.cart
400            {
401                transform_fwd(cart, coords);
402            }
403            coords.set_x(coords.x() * proj.fr_meter);
404            coords.set_y(coords.y() * proj.fr_meter);
405            coords.set_z(coords.z() * proj.vfr_meter);
406        }
407
408        // Classic proj.4 functions return plane coordinates in units of the semimajor axis
409        IoUnits::CLASSIC => {
410            if proj.a != 0. {
411                coords.set_x(coords.x() * proj.a);
412                coords.set_y(coords.y() * proj.a);
413            }
414            coords.set_x(proj.fr_meter * (coords.x() + proj.x0));
415            coords.set_y(proj.fr_meter * (coords.y() + proj.y0));
416            coords.set_z(proj.vfr_meter * (coords.z() + proj.z0));
417        }
418
419        // to continue processing in common with IoUnits::PROJECTED
420        IoUnits::PROJECTED => {
421            coords.set_x(proj.fr_meter * (coords.x() + proj.x0));
422            coords.set_y(proj.fr_meter * (coords.y() + proj.y0));
423            coords.set_z(proj.vfr_meter * (coords.z() + proj.z0));
424        }
425
426        IoUnits::RADIANS => {
427            coords.set_z(proj.vfr_meter * (coords.z() + proj.z0));
428            // not interested in adding this support. keeping for posterity
429            // if (proj.is_long_wrap_set) {
430            //     if (coordsz.lam != HUGE_VAL) {
431            //         coordsz.lam =
432            //             proj.long_wrap_center + adjlon(coordsz.lam - proj.long_wrap_center);
433            //     }
434            // }
435        }
436        _ => {}
437    }
438
439    if let Some(axisswap) = &proj_trans.axisswap {
440        axisswap.method.forward(coords);
441    }
442}