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 }