Coverage Report

Created: 2025-10-13 06:59

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/PROJ/src/projections/isea.cpp
Line
Count
Source
1
/*
2
   The public domain code for the forward direction was initially
3
   written by Nathan Wagner.
4
5
   The inverse projection was adapted from Java and eC by
6
   Jérôme Jacovella-St-Louis, originally from the Franz-Benjamin Mocnik's ISEA
7
   implementation found at
8
   https://github.com/mocnik-science/geogrid/blob/master/
9
    src/main/java/org/giscience/utils/geogrid/projections/ISEAProjection.java
10
   with the following license:
11
   --------------------------------------------------------------------------
12
   MIT License
13
14
   Copyright (c) 2017-2019 Heidelberg University
15
16
   Permission is hereby granted, free of charge, to any person obtaining a copy
17
   of this software and associated documentation files (the "Software"), to deal
18
   in the Software without restriction, including without limitation the rights
19
   to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
20
   copies of the Software, and to permit persons to whom the Software is
21
   furnished to do so, subject to the following conditions:
22
23
   The above copyright notice and this permission notice shall be included in
24
   all copies or substantial portions of the Software.
25
26
   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
27
   IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
28
   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
29
   AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
30
   LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
31
   OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
32
   SOFTWARE.
33
*/
34
35
/* The ISEA projection a projects a sphere on the icosahedron. Thereby the size
36
 * of areas mapped to the icosahedron are preserved. Angles and distances are
37
 * however slightly distorted. The angular distortion is below 17.27 degree, and
38
 * the scale variation is less than 16.3 per cent.
39
 *
40
 * The projection has been proposed and has been described in detail by:
41
 *
42
 * John P. Snyder: An equal-area map projection for polyhedral globes.
43
 * Cartographica, 29(1), 10–21, 1992. doi:10.3138/27H7-8K88-4882-1752
44
 *
45
 * Another description and improvements can be found in:
46
 *
47
 * Erika Harrison, Ali Mahdavi-Amiri, and Faramarz Samavati: Optimization of
48
 * inverse Snyder polyhedral projection. International Conference on Cyberworlds
49
 * 2011. doi:10.1109/CW.2011.36
50
 *
51
 * Erika Harrison, Ali Mahdavi-Amiri, and Faramarz Samavati: Analysis of inverse
52
 * Snyder optimizations. In: Marina L. Gavrilova, and C. J. Kenneth Tan (Eds):
53
 * Transactions on Computational Science XVI. Heidelberg, Springer, 2012. pp.
54
 * 134–148. doi:10.1007/978-3-642-32663-9_8
55
 */
56
57
#include <errno.h>
58
#include <float.h>
59
#include <math.h>
60
#include <stdio.h>
61
#include <stdlib.h>
62
#include <string.h>
63
64
#include <algorithm>
65
#include <limits>
66
67
#include "proj.h"
68
#include "proj_internal.h"
69
#include <math.h>
70
71
#define DEG36 0.62831853071795864768
72
#define DEG72 1.25663706143591729537
73
#define DEG90 M_PI_2
74
#define DEG108 1.88495559215387594306
75
32.5k
#define DEG120 2.09439510239319549229
76
#define DEG144 2.51327412287183459075
77
5.46k
#define DEG180 M_PI
78
79
/* sqrt(5)/M_PI */
80
0
#define ISEA_SCALE 0.8301572857837594396028083
81
82
/* 26.565051177 degrees */
83
#define V_LAT 0.46364760899944494524
84
85
// Latitude of center of top icosahedron faces
86
// atan((3 + sqrt(5)) / 4) = 52.6226318593487 degrees
87
#define E_RAD 0.91843818701052843323
88
89
// Latitude of center of faces mirroring top icosahedron face
90
// atan((3 - sqrt(5)) / 4) = 10.8123169635739 degrees
91
#define F_RAD 0.18871053078356206978
92
93
// #define phi ((1 + sqrt(5)) / 2)
94
// #define atanphi 1.01722196789785136772
95
96
// g: Spherical distance from center of polygon face to
97
//    any of its vertices on the sphere
98
// g = F + 2 * atan(phi) - 90 deg -- sdc2vos
99
36.9k
#define sdc2vos 0.6523581397843681859886783
100
101
38.4k
#define tang 0.76393202250021030358019673567 // tan(sdc2vos)
102
103
// theta (30 degrees) is plane angle between radius
104
// vector to center and adjacent edge of plane polygon
105
106
19.9k
#define tan30 0.57735026918962576450914878 // tan(DEG_TO_RAD * 30)
107
19.9k
#define cotTheta (1.0 / tan30)
108
109
// G: spherical angle between radius vector to center and adjacent edge
110
//    of spherical polygon on the globe (36 degrees)
111
//    cos(DEG_TO_RAD * 36)
112
5.46k
#define cosG 0.80901699437494742410229341718281905886
113
//    sin(DEG_TO_RAD * 36)
114
5.46k
#define sinG 0.587785252292473129168705954639072768597652
115
//    cos(g)
116
5.46k
#define cosSDC2VoS 0.7946544722917661229596057297879189448539
117
118
5.46k
#define sinGcosSDC2VoS (sinG * cosSDC2VoS) // sin G cos g
119
120
7.60k
#define SQRT3 1.73205080756887729352744634150587236694280525381038
121
7.34k
#define sin60 (SQRT3 / 2.0)
122
0
#define cos30 (SQRT3 / 2.0)
123
124
// tang * sin(60 deg)
125
7.34k
#define TABLE_G (tang * sin60)
126
127
// (1 / (2 * sqrt(5)) + 1 / 6.0) * sqrt(M_PI * sqrt(3))
128
38.4k
#define RprimeOverR 0.9103832815095032 // R' / R
129
130
/* H = 0.25 R tan g = */
131
5.46k
#define TABLE_H (0.25 * tang)
132
133
/* in radians */
134
1.14k
#define ISEA_STD_LAT 1.01722196792335072101
135
660
#define ISEA_STD_LONG .19634954084936207740
136
137
namespace { // anonymous namespace
138
139
struct GeoPoint {
140
    double lat, lon;
141
}; // In radians
142
143
struct hex {
144
    int iso;
145
    long x, y, z;
146
};
147
} // anonymous namespace
148
149
/* y *must* be positive down as the xy /iso conversion assumes this */
150
0
static void hex_xy(struct hex *h) {
151
0
    if (!h->iso)
152
0
        return;
153
0
    if (h->x >= 0) {
154
0
        h->y = -h->y - (h->x + 1) / 2;
155
0
    } else {
156
        /* need to round toward -inf, not toward zero, so x-1 */
157
0
        h->y = -h->y - h->x / 2;
158
0
    }
159
0
    h->iso = 0;
160
0
}
161
162
0
static void hex_iso(struct hex *h) {
163
0
    if (h->iso)
164
0
        return;
165
166
0
    if (h->x >= 0) {
167
0
        h->y = (-h->y - (h->x + 1) / 2);
168
0
    } else {
169
        /* need to round toward -inf, not toward zero, so x-1 */
170
0
        h->y = (-h->y - (h->x) / 2);
171
0
    }
172
173
0
    h->z = -h->x - h->y;
174
0
    h->iso = 1;
175
0
}
176
177
0
static void hexbin2(double width, double x, double y, long *i, long *j) {
178
0
    double z, rx, ry, rz;
179
0
    double abs_dx, abs_dy, abs_dz;
180
0
    long ix, iy, iz, s;
181
0
    struct hex h;
182
183
0
    x = x / cos(30 * M_PI / 180.0); /* rotated X coord */
184
0
    y = y - x / 2.0;                /* adjustment for rotated X */
185
186
    /* adjust for actual hexwidth */
187
0
    if (width == 0) {
188
0
        throw "Division by zero";
189
0
    }
190
0
    x /= width;
191
0
    y /= width;
192
193
0
    z = -x - y;
194
195
0
    rx = floor(x + 0.5);
196
0
    ix = lround(rx);
197
0
    ry = floor(y + 0.5);
198
0
    iy = lround(ry);
199
0
    rz = floor(z + 0.5);
200
0
    iz = lround(rz);
201
0
    if (fabs((double)ix + iy) > std::numeric_limits<int>::max() ||
202
0
        fabs((double)ix + iy + iz) > std::numeric_limits<int>::max()) {
203
0
        throw "Integer overflow";
204
0
    }
205
206
0
    s = ix + iy + iz;
207
208
0
    if (s) {
209
0
        abs_dx = fabs(rx - x);
210
0
        abs_dy = fabs(ry - y);
211
0
        abs_dz = fabs(rz - z);
212
213
0
        if (abs_dx >= abs_dy && abs_dx >= abs_dz) {
214
0
            ix -= s;
215
0
        } else if (abs_dy >= abs_dx && abs_dy >= abs_dz) {
216
0
            iy -= s;
217
0
        } else {
218
0
            iz -= s;
219
0
        }
220
0
    }
221
0
    h.x = ix;
222
0
    h.y = iy;
223
0
    h.z = iz;
224
0
    h.iso = 1;
225
226
0
    hex_xy(&h);
227
0
    *i = h.x;
228
0
    *j = h.y;
229
0
}
230
231
50.6k
#define numIcosahedronFaces 20
232
233
namespace { // anonymous namespace
234
enum isea_address_form { ISEA_PLANE, ISEA_Q2DI, ISEA_Q2DD, ISEA_HEX };
235
236
struct isea_sincos {
237
    double s, c;
238
};
239
240
struct isea_pt {
241
    double x, y;
242
};
243
244
} // anonymous namespace
245
246
// distortion
247
// static double maximumAngularDistortion = 17.27;
248
// static double maximumScaleVariation = 1.163;
249
// static double minimumScaleVariation = .860;
250
251
// Vertices of dodecahedron centered in icosahedron triangular faces
252
static const GeoPoint facesCenterDodecahedronVertices[numIcosahedronFaces] = {
253
    {E_RAD, DEG_TO_RAD * -144},  {E_RAD, DEG_TO_RAD * -72},
254
    {E_RAD, DEG_TO_RAD * 0},     {E_RAD, DEG_TO_RAD * 72},
255
    {E_RAD, DEG_TO_RAD * 144},   {F_RAD, DEG_TO_RAD * -144},
256
    {F_RAD, DEG_TO_RAD * -72},   {F_RAD, DEG_TO_RAD * 0},
257
    {F_RAD, DEG_TO_RAD * 72},    {F_RAD, DEG_TO_RAD * 144},
258
    {-F_RAD, DEG_TO_RAD * -108}, {-F_RAD, DEG_TO_RAD * -36},
259
    {-F_RAD, DEG_TO_RAD * 36},   {-F_RAD, DEG_TO_RAD * 108},
260
    {-F_RAD, DEG_TO_RAD * 180},  {-E_RAD, DEG_TO_RAD * -108},
261
    {-E_RAD, DEG_TO_RAD * -36},  {-E_RAD, DEG_TO_RAD * 36},
262
    {-E_RAD, DEG_TO_RAD * 108},  {-E_RAD, DEG_TO_RAD * 180}};
