Skip to content

Commit

Permalink
Add functions to encode/decode Google polylines
Browse files Browse the repository at this point in the history
* Support the popular and efficient Google polyline format by adding
GeoFunctions
 - "google_decode_polyline" that decodes a Google polyline string into
an array of ST_Points, specifying an optional precision
 - "google_encode_polyline" that encodes an array of ST_Points into
a Google polyline string, specifying an optional precision

Implements prestodb#23998
  • Loading branch information
jquirke authored and tdcmeehan committed Nov 21, 2024
1 parent a207c28 commit 7a11969
Show file tree
Hide file tree
Showing 2 changed files with 177 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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() {}

Expand Down Expand Up @@ -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<org.locationtech.jts.geom.Point> 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)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}

0 comments on commit 7a11969

Please sign in to comment.