Coverage Report

Created: 2026-06-09 07:04

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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_