263
264
// NOTE: Very similar to ISEAPlanarProjection::faceOrientation(),
265
//       but the forward projection sometimes is returning a negative M_PI
266
9.04k
static inline double az_adjustment(int triangle) {
267
9.04k
    if ((triangle >= 5 && triangle <= 9) || triangle == 15 || triangle == 16)
268
4.27k
        return M_PI;
269
4.76k
    else if (triangle >= 17)
270
0
        return -M_PI;
271
4.76k
    return 0;
272
9.04k
}
273
274
5.46k
static struct isea_pt isea_triangle_xy(int triangle) {
275
5.46k
    struct isea_pt c;
276
277
5.46k
    triangle %= numIcosahedronFaces;
278
279
5.46k
    c.x = TABLE_G * ((triangle % 5) - 2) * 2.0;
280
5.46k
    if (triangle > 9) {
281
1.88k
        c.x += TABLE_G;
282
1.88k
    }
283
284
    // REVIEW: This is likely related to
285
    //         pj_isea_data::yOffsets
286
5.46k
    switch (triangle / 5) {
287
1.64k
    case 0:
288
1.64k
        c.y = 5.0 * TABLE_H;
289
1.64k
        break;
290
1.93k
    case 1:
291
1.93k
        c.y = TABLE_H;
292
1.93k
        break;
293
1.88k
    case 2:
294
1.88k
        c.y = -TABLE_H;
295
1.88k
        break;
296
0
    case 3:
297
0
        c.y = -5.0 * TABLE_H;
298
0
        break;
299
0
    default:
300
        /* should be impossible */
301
0
        exit(EXIT_FAILURE);
302
5.46k
    }
303
5.46k
    c.x *= RprimeOverR;
304
5.46k
    c.y *= RprimeOverR;
305
306
5.46k
    return c;
307
5.46k
}
308
309
namespace { // anonymous namespace
310
311
class ISEAPlanarProjection;
312
313
struct pj_isea_data {
314
    double o_lat, o_lon, o_az; /* orientation, radians */
315
    int aperture;              /* valid values depend on partitioning method */
316
    int resolution;
317
    isea_address_form output; /* an isea_address_form */
318
    int triangle;             /* triangle of last transformed point */
319
    int quad;                 /* quad of last transformed point */
320
    isea_sincos vertexLatSinCos[numIcosahedronFaces];
321
322
    double R2;
323
    double Rprime;
324
    double Rprime2X;
325
    double RprimeTang;
326
    double Rprime2Tan2g;
327
    double triTang;
328
    double centerToBase;
329
    double triWidth;
330
    double yOffsets[4];
331
    double xo, yo;
332
    double sx, sy;
333
    ISEAPlanarProjection *p;
334
335
    void initialize(const PJ *P);
336
};
337
} // anonymous namespace
338
339
#ifdef _MSC_VER
340
#pragma warning(push)
341
/* disable unreachable code warning for return 0 */
342
#pragma warning(disable : 4702)
343
#endif
344
345
127k
#define SAFE_ARC_EPSILON 1E-15
346
347
5.46k
static inline double safeArcSin(double t) {
348
5.46k
    return fabs(t) < SAFE_ARC_EPSILON         ? 0
349
5.46k
           : fabs(t - 1.0) < SAFE_ARC_EPSILON ? M_PI / 2
350
5.46k
           : fabs(t + 1.0) < SAFE_ARC_EPSILON ? -M_PI / 2
351
5.46k
                                              : asin(t);
352
5.46k
}
353
354
36.9k
static inline double safeArcCos(double t) {
355
36.9k
    return fabs(t) < SAFE_ARC_EPSILON       ? M_PI / 2
356
36.9k
           : fabs(t + 1) < SAFE_ARC_EPSILON ? M_PI
357
36.9k
           : fabs(t - 1) < SAFE_ARC_EPSILON ? 0
358
36.9k
                                            : acos(t);
359
36.9k
}
360
361
#undef SAFE_ARC_EPSILON
362
363
/* coord needs to be in radians */
364
static int isea_snyder_forward(const struct pj_isea_data *data,
365
5.46k
                               const struct GeoPoint *ll, struct isea_pt *out) {
366
5.46k
    int i;
367
5.46k
    double sinLat = sin(ll->lat), cosLat = cos(ll->lat);
368
369
    /*
370
     * TODO by locality of reference, start by trying the same triangle
371
     * as last time
372
     */
373
36.9k
    for (i = 0; i < numIcosahedronFaces; i++) {
374
        /* additional variables from snyder */
375
36.9k
        double q, H, Ag, Azprime, Az, dprime, f, rho, x, y;
376
377
        /* variables used to store intermediate results */
378
36.9k
        double az_offset;
379
380
        /* how many multiples of 60 degrees we adjust the azimuth */
381
36.9k
        int Az_adjust_multiples;
382
383
36.9k
        const struct GeoPoint *center = &facesCenterDodecahedronVertices[i];
384
36.9k
        const struct isea_sincos *centerLatSinCos = &data->vertexLatSinCos[i];
385
36.9k
        double dLon = ll->lon - center->lon;
386
36.9k
        double cosLat_cosLon = cosLat * cos(dLon);
387
36.9k
        double cosZ =
388
36.9k
            centerLatSinCos->s * sinLat + centerLatSinCos->c * cosLat_cosLon;
389
36.9k
        double sinAz, cosAz;
390
391
        /* step 1 */
392
36.9k
        double z = safeArcCos(cosZ);
393
394
        /* not on this triangle */
395
36.9k
        if (z > sdc2vos /*g*/ + 0.000005) { /* TODO DBL_EPSILON */
396
27.9k
            continue;
397
27.9k
        }
398
399
        /* snyder eq 14 */
400
9.04k
        Az = atan2(cosLat * sin(dLon), centerLatSinCos->c * sinLat -
401
9.04k
                                           centerLatSinCos->s * cosLat_cosLon);
402
403
        /* step 2 */
404
405
        /* This calculates "some" vertex coordinate */
406
9.04k
        az_offset = az_adjustment(i);
407
408
9.04k
        Az -= az_offset;
409
410
        /* TODO I don't know why we do this.  It's not in snyder */
411
        /* maybe because we should have picked a better vertex */
412
9.04k
        if (Az < 0.0) {
413
6.76k
            Az += 2.0 * M_PI;
414
6.76k
        }
415
        /*
416
         * adjust Az for the point to fall within the range of 0 to
417
         * 2(90 - theta) or 60 degrees for the hexagon, by
418
         * and therefore 120 degrees for the triangle
419
         * of the icosahedron
420
         * subtracting or adding multiples of 60 degrees to Az and
421
         * recording the amount of adjustment
422
         */
423
424
9.04k
        Az_adjust_multiples = 0;
425
9.04k
        while (Az < 0.0) {
426
0
            Az += DEG120;
427
0
            Az_adjust_multiples--;
428
0
        }
429
18.0k
        while (Az > DEG120 + DBL_EPSILON) {
430
9.00k
            Az -= DEG120;
431
9.00k
            Az_adjust_multiples++;
432
9.00k
        }
433
434
        /* step 3 */
435
436
        /* Calculate q from eq 9. */
437
9.04k
        cosAz = cos(Az);
438
9.04k
        sinAz = sin(Az);
439
9.04k
        q = atan2(tang, cosAz + sinAz * cotTheta);
440
441
        /* not in this triangle */
442
9.04k
        if (z > q + 0.000005) {
443
3.58k
            continue;
444
3.58k
        }
445
        /* step 4 */
446
447
        /* Apply equations 5-8 and 10-12 in order */
448
449
        /* eq 5 */
450
        /* R' in the paper is for the truncated (icosahedron?) */
451
452
        /* eq 6 */
453
5.46k
        H = acos(sinAz * sinGcosSDC2VoS /* sin(G) * cos(g) */ - cosAz * cosG);
454
455
        /* eq 7 */
456
        /* Ag = (Az + G + H - DEG180) * M_PI * R * R / DEG180; */
457
5.46k
        Ag = Az + DEG_TO_RAD * 36 /* G */ + H - DEG180;
458
459
        /* eq 8 */
460
5.46k
        Azprime = atan2(2.0 * Ag, RprimeOverR * RprimeOverR * tang * tang -
461
5.46k
                                      2.0 * Ag * cotTheta);
462
463
        /* eq 10 */
464
        /* cot(theta) = 1.73205080756887729355 */
465
5.46k
        dprime = RprimeOverR * tang / (cos(Azprime) + sin(Azprime) * cotTheta);
466
467
        /* eq 11 */
468
5.46k
        f = dprime / (2.0 * RprimeOverR * sin(q / 2.0));
469
470
        /* eq 12 */
471
5.46k
        rho = 2.0 * RprimeOverR * f * sin(z / 2.0);
472
473
        /*
474
         * add back the same 60 degree multiple adjustment from step
475
         * 2 to Azprime
476
         */
477
478
5.46k
        Azprime += DEG120 * Az_adjust_multiples;
479
480
        /* calculate rectangular coordinates */
481
482
5.46k
        x = rho * sin(Azprime);
483
5.46k
        y = rho * cos(Azprime);
484
485
        /*
486
         * TODO
487
         * translate coordinates to the origin for the particular
488
         * hexagon on the flattened polyhedral map plot
489
         */
490
491
5.46k
        out->x = x;
492
5.46k
        out->y = y;
493
494
5.46k
        return i;
495
9.04k
    }
496
497
    /*
498
     * should be impossible, this implies that the coordinate is not on
499
     * any triangle
500
     */
501
502
0
    fprintf(stderr, "impossible transform: %f %f is not on any triangle\n",
503
0
            PJ_TODEG(ll->lon), PJ_TODEG(ll->lat));
504
505
0
    exit(EXIT_FAILURE);
506
5.46k
}
507
508
#ifdef _MSC_VER
509
#pragma warning(pop)
510
#endif
511
512
/*
513
 * return the new coordinates of any point in original coordinate system.
514
 * Define a point (newNPold) in original coordinate system as the North Pole in
515
 * new coordinate system, and the great circle connect the original and new
516
 * North Pole as the lon0 longitude in new coordinate system, given any point
517
 * in original coordinate system, this function return the new coordinates.
518
 */
