gistools/proj/project/
merc.rs

1use crate::proj::{
2    _msfn, CoordinateStep, LATITUDE_OF_FIRST_STANDARD_PARALLEL, MERCATOR, MERCATOR_VARIANT_A,
3    MERCATOR_VARIANT_B, Proj, ProjMethod, ProjectCoordinates, TransformCoordinates, WEB_MERCATOR,
4    sinhpsi2tanphi,
5};
6use alloc::rc::Rc;
7use core::cell::RefCell;
8use libm::{asinh, atan, atanh, cos, fabs, sin, sinh, tan};
9
10/// Mercator projection
11pub type MercatorProjection = MercatorBaseProjection<MERCATOR>;
12/// Mercator (variant A) projection
13pub type MercatorVariantAProjection = MercatorBaseProjection<MERCATOR_VARIANT_A>;
14/// Mercator (variant B) projection
15pub type MercatorVariantBProjection = MercatorBaseProjection<MERCATOR_VARIANT_B>;
16
17/// # Mercator Projection
18///
19/// The Mercator projection is a cylindrical map projection originating from the 16th century.
20/// It is widely recognized as the first regularly used map projection. It is a conformal projection
21/// where the equator projects to a straight line at constant scale. A rhumb line, or course of
22/// constant heading, projects to a straight line, making it suitable for navigational purposes.
23///
24/// **Classification**: Conformal cylindrical
25///
26/// **Available forms**: Forward and Inverse, spherical and ellipsoidal
27///
28/// **Defined area**: Global, but best used near the equator
29///
30/// **Alias**: `merc`
31///
32/// **Domain**: 2D
33///
34/// **Input type**: Geodetic coordinates
35///
36/// **Output type**: Projected coordinates
37///
38/// ## Projection String
39/// ```ini
40/// +proj=merc
41/// ```
42///
43/// ## Usage
44/// The Mercator projection is often used for equatorial regions and navigational charts. It is not
45/// suitable for world maps due to significant area distortions. For example, Greenland appears
46/// larger than South America in the projection, despite Greenland's actual area being approximately
47/// one-eighth of South America's.
48///
49/// **Examples:**
50///
51/// - Using latitude of true scale:
52///   ```bash
53///   $ echo 56.35 12.32 | proj +proj=merc +lat_ts=56.5
54///   3470306.37    759599.90
55///   ```
56/// - Using scaling factor:
57///   ```bash
58///   $ echo 56.35 12.32 | proj +proj=merc +k_0=2
59///   12545706.61    2746073.80
60///   ```
61///
62/// **Note**: `+lat_ts` and `+k_0` are mutually exclusive. If both are used, `+lat_ts` takes
63/// precedence over `+k_0`.
64///
65/// ## Parameters
66/// - `lat_ts`: Latitude of true scale
67/// - `k_0`: Scaling factor
68/// - `lon_0`: Longitude of origin
69/// - `x_0`: False easting
70/// - `y_0`: False northing
71/// - `ellps`: Ellipsoid
72/// - `R`: Radius of the sphere
73///
74/// ## Mathematical Definition
75///
76/// **Spheroidal Form**
77/// - **Forward Projection**:
78///   $$x = k_0 \cdot R \cdot \lambda$$
79///   $$y = k_0 \cdot R \cdot \psi$$
80///   where
81///   $$\psi = \ln\left(\tan\left(\frac{\pi}{4} + \frac{\phi}{2}\right)\right)$$
82/// - **Inverse Projection**:
83///   $$\lambda = x / (k_0 \cdot R)$$
84///   $$\psi = y / (k_0 \cdot R)$$
85///   $$\phi = \frac{\pi}{2} - 2 \cdot \arctan\left(\exp(-\psi)\right)$$
86///
87/// **Ellipsoidal Form**
88/// - **Forward Projection**:
89///   $$x = k_0 \cdot a \cdot \lambda$$
90///   $$y = k_0 \cdot a \cdot \psi$$
91///   where
92///   $$\psi = \ln\left(\tan\left(\frac{\pi}{4} + \frac{\phi}{2}\right)\right) - 0.5 \cdot e \cdot \ln\left(\frac{1 + e \cdot \sin(\phi)}{1 - e \cdot \sin(\phi)}\right)$$
93/// - **Inverse Projection**:
94///   $$\lambda = x / (k_0 \cdot a)$$
95///   $$\psi = y / (k_0 \cdot a)$$
96///   $$\phi = \arctan(\tau)$$
97///   where
98///   $$\tau = \tan(\phi)$$
99///
100/// ## Further Reading
101/// - [Wikipedia: Mercator Projection](https://en.wikipedia.org/wiki/Mercator_projection)
102/// - [Wolfram Mathworld: Mercator Projection](http://mathworld.wolfram.com/MercatorProjection.html)
103///
104/// ![Mercator Projection](https://github.com/Open-S2/gis-tools/blob/master/assets/proj4/projections/images/merc.png?raw=true)
105#[derive(Debug, Clone, PartialEq)]
106pub struct MercatorBaseProjection<const C: i64> {
107    proj: Rc<RefCell<Proj>>,
108    conv_case: ProjMethod,
109}
110impl<const C: i64> ProjectCoordinates for MercatorBaseProjection<C> {
111    fn code(&self) -> i64 {
112        C
113    }
114    fn name(&self) -> &'static str {
115        "Mercator"
116    }
117    fn names() -> &'static [&'static str] {
118        &[
119            "Mercator",
120            "Popular Visualisation Pseudo Mercator",
121            "Mercator_1SP",
122            "Mercator_2SP",
123            "Mercator (variant A)",
124            "Mercator (variant B)",
125            "Mercator_Auxiliary_Sphere",
126            "merc",
127        ]
128    }
129}
130impl<const C: i64> CoordinateStep for MercatorBaseProjection<C> {
131    fn new(proj: Rc<RefCell<Proj>>) -> Self {
132        let mut phits = 0.0;
133        let mut is_phits: bool = false;
134        let conv_case: ProjMethod;
135        {
136            let proj = &mut proj.borrow_mut();
137            if let Some(lat_ts) = proj.params.get(&LATITUDE_OF_FIRST_STANDARD_PARALLEL) {
138                phits = fabs(lat_ts.f64());
139                is_phits = true;
140            }
141            if phits >= core::f64::consts::FRAC_PI_2 {
142                panic!("Invalid value for lat_ts: |lat_ts| should be <= 90°");
143            }
144
145            if proj.es != 0.0 {
146                // ellipsoid case
147                conv_case = ProjMethod::Ellipsoidal;
148                if is_phits {
149                    proj.k0 = _msfn(sin(phits), cos(phits), proj.es);
150                }
151            } else {
152                // sphere case
153                conv_case = ProjMethod::Spheroidal;
154                if is_phits {
155                    proj.k0 = cos(phits);
156                }
157            }
158        }
159        MercatorBaseProjection { proj, conv_case }
160    }
161    fn forward<P: TransformCoordinates>(&self, p: &mut P) {
162        if self.conv_case == ProjMethod::Spheroidal {
163            merc_s_forward(&self.proj.borrow(), p);
164        } else {
165            merc_e_forward(&self.proj.borrow(), p);
166        }
167    }
168    fn inverse<P: TransformCoordinates>(&self, p: &mut P) {
169        if self.conv_case == ProjMethod::Spheroidal {
170            merc_s_inverse(&self.proj.borrow(), p);
171        } else {
172            merc_e_inverse(&self.proj.borrow(), p);
173        }
174    }
175}
176
177/// # Web Mercator / Pseudo Mercator Projection
178///
179/// The Web Mercator / Pseudo Mercator projection is a cylindrical map projection.
180/// This is a variant of the regular [Mercator](crate::proj::MercatorBaseProjection) projection,
181/// except that the computation is done on a sphere, using the semi-major axis of the ellipsoid.
182///
183/// From [Wikipedia](https://en.wikipedia.org/wiki/Web_Mercator):
184///
185/// > This projection is widely used by the `Web Mercator`, `Google Web Mercator`,
186/// > `Spheroidal Mercator`, `WGS 84 Web Mercator[1]` or `WGS 84/Pseudo-Mercator` is a
187/// > variant of the Mercator projection and is the de facto standard for Web
188/// > mapping applications. [...]
189/// > It is used by virtually all major online map providers [...]
190/// > Its official EPSG identifier is EPSG:3857, although others have been used
191/// > historically.
192///
193/// **Classification**: Cylindrical (non-conformal if used with an ellipsoid)
194///
195/// **Available forms**: Forward and Inverse
196///
197/// **Defined area**: Global
198///
199/// **Alias**: `webmerc`
200///
201/// **Domain**: 2D
202///
203/// **Input type**: Geodetic coordinates
204///
205/// **Output type**: Projected coordinates
206///
207/// ## Usage
208///
209/// ```bash
210/// $ echo 2 49 | proj +proj=webmerc +datum=WGS84
211/// 222638.98       6274861.39
212/// ```
213///
214/// ## Parameters
215///
216/// **Note**: All parameters for the projection are optional, except the ellipsoid
217/// definition, which is WGS84 for the typical use case of EPSG:3857.
218/// In which case, the other parameters are set to their default 0 value.
219///
220/// - `ellps`: Ellipsoid
221/// - `lon_0`: Longitude of origin
222/// - `x_0`: False easting
223/// - `y_0`: False northing
224///
225/// ## Mathematical Definition
226///
227/// The formulas describing the Mercator projection are adapted from G. Evenden's libproj manuals.
228///
229/// **Forward Projection**:
230///
231/// $$ x = λ $$
232/// $$ y = ln(tan(π/4 + φ/2)) $$
233///
234///
235/// **Inverse Projection**:
236///
237/// $$ λ = x $$
238/// $$ φ = π/2 - 2 * atan(exp(-y)) $$
239///
240///
241/// ## Further Reading
242///
243/// - [Wikipedia: Web Mercator](https://en.wikipedia.org/wiki/Web_Mercator)
244///
245/// ![Web Mercator Projection](https://github.com/Open-S2/gis-tools/blob/master/assets/proj4/projections/images/merc.png?raw=true)
246#[derive(Debug, Clone, PartialEq)]
247pub struct WebMercatorProjection {
248    proj: Rc<RefCell<Proj>>,
249}
250impl ProjectCoordinates for WebMercatorProjection {
251    fn code(&self) -> i64 {
252        WEB_MERCATOR
253    }
254    fn name(&self) -> &'static str {
255        "Web Mercator"
256    }
257    fn names() -> &'static [&'static str] {
258        &["Web Mercator", "Pseudo Mercator", "webmerc"]
259    }
260}
261impl CoordinateStep for WebMercatorProjection {
262    fn new(proj: Rc<RefCell<Proj>>) -> Self {
263        WebMercatorProjection { proj }
264    }
265    fn forward<P: TransformCoordinates>(&self, p: &mut P) {
266        merc_s_forward(&self.proj.borrow(), p);
267    }
268    fn inverse<P: TransformCoordinates>(&self, p: &mut P) {
269        merc_s_inverse(&self.proj.borrow(), p);
270    }
271}
272
273/// Ellipsoidal, mercator forward projection
274pub fn merc_e_forward<P: TransformCoordinates>(proj: &Proj, p: &mut P) {
275    p.set_x(proj.k0 * p.lam());
276    // Instead of calling tan and sin, call sin and cos which the compiler
277    // optimizes to a single call to sincos.
278    let phi = p.phi();
279    let sphi = phi.sin();
280    let cphi = phi.cos();
281    p.set_y(proj.k0 * (asinh(sphi / cphi) - proj.e * atanh(proj.e * sphi)));
282}
283
284/// Spheroidal, mercator forward
285pub fn merc_s_forward<P: TransformCoordinates>(proj: &Proj, p: &mut P) {
286    p.set_x(proj.k0 * p.lam());
287    p.set_y(proj.k0 * asinh(tan(p.phi())));
288}
289
290/// Ellipsoidal, mercator inverse
291pub fn merc_e_inverse<P: TransformCoordinates>(proj: &Proj, p: &mut P) {
292    p.set_phi(atan(sinhpsi2tanphi(sinh(p.y() / proj.k0), proj.e)));
293}
294
295/// Spheroidal, mercator inverse
296pub fn merc_s_inverse<P: TransformCoordinates>(proj: &Proj, p: &mut P) {
297    p.set_phi(atan(sinh(p.y() / proj.k0)));
298    p.set_lam(p.x() / proj.k0);
299}