/src/PROJ/src/projections/polyhedral/snyder.h
Line | Count | Source |
1 | | /****************************************************************************** |
2 | | * |
3 | | * Project: PROJ |
4 | | * Purpose: Snyder equal-area polyhedral projection — forward and inverse. |
5 | | * Author: Felix Palmer |
6 | | * |
7 | | ****************************************************************************** |
8 | | * Derived from A5 (Apache-2.0). |
9 | | * https://github.com/felixpalmer/a5 |
10 | | * (modules/projections/polyhedral.ts) |
11 | | * |
12 | | * Copyright (c) A5 contributors |
13 | | * |
14 | | * Licensed under the Apache License, Version 2.0 (the "License"); |
15 | | * you may not use this file except in compliance with the License. |
16 | | * You may obtain a copy of the License at |
17 | | * |
18 | | * http://www.apache.org/licenses/LICENSE-2.0 |
19 | | * |
20 | | * Unless required by applicable law or agreed to in writing, software |
21 | | * distributed under the License is distributed on an "AS IS" BASIS, |
22 | | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
23 | | * See the License for the specific language governing permissions and |
24 | | * limitations under the License. |
25 | | **************************************************************************** |
26 | | * Derived from DGGAL (BSD 3-Clause License) |
27 | | * |
28 | | * Copyright (c) 2014-2025, Ecere Corporation |
29 | | * |
30 | | * Redistribution and use in source and binary forms, with or without |
31 | | * modification, are permitted provided that the following conditions are met: |
32 | | * |
33 | | * 1. Redistributions of source code must retain the above copyright |
34 | | * notice, this list of conditions and the following disclaimer. |
35 | | * |
36 | | * 2. Redistributions in binary form must reproduce the above copyright notice, |
37 | | * this list of conditions and the following disclaimer in the documentation |
38 | | * and/or other materials provided with the distribution. |
39 | | * |
40 | | * 3. Neither the name of the copyright holder nor the names of its |
41 | | * contributors may be used to endorse or promote products derived from |
42 | | * this software without specific prior written permission. |
43 | | * |
44 | | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
45 | | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
46 | | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS |
47 | | * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE |
48 | | * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, |
49 | | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, |
50 | | * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
51 | | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
52 | | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT |
53 | | * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN |
54 | | * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
55 | | * POSSIBILITY OF SUCH DAMAGE. |
56 | | **************************************************************************** |
57 | | * |
58 | | * Closed-form equations adapted from: |
59 | | * https://brsr.github.io/2021/08/31/snyder-equal-area.html |
60 | | * |
61 | | ****************************************************************************/ |
62 | | |
63 | | #ifndef SNYDER_H |
64 | | #define SNYDER_H |
65 | | |
66 | | #include "sphere.h" |
67 | | #include "vec3.h" |
68 | | |
69 | | namespace polyhedral { |
70 | | |
71 | | struct Face2D { |
72 | | double x = 0.0, y = 0.0; |
73 | | }; |
74 | | |
75 | | struct Barycentric { |
76 | | double u = 0.0, v = 0.0, w = 0.0; |
77 | | }; |
78 | | |
79 | | struct FaceTriangle { |
80 | | Face2D a, b, c; |
81 | | }; |
82 | | |
83 | | struct SphericalTriangle { |
84 | | Vec3 a, b, c; |
85 | | }; |
86 | | |
87 | 0 | inline Barycentric face_to_barycentric(Face2D p, const FaceTriangle &tri) { |
88 | 0 | double d31[2] = {tri.a.x - tri.c.x, tri.a.y - tri.c.y}; |
89 | 0 | double d23[2] = {tri.c.x - tri.b.x, tri.c.y - tri.b.y}; |
90 | 0 | double d3p[2] = {p.x - tri.c.x, p.y - tri.c.y}; |
91 | |
|
92 | 0 | double det = d23[0] * d31[1] - d23[1] * d31[0]; |
93 | 0 | double b0 = (d23[0] * d3p[1] - d23[1] * d3p[0]) / det; |
94 | 0 | double b1 = (d31[0] * d3p[1] - d31[1] * d3p[0]) / det; |
95 | 0 | double b2 = 1.0 - (b0 + b1); |
96 | 0 | return {b0, b1, b2}; |
97 | 0 | } |
98 | | |
99 | | inline Face2D barycentric_to_face(const Barycentric &b, |
100 | 5.35k | const FaceTriangle &tri) { |
101 | 5.35k | return {b.u * tri.a.x + b.v * tri.b.x + b.w * tri.c.x, |
102 | 5.35k | b.u * tri.a.y + b.v * tri.b.y + b.w * tri.c.y}; |
103 | 5.35k | } |
104 | | |
105 | | inline Face2D snyder_fwd(const Vec3 &v, const SphericalTriangle &stri, |
106 | 5.35k | const FaceTriangle &ftri) { |
107 | 5.35k | Vec3 a = stri.a, b = stri.b, c = stri.c; |
108 | | |
109 | 5.35k | Vec3 z = vec3_normalize(vec3_subtract(v, a)); |
110 | 5.35k | Vec3 p = vec3_normalize(quadruple_product(a, z, b, c)); |
111 | | |
112 | | // Ensure p is on the same hemisphere as b and c (the quadruple product |
113 | | // may return the antipodal intersection depending on vertex winding) |
114 | 5.35k | if (vec3_dot(p, b) + vec3_dot(p, c) < 0) { |
115 | 3.33k | p = {-p.x, -p.y, -p.z}; |
116 | 3.33k | } |
117 | | |
118 | 5.35k | double h = vector_difference(a, v) / vector_difference(a, p); |
119 | 5.35k | double area_abc = spherical_triangle_area(a, b, c); |
120 | 5.35k | double scaled_area = h / area_abc; |
121 | 5.35k | double area_apc = spherical_triangle_area(a, p, c); |
122 | 5.35k | double area_abp = spherical_triangle_area(a, b, p); |
123 | | |
124 | 5.35k | Barycentric bary = {1.0 - h, scaled_area * area_apc, |
125 | 5.35k | scaled_area * area_abp}; |
126 | 5.35k | return barycentric_to_face(bary, ftri); |
127 | 5.35k | } |
128 | | |
129 | | inline Vec3 snyder_inv(const Face2D &face_point, const FaceTriangle &ftri, |
130 | 0 | const SphericalTriangle &stri) { |
131 | 0 | Vec3 a = stri.a, b = stri.b, c = stri.c; |
132 | 0 | Barycentric bcoords = face_to_barycentric(face_point, ftri); |
133 | |
|
134 | 0 | constexpr double threshold = 1.0 - 1e-14; |
135 | 0 | if (bcoords.u > threshold) |
136 | 0 | return a; |
137 | 0 | if (bcoords.v > threshold) |
138 | 0 | return b; |
139 | 0 | if (bcoords.w > threshold) |
140 | 0 | return c; |
141 | | |
142 | 0 | Vec3 c1 = vec3_cross(b, c); |
143 | 0 | double area_abc = spherical_triangle_area(a, b, c); |
144 | 0 | double h = 1.0 - bcoords.u; |
145 | 0 | double r = bcoords.w / h; |
146 | 0 | double alpha = r * area_abc; |
147 | 0 | double s = std::sin(alpha); |
148 | 0 | double half_c = std::sin(alpha / 2.0); |
149 | 0 | double cc = 2.0 * half_c * half_c; // 1 - cos(alpha) |
150 | |
|
151 | 0 | double c01 = vec3_dot(a, b); |
152 | 0 | double c12 = vec3_dot(b, c); |
153 | 0 | double c20 = vec3_dot(c, a); |
154 | 0 | double s12 = vec3_length(c1); |
155 | |
|
156 | 0 | double tp = vec3_dot(a, c1); // triple product A·(B×C) |
157 | 0 | double f = s * tp + cc * (c01 * c12 - c20); |
158 | 0 | double g = cc * s12 * (1.0 + c01); |
159 | 0 | double q = (2.0 / std::acos(c12)) * std::atan2(g, f); |
160 | 0 | Vec3 pp = slerp(b, c, q); |
161 | 0 | double k = vector_difference(a, pp); |
162 | 0 | double t = std::asin(h * k) / std::asin(k); |
163 | 0 | return slerp(a, pp, t); |
164 | 0 | } |
165 | | |
166 | | } // namespace polyhedral |
167 | | |
168 | | #endif // SNYDER_H |