/src/s2geometry/src/s2/s2loop_measures.cc
Line | Count | Source |
1 | | // Copyright 2018 Google Inc. All Rights Reserved. |
2 | | // |
3 | | // Licensed under the Apache License, Version 2.0 (the "License"); |
4 | | // you may not use this file except in compliance with the License. |
5 | | // You may obtain a copy of the License at |
6 | | // |
7 | | // http://www.apache.org/licenses/LICENSE-2.0 |
8 | | // |
9 | | // Unless required by applicable law or agreed to in writing, software |
10 | | // distributed under the License is distributed on an "AS-IS" BASIS, |
11 | | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
12 | | // See the License for the specific language governing permissions and |
13 | | // limitations under the License. |
14 | | // |
15 | | |
16 | | // Author: ericv@google.com (Eric Veach) |
17 | | |
18 | | #include "s2/s2loop_measures.h" |
19 | | |
20 | | #include <algorithm> |
21 | | #include <cfloat> |
22 | | #include <cmath> |
23 | | #include <limits> |
24 | | #include <ostream> |
25 | | #include <vector> |
26 | | |
27 | | #include "absl/container/inlined_vector.h" |
28 | | #include "absl/log/absl_check.h" |
29 | | #include "s2/s1angle.h" |
30 | | #include "s2/s2centroids.h" |
31 | | #include "s2/s2measures.h" |
32 | | #include "s2/s2point.h" |
33 | | #include "s2/s2point_span.h" |
34 | | |
35 | | using std::fabs; |
36 | | using std::max; |
37 | | using std::min; |
38 | | using std::vector; |
39 | | |
40 | | namespace S2 { |
41 | | |
42 | 0 | S1Angle GetPerimeter(S2PointLoopSpan loop) { |
43 | 0 | S1Angle perimeter = S1Angle::Zero(); |
44 | 0 | if (loop.size() <= 1) return perimeter; |
45 | 0 | for (size_t i = 0; i < loop.size(); ++i) { |
46 | 0 | perimeter += S1Angle(loop[i], loop[i + 1]); |
47 | 0 | } |
48 | 0 | return perimeter; |
49 | 0 | } |
50 | | |
51 | 0 | double GetArea(S2PointLoopSpan loop) { |
52 | 0 | double area = GetSignedArea(loop); |
53 | 0 | ABSL_DCHECK_LE(fabs(area), 2 * M_PI); |
54 | 0 | if (area < 0.0) area += 4 * M_PI; |
55 | 0 | return area; |
56 | 0 | } |
57 | | |
58 | 0 | double GetSignedArea(S2PointLoopSpan loop) { |
59 | | // It is surprisingly difficult to compute the area of a loop robustly. The |
60 | | // main issues are (1) whether degenerate loops are considered to be CCW or |
61 | | // not (i.e., whether their area is close to 0 or 4*Pi), and (2) computing |
62 | | // the areas of small loops with good relative accuracy. |
63 | | // |
64 | | // With respect to degeneracies, we would like GetArea() to be consistent |
65 | | // with S2Loop::Contains(S2Point) in that loops that contain many points |
66 | | // should have large areas, and loops that contain few points should have |
67 | | // small areas. For example, if a degenerate triangle is considered CCW |
68 | | // according to s2pred::Sign(), then it will contain very few points and |
69 | | // its area should be approximately zero. On the other hand if it is |
70 | | // considered clockwise, then it will contain virtually all points and so |
71 | | // its area should be approximately 4*Pi. |
72 | | // |
73 | | // More precisely, let U be the set of S2Points for which S2::IsUnitLength() |
74 | | // is true, let P(U) be the projection of those points onto the mathematical |
75 | | // unit sphere, and let V(P(U)) be the Voronoi diagram of the projected |
76 | | // points. Then for every loop x, we would like GetArea() to approximately |
77 | | // equal the sum of the areas of the Voronoi regions of the points p for |
78 | | // which x.Contains(p) is true. |
79 | | // |
80 | | // The second issue is that we want to compute the area of small loops |
81 | | // accurately. This requires having good relative precision rather than |
82 | | // good absolute precision. For example, if the area of a loop is 1e-12 and |
83 | | // the error is 1e-15, then the area only has 3 digits of accuracy. (For |
84 | | // reference, 1e-12 is about 40 square meters on the surface of the earth.) |
85 | | // We would like to have good relative accuracy even for small loops. |
86 | | // |
87 | | // To achieve these goals, we combine two different methods of computing the |
88 | | // area. This first method is based on the Gauss-Bonnet theorem, which says |
89 | | // that the area enclosed by the loop equals 2*Pi minus the total geodesic |
90 | | // curvature of the loop (i.e., the sum of the "turning angles" at all the |
91 | | // loop vertices). The big advantage of this method is that as long as we |
92 | | // use s2pred::Sign() to compute the turning angle at each vertex, then |
93 | | // degeneracies are always handled correctly. In other words, if a |
94 | | // degenerate loop is CCW according to the symbolic perturbations used by |
95 | | // s2pred::Sign(), then its turning angle will be approximately 2*Pi. |
96 | | // |
97 | | // The disadvantage of the Gauss-Bonnet method is that its absolute error is |
98 | | // about 2e-15 times the number of vertices (see GetCurvatureMaxError). |
99 | | // So, it cannot compute the area of small loops accurately. |
100 | | // |
101 | | // The second method is based on splitting the loop into triangles and |
102 | | // summing the area of each triangle. To avoid the difficulty and expense |
103 | | // of decomposing the loop into a union of non-overlapping triangles, |
104 | | // instead we compute a signed sum over triangles that may overlap (see the |
105 | | // comments for S2Loop::GetSurfaceIntegral). The advantage of this method |
106 | | // is that the area of each triangle can be computed with much better |
107 | | // relative accuracy (using l'Huilier's theorem). The disadvantage is that |
108 | | // the result is a signed area: CCW loops may yield a small positive value, |
109 | | // while CW loops may yield a small negative value (which is converted to a |
110 | | // positive area by adding 4*Pi). This means that small errors in computing |
111 | | // the signed area may translate into a very large error in the result (if |
112 | | // the sign of the sum is incorrect). |
113 | | // |
114 | | // So, our strategy is to combine these two methods as follows. First we |
115 | | // compute the area using the "signed sum over triangles" approach (since it |
116 | | // is generally more accurate). We also estimate the maximum error in this |
117 | | // result. If the signed area is too close to zero (i.e., zero is within |
118 | | // the error bounds), then we double-check the sign of the result using the |
119 | | // Gauss-Bonnet method. If the two methods disagree, we return the smallest |
120 | | // possible positive or negative area based on the result of GetCurvature(). |
121 | | // Otherwise we return the area that we computed originally. |
122 | | |
123 | | // The signed area should be between approximately -4*Pi and 4*Pi. |
124 | | // Normalize it to be in the range [-2*Pi, 2*Pi]. |
125 | 0 | double area = GetSurfaceIntegralKahan(loop, S2::SignedArea); |
126 | 0 | double max_error = GetCurvatureMaxError(loop); |
127 | | |
128 | | // Normalize the area to be in the range (-2*Pi, 2*Pi]. Effectively this |
129 | | // means that hemispheres are always interpreted as having positive area. |
130 | 0 | area = remainder(area, 4 * M_PI); |
131 | 0 | if (area == -2 * M_PI) area = 2 * M_PI; |
132 | | |
133 | | // If the area is a small negative or positive number, verify that the sign |
134 | | // of the result is consistent with the loop orientation. |
135 | 0 | if (fabs(area) <= max_error) { |
136 | 0 | double curvature = GetCurvature(loop); |
137 | | // Zero-area loops should have a curvature of approximately +/- 2*Pi. |
138 | 0 | ABSL_DCHECK(!(area == 0 && curvature == 0)); |
139 | 0 | if (curvature == 2 * M_PI) return 0.0; // Degenerate |
140 | 0 | if (area <= 0 && curvature > 0) { |
141 | 0 | return std::numeric_limits<double>::min(); |
142 | 0 | } |
143 | | // Full loops are handled by the case below. |
144 | 0 | if (area >= 0 && curvature < 0) { |
145 | 0 | return -std::numeric_limits<double>::min(); |
146 | 0 | } |
147 | 0 | } |
148 | 0 | return area; |
149 | 0 | } |
150 | | |
151 | 0 | double GetApproxArea(S2PointLoopSpan loop) { |
152 | 0 | return 2 * M_PI - GetCurvature(loop); |
153 | 0 | } |
154 | | |
155 | | S2PointLoopSpan PruneDegeneracies(S2PointLoopSpan loop, |
156 | 0 | vector<S2Point>* new_vertices) { |
157 | 0 | vector<S2Point>& vertices = *new_vertices; |
158 | 0 | vertices.clear(); |
159 | 0 | vertices.reserve(loop.size()); |
160 | | // Move vertices from `loop` to `vertices`, checking for degeneracies as we |
161 | | // go. Invariant: the partially constructed sequence `vertices` contains no |
162 | | // AAs nor ABAs. |
163 | 0 | for (const S2Point& v : loop) { |
164 | 0 | if (!vertices.empty()) { |
165 | 0 | if (v == vertices.back()) { |
166 | | // De-dup: AA -> A. |
167 | 0 | continue; |
168 | 0 | } |
169 | 0 | if (vertices.size() >= 2 && v == vertices.end()[-2]) { |
170 | | // Remove whisker: ABA -> A. |
171 | 0 | vertices.pop_back(); |
172 | 0 | continue; |
173 | 0 | } |
174 | 0 | } |
175 | | // The new vertex isn't involved in a degeneracy involving earlier vertices. |
176 | 0 | vertices.push_back(v); |
177 | 0 | } |
178 | 0 | if (vertices.size() >= 2 && vertices[0] == vertices.back()) { |
179 | | // Remove AA that wraps from end to beginning. |
180 | 0 | vertices.pop_back(); |
181 | 0 | } |
182 | | |
183 | | // Invariant from this point on: there are no more AA's (not even wrapped), |
184 | | // and no transformations we do after this (ABA -> A) introduce any AA's. |
185 | | |
186 | | // Check whether the loop was completely degenerate. |
187 | 0 | if (vertices.size() < 3) return S2PointLoopSpan(); |
188 | | |
189 | | // Otherwise some portion of the loop is guaranteed to be non-degenerate |
190 | | // (this requires some thought). |
191 | | // However there may still be some ABA->A's to do at the ends. |
192 | | |
193 | | // If the loop begins with BA and ends with A, or begins with A and ends with |
194 | | // AB, then there is an edge pair of the form ABA including the first and last |
195 | | // point, which we remove by removing the first and last point, leaving A at |
196 | | // the beginning or end. Do this as many times as we can. |
197 | | // As noted above, this is guaranteed to leave a non-degenerate loop. |
198 | 0 | int k = 0; |
199 | 0 | while (vertices[k + 1] == vertices.end()[-(k + 1)] || |
200 | 0 | vertices[k] == vertices.end()[-(k + 2)]) { |
201 | 0 | ++k; |
202 | 0 | } |
203 | 0 | return S2PointLoopSpan(vertices.data() + k, vertices.size() - 2 * k); |
204 | 0 | } |
205 | | |
206 | 0 | double GetCurvature(S2PointLoopSpan loop) { |
207 | | // By convention, a loop with no vertices contains all points on the sphere. |
208 | 0 | if (loop.empty()) return -2 * M_PI; |
209 | | |
210 | | // Remove any degeneracies from the loop. |
211 | 0 | vector<S2Point> vertices; |
212 | 0 | loop = PruneDegeneracies(loop, &vertices); |
213 | | |
214 | | // If the entire loop was degenerate, it's turning angle is defined as 2*Pi. |
215 | 0 | if (loop.empty()) return 2 * M_PI; |
216 | | |
217 | | // To ensure that we get the same result when the vertex order is rotated, |
218 | | // and that the result is negated when the vertex order is reversed, we need |
219 | | // to add up the individual turn angles in a consistent order. (In general, |
220 | | // adding up a set of numbers in a different order can change the sum due to |
221 | | // rounding errors.) |
222 | | // |
223 | | // Furthermore, if we just accumulate an ordinary sum then the worst-case |
224 | | // error is quadratic in the number of vertices. (This can happen with |
225 | | // spiral shapes, where the partial sum of the turning angles can be linear |
226 | | // in the number of vertices.) To avoid this we use the Kahan summation |
227 | | // algorithm (http://en.wikipedia.org/wiki/Kahan_summation_algorithm). |
228 | 0 | LoopOrder order = GetCanonicalLoopOrder(loop); |
229 | 0 | int i = order.first, dir = order.dir, n = loop.size(); |
230 | 0 | double sum = S2::TurnAngle(loop[(i + n - dir) % n], loop[i], |
231 | 0 | loop[(i + dir) % n]); |
232 | 0 | double compensation = 0; // Kahan summation algorithm |
233 | 0 | while (--n > 0) { |
234 | 0 | i += dir; |
235 | 0 | double angle = S2::TurnAngle(loop[i - dir], loop[i], loop[i + dir]); |
236 | 0 | double old_sum = sum; |
237 | 0 | angle += compensation; |
238 | 0 | sum += angle; |
239 | 0 | compensation = (old_sum - sum) + angle; |
240 | 0 | } |
241 | 0 | constexpr double kMaxCurvature = 2 * M_PI - 4 * DBL_EPSILON; |
242 | 0 | sum += compensation; |
243 | 0 | return max(-kMaxCurvature, min(kMaxCurvature, dir * sum)); |
244 | 0 | } |
245 | | |
246 | 0 | double GetCurvatureMaxError(S2PointLoopSpan loop) { |
247 | | // The maximum error can be bounded as follows: |
248 | | // 3.00 * DBL_EPSILON for RobustCrossProd(b, a) |
249 | | // 3.00 * DBL_EPSILON for RobustCrossProd(c, b) |
250 | | // 3.25 * DBL_EPSILON for Angle() |
251 | | // 2.00 * DBL_EPSILON for each addition in the Kahan summation |
252 | | // ------------------- |
253 | | // 11.25 * DBL_EPSILON |
254 | | // |
255 | | // TODO(b/203697029): This error estimate is approximate. There are two |
256 | | // issues: (1) SignedArea needs some improvements to ensure that its error is |
257 | | // actually never higher than GirardArea, and (2) although the number of |
258 | | // triangles in the sum is typically N-2, in theory it could be as high as |
259 | | // 2*N for pathological inputs. But in other respects this error bound is |
260 | | // very conservative since it assumes that the maximum error is achieved on |
261 | | // every triangle. |
262 | 0 | const double kMaxErrorPerVertex = 11.25 * DBL_EPSILON; |
263 | 0 | return kMaxErrorPerVertex * loop.size(); |
264 | 0 | } |
265 | | |
266 | 0 | S2Point GetCentroid(S2PointLoopSpan loop) { |
267 | | // GetSurfaceIntegral() returns either the integral of position over loop |
268 | | // interior, or the negative of the integral of position over the loop |
269 | | // exterior. But these two values are the same (!), because the integral of |
270 | | // position over the entire sphere is (0, 0, 0). |
271 | 0 | return GetSurfaceIntegral(loop, S2::TrueCentroid); |
272 | 0 | } |
273 | | |
274 | | static inline bool IsOrderLess(LoopOrder order1, LoopOrder order2, |
275 | 0 | S2PointLoopSpan loop) { |
276 | 0 | if (order1 == order2) return false; |
277 | | |
278 | 0 | int i1 = order1.first, i2 = order2.first; |
279 | 0 | int dir1 = order1.dir, dir2 = order2.dir; |
280 | 0 | ABSL_DCHECK_EQ(loop[i1], loop[i2]); |
281 | 0 | for (int n = loop.size(); --n > 0; ) { |
282 | 0 | i1 += dir1; |
283 | 0 | i2 += dir2; |
284 | 0 | if (loop[i1] < loop[i2]) return true; |
285 | 0 | if (loop[i1] > loop[i2]) return false; |
286 | 0 | } |
287 | 0 | return false; |
288 | 0 | } |
289 | | |
290 | 0 | LoopOrder GetCanonicalLoopOrder(S2PointLoopSpan loop) { |
291 | | // In order to handle loops with duplicate vertices and/or degeneracies, we |
292 | | // return the LoopOrder that minimizes the entire corresponding vertex |
293 | | // *sequence*. For example, suppose that vertices are sorted |
294 | | // alphabetically, and consider the loop CADBAB. The canonical loop order |
295 | | // would be (4, 1), corresponding to the vertex sequence ABCADB. (For |
296 | | // comparison, loop order (4, -1) yields the sequence ABDACB.) |
297 | | // |
298 | | // If two or more loop orders yield identical minimal vertex sequences, then |
299 | | // it doesn't matter which one we return (since they yield the same result). |
300 | | |
301 | | // For efficiency, we divide the process into two steps. First we find the |
302 | | // smallest vertex, and the set of vertex indices where that vertex occurs |
303 | | // (noting that the loop may contain duplicate vertices). Then we consider |
304 | | // both possible directions starting from each such vertex index, and return |
305 | | // the LoopOrder corresponding to the smallest vertex sequence. |
306 | 0 | int n = loop.size(); |
307 | 0 | if (n == 0) return LoopOrder(0, 1); |
308 | | |
309 | 0 | absl::InlinedVector<int, 4> min_indices; |
310 | 0 | min_indices.push_back(0); |
311 | 0 | for (int i = 1; i < n; ++i) { |
312 | 0 | if (loop[i] <= loop[min_indices[0]]) { |
313 | 0 | if (loop[i] < loop[min_indices[0]]) min_indices.clear(); |
314 | 0 | min_indices.push_back(i); |
315 | 0 | } |
316 | 0 | } |
317 | 0 | LoopOrder min_order(min_indices[0], 1); |
318 | 0 | for (int min_index : min_indices) { |
319 | 0 | LoopOrder order1(min_index, 1); |
320 | 0 | LoopOrder order2(min_index + n, -1); |
321 | 0 | if (IsOrderLess(order1, min_order, loop)) min_order = order1; |
322 | 0 | if (IsOrderLess(order2, min_order, loop)) min_order = order2; |
323 | 0 | } |
324 | 0 | return min_order; |
325 | 0 | } |
326 | | |
327 | 0 | bool IsNormalized(S2PointLoopSpan loop) { |
328 | | // We allow some error so that hemispheres are always considered normalized. |
329 | | // |
330 | | // TODO(ericv): This is no longer required by the S2Polygon implementation, |
331 | | // so alternatively we could create the invariant that a loop is normalized |
332 | | // if and only if its complement is not normalized. |
333 | 0 | return GetCurvature(loop) >= -GetCurvatureMaxError(loop); |
334 | 0 | } |
335 | | |
336 | 0 | std::ostream& operator<<(std::ostream& os, LoopOrder order) { |
337 | 0 | return os << "(" << order.first << ", " << order.dir << ")"; |
338 | 0 | } |
339 | | |
340 | | } // namespace S2 |