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