diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/BoundaryOp.java b/modules/core/src/main/java/org/locationtech/jts/operation/BoundaryOp.java index 427aba9f36..e4f165f76e 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/BoundaryOp.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/BoundaryOp.java @@ -21,6 +21,7 @@ import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.CoordinateArrays; import org.locationtech.jts.geom.CoordinateSequence; +import org.locationtech.jts.geom.Dimension; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryCollection; import org.locationtech.jts.geom.GeometryFactory; @@ -69,6 +70,37 @@ public static Geometry getBoundary(Geometry g, BoundaryNodeRule bnRule) return bop.getBoundary(); } + /** + * Tests if a geometry has a boundary (it is non-empty). + * The semantics are: + * + * + * @param geom the geometry providing the boundary + * @param boundaryNodeRule the Boundary Node Rule to use + * @return true if the boundary exists + */ + public static boolean hasBoundary(Geometry geom, BoundaryNodeRule boundaryNodeRule) { + // Note that this does not handle geometry collections with a non-empty linear element + if (geom.isEmpty()) return false; + switch (geom.getDimension()) { + case Dimension.P: return false; + /** + * Linear geometries might have an empty boundary due to boundary node rule. + */ + case Dimension.L: + Geometry boundary = BoundaryOp.getBoundary(geom, boundaryNodeRule); + return ! boundary.isEmpty(); + case Dimension.A: return true; + } + return true; + } + private Geometry geom; private GeometryFactory geomFact; private BoundaryNodeRule bnRule; diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relate/RelateComputer.java b/modules/core/src/main/java/org/locationtech/jts/operation/relate/RelateComputer.java index 05f06fbb66..f559a1b0ef 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/relate/RelateComputer.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relate/RelateComputer.java @@ -21,10 +21,12 @@ import java.util.Iterator; import java.util.List; +import org.locationtech.jts.algorithm.BoundaryNodeRule; import org.locationtech.jts.algorithm.LineIntersector; import org.locationtech.jts.algorithm.PointLocator; import org.locationtech.jts.algorithm.RobustLineIntersector; import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Dimension; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.IntersectionMatrix; import org.locationtech.jts.geom.Location; @@ -36,6 +38,7 @@ import org.locationtech.jts.geomgraph.Node; import org.locationtech.jts.geomgraph.NodeMap; import org.locationtech.jts.geomgraph.index.SegmentIntersector; +import org.locationtech.jts.operation.BoundaryOp; import org.locationtech.jts.util.Assert; /** @@ -79,7 +82,7 @@ public IntersectionMatrix computeIM() // if the Geometries don't overlap there is nothing to do if (! arg[0].getGeometry().getEnvelopeInternal().intersects( arg[1].getGeometry().getEnvelopeInternal()) ) { - computeDisjointIM(im); + computeDisjointIM(im, arg[0].getBoundaryNodeRule()); return im; } arg[0].computeSelfNodes(li, false); @@ -268,21 +271,56 @@ private void labelIntersectionNodes(int argIndex) /** * If the Geometries are disjoint, we need to enter their dimension and * boundary dimension in the Ext rows in the IM + * + * @param boundaryNodeRule the Boundary Node Rule to use */ - private void computeDisjointIM(IntersectionMatrix im) + private void computeDisjointIM(IntersectionMatrix im, BoundaryNodeRule boundaryNodeRule) { Geometry ga = arg[0].getGeometry(); if (! ga.isEmpty()) { im.set(Location.INTERIOR, Location.EXTERIOR, ga.getDimension()); - im.set(Location.BOUNDARY, Location.EXTERIOR, ga.getBoundaryDimension()); + im.set(Location.BOUNDARY, Location.EXTERIOR, getBoundaryDim(ga, boundaryNodeRule)); } Geometry gb = arg[1].getGeometry(); if (! gb.isEmpty()) { im.set(Location.EXTERIOR, Location.INTERIOR, gb.getDimension()); - im.set(Location.EXTERIOR, Location.BOUNDARY, gb.getBoundaryDimension()); + im.set(Location.EXTERIOR, Location.BOUNDARY, getBoundaryDim(gb, boundaryNodeRule)); } } - + + /** + * Compute the IM entry for the intersection of the boundary + * of a geometry with the Exterior. + * This is the nominal dimension of the boundary + * unless the boundary is empty, in which case it is {@link Dimension#FALSE}. + * For linear geometries the Boundary Node Rule determines + * whether the boundary is empty. + * + * @param geom the geometry providing the boundary + * @param boundaryNodeRule the Boundary Node Rule to use + * @return the IM dimension entry + */ + private static int getBoundaryDim(Geometry geom, BoundaryNodeRule boundaryNodeRule) + { + /** + * If the geometry has a non-empty boundary + * the intersection is the nominal dimension. + */ + if (BoundaryOp.hasBoundary(geom, boundaryNodeRule)) { + /** + * special case for lines, since Geometry.getBoundaryDimension is not aware + * of Boundary Node Rule. + */ + if (geom.getDimension() == 1) + return Dimension.P; + return geom.getBoundaryDimension(); + } + /** + * Otherwise intersection is F + */ + return Dimension.FALSE; + } + private void labelNodeEdges() { for (Iterator ni = nodes.iterator(); ni.hasNext(); ) { diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/BoundaryTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/BoundaryTest.java index c9dc607fb1..a458508674 100644 --- a/modules/core/src/test/java/org/locationtech/jts/operation/BoundaryTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/operation/BoundaryTest.java @@ -17,8 +17,8 @@ import org.locationtech.jts.io.ParseException; import org.locationtech.jts.io.WKTReader; -import junit.framework.TestCase; import junit.textui.TestRunner; +import test.jts.GeometryTestCase; /** @@ -28,7 +28,7 @@ * @version 1.7 */ public class BoundaryTest - extends TestCase + extends GeometryTestCase { private static final double TOLERANCE = 0.00005; @@ -121,8 +121,48 @@ public void testRing() "POINT (100 100)" ); } - - + public void testHasBoundaryPoint() + throws Exception + { + checkHasBoundary( "POINT (0 0)", false); + } + + public void testHasBoundaryPointEmpty() + throws Exception + { + checkHasBoundary( "POINT EMPTY", false); + } + + public void testHasBoundaryRingClosed() + throws Exception + { + checkHasBoundary( "LINESTRING (100 100, 20 20, 200 20, 100 100)", false); + } + + public void testHasBoundaryMultiLineStringClosed() + throws Exception + { + checkHasBoundary( "MULTILINESTRING ((0 0, 0 1), (0 1, 1 1, 1 0, 0 0))", false); + } + + public void testHasBoundaryMultiLineStringOpen() + throws Exception + { + checkHasBoundary( "MULTILINESTRING ((0 0, 0 2), (0 1, 1 1, 1 0, 0 0))"); + } + + public void testHasBoundaryPolygon() + throws Exception + { + checkHasBoundary( "POLYGON ((1 9, 9 9, 9 1, 1 1, 1 9))"); + } + + public void testHasBoundaryPolygonEmpty() + throws Exception + { + checkHasBoundary( "POLYGON EMPTY", false); + } + private void runBoundaryTest(String wkt, BoundaryNodeRule bnRule, String wktExpected) throws ParseException { @@ -136,4 +176,20 @@ private void runBoundaryTest(String wkt, BoundaryNodeRule bnRule, String wktExpe assertTrue(boundary.equalsExact(expected)); } + private void checkHasBoundary(String wkt) + { + checkHasBoundary(wkt, BoundaryNodeRule.MOD2_BOUNDARY_RULE, true); + } + + private void checkHasBoundary(String wkt, boolean expected) + { + checkHasBoundary(wkt, BoundaryNodeRule.MOD2_BOUNDARY_RULE, expected); + } + + private void checkHasBoundary(String wkt, BoundaryNodeRule bnRule, boolean expected) + { + Geometry g = read(wkt); + assertEquals(expected, BoundaryOp.hasBoundary(g, bnRule)); + } + } diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/relate/RelateBoundaryNodeRuleTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/relate/RelateBoundaryNodeRuleTest.java index 91aeff0e09..18f2e0a822 100644 --- a/modules/core/src/test/java/org/locationtech/jts/operation/relate/RelateBoundaryNodeRuleTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/operation/relate/RelateBoundaryNodeRuleTest.java @@ -105,6 +105,32 @@ public void testLineRingTouchAtEndpointAndInterior() runRelateTest(a, b, BoundaryNodeRule.ENDPOINT_BOUNDARY_RULE, "F01FF0102" ); } + public void testPolygonEmptyRing() + throws Exception + { + String a = "POLYGON EMPTY"; + String b = "LINESTRING (20 100, 20 220, 120 100, 20 100)"; + + // closed line has no boundary under SFS rule + runRelateTest(a, b, BoundaryNodeRule.OGC_SFS_BOUNDARY_RULE, "FFFFFF1F2" ); + + // closed line has boundary under ENDPOINT rule + runRelateTest(a, b, BoundaryNodeRule.ENDPOINT_BOUNDARY_RULE, "FFFFFF102" ); + } + + public void testPolygonEmptyMultiLineStringClosed() + throws Exception + { + String a = "POLYGON EMPTY"; + String b = "MULTILINESTRING ((0 0, 0 1), (0 1, 1 1, 1 0, 0 0))"; + + // closed line has no boundary under SFS rule + runRelateTest(a, b, BoundaryNodeRule.OGC_SFS_BOUNDARY_RULE, "FFFFFF1F2" ); + + // closed line has boundary under ENDPOINT rule + runRelateTest(a, b, BoundaryNodeRule.ENDPOINT_BOUNDARY_RULE, "FFFFFF102" ); + } + void runRelateTest(String wkt1, String wkt2, BoundaryNodeRule bnRule, String expectedIM) throws ParseException { @@ -113,6 +139,6 @@ void runRelateTest(String wkt1, String wkt2, BoundaryNodeRule bnRule, String exp IntersectionMatrix im = RelateOp.relate(g1, g2, bnRule); String imStr = im.toString(); //System.out.println(imStr); - assertTrue(im.matches(expectedIM)); + assertTrue("Expected " + expectedIM + ", found " + im, im.matches(expectedIM)); } } diff --git a/modules/tests/src/test/resources/testxml/general/TestRelateLA.xml b/modules/tests/src/test/resources/testxml/general/TestRelateLA.xml index 8872ace71b..acc895468f 100644 --- a/modules/tests/src/test/resources/testxml/general/TestRelateLA.xml +++ b/modules/tests/src/test/resources/testxml/general/TestRelateLA.xml @@ -187,4 +187,30 @@ + +LA - closed line / empty polygon + + LINESTRING(110 60, 20 150, 200 150, 110 60) + + + POLYGON EMPTY + + + true + + + + +LA - closed multiline / empty polygon + + MULTILINESTRING ((0 0, 0 1), (0 1, 1 1, 1 0, 0 0)) + + + POLYGON EMPTY + + + true + + + diff --git a/modules/tests/src/test/resources/testxml/general/TestRelateLL.xml b/modules/tests/src/test/resources/testxml/general/TestRelateLL.xml index 7606d01ff5..4c05b66642 100644 --- a/modules/tests/src/test/resources/testxml/general/TestRelateLL.xml +++ b/modules/tests/src/test/resources/testxml/general/TestRelateLL.xml @@ -307,5 +307,17 @@ + +LA - closed multiline / empty line + + MULTILINESTRING ((0 0, 0 1), (0 1, 1 1, 1 0, 0 0)) + + + LINESTRING EMPTY + + + true + +