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