519
520
/* formula from Snyder, Map Projections: A working manual, p31 */
521
/*
522
 * old north pole at np in new coordinates
523
 * could be simplified a bit with fewer intermediates
524
 *
525
 * TODO take a result pointer
526
 */
527
static struct GeoPoint snyder_ctran(const struct GeoPoint &np,
528
5.46k
                                    const struct GeoPoint &pt) {
529
5.46k
    struct GeoPoint result;
530
5.46k
    double phi = pt.lat, lambda = pt.lon;
531
5.46k
    double alpha = np.lat, beta = np.lon;
532
5.46k
    double dlambda = lambda - beta /* lambda0 */;
533
5.46k
    double cos_p = cos(phi), sin_p = sin(phi);
534
5.46k
    double cos_a = cos(alpha), sin_a = sin(alpha);
535
5.46k
    double cos_dlambda = cos(dlambda), sin_dlambda = sin(dlambda);
536
537
    /* mpawm 5-7 */
538
5.46k
    double sin_phip = sin_a * sin_p - cos_a * cos_p * cos_dlambda;
539
540
    /* mpawm 5-8b */
541
542
    /* use the two argument form so we end up in the right quadrant */
543
5.46k
    double lp_b = /* lambda prime minus beta */
544
5.46k
        atan2(cos_p * sin_dlambda, sin_a * cos_p * cos_dlambda + cos_a * sin_p);
545
5.46k
    double lambdap = lp_b + beta;
546
547
    /* normalize longitude */
548
    /* TODO can we just do a modulus ? */
549
5.46k
    lambdap = fmod(lambdap, 2 * M_PI);
550
10.5k
    while (lambdap > M_PI)
551
5.06k
        lambdap -= 2 * M_PI;
552
5.46k
    while (lambdap < -M_PI)
553
0
        lambdap += 2 * M_PI;
554
555
5.46k
    result.lat = safeArcSin(sin_phip);
556
5.46k
    result.lon = lambdap;
557
5.46k
    return result;
558
5.46k
}
559
560
static struct GeoPoint isea_ctran(const struct GeoPoint *np,
561
5.46k
                                  const struct GeoPoint *pt, double lon0) {
562
5.46k
    struct GeoPoint cnp = {np->lat, np->lon + M_PI};
563
5.46k
    struct GeoPoint npt = snyder_ctran(cnp, *pt);
564
565
5.46k
    npt.lon -= (/* M_PI */ -lon0 + np->lon);
566
    /*
567
     * snyder is down tri 3, isea is along side of tri1 from vertex 0 to
568
     * vertex 1 these are 180 degrees apart
569
     */
570
    // npt.lon += M_PI;
571
572
    /* normalize lon */
573
5.46k
    npt.lon = fmod(npt.lon, 2 * M_PI);
574
5.46k
    while (npt.lon > M_PI)
575
0
        npt.lon -= 2 * M_PI;
576
5.67k
    while (npt.lon < -M_PI)
577
210
        npt.lon += 2 * M_PI;
578
579
5.46k
    return npt;
580
5.46k
}
581
582
/* fuller's at 5.2454 west, 2.3009 N, adjacent at 7.46658 deg */
583
584
390
static int isea_grid_init(struct pj_isea_data *g) {
585
390
    int i;
586
587
390
    if (!g)
588
0
        return 0;
589
590
390
    g->o_lat = ISEA_STD_LAT;
591
390
    g->o_lon = ISEA_STD_LONG;
592
390
    g->o_az = 0.0;
593
390
    g->aperture = 4;
594
390
    g->resolution = 6;
595
596
8.19k
    for (i = 0; i < numIcosahedronFaces; i++) {
597
7.80k
        const GeoPoint *c = &facesCenterDodecahedronVertices[i];
598
7.80k
        g->vertexLatSinCos[i].s = sin(c->lat);
599
7.80k
        g->vertexLatSinCos[i].c = cos(c->lat);
600
7.80k
    }
601
390
    return 1;
602
390
}
603
604
0
static void isea_orient_isea(struct pj_isea_data *g) {
605
0
    if (!g)
606
0
        return;
607
0
    g->o_lat = ISEA_STD_LAT;
608
0
    g->o_lon = ISEA_STD_LONG;
609
0
    g->o_az = 0.0;
610
0
}
611
612
0
static void isea_orient_pole(struct pj_isea_data *g) {
613
0
    if (!g)
614
0
        return;
615
0
    g->o_lat = M_PI / 2.0;
616
0
    g->o_lon = 0.0;
617
0
    g->o_az = 0;
618
0
}
619
620
static int isea_transform(struct pj_isea_data *g, struct GeoPoint *in,
621
5.46k
                          struct isea_pt *out) {
622
5.46k
    struct GeoPoint i, pole;
623
5.46k
    int tri;
624
625
5.46k
    pole.lat = g->o_lat;
626
5.46k
    pole.lon = g->o_lon;
627
628
5.46k
    i = isea_ctran(&pole, in, g->o_az);
629
630
5.46k
    tri = isea_snyder_forward(g, &i, out);
631
5.46k
    g->triangle = tri;
632
633
5.46k
    return tri;
634
5.46k
}
635
636
5.46k
#define DOWNTRI(tri) ((tri / 5) % 2 == 1)
637
638
0
static void isea_rotate(struct isea_pt *pt, double degrees) {
639
0
    double rad;
640
641
0
    double x, y;
642
643
0
    rad = -degrees * M_PI / 180.0;
644
0
    while (rad >= 2.0 * M_PI)
645
0
        rad -= 2.0 * M_PI;
646
0
    while (rad <= -2.0 * M_PI)
647
0
        rad += 2.0 * M_PI;
648
649
0
    x = pt->x * cos(rad) + pt->y * sin(rad);
650
0
    y = -pt->x * sin(rad) + pt->y * cos(rad);
651
652
0
    pt->x = x;
653
0
    pt->y = y;
654
0
}
655
656
5.46k
static void isea_tri_plane(int tri, struct isea_pt *pt) {
657
5.46k
    struct isea_pt tc; /* center of triangle */
658
659
5.46k
    if (DOWNTRI(tri)) {
660
1.93k
        pt->x *= -1;
661
1.93k
        pt->y *= -1;
662
1.93k
    }
663
5.46k
    tc = isea_triangle_xy(tri);
664
5.46k
    pt->x += tc.x;
665
5.46k
    pt->y += tc.y;
666
5.46k
}
667
668
/* convert projected triangle coords to quad xy coords, return quad number */
669
0
static int isea_ptdd(int tri, struct isea_pt *pt) {
670
0
    int downtri, quadz;
671
672
0
    downtri = ((tri / 5) % 2 == 1);
673
0
    quadz = (tri % 5) + (tri / 10) * 5 + 1;
674
675
    // NOTE: This would always be a 60 degrees rotation if the flip were
676
    //       already done as in isea_tri_plane()
677
0
    isea_rotate(pt, downtri ? 240.0 : 60.0);
678
0
    if (downtri) {
679
0
        pt->x += 0.5;
680
        /* pt->y += cos(30.0 * M_PI / 180.0); */
681
0
        pt->y += cos30;
682
0
    }
683
0
    return quadz;
684
0
}
685
686
static int isea_dddi_ap3odd(struct pj_isea_data *g, int quadz,
687
0
                            struct isea_pt *pt, struct isea_pt *di) {
688
0
    struct isea_pt v;
689
0
    double hexwidth;
690
0
    double sidelength; /* in hexes */
691
0
    long d, i;
692
0
    long maxcoord;
693
0
    struct hex h;
694
695
    /* This is the number of hexes from apex to base of a triangle */
696
0
    sidelength = (pow(2.0, g->resolution) + 1.0) / 2.0;
697
698
    /* apex to base is cos(30deg) */
699
0
    hexwidth = cos(M_PI / 6.0) / sidelength;
700
701
    /* TODO I think sidelength is always x.5, so
702
     * (int)sidelength * 2 + 1 might be just as good
703
     */
704
0
    maxcoord = lround((sidelength * 2.0));
705
706
0
    v = *pt;
707
0
    hexbin2(hexwidth, v.x, v.y, &h.x, &h.y);
708
0
    h.iso = 0;
709
0
    hex_iso(&h);
710
711
0
    d = h.x - h.z;
712
0
    i = h.x + h.y + h.y;
713
714
    /*
715
     * you want to test for max coords for the next quad in the same
716
     * "row" first to get the case where both are max
717
     */
718
0
    if (quadz <= 5) {
719
0
        if (d == 0 && i == maxcoord) {
720
            /* north pole */
721
0
            quadz = 0;
722
0
            d = 0;
723
0
            i = 0;
724
0
        } else if (i == maxcoord) {
725
            /* upper right in next quad */
726
0
            quadz += 1;
727
0
            if (quadz == 6)
728
0
                quadz = 1;
729
0
            i = maxcoord - d;
730
0
            d = 0;
731
0
        } else if (d == maxcoord) {
732
            /* lower right in quad to lower right */
733
0
            quadz += 5;
734
0
            d = 0;
735
0
        }
736
0
    } else /* if (quadz >= 6) */ {
737
0
        if (i == 0 && d == maxcoord) {
738
            /* south pole */
739
0
            quadz = 11;
740
0
            d = 0;
741
0
            i = 0;
742
0
        } else if (d == maxcoord) {
743
            /* lower right in next quad */
744
0
            quadz += 1;
745
0
            if (quadz == 11)
746
0
                quadz = 6;
747
0
            d = maxcoord - i;
748
0
            i = 0;
749
0
        } else if (i == maxcoord) {
750
            /* upper right in quad to upper right */
751
0
            quadz = (quadz - 4) % 5;
752
0
            i = 0;
753
0
        }
754
0
    }
755
756
0
    di->x = d;
757
0
    di->y = i;
758
759
0
    g->quad = quadz;
760
0
    return quadz;
761
0
}
762
763
static int isea_dddi(struct pj_isea_data *g, int quadz, struct isea_pt *pt,
764
0
                     struct isea_pt *di) {
765
0
    struct isea_pt v;
766
0
    double hexwidth;
767
0
    long sidelength; /* in hexes */
768
0
    struct hex h;
769
770
0
    if (g->aperture == 3 && g->resolution % 2 != 0) {
771
0
        return isea_dddi_ap3odd(g, quadz, pt, di);
772
0
    }
773
    /* todo might want to do this as an iterated loop */
774
0
    if (g->aperture > 0) {
775
0
        double sidelengthDouble = pow(g->aperture, g->resolution / 2.0);
776
0
        if (fabs(sidelengthDouble) > std::numeric_limits<int>::max()) {
777
0
            throw "Integer overflow";
778
0
        }
779
0
        sidelength = lround(sidelengthDouble);
780
0
    } else {
781
0
        sidelength = g->resolution;
782
0
    }
783
784
0
    if (sidelength == 0) {
785
0
        throw "Division by zero";
786
0
    }
787
0
    hexwidth = 1.0 / sidelength;
788
789
0
    v = *pt;
790
0
    isea_rotate(&v, -30.0);
791
0
    hexbin2(hexwidth, v.x, v.y, &h.x, &h.y);
792
0
    h.iso = 0;
793
0
    hex_iso(&h);
794
795
    /* we may actually be on another quad */
796
0
    if (quadz <= 5) {
797
0
        if (h.x == 0 && h.z == -sidelength) {
798
            /* north pole */
799
0
            quadz = 0;
800
0
            h.z = 0;
801
0
            h.y = 0;
802
0
            h.x = 0;
803
0
        } else if (h.z == -sidelength) {
804
0
            quadz = quadz + 1;
805
0
            if (quadz == 6)
806
0
                quadz = 1;
807
0
            h.y = sidelength - h.x;
808
0
            h.z = h.x - sidelength;
809
0
            h.x = 0;
810
0
        } else if (h.x == sidelength) {
811
0
            quadz += 5;
812
0
            h.y = -h.z;
813
0
            h.x = 0;
814
0
        }
815
0
    } else /* if (quadz >= 6) */ {
816
0
        if (h.z == 0 && h.x == sidelength) {
817
            /* south pole */
818
0
            quadz = 11;
819
0
            h.x = 0;
820
0
            h.y = 0;
821
0
            h.z = 0;
822
0
        } else if (h.x == sidelength) {
823
0
            quadz = quadz + 1;
824
0
            if (quadz == 11)
825
0
                quadz = 6;
826
0
            h.x = h.y + sidelength;
827
0
            h.y = 0;
828
0
            h.z = -h.x;
829
0
        } else if (h.y == -sidelength) {
830
0
            quadz -= 4;
831
0
            h.y = 0;
832
0
            h.z = -h.x;
833
0
        }
834
0
    }
835
0
    di->x = h.x;
836
0
    di->y = -h.z;
837
838
0
    g->quad = quadz;
839
0
    return quadz;
840
0
}
841
842
static int isea_ptdi(struct pj_isea_data *g, int tri, struct isea_pt *pt,
843
0
                     struct isea_pt *di) {
844
0
    struct isea_pt v;
845
0
    int quadz;
846
847
0
    v = *pt;
848
0
    quadz = isea_ptdd(tri, &v);
849
0
    quadz = isea_dddi(g, quadz, &v, di);
850
0
    return quadz;
851
0
}
852
853
/* TODO just encode the quad in the d or i coordinate
854
 * quad is 0-11, which can be four bits.
855
 * d' = d << 4 + q, d = d' >> 4, q = d' & 0xf
856
 */
