Skip to content

Commit

Permalink
Use JTS for ST_IsValid, ST_IsSimple, and geometry_invalid_reason
Browse files Browse the repository at this point in the history
  • Loading branch information
jagill authored and arhimondr committed Feb 25, 2020
1 parent f6534c7 commit f4bc471
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 63 deletions.
7 changes: 5 additions & 2 deletions presto-docs/src/main/sphinx/functions/geospatial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ Accessors
.. function:: ST_IsSimple(Geometry) -> boolean

Returns ``true`` if this Geometry has no anomalous geometric points, such as self intersection or self tangency.
Use :func:`geometry_invalid_reason` to determine why the geometry is not simple.

.. function:: ST_IsRing(Geometry) -> boolean

Expand Down Expand Up @@ -431,8 +432,10 @@ Accessors

.. function:: geometry_invalid_reason(Geometry) -> varchar

Returns the reason for why the input geometry is not valid.
Returns ``null`` if the input is valid.
Returns the reason for why the input geometry is not valid or not simple.
If the geometry is neither valid no simple, it will only give the reason
for invalidity.
Returns ``null`` if the input is valid and simple.

.. function:: great_circle_distance(latitude1, longitude1, latitude2, longitude2) -> double

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,15 @@
import org.locationtech.jts.io.ParseException;
import org.locationtech.jts.io.WKTReader;
import org.locationtech.jts.io.WKTWriter;
import org.locationtech.jts.operation.IsSimpleOp;
import org.locationtech.jts.operation.valid.IsValidOp;
import org.locationtech.jts.operation.valid.TopologyValidationError;

import java.util.HashSet;
import java.util.Set;

import static com.facebook.presto.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT;
import static java.lang.String.format;

