Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion presto-docs/src/main/sphinx/functions/geospatial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,15 @@ Bing Tiles

These functions convert between geometries and
`Bing tiles <https://msdn.microsoft.com/en-us/library/bb259689.aspx>`_. 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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

follow up: Currently BingTile.decode must create an object of BingTile. The main reason for calling BingTile.decode is to validate the tile stored in a long. For efficiency I would recommend to have a dedicated method, e.g.: BingTile.validate or smthng.

}
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down