Coverage Report

Created: 2025-11-08 06:32

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