diff --git a/presto-docs/src/main/sphinx/functions/geospatial.rst b/presto-docs/src/main/sphinx/functions/geospatial.rst index 68fa934250655..aa8d94c8aa29e 100644 --- a/presto-docs/src/main/sphinx/functions/geospatial.rst +++ b/presto-docs/src/main/sphinx/functions/geospatial.rst @@ -409,6 +409,14 @@ Accessors Returns ``null`` if a LineString or a Point is empty or ``null``. +.. function:: line_interpolate_point(LineString, double) -> Geometry + + Returns the Point on the LineString at a fractional distance given by the + double argument. Throws an exception if the distance is not between 0 and 1. + + Returns an empty Point if the LineString is empty. Returns ``null`` if + either the LineString or double is null. + .. function:: geometry_invalid_reason(Geometry) -> varchar Returns the reason for why the input geometry is not valid. diff --git a/presto-geospatial-toolkit/src/main/java/com/facebook/presto/geospatial/GeometryUtils.java b/presto-geospatial-toolkit/src/main/java/com/facebook/presto/geospatial/GeometryUtils.java index 14d2d5a84f3d2..ab1bd4bb5ed65 100644 --- a/presto-geospatial-toolkit/src/main/java/com/facebook/presto/geospatial/GeometryUtils.java +++ b/presto-geospatial-toolkit/src/main/java/com/facebook/presto/geospatial/GeometryUtils.java @@ -23,12 +23,19 @@ import com.esri.core.geometry.ogc.OGCGeometry; import com.esri.core.geometry.ogc.OGCPoint; import com.esri.core.geometry.ogc.OGCPolygon; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.CoordinateSequenceFactory; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.impl.PackedCoordinateSequenceFactory; import java.util.HashSet; import java.util.Set; public final class GeometryUtils { + private static final CoordinateSequenceFactory COORDINATE_SEQUENCE_FACTORY = new PackedCoordinateSequenceFactory(); + private static final GeometryFactory GEOMETRY_FACTORY = new GeometryFactory(COORDINATE_SEQUENCE_FACTORY); + private GeometryUtils() {} /** @@ -212,4 +219,14 @@ public static boolean isPointOrRectangle(OGCGeometry ogcGeometry, Envelope envel return true; } + + public static org.locationtech.jts.geom.Point makeJtsEmptyPoint() + { + return GEOMETRY_FACTORY.createPoint(); + } + + public static org.locationtech.jts.geom.Point makeJtsPoint(Coordinate coordinate) + { + return GEOMETRY_FACTORY.createPoint(coordinate); + } } 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 7a866c6544b70..98f27483664ab 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 @@ -50,8 +50,10 @@ import com.google.common.base.VerifyException; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import io.airlift.slice.Slice; import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.LineString; import org.locationtech.jts.linearref.LengthIndexedLine; import java.util.ArrayDeque; @@ -81,6 +83,8 @@ import static com.facebook.presto.geospatial.GeometryType.POINT; import static com.facebook.presto.geospatial.GeometryType.POLYGON; import static com.facebook.presto.geospatial.GeometryUtils.getPointCount; +import static com.facebook.presto.geospatial.GeometryUtils.makeJtsEmptyPoint; +import static com.facebook.presto.geospatial.GeometryUtils.makeJtsPoint; import static com.facebook.presto.geospatial.serde.GeometrySerde.deserialize; import static com.facebook.presto.geospatial.serde.GeometrySerde.deserializeEnvelope; import static com.facebook.presto.geospatial.serde.GeometrySerde.deserializeType; @@ -547,6 +551,27 @@ public static Double lineLocatePoint(@SqlType(GEOMETRY_TYPE_NAME) Slice lineSlic return new LengthIndexedLine(line).indexOf(point.getCoordinate()) / line.getLength(); } + @Description("Returns the point in the line at the fractional length.") + @ScalarFunction("line_interpolate_point") + @SqlType(GEOMETRY_TYPE_NAME) + public static Slice lineInterpolatePoint(@SqlType(GEOMETRY_TYPE_NAME) Slice lineSlice, @SqlType(DOUBLE) double fraction) + { + if (!(0.0 <= fraction && fraction <= 1.0)) { + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, format("line_interpolate_point: Fraction must be between 0 and 1, but is %s", fraction)); + } + + Geometry geometry = JtsGeometrySerde.deserialize(lineSlice); + validateType("line_interpolate_point", geometry, ImmutableSet.of(LINE_STRING)); + LineString line = (LineString) geometry; + + if (line.isEmpty()) { + return JtsGeometrySerde.serialize(makeJtsEmptyPoint()); + } + + org.locationtech.jts.geom.Coordinate coordinate = new LengthIndexedLine(line).extractPoint(fraction * line.getLength()); + return JtsGeometrySerde.serialize(makeJtsPoint(coordinate)); + } + @SqlNullable @Description("Returns X maxima of a bounding box of a Geometry") @ScalarFunction("ST_XMax") @@ -1327,6 +1352,14 @@ private static void validateType(String function, OGCGeometry geometry, Set validTypes) + { + GeometryType type = GeometryType.getForJtsGeometryType(geometry.getGeometryType()); + if (!validTypes.contains(type)) { + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, format("%s only applies to %s. Input type is: %s", function, OR_JOINER.join(validTypes), type)); + } + } + private static void verifySameSpatialReference(OGCGeometry leftGeometry, OGCGeometry rightGeometry) { checkArgument(Objects.equals(leftGeometry.getEsriSpatialReference(), rightGeometry.getEsriSpatialReference()), "Input geometries must have the same spatial reference"); 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 0bfb91636f6c9..f7365084c8f0e 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 @@ -469,6 +469,30 @@ public void testLineLocatePoint() assertInvalidFunction("line_locate_point(ST_GeometryFromText('LINESTRING (0 0, 0 1, 2 1)'), ST_GeometryFromText('POLYGON ((1 1, 1 4, 4 4, 4 1, 1 1))'))", "Second argument to line_locate_point must be a Point. Got: Polygon"); } + @Test + public void testLineInterpolatePoint() + { + assertLineInterpolatePoint("LINESTRING EMPTY", 0.5, "POINT EMPTY"); + assertLineInterpolatePoint("LINESTRING (0 0, 0 1)", 0.2, "POINT (0 0.2)"); + assertLineInterpolatePoint("LINESTRING (0 0, 0 1)", 0.0, "POINT (0 0)"); + assertLineInterpolatePoint("LINESTRING (0 0, 0 1)", 1.0, "POINT (0 1)"); + assertLineInterpolatePoint("LINESTRING (0 0, 0 1, 3 1)", 0.0625, "POINT (0 0.25)"); + assertLineInterpolatePoint("LINESTRING (0 0, 0 1, 3 1)", 0.75, "POINT (2 1)"); + assertLineInterpolatePoint("LINESTRING (1 3, 5 4)", 0.0, "POINT (1 3)"); + assertLineInterpolatePoint("LINESTRING (1 3, 5 4)", 0.25, "POINT (2 3.25)"); + assertLineInterpolatePoint("LINESTRING (1 3, 5 4)", 1.0, "POINT (5 4)"); + + assertInvalidFunction("line_interpolate_point(ST_GeometryFromText('POLYGON ((1 1, 1 4, 4 4, 4 1, 1 1))'), 0.5)", "line_interpolate_point only applies to LINE_STRING. Input type is: POLYGON"); + assertInvalidFunction("line_interpolate_point(ST_GeometryFromText('MULTILINESTRING ((0 0, 0 1), (2 2, 4 2))'), 0.0)", "line_interpolate_point only applies to LINE_STRING. Input type is: MULTI_LINE_STRING"); + assertInvalidFunction("line_interpolate_point(ST_GeometryFromText('LINESTRING (0 0, 0 1, 2 1)'), -1)", "line_interpolate_point: Fraction must be between 0 and 1, but is -1.0"); + assertInvalidFunction("line_interpolate_point(ST_GeometryFromText('LINESTRING (0 0, 0 1, 2 1)'), 1.5)", "line_interpolate_point: Fraction must be between 0 and 1, but is 1.5"); + } + + private void assertLineInterpolatePoint(String sourceWkt, double distance, String expectedWkt) + { + assertFunction(format("ST_AsText(line_interpolate_point(ST_GeometryFromText('%s'), %s))", sourceWkt, distance), VARCHAR, expectedWkt); + } + @Test public void testSTMax() {