Coverage Report

Created: 2025-07-23 09:13

/src/gdal/proj/src/projections/adams.cpp
Line
Count
Source (jump to first uncovered line)
1
/*
2
 * Implementation of the Guyou, Pierce Quincuncial, Adams Hemisphere in a
3
 * Square, Adams World in a Square I & II projections.
4
 *
5
 * Based on original code from libproj4 written by Gerald Evenden. Adapted to
6
 * modern PROJ by Kristian Evers. Original code found in file src/proj_guyou.c,
7
 * see
8
 * https://github.com/rouault/libproj4/blob/master/libproject-1.01/src/proj_guyou.c
9
 * for reference.
10
 * Fix for Peirce Quincuncial projection to diamond or square by Toby C.
11
 * Wilkinson to correctly flip out southern hemisphere into the four triangles
12
 * of Peirce's quincunx. The fix inspired by a similar rotate and translate
13
 * solution applied by Jonathan Feinberg for cartopy, see
14
 * https://github.com/jonathf/cartopy/blob/8172cac7fc45cafc86573d408ce85b74258a9c28/lib/cartopy/peircequincuncial.py
15
 * Added original code for horizontal and vertical arrangement of hemispheres by
16
 * Toby C. Wilkinson to allow creation of lateral quincuncial projections, such
17
 * as Grieger's Triptychial, see, e.g.:
18
 * - Grieger, B. (2020). “Optimized global map projections for specific
19
 * applications: the triptychial projection and the Spilhaus projection”.
20
 * EGU2020-9885. https://doi.org/10.5194/egusphere-egu2020-9885
21
 *
22
 * Copyright (c) 2005, 2006, 2009 Gerald I. Evenden
23
 * Copyright (c) 2020 Kristian Evers
24
 * Copyright (c) 2021 Toby C Wilkinson
25
 *
26
 * Related material
27
 * ----------------
28
 *
29
 *  CONFORMAL PROJECTION OF THE SPHERE WITHIN A SQUARE, 1929, OSCAR S. ADAMS,
30
 *  U.S. COAST AND GEODETIC SURVEY, Special Publication No.153,
31
 *  ftp://ftp.library.noaa.gov/docs.lib/htdocs/rescue/cgs_specpubs/QB275U35no1531929.pdf
32
 *
33
 *  https://en.wikipedia.org/wiki/Guyou_hemisphere-in-a-square_projection
34
 *  https://en.wikipedia.org/wiki/Adams_hemisphere-in-a-square_projection
35
 *  https://en.wikipedia.org/wiki/Peirce_quincuncial_projection
36
 */
