diff --git a/presto-docs/src/main/sphinx/functions/geospatial.rst b/presto-docs/src/main/sphinx/functions/geospatial.rst index a6faaaad43a26..ff50b26741514 100644 --- a/presto-docs/src/main/sphinx/functions/geospatial.rst +++ b/presto-docs/src/main/sphinx/functions/geospatial.rst @@ -497,6 +497,28 @@ represent a valid tile will raise an exception. Creates a Bing tile object from a quadkey. +.. function:: bing_tile_parent(tile) -> BingTile + + Returns the parent of the Bing tile at one lower zoom level. + Throws an exception if tile is at zoom level 0. + +.. function:: bing_tile_parent(tile, newZoom) -> BingTile + + Returns the parent of the Bing tile at the specified lower zoom level. + Throws an exception if newZoom is less than 0, or newZoom is greater than + the tile's zoom. + +.. function:: bing_tile_children(tile) -> array(BingTile) + + Returns the children of the Bing tile at one higher zoom level. + Throws an exception if tile is at max zoom level. + +.. function:: bing_tile_children(tile, newZoom) -> array(BingTile) + + Returns the children of the Bing tile at the specified higher zoom level. + Throws an exception if newZoom is greater than the max zoom level, or + newZoom is less than the tile's zoom. + .. function:: bing_tile_at(latitude, longitude, zoom_level) -> BingTile Returns a Bing tile at a given zoom level containing a point at a given latitude diff --git a/presto-geospatial/pom.xml b/presto-geospatial/pom.xml index 6ce8d155fae52..aecbf45013b1a 100644 --- a/presto-geospatial/pom.xml +++ b/presto-geospatial/pom.xml @@ -160,6 +160,11 @@ test + + org.assertj + assertj-core + + com.facebook.presto presto-tests 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 ee7bce85f7523..371e572a31e34 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 @@ -17,7 +17,9 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +import java.util.List; import java.util.Objects; import static com.facebook.presto.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT; @@ -155,11 +157,55 @@ public String toQuadKey() return String.valueOf(quadKey); } + public List findChildren() + { + return findChildren(zoomLevel + 1); + } + + public List findChildren(int newZoom) + { + if (newZoom == zoomLevel) { + return ImmutableList.of(this); + } + + checkArgument(newZoom <= MAX_ZOOM_LEVEL, "newZoom must be less than or equal to %s: %s", MAX_ZOOM_LEVEL, newZoom); + checkArgument(newZoom >= zoomLevel, "newZoom must be greater than or equal to current zoom %s: %s", zoomLevel, newZoom); + + int zoomDelta = newZoom - zoomLevel; + int xNew = x << zoomDelta; + int yNew = y << zoomDelta; + ImmutableList.Builder builder = ImmutableList.builderWithExpectedSize(1 << (2 * zoomDelta)); + for (int yDelta = 0; yDelta < 1 << zoomDelta; ++yDelta) { + for (int xDelta = 0; xDelta < 1 << zoomDelta; ++xDelta) { + builder.add(BingTile.fromCoordinates(xNew + xDelta, yNew + yDelta, newZoom)); + } + } + return builder.build(); + } + + public BingTile findParent() + { + return findParent(zoomLevel - 1); + } + + public BingTile findParent(int newZoom) + { + if (newZoom == zoomLevel) { + return this; + } + + checkArgument(newZoom >= 0, "newZoom must be greater than or equal to 0: %s", newZoom); + checkArgument(newZoom <= zoomLevel, "newZoom must be less than or equal to current zoom %s: %s", zoomLevel, newZoom); + + int zoomDelta = zoomLevel - newZoom; + return BingTile.fromCoordinates(x >> zoomDelta, y >> zoomDelta, newZoom); + } + /** * 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() 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 6d1fd70b8c4fb..84c4717952beb 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 @@ -29,6 +29,8 @@ import com.google.common.collect.ImmutableList; import io.airlift.slice.Slice; +import java.util.List; + import static com.facebook.presto.common.function.OperatorType.CAST; import static com.facebook.presto.common.type.BigintType.BIGINT; import static com.facebook.presto.common.type.IntegerType.INTEGER; @@ -64,6 +66,7 @@ import static com.facebook.presto.plugin.geospatial.BingTileUtils.tileXYToLatitudeLongitude; 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.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Verify.verify; import static io.airlift.slice.Slices.utf8Slice; @@ -375,6 +378,68 @@ public static Slice bingTilePolygon(@SqlType(BingTileType.NAME) long input) return serialize(tileToEnvelope(tile)); } + @Description("Return the parent for a Bing tile") + @ScalarFunction("bing_tile_parent") + @SqlType(BingTileType.NAME) + public static long bingTileParent(@SqlType(BingTileType.NAME) long input) + { + BingTile tile = BingTile.decode(input); + try { + return tile.findParent().encode(); + } + catch (IllegalArgumentException e) { + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, e.getMessage(), e); + } + } + + @Description("Return the parent for the given zoom level for a Bing tile") + @ScalarFunction("bing_tile_parent") + @SqlType(BingTileType.NAME) + public static long bingTileParent(@SqlType(BingTileType.NAME) long input, @SqlType(StandardTypes.INTEGER) long newZoom) + { + BingTile tile = BingTile.decode(input); + try { + return tile.findParent(toIntExact(newZoom)).encode(); + } + catch (IllegalArgumentException e) { + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, e.getMessage(), e); + } + } + + @Description("Return the children for a Bing tile") + @ScalarFunction("bing_tile_children") + @SqlType("array(" + BingTileType.NAME + ")") + public static Block bingTileChildren(@SqlType(BingTileType.NAME) long input) + { + BingTile tile = BingTile.decode(input); + try { + List children = tile.findChildren(); + BlockBuilder blockBuilder = BIGINT.createBlockBuilder(null, children.size()); + children.stream().forEach(child -> BIGINT.writeLong(blockBuilder, child.encode())); + return blockBuilder.build(); + } + catch (IllegalArgumentException e) { + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, e.getMessage(), e); + } + } + + @Description("Return the children for the given zoom level for a Bing tile") + @ScalarFunction("bing_tile_children") + @SqlType("array(" + BingTileType.NAME + ")") + public static Block bingTileChildren(@SqlType(BingTileType.NAME) long input, @SqlType(StandardTypes.INTEGER) long newZoom) + { + BingTile tile = BingTile.decode(input); + try { + List children = tile.findChildren(toIntExact(newZoom)); + BlockBuilder blockBuilder = BIGINT.createBlockBuilder(null, children.size()); + children.stream().forEach(child -> BIGINT.writeLong(blockBuilder, child.encode())); + return blockBuilder.build(); + } + catch (IllegalArgumentException e) { + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, e.getMessage(), e); + } + } + @Description("Given a geometry and a zoom level, returns the minimum set of Bing tiles that fully covers that geometry") @ScalarFunction("geometry_to_bing_tiles") @SqlType("array(" + BingTileType.NAME + ")") diff --git a/presto-geospatial/src/test/java/com/facebook/presto/plugin/geospatial/TestBingTile.java b/presto-geospatial/src/test/java/com/facebook/presto/plugin/geospatial/TestBingTile.java index 8ffc10b57a99e..f101a614307d4 100644 --- a/presto-geospatial/src/test/java/com/facebook/presto/plugin/geospatial/TestBingTile.java +++ b/presto-geospatial/src/test/java/com/facebook/presto/plugin/geospatial/TestBingTile.java @@ -15,8 +15,11 @@ import com.facebook.presto.operator.scalar.AbstractTestFunctions; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableList; import org.testng.annotations.Test; +import java.util.List; + 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.BingTileUtils.MAX_LATITUDE; @@ -25,6 +28,9 @@ import static com.facebook.presto.plugin.geospatial.BingTileUtils.MIN_LONGITUDE; import static com.facebook.presto.plugin.geospatial.BingTileUtils.tileXToLongitude; import static com.facebook.presto.plugin.geospatial.BingTileUtils.tileYToLatitude; +import static com.google.common.collect.ImmutableList.toImmutableList; +import static java.lang.String.format; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.testng.Assert.assertEquals; public class TestBingTile @@ -90,4 +96,55 @@ public void testTileYToLatitude() assertEquals(tileYToLatitude(1 << zoom, zoom), MIN_LATITUDE, delta); } } + + @Test + public void testFindChildren() + { + assertEquals( + toSortedQuadkeys(BingTile.fromQuadKey("").findChildren()), + ImmutableList.of("0", "1", "2", "3")); + + assertEquals( + toSortedQuadkeys(BingTile.fromQuadKey("0123").findChildren()), + ImmutableList.of("01230", "01231", "01232", "01233")); + + assertEquals( + toSortedQuadkeys(BingTile.fromQuadKey("").findChildren(2)), + ImmutableList.of("00", "01", "02", "03", "10", "11", "12", "13", "20", "21", "22", "23", "30", "31", "32", "33")); + + assertThatThrownBy(() -> BingTile.fromCoordinates(0, 0, MAX_ZOOM_LEVEL).findChildren()) + .hasMessage(format("newZoom must be less than or equal to %s: %s", MAX_ZOOM_LEVEL, MAX_ZOOM_LEVEL + 1)); + + assertThatThrownBy(() -> BingTile.fromCoordinates(0, 0, 13).findChildren(MAX_ZOOM_LEVEL + 1)) + .hasMessage(format("newZoom must be less than or equal to %s: %s", MAX_ZOOM_LEVEL, MAX_ZOOM_LEVEL + 1)); + + assertThatThrownBy(() -> BingTile.fromCoordinates(0, 0, 13).findChildren(12)) + .hasMessage(format("newZoom must be greater than or equal to current zoom %s: %s", 13, 12)); + } + + private List toSortedQuadkeys(List tiles) + { + return tiles.stream() + .map(BingTile::toQuadKey) + .sorted() + .collect(toImmutableList()); + } + + @Test + public void testFindParent() + { + assertEquals(BingTile.fromQuadKey("0123").findParent().toQuadKey(), "012"); + assertEquals(BingTile.fromQuadKey("1").findParent().toQuadKey(), ""); + assertEquals(BingTile.fromQuadKey("0123").findParent(1).toQuadKey(), "0"); + assertEquals(BingTile.fromQuadKey("0123").findParent(4).toQuadKey(), "0123"); + + assertThatThrownBy(() -> BingTile.fromQuadKey("0123").findParent(5)) + .hasMessage(format("newZoom must be less than or equal to current zoom %s: %s", 4, 5)); + + assertThatThrownBy(() -> BingTile.fromQuadKey("").findParent()) + .hasMessage(format("newZoom must be greater than or equal to 0: %s", -1)); + + assertThatThrownBy(() -> BingTile.fromQuadKey("12").findParent(-1)) + .hasMessage(format("newZoom must be greater than or equal to 0: %s", -1)); + } } 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 06f4dcb4a8fe5..86776e5b89b05 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 @@ -29,6 +29,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.OptionalInt; import static com.facebook.presto.block.BlockAssertions.createTypedLongsBlock; import static com.facebook.presto.common.type.BigintType.BIGINT; @@ -39,6 +40,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.BingTileType.BING_TILE; import static com.facebook.presto.plugin.geospatial.BingTileUtils.MAX_LATITUDE; @@ -130,6 +132,57 @@ public void testBingTile() assertInvalidFunction("bing_tile(2, 7, 37)", "Zoom level must be <= 23"); } + @Test + public void testBingTileChildren() + { + assertBingTileChildren("0", OptionalInt.empty(), ImmutableList.of("00", "01", "02", "03")); + assertBingTileChildren("0", OptionalInt.of(3), ImmutableList.of( + "000", "001", "002", "003", + "010", "011", "012", "013", + "020", "021", "022", "023", + "030", "031", "032", "033")); + assertInvalidFunction("bing_tile_children(bing_tile('0'), 0)", "newZoom must be greater than or equal to current zoom 1: 0"); + assertInvalidFunction(format("bing_tile_children(bing_tile('0'), %s)", MAX_ZOOM_LEVEL + 1), format("newZoom must be less than or equal to %s: %s", MAX_ZOOM_LEVEL, MAX_ZOOM_LEVEL + 1)); + } + + private void assertBingTileChildren(String quadkey, OptionalInt newZoom, List childQuadkeys) + { + String children; + if (newZoom.isPresent()) { + children = format("bing_tile_children(bing_tile('%s'), %s)", quadkey, newZoom.getAsInt()); + } + else { + children = format("bing_tile_children(bing_tile('%s'))", quadkey); + } + + assertFunction( + format("array_sort(transform(%s, x -> bing_tile_quadkey(x)))", children), + new ArrayType(VARCHAR), + ImmutableList.sortedCopyOf(childQuadkeys)); + } + + @Test + public void testBingTileParent() + { + assertBingTileParent("03", OptionalInt.empty(), "0"); + assertBingTileParent("0123", OptionalInt.of(2), "01"); + assertInvalidFunction("bing_tile_parent(bing_tile('0'), 2)", "newZoom must be less than or equal to current zoom 1: 2"); + assertInvalidFunction(format("bing_tile_parent(bing_tile('0'), %s)", -1), "newZoom must be greater than or equal to 0: -1"); + } + + private void assertBingTileParent(String quadkey, OptionalInt newZoom, String parentQuadkey) + { + String parent; + if (newZoom.isPresent()) { + parent = format("bing_tile_parent(bing_tile('%s'), %s)", quadkey, newZoom.getAsInt()); + } + else { + parent = format("bing_tile_parent(bing_tile('%s'))", quadkey); + } + + assertFunction(format("bing_tile_quadkey(%s)", parent), VARCHAR, parentQuadkey); + } + @Test public void testPointToBingTile() {