Skip to content

Commit

Permalink
Fix Relate for closed linear geometry and empty geometry (#671)
Browse files Browse the repository at this point in the history
Signed-off-by: Martin Davis <[email protected]>
  • Loading branch information
dr-jts authored Jan 21, 2021
1 parent 04380b7 commit ac62dec
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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:
* <ul>
* <li>Empty geometries do not have boundaries.
* <li>Points do not have boundaries.
* <li>For linear geometries the existence of the boundary
* is determined by the {@link BoundaryNodeRule}.
* <li>Non-empty polygons always have a boundary.
* </ul>
*
* @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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

/**
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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(); ) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;


/**
Expand All @@ -28,7 +28,7 @@
* @version 1.7
*/
public class BoundaryTest
extends TestCase
extends GeometryTestCase
{
private static final double TOLERANCE = 0.00005;

Expand Down Expand Up @@ -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
{
Expand All @@ -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));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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));
}
}
26 changes: 26 additions & 0 deletions modules/tests/src/test/resources/testxml/general/TestRelateLA.xml
Original file line number Diff line number Diff line change
Expand Up @@ -187,4 +187,30 @@
</test>
</case>

<case>
<desc>LA - closed line / empty polygon</desc>
<a>
LINESTRING(110 60, 20 150, 200 150, 110 60)
</a>
<b>
POLYGON EMPTY
</b>
<test>
<op name="relate" arg1="A" arg2="B" arg3="FF1FFFFF2">true</op>
</test>
</case>

<case>
<desc>LA - closed multiline / empty polygon</desc>
<a>
MULTILINESTRING ((0 0, 0 1), (0 1, 1 1, 1 0, 0 0))
</a>
<b>
POLYGON EMPTY
</b>
<test>
<op name="relate" arg1="A" arg2="B" arg3="FF1FFFFF2">true</op>
</test>
</case>

</run>
12 changes: 12 additions & 0 deletions modules/tests/src/test/resources/testxml/general/TestRelateLL.xml
Original file line number Diff line number Diff line change
Expand Up @@ -307,5 +307,17 @@
</test>
</case>

<case>
<desc>LA - closed multiline / empty line</desc>
<a>
MULTILINESTRING ((0 0, 0 1), (0 1, 1 1, 1 0, 0 0))
</a>
<b>
LINESTRING EMPTY
</b>
<test>
<op name="relate" arg1="A" arg2="B" arg3="FF1FFFFF2">true</op>
</test>
</case>

</run>

0 comments on commit ac62dec

Please sign in to comment.