1 2 /* 3 * Copyright (c) 2016 Vivid Solutions. 4 * 5 * All rights reserved. This program and the accompanying materials 6 * are made available under the terms of the Eclipse Public License 2.0 7 * and Eclipse Distribution License v. 1.0 which accompanies this distribution. 8 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html 9 * and the Eclipse Distribution License is available at 10 * 11 * http://www.eclipse.org/org/documents/edl-v10.php. 12 */ 13 14 15 package org.locationtech.jts.operation.polygonize; 16 17 import java.util.ArrayList; 18 import java.util.Comparator; 19 import java.util.Iterator; 20 import java.util.List; 21 22 import org.locationtech.jts.algorithm.Orientation; 23 import org.locationtech.jts.algorithm.locate.IndexedPointInAreaLocator; 24 import org.locationtech.jts.algorithm.locate.PointOnGeometryLocator; 25 import org.locationtech.jts.geom.Coordinate; 26 import org.locationtech.jts.geom.CoordinateArrays; 27 import org.locationtech.jts.geom.CoordinateList; 28 import org.locationtech.jts.geom.Envelope; 29 import org.locationtech.jts.geom.GeometryFactory; 30 import org.locationtech.jts.geom.LineString; 31 import org.locationtech.jts.geom.LinearRing; 32 import org.locationtech.jts.geom.Location; 33 import org.locationtech.jts.geom.Polygon; 34 import org.locationtech.jts.geom.impl.CoordinateArraySequence; 35 import org.locationtech.jts.io.WKTWriter; 36 import org.locationtech.jts.planargraph.DirectedEdge; 37 import org.locationtech.jts.util.Assert; 38 39 40 /** 41 * Represents a ring of {@link PolygonizeDirectedEdge}s which form 42 * a ring of a polygon. The ring may be either an outer shell or a hole. 43 * 44 * @version 1.7 45 */ 46 class EdgeRing { 47 48 /** 49 * Find the innermost enclosing shell EdgeRing containing the argument EdgeRing, if any. 50 * The innermost enclosing ring is the <i>smallest</i> enclosing ring. 51 * The algorithm used depends on the fact that: 52 * <br> 53 * ring A contains ring B iff envelope(ring A) contains envelope(ring B) 54 * <br> 55 * This routine is only safe to use if the chosen point of the hole 56 * is known to be properly contained in a shell 57 * (which is guaranteed to be the case if the hole does not touch its shell) 58 * <p> 59 * To improve performance of this function the caller should 60 * make the passed shellList as small as possible (e.g. 61 * by using a spatial index filter beforehand). 62 * 63 * @return containing EdgeRing, if there is one 64 * or null if no containing EdgeRing is found 65 */ findEdgeRingContaining(EdgeRing testEr, List erList)66 public static EdgeRing findEdgeRingContaining(EdgeRing testEr, List erList) 67 { 68 LinearRing testRing = testEr.getRing(); 69 Envelope testEnv = testRing.getEnvelopeInternal(); 70 Coordinate testPt = testRing.getCoordinateN(0); 71 72 EdgeRing minRing = null; 73 Envelope minRingEnv = null; 74 for (Iterator it = erList.iterator(); it.hasNext(); ) { 75 EdgeRing tryEdgeRing = (EdgeRing) it.next(); 76 LinearRing tryRing = tryEdgeRing.getRing(); 77 Envelope tryShellEnv = tryRing.getEnvelopeInternal(); 78 // the hole envelope cannot equal the shell envelope 79 // (also guards against testing rings against themselves) 80 if (tryShellEnv.equals(testEnv)) continue; 81 82 // hole must be contained in shell 83 if (! tryShellEnv.contains(testEnv)) continue; 84 85 testPt = CoordinateArrays.ptNotInList(testRing.getCoordinates(), tryEdgeRing.getCoordinates()); 86 87 /** 88 * If testPt is null it indicates that the hole is exactly surrounded by the tryShell. 89 * This should not happen for fully noded/dissolved linework. 90 * For now just ignore this hole and continue - this should produce 91 * "best effort" output. 92 * In futher could flag this as an error (invalid ring). 93 */ 94 if (testPt == null) continue; 95 96 boolean isContained = tryEdgeRing.isInRing(testPt); 97 98 // check if the new containing ring is smaller than the current minimum ring 99 if (isContained) { 100 if (minRing == null 101 || minRingEnv.contains(tryShellEnv)) { 102 minRing = tryEdgeRing; 103 minRingEnv = minRing.getRing().getEnvelopeInternal(); 104 } 105 } 106 } 107 return minRing; 108 } 109 110 /** 111 * Traverses a ring of DirectedEdges, accumulating them into a list. 112 * This assumes that all dangling directed edges have been removed 113 * from the graph, so that there is always a next dirEdge. 114 * 115 * @param startDE the DirectedEdge to start traversing at 116 * @return a List of DirectedEdges that form a ring 117 */ findDirEdgesInRing(PolygonizeDirectedEdge startDE)118 public static List findDirEdgesInRing(PolygonizeDirectedEdge startDE) 119 { 120 PolygonizeDirectedEdge de = startDE; 121 List edges = new ArrayList(); 122 do { 123 edges.add(de); 124 de = de.getNext(); 125 Assert.isTrue(de != null, "found null DE in ring"); 126 Assert.isTrue(de == startDE || ! de.isInRing(), "found DE already in ring"); 127 } while (de != startDE); 128 return edges; 129 } 130 131 private GeometryFactory factory; 132 133 private List deList = new ArrayList(); 134 private DirectedEdge lowestEdge = null; 135 136 // cache the following data for efficiency 137 private LinearRing ring = null; 138 private IndexedPointInAreaLocator locator; 139 140 private Coordinate[] ringPts = null; 141 private List holes; 142 private EdgeRing shell; 143 private boolean isHole; 144 private boolean isProcessed = false; 145 private boolean isIncludedSet = false; 146 private boolean isIncluded = false; 147 EdgeRing(GeometryFactory factory)148 public EdgeRing(GeometryFactory factory) 149 { 150 this.factory = factory; 151 } 152 build(PolygonizeDirectedEdge startDE)153 public void build(PolygonizeDirectedEdge startDE) { 154 PolygonizeDirectedEdge de = startDE; 155 do { 156 add(de); 157 de.setRing(this); 158 de = de.getNext(); 159 Assert.isTrue(de != null, "found null DE in ring"); 160 Assert.isTrue(de == startDE || ! de.isInRing(), "found DE already in ring"); 161 } while (de != startDE); 162 } 163 164 /** 165 * Adds a {@link DirectedEdge} which is known to form part of this ring. 166 * @param de the {@link DirectedEdge} to add. 167 */ add(DirectedEdge de)168 private void add(DirectedEdge de) 169 { 170 deList.add(de); 171 } 172 173 /** 174 * Tests whether this ring is a hole. 175 * @return <code>true</code> if this ring is a hole 176 */ isHole()177 public boolean isHole() 178 { 179 return isHole; 180 } 181 182 /** 183 * Computes whether this ring is a hole. 184 * Due to the way the edges in the polygonization graph are linked, 185 * a ring is a hole if it is oriented counter-clockwise. 186 */ computeHole()187 public void computeHole() 188 { 189 LinearRing ring = getRing(); 190 isHole = Orientation.isCCW(ring.getCoordinates()); 191 } 192 193 /** 194 * Adds a hole to the polygon formed by this ring. 195 * @param hole the {@link LinearRing} forming the hole. 196 */ addHole(LinearRing hole)197 public void addHole(LinearRing hole) { 198 if (holes == null) 199 holes = new ArrayList(); 200 holes.add(hole); 201 } 202 203 /** 204 * Adds a hole to the polygon formed by this ring. 205 * @param hole the {@link LinearRing} forming the hole. 206 */ addHole(EdgeRing holeER)207 public void addHole(EdgeRing holeER) { 208 holeER.setShell(this); 209 LinearRing hole = holeER.getRing(); 210 if (holes == null) 211 holes = new ArrayList(); 212 holes.add(hole); 213 } 214 215 /** 216 * Computes the {@link Polygon} formed by this ring and any contained holes. 217 * 218 * @return the {@link Polygon} formed by this ring and its holes. 219 */ getPolygon()220 public Polygon getPolygon() 221 { 222 LinearRing[] holeLR = null; 223 if (holes != null) { 224 holeLR = new LinearRing[holes.size()]; 225 for (int i = 0; i < holes.size(); i++) { 226 holeLR[i] = (LinearRing) holes.get(i); 227 } 228 } 229 Polygon poly = factory.createPolygon(ring, holeLR); 230 return poly; 231 } 232 233 /** 234 * Tests if the {@link LinearRing} ring formed by this edge ring is topologically valid. 235 * 236 * @return true if the ring is valid 237 */ isValid()238 public boolean isValid() 239 { 240 getCoordinates(); 241 if (ringPts.length <= 3) return false; 242 getRing(); 243 return ring.isValid(); 244 } 245 isIncludedSet()246 public boolean isIncludedSet() { 247 return isIncludedSet; 248 } 249 isIncluded()250 public boolean isIncluded() { 251 return isIncluded; 252 } 253 setIncluded(boolean isIncluded)254 public void setIncluded(boolean isIncluded) { 255 this.isIncluded = isIncluded; 256 this.isIncludedSet = true; 257 } 258 getLocator()259 private PointOnGeometryLocator getLocator() { 260 if (locator == null) { 261 locator = new IndexedPointInAreaLocator(getRing()); 262 } 263 return locator; 264 } 265 isInRing(Coordinate pt)266 public boolean isInRing(Coordinate pt) { 267 /** 268 * Use an indexed point-in-polygon for performance 269 */ 270 return Location.EXTERIOR != getLocator().locate(pt); 271 //return PointLocation.isInRing(pt, getCoordinates()); 272 } 273 274 /** 275 * Computes the list of coordinates which are contained in this ring. 276 * The coordinates are computed once only and cached. 277 * 278 * @return an array of the {@link Coordinate}s in this ring 279 */ getCoordinates()280 private Coordinate[] getCoordinates() 281 { 282 if (ringPts == null) { 283 CoordinateList coordList = new CoordinateList(); 284 for (Iterator i = deList.iterator(); i.hasNext(); ) { 285 DirectedEdge de = (DirectedEdge) i.next(); 286 PolygonizeEdge edge = (PolygonizeEdge) de.getEdge(); 287 addEdge(edge.getLine().getCoordinates(), de.getEdgeDirection(), coordList); 288 } 289 ringPts = coordList.toCoordinateArray(); 290 } 291 return ringPts; 292 } 293 294 /** 295 * Gets the coordinates for this ring as a {@link LineString}. 296 * Used to return the coordinates in this ring 297 * as a valid geometry, when it has been detected that the ring is topologically 298 * invalid. 299 * @return a {@link LineString} containing the coordinates in this ring 300 */ getLineString()301 public LineString getLineString() 302 { 303 getCoordinates(); 304 return factory.createLineString(ringPts); 305 } 306 307 /** 308 * Returns this ring as a {@link LinearRing}, or null if an Exception occurs while 309 * creating it (such as a topology problem). Details of problems are written to 310 * standard output. 311 */ getRing()312 public LinearRing getRing() 313 { 314 if (ring != null) return ring; 315 getCoordinates(); 316 if (ringPts.length < 3) System.out.println(ringPts); 317 try { 318 ring = factory.createLinearRing(ringPts); 319 } 320 catch (Exception ex) { 321 System.out.println(ringPts); 322 } 323 return ring; 324 } 325 addEdge(Coordinate[] coords, boolean isForward, CoordinateList coordList)326 private static void addEdge(Coordinate[] coords, boolean isForward, CoordinateList coordList) 327 { 328 if (isForward) { 329 for (int i = 0; i < coords.length; i++) { 330 coordList.add(coords[i], false); 331 } 332 } 333 else { 334 for (int i = coords.length - 1; i >= 0; i--) { 335 coordList.add(coords[i], false); 336 } 337 } 338 } 339 340 /** 341 * Sets the containing shell ring of a ring that has been determined to be a hole. 342 * 343 * @param shell the shell ring 344 */ setShell(EdgeRing shell)345 public void setShell(EdgeRing shell) { 346 this.shell = shell; 347 } 348 349 /** 350 * Tests whether this ring has a shell assigned to it. 351 * 352 * @return true if the ring has a shell 353 */ hasShell()354 public boolean hasShell() { 355 return shell != null; 356 } 357 358 /** 359 * Gets the shell for this ring. The shell is the ring itself if it is not a hole, otherwise its parent shell. 360 * 361 * @return the shell for this ring 362 */ getShell()363 public EdgeRing getShell() { 364 if (isHole()) return shell; 365 return this; 366 } 367 /** 368 * Tests whether this ring is an outer hole. 369 * A hole is an outer hole if it is not contained by a shell. 370 * 371 * @return true if the ring is an outer hole. 372 */ isOuterHole()373 public boolean isOuterHole() { 374 if (! isHole) return false; 375 return ! hasShell(); 376 } 377 378 /** 379 * Tests whether this ring is an outer shell. 380 * 381 * @return true if the ring is an outer shell. 382 */ isOuterShell()383 public boolean isOuterShell() { 384 return getOuterHole() != null; 385 } 386 387 /** 388 * Gets the outer hole of a shell, if it has one. 389 * An outer hole is one that is not contained 390 * in any other shell. 391 * Each disjoint connected group of shells 392 * is surrounded by an outer hole. 393 * 394 * @return the outer hole edge ring, or null 395 */ getOuterHole()396 public EdgeRing getOuterHole() 397 { 398 /* 399 * Only shells can have outer holes 400 */ 401 if (isHole()) return null; 402 /* 403 * A shell is an outer shell if any edge is also in an outer hole. 404 * A hole is an outer hole if it is not contained by a shell. 405 */ 406 for (int i = 0; i < deList.size(); i++) { 407 PolygonizeDirectedEdge de = (PolygonizeDirectedEdge) deList.get(i); 408 EdgeRing adjRing = ((PolygonizeDirectedEdge) de.getSym()).getRing(); 409 if (adjRing.isOuterHole()) return adjRing; 410 } 411 return null; 412 } 413 414 /** 415 * Updates the included status for currently non-included shells 416 * based on whether they are adjacent to an included shell. 417 */ updateIncluded()418 public void updateIncluded() { 419 if (isHole()) return; 420 for (int i = 0; i < deList.size(); i++) { 421 PolygonizeDirectedEdge de = (PolygonizeDirectedEdge) deList.get(i); 422 EdgeRing adjShell = ((PolygonizeDirectedEdge) de.getSym()).getRing().getShell(); 423 424 if (adjShell != null && adjShell.isIncludedSet()) { 425 // adjacent ring has been processed, so set included to inverse of adjacent included 426 setIncluded(! adjShell.isIncluded()); 427 return; 428 } 429 } 430 } 431 432 /** 433 * Gets a string representation of this object. 434 * 435 * @return a string representing the object 436 */ toString()437 public String toString() { 438 return WKTWriter.toLineString(new CoordinateArraySequence(getCoordinates())); 439 } 440 441 /** 442 * @return whether the ring has been processed 443 */ isProcessed()444 public boolean isProcessed() { 445 return isProcessed; 446 } 447 448 /** 449 * @param isProcessed whether the ring has been processed 450 */ setProcessed(boolean isProcessed)451 public void setProcessed(boolean isProcessed) { 452 this.isProcessed = isProcessed; 453 } 454 455 /** 456 * Compares EdgeRings based on their envelope, 457 * using the standard lexicographic ordering. 458 * This ordering is sufficient to make edge ring sorting deterministic. 459 * 460 * @author mbdavis 461 * 462 */ 463 static class EnvelopeComparator implements Comparator { compare(Object obj0, Object obj1)464 public int compare(Object obj0, Object obj1) { 465 EdgeRing r0 = (EdgeRing) obj0; 466 EdgeRing r1 = (EdgeRing) obj1; 467 return r0.getRing().getEnvelope().compareTo(r1.getRing().getEnvelope()); 468 } 469 470 } 471 472 } 473