1 /*
2  * Copyright (c) 2019 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 
13 package org.locationtech.jts.operation.union;
14 
15 import java.util.ArrayList;
16 import java.util.HashSet;
17 import java.util.List;
18 import java.util.Set;
19 
20 import org.locationtech.jts.geom.Coordinate;
21 import org.locationtech.jts.geom.CoordinateSequence;
22 import org.locationtech.jts.geom.CoordinateSequenceFilter;
23 import org.locationtech.jts.geom.Envelope;
24 import org.locationtech.jts.geom.Geometry;
25 import org.locationtech.jts.geom.GeometryFactory;
26 import org.locationtech.jts.geom.LineSegment;
27 import org.locationtech.jts.geom.TopologyException;
28 import org.locationtech.jts.geom.util.GeometryCombiner;
29 
30 /**
31  * Unions MultiPolygons efficiently by
32  * using full topological union only for polygons which may overlap,
33  * and combining with the remaining polygons.
34  * Polygons which may overlap are those which intersect the common extent of the inputs.
35  * Polygons wholly outside this extent must be disjoint to the computed union.
36  * They can thus be simply combined with the union result,
37  * which is much more performant.
38  * (There is one caveat to this, which is discussed below).
39  * <p>
40  * This situation is likely to occur during cascaded polygon union,
41  * since the partitioning of polygons is done heuristically
42  * and thus may group disjoint polygons which can lie far apart.
43  * It may also occur in real world data which contains many disjoint polygons
44  * (e.g. polygons representing parcels on different street blocks).
45  *
46  * <h2>Algorithm</h2>
47  * The overlap region is determined as the common envelope of intersection.
48  * The input polygons are partitioned into two sets:
49  * <ul>
50  * <li>Overlapping: Polygons which intersect the overlap region, and thus potentially overlap each other
51  * <li>Disjoint: Polygons which are disjoint from (lie wholly outside) the overlap region
52  * </ul>
53  * The Overlapping set is fully unioned, and then combined with the Disjoint set.
54  * Performing a simple combine works because
55  * the disjoint polygons do not interact with each
56  * other (since the inputs are valid MultiPolygons).
57  * They also do not interact with the Overlapping polygons,
58  * since they are outside their envelope.
59  *
60  * <h2>Discussion</h2>
61  * In general the Overlapping set of polygons will
62  * extend beyond the overlap envelope.  This means that the union result
63  * will extend beyond the overlap region.
64  * There is a small chance that the topological
65  * union of the overlap region will shift the result linework enough
66  * that the result geometry intersects one of the Disjoint geometries.
67  * This situation is detected and if it occurs
68  * is remedied by falling back to performing a full union of the original inputs.
69  * Detection is done by a fairly efficient comparison of edge segments which
70  * extend beyond the overlap region.  If any segments have changed
71  * then there is a risk of introduced intersections, and full union is performed.
72  * <p>
73  * This situation has not been observed in JTS using floating precision,
74  * but it could happen due to snapping.  It has been observed
75  * in other APIs (e.g. GEOS) due to more aggressive snapping.
76  * It is more likely to happen if a Snap-Rounding overlay is used.
77  * <p>
78  * <b>NOTE: Test has shown that using this heuristic impairs performance.
79  * It has been removed from use.</b>
80  *
81  *
82  * @author mbdavis
83  *
84  * @deprecated due to impairing performance
85  *
86  */
87 public class OverlapUnion
88 {
89   /**
90    * Union a pair of geometries,
91    * using the more performant overlap union algorithm if possible.
92    *
93    * @param g0 a geometry to union
94    * @param g1 a geometry to union
95    * @param unionFun
96    * @return the union of the inputs
97    */
union(Geometry g0, Geometry g1, UnionStrategy unionFun)98 	public static Geometry union(Geometry g0, Geometry g1, UnionStrategy unionFun)
99 	{
100 		OverlapUnion union = new OverlapUnion(g0, g1, unionFun);
101 		return union.union();
102 	}
103 
104 	private GeometryFactory geomFactory;
105 
106 	private Geometry g0;
107 	private Geometry g1;
108 
109   private boolean isUnionSafe;
110 
111   private UnionStrategy unionFun;
112 
113 
114   /**
115    * Creates a new instance for unioning the given geometries.
116    *
117    * @param g0 a geometry to union
118    * @param g1 a geometry to union
119    */
OverlapUnion(Geometry g0, Geometry g1)120 	public OverlapUnion(Geometry g0, Geometry g1)
121 	{
122 		this(g0, g1, CascadedPolygonUnion.CLASSIC_UNION);
123 	}
124 
OverlapUnion(Geometry g0, Geometry g1, UnionStrategy unionFun)125 	public OverlapUnion(Geometry g0, Geometry g1, UnionStrategy unionFun) {
126     this.g0 = g0;
127     this.g1 = g1;
128     geomFactory = g0.getFactory();
129     this.unionFun = unionFun;
130   }
131 
132   /**
133    * Unions the input geometries,
134    * using the more performant overlap union algorithm if possible.
135    *
136    * @return the union of the inputs
137 	 */
union()138 	public Geometry union()
139 	{
140     Envelope overlapEnv = overlapEnvelope(g0,  g1);
141 
142     /**
143      * If no overlap, can just combine the geometries
144      */
145     if (overlapEnv.isNull()) {
146       Geometry g0Copy = g0.copy();
147       Geometry g1Copy = g1.copy();
148       return GeometryCombiner.combine(g0Copy, g1Copy);
149     }
150 
151     List<Geometry> disjointPolys = new ArrayList<Geometry>();
152 
153     Geometry g0Overlap = extractByEnvelope(overlapEnv, g0, disjointPolys);
154     Geometry g1Overlap = extractByEnvelope(overlapEnv, g1, disjointPolys);
155 
156 //    System.out.println("# geoms in common: " + intersectingPolys.size());
157     Geometry unionGeom = unionFull(g0Overlap, g1Overlap);
158 
159     Geometry result = null;
160     isUnionSafe = isBorderSegmentsSame(unionGeom, overlapEnv);
161     if (! isUnionSafe) {
162       // overlap union changed border segments... need to do full union
163       //System.out.println("OverlapUnion: Falling back to full union");
164       result = unionFull(g0, g1);
165     }
166     else {
167       //System.out.println("OverlapUnion: fast path");
168       result = combine(unionGeom, disjointPolys);
169     }
170     return result;
171 	}
172 
173 	/**
174 	 * Allows checking whether the optimized
175 	 * or full union was performed.
176 	 * Used for unit testing.
177 	 *
178 	 * @return true if the optimized union was performed
179 	 */
isUnionOptimized()180 	boolean isUnionOptimized() {
181 	  return isUnionSafe;
182 	}
183 
overlapEnvelope(Geometry g0, Geometry g1)184   private static Envelope overlapEnvelope(Geometry g0, Geometry g1) {
185     Envelope g0Env = g0.getEnvelopeInternal();
186     Envelope g1Env = g1.getEnvelopeInternal();
187     Envelope overlapEnv = g0Env.intersection(g1Env);
188     return overlapEnv;
189   }
190 
combine(Geometry unionGeom, List<Geometry> disjointPolys)191   private Geometry combine(Geometry unionGeom, List<Geometry> disjointPolys) {
192     if (disjointPolys.size() <= 0)
193       return unionGeom;
194 
195     disjointPolys.add(unionGeom);
196     Geometry result = GeometryCombiner.combine(disjointPolys);
197     return result;
198   }
199 
extractByEnvelope(Envelope env, Geometry geom, List<Geometry> disjointGeoms)200   private Geometry extractByEnvelope(Envelope env, Geometry geom,
201       List<Geometry> disjointGeoms)
202   {
203     List<Geometry> intersectingGeoms = new ArrayList<Geometry>();
204     for (int i = 0; i < geom.getNumGeometries(); i++) {
205       Geometry elem = geom.getGeometryN(i);
206       if (elem.getEnvelopeInternal().intersects(env)) {
207         intersectingGeoms.add(elem);
208       }
209       else {
210         Geometry copy = elem.copy();
211         disjointGeoms.add(copy);
212       }
213     }
214     return geomFactory.buildGeometry(intersectingGeoms);
215   }
216 
unionFull(Geometry geom0, Geometry geom1)217   private Geometry unionFull(Geometry geom0, Geometry geom1) {
218     // if both are empty collections, just return a copy of one of them
219     if (geom0.getNumGeometries() == 0
220         && geom1.getNumGeometries() == 0)
221       return geom0.copy();
222 
223     Geometry union = unionFun.union(geom0, geom1);
224     //Geometry union = geom0.union(geom1);
225     return union;
226   }
227 
228   /**
229    * Implements union using the buffer-by-zero trick.
230    * This seems to be more robust than overlay union,
231    * for reasons somewhat unknown.
232    *
233    * @param g0 a geometry
234    * @param g1 a geometry
235    * @return the union of the geometries
236    */
unionBuffer(Geometry g0, Geometry g1)237   private static Geometry unionBuffer(Geometry g0, Geometry g1)
238   {
239     GeometryFactory factory = g0.getFactory();
240     Geometry gColl = factory.createGeometryCollection(new Geometry[] { g0, g1 } );
241     Geometry union = gColl.buffer(0.0);
242     return union;
243   }
244 
isBorderSegmentsSame(Geometry result, Envelope env)245   private boolean isBorderSegmentsSame(Geometry result, Envelope env) {
246     List<LineSegment> segsBefore = extractBorderSegments(g0, g1, env);
247 
248     List<LineSegment> segsAfter = new ArrayList<LineSegment>();
249     extractBorderSegments(result, env, segsAfter);
250 
251     //System.out.println("# seg before: " + segsBefore.size() + " - # seg after: " + segsAfter.size());
252     return isEqual(segsBefore, segsAfter);
253   }
254 
isEqual(List<LineSegment> segs0, List<LineSegment> segs1)255   private boolean isEqual(List<LineSegment> segs0, List<LineSegment> segs1) {
256     if (segs0.size() != segs1.size())
257       return false;
258 
259     Set<LineSegment> segIndex = new HashSet<LineSegment>(segs0);
260 
261     for (LineSegment seg : segs1) {
262       if (! segIndex.contains(seg)) {
263         //System.out.println("Found changed border seg: " + seg);
264         return false;
265       }
266     }
267     return true;
268   }
269 
extractBorderSegments(Geometry geom0, Geometry geom1, Envelope env)270   private List<LineSegment> extractBorderSegments(Geometry geom0, Geometry geom1, Envelope env) {
271     List<LineSegment> segs = new ArrayList<LineSegment>();
272     extractBorderSegments(geom0, env, segs);
273     if (geom1 != null)
274       extractBorderSegments(geom1, env, segs);
275     return segs;
276   }
277 
intersects(Envelope env, Coordinate p0, Coordinate p1)278   private static boolean intersects(Envelope env, Coordinate p0, Coordinate p1) {
279     return env.intersects(p0) || env.intersects(p1);
280   }
281 
containsProperly(Envelope env, Coordinate p0, Coordinate p1)282   private static boolean containsProperly(Envelope env, Coordinate p0, Coordinate p1) {
283     return containsProperly(env, p0) && containsProperly(env, p1);
284   }
285 
containsProperly(Envelope env, Coordinate p)286   private static boolean containsProperly(Envelope env, Coordinate p) {
287       if (env.isNull()) return false;
288       return p.getX() > env.getMinX() &&
289           p.getX() < env.getMaxX() &&
290           p.getY() > env.getMinY() &&
291           p.getY() < env.getMaxY();
292   }
293 
extractBorderSegments(Geometry geom, Envelope env, List<LineSegment> segs)294   private static void extractBorderSegments(Geometry geom, Envelope env, List<LineSegment> segs) {
295     geom.apply(new CoordinateSequenceFilter() {
296 
297       public void filter(CoordinateSequence seq, int i) {
298         if (i <= 0) return;
299 
300         // extract LineSegment
301         Coordinate p0 = seq.getCoordinate(i - 1);
302         Coordinate p1 = seq.getCoordinate(i);
303         boolean isBorder = intersects(env, p0, p1) && ! containsProperly(env, p0, p1);
304         if (isBorder) {
305           LineSegment seg = new LineSegment(p0, p1);
306           segs.add(seg);
307         }
308       }
309 
310       public boolean isDone() {   return false;   }
311 
312       public boolean isGeometryChanged() {   return false;   }
313 
314     });
315   }
316 
317 }
318 
319