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 org.locationtech.jts.geom.Location; 15 import org.locationtech.jts.geom.Position; 16 17 /** 18 * A structure recording the topological situation 19 * for an edge in a topology graph 20 * used during overlay processing. 21 * A label contains the topological {@link Location}s for 22 * one or two input geometries to an overlay operation. 23 * An input geometry may be either a Line or an Area. 24 * The label locations for each input geometry are populated 25 * with the {@Location}s for the edge {@link Position}s 26 * when they are created or once they are computed by topological evaluation. 27 * A label also records the (effective) dimension of each input geometry. 28 * For area edges the role (shell or hole) 29 * of the originating ring is recorded, to allow 30 * determination of edge handling in collapse cases. 31 * <p> 32 * In an {@link OverlayGraph} a single label is shared between 33 * the two oppositely-oriented {@ link OverlayEdge}s of a symmetric pair. 34 * Accessors for orientation-sensitive information 35 * are parameterized by the orientation of the containing edge. 36 * <p> 37 * For each input geometry (0 and 1), the label records 38 * that an edge is in one of the following states 39 * (identified by the <code>dim</code> field). 40 * Each state has additional information about the edge topology. 41 * <ul> 42 * <li>A <b>Boundary</b> edge of an Area (polygon) 43 * <ul> 44 * <li><code>dim</code> = DIM_BOUNDARY</li> 45 * <li><code>locLeft, locRight</code> : the locations of the edge sides for the Area</li> 46 * <li><code>locLine</code> : INTERIOR</li> 47 * <li><code>isHole</code> : whether the 48 * edge is in a shell or a hole (the ring role)</li> 49 * </ul> 50 * </li> 51 * <li>A <b>Collapsed</b> edge of an input Area 52 * (formed by merging two or more parent edges) 53 * <ul> 54 * <li><code>dim</code> = DIM_COLLAPSE</li> 55 * <li><code>locLine</code> : the location of the edge relative to the effective input Area 56 * (a collapsed spike is EXTERIOR, a collapsed gore or hole is INTERIOR)</li> 57 * <li><code>isHole</code> : <code>true</code> if all parent edges are in holes; 58 * <code>false</code> if some parent edge is in a shell 59 * </ul> 60 * </li> 61 * <li>A <b>Line</b> edge from an input line 62 * <ul> 63 * <li><code>dim</code> = DIM_LINE</li> 64 * <li><code>locLine</code> : the location of the edge relative to the Line. 65 * Initialized to LOC_UNKNOWN to simplify logic.</li> 66 * </ul> 67 * </li> 68 * <li>An edge which is <b>Not Part</b> of an input geometry 69 * (and thus must be part of the other geometry). 70 * <ul> 71 * <li><code>dim</code> = NOT_PART</li> 72 * </ul> 73 * </li> 74 * </ul> 75 * Note that: 76 * <ul> 77 * <li>an edge cannot be both a Collapse edge and a Line edge in the same input geometry, 78 * because input geometries must be homogeneous in dimension. 79 * <li>an edge may be an Boundary edge in one input geometry 80 * and a Line or Collapse edge in the other input. 81 * </ul> 82 * 83 * @author Martin Davis 84 * 85 */ 86 class OverlayLabel { 87 88 private static final char SYM_UNKNOWN = '#'; 89 private static final char SYM_BOUNDARY = 'B'; 90 private static final char SYM_COLLAPSE = 'C'; 91 private static final char SYM_LINE = 'L'; 92 93 /** 94 * The dimension of an input geometry which is not known 95 */ 96 public static final int DIM_UNKNOWN = -1; 97 98 /** 99 * The dimension of an edge which is not part of a specified input geometry. 100 */ 101 public static final int DIM_NOT_PART = DIM_UNKNOWN; 102 103 /** 104 * The dimension of an edge which is a line. 105 */ 106 public static final int DIM_LINE = 1; 107 108 /** 109 * The dimension for an edge which is part of an input Area geometry boundary. 110 */ 111 public static final int DIM_BOUNDARY = 2; 112 113 /** 114 * The dimension for an edge which is a collapsed part of an input Area geometry boundary. 115 * A collapsed edge represents two or more line segments which have the same endpoints. 116 * They usually are caused by edges in valid polygonal geometries 117 * having their endpoints become identical due to precision reduction. 118 */ 119 public static final int DIM_COLLAPSE = 3; 120 121 /** 122 * Indicates that the location is currently unknown 123 */ 124 public static int LOC_UNKNOWN = Location.NONE; 125 126 127 private int aDim = DIM_NOT_PART; 128 private boolean aIsHole = false; 129 private int aLocLeft = LOC_UNKNOWN; 130 private int aLocRight = LOC_UNKNOWN; 131 private int aLocLine = LOC_UNKNOWN; 132 133 private int bDim = DIM_NOT_PART; 134 private boolean bIsHole = false; 135 private int bLocLeft = LOC_UNKNOWN; 136 private int bLocRight = LOC_UNKNOWN; 137 private int bLocLine = LOC_UNKNOWN; 138 139 140 /** 141 * Creates a label for an Area edge. 142 * 143 * @param index the input index of the parent geometry 144 * @param locLeft the location of the left side of the edge 145 * @param locRight the location of the right side of the edge 146 * @param isHole whether the edge role is a hole or a shell 147 */ OverlayLabel(int index, int locLeft, int locRight, boolean isHole)148 public OverlayLabel(int index, int locLeft, int locRight, boolean isHole) 149 { 150 initBoundary(index, locLeft, locRight, isHole); 151 } 152 153 /** 154 * Creates a label for a Line edge. 155 * 156 * @param index the input index of the parent geometry 157 */ OverlayLabel(int index)158 public OverlayLabel(int index) 159 { 160 initLine(index); 161 } 162 163 /** 164 * Creates an uninitialized label. 165 * 166 */ OverlayLabel()167 public OverlayLabel() 168 { 169 } 170 171 /** 172 * Creates a label which is a copy of another label. 173 * 174 * @param lbl 175 */ OverlayLabel(OverlayLabel lbl)176 public OverlayLabel(OverlayLabel lbl) { 177 this.aLocLeft = lbl.aLocLeft; 178 this.aLocRight = lbl.aLocRight; 179 this.aLocLine = lbl.aLocLine; 180 this.aDim = lbl.aDim; 181 this.aIsHole = lbl.aIsHole; 182 183 this.bLocLeft = lbl.bLocLeft; 184 this.bLocRight = lbl.bLocRight; 185 this.bLocLine = lbl.bLocLine; 186 this.bDim = lbl.bDim; 187 this.bIsHole = lbl.bIsHole; 188 } 189 190 /** 191 * Gets the effective dimension of the given input geometry. 192 * 193 * @param index the input geometry index 194 * @return the dimension 195 * 196 * @see #DIM_UNKNOWN 197 * @see #DIM_NOT_PART 198 * @see #DIM_LINE 199 * @see #DIM_BOUNDARY 200 * @see #DIM_COLLAPSE 201 */ dimension(int index)202 public int dimension(int index) { 203 if (index == 0) 204 return aDim; 205 return bDim; 206 } 207 208 /** 209 * Initializes the label for an input geometry which is an Area boundary. 210 * 211 * @param index the input index of the parent geometry 212 * @param locLeft the location of the left side of the edge 213 * @param locRight the location of the right side of the edge 214 * @param isHole whether the edge role is a hole or a shell 215 */ initBoundary(int index, int locLeft, int locRight, boolean isHole)216 public void initBoundary(int index, int locLeft, int locRight, boolean isHole) { 217 if (index == 0) { 218 aDim = DIM_BOUNDARY; 219 aIsHole = isHole; 220 aLocLeft = locLeft; 221 aLocRight = locRight; 222 aLocLine = Location.INTERIOR; 223 } 224 else { 225 bDim = DIM_BOUNDARY; 226 bIsHole = isHole; 227 bLocLeft = locLeft; 228 bLocRight = locRight; 229 bLocLine = Location.INTERIOR; 230 } 231 } 232 233 /** 234 * Initializes the label for an edge which is the collapse of 235 * part of the boundary of an Area input geometry. 236 * The location of the collapsed edge relative to the 237 * parent area geometry is initially unknown. 238 * It must be determined from the topology of the overlay graph 239 * 240 * @param index the index of the parent input geometry 241 * @param isHole whether the dominant edge role is a hole or a shell 242 */ initCollapse(int index, boolean isHole)243 public void initCollapse(int index, boolean isHole) { 244 if (index == 0) { 245 aDim = DIM_COLLAPSE; 246 aIsHole = isHole; 247 } 248 else { 249 bDim = DIM_COLLAPSE; 250 bIsHole = isHole; 251 } 252 } 253 254 /** 255 * Initializes the label for an input geometry which is a Line. 256 * 257 * @param index the index of the parent input geometry 258 */ initLine(int index)259 public void initLine(int index) { 260 if (index == 0) { 261 aDim = DIM_LINE; 262 aLocLine = LOC_UNKNOWN; 263 } 264 else { 265 bDim = DIM_LINE; 266 bLocLine = LOC_UNKNOWN; 267 } 268 } 269 270 /** 271 * Initializes the label for an edge which is not part of an input geometry. 272 * @param index the index of the input geometry 273 */ initNotPart(int index)274 public void initNotPart(int index) { 275 // this assumes locations are initialized to UNKNOWN 276 if (index == 0) { 277 aDim = DIM_NOT_PART; 278 } 279 else { 280 bDim = DIM_NOT_PART; 281 } 282 } 283 284 /** 285 * Sets the line location. 286 * 287 * This is used to set the locations for linear edges 288 * encountered during area label propagation. 289 * 290 * @param index source to update 291 * @param loc location to set 292 */ setLocationLine(int index, int loc)293 public void setLocationLine(int index, int loc) { 294 if (index == 0) { 295 aLocLine = loc; 296 } 297 else { 298 bLocLine = loc; 299 } 300 } 301 302 /** 303 * Sets the location of all postions for a given input. 304 * 305 * @param index the index of the input geometry 306 * @param loc the location to set 307 */ setLocationAll(int index, int loc)308 public void setLocationAll(int index, int loc) { 309 if (index == 0) { 310 aLocLine = loc; 311 aLocLeft = loc; 312 aLocRight = loc; 313 } 314 else { 315 bLocLine = loc; 316 bLocLeft = loc; 317 bLocRight = loc; 318 } 319 } 320 321 /** 322 * Sets the location for a collapsed edge (the Line position) 323 * for an input geometry, 324 * depending on the ring role recorded in the label. 325 * If the input geometry edge is from a shell, 326 * the location is {@link Location#EXTERIOR}, if it is a hole 327 * it is {@link Location#INTERIOR}. 328 * 329 * @param index the index of the input geometry 330 */ setLocationCollapse(int index)331 public void setLocationCollapse(int index) { 332 int loc = isHole(index) ? Location.INTERIOR : Location.EXTERIOR; 333 if (index == 0) { 334 aLocLine = loc; 335 } 336 else { 337 bLocLine = loc; 338 } 339 } 340 341 /** 342 * Tests whether at least one of the sources is a Line. 343 * 344 * @return true if at least one source is a line 345 */ isLine()346 public boolean isLine() { 347 return aDim == DIM_LINE || bDim == DIM_LINE; 348 } 349 350 /** 351 * Tests whether a source is a Line. 352 * 353 * @param index the index of the input geometry 354 * @return true if the input is a Line 355 */ isLine(int index)356 public boolean isLine(int index) { 357 if (index == 0) { 358 return aDim == DIM_LINE; 359 } 360 return bDim == DIM_LINE; 361 } 362 363 /** 364 * Tests whether an edge is linear (a Line or a Collapse) in an input geometry. 365 * 366 * @param index the index of the input geometry 367 * @return true if the edge is linear 368 */ isLinear(int index)369 public boolean isLinear(int index) { 370 if (index == 0) { 371 return aDim == DIM_LINE || aDim == DIM_COLLAPSE; 372 } 373 return bDim == DIM_LINE || bDim == DIM_COLLAPSE; 374 } 375 376 /** 377 * Tests whether a the source of a label is known. 378 * 379 * @param index the index of the source geometry 380 * @return true if the source is known 381 */ isKnown(int index)382 public boolean isKnown(int index) { 383 if (index == 0) { 384 return aDim != DIM_UNKNOWN; 385 } 386 return bDim != DIM_UNKNOWN; 387 } 388 389 /** 390 * Tests whether a label is for an edge which is not part 391 * of a given input geometry. 392 * 393 * @param index the index of the source geometry 394 * @return true if the edge is not part of the geometry 395 */ isNotPart(int index)396 public boolean isNotPart(int index) { 397 if (index == 0) { 398 return aDim == DIM_NOT_PART; 399 } 400 return bDim == DIM_NOT_PART; 401 } 402 403 /** 404 * Tests if a label is for an edge which is in the boundary of either source geometry. 405 * 406 * @return true if the label is a boundary for either source 407 */ isBoundaryEither()408 public boolean isBoundaryEither() { 409 return aDim == DIM_BOUNDARY || bDim == DIM_BOUNDARY; 410 } 411 412 /** 413 * Tests if a label is for an edge which is in the boundary of both source geometries. 414 * 415 * @return true if the label is a boundary for both sources 416 */ isBoundaryBoth()417 public boolean isBoundaryBoth() { 418 return aDim == DIM_BOUNDARY && bDim == DIM_BOUNDARY; 419 } 420 421 /** 422 * Tests if the label is a collapsed edge of one area 423 * and is a (non-collapsed) boundary edge of the other area. 424 * 425 * @return true if the label is for a collapse coincident with a boundary 426 */ isBoundaryCollapse()427 public boolean isBoundaryCollapse() { 428 if (isLine()) return false; 429 return ! isBoundaryBoth(); 430 } 431 432 /** 433 * Tests if a label is for an edge where two 434 * area touch along their boundary. 435 * 436 * @return true if the edge is a boundary touch 437 */ isBoundaryTouch()438 public boolean isBoundaryTouch() { 439 return isBoundaryBoth() 440 && getLocation(0, Position.RIGHT, true) != getLocation(1, Position.RIGHT, true); 441 } 442 443 /** 444 * Tests if a label is for an edge which is in the boundary of a source geometry. 445 * Collapses are not reported as being in the boundary. 446 * 447 * @param index the index of the input geometry 448 * @return true if the label is a boundary for the source 449 */ isBoundary(int index)450 public boolean isBoundary(int index) { 451 if (index == 0) { 452 return aDim == DIM_BOUNDARY; 453 } 454 return bDim == DIM_BOUNDARY; 455 } 456 457 /** 458 * Tests whether a label is for an edge which is a boundary of one geometry 459 * and not part of the other. 460 * 461 * @return true if the edge is a boundary singleton 462 */ isBoundarySingleton()463 public boolean isBoundarySingleton() { 464 if (aDim == DIM_BOUNDARY && bDim == DIM_NOT_PART) return true; 465 if (bDim == DIM_BOUNDARY && aDim == DIM_NOT_PART) return true; 466 return false; 467 } 468 469 /** 470 * Tests if the line location for a source is unknown. 471 * 472 * @param index the index of the input geometry 473 * @return true if the line location is unknown 474 */ isLineLocationUnknown(int index)475 public boolean isLineLocationUnknown(int index) { 476 if (index == 0) { 477 return aLocLine == LOC_UNKNOWN; 478 } 479 else { 480 return bLocLine == LOC_UNKNOWN; 481 } 482 } 483 484 /** 485 * Tests if a line edge is inside a source geometry 486 * (i.e. it has location {@link Location#INTERIOR}). 487 * 488 * @param index the index of the input geometry 489 * @return true if the line is inside the source geometry 490 */ isLineInArea(int index)491 public boolean isLineInArea(int index) { 492 if (index == 0) { 493 return aLocLine == Location.INTERIOR; 494 } 495 return bLocLine == Location.INTERIOR; 496 } 497 498 /** 499 * Tests if the ring role of an edge is a hole. 500 * 501 * @param index the index of the input geometry 502 * @return true if the ring role is a hole 503 */ isHole(int index)504 public boolean isHole(int index) { 505 if (index == 0) { 506 return aIsHole; 507 } 508 else { 509 return bIsHole; 510 } 511 } 512 513 /** 514 * Tests if an edge is a Collapse for a source geometry. 515 * 516 * @param index the index of the input geometry 517 * @return true if the label indicates the edge is a collapse for the source 518 */ isCollapse(int index)519 public boolean isCollapse(int index) { 520 return dimension(index) == DIM_COLLAPSE; 521 } 522 523 /** 524 * Tests if a label is a Collapse has location {@link Location#INTERIOR}, 525 * to at least one source geometry. 526 * 527 * @return true if the label is an Interior Collapse to a source geometry 528 */ isInteriorCollapse()529 public boolean isInteriorCollapse() { 530 if (aDim == DIM_COLLAPSE && aLocLine == Location.INTERIOR) return true; 531 if (bDim == DIM_COLLAPSE && bLocLine == Location.INTERIOR) return true; 532 return false; 533 } 534 535 /** 536 * Tests if a label is a Collapse 537 * and NotPart with location {@link Location#INTERIOR} for the other geometry. 538 * 539 * @return true if the label is a Collapse and a NotPart with Location Interior 540 */ isCollapseAndNotPartInterior()541 public boolean isCollapseAndNotPartInterior() { 542 if (aDim == DIM_COLLAPSE && bDim == DIM_NOT_PART && bLocLine == Location.INTERIOR) return true; 543 if (bDim == DIM_COLLAPSE && aDim == DIM_NOT_PART && aLocLine == Location.INTERIOR) return true; 544 return false; 545 } 546 547 /** 548 * Gets the line location for a source geometry. 549 * 550 * @param index the index of the input geometry 551 * @return the line location for the source 552 */ getLineLocation(int index)553 public int getLineLocation(int index) { 554 if (index == 0) { 555 return aLocLine; 556 } 557 else { 558 return bLocLine; 559 } 560 } 561 562 /** 563 * Tests if a line is in the interior of a source geometry. 564 * 565 * @param index the index of the source geometry 566 * @return true if the label is a line and is interior 567 */ isLineInterior(int index)568 public boolean isLineInterior(int index) { 569 if (index == 0) { 570 return aLocLine == Location.INTERIOR; 571 } 572 return bLocLine == Location.INTERIOR; 573 } 574 575 /** 576 * Gets the location for a {@link Position} of an edge of a source 577 * for an edge with given orientation. 578 * 579 * @param index the index of the source geometry 580 * @param position the position to get the location for 581 * @param isForward true if the orientation of the containing edge is forward 582 * @return the location of the oriented position in the source 583 */ getLocation(int index, int position, boolean isForward)584 public int getLocation(int index, int position, boolean isForward) { 585 if (index == 0) { 586 switch (position) { 587 case Position.LEFT: return isForward ? aLocLeft : aLocRight; 588 case Position.RIGHT: return isForward ? aLocRight : aLocLeft; 589 case Position.ON: return aLocLine; 590 } 591 } 592 // index == 1 593 switch (position) { 594 case Position.LEFT: return isForward ? bLocLeft : bLocRight; 595 case Position.RIGHT: return isForward ? bLocRight : bLocLeft; 596 case Position.ON: return bLocLine; 597 } 598 return LOC_UNKNOWN; 599 } 600 601 /** 602 * Gets the location for this label for either 603 * a Boundary or a Line edge. 604 * This supports a simple determination of 605 * whether the edge should be included as a result edge. 606 * 607 * @param index the source index 608 * @param position the position for a boundary label 609 * @param isForward the direction for a boundary label 610 * @return the location for the specified position 611 */ getLocationBoundaryOrLine(int index, int position, boolean isForward)612 public int getLocationBoundaryOrLine(int index, int position, boolean isForward) { 613 if (isBoundary(index)) { 614 return getLocation(index, position, isForward); 615 } 616 return getLineLocation(index); 617 } 618 619 /** 620 * Gets the linear location for the given source. 621 * 622 * @param index the source geometry index 623 * @return the linear location for the source 624 */ getLocation(int index)625 public int getLocation(int index) { 626 if (index == 0) { 627 return aLocLine; 628 } 629 return bLocLine; 630 } 631 632 /** 633 * Tests whether this label has side position information 634 * for a source geometry. 635 * 636 * @param index the source geometry index 637 * @return true if at least one side position is known 638 */ hasSides(int index)639 public boolean hasSides(int index) { 640 if (index == 0) { 641 return aLocLeft != LOC_UNKNOWN 642 || aLocRight != LOC_UNKNOWN; 643 } 644 return bLocLeft != LOC_UNKNOWN 645 || bLocRight != LOC_UNKNOWN; 646 } 647 648 /** 649 * Creates a copy of this label. 650 * 651 * @return a copy of the label 652 */ copy()653 public OverlayLabel copy() { 654 return new OverlayLabel(this); 655 } 656 toString()657 public String toString() 658 { 659 return toString(true); 660 } 661 toString(boolean isForward)662 public String toString(boolean isForward) 663 { 664 StringBuilder buf = new StringBuilder(); 665 buf.append("A:"); 666 buf.append(locationString(0, isForward)); 667 buf.append("/B:"); 668 buf.append(locationString(1, isForward)); 669 return buf.toString(); 670 } 671 locationString(int index, boolean isForward)672 private String locationString(int index, boolean isForward) { 673 StringBuilder buf = new StringBuilder(); 674 if (isBoundary(index)) { 675 buf.append( Location.toLocationSymbol( getLocation(index, Position.LEFT, isForward) ) ); 676 buf.append( Location.toLocationSymbol( getLocation(index, Position.RIGHT, isForward) ) ); 677 } 678 else { 679 // is a linear edge 680 buf.append( Location.toLocationSymbol( index == 0 ? aLocLine : bLocLine )); 681 } 682 if (isKnown(index)) 683 buf.append( dimensionSymbol(index == 0 ? aDim : bDim) ); 684 if (isCollapse(index)) { 685 buf.append( ringRoleSymbol( index == 0 ? aIsHole : bIsHole )); 686 } 687 return buf.toString(); 688 } 689 690 /** 691 * Gets a symbol for the a ring role (Shell or Hole). 692 * 693 * @param isHole true for a hole, false for a shell 694 * @return the ring role symbol character 695 */ ringRoleSymbol(boolean isHole)696 public static char ringRoleSymbol(boolean isHole) { 697 return isHole ? 'h' : 's'; 698 } 699 700 /** 701 * Gets the symbol for the dimension code of an edge. 702 * 703 * @param dim the dimension code 704 * @return the dimension symbol character 705 */ dimensionSymbol(int dim)706 public static char dimensionSymbol(int dim) { 707 switch (dim) { 708 case DIM_LINE: return SYM_LINE; 709 case DIM_COLLAPSE: return SYM_COLLAPSE; 710 case DIM_BOUNDARY: return SYM_BOUNDARY; 711 } 712 return SYM_UNKNOWN; 713 } 714 715 716 } 717