diff --git a/presto-docs/src/main/sphinx/functions/geospatial.rst b/presto-docs/src/main/sphinx/functions/geospatial.rst index 1c9165391c23..7ee187fa9cd0 100644 --- a/presto-docs/src/main/sphinx/functions/geospatial.rst +++ b/presto-docs/src/main/sphinx/functions/geospatial.rst @@ -286,6 +286,15 @@ Accessors Returns the great-circle distance in meters between two SphericalGeography points. +.. function:: geometry_nearest_points(Geometry, Geometry) -> array(Point) + + Returns the points on each geometry nearest the other. If either geometry + is empty, return ``NULL``. Otherwise, return an array of two Points that have + the minimum distance of any two points on the geometries. The first Point + will be from the first Geometry argument, the second from the second Geometry + argument. If there are multiple pairs with the minimum distance, one pair + is chosen arbitrarily. + .. function:: ST_GeometryN(Geometry, index) -> Geometry Returns the geometry element at a given index (indices start at 1). diff --git a/presto-geospatial/src/main/java/com/facebook/presto/plugin/geospatial/GeoFunctions.java b/presto-geospatial/src/main/java/com/facebook/presto/plugin/geospatial/GeoFunctions.java index 4edfe1793839..6132a135fc68 100644 --- a/presto-geospatial/src/main/java/com/facebook/presto/plugin/geospatial/GeoFunctions.java +++ b/presto-geospatial/src/main/java/com/facebook/presto/plugin/geospatial/GeoFunctions.java @@ -56,6 +56,7 @@ import org.locationtech.jts.geom.TopologyException; import org.locationtech.jts.geom.impl.PackedCoordinateSequenceFactory; import org.locationtech.jts.linearref.LengthIndexedLine; +import org.locationtech.jts.operation.distance.DistanceOp; import java.util.ArrayList; import java.util.EnumSet; @@ -945,6 +946,29 @@ public static Double stDistance(@SqlType(GEOMETRY_TYPE_NAME) Slice left, @SqlTyp return leftGeometry.isEmpty() || rightGeometry.isEmpty() ? null : leftGeometry.distance(rightGeometry); } + @SqlNullable + @Description("Return the closest points on the two geometries") + @ScalarFunction("geometry_nearest_points") + @SqlType("array(" + GEOMETRY_TYPE_NAME + ")") + public static Block geometryNearestPoints(@SqlType(GEOMETRY_TYPE_NAME) Slice left, @SqlType(GEOMETRY_TYPE_NAME) Slice right) + { + Geometry leftGeometry = deserialize(left); + Geometry rightGeometry = deserialize(right); + if (leftGeometry.isEmpty() || rightGeometry.isEmpty()) { + return null; + } + try { + Coordinate[] nearestCoordinates = DistanceOp.nearestPoints(leftGeometry, rightGeometry); + BlockBuilder blockBuilder = GEOMETRY.createBlockBuilder(null, 2); + GEOMETRY.writeSlice(blockBuilder, serialize(createJtsPoint(nearestCoordinates[0]))); + GEOMETRY.writeSlice(blockBuilder, serialize(createJtsPoint(nearestCoordinates[1]))); + return blockBuilder.build(); + } + catch (TopologyException e) { + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, e.getMessage(), e); + } + } + @SqlNullable @Description("Returns a line string representing the exterior ring of the POLYGON") @ScalarFunction("ST_ExteriorRing") diff --git a/presto-geospatial/src/test/java/com/facebook/presto/plugin/geospatial/TestGeoFunctions.java b/presto-geospatial/src/test/java/com/facebook/presto/plugin/geospatial/TestGeoFunctions.java index 328dc064f927..c7ed7921efde 100644 --- a/presto-geospatial/src/test/java/com/facebook/presto/plugin/geospatial/TestGeoFunctions.java +++ b/presto-geospatial/src/test/java/com/facebook/presto/plugin/geospatial/TestGeoFunctions.java @@ -789,6 +789,43 @@ public void testSTDistance() assertFunction("ST_Distance(ST_GeometryFromText('MULTIPOLYGON EMPTY'), ST_GeometryFromText('POLYGON ((10 100, 30 10, 30 100, 10 100))'))", DOUBLE, null); } + @Test + public void testGeometryNearestPoints() + { + assertNearestPoints("POINT (50 100)", "POINT (150 150)", "POINT (50 100)", "POINT (150 150)"); + assertNearestPoints("MULTIPOINT (50 100, 50 200)", "POINT (50 100)", "POINT (50 100)", "POINT (50 100)"); + assertNearestPoints("LINESTRING (50 100, 50 200)", "LINESTRING (10 10, 20 20)", "POINT (50 100)", "POINT (20 20)"); + assertNearestPoints("MULTILINESTRING ((1 1, 5 1), (2 4, 4 4))", "LINESTRING (10 20, 20 50)", "POINT (4 4)", "POINT (10 20)"); + assertNearestPoints("POLYGON ((1 1, 1 3, 3 3, 3 1, 1 1))", "POLYGON ((4 4, 4 5, 5 5, 5 4, 4 4))", "POINT (3 3)", "POINT (4 4)"); + assertNearestPoints("MULTIPOLYGON (((1 1, 1 3, 3 3, 3 1, 1 1)), ((0 0, 0 2, 2 2, 2 0, 0 0)))", "POLYGON ((10 100, 30 10, 30 100, 10 100))", "POINT (3 3)", "POINT (30 10)"); + assertNearestPoints("GEOMETRYCOLLECTION (POINT (0 0), LINESTRING (0 20, 20 0))", "POLYGON ((5 5, 5 6, 6 6, 6 5, 5 5))", "POINT (10 10)", "POINT (6 6)"); + + assertNoNearestPoints("POINT EMPTY", "POINT (150 150)"); + assertNoNearestPoints("POINT (50 100)", "POINT EMPTY"); + assertNoNearestPoints("POINT EMPTY", "POINT EMPTY"); + assertNoNearestPoints("MULTIPOINT EMPTY", "POINT (50 100)"); + assertNoNearestPoints("LINESTRING (50 100, 50 200)", "LINESTRING EMPTY"); + assertNoNearestPoints("MULTILINESTRING EMPTY", "LINESTRING (10 20, 20 50)"); + assertNoNearestPoints("POLYGON ((1 1, 1 3, 3 3, 3 1, 1 1))", "POLYGON EMPTY"); + assertNoNearestPoints("MULTIPOLYGON EMPTY", "POLYGON ((10 100, 30 10, 30 100, 10 100))"); + } + + private void assertNearestPoints(String leftInputWkt, String rightInputWkt, String leftPointWkt, String rightPointWkt) + { + assertFunction( + format("transform(geometry_nearest_points(ST_GeometryFromText('%s'), ST_GeometryFromText('%s')), x -> ST_ASText(x))", leftInputWkt, rightInputWkt), + new ArrayType(VARCHAR), + ImmutableList.of(leftPointWkt, rightPointWkt)); + } + + private void assertNoNearestPoints(String leftInputWkt, String rightInputWkt) + { + assertFunction( + format("geometry_nearest_points(ST_GeometryFromText('%s'), ST_GeometryFromText('%s'))", leftInputWkt, rightInputWkt), + new ArrayType(GEOMETRY), + null); + } + @Test public void testSTExteriorRing() {