Coverage Report

Created: 2025-06-13 06:29

/src/proj/src/projections/isea.cpp
Line
Count
Source (jump to first uncovered line)
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
0
#define DEG120 2.09439510239319549229
76
#define DEG144 2.51327412287183459075
77
0
#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
0
#define sdc2vos 0.6523581397843681859886783
100
101
332
#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
0
#define tan30 0.57735026918962576450914878 // tan(DEG_TO_RAD * 30)
107
0
#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
0
#define cosG 0.80901699437494742410229341718281905886
113
//    sin(DEG_TO_RAD * 36)
114
0
#define sinG 0.587785252292473129168705954639072768597652
115
//    cos(g)
116
0
#define cosSDC2VoS 0.7946544722917661229596057297879189448539
117
118
0
#define sinGcosSDC2VoS (sinG * cosSDC2VoS) // sin G cos g
119
120
332
#define SQRT3 1.73205080756887729352744634150587236694280525381038
121
0
#define sin60 (SQRT3 / 2.0)
122
0
#define cos30 (SQRT3 / 2.0)
123
124
// tang * sin(60 deg)
125
0
#define TABLE_G (tang * sin60)
126
127
// (1 / (2 * sqrt(5)) + 1 / 6.0) * sqrt(M_PI * sqrt(3))
128
332
#define RprimeOverR 0.9103832815095032 // R' / R
129
130
/* H = 0.25 R tan g = */
131
0
#define TABLE_H (0.25 * tang)
132
133
/* in radians */
134
1.51k
#define ISEA_STD_LAT 1.01722196792335072101
135
841
#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
11.2k
#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
0
static inline double az_adjustment(int triangle) {
267
0
    if ((triangle >= 5 && triangle <= 9) || triangle == 15 || triangle == 16)
268
0
        return M_PI;
269
0
    else if (triangle >= 17)
270
0
        return -M_PI;
271
0
    return 0;
272
0
}
273
274
0
static struct isea_pt isea_triangle_xy(int triangle) {
275
0
    struct isea_pt c;
276
277
0
    triangle %= numIcosahedronFaces;
278
279
0
    c.x = TABLE_G * ((triangle % 5) - 2) * 2.0;
280
0
    if (triangle > 9) {
281
0
        c.x += TABLE_G;
282
0
    }
283
284
    // REVIEW: This is likely related to
285
    //         pj_isea_data::yOffsets
286
0
    switch (triangle / 5) {
287
0
    case 0:
288
0
        c.y = 5.0 * TABLE_H;
289
0
        break;
290
0
    case 1:
291
0
        c.y = TABLE_H;
292
0
        break;
293
0
    case 2:
294
0
        c.y = -TABLE_H;
295
0
        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
0
    }
303
0
    c.x *= RprimeOverR;
304
0
    c.y *= RprimeOverR;
305
306
0
    return c;
307
0
}
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
0
#define SAFE_ARC_EPSILON 1E-15
346
347
0
static inline double safeArcSin(double t) {
348
0
    return fabs(t) < SAFE_ARC_EPSILON         ? 0
349
0
           : fabs(t - 1.0) < SAFE_ARC_EPSILON ? M_PI / 2
350
0
           : fabs(t + 1.0) < SAFE_ARC_EPSILON ? -M_PI / 2
351
0
                                              : asin(t);
352
0
}
353
354
0
static inline double safeArcCos(double t) {
355
0
    return fabs(t) < SAFE_ARC_EPSILON       ? M_PI / 2
356
0
           : fabs(t + 1) < SAFE_ARC_EPSILON ? M_PI
357
0
           : fabs(t - 1) < SAFE_ARC_EPSILON ? 0
358
0
                                            : acos(t);
359
0
}
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
0
                               const struct GeoPoint *ll, struct isea_pt *out) {
366
0
    int i;
367
0
    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
0
    for (i = 0; i < numIcosahedronFaces; i++) {
374
        /* additional variables from snyder */
375
0
        double q, H, Ag, Azprime, Az, dprime, f, rho, x, y;
376
377
        /* variables used to store intermediate results */
378
0
        double az_offset;
379
380
        /* how many multiples of 60 degrees we adjust the azimuth */
381
0
        int Az_adjust_multiples;
382
383
0
        const struct GeoPoint *center = &facesCenterDodecahedronVertices[i];
384
0
        const struct isea_sincos *centerLatSinCos = &data->vertexLatSinCos[i];
385
0
        double dLon = ll->lon - center->lon;
386
0
        double cosLat_cosLon = cosLat * cos(dLon);
387
0
        double cosZ =
388
0
            centerLatSinCos->s * sinLat + centerLatSinCos->c * cosLat_cosLon;
389
0
        double sinAz, cosAz;
390
391
        /* step 1 */
392
0
        double z = safeArcCos(cosZ);
393
394
        /* not on this triangle */
395
0
        if (z > sdc2vos /*g*/ + 0.000005) { /* TODO DBL_EPSILON */
396
0
            continue;
397
0
        }
398
399
        /* snyder eq 14 */
400
0
        Az = atan2(cosLat * sin(dLon), centerLatSinCos->c * sinLat -
401
0
                                           centerLatSinCos->s * cosLat_cosLon);
402
403
        /* step 2 */
404
405
        /* This calculates "some" vertex coordinate */
406
0
        az_offset = az_adjustment(i);
407
408
0
        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
0
        if (Az < 0.0) {
413
0
            Az += 2.0 * M_PI;
414
0
        }
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
0
        Az_adjust_multiples = 0;
425
0
        while (Az < 0.0) {
426
0
            Az += DEG120;
427
0
            Az_adjust_multiples--;
428
0
        }
429
0
        while (Az > DEG120 + DBL_EPSILON) {
430
0
            Az -= DEG120;
431
0
            Az_adjust_multiples++;
432
0
        }
433
434
        /* step 3 */
435
436
        /* Calculate q from eq 9. */
437
0
        cosAz = cos(Az);
438
0
        sinAz = sin(Az);
439
0
        q = atan2(tang, cosAz + sinAz * cotTheta);
440
441
        /* not in this triangle */
442
0
        if (z > q + 0.000005) {
443
0
            continue;
444
0
        }
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
0
        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
0
        Ag = Az + DEG_TO_RAD * 36 /* G */ + H - DEG180;
458
459
        /* eq 8 */
460
0
        Azprime = atan2(2.0 * Ag, RprimeOverR * RprimeOverR * tang * tang -
461
0
                                      2.0 * Ag * cotTheta);
462
463
        /* eq 10 */
464
        /* cot(theta) = 1.73205080756887729355 */
465
0
        dprime = RprimeOverR * tang / (cos(Azprime) + sin(Azprime) * cotTheta);
466
467
        /* eq 11 */
468
0
        f = dprime / (2.0 * RprimeOverR * sin(q / 2.0));
469
470
        /* eq 12 */
471
0
        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
0
        Azprime += DEG120 * Az_adjust_multiples;
479
480
        /* calculate rectangular coordinates */
481
482
0
        x = rho * sin(Azprime);
483
0
        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
0
        out->x = x;
492
0
        out->y = y;
493
494
0
        return i;
495
0
    }
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
0
}
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
0
                                    const struct GeoPoint &pt) {
529
0
    struct GeoPoint result;
530
0
    double phi = pt.lat, lambda = pt.lon;
531
0
    double alpha = np.lat, beta = np.lon;
532
0
    double dlambda = lambda - beta /* lambda0 */;
533
0
    double cos_p = cos(phi), sin_p = sin(phi);
534
0
    double cos_a = cos(alpha), sin_a = sin(alpha);
535
0
    double cos_dlambda = cos(dlambda), sin_dlambda = sin(dlambda);
536
537
    /* mpawm 5-7 */
538
0
    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
0
    double lp_b = /* lambda prime minus beta */
544
0
        atan2(cos_p * sin_dlambda, sin_a * cos_p * cos_dlambda + cos_a * sin_p);
545
0
    double lambdap = lp_b + beta;
546
547
    /* normalize longitude */
548
    /* TODO can we just do a modulus ? */
549
0
    lambdap = fmod(lambdap, 2 * M_PI);
550
0
    while (lambdap > M_PI)
551
0
        lambdap -= 2 * M_PI;
552
0
    while (lambdap < -M_PI)
553
0
        lambdap += 2 * M_PI;
554
555
0
    result.lat = safeArcSin(sin_phip);
556
0
    result.lon = lambdap;
557
0
    return result;
558
0
}
559
560
static struct GeoPoint isea_ctran(const struct GeoPoint *np,
561
0
                                  const struct GeoPoint *pt, double lon0) {
562
0
    struct GeoPoint cnp = {np->lat, np->lon + M_PI};
563
0
    struct GeoPoint npt = snyder_ctran(cnp, *pt);
564
565
0
    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
0
    npt.lon = fmod(npt.lon, 2 * M_PI);
574
0
    while (npt.lon > M_PI)
575
0
        npt.lon -= 2 * M_PI;
576
0
    while (npt.lon < -M_PI)
577
0
        npt.lon += 2 * M_PI;
578
579
0
    return npt;
580
0
}
581
582
/* fuller's at 5.2454 west, 2.3009 N, adjacent at 7.46658 deg */
583
584
536
static int isea_grid_init(struct pj_isea_data *g) {
585
536
    int i;
586
587
536
    if (!g)
588
0
        return 0;
589
590
536
    g->o_lat = ISEA_STD_LAT;
591
536
    g->o_lon = ISEA_STD_LONG;
592
536
    g->o_az = 0.0;
593
536
    g->aperture = 4;
594
536
    g->resolution = 6;
595
596
11.2k
    for (i = 0; i < numIcosahedronFaces; i++) {
597
10.7k
        const GeoPoint *c = &facesCenterDodecahedronVertices[i];
598
10.7k
        g->vertexLatSinCos[i].s = sin(c->lat);
599
10.7k
        g->vertexLatSinCos[i].c = cos(c->lat);
600
10.7k
    }
601
536
    return 1;
602
536
}
603
604
3
static void isea_orient_isea(struct pj_isea_data *g) {
605
3
    if (!g)
606
0
        return;
607
3
    g->o_lat = ISEA_STD_LAT;
608
3
    g->o_lon = ISEA_STD_LONG;
609
3
    g->o_az = 0.0;
610
3
}
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
0
                          struct isea_pt *out) {
622
0
    struct GeoPoint i, pole;
623
0
    int tri;
624
625
0
    pole.lat = g->o_lat;
626
0
    pole.lon = g->o_lon;
627
628
0
    i = isea_ctran(&pole, in, g->o_az);
629
630
0
    tri = isea_snyder_forward(g, &i, out);
631
0
    g->triangle = tri;
632
633
0
    return tri;
634
0
}
635
636
0
#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
0
static void isea_tri_plane(int tri, struct isea_pt *pt) {
657
0
    struct isea_pt tc; /* center of triangle */
658
659
0
    if (DOWNTRI(tri)) {
660
0
        pt->x *= -1;
661
0
        pt->y *= -1;
662
0
    }
663
0
    tc = isea_triangle_xy(tri);
664
0
    pt->x += tc.x;
665
0
    pt->y += tc.y;
666
0
}
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
0
                                   struct GeoPoint *in) {
926
0
    isea_pt out;
927
0
    int tri = isea_transform(g, in, &out);
928
929
0
    if (g->output == ISEA_PLANE)
930
0
        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
0
    return out;
956
0
}
957
958
/*
959
 * Proj 4 integration code follows
960
 */
