Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix RelateOp for empty geometry and closed linear geometry #671

Merged
merged 1 commit into from
Jan 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>