/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 | ¶ms->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 |