Coverage Report

Created: 2026-10-13 07:22

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}