/src/s2geometry/src/s2/s2loop_measures.h
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 | | // Defines various angle and area measures for loops on the sphere. These are |
19 | | // low-level methods that work directly with arrays of S2Points. They are |
20 | | // used to implement the methods in s2shapeindex_measures.h, |
21 | | // s2shape_measures.h, s2loop.h, and s2polygon.h. |
22 | | // |
23 | | // See s2polyline_measures.h, s2edge_distances.h, and s2measures.h for |
24 | | // additional low-level methods. |
25 | | |
26 | | #ifndef S2_S2LOOP_MEASURES_H_ |
27 | | #define S2_S2LOOP_MEASURES_H_ |
28 | | |
29 | | #include <cmath> |
30 | | #include <ostream> |
31 | | #include <vector> |
32 | | |
33 | | #include "absl/log/absl_check.h" |
34 | | #include "s2/_fp_contract_off.h" // IWYU pragma: keep |
35 | | #include "s2/s1angle.h" |
36 | | #include "s2/s2edge_crossings.h" |
37 | | #include "s2/s2point.h" |
38 | | #include "s2/s2point_span.h" |
39 | | #include "s2/s2pointutil.h" |
40 | | |
41 | | namespace S2 { |
42 | | |
43 | | // Returns the perimeter of the loop. |
44 | | S1Angle GetPerimeter(S2PointLoopSpan loop); |
45 | | |
46 | | // Returns the area of the loop interior, i.e. the region on the left side of |
47 | | // the loop. The result is between 0 and 4*Pi steradians. The implementation |
48 | | // ensures that nearly-degenerate clockwise loops have areas close to zero, |
49 | | // while nearly-degenerate counter-clockwise loops have areas close to 4*Pi. |
50 | | double GetArea(S2PointLoopSpan loop); |
51 | | |
52 | | // Like GetArea(), except that this method is faster and has more error. The |
53 | | // result is between 0 and 4*Pi steradians. The maximum error is 2.22e-15 |
54 | | // steradians per loop vertex, which works out to about 0.09 square meters per |
55 | | // vertex on the Earth's surface. For example, a loop with 100 vertices has a |
56 | | // maximum error of about 9 square meters. (The actual error is typically |
57 | | // much smaller than this.) The error bound can be computed using |
58 | | // GetCurvatureMaxError(), which returns the maximum error in steradians. |
59 | | double GetApproxArea(S2PointLoopSpan loop); |
60 | | |
61 | | // Returns either the positive area of the region on the left side of the |
62 | | // loop, or the negative area of the region on the right side of the loop, |
63 | | // whichever is smaller in magnitude. The result is between -2*Pi and 2*Pi |
64 | | // steradians. This method is used to accurately compute the area of polygons |
65 | | // consisting of multiple loops. |
66 | | // |
67 | | // The following cases are handled specially: |
68 | | // |
69 | | // - Counter-clockwise loops are guaranteed to have positive area, and |
70 | | // clockwise loops are guaranteed to have negative area. |
71 | | // |
72 | | // - Degenerate loops (consisting of an isolated vertex or composed entirely |
73 | | // of sibling edge pairs) have an area of exactly zero. |
74 | | // |
75 | | // - The full loop (containing all points, and represented as a loop with no |
76 | | // vertices) has a negative area with the minimum possible magnitude. |
77 | | // (This is the "signed equivalent" of having an area of 4*Pi.) |
78 | | double GetSignedArea(S2PointLoopSpan loop); |
79 | | |
80 | | // Returns the geodesic curvature of the loop, defined as the sum of the turn |
81 | | // angles at each vertex (see S2::TurnAngle). The result is positive if the |
82 | | // loop is counter-clockwise, negative if the loop is clockwise, and zero if |
83 | | // the loop is a great circle. The geodesic curvature is equal to 2*Pi minus |
84 | | // the area of the loop. |
85 | | // |
86 | | // The following cases are handled specially: |
87 | | // |
88 | | // - Degenerate loops (consisting of an isolated vertex or composed entirely |
89 | | // of sibling edge pairs) have a curvature of 2*Pi exactly. |
90 | | // |
91 | | // - The full loop (containing all points, and represented as a loop with no |
92 | | // vertices) has a curvature of -2*Pi exactly. |
93 | | // |
94 | | // - All other loops have a non-zero curvature in the range (-2*Pi, 2*Pi). |
95 | | // For any such loop, reversing the order of the vertices is guaranteed to |
96 | | // negate the curvature. This property can be used to define a unique |
97 | | // normalized orientation for every loop. |
98 | | double GetCurvature(S2PointLoopSpan loop); |
99 | | |
100 | | // Returns the maximum error in GetCurvature() for the given loop. This value |
101 | | // is also an upper bound on the error in GetArea(), GetSignedArea(), and |
102 | | // GetApproxArea(). |
103 | | double GetCurvatureMaxError(S2PointLoopSpan loop); |
104 | | |
105 | | // Returns the true centroid of the loop multiplied by the area of the loop |
106 | | // (see s2centroids.h for details on centroids). The result is not unit |
107 | | // length, so you may want to normalize it. Also note that in general, the |
108 | | // centroid may not be contained by the loop. |
109 | | // |
110 | | // The result is scaled by the loop area for two reasons: (1) it is cheaper to |
111 | | // compute this way, and (2) it makes it easier to compute the centroid of |
112 | | // more complicated shapes (by splitting them into disjoint regions and adding |
113 | | // their centroids). |
114 | | S2Point GetCentroid(S2PointLoopSpan loop); |
115 | | |
116 | | // Returns true if the loop area is at most 2*Pi. (A small amount of error is |
117 | | // allowed in order to ensure that loops representing an entire hemisphere are |
118 | | // always considered normalized.) |
119 | | // |
120 | | // Degenerate loops are handled consistently with s2pred::Sign(), i.e., if a |
121 | | // loop can be expressed as the union of degenerate or nearly-degenerate |
122 | | // counter-clockwise triangles then this method will return true. |
123 | | bool IsNormalized(S2PointLoopSpan loop); |
124 | | |
125 | | // LoopOrder represents a cyclic ordering of the loop vertices, starting at |
126 | | // the index "first" and proceeding in direction "dir" (either +1 or -1). |
127 | | // "first" and "dir" must be chosen such that (first, ..., first + n * dir) |
128 | | // are all in the range [0, 2*n-1] as required by S2PointLoopSpan::operator[]. |
129 | | struct LoopOrder { |
130 | 0 | LoopOrder(int _first, int _dir) : first(_first), dir(_dir) {} |
131 | | int first; |
132 | | int dir; |
133 | | }; |
134 | | bool operator==(LoopOrder x, LoopOrder y); |
135 | | std::ostream& operator<<(std::ostream& os, LoopOrder order); |
136 | | |
137 | | // Returns an index "first" and a direction "dir" such that the vertex |
138 | | // sequence (first, first + dir, ..., first + (n - 1) * dir) does not change |
139 | | // when the loop vertex order is rotated or reversed. This allows the loop |
140 | | // vertices to be traversed in a canonical order. |
141 | | LoopOrder GetCanonicalLoopOrder(S2PointLoopSpan loop); |
142 | | |
143 | | namespace internal { |
144 | | |
145 | | // Returns the oriented surface integral of some quantity f(x) over the loop |
146 | | // interior, given a function f_tri(A,B,C) that returns the corresponding |
147 | | // integral over the spherical triangle ABC. Here "oriented surface integral" |
148 | | // means: |
149 | | // |
150 | | // (1) f_tri(A,B,C) should return the integral of f if ABC is counterclockwise |
151 | | // and the integral of -f if ABC is clockwise. |
152 | | // |
153 | | // (2) The result is the integral of f over the loop interior plus or minus |
154 | | // some multiple of the integral of f over the entire sphere. |
155 | | // |
156 | | // Note that there are at least two common situations where property (2) above |
157 | | // is not a limitation: |
158 | | // |
159 | | // - When the integral of f over the entire sphere is zero. For example this |
160 | | // is true when computing centroids. |
161 | | // |
162 | | // - When f is non-negative and the integral over the entire sphere is a |
163 | | // constant known in advance. In this case the correct result can be |
164 | | // obtained by using std::remainder appropriately. |
165 | | // |
166 | | // Accumulation of the result can be customized via the sum parameter. |
167 | | // Intermediate results are summed via operator+=. |
168 | | // |
169 | | // REQUIRES: The default constructor for T must initialize the value to zero. |
170 | | // (This is true for built-in types such as "double".) |
171 | | template <class T, class TAccumulator = T> |
172 | | void GetSurfaceIntegral(S2PointLoopSpan loop, |
173 | | T f_tri(const S2Point&, const S2Point&, const S2Point&), |
174 | | TAccumulator& sum); |
175 | | |
176 | | // Compensated sum using Kahan's algorithm. This doesn't use the higher-order |
177 | | // variations so it's not as robust against wildly ill-conditioned inputs as it |
178 | | // could be in the interest of speed. It's very accurate in general for long |
179 | | // sequences of accumulation though. |
180 | | template <typename T> |
181 | | class KahanSum { |
182 | | public: |
183 | 0 | KahanSum() = default; |
184 | | explicit KahanSum(T value) : sum_(value) {} |
185 | | |
186 | | // Adds value to running total with compensate summation. |
187 | 0 | void operator+=(T value) { |
188 | 0 | T tmp1 = value - err_; |
189 | 0 | T tmp2 = sum_ + tmp1; |
190 | 0 | err_ = (tmp2 - sum_) - tmp1; |
191 | 0 | sum_ = tmp2; |
192 | 0 | } |
193 | | |
194 | | // Explicitly return the final sum as an instance of T. |
195 | 0 | explicit operator T() const { return sum_; } |
196 | | |
197 | | // Returns the current compensation value. |
198 | | T Compensation() const { return err_; } |
199 | | |
200 | | private: |
201 | | T sum_ = T(); |
202 | | T err_ = T(); |
203 | | }; |
204 | | |
205 | | } // namespace internal |
206 | | |
207 | | // Accumulates the result naively into a variable of type T. |
208 | | template <class T> |
209 | | T GetSurfaceIntegral(S2PointLoopSpan loop, |
210 | 0 | T f_tri(const S2Point&, const S2Point&, const S2Point&)) { |
211 | 0 | T sum = T(); |
212 | 0 | internal::GetSurfaceIntegral(loop, f_tri, sum); |
213 | 0 | return sum; |
214 | 0 | } |
215 | | |
216 | | // Accumulates the result using a Kahan sum which accumulates much less error |
217 | | // for long sequences of numbers. |
218 | | template <class T> |
219 | | T GetSurfaceIntegralKahan(S2PointLoopSpan loop, |
220 | | T f_tri(const S2Point&, const S2Point&, |
221 | 0 | const S2Point&)) { |
222 | 0 | internal::KahanSum<T> sum; |
223 | 0 | internal::GetSurfaceIntegral(loop, f_tri, sum); |
224 | 0 | return (T)sum; |
225 | 0 | } |
226 | | |
227 | | // Returns a new loop obtained by removing all degeneracies from "loop" |
228 | | // that can be detected by only comparing adjacent vertices and edges |
229 | | // for equality (not doing any geometric examination of them). |
230 | | // More specifically, the function repeatedly finds any vertex subsequences |
231 | | // of the form AA or ABA, and collapes them to A, until there are no more, |
232 | | // and a loop of length 1 or 2 will be turned into an empty loop. |
233 | | // |
234 | | // NOTE: it doesn't matter what order such degeneracies are processed in; |
235 | | // the resulting pruned loop is uniquely determined, up to cyclic permutation. |
236 | | // (This isn't obvious.) |
237 | | // |
238 | | // CAVEAT: notice that GetCurvature() (and other functions in this file) |
239 | | // may return a different answer when called on the resulting pruned loop from |
240 | | // when it's called on the original loop. Specifically, according to |
241 | | // GetCurvature()'s contract, when the original loop is nonempty but degenerate, |
242 | | // calling GetCurvature() on it will yield 2*PI ("empty") before pruning, |
243 | | // but -2*PI ("full") after pruning. |
244 | | // |
245 | | // "new_vertices" represents storage where new loop vertices may be written. |
246 | | // Note that the S2PointLoopSpan result may be a subsequence of either "loop" |
247 | | // or "new_vertices", and therefore "new_vertices" must persist until the |
248 | | // result of this method is no longer needed. |
249 | | S2PointLoopSpan PruneDegeneracies(S2PointLoopSpan loop, |
250 | | std::vector<S2Point>* new_vertices); |
251 | | |
252 | | //////////////////// Implementation details follow //////////////////////// |
253 | | |
254 | 0 | inline bool operator==(LoopOrder x, LoopOrder y) { |
255 | 0 | return x.first == y.first && x.dir == y.dir; |
256 | 0 | } |
257 | | |
258 | | template <class T, class TAccumulator> |
259 | | void internal::GetSurfaceIntegral(S2PointLoopSpan loop, |
260 | | T f_tri(const S2Point&, const S2Point&, |
261 | | const S2Point&), |
262 | 0 | TAccumulator& sum) { |
263 | | // We sum "f_tri" over a collection T of oriented triangles, possibly |
264 | | // overlapping. Let the sign of a triangle be +1 if it is CCW and -1 |
265 | | // otherwise, and let the sign of a point "x" be the sum of the signs of the |
266 | | // triangles containing "x". Then the collection of triangles T is chosen |
267 | | // such that every point in the loop interior has the same sign x, and every |
268 | | // point in the loop exterior has the same sign (x - 1). Furthermore almost |
269 | | // always it is true that x == 0 or x == 1, meaning that either |
270 | | // |
271 | | // (1) Each point in the loop interior has sign +1, and sign 0 otherwise; or |
272 | | // (2) Each point in the loop exterior has sign -1, and sign 0 otherwise. |
273 | | // |
274 | | // The triangles basically consist of a "fan" from vertex 0 to every loop |
275 | | // edge that does not include vertex 0. However, what makes this a bit |
276 | | // tricky is that spherical edges become numerically unstable as their |
277 | | // length approaches 180 degrees. Of course there is not much we can do if |
278 | | // the loop itself contains such edges, but we would like to make sure that |
279 | | // all the triangle edges under our control (i.e., the non-loop edges) are |
280 | | // stable. For example, consider a loop around the equator consisting of |
281 | | // four equally spaced points. This is a well-defined loop, but we cannot |
282 | | // just split it into two triangles by connecting vertex 0 to vertex 2. |
283 | | // |
284 | | // We handle this type of situation by moving the origin of the triangle fan |
285 | | // whenever we are about to create an unstable edge. We choose a new |
286 | | // location for the origin such that all relevant edges are stable. We also |
287 | | // create extra triangles with the appropriate orientation so that the sum |
288 | | // of the triangle signs is still correct at every point. |
289 | | |
290 | | // The maximum length of an edge for it to be considered numerically stable. |
291 | | // The exact value is fairly arbitrary since it depends on the stability of |
292 | | // the "f_tri" function. The value below is quite conservative but could be |
293 | | // reduced further if desired. |
294 | 0 | static const double kMaxLength = M_PI - 1e-5; |
295 | | |
296 | | // The default constructor for TAccumulator must initialize the value to zero. |
297 | | // (This is true for built-in types such as "double".) |
298 | 0 | if (loop.size() < 3) return; |
299 | | |
300 | 0 | S2Point origin = loop[0]; |
301 | 0 | for (size_t i = 1; i + 1 < loop.size(); ++i) { |
302 | | // Let V_i be loop[i], let O be the current origin, and let length(A, B) |
303 | | // be the length of edge (A, B). At the start of each loop iteration, the |
304 | | // "leading edge" of the triangle fan is (O, V_i), and we want to extend |
305 | | // the triangle fan so that the leading edge is (O, V_i+1). |
306 | | // |
307 | | // Invariants: |
308 | | // 1. length(O, V_i) < kMaxLength for all (i > 1). |
309 | | // 2. Either O == V_0, or O is approximately perpendicular to V_0. |
310 | | // 3. "sum" is the oriented integral of f over the area defined by |
311 | | // (O, V_0, V_1, ..., V_i). |
312 | 0 | ABSL_DCHECK(i == 1 || origin.Angle(loop[i]) < kMaxLength); |
313 | 0 | ABSL_DCHECK(origin == loop[0] || |
314 | 0 | std::fabs(origin.DotProd(loop[0])) < 1e-15); |
315 | |
|
316 | 0 | if (loop[i + 1].Angle(origin) > kMaxLength) { |
317 | | // We are about to create an unstable edge, so choose a new origin O' |
318 | | // for the triangle fan. |
319 | 0 | S2Point old_origin = origin; |
320 | 0 | if (origin == loop[0]) { |
321 | | // The following point O' is well-separated from V_i and V_0 (and |
322 | | // therefore V_i+1 as well). Moving the origin transforms the leading |
323 | | // edge of the triangle fan into a two-edge chain (V_0, O', V_i). |
324 | 0 | origin = S2::RobustCrossProd(loop[0], loop[i]).Normalize(); |
325 | 0 | } else if (loop[i].Angle(loop[0]) < kMaxLength) { |
326 | | // All edges of the triangle (O, V_0, V_i) are stable, so we can |
327 | | // revert to using V_0 as the origin. This changes the leading edge |
328 | | // chain (V_0, O, V_i) back into a single edge (V_0, V_i). |
329 | 0 | origin = loop[0]; |
330 | 0 | } else { |
331 | | // (O, V_i+1) and (V_0, V_i) are antipodal pairs, and O and V_0 are |
332 | | // perpendicular. Therefore V_0.CrossProd(O) is approximately |
333 | | // perpendicular to all of {O, V_0, V_i, V_i+1}, and we can choose |
334 | | // this point O' as the new origin. |
335 | | // |
336 | | // NOTE(ericv): The following line is the reason why in rare cases the |
337 | | // triangle sum can have a sign other than -1, 0, or 1. To fix this |
338 | | // we would need to choose either "-origin" or "origin" below |
339 | | // depending on whether the signed area of the triangles chosen so far |
340 | | // is positive or negative respectively. This is easy in the case of |
341 | | // GetSignedArea() but would be extra work for GetCentroid(). In any |
342 | | // case this does not cause any problems in practice. |
343 | 0 | origin = loop[0].CrossProd(old_origin); |
344 | | |
345 | | // The following two triangles transform the leading edge chain from |
346 | | // (V_0, O, V_i) to (V_0, O', V_i+1). |
347 | | // |
348 | | // First we advance the edge (V_0, O) to (V_0, O'). |
349 | 0 | sum += f_tri(loop[0], old_origin, origin); |
350 | 0 | } |
351 | | // Advance the edge (O, V_i) to (O', V_i). |
352 | 0 | sum += f_tri(old_origin, loop[i], origin); |
353 | 0 | } |
354 | | // Advance the edge (O, V_i) to (O, V_i+1). |
355 | 0 | sum += f_tri(origin, loop[i], loop[i+1]); |
356 | 0 | } |
357 | | // If the origin is not V_0, we need to sum one more triangle. |
358 | 0 | if (origin != loop[0]) { |
359 | | // Advance the edge (O, V_n-1) to (O, V_0). |
360 | 0 | sum += f_tri(origin, loop[loop.size() - 1], loop[0]); |
361 | 0 | } |
362 | 0 | } Unexecuted instantiation: void S2::internal::GetSurfaceIntegral<double, S2::internal::KahanSum<double> >(S2PointLoopSpan, double (*)(S2Point const&, S2Point const&, S2Point const&), S2::internal::KahanSum<double>&) Unexecuted instantiation: void S2::internal::GetSurfaceIntegral<S2Point, S2Point>(S2PointLoopSpan, S2Point (*)(S2Point const&, S2Point const&, S2Point const&), S2Point&) |
363 | | |
364 | | } // namespace S2 |
365 | | |
366 | | #endif // S2_S2LOOP_MEASURES_H_ |