diff --git a/presto-main/src/main/java/com/facebook/presto/geospatial/GeoFunctions.java b/presto-main/src/main/java/com/facebook/presto/geospatial/GeoFunctions.java index 0708941139982..e8e600b865aae 100644 --- a/presto-main/src/main/java/com/facebook/presto/geospatial/GeoFunctions.java +++ b/presto-main/src/main/java/com/facebook/presto/geospatial/GeoFunctions.java @@ -28,6 +28,7 @@ import com.facebook.presto.common.block.BlockBuilder; import com.facebook.presto.common.type.IntegerType; import com.facebook.presto.common.type.KdbTreeType; +import com.facebook.presto.common.type.StandardTypes; import com.facebook.presto.geospatial.serde.EsriGeometrySerde; import com.facebook.presto.geospatial.serde.GeometrySerializationType; import com.facebook.presto.spi.PrestoException; @@ -55,6 +56,7 @@ import org.locationtech.jts.linearref.LengthIndexedLine; import org.locationtech.jts.operation.distance.DistanceOp; +import java.text.StringCharacterIterator; import java.util.ArrayList; import java.util.EnumSet; import java.util.Iterator; @@ -133,6 +135,8 @@ public final class GeoFunctions .build(); private static final int NUMBER_OF_DIMENSIONS = 3; private static final Block EMPTY_ARRAY_OF_INTS = IntegerType.INTEGER.createFixedSizeBlockBuilder(0).build(); + private static final long DEFAULT_POLYLINE_PRECISION_EXPONENT = 5; + private static final long MINIMUM_POLYLINE_PRECISION_EXPONENT = 1; private GeoFunctions() {} @@ -1341,4 +1345,112 @@ public Slice next() } }; } + + @Description("Decodes a Google Polyline string into an array of Points") + @ScalarFunction("google_polyline_decode") + @SqlType("array(" + GEOMETRY_TYPE_NAME + ")") + public static Block gMapsPolylineDecode(@SqlType(StandardTypes.VARCHAR) Slice polyline) throws PrestoException + { + return googlePolylineDecodePrecision(polyline, DEFAULT_POLYLINE_PRECISION_EXPONENT); + } + + @Description("Decodes a Google Polyline string into an array of Points, specifying encoded precision") + @ScalarFunction("google_polyline_decode") + @SqlType("array(" + GEOMETRY_TYPE_NAME + ")") + public static Block googlePolylineDecodePrecision(@SqlType(StandardTypes.VARCHAR) Slice polyline, @SqlType(INTEGER) final long precisionExponent) throws PrestoException + { + if (precisionExponent < MINIMUM_POLYLINE_PRECISION_EXPONENT) { + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, "Polyline precision must be greater or equal to " + MINIMUM_POLYLINE_PRECISION_EXPONENT); + } + + double precision = Math.pow(10.0, (double) precisionExponent); + List points = new ArrayList<>(); + int lat = 0; + int lng = 0; + + StringCharacterIterator encodedStringIterator = new StringCharacterIterator(polyline.toStringUtf8()); + while (encodedStringIterator.current() != StringCharacterIterator.DONE) { + lat += decodeNextDelta(encodedStringIterator); + lng += decodeNextDelta(encodedStringIterator); + points.add(createJtsPoint((double) lat / precision, (double) lng / precision)); + } + + final BlockBuilder blockBuilder = GEOMETRY.createBlockBuilder(null, points.size()); + points.forEach(p -> GEOMETRY.writeSlice(blockBuilder, serialize(p))); + return blockBuilder.build(); + } + + // implements decoding of the encoding method specified by + // https://developers.google.com/maps/documentation/utilities/polylinealgorithm + private static int decodeNextDelta(StringCharacterIterator encodedStringIterator) + { + int character; + int shift = 0; + int result = 0; + + do { + character = encodedStringIterator.current(); + if (character == StringCharacterIterator.DONE) { + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, "Input is not a valid Google polyline string"); + } + character -= 0x3f; + result |= (character & 0x1f) << shift; + shift += 5; + encodedStringIterator.next(); + } while (character >= 0x20); + return ((result & 1) != 0 ? ~(result >> 1) : (result >> 1)); + } + + @Description("Encodes an array of ST_Points into a Google Polyline") + @ScalarFunction("google_polyline_encode") + @SqlType(VARCHAR) + public static Slice googlePolylineEncode(@SqlType("array(" + GEOMETRY_TYPE_NAME + ")") Block points) throws PrestoException + { + return googlePolylineEncodePrecision(points, DEFAULT_POLYLINE_PRECISION_EXPONENT); + } + + @Description("Encodes an array of ST_Points into a Google Polyline, specifying encoded precision") + @ScalarFunction("google_polyline_encode") + @SqlType(VARCHAR) + public static Slice googlePolylineEncodePrecision(@SqlType("array(" + GEOMETRY_TYPE_NAME + ")") Block points, @SqlType(INTEGER) final long precisionExponent) throws PrestoException + { + if (precisionExponent < MINIMUM_POLYLINE_PRECISION_EXPONENT) { + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, "Polyline precision must be greater or equal to " + MINIMUM_POLYLINE_PRECISION_EXPONENT); + } + final double precision = Math.pow(10.0, (double) precisionExponent); + final CoordinateSequence coordinateSequence = readPointCoordinates(points, "gMapsPolylineEncodePrecision", false); + StringBuilder stringBuilder = new StringBuilder(); + long prevX = 0; + long prevY = 0; + for (int i = 0; i < coordinateSequence.size(); i++) { + long x = Math.round(coordinateSequence.getX(i) * precision); + long y = Math.round(coordinateSequence.getY(i) * precision); + if (i == 0) { + encodeNextDelta(x, stringBuilder); + encodeNextDelta(y, stringBuilder); + } + else { + encodeNextDelta(x - prevX, stringBuilder); + encodeNextDelta(y - prevY, stringBuilder); + } + prevX = x; + prevY = y; + } + return utf8Slice(stringBuilder.toString()); + } + + // implements encoding of the encoding method specified by + // https://developers.google.com/maps/documentation/utilities/polylinealgorithm + private static void encodeNextDelta(long delta, StringBuilder stringBuilder) + { + delta <<= 1; + if (delta < 0) { + delta = ~delta; + } + while (delta >= 0x20) { + stringBuilder.append(Character.toChars((int) (((delta & 0x1f) | 0x20)) + 0x3f)); + delta >>= 5; + } + stringBuilder.append(Character.toChars((int) (delta + 0x3f))); + } } diff --git a/presto-main/src/test/java/com/facebook/presto/geospatial/TestGeoFunctions.java b/presto-main/src/test/java/com/facebook/presto/geospatial/TestGeoFunctions.java index 2f51a61a1a404..b651f9188161b 100644 --- a/presto-main/src/test/java/com/facebook/presto/geospatial/TestGeoFunctions.java +++ b/presto-main/src/test/java/com/facebook/presto/geospatial/TestGeoFunctions.java @@ -1371,4 +1371,69 @@ private void assertInvalidGeometryJson(String json) { assertInvalidFunction("geometry_from_geojson('" + json + "')", "Invalid GeoJSON:.*"); } + + @Test + public void testGooglePolylineDecode() + { + // Google's standard example + assertFunction("google_polyline_decode('_p~iF~ps|U_ulLnnqC_mqNvxq`@')", + new ArrayType(GEOMETRY), + ImmutableList.of("POINT (38.5 -120.2)", "POINT (40.7 -120.95)", "POINT (43.252 -126.453)")); + + /* more complex scenario: + (1) precision in the input floats higher than supported + (2) duplicate points in a row + (3) line crosses back over itself (not very significant, but hey) + (4) line terminates at the origin (non consecutive duplicate points) + 37.78327388736858, -122.43876656873093 + 37.7588492882026, -122.43533334119186 + 37.76373485345523, -122.41027078015671 + 37.76780591132951, -122.42537698132858 + 37.76780591132951, -122.42537698132858 + 37.76834870211408, -122.45421609265671 + 37.78327388736858, -122.43876656873093 + */ + assertFunction("google_polyline_decode('mpreFhyhjVrwCoTo]s{CoXl}A??kBfsDg|Aq_B')", + new ArrayType(GEOMETRY), + ImmutableList.of( + "POINT (37.78327 -122.43877)", + "POINT (37.75885 -122.43533)", + "POINT (37.76373 -122.41027)", + "POINT (37.76781 -122.42538)", + "POINT (37.76781 -122.42538)", + "POINT (37.76835 -122.45422)", + "POINT (37.78327 -122.43877)")); + + assertInvalidFunction("google_polyline_decode('A')", INVALID_FUNCTION_ARGUMENT, "Input is not a valid Google polyline string"); + + assertFunction("google_polyline_decode('_izlhA~rlgdF_{geC~ywl@_kwzCn`{nI', 6)", + new ArrayType(GEOMETRY), + ImmutableList.of("POINT (38.5 -120.2)", "POINT (40.7 -120.95)", "POINT (43.252 -126.453)")); + assertInvalidFunction("google_polyline_decode('_p~iF~ps|U_ulLnnqC_mqNvxq`@', 0) ", INVALID_FUNCTION_ARGUMENT, "Polyline precision must be greater or equal to 1"); + } + + @Test + public void testGooglePolylineEncode() + { + // Google's standard example + assertFunction("google_polyline_encode(ARRAY[ST_Point(38.5, -120.2), ST_Point(40.7, -120.95), ST_Point(43.252, -126.453)])", + VARCHAR, "_p~iF~ps|U_ulLnnqC_mqNvxq`@"); + assertInvalidFunction("google_polyline_encode(ARRAY[ST_Point(37.78327, -122.43877)], 0)", INVALID_FUNCTION_ARGUMENT, "Polyline precision must be greater or equal to 1"); + + // the more complex example in the decode tests + assertFunction( + new StringBuilder("google_polyline_encode(") + .append("ARRAY[") + .append("ST_Point(37.78327, -122.43877),") + .append("ST_Point(37.75885, -122.43533),") + .append("ST_Point(37.76373, -122.41027),") + .append("ST_Point(37.76781, -122.42538),") + .append("ST_Point(37.76781, -122.42538),") + .append("ST_Point(37.76835, -122.45422),") + .append("ST_Point(37.78327, -122.43877)") + .append("])") + .toString(), + VARCHAR, + "mpreFhyhjVrwCoTo]s{CoXl}A??kBfsDg|Aq_B"); + } }