Coverage Report

Created: 2025-08-25 06:55

/src/s2geometry/src/s2/s2polyline_simplifier.cc
Line
Count
Source (jump to first uncovered line)
1
// Copyright 2016 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/s2polyline_simplifier.h"
19
20
#include <cfloat>
21
#include <cmath>
22
#include <vector>
23
24
#include "absl/log/absl_check.h"
25
#include "s2/s1chord_angle.h"
26
#include "s2/s1interval.h"
27
#include "s2/s2point.h"
28
29
0
void S2PolylineSimplifier::Init(const S2Point& src) {
30
0
  src_ = src;
31
0
  window_ = S1Interval::Full();
32
0
  ranges_to_avoid_.clear();
33
34
  // Precompute basis vectors for the tangent space at "src".  This is similar
35
  // to GetFrame() except that we don't normalize the vectors.  As it turns
36
  // out, the two basis vectors below have the same magnitude (up to the
37
  // length error in S2Point::Normalize).
38
39
  // Find the index of the component whose magnitude is smallest.
40
0
  S2Point tmp = src.Abs();
41
0
  int i = (tmp[0] < tmp[1] ?
42
0
           (tmp[0] < tmp[2] ? 0 : 2) : (tmp[1] < tmp[2] ? 1 : 2));
43
44
  // We define the "y" basis vector as the cross product of "src" and the
45
  // basis vector for axis "i".  Let "j" and "k" be the indices of the other
46
  // two components in cyclic order.
47
0
  int j = (i == 2 ? 0 : i + 1), k = (i == 0 ? 2 : i - 1);
48
0
  y_dir_[i] = 0;
49
0
  y_dir_[j] = src[k];
50
0
  y_dir_[k] = -src[j];
51
52
  // Compute the cross product of "y_dir" and "src".  We write out the cross
53
  // product here mainly for documentation purposes; it also happens to save a
54
  // few multiplies because unfortunately the optimizer does *not* get rid of
55
  // multiplies by zero (since these multiplies propagate NaN, for example).
56
0
  x_dir_[i] = src[j] * src[j] + src[k] * src[k];
57
0
  x_dir_[j] = -src[j] * src[i];
58
0
  x_dir_[k] = -src[k] * src[i];
59
0
}
60
61
0
bool S2PolylineSimplifier::Extend(const S2Point& dst) const {
62
  // We limit the maximum edge length to 90 degrees in order to simplify the
63
  // error bounds.  (The error gets arbitrarily large as the edge length
64
  // approaches 180 degrees.)
65
0
  if (S1ChordAngle(src_, dst) > S1ChordAngle::Right()) return false;
66
67
  // Otherwise check whether this vertex is in the acceptable angle range.
68
0
  double dir = GetDirection(dst);
69
0
  if (!window_.Contains(dir)) return false;
70
71
  // Also check any angles ranges to avoid that have not been processed yet.
72
0
  for (const auto& range : ranges_to_avoid_) {
73
0
    if (range.interval.Contains(dir)) return false;
74
0
  }
75
0
  return true;
76
0
}
77
78
0
bool S2PolylineSimplifier::TargetDisc(const S2Point& p, S1ChordAngle r) {
79
  // Shrink the target interval by the maximum error from all sources.  This
80
  // guarantees that the output edge will intersect the given disc.
81
0
  double semiwidth = GetSemiwidth(p, r, -1 /*round down*/);
82
0
  if (semiwidth >= M_PI) {
83
    // The target disc contains "src", so there is nothing to do.
84
0
    return true;
85
0
  }
86
0
  if (semiwidth < 0) {
87
0
    window_ = S1Interval::Empty();
88
0
    return false;
89
0
  }
90
  // Otherwise compute the angle interval corresponding to the target disc and
91
  // intersect it with the current window.
92
0
  double center = GetDirection(p);
93
0
  S1Interval target = S1Interval::FromPoint(center).Expanded(semiwidth);
94
0
  window_ = window_.Intersection(target);
95
96
  // If there are any angle ranges to avoid, they can be processed now.
97
0
  for (const auto& range : ranges_to_avoid_) {
98
0
    AvoidRange(range.interval, range.on_left);
99
0
  }
100
0
  ranges_to_avoid_.clear();
101
102
0
  return !window_.is_empty();
103
0
}
104
105
bool S2PolylineSimplifier::AvoidDisc(const S2Point& p, S1ChordAngle r,
106
0
                                     bool disc_on_left) {
107
  // Expand the interval by the maximum error from all sources.  This
108
  // guarantees that the final output edge will avoid the given disc.
109
0
  double semiwidth = GetSemiwidth(p, r, 1 /*round up*/);
110
0
  if (semiwidth >= M_PI) {
111
    // The disc to avoid contains "src", so it can't be avoided.
112
0
    window_ = S1Interval::Empty();
113
0
    return false;
114
0
  }
115
  // Compute the disallowed range of angles: the angle subtended by the disc
116
  // on one side, and 90 degrees on the other (to satisfy "disc_on_left").
117
0
  double center = GetDirection(p);
118
0
  double dleft = disc_on_left ? M_PI_2 : semiwidth;
119
0
  double dright = disc_on_left ? semiwidth : M_PI_2;
120
0
  S1Interval avoid_interval(remainder(center - dright, 2 * M_PI),
121
0
                            remainder(center + dleft, 2 * M_PI));
122
123
0
  if (window_.is_full()) {
124
    // Discs to avoid can't be processed until window_ is reduced to at most
125
    // 180 degrees by a call to TargetDisc().  Save it for later.
126
0
    ranges_to_avoid_.push_back(RangeToAvoid{avoid_interval, disc_on_left});
127
0
    return true;
128
0
  }
129
0
  AvoidRange(avoid_interval, disc_on_left);
130
0
  return !window_.is_empty();
131
0
}
132
133
void S2PolylineSimplifier::AvoidRange(const S1Interval& avoid_interval,
134
0
                                      bool disc_on_left) {
135
  // If "avoid_interval" is a proper subset of "window_", then in theory the
136
  // result should be two intervals.  One interval points towards the given
137
  // disc and passes on the correct side of it, while the other interval points
138
  // away from the disc.  However the latter interval never contains an
139
  // acceptable output edge direction (as long as this class is being used
140
  // correctly) and can be safely ignored.  This is true because (1) "window_"
141
  // is not full, which means that it contains at least one vertex of the input
142
  // polyline and is at most 180 degrees in length, and (2) "disc_on_left" is
143
  // computed with respect to the next edge of the input polyline, which means
144
  // that the next input vertex is either inside "avoid_interval" or somewhere
145
  // in the 180 degrees to its right/left according to "disc_on_left", which
146
  // means that it cannot be contained by the subinterval that we ignore.
147
0
  ABSL_DCHECK(!window_.is_full());
148
0
  if (window_.Contains(avoid_interval)) {
149
0
    if (disc_on_left) {
150
0
      window_ = S1Interval(window_.lo(), avoid_interval.lo());
151
0
    } else {
152
0
      window_ = S1Interval(avoid_interval.hi(), window_.hi());
153
0
    }
154
0
  } else {
155
0
    window_ = window_.Intersection(avoid_interval.Complement());
156
0
  }
157
0
}
158
159
0
double S2PolylineSimplifier::GetDirection(const S2Point& p) const {
160
0
  return atan2(p.DotProd(y_dir_), p.DotProd(x_dir_));
161
0
}
162
163
// Computes half the angle in radians subtended from the source vertex by a
164
// disc of radius "r" centered at "p", rounding the result conservatively up
165
// or down according to whether round_direction is +1 or -1.  (So for example,
166
// if round_direction == +1 then the return value is an upper bound on the
167
// true result.)
168
double S2PolylineSimplifier::GetSemiwidth(const S2Point& p, S1ChordAngle r,
169
0
                                          int round_direction) const {
170
0
  double constexpr DBL_ERR = 0.5 * DBL_EPSILON;
171
172
  // Using spherical trigonometry,
173
  //
174
  //   sin(semiwidth) = sin(r) / sin(a)
175
  //
176
  // where "a" is the angle between "src" and "p".  Rather than measuring
177
  // these angles, instead we measure the squared chord lengths through the
178
  // interior of the sphere (i.e., Cartersian distance).  Letting "r2" be the
179
  // squared chord distance corresponding to "r", and "a2" be the squared
180
  // chord distance corresponding to "a", we use the relationships
181
  //
182
  //    sin^2(r) = r2 (1 - r2 / 4)
183
  //    sin^2(a) = d2 (1 - d2 / 4)
184
  //
185
  // which follow from the fact that r2 = (2 * sin(r / 2)) ^ 2, etc.
186
187
  // "a2" has a relative error up to 5 * DBL_ERR, plus an absolute error of up
188
  // to 64 * DBL_ERR^2 (because "src" and "p" may differ from unit length by
189
  // up to 4 * DBL_ERR).  We can correct for the relative error later, but for
190
  // the absolute error we use "round_direction" to account for it now.
191
0
  double r2 = r.length2();
192
0
  double a2 = S1ChordAngle(src_, p).length2();
193
0
  a2 -= 64 * DBL_ERR * DBL_ERR * round_direction;
194
0
  if (a2 <= r2) return M_PI;  // The given disc contains "src".
195
196
0
  double sin2_r = r2 * (1 - 0.25 * r2);
197
0
  double sin2_a = a2 * (1 - 0.25 * a2);
198
0
  double semiwidth = asin(sqrt(sin2_r / sin2_a));
199
200
  // We compute bounds on the errors from all sources:
201
  //
202
  //   - The call to GetSemiwidth (this call).
203
  //   - The call to GetDirection that computes the center of the interval.
204
  //   - The call to GetDirection in Extend that tests whether a given point
205
  //     is an acceptable destination vertex.
206
  //
207
  // Summary of the errors in GetDirection:
208
  //
209
  // y_dir_ has no error.
210
  //
211
  // x_dir_ has a relative error of DBL_ERR in two components, a relative
212
  // error of 2 * DBL_ERR in the other component, plus an overall relative
213
  // length error of 4 * DBL_ERR (compared to y_dir_) because "src" is assumed
214
  // to be normalized only to within the tolerances of S2Point::Normalize().
215
  //
216
  // p.DotProd(y_dir_) has a relative error of 1.5 * DBL_ERR and an
217
  // absolute error of 1.5 * DBL_ERR * y_dir_.Norm().
218
  //
219
  // p.DotProd(x_dir_) has a relative error of 5.5 * DBL_ERR and an absolute
220
  // error of 3.5 * DBL_ERR * y_dir_.Norm() (noting that x_dir_ and y_dir_
221
  // have the same length to within a relative error of 4 * DBL_ERR).
222
  //
223
  // It's possible to show by taking derivatives that these errors can affect
224
  // the angle atan2(y, x) by up 7.093 * DBL_ERR radians.  Rounding up and
225
  // including the call to atan2 gives a final error bound of 10 * DBL_ERR.
226
  //
227
  // Summary of the errors in GetSemiwidth:
228
  //
229
  // The distance a2 has a relative error of 5 * DBL_ERR plus an absolute
230
  // error of 64 * DBL_ERR^2 because the points "src" and "p" may differ from
231
  // unit length (by up to 4 * DBL_ERR).  We have already accounted for the
232
  // absolute error above, leaving only the relative error.
233
  //
234
  // sin2_r has a relative error of 2 * DBL_ERR.
235
  //
236
  // sin2_a has a relative error of 12 * DBL_ERR assuming that a2 <= 2,
237
  // i.e. distance(src, p) <= 90 degrees.  (The relative error gets
238
  // arbitrarily larger as this distance approaches 180 degrees.)
239
  //
240
  // semiwidth has a relative error of 17 * DBL_ERR.
241
  //
242
  // Finally, (center +/- semiwidth) has a rounding error of up to 4 * DBL_ERR
243
  // because in theory, the result magnitude may be as large as 1.5 * M_PI
244
  // which is larger than 4.0.  This gives a total error of:
245
0
  double error = (2 * 10 + 4) * DBL_ERR + 17 * DBL_ERR * semiwidth;
246
0
  return semiwidth + round_direction * error;
247
0
}