public final class GeometryUtils
{
Expand Down Expand Up @@ -276,4 +280,49 @@ public static org.locationtech.jts.geom.Geometry createJtsEmptyPolygon()
{
return GEOMETRY_FACTORY.createPolygon();
}

public static String getGeometryInvalidReason(org.locationtech.jts.geom.Geometry geometry)
{
IsValidOp validOp = new IsValidOp(geometry);
IsSimpleOp simpleOp = new IsSimpleOp(geometry);
try {
TopologyValidationError err = validOp.getValidationError();
if (err != null) {
return err.getMessage();
}
}
catch (UnsupportedOperationException e) {
// This is thrown if the type of geometry is unsupported by JTS.
// It should not happen in practice.
throw new PrestoException(INVALID_FUNCTION_ARGUMENT, "Geometry type not valid", e);
}
if (!simpleOp.isSimple()) {
String errorDescription;
String geometryType = geometry.getGeometryType();
switch (GeometryType.getForJtsGeometryType(geometryType)) {
case POINT:
errorDescription = "Invalid point";
break;
case MULTI_POINT:
errorDescription = "Repeated point";
break;
case LINE_STRING:
case MULTI_LINE_STRING:
errorDescription = "Self-intersection at or near";
break;
case POLYGON:
case MULTI_POLYGON:
case GEOMETRY_COLLECTION:
// In OGC (which JTS follows): Polygons, MultiPolygons, Geometry Collections are simple.
// This shouldn't happen, but in case it does, return a reasonable generic message.
errorDescription = "Topology exception at or near";
break;
default:
throw new PrestoException(INVALID_FUNCTION_ARGUMENT, format("Unknown geometry type: %s", geometryType));
}
org.locationtech.jts.geom.Coordinate nonSimpleLocation = simpleOp.getNonSimpleLocation();
return format("[%s] %s: (%s %s)", geometryType, errorDescription, nonSimpleLocation.getX(), nonSimpleLocation.getY());
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,7 @@
import com.esri.core.geometry.GeometryException;
import com.esri.core.geometry.ListeningGeometryCursor;
import com.esri.core.geometry.MultiPath;
import com.esri.core.geometry.MultiVertexGeometry;
import com.esri.core.geometry.NonSimpleResult;
import com.esri.core.geometry.NonSimpleResult.Reason;
import com.esri.core.geometry.OperatorSimplifyOGC;
import com.esri.core.geometry.OperatorUnion;
import com.esri.core.geometry.Point;
import com.esri.core.geometry.Polygon;
Expand Down Expand Up @@ -58,6 +55,7 @@
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.MultiLineString;
import org.locationtech.jts.geom.TopologyException;
import org.locationtech.jts.geom.impl.PackedCoordinateSequenceFactory;
import org.locationtech.jts.linearref.LengthIndexedLine;

Expand Down Expand Up @@ -93,6 +91,7 @@
import static com.facebook.presto.geospatial.GeometryUtils.createJtsLineString;
import static com.facebook.presto.geospatial.GeometryUtils.createJtsMultiPoint;
import static com.facebook.presto.geospatial.GeometryUtils.createJtsPoint;
import static com.facebook.presto.geospatial.GeometryUtils.getGeometryInvalidReason;
import static com.facebook.presto.geospatial.GeometryUtils.getPointCount;
import static com.facebook.presto.geospatial.GeometryUtils.jtsGeometryFromWkt;
import static com.facebook.presto.geospatial.GeometryUtils.wktFromJtsGeometry;
Expand Down Expand Up @@ -443,25 +442,30 @@ public static Boolean stIsEmpty(@SqlType(GEOMETRY_TYPE_NAME) Slice input)
@SqlType(BOOLEAN)
public static boolean stIsSimple(@SqlType(GEOMETRY_TYPE_NAME) Slice input)
{
OGCGeometry geometry = EsriGeometrySerde.deserialize(input);
return geometry.isEmpty() || geometry.isSimple();
try {
return deserialize(input).isSimple();
}
catch (PrestoException e) {
if (e.getCause() instanceof TopologyException) {
return false;
}
throw e;
}
}

@Description("Returns true if the input geometry is well formed")
@ScalarFunction("ST_IsValid")
@SqlType(BOOLEAN)
public static boolean stIsValid(@SqlType(GEOMETRY_TYPE_NAME) Slice input)
{
GeometryCursor cursor = EsriGeometrySerde.deserialize(input).getEsriGeometryCursor();
while (true) {
com.esri.core.geometry.Geometry geometry = cursor.next();
if (geometry == null) {
return true;
}

if (!OperatorSimplifyOGC.local().isSimpleOGC(geometry, null, true, null, null)) {
try {
return deserialize(input).isValid();
}
catch (PrestoException e) {
if (e.getCause() instanceof TopologyException) {
return false;
}
throw e;
}
}

Expand All @@ -471,35 +475,15 @@ public static boolean stIsValid(@SqlType(GEOMETRY_TYPE_NAME) Slice input)
@SqlNullable
public static Slice invalidReason(@SqlType(GEOMETRY_TYPE_NAME) Slice input)
{
GeometryCursor cursor = EsriGeometrySerde.deserialize(input).getEsriGeometryCursor();
NonSimpleResult result = new NonSimpleResult();
while (true) {
com.esri.core.geometry.Geometry geometry = cursor.next();
if (geometry == null) {
return null;
}

if (!OperatorSimplifyOGC.local().isSimpleOGC(geometry, null, true, result, null)) {
String reasonText = NON_SIMPLE_REASONS.getOrDefault(result.m_reason, result.m_reason.name());

if (!(geometry instanceof MultiVertexGeometry)) {
return utf8Slice(reasonText);
}

MultiVertexGeometry multiVertexGeometry = (MultiVertexGeometry) geometry;
if (result.m_vertexIndex1 >= 0 && result.m_vertexIndex2 >= 0) {
Point point1 = multiVertexGeometry.getPoint(result.m_vertexIndex1);
Point point2 = multiVertexGeometry.getPoint(result.m_vertexIndex2);
return utf8Slice(format("%s at or near (%s %s) and (%s %s)", reasonText, point1.getX(), point1.getY(), point2.getX(), point2.getY()));
}

if (result.m_vertexIndex1 >= 0) {
Point point = multiVertexGeometry.getPoint(result.m_vertexIndex1);
return utf8Slice(format("%s at or near (%s %s)", reasonText, point.getX(), point.getY()));
}

return utf8Slice(reasonText);
try {
Geometry geometry = deserialize(input);
return utf8Slice(getGeometryInvalidReason(geometry));
}
catch (PrestoException e) {
if (e.getCause() instanceof TopologyException) {
return utf8Slice(e.getMessage());
}
throw e;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,9 @@ public void testSTIsSimple()
assertSimpleGeometry("POLYGON EMPTY");
assertSimpleGeometry("POLYGON ((2 0, 2 1, 3 1, 2 0))");
assertSimpleGeometry("MULTIPOLYGON (((1 1, 1 3, 3 3, 3 1, 1 1)), ((2 4, 2 6, 6 6, 6 4, 2 4)))");
assertNotSimpleGeometry("LINESTRING (0 0, -1 0.5, 0 1, 1 1, 1 0, 0 1, 0 0)");
assertNotSimpleGeometry("MULTIPOINT ((0 0), (0 1), (1 1), (0 1))");
assertNotSimpleGeometry("LINESTRING (0 0, -1 0.5, 0 1, 1 1, 1 0, 0 1, 0 0)");
}

@Test
Expand Down Expand Up @@ -410,11 +413,13 @@ public void testSTIsValid()
assertValidGeometry("POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))");
assertValidGeometry("MULTIPOLYGON (((1 1, 1 3, 3 3, 3 1, 1 1)), ((2 4, 2 6, 6 6, 6 4, 2 4)))");
assertValidGeometry("GEOMETRYCOLLECTION (POINT (1 2), LINESTRING (0 0, 1 2, 3 4), POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0)))");
assertValidGeometry("MULTIPOINT ((0 0), (0 1), (1 1), (0 1))");
// JTS considers LineStrings with repeated points valid/simple (it drops the dups)
assertValidGeometry("LINESTRING (0 0, 0 1, 0 1, 1 1, 1 0, 0 0)");
// Valid but not simple
assertValidGeometry("LINESTRING (0 0, -1 0.5, 0 1, 1 1, 1 0, 0 1, 0 0)");

// invalid geometries
assertInvalidGeometry("MULTIPOINT ((0 0), (0 1), (1 1), (0 1))");
assertInvalidGeometry("LINESTRING (0 0, 0 1, 0 1, 1 1, 1 0, 0 0)");
assertInvalidGeometry("LINESTRING (0 0, -1 0.5, 0 1, 1 1, 1 0, 0 1, 0 0)");
assertInvalidGeometry("POLYGON ((0 0, 1 1, 0 1, 1 0, 0 0))");
assertInvalidGeometry("POLYGON ((0 0, 0 1, 0 1, 1 1, 1 0, 0 0), (2 2, 2 3, 3 3, 3 2, 2 2))");
assertInvalidGeometry("POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0), (2 2, 2 3, 3 3, 3 2, 2 2))");
Expand Down Expand Up @@ -444,28 +449,27 @@ private void assertInvalidGeometry(String wkt)
public void testGeometryInvalidReason()
{
// invalid geometries
assertInvalidReason("MULTIPOINT ((0 0), (0 1), (1 1), (0 1))", "Repeated points at or near (0.0 1.0) and (0.0 1.0)");
assertInvalidReason("LINESTRING (0 0, 0 1, 0 1, 1 1, 1 0, 0 0)", "Degenerate segments at or near (0.0 1.0)");
assertInvalidReason("LINESTRING (0 0, -1 0.5, 0 1, 1 1, 1 0, 0 1, 0 0)", "Self-tangency at or near (0.0 1.0) and (0.0 1.0)");
assertInvalidReason("POLYGON ((0 0, 1 1, 0 1, 1 0, 0 0))", "Intersecting or overlapping segments at or near (1.0 0.0) and (1.0 1.0)");
assertInvalidReason("POLYGON ((0 0, 0 1, 0 1, 1 1, 1 0, 0 0), (2 2, 2 3, 3 3, 3 2, 2 2))", "Degenerate segments at or near (0.0 1.0)");
assertInvalidReason("POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0), (2 2, 2 3, 3 3, 3 2, 2 2))", "RingOrientation");
assertInvalidReason("POLYGON ((0 0, 0 1, 2 1, 1 1, 1 0, 0 0))", "Intersecting or overlapping segments at or near (0.0 1.0) and (2.0 1.0)");
assertInvalidReason("POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0), (0 1, 1 1, 0.5 0.5, 0 1))", "Self-intersection at or near (0.0 1.0) and (1.0 1.0)");
assertInvalidReason("POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0), (0 0, 0.5 0.7, 1 1, 0.5 0.4, 0 0))", "Disconnected interior at or near (0.0 1.0)");
assertInvalidReason("POLYGON ((0 0, -1 0.5, 0 1, 1 1, 1 0, 0 1, 0 0))", "Self-tangency at or near (0.0 1.0) and (0.0 1.0)");
assertInvalidReason("MULTIPOLYGON (((0 0, 0 1, 1 1, 1 0, 0 0)), ((0.5 0.5, 0.5 2, 2 2, 2 0.5, 0.5 0.5)))", "Intersecting or overlapping segments at or near (0.0 1.0) and (0.5 0.5)");
assertInvalidReason("GEOMETRYCOLLECTION (POINT (1 2), POLYGON ((0 0, 0 1, 2 1, 1 1, 1 0, 0 0)))", "Intersecting or overlapping segments at or near (0.0 1.0) and (2.0 1.0)");
assertInvalidReason("MULTIPOINT ((0 0), (0 1), (1 1), (0 1))", "[MultiPoint] Repeated point: (0.0 1.0)");
assertInvalidReason("LINESTRING (0 0, -1 0.5, 0 1, 1 1, 1 0, 0 1, 0 0)", "[LineString] Self-intersection at or near: (0.0 1.0)");
assertInvalidReason("POLYGON ((0 0, 1 1, 0 1, 1 0, 0 0))", "Error constructing Polygon: shell is empty but holes are not");
assertInvalidReason("POLYGON ((0 0, 0 1, 0 1, 1 1, 1 0, 0 0), (2 2, 2 3, 3 3, 3 2, 2 2))", "Hole lies outside shell");
assertInvalidReason("POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0), (2 2, 2 3, 3 3, 3 2, 2 2))", "Hole lies outside shell");
assertInvalidReason("POLYGON ((0 0, 0 1, 2 1, 1 1, 1 0, 0 0))", "Self-intersection");
assertInvalidReason("POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0), (0 1, 1 1, 0.5 0.5, 0 1))", "Self-intersection");
assertInvalidReason("POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0), (0 0, 0.5 0.7, 1 1, 0.5 0.4, 0 0))", "Interior is disconnected");
assertInvalidReason("POLYGON ((0 0, -1 0.5, 0 1, 1 1, 1 0, 0 1, 0 0))", "Ring Self-intersection");
assertInvalidReason("MULTIPOLYGON (((0 0, 0 1, 1 1, 1 0, 0 0)), ((0.5 0.5, 0.5 2, 2 2, 2 0.5, 0.5 0.5)))", "Self-intersection");
assertInvalidReason("GEOMETRYCOLLECTION (POINT (1 2), POLYGON ((0 0, 0 1, 2 1, 1 1, 1 0, 0 0)))", "Self-intersection");

// non-simple geometries
assertInvalidReason("MULTIPOINT (1 2, 2 4, 3 6, 1 2)", "Repeated points at or near (1.0 2.0) and (1.0 2.0)");
assertInvalidReason("LINESTRING (0 0, 1 1, 1 0, 0 1)", "Intersecting or overlapping segments at or near (0.0 0.0) and (1.0 0.0)");
assertInvalidReason("MULTILINESTRING ((1 1, 5 1), (2 4, 4 0))", "Intersecting or overlapping segments at or near (1.0 1.0) and (2.0 4.0)");
assertInvalidReason("MULTIPOINT (1 2, 2 4, 3 6, 1 2)", "[MultiPoint] Repeated point: (1.0 2.0)");
assertInvalidReason("LINESTRING (0 0, 1 1, 1 0, 0 1)", "[LineString] Self-intersection at or near: (0.5 0.5)");
assertInvalidReason("MULTILINESTRING ((1 1, 5 1), (2 4, 4 0))", "[MultiLineString] Self-intersection at or near: (3.5 1.0)");
}

private void assertInvalidReason(String wkt, String reason) {
private void assertInvalidReason(String wkt, String reason)
{
assertFunction("geometry_invalid_reason(ST_GeometryFromText('" + wkt + "'))", VARCHAR, reason);

}

@Test
Expand Down

0 comments on commit f4bc471

Please sign in to comment.