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