37
38
#include <errno.h>
39
#include <math.h>
40
41
#include <algorithm>
42
43
#include "proj.h"
44
#include "proj_internal.h"
45
46
PROJ_HEAD(guyou, "Guyou") "\n\tMisc Sph No inv";
47
PROJ_HEAD(peirce_q, "Peirce Quincuncial") "\n\tMisc Sph No inv";
48
PROJ_HEAD(adams_hemi, "Adams Hemisphere in a Square") "\n\tMisc Sph No inv";
49
PROJ_HEAD(adams_ws1, "Adams World in a Square I") "\n\tMisc Sph No inv";
50
PROJ_HEAD(adams_ws2, "Adams World in a Square II") "\n\tMisc Sph No inv";
51
52
namespace { // anonymous namespace
53
54
enum projection_type {
55
    GUYOU,
56
    PEIRCE_Q,
57
    ADAMS_HEMI,
58
    ADAMS_WS1,
59
    ADAMS_WS2,
60
};
61
62
enum peirce_shape {
63
    PEIRCE_Q_SQUARE,
64
    PEIRCE_Q_DIAMOND,
65
    PEIRCE_Q_NHEMISPHERE,
66
    PEIRCE_Q_SHEMISPHERE,
67
    PEIRCE_Q_HORIZONTAL,
68
    PEIRCE_Q_VERTICAL,
69
};
70
71
struct pj_adams_data {
72
    projection_type mode;
73
    peirce_shape pqshape;
74
    double scrollx = 0.0;
75
    double scrolly = 0.0;
76
};
77
78
} // anonymous namespace
79
80
0
#define TOL 1e-9
81
0
#define RSQRT2 0.7071067811865475244008443620
82
83
0
static double ell_int_5(double phi) {
84
    /* Procedure to compute elliptic integral of the first kind
85
     * where k^2=0.5.  Precision good to better than 1e-7
86
     * The approximation is performed with an even Chebyshev
87
     * series, thus the coefficients below are the even values
88
     * and where series evaluation  must be multiplied by the argument. */
89
0
    constexpr double C0 = 2.19174570831038;
90
0
    static const double C[] = {
91
0
        -8.58691003636495e-07, 2.02692115653689e-07, 3.12960480765314e-05,
92
0
        5.30394739921063e-05,  -0.0012804644680613,  -0.00575574836830288,
93
0
        0.0914203033408211,
94
0
    };
95
96
0
    double y = phi * M_2_PI;
97
0
    y = 2. * y * y - 1.;
98
0
    double y2 = 2. * y;
99
0
    double d1 = 0.0;
100
0
    double d2 = 0.0;
101
0
    for (double c : C) {
102
0
        double temp = d1;
103
0
        d1 = y2 * d1 - d2 + c;
104
0
        d2 = temp;
105
0
    }
106
107
0
    return phi * (y * d1 - d2 + 0.5 * C0);
108
0
}
109
110
0
static PJ_XY adams_forward(PJ_LP lp, PJ *P) {
111
0
    double a = 0., b = 0.;
112
0
    bool sm = false, sn = false;
113
0
    PJ_XY xy;
114
0
    const struct pj_adams_data *Q =
115
0
        static_cast<const struct pj_adams_data *>(P->opaque);
116
117
0
    switch (Q->mode) {
118
0
    case GUYOU:
119
0
        if ((fabs(lp.lam) - TOL) > M_PI_2) {
120
0
            proj_errno_set(P, PROJ_ERR_COORD_TRANSFM_OUTSIDE_PROJECTION_DOMAIN);
121
0
            return proj_coord_error().xy;
122
0
        }
123
124
0
        if (fabs(fabs(lp.phi) - M_PI_2) < TOL) {
125
0
            xy.x = 0;
126
0
            xy.y = lp.phi < 0 ? -1.85407 : 1.85407;
127
0
            return xy;
128
0
        } else {
129
0
            const double sl = sin(lp.lam);
130
0
            const double sp = sin(lp.phi);
131
0
            const double cp = cos(lp.phi);
132
0
            a = aacos(P->ctx, (cp * sl - sp) * RSQRT2);
133
0
            b = aacos(P->ctx, (cp * sl + sp) * RSQRT2);
134
0
            sm = lp.lam < 0.;
135
0
            sn = lp.phi < 0.;
136
0
        }
137
0
        break;
138
0
    case PEIRCE_Q: {
139
        /* lam0 - note that the original Peirce model used a central meridian of
140
         * around -70deg, but the default within proj is +lon0=0 */
141
0
        if (Q->pqshape == PEIRCE_Q_NHEMISPHERE) {
142
0
            if (lp.phi < -TOL) {
143
0
                proj_errno_set(
144
0
                    P, PROJ_ERR_COORD_TRANSFM_OUTSIDE_PROJECTION_DOMAIN);
145
0
                return proj_coord_error().xy;
146
0
            }
147
0
        }
148
0
        if (Q->pqshape == PEIRCE_Q_SHEMISPHERE) {
149
0
            if (lp.phi > -TOL) {
150
0
                proj_errno_set(
151
0
                    P, PROJ_ERR_COORD_TRANSFM_OUTSIDE_PROJECTION_DOMAIN);
152
0
                return proj_coord_error().xy;
153
0
            }
154
0
        }
155
0
        const double sl = sin(lp.lam);
156
0
        const double cl = cos(lp.lam);
157
0
        const double cp = cos(lp.phi);
158
0
        a = aacos(P->ctx, cp * (sl + cl) * RSQRT2);
159
0
        b = aacos(P->ctx, cp * (sl - cl) * RSQRT2);
160
0
        sm = sl < 0.;
161
0
        sn = cl > 0.;
162
0
    } break;
163
0
    case ADAMS_HEMI: {
164
0
        const double sp = sin(lp.phi);
165
0
        if ((fabs(lp.lam) - TOL) > M_PI_2) {
166
0
            proj_errno_set(P, PROJ_ERR_COORD_TRANSFM_OUTSIDE_PROJECTION_DOMAIN);
167
0
            return proj_coord_error().xy;
168
0
        }
169
0
        a = cos(lp.phi) * sin(lp.lam);
170
0
        sm = (sp + a) < 0.;
171
0
        sn = (sp - a) < 0.;
172
0
        a = aacos(P->ctx, a);
173
0
        b = M_PI_2 - lp.phi;
174
0
    } break;
175
0
    case ADAMS_WS1: {
176
0
        const double sp = tan(0.5 * lp.phi);
177
0
        b = cos(aasin(P->ctx, sp)) * sin(0.5 * lp.lam);
178
0
        a = aacos(P->ctx, (b - sp) * RSQRT2);
179
0
        b = aacos(P->ctx, (b + sp) * RSQRT2);
180
0
        sm = lp.lam < 0.;
181
0
        sn = lp.phi < 0.;
182
0
    } break;
183
0
    case ADAMS_WS2: {
184
0
        const double spp = tan(0.5 * lp.phi);
185
0
        a = cos(aasin(P->ctx, spp)) * sin(0.5 * lp.lam);
186
0
        sm = (spp + a) < 0.;
187
0
        sn = (spp - a) < 0.;
188
0
        b = aacos(P->ctx, spp);
189
0
        a = aacos(P->ctx, a);
190
0
    } break;
191
0
    }
192
193
0
    double m = aasin(P->ctx, sqrt((1. + std::min(0.0, cos(a + b)))));
194
0
    if (sm)
195
0
        m = -m;
196
197
0
    double n = aasin(P->ctx, sqrt(fabs(1. - std::max(0.0, cos(a - b)))));
198
0
    if (sn)
199
0
        n = -n;
200
201
0
    xy.x = ell_int_5(m);
202
0
    xy.y = ell_int_5(n);
203
204
0
    if (Q->mode == PEIRCE_Q) {
205
        /* Constant complete elliptic integral of the first kind with m=0.5,
206
         * calculated using
207
         * https://docs.scipy.org/doc/scipy/reference/generated/scipy.special.ellipk.html
208
         * . Used as basic as scaled shift distance */
209
0
        constexpr double shd = 1.8540746773013719 * 2;
210
211
        /* For square and diamond Quincuncial projections, spin out southern
212
         * hemisphere to triangular segments of quincunx (before rotation for
213
         * square)*/
214
0
        if (Q->pqshape == PEIRCE_Q_SQUARE || (Q->pqshape == PEIRCE_Q_DIAMOND)) {
215
0
            if (lp.phi < 0.) { /* fold out segments */
216
0
                if (lp.lam < (-0.75 * M_PI))
217
0
                    xy.y = shd -
218
0
                           xy.y; /* top left segment, shift up and reflect y */
219
0
                if ((lp.lam < (-0.25 * M_PI)) && (lp.lam >= (-0.75 * M_PI)))
220
0
                    xy.x = -shd -
221
0
                           xy.x; /* left segment, shift left and reflect x */
222
0
                if ((lp.lam < (0.25 * M_PI)) && (lp.lam >= (-0.25 * M_PI)))
223
0
                    xy.y = -shd -
224
0
                           xy.y; /* bottom segment, shift down and reflect y */
225
0
                if ((lp.lam < (0.75 * M_PI)) && (lp.lam >= (0.25 * M_PI)))
226
0
                    xy.x = shd -
227
0
                           xy.x; /* right segment, shift right and reflect x */
228
0
                if (lp.lam >= (0.75 * M_PI))
229
0
                    xy.y = shd -
230
0
                           xy.y; /* top right segment, shift up and reflect y */
231
0
            }
232
0
        }
233
234
        /* For square types rotate xy by 45 deg */
235
0
        if (Q->pqshape == PEIRCE_Q_SQUARE) {
236
0
            const double temp = xy.x;
237
0
            xy.x = RSQRT2 * (xy.x - xy.y);
238
0
            xy.y = RSQRT2 * (temp + xy.y);
239
0
        }
240
241
        /* For rectangle Quincuncial projs, spin out southern hemisphere to east
242
         * (horizontal) or north (vertical) after rotation */
243
0
        if (Q->pqshape == PEIRCE_Q_HORIZONTAL) {
244
0
            if (lp.phi < 0.) {
245
0
                xy.x = shd - xy.x; /* reflect x to east */
246
0
            }
247
0
            xy.x = xy.x - (shd / 2); /* shift everything so origin is in middle
248
                                        of two hemispheres */
249
0
        }
250
0
        if (Q->pqshape == PEIRCE_Q_VERTICAL) {
251
0
            if (lp.phi < 0.) {
252
0
                xy.y = shd - xy.y; /* reflect y to north */
253
0
            }
254
0
            xy.y = xy.y - (shd / 2); /* shift everything so origin is in middle
255
                                        of two hemispheres */
256
0
        }
257
258
        // if o_scrollx param present, scroll x
259
0
        if (!(Q->scrollx == 0.0) && (Q->pqshape == PEIRCE_Q_HORIZONTAL)) {
260
0
            double xscale = 2.0;
261
0
            double xthresh = shd / 2;
262
0
            xy.x = xy.x +
263
0
                   (Q->scrollx *
264
0
                    (xthresh * 2 * xscale)); /*shift relative to proj width*/
265
0
            if (xy.x >= (xthresh * xscale)) {
266
0
                xy.x = xy.x - (shd * xscale);
267
0
            } else if (xy.x < -(xthresh * xscale)) {
268
0
                xy.x = xy.x + (shd * xscale);
269
0
            }
270
0
        }
271
272
        // if o_scrolly param present, scroll y
273
0
        if (!(Q->scrolly == 0.0) && (Q->pqshape == PEIRCE_Q_VERTICAL)) {
274
0
            double yscale = 2.0;
275
0
            double ythresh = shd / 2;
276
0
            xy.y = xy.y +
277
0
                   (Q->scrolly *
278
0
                    (ythresh * 2 * yscale)); /*shift relative to proj height*/
279
0
            if (xy.y >= (ythresh * yscale)) {
280
0
                xy.y = xy.y - (shd * yscale);
281
0
            } else if (xy.y < -(ythresh * yscale)) {
282
0
                xy.y = xy.y + (shd * yscale);
283
0
            }
284
0
        }
285
0
    }
286
287
0
    if (Q->mode == ADAMS_HEMI || Q->mode == ADAMS_WS2) { /* rotate by 45deg. */
288
0
        const double temp = xy.x;
289
0
        xy.x = RSQRT2 * (xy.x - xy.y);
290
0
        xy.y = RSQRT2 * (temp + xy.y);
291
0
    }
292
293
0
    return xy;
294
0
}
295
296
0
static PJ_LP adams_inverse(PJ_XY xy, PJ *P) {
297
0
    PJ_LP lp;
298
299
    // Only implemented for ADAMS_WS2
300
    // Uses Newton-Raphson method on the following pair of functions:
301
    //      f_x(lam,phi) = adams_forward(lam, phi).x - xy.x
302
    //      f_y(lam,phi) = adams_forward(lam, phi).y - xy.y
303
304
    // Initial guess (very rough, especially at high northings)
305
    // The magic values are got with:
306
    //  echo 0   90 | src/proj -f "%.8f" +proj=adams_ws2 +R=1
307
    //  echo 180 0  | src/proj -f "%.8f" +proj=adams_ws2 +R=1
308
0
    lp.phi = std::max(std::min(xy.y / 2.62181347, 1.0), -1.0) * M_HALFPI;
309
0
    lp.lam =
310
0
        fabs(lp.phi) >= M_HALFPI
311
0
            ? 0
312
0
            : std::max(std::min(xy.x / 2.62205760 / cos(lp.phi), 1.0), -1.0) *
313
0
                  M_PI;
314
315
0
    constexpr double deltaXYTolerance = 1e-10;
316
0
    return pj_generic_inverse_2d(xy, P, lp, deltaXYTolerance);
317
0
}
318
319
0
static PJ_LP peirce_q_square_inverse(PJ_XY xy, PJ *P) {
320
    /* Heuristics based on trial and repeat process */
321
0
    PJ_LP lp;
322
0
    lp.phi = 0;
323
0
    if (xy.x == 0 && xy.y < 0) {
324
0
        lp.lam = -M_PI / 4;
325
0
        if (fabs(xy.y) < 2.622057580396)
326
0
            lp.phi = M_PI / 4;
327
0
    } else if (xy.x > 0 && fabs(xy.y) < 1e-7)
328
0
        lp.lam = M_PI / 4;
329
0
    else if (xy.x < 0 && fabs(xy.y) < 1e-7) {
330
0
        lp.lam = -3 * M_PI / 4;
331
0
        lp.phi = M_PI / 2 / 2.622057574224 * xy.x + M_PI / 2;
332
0
    } else if (fabs(xy.x) < 1e-7 && xy.y > 0)
333
0
        lp.lam = 3 * M_PI / 4;
334
0
    else if (xy.x >= 0 && xy.y <= 0) {
335
0
        lp.lam = 0;
336
0
        if (xy.x == 0 && xy.y == 0) {
337
0
            lp.phi = M_PI / 2;
338
0
            return lp;
339
0
        }
340
0
    } else if (xy.x >= 0 && xy.y >= 0)
341
0
        lp.lam = M_PI / 2;
342
0
    else if (xy.x <= 0 && xy.y >= 0) {
343
0
        if (fabs(xy.x) < fabs(xy.y))
344
0
            lp.lam = M_PI * 0.9;
345
0
        else
346
0
            lp.lam = -M_PI * 0.9;
347
0
    } else /* if( xy.x <= 0 && xy.y <= 0 ) */
348
0
        lp.lam = -M_PI / 2;
349
350
0
    constexpr double deltaXYTolerance = 1e-10;
351
0
    return pj_generic_inverse_2d(xy, P, lp, deltaXYTolerance);
352
0
}
353
354
0
static PJ_LP peirce_q_diamond_inverse(PJ_XY xy, PJ *P) {
355
    /* Heuristics based on a trial and repeat process */
356
0
    PJ_LP lp;
357
0
    lp.phi = 0;
358
0
    if (xy.x >= 0 && xy.y <= 0) {
359
0
        lp.lam = M_PI / 4;
360
0
        if (xy.x > 0 && xy.y == 0) {
361
0
            lp.lam = M_PI / 2;
362
0
            lp.phi = 0;
363
0
        } else if (xy.x == 0 && xy.y == 0) {
364
0
            lp.lam = 0;
365
0
            lp.phi = M_PI / 2;
366
0
            return lp;
367
0
        } else if (xy.x == 0 && xy.y < 0) {
368
0
            lp.lam = 0;
369
0
            lp.phi = M_PI / 4;
370
0
        }
371
0
    } else if (xy.x >= 0 && xy.y >= 0)
372
0
        lp.lam = 3 * M_PI / 4;
373
0
    else if (xy.x <= 0 && xy.y >= 0) {
374
0
        lp.lam = -3 * M_PI / 4;
375
0
    } else /* if( xy.x <= 0 && xy.y <= 0 ) */
376
0
        lp.lam = -M_PI / 4;
377
378
0
    if (fabs(xy.x) > 1.8540746773013719 + 1e-3 ||
379
0
        fabs(xy.y) > 1.8540746773013719 + 1e-3) {
380
0
        lp.phi = -M_PI / 4;
381
0
    }
382
383
0
    constexpr double deltaXYTolerance = 1e-10;
384
0
    return pj_generic_inverse_2d(xy, P, lp, deltaXYTolerance);
385
0
}
386
387
15.4k
static PJ *pj_adams_setup(PJ *P, projection_type mode) {
388
15.4k
    struct pj_adams_data *Q = static_cast<struct pj_adams_data *>(
389
15.4k
        calloc(1, sizeof(struct pj_adams_data)));
390
391
15.4k
    if (nullptr == Q)
392
0
        return pj_default_destructor(P, PROJ_ERR_OTHER /*ENOMEM*/);
393
15.4k
    P->opaque = Q;
394
395
15.4k
    P->es = 0;
396
15.4k
    P->fwd = adams_forward;
397
398
15.4k
    Q->mode = mode;
399
15.4k
    if (mode == ADAMS_WS2)
400
3.20k
        P->inv = adams_inverse;
401
402
15.4k
    if (mode == PEIRCE_Q) {
403
        // Quincuncial projections shape options: square, diamond, hemisphere,
404
        // horizontal (rectangle) or vertical (rectangle)
405
5.62k
        const char *pqshape = pj_param(P->ctx, P->params, "sshape").s;
406
407
5.62k
        if (!pqshape)
408
5.23k
            pqshape = "diamond"; /* default if shape value not supplied */
409
410
5.62k
        if (strcmp(pqshape, "square") == 0) {
411
89
            Q->pqshape = PEIRCE_Q_SQUARE;
412
89
            P->inv = peirce_q_square_inverse;
413
5.53k
        } else if (strcmp(pqshape, "diamond") == 0) {
414
5.35k
            Q->pqshape = PEIRCE_Q_DIAMOND;
415
5.35k
            P->inv = peirce_q_diamond_inverse;
416
5.35k
        } else if (strcmp(pqshape, "nhemisphere") == 0) {
417
0
            Q->pqshape = PEIRCE_Q_NHEMISPHERE;
418
181
        } else if (strcmp(pqshape, "shemisphere") == 0) {
419
0
            Q->pqshape = PEIRCE_Q_SHEMISPHERE;
420
181
        } else if (strcmp(pqshape, "horizontal") == 0) {
421
10
            Q->pqshape = PEIRCE_Q_HORIZONTAL;
422
10
            if (pj_param(P->ctx, P->params, "tscrollx").i) {
423
0
                double scrollx;
424
0
                scrollx = pj_param(P->ctx, P->params, "dscrollx").f;
425
0
                if (scrollx > 1 || scrollx < -1) {
426
0
                    proj_log_error(
427
0
                        P, _("Invalid value for scrollx: |scrollx| should "
428
0
                             "between -1 and 1"));
429
0
                    return pj_default_destructor(
430
0
                        P, PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
431
0
                }
432
0
                Q->scrollx = scrollx;
433
0
            }
434
171
        } else if (strcmp(pqshape, "vertical") == 0) {
435
28
            Q->pqshape = PEIRCE_Q_VERTICAL;
436
28
            if (pj_param(P->ctx, P->params, "tscrolly").i) {
437
0
                double scrolly;
438
0
                scrolly = pj_param(P->ctx, P->params, "dscrolly").f;
439
0
                if (scrolly > 1 || scrolly < -1) {
440
0
                    proj_log_error(
441
0
                        P, _("Invalid value for scrolly: |scrolly| should "
442
0
                             "between -1 and 1"));
443
0
                    return pj_default_destructor(
444
0
                        P, PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
445
0
                }
446
0
                Q->scrolly = scrolly;
447
0
            }
448
143
        } else {
449
143
            proj_log_error(P,
450
143
                           _("peirce_q: invalid value for 'shape' parameter"));
451
143
            return pj_default_destructor(P,
452
143
                                         PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
453
143
        }
454
5.62k
    }
455
456
15.3k
    return P;
457
15.4k
}
458
459
2.37k
PJ *PJ_PROJECTION(guyou) { return pj_adams_setup(P, GUYOU); }
460
461
5.62k
PJ *PJ_PROJECTION(peirce_q) { return pj_adams_setup(P, PEIRCE_Q); }
462
463
692
PJ *PJ_PROJECTION(adams_hemi) { return pj_adams_setup(P, ADAMS_HEMI); }
464
465
3.55k
PJ *PJ_PROJECTION(adams_ws1) { return pj_adams_setup(P, ADAMS_WS1); }
466
467
3.20k
PJ *PJ_PROJECTION(adams_ws2) { return pj_adams_setup(P, ADAMS_WS2); }
468
469
#undef TOL
470
#undef RSQRT2