/src/PROJ/src/projections/aitoff.cpp
Line | Count | Source |
1 | | /****************************************************************************** |
2 | | * Project: PROJ.4 |
3 | | * Purpose: Implementation of the aitoff (Aitoff) and wintri (Winkel Tripel) |
4 | | * projections. |
5 | | * Author: Gerald Evenden (1995) |
6 | | * Drazen Tutic, Lovro Gradiser (2015) - add inverse |
7 | | * Thomas Knudsen (2016) - revise/add regression tests |
8 | | * Robert Kleffner (2026) - closed-form Aitoff inverse |
9 | | * |
10 | | ****************************************************************************** |
11 | | * Copyright (c) 1995, Gerald Evenden |
12 | | * |
13 | | * Permission is hereby granted, free of charge, to any person obtaining a |
14 | | * copy of this software and associated documentation files (the "Software"), |
15 | | * to deal in the Software without restriction, including without limitation |
16 | | * the rights to use, copy, modify, merge, publish, distribute, sublicense, |
17 | | * and/or sell copies of the Software, and to permit persons to whom the |
18 | | * Software is furnished to do so, subject to the following conditions: |
19 | | * |
20 | | * The above copyright notice and this permission notice shall be included |
21 | | * in all copies or substantial portions of the Software. |
22 | | * |
23 | | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS |
24 | | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
25 | | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL |
26 | | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
27 | | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
28 | | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER |
29 | | * DEALINGS IN THE SOFTWARE. |
30 | | *****************************************************************************/ |
31 | | |
32 | | #include <errno.h> |
33 | | #include <math.h> |
34 | | |
35 | | #include "proj.h" |
36 | | #include "proj_internal.h" |
37 | | |
38 | | namespace pj_aitoff_ns { |
39 | | enum Mode { AITOFF = 0, WINKEL_TRIPEL = 1 }; |
40 | | } |
41 | | |
42 | | namespace { // anonymous namespace |
43 | | struct pj_aitoff_data { |
44 | | double cosphi1; |
45 | | enum pj_aitoff_ns::Mode mode; |
46 | | }; |
47 | | } // anonymous namespace |
48 | | |
49 | | PROJ_HEAD(aitoff, "Aitoff") "\n\tMisc Sph"; |
50 | | PROJ_HEAD(wintri, "Winkel Tripel") "\n\tMisc Sph\n\tlat_1"; |
51 | | |
52 | | #if 0 |
53 | | FORWARD(aitoff_s_forward); /* spheroid */ |
54 | | #endif |
55 | | |
56 | 0 | static PJ_XY aitoff_s_forward(PJ_LP lp, PJ *P) { /* Spheroidal, forward */ |
57 | 0 | PJ_XY xy = {0.0, 0.0}; |
58 | 0 | struct pj_aitoff_data *Q = static_cast<struct pj_aitoff_data *>(P->opaque); |
59 | |
|
60 | | #if 0 |
61 | | // Likely domain of validity for wintri in +over mode. Should be confirmed |
62 | | // Cf https://lists.osgeo.org/pipermail/gdal-dev/2023-April/057164.html |
63 | | if (Q->mode == WINKEL_TRIPEL && fabs(lp.lam) > 2 * M_PI) { |
64 | | proj_errno_set(P, PROJ_ERR_COORD_TRANSFM_OUTSIDE_PROJECTION_DOMAIN); |
65 | | return xy; |
66 | | } |
67 | | #endif |
68 | 0 | const double sin_half_lam = sin(0.5 * lp.lam); |
69 | 0 | const double sin_phi = sin(lp.phi); |
70 | 0 | const double cos_phi = cos(lp.phi); |
71 | | /* basic Aitoff; computing it with atan2 (instead of standard acos) |
72 | | * keeps relative accuracy near the origin. */ |
73 | 0 | const double sin_alpha = hypot(cos_phi * sin_half_lam, sin_phi); |
74 | | /* alpha / sin(alpha), with limit 1 at the origin */ |
75 | 0 | const double A = |
76 | 0 | sin_alpha == 0 |
77 | 0 | ? 1. |
78 | 0 | : atan2(sin_alpha, cos_phi * cos(0.5 * lp.lam)) / sin_alpha; |
79 | 0 | xy.x = 2. * cos_phi * sin_half_lam * A; |
80 | 0 | xy.y = sin_phi * A; |
81 | 0 | if (Q->mode == pj_aitoff_ns::WINKEL_TRIPEL) { |
82 | 0 | xy.x = (xy.x + lp.lam * Q->cosphi1) * 0.5; |
83 | 0 | xy.y = (xy.y + lp.phi) * 0.5; |
84 | 0 | } |
85 | 0 | return (xy); |
86 | 0 | } |
87 | | |
88 | | /****************************************************************************** |
89 | | * |
90 | | * Closed-form inverse of the Aitoff projection. Short form equations are: |
91 | | * |
92 | | * alpha = hypot(x/2, y) (angular distance to (phi, lambda/2)) |
93 | | * phi = asin(y * sin(alpha) / alpha) |
94 | | * lambda = 2 * atan2((x/2) * sin(alpha), alpha * cos(alpha)) |
95 | | * |
96 | | * atan2 places lambda/2 in (-pi, pi]. The domain of the inverse is therefore |
97 | | * the whole ellipse x^2 + 4*y^2 <= (2*pi)^2, on which the sphere is double |
98 | | * covered; longitudes beyond +/-pi are returned only with +over. The poles |
99 | | * need no special case. |
100 | | * |
101 | | * Reference: |
102 | | * J. P. Snyder (1987). Map Projections -- A Working Manual. USGS |
103 | | * Professional Paper 1395, pp. 195-197. |
104 | | * |
105 | | *****************************************************************************/ |
106 | | |
107 | | /* Returns false, leaving *lp untouched, when (x, y) is outside the ellipse |
108 | | * x^2 + 4*y^2 = (2*pi)^2 and so has no preimage. */ |
109 | 0 | static bool aitoff_s_inverse_closed(PJ_XY xy, PJ_LP *lp) { |
110 | 0 | const double half_x = 0.5 * xy.x; |
111 | 0 | const double alpha = hypot(half_x, xy.y); |
112 | 0 | const double sin_alpha = sin(alpha); |
113 | 0 | const double cos_alpha = cos(alpha); |
114 | | /* Testing sin_alpha is needed because with long doubles sin(M_PI) < 0. */ |
115 | 0 | if (alpha > M_PI || sin_alpha < 0) |
116 | 0 | return false; |
117 | | /* The asin argument is guaranteed to lie in [-1, 1]: hypot(a, b) >= |b| |
118 | | * and 0 <= sin_alpha <= 1. */ |
119 | 0 | lp->phi = alpha == 0.0 ? xy.y : asin((xy.y / alpha) * sin_alpha); |
120 | 0 | lp->lam = 2. * atan2(half_x * sin_alpha, alpha * cos_alpha); |
121 | 0 | return true; |
122 | 0 | } |
123 | | |
124 | | /****************************************************************************** |
125 | | * |
126 | | * Inverse of the Aitoff and Winkel Tripel projections. |
127 | | * |
128 | | * Aitoff uses the closed form above. Winkel Tripel, the mean of the Aitoff and |
129 | | * equirectangular projections, has no known closed-form inverse; it is inverted |
130 | | * with the Newton-Raphson method of Bildirici and Ipbuker (2002), added by |
131 | | * Drazen Tutic and Lovro Gradiser (2015). Expected accuracy is defined by |
132 | | * EPSILON = 1e-12, appropriate for most applications of the projection. |
133 | | * |
134 | | * Reference: |
135 | | * I. O. Bildirici and C. Ipbuker (2002). A General Algorithm for the Inverse |
136 | | * Transformation of Map Projections Using Jacobian Matrices. Proceedings of |
137 | | * the Third International Symposium on Mathematical and Computational |
138 | | * Applications, pp. 175-182, Turkey. |
139 | | * |
140 | | * Contact: dtutic at geof.hr, 2015-02-16 |
141 | | * |
142 | | *****************************************************************************/ |
143 | | |
144 | 0 | static PJ_LP aitoff_s_inverse(PJ_XY xy, PJ *P) { /* Spheroidal, inverse */ |
145 | 0 | PJ_LP lp = {0.0, 0.0}; |
146 | 0 | struct pj_aitoff_data *Q = static_cast<struct pj_aitoff_data *>(P->opaque); |
147 | |
|
148 | 0 | if (Q->mode == pj_aitoff_ns::AITOFF) { |
149 | 0 | if (!aitoff_s_inverse_closed(xy, &lp)) { |
150 | 0 | proj_errno_set(P, PROJ_ERR_COORD_TRANSFM_OUTSIDE_PROJECTION_DOMAIN); |
151 | 0 | lp.lam = lp.phi = HUGE_VAL; |
152 | 0 | } |
153 | 0 | return lp; |
154 | 0 | } |
155 | | |
156 | 0 | int iter, MAXITER = 10, round = 0, MAXROUND = 20; |
157 | 0 | double EPSILON = 1e-12, D, C, f1, f2, f1p, f1l, f2p, f2l, dp, dl, sl, sp, |
158 | 0 | cp, cl, x, y; |
159 | |
|
160 | 0 | if ((fabs(xy.x) < EPSILON) && (fabs(xy.y) < EPSILON)) { |
161 | 0 | lp.phi = 0.; |
162 | 0 | lp.lam = 0.; |
163 | 0 | return lp; |
164 | 0 | } |
165 | | |
166 | | /* initial values for Newton-Raphson method */ |
167 | 0 | lp.phi = xy.y; |
168 | 0 | lp.lam = xy.x; |
169 | 0 | do { |
170 | 0 | iter = 0; |
171 | 0 | do { |
172 | 0 | sl = sin(lp.lam * 0.5); |
173 | 0 | cl = cos(lp.lam * 0.5); |
174 | 0 | sp = sin(lp.phi); |
175 | 0 | cp = cos(lp.phi); |
176 | 0 | D = cp * cl; |
177 | 0 | C = 1. - D * D; |
178 | 0 | const double denom = pow(C, 1.5); |
179 | 0 | if (denom == 0) { |
180 | 0 | proj_errno_set( |
181 | 0 | P, PROJ_ERR_COORD_TRANSFM_OUTSIDE_PROJECTION_DOMAIN); |
182 | 0 | return lp; |
183 | 0 | } |
184 | 0 | D = acos(D) / denom; |
185 | 0 | f1 = 2. * D * C * cp * sl; |
186 | 0 | f2 = D * C * sp; |
187 | 0 | f1p = 2. * (sl * cl * sp * cp / C - D * sp * sl); |
188 | 0 | f1l = cp * cp * sl * sl / C + D * cp * cl * sp * sp; |
189 | 0 | f2p = sp * sp * cl / C + D * sl * sl * cp; |
190 | 0 | f2l = 0.5 * (sp * cp * sl / C - D * sp * cp * cp * sl * cl); |
191 | 0 | if (Q->mode == pj_aitoff_ns::WINKEL_TRIPEL) { |
192 | 0 | f1 = 0.5 * (f1 + lp.lam * Q->cosphi1); |
193 | 0 | f2 = 0.5 * (f2 + lp.phi); |
194 | 0 | f1p *= 0.5; |
195 | 0 | f1l = 0.5 * (f1l + Q->cosphi1); |
196 | 0 | f2p = 0.5 * (f2p + 1.); |
197 | 0 | f2l *= 0.5; |
198 | 0 | } |
199 | 0 | f1 -= xy.x; |
200 | 0 | f2 -= xy.y; |
201 | 0 | dp = f1p * f2l - f2p * f1l; |
202 | 0 | dl = (f2 * f1p - f1 * f2p) / dp; |
203 | 0 | dp = (f1 * f2l - f2 * f1l) / dp; |
204 | 0 | dl = fmod(dl, M_PI); /* set to interval [-M_PI, M_PI] */ |
205 | 0 | lp.phi -= dp; |
206 | 0 | lp.lam -= dl; |
207 | 0 | } while ((fabs(dp) > EPSILON || fabs(dl) > EPSILON) && |
208 | 0 | (iter++ < MAXITER)); |
209 | 0 | if (lp.phi > M_PI_2) |
210 | 0 | lp.phi -= |
211 | 0 | 2. * (lp.phi - |
212 | 0 | M_PI_2); /* correct if symmetrical solution for Aitoff */ |
213 | 0 | if (lp.phi < -M_PI_2) |
214 | 0 | lp.phi -= |
215 | 0 | 2. * (lp.phi + |
216 | 0 | M_PI_2); /* correct if symmetrical solution for Aitoff */ |
217 | | |
218 | | /* calculate x,y coordinates with solution obtained */ |
219 | 0 | if ((D = acos(cos(lp.phi) * cos(C = 0.5 * lp.lam))) != |
220 | 0 | 0.0) { /* Aitoff */ |
221 | 0 | y = 1. / sin(D); |
222 | 0 | x = 2. * D * cos(lp.phi) * sin(C) * y; |
223 | 0 | y *= D * sin(lp.phi); |
224 | 0 | } else |
225 | 0 | x = y = 0.; |
226 | 0 | if (Q->mode == pj_aitoff_ns::WINKEL_TRIPEL) { |
227 | 0 | x = (x + lp.lam * Q->cosphi1) * 0.5; |
228 | 0 | y = (y + lp.phi) * 0.5; |
229 | 0 | } |
230 | | /* if too far from given values of x,y, repeat with better approximation |
231 | | * of phi,lam */ |
232 | 0 | } while (((fabs(xy.x - x) > EPSILON) || (fabs(xy.y - y) > EPSILON)) && |
233 | 0 | (round++ < MAXROUND)); |
234 | | |
235 | 0 | if (iter == MAXITER && round == MAXROUND) { |
236 | 0 | proj_context_errno_set( |
237 | 0 | P->ctx, PROJ_ERR_COORD_TRANSFM_OUTSIDE_PROJECTION_DOMAIN); |
238 | | /* fprintf(stderr, "Warning: Accuracy of 1e-12 not reached. Last |
239 | | * increments: dlat=%e and dlon=%e\n", dp, dl); */ |
240 | 0 | } |
241 | |
|
242 | 0 | return lp; |
243 | 0 | } |
244 | | |
245 | 55 | static PJ *pj_aitoff_setup(PJ *P) { |
246 | 55 | P->inv = aitoff_s_inverse; |
247 | 55 | P->fwd = aitoff_s_forward; |
248 | 55 | P->es = 0.; |
249 | 55 | return P; |
250 | 55 | } |
251 | | |
252 | 33 | PJ *PJ_PROJECTION(aitoff) { |
253 | 33 | struct pj_aitoff_data *Q = static_cast<struct pj_aitoff_data *>( |
254 | 33 | calloc(1, sizeof(struct pj_aitoff_data))); |
255 | 33 | if (nullptr == Q) |
256 | 0 | return pj_default_destructor(P, PROJ_ERR_OTHER /*ENOMEM*/); |
257 | 33 | P->opaque = Q; |
258 | | |
259 | 33 | Q->mode = pj_aitoff_ns::AITOFF; |
260 | 33 | return pj_aitoff_setup(P); |
261 | 33 | } |
262 | | |
263 | 22 | PJ *PJ_PROJECTION(wintri) { |
264 | 22 | struct pj_aitoff_data *Q = static_cast<struct pj_aitoff_data *>( |
265 | 22 | calloc(1, sizeof(struct pj_aitoff_data))); |
266 | 22 | if (nullptr == Q) |
267 | 0 | return pj_default_destructor(P, PROJ_ERR_OTHER /*ENOMEM*/); |
268 | 22 | P->opaque = Q; |
269 | | |
270 | 22 | Q->mode = pj_aitoff_ns::WINKEL_TRIPEL; |
271 | 22 | if (pj_param(P->ctx, P->params, "tlat_1").i) { |
272 | 3 | if ((Q->cosphi1 = cos(pj_param(P->ctx, P->params, "rlat_1").f)) == 0.) { |
273 | 0 | proj_log_error( |
274 | 0 | P, _("Invalid value for lat_1: |lat_1| should be < 90°")); |
275 | 0 | return pj_default_destructor(P, |
276 | 0 | PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE); |
277 | 0 | } |
278 | 3 | } else /* 50d28' or acos(2/pi) */ |
279 | 19 | Q->cosphi1 = 0.636619772367581343; |
280 | 22 | return pj_aitoff_setup(P); |
281 | 22 | } |