1 /* 2 * Copyright (c) 2020 Martin Davis. 3 * 4 * All rights reserved. This program and the accompanying materials 5 * are made available under the terms of the Eclipse Public License 2.0 6 * and Eclipse Distribution License v. 1.0 which accompanies this distribution. 7 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html 8 * and the Eclipse Distribution License is available at 9 * 10 * http://www.eclipse.org/org/documents/edl-v10.php. 11 */ 12 package org.locationtech.jts.operation.overlayng; 13 14 import java.util.Collection; 15 16 import org.locationtech.jts.geom.Envelope; 17 import org.locationtech.jts.geom.Geometry; 18 import org.locationtech.jts.geom.GeometryFactory; 19 import org.locationtech.jts.geom.PrecisionModel; 20 import org.locationtech.jts.geom.TopologyException; 21 import org.locationtech.jts.noding.ValidatingNoder; 22 import org.locationtech.jts.noding.snap.SnappingNoder; 23 import org.locationtech.jts.operation.union.UnaryUnionOp; 24 import org.locationtech.jts.operation.union.UnionStrategy; 25 26 /** 27 * Performs an overlay operation using {@link OverlayNG}, 28 * providing full robustness by using a series of 29 * increasingly robust (but slower) noding strategies. 30 * <p> 31 * The noding strategies used are: 32 * <ol> 33 * <li>A simple, fast noder using FLOATING precision. 34 * <li>A {@link SnappingNoder} using an automatically-determined snap tolerance 35 * <li>First snapping each geometry to itself, 36 * and then overlaying them using a <code>SnappingNoder</code>. 37 * <li>The above two strategies are repeated with increasing snap tolerance, up to a limit. 38 * <li>Finally a {@link SnapRoundngNoder} is used with a automatically-determined scale factor 39 * intended to preserve input precision while still preventing robustness problems. 40 * </ol> 41 * If all of the above attempts fail to compute a valid overlay, 42 * the original {@link TopologyException} is thrown. 43 * In practice this is extremely unlikely to occur. 44 * <p> 45 * This algorithm relies on each overlay operation execution 46 * throwing a {@link TopologyException} if it is unable 47 * to compute the overlay correctly. 48 * Generally this occurs because the noding phase does 49 * not produce a valid noding. 50 * This requires the use of a {@link ValidatingNoder} 51 * in order to check the results of using a floating noder. 52 * 53 * @author Martin Davis 54 * 55 * @see OverlayNG 56 */ 57 public class OverlayNGRobust 58 { 59 /** 60 * Computes the unary union of a geometry using robust computation. 61 * 62 * @param geom the geometry to union 63 * @return the union result 64 * 65 * @see UnaryUnionOp 66 */ union(Geometry geom)67 public static Geometry union(Geometry geom) { 68 UnaryUnionOp op = new UnaryUnionOp(geom); 69 op.setUnionFunction(OVERLAY_UNION); 70 return op.union(); 71 } 72 73 /** 74 * Computes the unary union of a collection of geometries using robust computation. 75 * 76 * @param geoms the collection of geometries to union 77 * @return the union result 78 * 79 * @see UnaryUnionOp 80 */ union(Collection<Geometry> geoms)81 public static Geometry union(Collection<Geometry> geoms) { 82 UnaryUnionOp op = new UnaryUnionOp(geoms); 83 op.setUnionFunction(OVERLAY_UNION); 84 return op.union(); 85 } 86 87 /** 88 * Computes the unary union of a collection of geometries using robust computation. 89 * 90 * @param geoms the collection of geometries to union 91 * @param geomFact the geometry factory to use 92 * @return the union of the geometries 93 */ union(Collection<Geometry> geoms, GeometryFactory geomFact)94 public static Geometry union(Collection<Geometry> geoms, GeometryFactory geomFact) { 95 UnaryUnionOp op = new UnaryUnionOp(geoms, geomFact); 96 op.setUnionFunction(OVERLAY_UNION); 97 return op.union(); 98 } 99 100 private static UnionStrategy OVERLAY_UNION = new UnionStrategy() { 101 102 public Geometry union(Geometry g0, Geometry g1) { 103 return overlay(g0, g1, OverlayNG.UNION ); 104 } 105 106 @Override 107 public boolean isFloatingPrecision() { 108 return true; 109 } 110 }; 111 112 /** 113 * Overlay two geometries, using heuristics to ensure 114 * computation completes correctly. 115 * In practice the heuristics are observed to be fully correct. 116 * 117 * @param geom0 a geometry 118 * @param geom1 a geometry 119 * @param opCode the overlay operation code (from {@link OverlayNG} 120 * @return the overlay result geometry 121 * 122 * @see OverlayNG 123 */ overlay(Geometry geom0, Geometry geom1, int opCode)124 public static Geometry overlay(Geometry geom0, Geometry geom1, int opCode) 125 { 126 Geometry result; 127 RuntimeException exOriginal; 128 129 /** 130 * First try overlay with a FLOAT noder, which is fast and causes least 131 * change to geometry coordinates 132 * By default the noder is validated, which is required in order 133 * to detect certain invalid noding situations which otherwise 134 * cause incorrect overlay output. 135 */ 136 try { 137 result = OverlayNG.overlay(geom0, geom1, opCode ); 138 return result; 139 } 140 catch (RuntimeException ex) { 141 /** 142 * Capture original exception, 143 * so it can be rethrown if the remaining strategies all fail. 144 */ 145 exOriginal = ex; 146 } 147 148 /** 149 * On failure retry using snapping noding with a "safe" tolerance. 150 * if this throws an exception just let it go, 151 * since it is something that is not a TopologyException 152 */ 153 result = overlaySnapTries(geom0, geom1, opCode); 154 if (result != null) 155 return result; 156 157 /** 158 * On failure retry using snap-rounding with a heuristic scale factor (grid size). 159 */ 160 result = overlaySR(geom0, geom1, opCode); 161 if (result != null) 162 return result; 163 164 /** 165 * Just can't get overlay to work, so throw original error. 166 */ 167 throw exOriginal; 168 } 169 170 private static final int NUM_SNAP_TRIES = 5; 171 172 /** 173 * Attempt overlay using snapping with repeated tries with increasing snap tolerances. 174 * 175 * @param geom0 176 * @param geom1 177 * @param opCode 178 * @return the computed overlay result, or null if the overlay fails 179 */ overlaySnapTries(Geometry geom0, Geometry geom1, int opCode)180 private static Geometry overlaySnapTries(Geometry geom0, Geometry geom1, int opCode) { 181 Geometry result; 182 double snapTol = snapTolerance(geom0, geom1); 183 184 for (int i = 0; i < NUM_SNAP_TRIES; i++) { 185 186 result = overlaySnapping(geom0, geom1, opCode, snapTol); 187 if (result != null) return result; 188 189 /** 190 * Now try snapping each input individually, 191 * and then doing the overlay. 192 */ 193 result = overlaySnapBoth(geom0, geom1, opCode, snapTol); 194 if (result != null) return result; 195 196 // increase the snap tolerance and try again 197 snapTol = snapTol * 10; 198 } 199 // failed to compute overlay 200 return null; 201 } 202 203 /** 204 * Attempt overlay using a {@link SnappingNoder}. 205 * 206 * @param geom0 207 * @param geom1 208 * @param opCode 209 * @param snapTol 210 * @return the computed overlay result, or null if the overlay fails 211 */ overlaySnapping(Geometry geom0, Geometry geom1, int opCode, double snapTol)212 private static Geometry overlaySnapping(Geometry geom0, Geometry geom1, int opCode, double snapTol) { 213 try { 214 return overlaySnapTol(geom0, geom1, opCode, snapTol); 215 } 216 catch (TopologyException ex) { 217 //---- ignore exception, return null result to indicate failure 218 219 //System.out.println("Snapping with " + snapTol + " - FAILED"); 220 //log("Snapping with " + snapTol + " - FAILED", geom0, geom1); 221 } 222 return null; 223 } 224 225 /** 226 * Attempt overlay with first snapping each geometry individually. 227 * 228 * @param geom0 229 * @param geom1 230 * @param opCode 231 * @param snapTol 232 * @return the computed overlay result, or null if the overlay fails 233 */ overlaySnapBoth(Geometry geom0, Geometry geom1, int opCode, double snapTol)234 private static Geometry overlaySnapBoth(Geometry geom0, Geometry geom1, int opCode, double snapTol) { 235 try { 236 Geometry snap0 = snapSelf(geom0, snapTol); 237 Geometry snap1 = snapSelf(geom1, snapTol); 238 //log("Snapping BOTH with " + snapTol, geom0, geom1); 239 240 return overlaySnapTol(snap0, snap1, opCode, snapTol); 241 } 242 catch (TopologyException ex) { 243 //---- ignore exception, return null result to indicate failure 244 } 245 return null; 246 } 247 248 /** 249 * Self-snaps a geometry by running a union operation with it as the only input. 250 * This helps to remove narrow spike/gore artifacts to simplify the geometry, 251 * which improves robustness. 252 * Collapsed artifacts are removed from the result to allow using 253 * it in further overlay operations. 254 * 255 * @param geom geometry to self-snap 256 * @param snapTol snap tolerance 257 * @return the snapped geometry (homogeneous) 258 */ snapSelf(Geometry geom, double snapTol)259 private static Geometry snapSelf(Geometry geom, double snapTol) { 260 OverlayNG ov = new OverlayNG(geom, null); 261 SnappingNoder snapNoder = new SnappingNoder(snapTol); 262 ov.setNoder(snapNoder); 263 /** 264 * Ensure the result is not mixed-dimension, 265 * since it will be used in further overlay computation. 266 * It may however be lower dimension, if it collapses completely due to snapping. 267 */ 268 ov.setStrictMode(true); 269 return ov.getResult(); 270 } 271 overlaySnapTol(Geometry geom0, Geometry geom1, int opCode, double snapTol)272 private static Geometry overlaySnapTol(Geometry geom0, Geometry geom1, int opCode, double snapTol) { 273 SnappingNoder snapNoder = new SnappingNoder(snapTol); 274 return OverlayNG.overlay(geom0, geom1, opCode, snapNoder); 275 } 276 277 //============================================ 278 279 /** 280 * A factor for a snapping tolerance distance which 281 * should allow noding to be computed robustly. 282 */ 283 private static final double SNAP_TOL_FACTOR = 1e12; 284 285 /** 286 * Computes a heuristic snap tolerance distance 287 * for overlaying a pair of geometries using a {@link SnappingNoder}. 288 * 289 * @param geom0 290 * @param geom1 291 * @return the snap tolerance 292 */ snapTolerance(Geometry geom0, Geometry geom1)293 private static double snapTolerance(Geometry geom0, Geometry geom1) { 294 double tol0 = snapTolerance(geom0); 295 double tol1 = snapTolerance(geom1); 296 double snapTol = Math.max(tol0, tol1); 297 return snapTol; 298 } 299 snapTolerance(Geometry geom)300 private static double snapTolerance(Geometry geom) { 301 double magnitude = ordinateMagnitude(geom); 302 return magnitude / SNAP_TOL_FACTOR; 303 } 304 305 /** 306 * Computes the largest magnitude of the ordinates of a geometry, 307 * based on the geometry envelope. 308 * 309 * @param geom a geometry 310 * @return the magnitude of the largest ordinate 311 */ ordinateMagnitude(Geometry geom)312 private static double ordinateMagnitude(Geometry geom) { 313 if (geom == null || geom.isEmpty()) return 0; 314 Envelope env = geom.getEnvelopeInternal(); 315 double magMax = Math.max( 316 Math.abs(env.getMaxX()), Math.abs(env.getMaxY())); 317 double magMin = Math.max( 318 Math.abs(env.getMinX()), Math.abs(env.getMinY())); 319 return Math.max(magMax, magMin); 320 } 321 322 //=============================================== 323 /* 324 private static void log(String msg, Geometry geom0, Geometry geom1) { 325 System.out.println(msg); 326 System.out.println(geom0); 327 System.out.println(geom1); 328 } 329 */ 330 331 /** 332 * Attempt Overlay using Snap-Rounding with an automatically-determined 333 * scale factor. 334 * 335 * @param geom0 336 * @param geom1 337 * @param opCode 338 * @return the computed overlay result, or null if the overlay fails 339 */ overlaySR(Geometry geom0, Geometry geom1, int opCode)340 private static Geometry overlaySR(Geometry geom0, Geometry geom1, int opCode) 341 { 342 Geometry result; 343 try { 344 //System.out.println("OverlaySnapIfNeeded: trying snap-rounding"); 345 double scaleSafe = PrecisionUtil.safeScale(geom0, geom1); 346 PrecisionModel pmSafe = new PrecisionModel(scaleSafe); 347 result = OverlayNG.overlay(geom0, geom1, opCode, pmSafe); 348 return result; 349 } 350 catch (TopologyException ex) { 351 //---- ignore exception, return null result to indicate failure 352 } 353 return null; 354 } 355 356 } 357