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