857
/* convert a q2di to global hex coord */
858
static int isea_hex(struct pj_isea_data *g, int tri, struct isea_pt *pt,
859
0
                    struct isea_pt *hex) {
860
0
    struct isea_pt v;
861
#ifdef FIXME
862
    long sidelength;
863
    long d, i, x, y;
864
#endif
865
0
    int quadz;
866
867
0
    quadz = isea_ptdi(g, tri, pt, &v);
868
869
0
    if (v.x < (INT_MIN >> 4) || v.x > (INT_MAX >> 4)) {
870
0
        throw "Invalid shift";
871
0
    }
872
0
    hex->x = ((int)v.x * 16) + quadz;
873
0
    hex->y = v.y;
874
875
0
    return 1;
876
#ifdef FIXME
877
    d = lround(floor(v.x));
878
    i = lround(floor(v.y));
879
880
    /* Aperture 3 odd resolutions */
881
    if (g->aperture == 3 && g->resolution % 2 != 0) {
882
        long offset = lround((pow(3.0, g->resolution - 1) + 0.5));
883
884
        d += offset * ((g->quadz - 1) % 5);
885
        i += offset * ((g->quadz - 1) % 5);
886
887
        if (quadz == 0) {
888
            d = 0;
889
            i = offset;
890
        } else if (quadz == 11) {
891
            d = 2 * offset;
892
            i = 0;
893
        } else if (quadz > 5) {
894
            d += offset;
895
        }
896
897
        x = (2 * d - i) / 3;
898
        y = (2 * i - d) / 3;
899
900
        hex->x = x + offset / 3;
901
        hex->y = y + 2 * offset / 3;
902
        return 1;
903
    }
904
905
    /* aperture 3 even resolutions and aperture 4 */
906
    sidelength = lround((pow(g->aperture, g->resolution / 2.0)));
907
    if (g->quad == 0) {
908
        hex->x = 0;
909
        hex->y = sidelength;
910
    } else if (g->quad == 11) {
911
        hex->x = sidelength * 2;
912
        hex->y = 0;
913
    } else {
914
        hex->x = d + sidelength * ((g->quad - 1) % 5);
915
        if (g->quad > 5)
916
            hex->x += sidelength;
917
        hex->y = i + sidelength * ((g->quad - 1) % 5);
918
    }
919
920
    return 1;
921
#endif
922
0
}
923
924
static struct isea_pt isea_forward(struct pj_isea_data *g,
925
5.46k
                                   struct GeoPoint *in) {
926
5.46k
    isea_pt out;
927
5.46k
    int tri = isea_transform(g, in, &out);
928
929
5.46k
    if (g->output == ISEA_PLANE)
930
5.46k
        isea_tri_plane(tri, &out);
931
0
    else {
932
0
        isea_pt coord;
933
934
        /* convert to isea standard triangle size */
935
0
        out.x *= ISEA_SCALE; // / g->radius;
936
0
        out.y *= ISEA_SCALE; // / g->radius;
937
0
        out.x += 0.5;
938
0
        out.y += 2.0 * .14433756729740644112;
939
940
0
        switch (g->output) {
941
0
        case ISEA_PLANE:
942
            /* already handled above -- GCC should not be complaining */
943
0
        case ISEA_Q2DD:
944
            /* Same as above, we just don't print as much */
945
0
            g->quad = isea_ptdd(tri, &out);
946
0
            break;
947
0
        case ISEA_Q2DI:
948
0
            g->quad = isea_ptdi(g, tri, &out, &coord);
949
0
            return coord;
950
0
        case ISEA_HEX:
951
0
            isea_hex(g, tri, &out, &coord);
952
0
            return coord;
953
0
        }
954
0
    }
955
5.46k
    return out;
956
5.46k
}
957
958
/*
959
 * Proj 4 integration code follows
960
 */