961
962
PROJ_HEAD(isea, "Icosahedral Snyder Equal Area") "\n\tSph";
963
964
0
static PJ_XY isea_s_forward(PJ_LP lp, PJ *P) { /* Spheroidal, forward */
965
0
    PJ_XY xy = {0.0, 0.0};
966
0
    struct pj_isea_data *Q = static_cast<struct pj_isea_data *>(P->opaque);
967
0
    struct isea_pt out;
968
0
    struct GeoPoint in;
969
970
    //  TODO: Convert geodetic latitude to authalic latitude if not
971
    //        spherical as in eqearth, healpix, laea, etc.
972
0
    in.lat = lp.phi;
973
0
    in.lon = lp.lam;
974
975
0
    try {
976
0
        out = isea_forward(Q, &in);
977
0
    } 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
0
    xy.x = out.x;
983
0
    xy.y = out.y;
984
985
0
    return xy;
986
0
}
987
988
static PJ_LP isea_s_inverse(PJ_XY xy, PJ *P);
989
990
536
PJ *PJ_PROJECTION(isea) {
991
536
    char *opt;
992
536
    struct pj_isea_data *Q = static_cast<struct pj_isea_data *>(
993
536
        calloc(1, sizeof(struct pj_isea_data)));
994
536
    if (nullptr == Q)
995
0
        return pj_default_destructor(P, PROJ_ERR_OTHER /*ENOMEM*/);
996
536
    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
536
    P->fwd = isea_s_forward;
1001
536
    P->inv = isea_s_inverse;
1002
536
    isea_grid_init(Q);
1003
536
    Q->output = ISEA_PLANE;
1004
1005
    /*      P->radius = P->a; / * otherwise defaults to 1 */
1006
    /* calling library will scale, I think */
1007
1008
536
    opt = pj_param(P->ctx, P->params, "sorient").s;
1009
536
    if (opt) {
1010
6
        if (!strcmp(opt, "isea")) {
1011
3
            isea_orient_isea(Q);
1012
3
        } else if (!strcmp(opt, "pole")) {
1013
0
            isea_orient_pole(Q);
1014
3
        } else {
1015
3
            proj_log_error(
1016
3
                P,
1017
3
                _("Invalid value for orient: only isea or pole are supported"));
1018
3
            return pj_default_destructor(P,
1019
3
                                         PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
1020
3
        }
1021
6
    }
1022
1023
533
    if (pj_param(P->ctx, P->params, "tazi").i) {
1024
19
        Q->o_az = pj_param(P->ctx, P->params, "razi").f;
1025
19
    }
1026
1027
533
    if (pj_param(P->ctx, P->params, "tlon_0").i) {
1028
198
        Q->o_lon = pj_param(P->ctx, P->params, "rlon_0").f;
1029
198
    }
1030
1031
533
    if (pj_param(P->ctx, P->params, "tlat_0").i) {
1032
185
        Q->o_lat = pj_param(P->ctx, P->params, "rlat_0").f;
1033
185
    }
1034
1035
533
    opt = pj_param(P->ctx, P->params, "smode").s;
1036
533
    if (opt) {
1037
46
        if (!strcmp(opt, "plane")) {
1038
3
            Q->output = ISEA_PLANE;
1039
43
        } else if (!strcmp(opt, "di")) {
1040
14
            Q->output = ISEA_Q2DI;
1041
29
        } else if (!strcmp(opt, "dd")) {
1042
6
            Q->output = ISEA_Q2DD;
1043
23
        } else if (!strcmp(opt, "hex")) {
1044
20
            Q->output = ISEA_HEX;
1045
20
        } 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
46
    }
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
530
    if (pj_param(P->ctx, P->params, "tresolution").i) {
1060
0
        Q->resolution = pj_param(P->ctx, P->params, "iresolution").i;
1061
530
    } else {
1062
530
        Q->resolution = 4;
1063
530
    }
1064
1065
530
    if (pj_param(P->ctx, P->params, "taperture").i) {
1066
3
        Q->aperture = pj_param(P->ctx, P->params, "iaperture").i;
1067
527
    } else {
1068
527
        Q->aperture = 3;
1069
527
    }
1070
1071
530
    Q->initialize(P);
1072
1073
530
    return P;
1074
533
}
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
530
void pj_isea_data::initialize(const PJ *P) {
1327
530
    struct pj_isea_data *Q = static_cast<struct pj_isea_data *>(P->opaque);
1328
    // Only supporting default planar options for now
1329
530
    if (Q->output == ISEA_PLANE && Q->o_az == 0.0 && Q->aperture == 3.0 &&
1330
530
        Q->resolution == 4.) {
1331
        // Only supporting +orient=isea and +orient=pole for now
1332
487
        if (Q->o_lat == ISEA_STD_LAT && Q->o_lon == ISEA_STD_LONG)
1333
280
            p = &standardISEA;
1334
207
        else if (Q->o_lat == M_PI / 2.0 && Q->o_lon == 0)
1335
52
            p = &polarISEA;
1336
155
        else
1337
155
            p = nullptr;
1338
487
    }
1339
1340
530
    if (p != nullptr) {
1341
332
        if (P->e > 0) {
1342
314
            double a2 = P->a * P->a, c2 = P->b * P->b;
1343
314
            double log1pe_1me = log((1 + P->e) / (1 - P->e));
1344
314
            double S = M_PI * (2 * a2 + c2 / P->e * log1pe_1me);
1345
314
            R2 = S / (4 * M_PI);             // [WGS84] R = 6371007.1809184747 m
1346
314
            Rprime = RprimeOverR * sqrt(R2); // R'
1347
314
        } else {
1348
18
            R2 = P->a * P->a;            // R^2
1349
18
            Rprime = RprimeOverR * P->a; // R'
1350
18
        }
1351
1352
332
        Rprime2X = 2 * Rprime;
1353
332
        RprimeTang = Rprime * tang; // twice the center-to-base distance
1354
332
        centerToBase = RprimeTang / 2;
1355
332
        triWidth = RprimeTang * SQRT3;
1356
332
        Rprime2Tan2g = RprimeTang * RprimeTang;
1357
1358
332
        yOffsets[0] = -2 * centerToBase;
1359
332
        yOffsets[1] = -4 * centerToBase;
1360
332
        yOffsets[2] = -5 * centerToBase;
1361
332
        yOffsets[3] = -7 * centerToBase;
1362
1363
332
        xo = 2.5 * triWidth;
1364
332
        yo = -1.5 * centerToBase;
1365
332
        sx = 1.0 / triWidth;
1366
332
        sy = 1.0 / (3 * centerToBase);
1367
332
    }
1368
530
}
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