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