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