diff --git a/presto-docs/src/main/sphinx/functions/geospatial.rst b/presto-docs/src/main/sphinx/functions/geospatial.rst index ef23b047b0630..64c31770480e2 100644 --- a/presto-docs/src/main/sphinx/functions/geospatial.rst +++ b/presto-docs/src/main/sphinx/functions/geospatial.rst @@ -456,7 +456,15 @@ Bing Tiles These functions convert between geometries and `Bing tiles `_. For -Bing tiles, ``x`` and ``y`` refer to ``tile_x`` and ``tile_y``. +Bing tiles, ``x`` and ``y`` refer to ``tile_x`` and ``tile_y``. Bing Tiles +can be cast to and from BigInts, using an internal representation that encodes +the ``zoom``, ``x``, and ``y`` efficiently:: + + cast(cast(tile AS BIGINT) AS BINGTILE) + +While every tile can be cast to a bigint, casting from a bigint that does not +represent a valid tile will raise an exception. + .. function:: bing_tile(x, y, zoom_level) -> BingTile diff --git a/presto-geospatial/src/main/java/com/facebook/presto/plugin/geospatial/BingTile.java b/presto-geospatial/src/main/java/com/facebook/presto/plugin/geospatial/BingTile.java index 5f0ad94430063..ee7bce85f7523 100644 --- a/presto-geospatial/src/main/java/com/facebook/presto/plugin/geospatial/BingTile.java +++ b/presto-geospatial/src/main/java/com/facebook/presto/plugin/geospatial/BingTile.java @@ -16,16 +16,24 @@ import com.facebook.presto.spi.PrestoException; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.annotations.VisibleForTesting; import java.util.Objects; import static com.facebook.presto.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT; import static com.google.common.base.MoreObjects.toStringHelper; import static com.google.common.base.Preconditions.checkArgument; +import static java.lang.String.format; public final class BingTile { public static final int MAX_ZOOM_LEVEL = 23; + @VisibleForTesting + static final int VERSION_OFFSET = 63 - 5; + private static final int VERSION = 0; + private static final int BITS_23 = (1 << 24) - 1; + private static final int BITS_5 = (1 << 6) - 1; + private static final int ZOOM_OFFSET = 31 - 5; private final int x; private final int y; @@ -148,19 +156,38 @@ public String toQuadKey() } /** - * Encodes Bing tile as a 64-bit long: 23 bits for X, followed by 23 bits for Y, - * followed by 5 bits for zoomLevel + * Encodes Bing tile as a 64-bit long: + * Version (5 bits), 0 (4 bits), x (23 bits), Zoom (5 bits), 0 (4 bits), y (23 bits) + * (high bits left, low bits right). + * + * This arrangement maximizes low-bit entropy for the Java long hash function. */ public long encode() { - return (((long) x) << 28) + (y << 5) + zoomLevel; + // Java's long hash function just XORs itself right shifted 32. + // This is used for bucketing, so if you have 2^k buckets, this only + // keeps the k lowest bits. This puts the highest entropy bits + // (finest resolution x and y bits) in places that contribute to the + // low bits of the hash. + return (((long) VERSION << VERSION_OFFSET) | y | ((long) x << 32) | ((long) zoomLevel << ZOOM_OFFSET)); } public static BingTile decode(long tile) { - int tileX = (int) (tile >> 28); - int tileY = (int) ((tile % (1 << 28)) >> 5); - int zoomLevel = (int) (tile % (1 << 5)); + int version = (int) (tile >>> VERSION_OFFSET) & BITS_5; + if (version == 0) { + return decodeV0(tile); + } + else { + throw new IllegalArgumentException(format("Unknown Bing Tile encoding version: %s", version)); + } + } + + private static BingTile decodeV0(long tile) + { + int tileX = (int) (tile >>> 32) & BITS_23; + int tileY = (int) tile & BITS_23; + int zoomLevel = (int) (tile >>> ZOOM_OFFSET) & BITS_5; return new BingTile(tileX, tileY, zoomLevel); } diff --git a/presto-geospatial/src/main/java/com/facebook/presto/plugin/geospatial/BingTileFunctions.java b/presto-geospatial/src/main/java/com/facebook/presto/plugin/geospatial/BingTileFunctions.java index ddbe94e9e7585..0dcdd1dd5d181 100644 --- a/presto-geospatial/src/main/java/com/facebook/presto/plugin/geospatial/BingTileFunctions.java +++ b/presto-geospatial/src/main/java/com/facebook/presto/plugin/geospatial/BingTileFunctions.java @@ -22,6 +22,7 @@ import com.facebook.presto.spi.block.BlockBuilder; import com.facebook.presto.spi.function.Description; import com.facebook.presto.spi.function.ScalarFunction; +import com.facebook.presto.spi.function.ScalarOperator; import com.facebook.presto.spi.function.SqlType; import com.facebook.presto.spi.type.RowType; import com.facebook.presto.spi.type.StandardTypes; @@ -38,7 +39,9 @@ import static com.facebook.presto.geospatial.serde.EsriGeometrySerde.serialize; import static com.facebook.presto.plugin.geospatial.BingTile.MAX_ZOOM_LEVEL; import static com.facebook.presto.plugin.geospatial.GeometryType.GEOMETRY_TYPE_NAME; +import static com.facebook.presto.spi.StandardErrorCode.INVALID_CAST_ARGUMENT; import static com.facebook.presto.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT; +import static com.facebook.presto.spi.function.OperatorType.CAST; import static com.facebook.presto.spi.type.BigintType.BIGINT; import static com.facebook.presto.spi.type.IntegerType.INTEGER; import static com.google.common.base.Preconditions.checkArgument; @@ -85,6 +88,29 @@ public class BingTileFunctions private BingTileFunctions() {} + @Description("Encodes a Bing tile into a bigint") + @ScalarOperator(CAST) + @SqlType(StandardTypes.BIGINT) + public static long castToBigint(@SqlType(BingTileType.NAME) long tile) + { + return tile; + } + + @Description("Decodes a Bing tile from a bigint") + @ScalarOperator(CAST) + @SqlType(BingTileType.NAME) + public static long castFromBigint(@SqlType(StandardTypes.BIGINT) long tile) + { + try { + BingTile.decode(tile); + } + catch (IllegalArgumentException e) { + throw new PrestoException(INVALID_CAST_ARGUMENT, + format("Invalid bigint tile encoding: %s", tile)); + } + return tile; + } + @Description("Creates a Bing tile from XY coordinates and zoom level") @ScalarFunction("bing_tile") @SqlType(BingTileType.NAME) diff --git a/presto-geospatial/src/test/java/com/facebook/presto/plugin/geospatial/TestBingTileFunctions.java b/presto-geospatial/src/test/java/com/facebook/presto/plugin/geospatial/TestBingTileFunctions.java index e572b54f5942f..3852e80a766da 100644 --- a/presto-geospatial/src/test/java/com/facebook/presto/plugin/geospatial/TestBingTileFunctions.java +++ b/presto-geospatial/src/test/java/com/facebook/presto/plugin/geospatial/TestBingTileFunctions.java @@ -35,6 +35,7 @@ import static com.facebook.presto.metadata.FunctionExtractor.extractFunctions; import static com.facebook.presto.operator.aggregation.AggregationTestUtils.assertAggregation; import static com.facebook.presto.operator.scalar.ApplyFunction.APPLY_FUNCTION; +import static com.facebook.presto.plugin.geospatial.BingTile.MAX_ZOOM_LEVEL; import static com.facebook.presto.plugin.geospatial.BingTile.fromCoordinates; import static com.facebook.presto.plugin.geospatial.BingTileFunctions.MAX_LATITUDE; import static com.facebook.presto.plugin.geospatial.BingTileFunctions.MIN_LONGITUDE; @@ -80,6 +81,55 @@ public void testSerialization() assertEquals(tile, objectMapper.readerFor(BingTile.class).readValue(json)); } + @Test + public void testBingTileEncoding() + { + for (int zoom = 0; zoom <= MAX_ZOOM_LEVEL; zoom++) { + int maxValue = (1 << zoom) - 1; + testEncodingRoundTrip(0, 0, zoom); + testEncodingRoundTrip(0, maxValue, zoom); + testEncodingRoundTrip(maxValue, 0, zoom); + testEncodingRoundTrip(maxValue, maxValue, zoom); + } + } + + private void testEncodingRoundTrip(int x, int y, int zoom) + { + BingTile expected = BingTile.fromCoordinates(x, y, zoom); + BingTile actual = BingTile.decode(expected.encode()); + assertEquals(actual, expected); + } + + @Test + public void testBingTileCast() + { + assertBingTileCast(0, 0, 0); + assertBingTileCast(0, 0, 1); + assertBingTileCast(0, 0, 10); + assertBingTileCast(125, 900, 10); + assertBingTileCast(0, 0, 23); + assertBingTileCast((1 << 23) - 1, (1 << 23) - 1, 23); + + // X/Y too big + assertBingTileCastInvalid(256L | (256L << 32) | (4L << 27)); + + // Wrong version + assertBingTileCastInvalid(1L << BingTile.VERSION_OFFSET); + } + + private void assertBingTileCast(int x, int y, int zoom) + { + BingTile tile = BingTile.fromCoordinates(x, y, zoom); + assertFunction(format("cast(cast(%s as bigint) as bingtile)", tile.encode()), BING_TILE, tile); + assertFunction(format("cast(bing_tile('%s') as bigint)", tile.toQuadKey()), BIGINT, tile.encode()); + } + + private void assertBingTileCastInvalid(long encoding) + { + assertInvalidCast(format("cast(cast(%s as bigint) as bingtile)", encoding), + format("Invalid bigint tile encoding: %s", encoding)); + } + @Test public void testArrayOfBingTiles() throws Exception