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.ArrayList; 15 import java.util.List; 16 17 import org.locationtech.jts.algorithm.Orientation; 18 import org.locationtech.jts.algorithm.locate.IndexedPointInAreaLocator; 19 import org.locationtech.jts.algorithm.locate.PointOnGeometryLocator; 20 import org.locationtech.jts.geom.Coordinate; 21 import org.locationtech.jts.geom.CoordinateArrays; 22 import org.locationtech.jts.geom.CoordinateList; 23 import org.locationtech.jts.geom.Envelope; 24 import org.locationtech.jts.geom.GeometryFactory; 25 import org.locationtech.jts.geom.LinearRing; 26 import org.locationtech.jts.geom.Location; 27 import org.locationtech.jts.geom.Polygon; 28 import org.locationtech.jts.geom.TopologyException; 29 30 class OverlayEdgeRing { 31 32 private OverlayEdge startEdge; 33 private LinearRing ring; 34 private boolean isHole; 35 private Coordinate[] ringPts; 36 private IndexedPointInAreaLocator locator; 37 private OverlayEdgeRing shell; 38 private List<OverlayEdgeRing> holes = new ArrayList<OverlayEdgeRing>(); // a list of EdgeRings which are holes in this EdgeRing 39 OverlayEdgeRing(OverlayEdge start, GeometryFactory geometryFactory)40 public OverlayEdgeRing(OverlayEdge start, GeometryFactory geometryFactory) { 41 startEdge = start; 42 ringPts = computeRingPts(start); 43 computeRing(ringPts, geometryFactory); 44 } 45 getRing()46 public LinearRing getRing() { 47 return ring; 48 } 49 50 /** 51 * Tests whether this ring is a hole. 52 * @return <code>true</code> if this ring is a hole 53 */ isHole()54 public boolean isHole() 55 { 56 return isHole; 57 } 58 59 /** 60 * Sets the containing shell ring of a ring that has been determined to be a hole. 61 * 62 * @param shell the shell ring 63 */ setShell(OverlayEdgeRing shell)64 public void setShell(OverlayEdgeRing shell) { 65 this.shell = shell; 66 if (shell != null) shell.addHole(this); 67 } 68 69 /** 70 * Tests whether this ring has a shell assigned to it. 71 * 72 * @return true if the ring has a shell 73 */ hasShell()74 public boolean hasShell() { 75 return shell != null; 76 } 77 78 /** 79 * Gets the shell for this ring. The shell is the ring itself if it is not a hole, otherwise its parent shell. 80 * 81 * @return the shell for this ring 82 */ getShell()83 public OverlayEdgeRing getShell() { 84 if (isHole()) return shell; 85 return this; 86 } 87 addHole(OverlayEdgeRing ring)88 public void addHole(OverlayEdgeRing ring) { holes.add(ring); } 89 computeRingPts(OverlayEdge start)90 private Coordinate[] computeRingPts(OverlayEdge start) { 91 OverlayEdge edge = start; 92 CoordinateList pts = new CoordinateList(); 93 do { 94 if (edge.getEdgeRing() == this) 95 throw new TopologyException("Edge visited twice during ring-building at " + edge.getCoordinate(), edge.getCoordinate()); 96 97 //edges.add(de); 98 //Debug.println(de); 99 //Debug.println(de.getEdge()); 100 101 // only valid for polygonal output 102 //Assert.isTrue(edge.getLabel().isBoundaryEither()); 103 104 edge.addCoordinates(pts); 105 edge.setEdgeRing(this); 106 if (edge.nextResult() == null) 107 throw new TopologyException("Found null edge in ring", edge.dest()); 108 109 edge = edge.nextResult(); 110 } while (edge != start); 111 pts.closeRing(); 112 return pts.toCoordinateArray(); 113 } 114 computeRing(Coordinate[] ringPts, GeometryFactory geometryFactory)115 private void computeRing(Coordinate[] ringPts, GeometryFactory geometryFactory) { 116 if (ring != null) return; // don't compute more than once 117 ring = geometryFactory.createLinearRing(ringPts); 118 isHole = Orientation.isCCW(ring.getCoordinates()); 119 } 120 121 /** 122 * Computes the list of coordinates which are contained in this ring. 123 * The coordinates are computed once only and cached. 124 * 125 * @return an array of the {@link Coordinate}s in this ring 126 */ getCoordinates()127 private Coordinate[] getCoordinates() 128 { 129 return ringPts; 130 } 131 132 /** 133 * Finds the innermost enclosing shell OverlayEdgeRing 134 * containing this OverlayEdgeRing, if any. 135 * The innermost enclosing ring is the <i>smallest</i> enclosing ring. 136 * The algorithm used depends on the fact that: 137 * <br> 138 * ring A contains ring B iff envelope(ring A) contains envelope(ring B) 139 * <br> 140 * This routine is only safe to use if the chosen point of the hole 141 * is known to be properly contained in a shell 142 * (which is guaranteed to be the case if the hole does not touch its shell) 143 * <p> 144 * To improve performance of this function the caller should 145 * make the passed shellList as small as possible (e.g. 146 * by using a spatial index filter beforehand). 147 * 148 * @return containing EdgeRing, if there is one 149 * or null if no containing EdgeRing is found 150 */ findEdgeRingContaining(List<OverlayEdgeRing> erList)151 public OverlayEdgeRing findEdgeRingContaining(List<OverlayEdgeRing> erList) 152 { 153 LinearRing testRing = this.getRing(); 154 Envelope testEnv = testRing.getEnvelopeInternal(); 155 Coordinate testPt = testRing.getCoordinateN(0); 156 157 OverlayEdgeRing minRing = null; 158 Envelope minRingEnv = null; 159 for (OverlayEdgeRing tryEdgeRing: erList ) { 160 LinearRing tryRing = tryEdgeRing.getRing(); 161 Envelope tryShellEnv = tryRing.getEnvelopeInternal(); 162 // the hole envelope cannot equal the shell envelope 163 // (also guards against testing rings against themselves) 164 if (tryShellEnv.equals(testEnv)) continue; 165 166 // hole must be contained in shell 167 if (! tryShellEnv.contains(testEnv)) continue; 168 169 testPt = CoordinateArrays.ptNotInList(testRing.getCoordinates(), tryEdgeRing.getCoordinates()); 170 171 boolean isContained = tryEdgeRing.isInRing(testPt); 172 173 // check if the new containing ring is smaller than the current minimum ring 174 if (isContained) { 175 if (minRing == null 176 || minRingEnv.contains(tryShellEnv)) { 177 minRing = tryEdgeRing; 178 minRingEnv = minRing.getRing().getEnvelopeInternal(); 179 } 180 } 181 } 182 return minRing; 183 } 184 getLocator()185 private PointOnGeometryLocator getLocator() { 186 if (locator == null) { 187 locator = new IndexedPointInAreaLocator(getRing()); 188 } 189 return locator; 190 } 191 isInRing(Coordinate pt)192 public boolean isInRing(Coordinate pt) { 193 /** 194 * Use an indexed point-in-polygon for performance 195 */ 196 return Location.EXTERIOR != getLocator().locate(pt); 197 //return PointLocation.isInRing(pt, getCoordinates()); 198 } 199 getCoordinate()200 public Coordinate getCoordinate() { 201 return ringPts[0]; 202 } 203 204 /** 205 * Computes the {@link Polygon} formed by this ring and any contained holes. 206 * 207 * @return the {@link Polygon} formed by this ring and its holes. 208 */ toPolygon(GeometryFactory factory)209 public Polygon toPolygon(GeometryFactory factory) 210 { 211 LinearRing[] holeLR = null; 212 if (holes != null) { 213 holeLR = new LinearRing[holes.size()]; 214 for (int i = 0; i < holes.size(); i++) { 215 holeLR[i] = (LinearRing) holes.get(i).getRing(); 216 } 217 } 218 Polygon poly = factory.createPolygon(ring, holeLR); 219 return poly; 220 } 221 getEdge()222 public OverlayEdge getEdge() { 223 return startEdge; 224 } 225 } 226