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 package org.locationtech.jts.operation.overlayng; 13 14 import java.util.Collection; 15 import java.util.List; 16 17 import org.locationtech.jts.geom.Envelope; 18 import org.locationtech.jts.geom.Geometry; 19 import org.locationtech.jts.geom.GeometryFactory; 20 import org.locationtech.jts.geom.LineString; 21 import org.locationtech.jts.geom.Location; 22 import org.locationtech.jts.geom.Point; 23 import org.locationtech.jts.geom.Polygon; 24 import org.locationtech.jts.geom.PrecisionModel; 25 import org.locationtech.jts.geom.TopologyException; 26 import org.locationtech.jts.geomgraph.Label; 27 import org.locationtech.jts.noding.MCIndexNoder; 28 import org.locationtech.jts.noding.Noder; 29 import org.locationtech.jts.noding.snap.SnappingNoder; 30 import org.locationtech.jts.noding.snapround.SnapRoundingNoder; 31 import org.locationtech.jts.operation.overlay.OverlayOp; 32 33 /** 34 * Computes the geometric overlay of two {@link Geometry}s, 35 * using an explicit precision model to allow robust computation. 36 * The overlay can be used to determine any of the 37 * following set-theoretic operations (boolean combinations) of the geometries: 38 * <ul> 39 * <li>{@link INTERSECTION} - all points which lie in both geometries 40 * <li>{@link UNION} - all points which lie in at least one geometry 41 * <li>{@link DIFFERENCE} - all points which lie in the first geometry but not the second 42 * <li>{@link SYMDIFFERENCE} - all points which lie in one geometry but not both 43 * </ul> 44 * Input geometries may have different dimension. 45 * Input collections must be homogeneous (all elements must have the same dimension). 46 * <p> 47 * The precision model used for the computation can be supplied 48 * independent of the precision model of the input geometry. 49 * The main use for this is to allow using a fixed precision 50 * for geometry with a floating precision model. 51 * This does two things: ensures robust computation; 52 * and forces the output to be validly rounded to the precision model. 53 * <p> 54 * For fixed precision models noding is performed using a {@link SnapRoundingNoder}. 55 * This provides robust computation (as long as precision is limited to 56 * around 13 decimal digits). 57 * <p> 58 * For floating precision an {@link MCIndexNoder} is used. 59 * This is not fully robust, so can sometimes result in 60 * {@link TopologyException}s being thrown. 61 * For robust full-precision overlay see {@link OverlayNGRobust}. 62 * <p> 63 * A custom {@link Noder} can be supplied. 64 * This allows using a more performant noding strategy in specific cases, 65 * for instance in {@link CoverageUnion}. 66 * <p> 67 * <b>Note:</b If a {@link SnappingNoder} is used 68 * it is best to specify a fairly small snap tolerance, 69 * since the intersection clipping optimization can 70 * interact with the snapping to alter the result. 71 * <p> 72 * Optionally the overlay computation can process using strict mode 73 * (via {@link #setStrictMode(boolean)}. 74 * In strict mode result semantics are: 75 * <ul> 76 * <li>Lines and Points resulting from topology collapses are not included in the result</li> 77 * <li>Result geometry is homogeneous 78 * for the {@link #INTERSECTION} and {@link #DIFFERENCE} operations. 79 * <li>Result geometry is homogeneous 80 * for the {@link #UNION} and {@link #SYMDIFFERENCE} operations 81 * if the inputs have the same dimension</li> 82 * </ul> 83 * Strict mode has the following benefits: 84 * <ul> 85 * <li>Results are simpler</li> 86 * <li>Overlay operations are chainable 87 * without needing to remove lower-dimension elements</li> 88 * </ul> 89 * The original JTS overlay semantics corresponds to non-strict mode. 90 * <p> 91 * If a robustness error occurs, a {@link TopologyException} is thrown. 92 * These are usually caused by numerical rounding causing the noding output 93 * to not be fully noded. 94 * For robust computation with full-precision {@link OverlayNGRobust} can be used. 95 * 96 * @author mdavis 97 * 98 * @see OverlayNGRobust 99 * 100 */ 101 public class OverlayNG 102 { 103 /** 104 * The code for the Intersection overlay operation. 105 */ 106 public static final int INTERSECTION = OverlayOp.INTERSECTION; 107 108 /** 109 * The code for the Union overlay operation. 110 */ 111 public static final int UNION = OverlayOp.UNION; 112 113 /** 114 * The code for the Difference overlay operation. 115 */ 116 public static final int DIFFERENCE = OverlayOp.DIFFERENCE; 117 118 /** 119 * The code for the Symmetric Difference overlay operation. 120 */ 121 public static final int SYMDIFFERENCE = OverlayOp.SYMDIFFERENCE; 122 123 /** 124 * The default setting for Strict Mode. 125 * 126 * The original JTS overlay semantics used non-strict result 127 * semantics, including; 128 * - An Intersection result can be mixed-dimension, 129 * due to inclusion of intersection components of all dimensions 130 * - Results can include lines caused by Area topology collapse 131 */ 132 static final boolean STRICT_MODE_DEFAULT = false; 133 134 /** 135 * Tests whether a point with a given topological {@link Label} 136 * relative to two geometries is contained in 137 * the result of overlaying the geometries using 138 * a given overlay operation. 139 * <p> 140 * The method handles arguments of {@link Location#NONE} correctly 141 * 142 * @param label the topological label of the point 143 * @param opCode the code for the overlay operation to test 144 * @return true if the label locations correspond to the overlayOpCode 145 */ isResultOfOpPoint(OverlayLabel label, int opCode)146 static boolean isResultOfOpPoint(OverlayLabel label, int opCode) 147 { 148 int loc0 = label.getLocation(0); 149 int loc1 = label.getLocation(1); 150 return isResultOfOp(opCode, loc0, loc1); 151 } 152 153 /** 154 * Tests whether a point with given {@link Location}s 155 * relative to two geometries would be contained in 156 * the result of overlaying the geometries using 157 * a given overlay operation. 158 * This is used to determine whether components 159 * computed during the overlay process should be 160 * included in the result geometry. 161 * <p> 162 * The method handles arguments of {@link Location#NONE} correctly. 163 * 164 * @param overlayOpCode the code for the overlay operation to test 165 * @param loc0 the code for the location in the first geometry 166 * @param loc1 the code for the location in the second geometry 167 * 168 * @return true if a point with given locations is in the result of the overlay operation 169 */ isResultOfOp(int overlayOpCode, int loc0, int loc1)170 static boolean isResultOfOp(int overlayOpCode, int loc0, int loc1) 171 { 172 if (loc0 == Location.BOUNDARY) loc0 = Location.INTERIOR; 173 if (loc1 == Location.BOUNDARY) loc1 = Location.INTERIOR; 174 switch (overlayOpCode) { 175 case INTERSECTION: 176 return loc0 == Location.INTERIOR 177 && loc1 == Location.INTERIOR; 178 case UNION: 179 return loc0 == Location.INTERIOR 180 || loc1 == Location.INTERIOR; 181 case DIFFERENCE: 182 return loc0 == Location.INTERIOR 183 && loc1 != Location.INTERIOR; 184 case SYMDIFFERENCE: 185 return ( loc0 == Location.INTERIOR && loc1 != Location.INTERIOR) 186 || ( loc0 != Location.INTERIOR && loc1 == Location.INTERIOR); 187 } 188 return false; 189 } 190 191 /** 192 * Computes an overlay operation for 193 * the given geometry operands, with the 194 * noding strategy determined by the precision model. 195 * 196 * @param geom0 the first geometry argument 197 * @param geom1 the second geometry argument 198 * @param opCode the code for the desired overlay operation 199 * @param pm the precision model to use 200 * @return the result of the overlay operation 201 */ overlay(Geometry geom0, Geometry geom1, int opCode, PrecisionModel pm)202 public static Geometry overlay(Geometry geom0, Geometry geom1, 203 int opCode, PrecisionModel pm) 204 { 205 OverlayNG ov = new OverlayNG(geom0, geom1, pm, opCode); 206 Geometry geomOv = ov.getResult(); 207 return geomOv; 208 } 209 210 /** 211 * Computes an overlay operation on the given geometry operands, 212 * using a supplied {@link Noder}. 213 * 214 * @param geom0 the first geometry argument 215 * @param geom1 the second geometry argument 216 * @param opCode the code for the desired overlay operation 217 * @param pm the precision model to use (which may be null if the noder does not use one) 218 * @param noder the noder to use 219 * @return the result of the overlay operation 220 */ overlay(Geometry geom0, Geometry geom1, int opCode, PrecisionModel pm, Noder noder)221 public static Geometry overlay(Geometry geom0, Geometry geom1, 222 int opCode, PrecisionModel pm, Noder noder) 223 { 224 OverlayNG ov = new OverlayNG(geom0, geom1, pm, opCode); 225 ov.setNoder(noder); 226 Geometry geomOv = ov.getResult(); 227 return geomOv; 228 } 229 230 /** 231 * Computes an overlay operation on the given geometry operands, 232 * using a supplied {@link Noder}. 233 * 234 * @param geom0 the first geometry argument 235 * @param geom1 the second geometry argument 236 * @param opCode the code for the desired overlay operation 237 * @param noder the noder to use 238 * @return the result of the overlay operation 239 */ overlay(Geometry geom0, Geometry geom1, int opCode, Noder noder)240 public static Geometry overlay(Geometry geom0, Geometry geom1, 241 int opCode, Noder noder) 242 { 243 OverlayNG ov = new OverlayNG(geom0, geom1, null, opCode); 244 ov.setNoder(noder); 245 Geometry geomOv = ov.getResult(); 246 return geomOv; 247 } 248 249 /** 250 * Computes an overlay operation on 251 * the given geometry operands, 252 * using the precision model of the geometry. 253 * and an appropriate noder. 254 * <p> 255 * The noder is chosen according to the precision model specified. 256 * <ul> 257 * <li>For {@link PrecisionModel#FIXED} 258 * a snap-rounding noder is used, and the computation is robust. 259 * <li>For {@link PrecisionModel#FLOATING} 260 * a non-snapping noder is used, 261 * and this computation may not be robust. 262 * If errors occur a {@link TopologyException} is thrown. 263 * </ul> 264 * 265 * 266 * @param geom0 the first argument geometry 267 * @param geom1 the second argument geometry 268 * @param opCode the code for the desired overlay operation 269 * @return the result of the overlay operation 270 */ overlay(Geometry geom0, Geometry geom1, int opCode)271 public static Geometry overlay(Geometry geom0, Geometry geom1, int opCode) 272 { 273 OverlayNG ov = new OverlayNG(geom0, geom1, opCode); 274 return ov.getResult(); 275 } 276 277 /** 278 * Computes a union operation on 279 * the given geometry, with the supplied precision model. 280 * <p> 281 * The input must be a valid geometry. 282 * Collections must be homogeneous. 283 * <p> 284 * To union an overlapping set of polygons in a more performant way use {@link UnaryUnionNG}. 285 * To union a polyonal coverage or linear network in a more performant way, 286 * use {@link CoverageUnion}. 287 * 288 * @param geom0 the geometry 289 * @param pm the precision model to use 290 * @return the result of the union operation 291 * 292 * @see OverlayMixedPoints 293 */ union(Geometry geom, PrecisionModel pm)294 static Geometry union(Geometry geom, PrecisionModel pm) 295 { 296 OverlayNG ov = new OverlayNG(geom, pm); 297 Geometry geomOv = ov.getResult(); 298 return geomOv; 299 } 300 301 /** 302 * Computes a union of a single geometry using a custom noder. 303 * <p> 304 * The primary use of this is to support coverage union. 305 * Because of this the overlay is performed using strict mode. 306 * 307 * @param geom the geometry to union 308 * @param pm the precision model to use (maybe be null) 309 * @param noder the noder to use 310 * @return the result geometry 311 * 312 * @see CoverageUnion 313 */ union(Geometry geom, PrecisionModel pm, Noder noder)314 static Geometry union(Geometry geom, PrecisionModel pm, Noder noder) 315 { 316 OverlayNG ov = new OverlayNG(geom, pm); 317 ov.setNoder(noder); 318 ov.setStrictMode(true); 319 Geometry geomOv = ov.getResult(); 320 return geomOv; 321 } 322 323 private int opCode; 324 private InputGeometry inputGeom; 325 private GeometryFactory geomFact; 326 private PrecisionModel pm; 327 private Noder noder; 328 private boolean isStrictMode = STRICT_MODE_DEFAULT; 329 private boolean isOptimized = true; 330 private boolean isAreaResultOnly = false; 331 private boolean isOutputEdges = false; 332 private boolean isOutputResultEdges = false; 333 private boolean isOutputNodedEdges = false; 334 335 /** 336 * Creates an overlay operation on the given geometries, 337 * with a defined precision model. 338 * The noding strategy is determined by the precision model. 339 * 340 * @param geom0 the A operand geometry 341 * @param geom1 the B operand geometry (may be null) 342 * @param pm the precision model to use 343 * @param opCode the overlay opcode 344 */ OverlayNG(Geometry geom0, Geometry geom1, PrecisionModel pm, int opCode)345 public OverlayNG(Geometry geom0, Geometry geom1, PrecisionModel pm, int opCode) { 346 this.pm = pm; 347 this.opCode = opCode; 348 geomFact = geom0.getFactory(); 349 inputGeom = new InputGeometry( geom0, geom1 ); 350 } 351 352 /** 353 * Creates an overlay operation on the given geometries 354 * using the precision model of the geometries. 355 * <p> 356 * The noder is chosen according to the precision model specified. 357 * <ul> 358 * <li>For {@link PrecisionModel#FIXED} 359 * a snap-rounding noder is used, and the computation is robust. 360 * <li>For {@link PrecisionModel#FLOATING} 361 * a non-snapping noder is used, 362 * and this computation may not be robust. 363 * If errors occur a {@link TopologyException} is thrown. 364 * </ul> 365 * 366 * @param geom0 the A operand geometry 367 * @param geom1 the B operand geometry (may be null) 368 * @param opCode the overlay opcode 369 */ OverlayNG(Geometry geom0, Geometry geom1, int opCode)370 public OverlayNG(Geometry geom0, Geometry geom1, int opCode) { 371 this(geom0, geom1, geom0.getFactory().getPrecisionModel(), opCode); 372 } 373 374 /** 375 * Creates a union of a single geometry with a given precision model. 376 * 377 * @param geom the geometry 378 * @param pm the precision model to use 379 */ OverlayNG(Geometry geom, PrecisionModel pm)380 OverlayNG(Geometry geom, PrecisionModel pm) { 381 this(geom, null, pm, UNION); 382 } 383 384 /** 385 * Sets whether the overlay results are computed according to strict mode 386 * semantics. 387 * <ul> 388 * <li>Lines resulting from topology collapse are not included 389 * <li>Result geometry is homogeneous 390 * for the {@link #INTERSECTION} and {@link #DIFFERENCE} operations. 391 * <li>Result geometry is homogeneous 392 * for the {@link #UNION} and {@link #SYMDIFFERENCE} operations 393 * if the inputs have the same dimension 394 * </ul> 395 * 396 * @param isStrictMode true if strict mode is to be used 397 */ setStrictMode(boolean isStrictMode)398 public void setStrictMode(boolean isStrictMode) { 399 this.isStrictMode = isStrictMode; 400 } 401 402 /** 403 * Sets whether overlay processing optimizations are enabled. 404 * It may be useful to disable optimizations 405 * for testing purposes. 406 * Default is TRUE (optimization enabled). 407 * 408 * @param isOptimized whether to optimize processing 409 */ setOptimized(boolean isOptimized)410 public void setOptimized(boolean isOptimized) { 411 this.isOptimized = isOptimized; 412 } 413 414 /** 415 * Sets whether the result can contain only {@link Polygon} components. 416 * This is used if it is known that the result must be an (possibly empty) area. 417 * 418 * @param isAreaResultOnly true if the result should contain only area components 419 */ setAreaResultOnly(boolean isAreaResultOnly)420 void setAreaResultOnly(boolean isAreaResultOnly) { 421 this.isAreaResultOnly = isAreaResultOnly; 422 } 423 424 //------ Testing options ------- 425 426 /** 427 * 428 * @param isOutputEdges 429 */ setOutputEdges(boolean isOutputEdges )430 public void setOutputEdges(boolean isOutputEdges ) { 431 this.isOutputEdges = isOutputEdges; 432 } 433 setOutputNodedEdges(boolean isOutputNodedEdges )434 public void setOutputNodedEdges(boolean isOutputNodedEdges ) { 435 this.isOutputEdges = true; 436 this.isOutputNodedEdges = isOutputNodedEdges; 437 } 438 setOutputResultEdges(boolean isOutputResultEdges )439 public void setOutputResultEdges(boolean isOutputResultEdges ) { 440 this.isOutputResultEdges = isOutputResultEdges; 441 } 442 //--------------------------------- 443 setNoder(Noder noder)444 void setNoder(Noder noder) { 445 this.noder = noder; 446 } 447 448 /** 449 * Gets the result of the overlay operation. 450 * 451 * @return the result of the overlay operation. 452 * 453 * @throws IllegalArgumentException if the input is not supported (e.g. a mixed-dimension geometry) 454 * @throws TopologyException if a robustness error occurs 455 */ getResult()456 public Geometry getResult() { 457 // handle empty inputs which determine result 458 if (OverlayUtil.isEmptyResult(opCode, 459 inputGeom.getGeometry(0), 460 inputGeom.getGeometry(1), 461 pm)) { 462 return createEmptyResult(); 463 } 464 465 /** 466 * The elevation model is only computed if the input geometries have Z values. 467 */ 468 ElevationModel elevModel = ElevationModel.create(inputGeom.getGeometry(0), inputGeom.getGeometry(1)); 469 Geometry result; 470 if (inputGeom.isAllPoints()) { 471 // handle Point-Point inputs 472 result = OverlayPoints.overlay(opCode, inputGeom.getGeometry(0), inputGeom.getGeometry(1), pm); 473 } 474 else if (! inputGeom.isSingle() && inputGeom.hasPoints()) { 475 // handle Point-nonPoint inputs 476 result = OverlayMixedPoints.overlay(opCode, inputGeom.getGeometry(0), inputGeom.getGeometry(1), pm); 477 } 478 else { 479 // handle case where both inputs are formed of edges (Lines and Polygons) 480 result = computeEdgeOverlay(); 481 } 482 /** 483 * This is a no-op if the elevation model was not computed due to Z not present 484 */ 485 elevModel.populateZ(result); 486 return result; 487 } 488 computeEdgeOverlay()489 private Geometry computeEdgeOverlay() { 490 491 List<Edge> edges = nodeEdges(); 492 493 OverlayGraph graph = buildGraph(edges); 494 495 if (isOutputNodedEdges) { 496 return OverlayUtil.toLines(graph, isOutputEdges, geomFact); 497 } 498 499 labelGraph(graph); 500 //for (OverlayEdge e : graph.getEdges()) { Debug.println(e); } 501 502 if (isOutputEdges || isOutputResultEdges) { 503 return OverlayUtil.toLines(graph, isOutputEdges, geomFact); 504 } 505 506 return extractResult(opCode, graph); 507 } 508 nodeEdges()509 private List<Edge> nodeEdges() { 510 /** 511 * Node the edges, using whatever noder is being used 512 */ 513 EdgeNodingBuilder nodingBuilder = new EdgeNodingBuilder(pm, noder); 514 515 /** 516 * Optimize Intersection and Difference by clipping to the 517 * result extent, if enabled. 518 */ 519 if ( isOptimized ) { 520 Envelope clipEnv = OverlayUtil.clippingEnvelope(opCode, inputGeom, pm); 521 if (clipEnv != null) 522 nodingBuilder.setClipEnvelope( clipEnv ); 523 } 524 525 List<Edge> mergedEdges = nodingBuilder.build( 526 inputGeom.getGeometry(0), 527 inputGeom.getGeometry(1)); 528 529 /** 530 * Record if an input geometry has collapsed. 531 * This is used to avoid trying to locate disconnected edges 532 * against a geometry which has collapsed completely. 533 */ 534 inputGeom.setCollapsed(0, ! nodingBuilder.hasEdgesFor(0) ); 535 inputGeom.setCollapsed(1, ! nodingBuilder.hasEdgesFor(1) ); 536 537 return mergedEdges; 538 } 539 buildGraph(Collection<Edge> edges)540 private OverlayGraph buildGraph(Collection<Edge> edges) { 541 OverlayGraph graph = new OverlayGraph(); 542 for (Edge e : edges) { 543 graph.addEdge(e.getCoordinates(), e.createLabel()); 544 } 545 return graph; 546 } 547 labelGraph(OverlayGraph graph)548 private void labelGraph(OverlayGraph graph) { 549 OverlayLabeller labeller = new OverlayLabeller(graph, inputGeom); 550 labeller.computeLabelling(); 551 labeller.markResultAreaEdges(opCode); 552 labeller.unmarkDuplicateEdgesFromResultArea(); 553 } 554 555 /** 556 * Extracts the result geometry components from the fully labelled topology graph. 557 * <p> 558 * This method implements the semantic that the result of an 559 * intersection operation is homogeneous with highest dimension. 560 * In other words, 561 * if an intersection has components of a given dimension 562 * no lower-dimension components are output. 563 * For example, if two polygons intersect in an area, 564 * no linestrings or points are included in the result, 565 * even if portions of the input do meet in lines or points. 566 * This semantic choice makes more sense for typical usage, 567 * in which only the highest dimension components are of interest. 568 * 569 * @param opCode the overlay operation 570 * @param graph the topology graph 571 * @return the result geometry 572 */ extractResult(int opCode, OverlayGraph graph)573 private Geometry extractResult(int opCode, OverlayGraph graph) { 574 boolean isAllowMixedIntResult = ! isStrictMode; 575 576 //--- Build polygons 577 List<OverlayEdge> resultAreaEdges = graph.getResultAreaEdges(); 578 PolygonBuilder polyBuilder = new PolygonBuilder(resultAreaEdges, geomFact); 579 List<Polygon> resultPolyList = polyBuilder.getPolygons(); 580 boolean hasResultAreaComponents = resultPolyList.size() > 0; 581 582 List<LineString> resultLineList = null; 583 List<Point> resultPointList = null; 584 585 if (! isAreaResultOnly) { 586 //--- Build lines 587 boolean allowResultLines = ! hasResultAreaComponents 588 || isAllowMixedIntResult 589 || opCode == SYMDIFFERENCE 590 || opCode == UNION; 591 if ( allowResultLines ) { 592 LineBuilder lineBuilder = new LineBuilder(inputGeom, graph, hasResultAreaComponents, opCode, geomFact); 593 lineBuilder.setStrictMode(isStrictMode); 594 resultLineList = lineBuilder.getLines(); 595 } 596 /** 597 * Operations with point inputs are handled elsewhere. 598 * Only an Intersection op can produce point results 599 * from non-point inputs. 600 */ 601 boolean hasResultComponents = hasResultAreaComponents || resultLineList.size() > 0; 602 boolean allowResultPoints = ! hasResultComponents || isAllowMixedIntResult; 603 if ( opCode == INTERSECTION && allowResultPoints ) { 604 IntersectionPointBuilder pointBuilder = new IntersectionPointBuilder(graph, geomFact); 605 pointBuilder.setStrictMode(isStrictMode); 606 resultPointList = pointBuilder.getPoints(); 607 } 608 } 609 610 if (isEmpty(resultPolyList) 611 && isEmpty(resultLineList) 612 && isEmpty(resultPointList)) 613 return createEmptyResult(); 614 615 Geometry resultGeom = OverlayUtil.createResultGeometry(resultPolyList, resultLineList, resultPointList, geomFact); 616 return resultGeom; 617 } 618 isEmpty(List list)619 private static boolean isEmpty(List list) { 620 return list == null || list.size() == 0; 621 } 622 createEmptyResult()623 private Geometry createEmptyResult() { 624 return OverlayUtil.createEmptyResult( 625 OverlayUtil.resultDimension(opCode, 626 inputGeom.getDimension(0), 627 inputGeom.getDimension(1)), 628 geomFact); 629 } 630 631 } 632 633 634