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 // Original Author: ericv@google.com (Eric Veach)
17 // Converted to D:  madric@gmail.com (Vijay Nayar)
18 
19 module s2.s2polyline_simplifier;
20 
21 import s2.s1chord_angle;
22 import s2.s1interval;
23 import s2.s2point;
24 
25 import std.math;
26 
27 /**
28  * This is a helper class for simplifying polylines.  It allows you to compute
29  * a maximal edge that intersects a sequence of discs, and that optionally
30  * avoids a different sequence of discs.  The results are conservative in that
31  * the edge is guaranteed to intersect or avoid the specified discs using
32  * exact arithmetic (see s2predicates.h).
33  *
34  * Note that S2Builder can also simplify polylines and supports more features
35  * (e.g., snapping to S2CellId centers), so it is only recommended to use this
36  * class if S2Builder does not meet your needs.
37  *
38  * Here is a simple example showing how to simplify a polyline into a sequence
39  * of edges that stay within "max_error" of the original edges:
40  *
41  *   vector<S2Point> v = { ... };
42  *   S2PolylineSimplifier simplifier;
43  *   simplifier.Init(v[0]);
44  *   for (int i = 1; i < v.size(); ++i) {
45  *     if (!simplifier.Extend(v[i])) {
46  *       OutputEdge(simplifier.src(), v[i-1]);
47  *       simplifier.Init(v[i-1]);
48  *     }
49  *     simplifier.TargetDisc(v[i], max_error);
50  *   }
51  *   OutputEdge(simplifer.src(), v.back());
52  *
53  * Note that the points targeted by TargetDisc do not need to be the same as
54  * the candidate endpoints passed to Extend.  So for example, you could target
55  * the original vertices of a polyline, but only consider endpoints that are
56  * snapped to E7 coordinates or S2CellId centers.
57  *
58  * Please be aware that this class works by maintaining a range of acceptable
59  * angles (bearings) from the start vertex to the hypothetical destination
60  * vertex.  It does not keep track of distances to any of the discs to be
61  * targeted or avoided.  Therefore to use this class correctly, constraints
62  * should be added in increasing order of distance.  (The actual requirement
63  * is slightly weaker than this, which is why it is not enforced, but
64  * basically you should only call TargetDisc() and AvoidDisc() with arguments
65  * that you want to constrain the immediately following call to Extend().)
66  */
67 class S2PolylineSimplifier {
68 public:
69   this() {}
70 
71   /// Starts a new simplified edge at "src".
72   void initialize(in S2Point src) {
73     _src = src;
74     _window = S1Interval.full();
75 
76     // Precompute basis vectors for the tangent space at "src".  This is similar
77     // to GetFrame() except that we don't normalize the vectors.  As it turns
78     // out, the two basis vectors below have the same magnitude (up to the
79     // length error in S2Point::Normalize).
80 
81     // Find the index of the component whose magnitude is smallest.
82     S2Point tmp = src.abs();
83     int i = (tmp[0] < tmp[1] ?
84         (tmp[0] < tmp[2] ? 0 : 2) : (tmp[1] < tmp[2] ? 1 : 2));
85 
86     // We define the "y" basis vector as the cross product of "src" and the
87     // basis vector for axis "i".  Let "j" and "k" be the indices of the other
88     // two components in cyclic order.
89     int j = (i == 2 ? 0 : i + 1), k = (i == 0 ? 2 : i - 1);
90     _yDir[i] = 0;
91     _yDir[j] = src[k];
92     _yDir[k] = -src[j];
93 
94     // Compute the cross product of "y_dir" and "src".  We write out the cross
95     // product here mainly for documentation purposes; it also happens to save a
96     // few multiplies because unfortunately the optimizer does *not* get rid of
97     // multiplies by zero (since these multiplies propagate NaN, for example).
98     _xDir[i] = src[j] * src[j] + src[k] * src[k];
99     _xDir[j] = -src[j] * src[i];
100     _xDir[k] = -src[k] * src[i];
101   }
102 
103   // Returns the source vertex of the output edge.
104   //S2Point src() const;
105 
106   /**
107    * Returns true if the edge (src, dst) satisfies all of the targeting
108    * requirements so far.  Returns false if the edge would be longer than
109    * 90 degrees (such edges are not supported).
110    */
111   bool extend(in S2Point dst) const {
112     // We limit the maximum edge length to 90 degrees in order to simplify the
113     // error bounds.  (The error gets arbitrarily large as the edge length
114     // approaches 180 degrees.)
115     if (S1ChordAngle(_src, dst) > S1ChordAngle.right()) return false;
116 
117     // Otherwise check whether this vertex is in the acceptable angle range.
118     return _window.contains(getAngle(dst));
119   }
120 
121   /// Requires that the output edge must pass through the given disc.
122   bool targetDisc(in S2Point p, S1ChordAngle r) {
123     // Shrink the target interval by the maximum error from all sources.  This
124     // guarantees that the output edge will intersect the given disc.
125     double semiwidth = getSemiwidth(p, r, -1 /*round down*/);
126     if (semiwidth >= M_PI) {
127       // The target disc contains "src", so there is nothing to do.
128       return true;
129     }
130     if (semiwidth < 0) {
131       _window = S1Interval.empty();
132       return false;
133     }
134     // Otherwise compute the angle interval corresponding to the target disc and
135     // intersect it with the current window.
136     double center = getAngle(p);
137     S1Interval target = S1Interval.fromPoint(center).expanded(semiwidth);
138     _window = _window.intersection(target);
139     return !_window.isEmpty();
140   }
141 
142   /**
143    * Requires that the output edge must avoid the given disc.  "disc_on_left"
144    * specifies whether the disc must be to the left or right of the edge.
145    * (This feature allows the simplified edge to preserve the topology of the
146    * original polyline with respect to other nearby points.)
147    *
148    * If your input is a polyline, you can compute "disc_on_left" as follows.
149    * Let the polyline be ABCDE and assume that it already avoids a set of
150    * points X_i.  Suppose that you have aleady added ABC to the simplifer, and
151    * now want to extend the edge chain to D.  First find the X_i that are near
152    * the edge CD, then discard the ones such that AX_i <= AC or AX_i >= AD
153    * (since these points have either already been considered or aren't
154    * relevant yet).  Now X_i is to the left of the polyline if and only if
155    * s2pred::OrderedCCW(A, D, X, C) (in other words, if X_i is to the left of
156    * the angle wedge ACD).
157    */
158   bool avoidDisc(in S2Point p, S1ChordAngle r, bool disc_on_left) {
159     // Expand the interval by the maximum error from all sources.  This
160     // guarantees that the final output edge will avoid the given disc.
161     double semiwidth = getSemiwidth(p, r, 1 /*round up*/);
162     if (semiwidth >= M_PI) {
163       // The avoidance disc contains "src", so it is impossible to avoid.
164       _window = S1Interval.empty();
165       return false;
166     }
167     double center = getAngle(p);
168     double opposite = (center > 0) ? center - M_PI : center + M_PI;
169     S1Interval target =
170         (disc_on_left ? S1Interval(opposite, center) : S1Interval(center, opposite));
171     _window = _window.intersection(target.expanded(-semiwidth));
172     return !_window.isEmpty();
173   }
174 
175 private:
176   double getAngle(in S2Point p) const {
177     return atan2(p.dotProd(_yDir), p.dotProd(_xDir));
178   }
179   double getSemiwidth(in S2Point p, S1ChordAngle r, int round_direction) const {
180     enum double DBL_ERR = 0.5 * double.epsilon;
181 
182     // Using spherical trigonometry,
183     //
184     //   sin(semiwidth) = sin(r) / sin(a)
185     //
186     // where "a" is the angle between "src" and "p".  Rather than measuring
187     // these angles, instead we measure the squared chord lengths through the
188     // interior of the sphere (i.e., Cartersian distance).  Letting "r2" be the
189     // squared chord distance corresponding to "r", and "a2" be the squared
190     // chord distance corresponding to "a", we use the relationships
191     //
192     //    sin^2(r) = r2 (1 - r2 / 4)
193     //    sin^2(a) = d2 (1 - d2 / 4)
194     //
195     // which follow from the fact that r2 = (2 * sin(r / 2)) ^ 2, etc.
196 
197     // "a2" has a relative error up to 5 * DBL_ERR, plus an absolute error of up
198     // to 64 * DBL_ERR^2 (because "src" and "p" may differ from unit length by
199     // up to 4 * DBL_ERR).  We can correct for the relative error later, but for
200     // the absolute error we use "round_direction" to account for it now.
201     double r2 = r.length2();
202     double a2 = S1ChordAngle(_src, p).length2();
203     a2 -= 64 * DBL_ERR * DBL_ERR * round_direction;
204     if (a2 <= r2) return M_PI;  // The given disc contains "src".
205 
206     double sin2_r = r2 * (1 - 0.25 * r2);
207     double sin2_a = a2 * (1 - 0.25 * a2);
208     double semiwidth = asin(sqrt(sin2_r / sin2_a));
209 
210     // We compute bounds on the errors from all sources:
211     //
212     //   - The call to GetSemiwidth (this call).
213     //   - The call to GetAngle that computes the center of the interval.
214     //   - The call to GetAngle in Extend that tests whether a given point
215     //     is an acceptable destination vertex.
216     //
217     // Summary of the errors in GetAngle:
218     //
219     // y_dir_ has no error.
220     //
221     // x_dir_ has a relative error of DBL_ERR in two components, a relative
222     // error of 2 * DBL_ERR in the other component, plus an overall relative
223     // length error of 4 * DBL_ERR (compared to y_dir_) because "src" is assumed
224     // to be normalized only to within the tolerances of S2Point::Normalize().
225     //
226     // p.DotProd(y_dir_) has a relative error of 1.5 * DBL_ERR and an
227     // absolute error of 1.5 * DBL_ERR * y_dir_.Norm().
228     //
229     // p.DotProd(x_dir_) has a relative error of 5.5 * DBL_ERR and an absolute
230     // error of 3.5 * DBL_ERR * y_dir_.Norm() (noting that x_dir_ and y_dir_
231     // have the same length to within a relative error of 4 * DBL_ERR).
232     //
233     // It's possible to show by taking derivatives that these errors can affect
234     // the angle atan2(y, x) by up 7.093 * DBL_ERR radians.  Rounding up and
235     // including the call to atan2 gives a final error bound of 10 * DBL_ERR.
236     //
237     // Summary of the errors in GetSemiwidth:
238     //
239     // The distance a2 has a relative error of 5 * DBL_ERR plus an absolute
240     // error of 64 * DBL_ERR^2 because the points "src" and "p" may differ from
241     // unit length (by up to 4 * DBL_ERR).  We have already accounted for the
242     // absolute error above, leaving only the relative error.
243     //
244     // sin2_r has a relative error of 2 * DBL_ERR.
245     //
246     // sin2_a has a relative error of 12 * DBL_ERR assuming that a2 <= 2,
247     // i.e. distance(src, p) <= 90 degrees.  (The relative error gets
248     // arbitrarily larger as this distance approaches 180 degrees.)
249     //
250     // semiwidth has a relative error of 17 * DBL_ERR.
251     //
252     // Finally, (center +/- semiwidth) has a rounding error of up to 4 * DBL_ERR
253     // because in theory, the result magnitude may be as large as 1.5 * M_PI
254     // which is larger than 4.0.  This gives a total error of:
255     double error = (2 * 10 + 4) * DBL_ERR + 17 * DBL_ERR * semiwidth;
256     return semiwidth + round_direction * error;
257   }
258 
259   S2Point _src;
260   S2Point _xDir, _yDir;
261   S1Interval _window;
262 }