961
962
PROJ_HEAD(isea, "Icosahedral Snyder Equal Area") "\n\tSph";
963
964
5.46k
static PJ_XY isea_s_forward(PJ_LP lp, PJ *P) { /* Spheroidal, forward */
965
5.46k
    PJ_XY xy = {0.0, 0.0};
966
5.46k
    struct pj_isea_data *Q = static_cast<struct pj_isea_data *>(P->opaque);
967
5.46k
    struct isea_pt out;
968
5.46k
    struct GeoPoint in;
969
970
    //  TODO: Convert geodetic latitude to authalic latitude if not
971
    //        spherical as in eqearth, healpix, laea, etc.
972
5.46k
    in.lat = lp.phi;
973
5.46k
    in.lon = lp.lam;
974
975
5.46k
    try {
976
5.46k
        out = isea_forward(Q, &in);
977
5.46k
    } catch (const char *) {
978
0
        proj_errno_set(P, PROJ_ERR_COORD_TRANSFM_OUTSIDE_PROJECTION_DOMAIN);
979
0
        return proj_coord_error().xy;
980
0
    }
981
982
5.46k
    xy.x = out.x;
983
5.46k
    xy.y = out.y;
984
985
5.46k
    return xy;
986
5.46k
}
987
988
static PJ_LP isea_s_inverse(PJ_XY xy, PJ *P);
989
990
390
PJ *PJ_PROJECTION(isea) {
991
390
    char *opt;
992
390
    struct pj_isea_data *Q = static_cast<struct pj_isea_data *>(
993
390
        calloc(1, sizeof(struct pj_isea_data)));
994
390
    if (nullptr == Q)
995
0
        return pj_default_destructor(P, PROJ_ERR_OTHER /*ENOMEM*/);
996
390
    P->opaque = Q;
997
998
    // NOTE: if a inverse was needed, there is some material at
999
    // https://brsr.github.io/2021/08/31/snyder-equal-area.html
1000
390
    P->fwd = isea_s_forward;
1001
390
    P->inv = isea_s_inverse;
1002
390
    isea_grid_init(Q);
1003
390
    Q->output = ISEA_PLANE;
1004
1005
    /*      P->radius = P->a; / * otherwise defaults to 1 */
1006
    /* calling library will scale, I think */
1007
1008
390
    opt = pj_param(P->ctx, P->params, "sorient").s;
1009
390
    if (opt) {
1010
2
        if (!strcmp(opt, "isea")) {
1011
0
            isea_orient_isea(Q);
1012
2
        } else if (!strcmp(opt, "pole")) {
1013
0
            isea_orient_pole(Q);
1014
2
        } else {
1015
2
            proj_log_error(
1016
2
                P,
1017
2
                _("Invalid value for orient: only isea or pole are supported"));
1018
2
            return pj_default_destructor(P,
1019
2
                                         PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
1020
2
        }
1021
2
    }
1022
1023
388
    if (pj_param(P->ctx, P->params, "tazi").i) {
1024
3
        Q->o_az = pj_param(P->ctx, P->params, "razi").f;
1025
3
    }
1026
1027
388
    if (pj_param(P->ctx, P->params, "tlon_0").i) {
1028
97
        Q->o_lon = pj_param(P->ctx, P->params, "rlon_0").f;
1029
97
    }
1030
1031
388
    if (pj_param(P->ctx, P->params, "tlat_0").i) {
1032
108
        Q->o_lat = pj_param(P->ctx, P->params, "rlat_0").f;
1033
108
    }
1034
1035
388
    opt = pj_param(P->ctx, P->params, "smode").s;
1036
388
    if (opt) {
1037
10
        if (!strcmp(opt, "plane")) {
1038
0
            Q->output = ISEA_PLANE;
1039
10
        } else if (!strcmp(opt, "di")) {
1040
2
            Q->output = ISEA_Q2DI;
1041
8
        } else if (!strcmp(opt, "dd")) {
1042
5
            Q->output = ISEA_Q2DD;
1043
5
        } else if (!strcmp(opt, "hex")) {
1044
0
            Q->output = ISEA_HEX;
1045
3
        } else {
1046
3
            proj_log_error(P, _("Invalid value for mode: only plane, di, dd or "
1047
3
                                "hex are supported"));
1048
3
            return pj_default_destructor(P,
1049
3
                                         PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
1050
3
        }
1051
10
    }
1052
1053
    /* REVIEW: Was this an undocumented +rescale= parameter?
1054
    if (pj_param(P->ctx, P->params, "trescale").i) {
1055
        Q->radius = ISEA_SCALE;
1056
    }
1057
    */
1058
1059
385
    if (pj_param(P->ctx, P->params, "tresolution").i) {
1060
0
        Q->resolution = pj_param(P->ctx, P->params, "iresolution").i;
1061
385
    } else {
1062
385
        Q->resolution = 4;
1063
385
    }
1064
1065
385
    if (pj_param(P->ctx, P->params, "taperture").i) {
1066
0
        Q->aperture = pj_param(P->ctx, P->params, "iaperture").i;
1067
385
    } else {
1068
385
        Q->aperture = 3;
1069
385
    }
1070
1071
385
    Q->initialize(P);
1072
1073
385
    return P;
1074
388
}
1075
1076
0
#define Min std::min
1077
0
#define Max std::max
1078
1079
0
#define inf std::numeric_limits<double>::infinity()
1080
1081
// static define precision = DEG_TO_RAD * 1e-9;
1082
0
#define precision (DEG_TO_RAD * 1e-11)
1083
0
#define precisionPerDefinition (DEG_TO_RAD * 1e-5)
1084
1085
0
#define AzMax (DEG_TO_RAD * 120)
1086
1087
0
#define westVertexLon (DEG_TO_RAD * -144)
1088
1089
namespace { // anonymous namespace
1090
1091
struct ISEAFacePoint {
1092
    int face;
1093
    double x, y;
1094
};
1095
1096
class ISEAPlanarProjection {
1097
  public:
1098
    explicit ISEAPlanarProjection(const GeoPoint &value)
1099
4
        : orientation(value), cosOrientationLat(cos(value.lat)),
1100
4
          sinOrientationLat(sin(value.lat)) {}
1101
1102
    bool cartesianToGeo(const PJ_XY &inPosition, const pj_isea_data *params,
1103
0
                        GeoPoint &result) {
1104
0
        bool r = false;
1105
0
        static const double epsilon = 1E-11;
1106
0
        int face = 0;
1107
0
        PJ_XY position = inPosition;
1108
1109
0
#define sr -sin60 // sin(-60)
1110
0
#define cr 0.5    // cos(-60)
1111
0
        if (position.x < 0 ||
1112
0
            (position.x < params->triWidth / 2 && position.y < 0 &&
1113
0
             position.y * cr < position.x * sr))
1114
0
            position.x += 5 * params->triWidth; // Wrap around
1115
// Rotate and shear to determine face if not stored in position.z
1116
0
#define shearX (1.0 / SQRT3)
1117
0
        double yp = -(position.x * sr + position.y * cr);
1118
0
        double x =
1119
0
            (position.x * cr - position.y * sr + yp * shearX) * params->sx;
1120
0
        double y = yp * params->sy;
1121
0
#undef shearX
1122
0
#undef sr
1123
0
#undef cr
1124
1125
0
        if (x < 0 || (y > x && x < 5 - epsilon))
1126
0
            x += epsilon;
1127
0
        else if (x > 5 || (y < x && x > 0 + epsilon))
1128
0
            x -= epsilon;
1129
1130
0
        if (y < 0 || (x > y && y < 6 - epsilon))
1131
0
            y += epsilon;
1132
0
        else if (y > 6 || (x < y && y > 0 + epsilon))
1133
0
            y -= epsilon;
1134
1135
0
        if (x >= 0 && x <= 5 && y >= 0 && y <= 6) {
1136
0
            int ix = Max(0, Min(4, (int)x));
1137
0
            int iy = Max(0, Min(5, (int)y));
1138
1139
0
            if (iy == ix || iy == ix + 1) {
1140
0
                int rhombus = ix + iy;
1141
0
                bool top = x - ix > y - iy;
1142
0
                face = -1;
1143
1144
0
                switch (rhombus) {
1145
0
                case 0:
1146
0
                    face = top ? 0 : 5;
1147
0
                    break;
1148
0
                case 2:
1149
0
                    face = top ? 1 : 6;
1150
0
                    break;
1151
0
                case 4:
1152
0
                    face = top ? 2 : 7;
1153
0
                    break;
1154
0
                case 6:
1155
0
                    face = top ? 3 : 8;
1156
0
                    break;
1157
0
                case 8:
1158
0
                    face = top ? 4 : 9;
1159
0
                    break;
1160
1161
0
                case 1:
1162
0
                    face = top ? 10 : 15;
1163
0
                    break;
1164
0
                case 3:
1165
0
                    face = top ? 11 : 16;
1166
0
                    break;
1167
0
                case 5:
1168
0
                    face = top ? 12 : 17;
1169
0
                    break;
1170
0
                case 7:
1171
0
                    face = top ? 13 : 18;
1172
0
                    break;
1173
0
                case 9:
1174
0
                    face = top ? 14 : 19;
1175
0
                    break;
1176
0
                }
1177
0
                face++;
1178
0
            }
1179
0
        }
1180
1181
0
        if (face) {
1182
0
            int fy = (face - 1) / 5, fx = (face - 1) - 5 * fy;
1183
0
            double rx =
1184
0
                position.x - (2 * fx + fy / 2 + 1) * params->triWidth / 2;
1185
0
            double ry =
1186
0
                position.y - (params->yOffsets[fy] + 3 * params->centerToBase);
1187
0
            GeoPoint dst;
1188
1189
0
            r = icosahedronToSphere({face - 1, rx, ry}, params, dst);
1190
1191
0
            if (dst.lon < -M_PI - epsilon)
1192
0
                dst.lon += 2 * M_PI;
1193
0
            else if (dst.lon > M_PI + epsilon)
1194
0
                dst.lon -= 2 * M_PI;
1195
1196
0
            result = {dst.lat, dst.lon};
1197
0
        }
1198
0
        return r;
1199
0
    }
1200
1201
    // Converts coordinates on the icosahedron to geographic coordinates
1202
    // (inverse projection)
1203
    bool icosahedronToSphere(const ISEAFacePoint &c, const pj_isea_data *params,
1204
0
                             GeoPoint &r) {
1205
0
        if (c.face >= 0 && c.face < numIcosahedronFaces) {
1206
0
            double Az = atan2(c.x, c.y); // Az'
1207
0
            double rho = sqrt(c.x * c.x + c.y * c.y);
1208
0
            double AzAdjustment = faceOrientation(c.face);
1209
1210
0
            Az += AzAdjustment;
1211
0
            while (Az < 0) {
1212
0
                AzAdjustment += AzMax;
1213
0
                Az += AzMax;
1214
0
            }
1215
0
            while (Az > AzMax) {
1216
0
                AzAdjustment -= AzMax;
1217
0
                Az -= AzMax;
1218
0
            }
1219
0
            {
1220
0
                double sinAz = sin(Az), cosAz = cos(Az);
1221
0
                double cotAz = cosAz / sinAz;
1222
0
                double area = params->Rprime2Tan2g /
1223
0
                              (2 * (cotAz + cotTheta)); // A_G or A_{ABD}
1224
0
                double deltaAz = 10 * precision;
1225
0
                double degAreaOverR2Plus180Minus36 =
1226
0
                    area / params->R2 - westVertexLon;
1227
0
                double Az_earth = Az;
1228
1229
0
                while (fabs(deltaAz) > precision) {
1230
0
                    double sinAzEarth = sin(Az_earth),
1231
0
                           cosAzEarth = cos(Az_earth);
1232
0
                    double H =
1233
0
                        acos(sinAzEarth * sinGcosSDC2VoS - cosAzEarth * cosG);
1234
0
                    double FAz_earth = degAreaOverR2Plus180Minus36 - H -
1235
0
                                       Az_earth; // F(Az) or g(Az)
1236
0
                    double F2Az_earth =
1237
0
                        (cosAzEarth * sinGcosSDC2VoS + sinAzEarth * cosG) /
1238
0
                            sin(H) -
1239
0
                        1;                             // F'(Az) or g'(Az)
1240
0
                    deltaAz = -FAz_earth / F2Az_earth; // Delta Az^0 or Delta Az
1241
0
                    Az_earth += deltaAz;
1242
0
                }
1243
0
                {
1244
0
                    double sinAz_earth = sin(Az_earth),
1245
0
                           cosAz_earth = cos(Az_earth);
1246
0
                    double q =
1247
0
                        atan2(tang, (cosAz_earth + sinAz_earth * cotTheta));
1248
0
                    double d =
1249
0
                        params->RprimeTang / (cosAz + sinAz * cotTheta); // d'
1250
0
                    double f = d / (params->Rprime2X * sin(q / 2));      // f
1251
0
                    double z = 2 * asin(rho / (params->Rprime2X * f));
1252
1253
0
                    Az_earth -= AzAdjustment;
1254
0
                    {
1255
0
                        const isea_sincos *latSinCos =
1256
0
                            &params->vertexLatSinCos[c.face];
1257
0
                        double sinLat0 = latSinCos->s, cosLat0 = latSinCos->c;
1258
0
                        double sinZ = sin(z), cosZ = cos(z);
1259
0
                        double cosLat0SinZ = cosLat0 * sinZ;
1260
0
                        double latSin =
1261
0
                            sinLat0 * cosZ + cosLat0SinZ * cos(Az_earth);
1262
0
                        double lat = safeArcSin(latSin);
1263
0
                        double lon =
1264
0
                            facesCenterDodecahedronVertices[c.face].lon +
1265
0
                            atan2(sin(Az_earth) * cosLat0SinZ,
1266
0
                                  cosZ - sinLat0 * sin(lat));
1267
1268
0
                        revertOrientation({lat, lon}, r);
1269
0
                    }
1270
0
                }
1271
0
            }
1272
0
            return true;
1273
0
        }
1274
0
        r = {inf, inf};
1275
0
        return false;
1276
0
    }
1277
1278
  private:
1279
    GeoPoint orientation;
1280
    double cosOrientationLat, sinOrientationLat;
1281
1282
0
    inline void revertOrientation(const GeoPoint &c, GeoPoint &r) {
1283
0
        double lon = (c.lat < DEG_TO_RAD * -90 + precisionPerDefinition ||
1284
0
                      c.lat > DEG_TO_RAD * 90 - precisionPerDefinition)
1285
0
                         ? 0
1286
0
                         : c.lon;
1287
0
        if (orientation.lat != 0.0 || orientation.lon != 0.0) {
1288
0
            double sinLat = sin(c.lat), cosLat = cos(c.lat);
1289
0
            double sinLon = sin(lon), cosLon = cos(lon);
1290
0
            double cosLonCosLat = cosLon * cosLat;
1291
0
            r = {asin(sinLat * cosOrientationLat -
1292
0
                      cosLonCosLat * sinOrientationLat),
1293
0
                 atan2(sinLon * cosLat, cosLonCosLat * cosOrientationLat +
1294
0
                                            sinLat * sinOrientationLat) -
1295
0
                     orientation.lon};
1296
0
        } else
1297
0
            r = {c.lat, lon};
1298
0
    }
1299
1300
0
    static inline double faceOrientation(int face) {
1301
0
        return (face <= 4 || (10 <= face && face <= 14)) ? 0 : DEG_TO_RAD * 180;
1302
0
    }
1303
};
1304
1305
// Orientation symmetric to equator (+proj=isea)
1306
/* Sets the orientation of the icosahedron such that the north and the south
1307
 * poles are mapped to the edge midpoints of the icosahedron. The equator is
1308
 * thus mapped symmetrically.
1309
 */
1310
static ISEAPlanarProjection standardISEA(
1311
    /* DEG_TO_RAD * (90 - 58.282525589) = 31.7174744114613 */
1312
    {(E_RAD + F_RAD) / 2, DEG_TO_RAD * -11.25});
1313
1314
// Polar orientation (+proj=isea +orient=pole)
1315
/*
1316
 * One corner of the icosahedron is, by default, facing to the north pole, and
1317
 * one to the south pole. The provided orientation is relative to the default
1318
 * orientation.
1319
 *
1320
 * The orientation shifts every location by the angle orientation.lon in
1321
 * direction of positive longitude, and thereafter by the angle orientation.lat
1322
 * in direction of positive latitude.
1323
 */
1324
static ISEAPlanarProjection polarISEA({0, 0});
1325
1326
385
void pj_isea_data::initialize(const PJ *P) {
1327
385
    struct pj_isea_data *Q = static_cast<struct pj_isea_data *>(P->opaque);
1328
    // Only supporting default planar options for now
1329
385
    if (Q->output == ISEA_PLANE && Q->o_az == 0.0 && Q->aperture == 3.0 &&
1330
378
        Q->resolution == 4.) {
1331
        // Only supporting +orient=isea and +orient=pole for now
1332
378
        if (Q->o_lat == ISEA_STD_LAT && Q->o_lon == ISEA_STD_LONG)
1333
223
            p = &standardISEA;
1334
155
        else if (Q->o_lat == M_PI / 2.0 && Q->o_lon == 0)
1335
41
            p = &polarISEA;
1336
114
        else
1337
114
            p = nullptr;
1338
378
    }
1339
1340
385
    if (p != nullptr) {
1341
264
        if (P->e > 0) {
1342
170
            double a2 = P->a * P->a, c2 = P->b * P->b;
1343
170
            double log1pe_1me = log((1 + P->e) / (1 - P->e));
1344
170
            double S = M_PI * (2 * a2 + c2 / P->e * log1pe_1me);
1345
170
            R2 = S / (4 * M_PI);             // [WGS84] R = 6371007.1809184747 m
1346
170
            Rprime = RprimeOverR * sqrt(R2); // R'
1347
170
        } else {
1348
94
            R2 = P->a * P->a;            // R^2
1349
94
            Rprime = RprimeOverR * P->a; // R'
1350
94
        }
1351
1352
264
        Rprime2X = 2 * Rprime;
1353
264
        RprimeTang = Rprime * tang; // twice the center-to-base distance
1354
264
        centerToBase = RprimeTang / 2;
1355
264
        triWidth = RprimeTang * SQRT3;
1356
264
        Rprime2Tan2g = RprimeTang * RprimeTang;
1357
1358
264
        yOffsets[0] = -2 * centerToBase;
1359
264
        yOffsets[1] = -4 * centerToBase;
1360
264
        yOffsets[2] = -5 * centerToBase;
1361
264
        yOffsets[3] = -7 * centerToBase;
1362
1363
264
        xo = 2.5 * triWidth;
1364
264
        yo = -1.5 * centerToBase;
1365
264
        sx = 1.0 / triWidth;
1366
264
        sy = 1.0 / (3 * centerToBase);
1367
264
    }
1368
385
}
1369
1370
} // anonymous namespace
1371
1372
0
static PJ_LP isea_s_inverse(PJ_XY xy, PJ *P) {
1373
0
    const struct pj_isea_data *Q =
1374
0
        static_cast<struct pj_isea_data *>(P->opaque);
1375
0
    ISEAPlanarProjection *p = Q->p;
1376
1377
0
    if (p) {
1378
        // Default origin of +proj=isea is different (OGC:1534 is
1379
        // +x_0=19186144.870934911 +y_0=-3323137.7717836285)
1380
0
        PJ_XY input{xy.x * P->a + Q->xo, xy.y * P->a + Q->yo};
1381
0
        GeoPoint result;
1382
1383
0
        if (p->cartesianToGeo(input, Q, result))
1384
            //  TODO: Convert authalic latitude to geodetic latitude if not
1385
            //        spherical as in eqearth, healpix, laea, etc.
1386
0
            return {result.lon, result.lat};
1387
0
        else
1388
0
            return {inf, inf};
1389
0
    } else
1390
0
        return {inf, inf};
1391
0
}
1392
1393
#undef ISEA_STD_LAT
1394
#undef ISEA_STD_LONG
1395
1396
#undef numIcosahedronFaces
1397
#undef precision
1398
#undef precisionPerDefinition
1399
1400
#undef AzMax
1401
#undef sdc2vos
1402
#undef tang
1403
#undef cotTheta
1404
#undef cosG
1405
#undef sinGcosSDC2VoS
1406
#undef westVertexLon
1407
1408
#undef RprimeOverR
1409
1410
#undef Min
1411
#undef Max
1412
1413
#undef inf
1414
1415
#undef E_RAD
1416
#undef F_RAD
1417
1418
#undef DEG36
1419
#undef DEG72
1420
#undef DEG90
1421
#undef DEG108
1422
#undef DEG120
1423
#undef DEG144
1424
#undef DEG180
1425
#undef ISEA_SCALE
1426
#undef V_LAT
1427
#undef TABLE_G
1428
#undef TABLE_H