This is an automated email from the ASF dual-hosted git repository.
jiayu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sedona.git
The following commit(s) were added to refs/heads/master by this push:
new a648f0c7b5 [GH-1327] Add Bing Tile functions (#2668)
a648f0c7b5 is described below
commit a648f0c7b53e834c366a3c508d66ab6e8976212e
Author: Jia Yu <[email protected]>
AuthorDate: Sat Feb 21 12:59:07 2026 -0700
[GH-1327] Add Bing Tile functions (#2668)
---
.../java/org/apache/sedona/common/Functions.java | 125 +++++
.../org/apache/sedona/common/utils/BingTile.java | 586 +++++++++++++++++++++
.../org/apache/sedona/common/FunctionsTest.java | 311 +++++++++++
docs/api/flink/Function.md | 183 +++++++
docs/api/snowflake/vector-data/Function.md | 162 ++++++
docs/api/sql/Function.md | 183 +++++++
.../main/java/org/apache/sedona/flink/Catalog.java | 9 +
.../apache/sedona/flink/expressions/Functions.java | 89 ++++
.../java/org/apache/sedona/flink/FunctionTest.java | 178 +++++++
python/sedona/spark/sql/st_functions.py | 137 +++++
.../sedona/snowflake/snowsql/TestFunctions.java | 60 +++
.../sedona/snowflake/snowsql/TestFunctionsV2.java | 60 +++
.../org/apache/sedona/snowflake/snowsql/UDFs.java | 48 ++
.../apache/sedona/snowflake/snowsql/UDFsV2.java | 61 +++
.../scala/org/apache/sedona/sql/UDF/Catalog.scala | 9 +
.../sql/sedona_sql/expressions/Functions.scala | 77 +++
.../sql/sedona_sql/expressions/st_functions.scala | 40 ++
.../apache/sedona/sql/dataFrameAPITestScala.scala | 107 ++++
.../sedona/sql/functions/STBingTileFunctions.scala | 254 +++++++++
19 files changed, 2679 insertions(+)
diff --git a/common/src/main/java/org/apache/sedona/common/Functions.java
b/common/src/main/java/org/apache/sedona/common/Functions.java
index 22b9a2fbf4..76bea7e695 100644
--- a/common/src/main/java/org/apache/sedona/common/Functions.java
+++ b/common/src/main/java/org/apache/sedona/common/Functions.java
@@ -1815,6 +1815,131 @@ public class Functions {
return polygons.toArray(new Polygon[0]);
}
+ // =========================================================================
+ // Bing Tile functions
+ // =========================================================================
+
+ /**
+ * Creates a Bing tile quadkey from tile XY coordinates and zoom level.
+ *
+ * @param tileX the tile X coordinate
+ * @param tileY the tile Y coordinate
+ * @param zoomLevel the zoom level (1 to 23)
+ * @return the quadkey string
+ */
+ public static String bingTile(int tileX, int tileY, int zoomLevel) {
+ return BingTile.fromCoordinates(tileX, tileY, zoomLevel).toQuadKey();
+ }
+
+ /**
+ * Returns the Bing tile quadkey at a given zoom level containing the
specified point.
+ *
+ * @param longitude the longitude (-180 to 180)
+ * @param latitude the latitude (-85.05112878 to 85.05112878)
+ * @param zoomLevel the zoom level (1 to 23)
+ * @return the quadkey string
+ */
+ public static String bingTileAt(double longitude, double latitude, int
zoomLevel) {
+ return BingTile.fromLatLon(latitude, longitude, zoomLevel).toQuadKey();
+ }
+
+ /**
+ * Returns the 3x3 neighborhood of Bing tiles around the tile containing the
specified point.
+ *
+ * @param longitude the longitude
+ * @param latitude the latitude
+ * @param zoomLevel the zoom level (1 to 23)
+ * @return array of quadkey strings
+ */
+ public static String[] bingTilesAround(double longitude, double latitude,
int zoomLevel) {
+ List<BingTile> tiles = BingTile.tilesAround(latitude, longitude,
zoomLevel);
+ return tiles.stream().map(BingTile::toQuadKey).toArray(String[]::new);
+ }
+
+ /**
+ * Returns the zoom level of a Bing tile quadkey.
+ *
+ * @param quadKey the quadkey string
+ * @return the zoom level
+ */
+ public static int bingTileZoomLevel(String quadKey) {
+ // Validate the quadkey by parsing it
+ BingTile.fromQuadKey(quadKey);
+ return quadKey.length();
+ }
+
+ /**
+ * Returns the X coordinate of a Bing tile from its quadkey.
+ *
+ * @param quadKey the quadkey string
+ * @return the tile X coordinate
+ */
+ public static int bingTileX(String quadKey) {
+ return BingTile.fromQuadKey(quadKey).getX();
+ }
+
+ /**
+ * Returns the Y coordinate of a Bing tile from its quadkey.
+ *
+ * @param quadKey the quadkey string
+ * @return the tile Y coordinate
+ */
+ public static int bingTileY(String quadKey) {
+ return BingTile.fromQuadKey(quadKey).getY();
+ }
+
+ /**
+ * Returns the polygon representation of a Bing tile given its quadkey.
+ *
+ * @param quadKey the quadkey string
+ * @return the tile polygon
+ */
+ public static Geometry bingTilePolygon(String quadKey) {
+ return BingTile.fromQuadKey(quadKey).toPolygon();
+ }
+
+ /**
+ * Validates and normalizes a Bing tile quadkey string.
+ *
+ * <p>The input quadkey is parsed into a {@code BingTile} for validation,
and the corresponding
+ * (possibly normalized) quadkey string is returned. Invalid quadkeys will
cause {@code
+ * BingTile.fromQuadKey} to throw an exception.
+ *
+ * @param quadKey the quadkey string to validate and normalize
+ * @return the validated and normalized quadkey string
+ */
+ public static String bingTileQuadKey(String quadKey) {
+ // Validate and return. This serves as a validation/normalization function.
+ return BingTile.fromQuadKey(quadKey).toQuadKey();
+ }
+
+ /**
+ * Returns the minimum set of Bing tile quadkeys that fully cover a given
geometry at the
+ * specified zoom level.
+ *
+ * @param geometry the geometry to cover
+ * @param zoomLevel the zoom level (1 to 23)
+ * @return array of quadkey strings
+ */
+ public static String[] bingTileCellIDs(Geometry geometry, int zoomLevel) {
+ List<BingTile> tiles = BingTile.tilesCovering(geometry, zoomLevel);
+ return tiles.stream().map(BingTile::toQuadKey).toArray(String[]::new);
+ }
+
+ /**
+ * Converts an array of Bing tile quadkeys to their polygon geometries.
+ *
+ * @param quadKeys the array of quadkey strings
+ * @return array of polygon geometries
+ */
+ public static Geometry[] bingTileToGeom(String[] quadKeys) {
+ Geometry[] polygons = new Geometry[quadKeys.length];
+ for (int i = 0; i < quadKeys.length; i++) {
+ polygons[i] = BingTile.fromQuadKey(quadKeys[i]).toPolygon();
+ }
+ return polygons;
+ }
+
public static Geometry simplify(Geometry geom, double distanceTolerance) {
return DouglasPeuckerSimplifier.simplify(geom, distanceTolerance);
}
diff --git a/common/src/main/java/org/apache/sedona/common/utils/BingTile.java
b/common/src/main/java/org/apache/sedona/common/utils/BingTile.java
new file mode 100644
index 0000000000..d3ebf30c93
--- /dev/null
+++ b/common/src/main/java/org/apache/sedona/common/utils/BingTile.java
@@ -0,0 +1,586 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sedona.common.utils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import org.locationtech.jts.geom.Coordinate;
+import org.locationtech.jts.geom.Envelope;
+import org.locationtech.jts.geom.Geometry;
+import org.locationtech.jts.geom.GeometryFactory;
+import org.locationtech.jts.geom.Polygon;
+
+/**
+ * Represents a Bing Maps tile. Bing Maps uses a quadtree-based tiling system
where tiles are
+ * identified by (x, y, zoomLevel) coordinates or equivalently by a quadkey
string.
+ *
+ * <p>The implementation is based on the Bing Maps Tile System specification:
+ * https://docs.microsoft.com/en-us/bingmaps/articles/bing-maps-tile-system
+ *
+ * <p>Ported from https://github.com/wwbrannon/bing-tile-hive (Apache-2.0
license), which itself is
+ * a port of Presto's Bing Tile implementation. Adapted for JTS geometry.
+ */
+public final class BingTile {
+
+ public static final int MAX_ZOOM_LEVEL = 23;
+ private static final int TILE_PIXELS = 256;
+ private static final double MAX_LATITUDE = 85.05112878;
+ private static final double MIN_LATITUDE = -85.05112878;
+ private static final double MAX_LONGITUDE = 180;
+ private static final double MIN_LONGITUDE = -180;
+
+ private static final int OPTIMIZED_TILING_MIN_ZOOM_LEVEL = 10;
+
+ private static final String LATITUDE_OUT_OF_RANGE =
+ "Latitude must be between " + MIN_LATITUDE + " and " + MAX_LATITUDE;
+ private static final String LONGITUDE_OUT_OF_RANGE =
+ "Longitude must be between " + MIN_LONGITUDE + " and " + MAX_LONGITUDE;
+ private static final String QUAD_KEY_EMPTY = "QuadKey must not be empty
string";
+ private static final String QUAD_KEY_TOO_LONG =
+ "QuadKey must be " + MAX_ZOOM_LEVEL + " characters or less";
+ private static final String ZOOM_LEVEL_TOO_SMALL = "Zoom level must be > 0";
+ private static final String ZOOM_LEVEL_TOO_LARGE = "Zoom level must be <= "
+ MAX_ZOOM_LEVEL;
+
+ private static final GeometryFactory GEOMETRY_FACTORY = new
GeometryFactory();
+
+ private final int x;
+ private final int y;
+ private final int zoomLevel;
+
+ // =========================================================================
+ // Constructors
+ // =========================================================================
+
+ private BingTile(int x, int y, int zoomLevel) {
+ checkZoomLevel(zoomLevel);
+ checkCoordinate(x, zoomLevel);
+ checkCoordinate(y, zoomLevel);
+ this.x = x;
+ this.y = y;
+ this.zoomLevel = zoomLevel;
+ }
+
+ // =========================================================================
+ // Validation methods
+ // =========================================================================
+
+ private static void checkCondition(boolean condition, String formatString,
Object... args) {
+ if (!condition) {
+ throw new IllegalArgumentException(String.format(formatString, args));
+ }
+ }
+
+ private static void checkZoomLevel(long zoomLevel) {
+ checkCondition(zoomLevel > 0, ZOOM_LEVEL_TOO_SMALL);
+ checkCondition(zoomLevel <= MAX_ZOOM_LEVEL, ZOOM_LEVEL_TOO_LARGE);
+ }
+
+ private static void checkCoordinate(long coordinate, long zoomLevel) {
+ checkCondition(
+ coordinate >= 0 && coordinate < (1 << zoomLevel),
+ "XY coordinates for a Bing tile at zoom level %s must be within [0,
%s) range",
+ zoomLevel,
+ 1 << zoomLevel);
+ }
+
+ private static void checkQuadKey(String quadkey) {
+ checkCondition(quadkey.length() > 0, QUAD_KEY_EMPTY);
+ checkCondition(quadkey.length() <= MAX_ZOOM_LEVEL, QUAD_KEY_TOO_LONG);
+ }
+
+ private static void checkLatitude(double latitude) {
+ checkCondition(latitude >= MIN_LATITUDE && latitude <= MAX_LATITUDE,
LATITUDE_OUT_OF_RANGE);
+ }
+
+ private static void checkLongitude(double longitude) {
+ checkCondition(
+ longitude >= MIN_LONGITUDE && longitude <= MAX_LONGITUDE,
LONGITUDE_OUT_OF_RANGE);
+ }
+
+ // =========================================================================
+ // Utility functions for converting to/from latitude/longitude
+ // =========================================================================
+
+ private static long mapSize(int zoomLevel) {
+ return 256L << zoomLevel;
+ }
+
+ private static double clip(double n, double minValue, double maxValue) {
+ return Math.min(Math.max(n, minValue), maxValue);
+ }
+
+ private static int axisToCoordinates(double axis, long mapSize) {
+ int tileAxis = (int) clip(axis * mapSize, 0, mapSize - 1);
+ return tileAxis / TILE_PIXELS;
+ }
+
+ private static int longitudeToTileX(double longitude, long mapSize) {
+ double x = (longitude + 180) / 360;
+ return axisToCoordinates(x, mapSize);
+ }
+
+ private static int latitudeToTileY(double latitude, long mapSize) {
+ double sinLatitude = Math.sin(latitude * Math.PI / 180);
+ double y = 0.5 - Math.log((1 + sinLatitude) / (1 - sinLatitude)) / (4 *
Math.PI);
+ return axisToCoordinates(y, mapSize);
+ }
+
+ /**
+ * Converts tile XY coordinates to the upper-left corner latitude/longitude
of the tile.
+ *
+ * @return a Coordinate with x=longitude, y=latitude
+ */
+ private static Coordinate tileXYToLatitudeLongitude(int tileX, int tileY,
int zoomLevel) {
+ long mapSize = mapSize(zoomLevel);
+ double x = (clip(tileX * TILE_PIXELS, 0, mapSize) / mapSize) - 0.5;
+ double y = 0.5 - (clip(tileY * TILE_PIXELS, 0, mapSize) / mapSize);
+ double lat = 90 - 360 * Math.atan(Math.exp(-y * 2 * Math.PI)) / Math.PI;
+ double lon = 360 * x;
+ return new Coordinate(lon, lat);
+ }
+
+ // =========================================================================
+ // Factory methods
+ // =========================================================================
+
+ /**
+ * Creates a BingTile from XY coordinates and zoom level.
+ *
+ * @param x the tile X coordinate
+ * @param y the tile Y coordinate
+ * @param zoomLevel the zoom level (1 to 23)
+ * @return a new BingTile
+ */
+ public static BingTile fromCoordinates(int x, int y, int zoomLevel) {
+ return new BingTile(x, y, zoomLevel);
+ }
+
+ /**
+ * Creates a BingTile from a latitude/longitude point at the given zoom
level.
+ *
+ * @param latitude the latitude (-85.05112878 to 85.05112878)
+ * @param longitude the longitude (-180 to 180)
+ * @param zoomLevel the zoom level (1 to 23)
+ * @return a new BingTile
+ */
+ public static BingTile fromLatLon(double latitude, double longitude, int
zoomLevel) {
+ checkLatitude(latitude);
+ checkLongitude(longitude);
+ checkZoomLevel(zoomLevel);
+
+ long mapSize = mapSize(zoomLevel);
+ int tileX = longitudeToTileX(longitude, mapSize);
+ int tileY = latitudeToTileY(latitude, mapSize);
+ return new BingTile(tileX, tileY, zoomLevel);
+ }
+
+ /**
+ * Creates a BingTile from a quadkey string.
+ *
+ * @param quadKey the quadkey string (e.g. "0231")
+ * @return a new BingTile
+ */
+ public static BingTile fromQuadKey(String quadKey) {
+ checkQuadKey(quadKey);
+
+ int zoomLevel = quadKey.length();
+ checkZoomLevel(zoomLevel);
+
+ int tileX = 0;
+ int tileY = 0;
+
+ for (int i = zoomLevel; i > 0; i--) {
+ int mask = 1 << (i - 1);
+ switch (quadKey.charAt(zoomLevel - i)) {
+ case '0':
+ break;
+ case '1':
+ tileX |= mask;
+ break;
+ case '2':
+ tileY |= mask;
+ break;
+ case '3':
+ tileX |= mask;
+ tileY |= mask;
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid QuadKey digit: " +
quadKey);
+ }
+ }
+ return new BingTile(tileX, tileY, zoomLevel);
+ }
+
+ // =========================================================================
+ // Accessors
+ // =========================================================================
+
+ public int getX() {
+ return x;
+ }
+
+ public int getY() {
+ return y;
+ }
+
+ public int getZoomLevel() {
+ return zoomLevel;
+ }
+
+ // =========================================================================
+ // Conversion methods
+ // =========================================================================
+
+ /**
+ * Converts this tile to its quadkey string representation.
+ *
+ * @return the quadkey string
+ */
+ public String toQuadKey() {
+ char[] quadKey = new char[this.zoomLevel];
+ for (int i = this.zoomLevel; i > 0; i--) {
+ char digit = '0';
+ int mask = 1 << (i - 1);
+ if ((this.x & mask) != 0) {
+ digit++;
+ }
+ if ((this.y & mask) != 0) {
+ digit += 2;
+ }
+ quadKey[this.zoomLevel - i] = digit;
+ }
+ return String.valueOf(quadKey);
+ }
+
+ /**
+ * Returns the bounding box envelope of this tile.
+ *
+ * @return the JTS Envelope
+ */
+ public Envelope toEnvelope() {
+ Coordinate upperLeftCorner = tileXYToLatitudeLongitude(this.x, this.y,
this.zoomLevel);
+ Coordinate lowerRightCorner = tileXYToLatitudeLongitude(this.x + 1, this.y
+ 1, this.zoomLevel);
+
+ return new Envelope(
+ upperLeftCorner.x, lowerRightCorner.x, lowerRightCorner.y,
upperLeftCorner.y);
+ }
+
+ /**
+ * Returns the polygon geometry of this tile.
+ *
+ * @return the JTS Polygon
+ */
+ public Polygon toPolygon() {
+ Envelope envelope = toEnvelope();
+ Coordinate[] coordinates =
+ new Coordinate[] {
+ new Coordinate(envelope.getMinX(), envelope.getMinY()),
+ new Coordinate(envelope.getMinX(), envelope.getMaxY()),
+ new Coordinate(envelope.getMaxX(), envelope.getMaxY()),
+ new Coordinate(envelope.getMaxX(), envelope.getMinY()),
+ new Coordinate(envelope.getMinX(), envelope.getMinY())
+ };
+ return GEOMETRY_FACTORY.createPolygon(coordinates);
+ }
+
+ // =========================================================================
+ // Overridden Object methods
+ // =========================================================================
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (other == null || getClass() != other.getClass()) {
+ return false;
+ }
+ BingTile otherTile = (BingTile) other;
+ return this.x == otherTile.x && this.y == otherTile.y && this.zoomLevel ==
otherTile.zoomLevel;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(x, y, zoomLevel);
+ }
+
+ @Override
+ public String toString() {
+ return toQuadKey();
+ }
+
+ // =========================================================================
+ // Spatial query methods
+ // =========================================================================
+
+ /**
+ * Returns the tiles around a given latitude/longitude at the specified zoom
level. Returns the
+ * 3x3 neighborhood of tiles centered on the tile containing the point.
+ *
+ * @param latitude the latitude
+ * @param longitude the longitude
+ * @param zoomLevel the zoom level
+ * @return list of BingTile objects
+ */
+ public static List<BingTile> tilesAround(double latitude, double longitude,
int zoomLevel) {
+ checkLatitude(latitude);
+ checkLongitude(longitude);
+ checkZoomLevel(zoomLevel);
+
+ List<BingTile> ret = new ArrayList<>();
+ long mapSize = mapSize(zoomLevel);
+ long maxTileIndex = (mapSize / TILE_PIXELS) - 1;
+
+ int tileX = longitudeToTileX(longitude, mapSize);
+ int tileY = latitudeToTileY(latitude, mapSize);
+
+ for (int i = -1; i <= 1; i++) {
+ for (int j = -1; j <= 1; j++) {
+ int newX = tileX + i;
+ int newY = tileY + j;
+ if (newX >= 0 && newX <= maxTileIndex && newY >= 0 && newY <=
maxTileIndex) {
+ ret.add(BingTile.fromCoordinates(newX, newY, zoomLevel));
+ }
+ }
+ }
+ return ret;
+ }
+
+ /**
+ * Returns the minimum set of Bing tiles that fully covers a given geometry
at the specified zoom
+ * level.
+ *
+ * @param geometry the JTS Geometry to cover
+ * @param zoomLevel the zoom level (1 to 23)
+ * @return list of BingTile objects covering the geometry
+ */
+ public static List<BingTile> tilesCovering(Geometry geometry, int zoomLevel)
{
+ checkZoomLevel(zoomLevel);
+
+ List<BingTile> ret = new ArrayList<>();
+ if (geometry.isEmpty()) {
+ return ret;
+ }
+
+ Envelope envelope = geometry.getEnvelopeInternal();
+
+ checkLatitude(envelope.getMinY());
+ checkLatitude(envelope.getMaxY());
+ checkLongitude(envelope.getMinX());
+ checkLongitude(envelope.getMaxX());
+
+ boolean pointOrRectangle = isPointOrRectangle(geometry, envelope);
+
+ BingTile leftUpperTile = fromLatLon(envelope.getMaxY(),
envelope.getMinX(), zoomLevel);
+ BingTile rightLowerTile = getTileCoveringLowerRightCorner(envelope,
zoomLevel);
+
+ // XY coordinates start at (0,0) in the left upper corner and increase
left to right and
+ // top to bottom
+ long tileCount =
+ (long) (rightLowerTile.getX() - leftUpperTile.getX() + 1)
+ * (rightLowerTile.getY() - leftUpperTile.getY() + 1);
+
+ checkGeometryToBingTilesLimits(geometry, pointOrRectangle, tileCount);
+
+ if (pointOrRectangle || zoomLevel <= OPTIMIZED_TILING_MIN_ZOOM_LEVEL) {
+ // Collect tiles covering the bounding box and check each tile for
intersection
+ for (int x = leftUpperTile.getX(); x <= rightLowerTile.getX(); x++) {
+ for (int y = leftUpperTile.getY(); y <= rightLowerTile.getY(); y++) {
+ BingTile tile = BingTile.fromCoordinates(x, y, zoomLevel);
+ if (pointOrRectangle || !tileDisjoint(tile, geometry)) {
+ ret.add(tile);
+ }
+ }
+ }
+ } else {
+ // Optimized tiling: identify large tiles fully covered by geometry then
expand
+ BingTile[] tiles =
+ getTilesInBetween(leftUpperTile, rightLowerTile,
OPTIMIZED_TILING_MIN_ZOOM_LEVEL);
+ for (BingTile tile : tiles) {
+ appendIntersectingSubtiles(geometry, zoomLevel, tile, ret);
+ }
+ }
+
+ return ret;
+ }
+
+ // =========================================================================
+ // Internal helper methods for tilesCovering
+ // =========================================================================
+
+ private static boolean isPointOrRectangle(Geometry geometry, Envelope
envelope) {
+ if (geometry instanceof org.locationtech.jts.geom.Point) {
+ return true;
+ }
+ if (!(geometry instanceof Polygon)) {
+ return false;
+ }
+ Polygon polygon = (Polygon) geometry;
+ if (polygon.getNumInteriorRing() > 0) {
+ return false;
+ }
+ Coordinate[] coords = polygon.getExteriorRing().getCoordinates();
+ // A closed rectangle has 5 coordinates (first == last)
+ if (coords.length != 5) {
+ return false;
+ }
+ // Check that all corners match the envelope corners
+ for (int i = 0; i < 4; i++) {
+ Coordinate c = coords[i];
+ boolean matchesCorner =
+ (c.x == envelope.getMinX() || c.x == envelope.getMaxX())
+ && (c.y == envelope.getMinY() || c.y == envelope.getMaxY());
+ if (!matchesCorner) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static void checkGeometryToBingTilesLimits(
+ Geometry geometry, boolean pointOrRectangle, long tileCount) {
+ if (pointOrRectangle) {
+ checkCondition(
+ tileCount <= 1_000_000,
+ "The number of input tiles is too large (more than 1M) to compute a
set of covering Bing tiles.");
+ } else {
+ checkCondition(
+ (int) tileCount == tileCount,
+ "The zoom level is too high to compute a set of covering Bing
tiles.");
+
+ long pointCount = geometry.getNumPoints();
+ long complexity;
+ try {
+ complexity = Math.multiplyExact(tileCount, pointCount);
+ } catch (ArithmeticException e) {
+ throw new IllegalArgumentException(
+ "The zoom level is too high or the geometry is too complex to
compute a set of covering Bing tiles. "
+ + "Please use a lower zoom level or convert the geometry to
its bounding box using the ST_Envelope function.");
+ }
+ checkCondition(
+ complexity <= 25_000_000,
+ "The zoom level is too high or the geometry is too complex to
compute a set of covering Bing tiles. "
+ + "Please use a lower zoom level or convert the geometry to its
bounding box using the ST_Envelope function.");
+ }
+ }
+
+ private static BingTile getTileCoveringLowerRightCorner(Envelope envelope,
int zoomLevel) {
+ BingTile tile = fromLatLon(envelope.getMinY(), envelope.getMaxX(),
zoomLevel);
+
+ // If the tile covering the lower right corner of the envelope overlaps
the envelope only
+ // at the border then return a tile shifted to the left and/or up
+ int deltaX = 0;
+ int deltaY = 0;
+
+ Coordinate upperLeftCorner =
+ tileXYToLatitudeLongitude(tile.getX(), tile.getY(),
tile.getZoomLevel());
+ if (upperLeftCorner.x == envelope.getMaxX()) {
+ deltaX = -1;
+ }
+ if (upperLeftCorner.y == envelope.getMinY()) {
+ deltaY = -1;
+ }
+
+ if (deltaX == 0 && deltaY == 0) {
+ return tile;
+ } else {
+ return BingTile.fromCoordinates(
+ tile.getX() + deltaX, tile.getY() + deltaY, tile.getZoomLevel());
+ }
+ }
+
+ private static BingTile[] getTilesInBetween(
+ BingTile leftUpperTile, BingTile rightLowerTile, int zoomLevel) {
+ checkCondition(
+ leftUpperTile.getZoomLevel() == rightLowerTile.getZoomLevel(),
"Mismatched zoom levels");
+ checkCondition(leftUpperTile.getZoomLevel() > zoomLevel, "Tile zoom level
too low");
+
+ int divisor = 1 << (leftUpperTile.getZoomLevel() - zoomLevel);
+
+ int minX = (int) Math.floor((double) leftUpperTile.getX() / divisor);
+ int maxX = (int) Math.floor((double) rightLowerTile.getX() / divisor);
+ int minY = (int) Math.floor((double) leftUpperTile.getY() / divisor);
+ int maxY = (int) Math.floor((double) rightLowerTile.getY() / divisor);
+
+ BingTile[] tiles = new BingTile[(maxX - minX + 1) * (maxY - minY + 1)];
+
+ int index = 0;
+ for (int x = minX; x <= maxX; x++) {
+ for (int y = minY; y <= maxY; y++) {
+ tiles[index] = BingTile.fromCoordinates(x, y,
OPTIMIZED_TILING_MIN_ZOOM_LEVEL);
+ index++;
+ }
+ }
+ return tiles;
+ }
+
+ private static boolean tileDisjoint(BingTile tile, Geometry geometry) {
+ Polygon tilePolygon = tile.toPolygon();
+ return tilePolygon.disjoint(geometry);
+ }
+
+ private static boolean geometryContainsTile(Geometry geometry, BingTile
tile) {
+ Polygon tilePolygon = tile.toPolygon();
+ return geometry.contains(tilePolygon);
+ }
+
+ private static void appendIntersectingSubtiles(
+ Geometry geometry, int zoomLevel, BingTile tile, List<BingTile> result) {
+ int tileZoomLevel = tile.getZoomLevel();
+ checkCondition(tileZoomLevel <= zoomLevel, "Tile zoom level too high");
+
+ if (tileZoomLevel == zoomLevel) {
+ if (!tileDisjoint(tile, geometry)) {
+ result.add(tile);
+ }
+ return;
+ }
+
+ if (geometryContainsTile(geometry, tile)) {
+ // Tile is fully contained — add all sub-tiles at the target zoom level
+ int subTileCount = 1 << (zoomLevel - tileZoomLevel);
+ int minX = subTileCount * tile.getX();
+ int minY = subTileCount * tile.getY();
+
+ for (int x = minX; x < minX + subTileCount; x++) {
+ for (int y = minY; y < minY + subTileCount; y++) {
+ result.add(BingTile.fromCoordinates(x, y, zoomLevel));
+ }
+ }
+ return;
+ }
+
+ if (tileDisjoint(tile, geometry)) {
+ return;
+ }
+
+ // Recurse into the 4 children
+ int nextZoomLevel = tileZoomLevel + 1;
+ int minX = 2 * tile.getX();
+ int minY = 2 * tile.getY();
+
+ for (int x = minX; x < minX + 2; x++) {
+ for (int y = minY; y < minY + 2; y++) {
+ appendIntersectingSubtiles(
+ geometry, zoomLevel, BingTile.fromCoordinates(x, y,
nextZoomLevel), result);
+ }
+ }
+ }
+}
diff --git a/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
b/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
index 61fd47ae34..6d9e04b65f 100644
--- a/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
+++ b/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
@@ -5387,4 +5387,315 @@ public class FunctionsTest extends TestBase {
// 13 chars * 5 bits = 65 > 64 (MAX_BIT_PRECISION)
Functions.geohashNeighbors("0123456789abc");
}
+
+ // =========================================================================
+ // Bing Tile function tests
+ // =========================================================================
+
+ // --- ST_BingTile (from coordinates and from quadkey) ---
+
+ @Test
+ public void testBingTileFromCoordinates() {
+ // bing_tile(3, 5, 3) = quadkey "213"
+ assertEquals("213", Functions.bingTile(3, 5, 3));
+ // bing_tile(21845, 13506, 15) = quadkey "123030123010121"
+ assertEquals("123030123010121", Functions.bingTile(21845, 13506, 15));
+ }
+
+ @Test
+ public void testBingTileQuadKeyRoundTrip() {
+ assertEquals("213", Functions.bingTileQuadKey(Functions.bingTile(3, 5,
3)));
+ assertEquals(
+ "123030123010121", Functions.bingTileQuadKey(Functions.bingTile(21845,
13506, 15)));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testBingTileInvalidEmptyQuadKey() {
+ Functions.bingTileQuadKey("");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testBingTileInvalidQuadKeyDigits() {
+ Functions.bingTileQuadKey("test");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testBingTileInvalidQuadKeyDigits2() {
+ // "12345" contains '4' and '5' which are invalid
+ Functions.bingTileQuadKey("12345");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testBingTileQuadKeyTooLong() {
+ // quadkey > 23 chars
+
Functions.bingTileQuadKey("101010101010101010101010101010100101010101001010");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testBingTileXYOutOfRange() {
+ // x=10 at zoom 3 is out of [0, 8) range
+ Functions.bingTile(10, 2, 3);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testBingTileXYOutOfRange2() {
+ // y=10 at zoom 3 is out of [0, 8) range
+ Functions.bingTile(2, 10, 3);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testBingTileZoomOutOfRange() {
+ // zoom=37 > 23
+ Functions.bingTile(2, 7, 37);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testBingTileZoomZero() {
+ // zoom=0 is invalid
+ Functions.bingTile(0, 0, 0);
+ }
+
+ // --- ST_BingTileAt (point to tile) ---
+
+ @Test
+ public void testBingTileAt() {
+ // bingTileAt(lon=60, lat=30.12, zoom=15) = tile(21845, 13506, 15)
+ String qk = Functions.bingTileAt(60, 30.12, 15);
+ assertEquals("123030123010121", qk);
+ assertEquals(21845, Functions.bingTileX(qk));
+ assertEquals(13506, Functions.bingTileY(qk));
+
+ // bingTileAt(lon=-0.002, lat=0, zoom=1) = tile(0, 1, 1)
+ qk = Functions.bingTileAt(-0.002, 0, 1);
+ assertEquals(0, Functions.bingTileX(qk));
+ assertEquals(1, Functions.bingTileY(qk));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testBingTileAtLongitudeOutOfRange() {
+ // longitude 600 is out of range
+ Functions.bingTileAt(600, 30.12, 15);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testBingTileAtLatitudeOutOfRange() {
+ // latitude 300.12 is out of range
+ Functions.bingTileAt(60, 300.12, 15);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testBingTileAtZoomTooSmall() {
+ // zoom=0
+ Functions.bingTileAt(60, 30.12, 0);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testBingTileAtZoomTooLarge() {
+ // zoom=40
+ Functions.bingTileAt(60, 30.12, 40);
+ }
+
+ // --- ST_BingTileCoordinates (X, Y) ---
+
+ @Test
+ public void testBingTileCoordinates() {
+ // bing_tile_coordinates('213') = (3, 5)
+ assertEquals(3, Functions.bingTileX("213"));
+ assertEquals(5, Functions.bingTileY("213"));
+
+ // bing_tile_coordinates('123030123010121') = (21845, 13506)
+ assertEquals(21845, Functions.bingTileX("123030123010121"));
+ assertEquals(13506, Functions.bingTileY("123030123010121"));
+ }
+
+ // --- ST_BingTileZoomLevel ---
+
+ @Test
+ public void testBingTileZoomLevel() {
+ assertEquals(3, Functions.bingTileZoomLevel("213"));
+ assertEquals(15, Functions.bingTileZoomLevel("123030123010121"));
+ }
+
+ // --- ST_BingTilesAround ---
+
+ @Test
+ public void testBingTilesAround() {
+ // bingTilesAround(lon=60, lat=30.12, zoom=1) = ["0", "2", "1", "3"]
+ String[] tiles = Functions.bingTilesAround(60, 30.12, 1);
+ assertEquals(4, tiles.length);
+ assertArrayEquals(new String[] {"0", "2", "1", "3"}, tiles);
+
+ // bingTilesAround(lon=60, lat=30.12, zoom=15) = 9 tiles (3x3 neighborhood)
+ tiles = Functions.bingTilesAround(60, 30.12, 15);
+ assertEquals(9, tiles.length);
+ assertArrayEquals(
+ new String[] {
+ "123030123010102",
+ "123030123010120",
+ "123030123010122",
+ "123030123010103",
+ "123030123010121",
+ "123030123010123",
+ "123030123010112",
+ "123030123010130",
+ "123030123010132"
+ },
+ tiles);
+ }
+
+ @Test
+ public void testBingTilesAroundCorner() {
+ // corner case at (lon=-180, lat=-85.05112878, zoom=1) = all 4 tiles
+ String[] tiles = Functions.bingTilesAround(-180, -85.05112878, 1);
+ assertArrayEquals(new String[] {"0", "2", "1", "3"}, tiles);
+
+ // corner at (lon=-180, lat=-85.05112878, zoom=3) = 4 tiles
+ tiles = Functions.bingTilesAround(-180, -85.05112878, 3);
+ assertArrayEquals(new String[] {"220", "222", "221", "223"}, tiles);
+ }
+
+ @Test
+ public void testBingTilesAroundEdge() {
+ // edge case at (lon=0, lat=-85.05112878, zoom=2) = 6 tiles
+ String[] tiles = Functions.bingTilesAround(0, -85.05112878, 2);
+ assertArrayEquals(new String[] {"21", "23", "30", "32", "31", "33"},
tiles);
+
+ // edge at (lon=0, lat=85.05112878, zoom=2) = 6 tiles
+ tiles = Functions.bingTilesAround(0, 85.05112878, 2);
+ assertArrayEquals(new String[] {"01", "03", "10", "12", "11", "13"},
tiles);
+ }
+
+ // --- ST_BingTilePolygon ---
+
+ @Test
+ public void testBingTilePolygon() {
+ // bing_tile_polygon('123030123010121')
+ Geometry polygon = Functions.bingTilePolygon("123030123010121");
+ assertNotNull(polygon);
+ assertTrue(polygon instanceof Polygon);
+ assertEquals(5, polygon.getCoordinates().length);
+
+ // Expected envelope for tile '123030123010121'
+ Envelope env = polygon.getEnvelopeInternal();
+ assertEquals(59.996337890625, env.getMinX(), 1e-10);
+ assertEquals(60.00732421875, env.getMaxX(), 1e-10);
+ assertEquals(30.11662158281937, env.getMinY(), 1e-10);
+ assertEquals(30.12612436422458, env.getMaxY(), 1e-10);
+ }
+
+ @Test
+ public void testBingTilePolygonBottomRightCorner() {
+ // bottom-right corner of tile(1,1,1) = (180, -85.05112877980659)
+ Geometry polygon = Functions.bingTilePolygon(Functions.bingTile(1, 1, 1));
+ Envelope env = polygon.getEnvelopeInternal();
+ assertEquals(180.0, env.getMaxX(), 1e-10);
+ assertEquals(-85.05112877980659, env.getMinY(), 1e-10);
+
+ // bottom-right corner of tile(3,3,2) = (180, -85.05112877980659)
+ polygon = Functions.bingTilePolygon(Functions.bingTile(3, 3, 2));
+ env = polygon.getEnvelopeInternal();
+ assertEquals(180.0, env.getMaxX(), 1e-10);
+ assertEquals(-85.05112877980659, env.getMinY(), 1e-10);
+ }
+
+ @Test
+ public void testBingTilePolygonOriginCorner() {
+ // top-left corner of tile(0,0,1) bottom-right = (0, 0)
+ Geometry polygon = Functions.bingTilePolygon(Functions.bingTile(0, 0, 1));
+ Envelope env = polygon.getEnvelopeInternal();
+ assertEquals(0.0, env.getMaxX(), 1e-10);
+ assertEquals(0.0, env.getMinY(), 1e-10);
+
+ // top-left corner of tile(0,0,1) = (-180, 85.05112877980659)
+ assertEquals(-180.0, env.getMinX(), 1e-10);
+ assertEquals(85.05112877980659, env.getMaxY(), 1e-10);
+ }
+
+ // --- ST_BingTileCellIDs (geometry_to_bing_tiles) ---
+
+ @Test
+ public void testBingTileCellIDsPoint() {
+ // geometry_to_bing_tiles(POINT(60 30.12), 10) = ["1230301230"]
+ Geometry point = GEOMETRY_FACTORY.createPoint(new Coordinate(60, 30.12));
+ String[] tiles = Functions.bingTileCellIDs(point, 10);
+ assertArrayEquals(new String[] {"1230301230"}, tiles);
+
+ // geometry_to_bing_tiles(POINT(60 30.12), 15) = ["123030123010121"]
+ tiles = Functions.bingTileCellIDs(point, 15);
+ assertArrayEquals(new String[] {"123030123010121"}, tiles);
+
+ // geometry_to_bing_tiles(POINT(60 30.12), 16) = ["1230301230101212"]
+ tiles = Functions.bingTileCellIDs(point, 16);
+ assertArrayEquals(new String[] {"1230301230101212"}, tiles);
+ }
+
+ @Test
+ public void testBingTileCellIDsPolygon() {
+ // geometry_to_bing_tiles(POLYGON((0 0, 0 10, 10 10, 10 0)), 6)
+ Geometry polygon =
+ GEOMETRY_FACTORY.createPolygon(
+ coordArray(0.0, 0.0, 0.0, 10.0, 10.0, 10.0, 10.0, 0.0, 0.0, 0.0));
+ String[] tiles = Functions.bingTileCellIDs(polygon, 6);
+ assertArrayEquals(new String[] {"122220", "122222", "122221", "122223"},
tiles);
+ }
+
+ @Test
+ public void testBingTileCellIDsTriangle() {
+ // geometry_to_bing_tiles(POLYGON((10 10, -10 10, -20 -15, 10 10)), 3)
+ Geometry triangle =
+ GEOMETRY_FACTORY.createPolygon(
+ coordArray(10.0, 10.0, -10.0, 10.0, -20.0, -15.0, 10.0, 10.0));
+ String[] tiles = Functions.bingTileCellIDs(triangle, 3);
+ assertArrayEquals(new String[] {"033", "211", "122"}, tiles);
+ }
+
+ @Test
+ public void testBingTileCellIDsEmpty() {
+ // POINT EMPTY, POLYGON EMPTY → empty list
+ Geometry emptyPoint = GEOMETRY_FACTORY.createPoint();
+ assertEquals(0, Functions.bingTileCellIDs(emptyPoint, 10).length);
+
+ Geometry emptyPolygon = GEOMETRY_FACTORY.createPolygon();
+ assertEquals(0, Functions.bingTileCellIDs(emptyPolygon, 10).length);
+ }
+
+ @Test
+ public void testBingTileCellIDsSelfRoundTrip() {
+ // geometry_to_bing_tiles(bing_tile_polygon('1230301230'), 10) =
["1230301230"]
+ Geometry tilePolygon = Functions.bingTilePolygon("1230301230");
+ String[] tiles = Functions.bingTileCellIDs(tilePolygon, 10);
+ assertArrayEquals(new String[] {"1230301230"}, tiles);
+
+ // expanding to zoom 11 = 4 child tiles
+ tiles = Functions.bingTileCellIDs(tilePolygon, 11);
+ assertArrayEquals(
+ new String[] {"12303012300", "12303012302", "12303012301",
"12303012303"}, tiles);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testBingTileCellIDsZoomTooSmall() {
+ // zoom=0 is invalid
+ Geometry point = GEOMETRY_FACTORY.createPoint(new Coordinate(60, 30.12));
+ Functions.bingTileCellIDs(point, 0);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testBingTileCellIDsZoomTooLarge() {
+ // zoom=40 is invalid
+ Geometry point = GEOMETRY_FACTORY.createPoint(new Coordinate(60, 30.12));
+ Functions.bingTileCellIDs(point, 40);
+ }
+
+ // --- ST_BingTileToGeom ---
+
+ @Test
+ public void testBingTileToGeom() {
+ String[] quadkeys = new String[] {"0", "1", "2", "3"};
+ Geometry[] polygons = Functions.bingTileToGeom(quadkeys);
+ assertEquals(4, polygons.length);
+ for (Geometry g : polygons) {
+ assertTrue(g instanceof Polygon);
+ assertEquals(5, g.getCoordinates().length);
+ }
+ }
}
diff --git a/docs/api/flink/Function.md b/docs/api/flink/Function.md
index 58871582a1..180ebccc21 100644
--- a/docs/api/flink/Function.md
+++ b/docs/api/flink/Function.md
@@ -663,6 +663,189 @@ Output:
32618
```
+## ST_BingTile
+
+Introduction: Creates a Bing Tile quadkey from tile XY coordinates and a zoom
level.
+
+Format: `ST_BingTile(tileX: Integer, tileY: Integer, zoomLevel: Integer)`
+
+Since: `v1.9.0`
+
+Example:
+
+```sql
+SELECT ST_BingTile(3, 5, 3)
+```
+
+Output:
+
+```
+213
+```
+
+## ST_BingTileAt
+
+Introduction: Returns the Bing Tile quadkey for a given point (longitude,
latitude) at a specified zoom level.
+
+Format: `ST_BingTileAt(longitude: Double, latitude: Double, zoomLevel:
Integer)`
+
+Since: `v1.9.0`
+
+Example:
+
+```sql
+SELECT ST_BingTileAt(60, 30.12, 15)
+```
+
+Output:
+
+```
+123030123010121
+```
+
+## ST_BingTileCellIDs
+
+Introduction: Returns an array of Bing Tile quadkey strings that cover the
given geometry at the specified zoom level.
+
+Format: `ST_BingTileCellIDs(geom: Geometry, zoomLevel: Integer)`
+
+Since: `v1.9.0`
+
+Example:
+
+```sql
+SELECT ST_BingTileCellIDs(ST_GeomFromText('POINT(60 30.12)'), 10)
+```
+
+Output:
+
+```
+[1230301230]
+```
+
+## ST_BingTilePolygon
+
+Introduction: Returns the bounding polygon (Geometry) of the Bing Tile
identified by the given quadkey.
+
+Format: `ST_BingTilePolygon(quadKey: String)`
+
+Since: `v1.9.0`
+
+Example:
+
+```sql
+SELECT ST_AsText(ST_BingTilePolygon('213'))
+```
+
+Output:
+
+```
+POLYGON ((0 0, 0 -40.97989806962013, 45 -40.97989806962013, 45 0, 0 0))
+```
+
+## ST_BingTilesAround
+
+Introduction: Returns an array of Bing Tile quadkey strings representing the
neighborhood tiles around the tile that contains the given point (longitude,
latitude) at the specified zoom level. Returns the 3×3 neighborhood (up to 9
tiles), or fewer tiles at the edges/corners of the map.
+
+Format: `ST_BingTilesAround(longitude: Double, latitude: Double, zoomLevel:
Integer)`
+
+Since: `v1.9.0`
+
+Example:
+
+```sql
+SELECT ST_BingTilesAround(60, 30.12, 1)
+```
+
+Output:
+
+```
+[0, 2, 1, 3]
+```
+
+## ST_BingTileToGeom
+
+Introduction: Returns an array of Polygons for the corresponding Bing Tile
quadkeys.
+
+!!!Hint
+ To convert a Polygon array to a single geometry, use
[ST_Collect](#st_collect).
+
+Format: `ST_BingTileToGeom(quadKeys: Array[String])`
+
+Since: `v1.9.0`
+
+Example:
+
+```sql
+SELECT ST_BingTileToGeom(array('0', '1', '2', '3'))
+```
+
+Output:
+
+```
+[POLYGON ((-180 85.05112877980659, -180 0, 0 0, 0 85.05112877980659, -180
85.05112877980659)), ...]
+```
+
+## ST_BingTileX
+
+Introduction: Returns the tile X coordinate of the Bing Tile identified by the
given quadkey.
+
+Format: `ST_BingTileX(quadKey: String)`
+
+Since: `v1.9.0`
+
+Example:
+
+```sql
+SELECT ST_BingTileX('213')
+```
+
+Output:
+
+```
+3
+```
+
+## ST_BingTileY
+
+Introduction: Returns the tile Y coordinate of the Bing Tile identified by the
given quadkey.
+
+Format: `ST_BingTileY(quadKey: String)`
+
+Since: `v1.9.0`
+
+Example:
+
+```sql
+SELECT ST_BingTileY('213')
+```
+
+Output:
+
+```
+5
+```
+
+## ST_BingTileZoomLevel
+
+Introduction: Returns the zoom level of the Bing Tile identified by the given
quadkey.
+
+Format: `ST_BingTileZoomLevel(quadKey: String)`
+
+Since: `v1.9.0`
+
+Example:
+
+```sql
+SELECT ST_BingTileZoomLevel('213')
+```
+
+Output:
+
+```
+3
+```
+
## ST_Boundary
Introduction: Returns the closure of the combinatorial boundary of this
Geometry.
diff --git a/docs/api/snowflake/vector-data/Function.md
b/docs/api/snowflake/vector-data/Function.md
index fdb09919fb..185e009901 100644
--- a/docs/api/snowflake/vector-data/Function.md
+++ b/docs/api/snowflake/vector-data/Function.md
@@ -497,6 +497,168 @@ Output:
32618
```
+## ST_BingTile
+
+Introduction: Creates a Bing Tile quadkey from tile XY coordinates and a zoom
level.
+
+Format: `ST_BingTile(tileX: Int, tileY: Int, zoomLevel: Int)`
+
+SQL example:
+
+```sql
+SELECT ST_BingTile(3, 5, 3)
+```
+
+Output:
+
+```
+213
+```
+
+## ST_BingTileAt
+
+Introduction: Returns the Bing Tile quadkey for a given point (longitude,
latitude) at a specified zoom level.
+
+Format: `ST_BingTileAt(longitude: Double, latitude: Double, zoomLevel: Int)`
+
+SQL example:
+
+```sql
+SELECT ST_BingTileAt(60, 30.12, 15)
+```
+
+Output:
+
+```
+123030123010121
+```
+
+## ST_BingTileCellIDs
+
+Introduction: Returns an array of Bing Tile quadkey strings that cover the
given geometry at the specified zoom level.
+
+Format: `ST_BingTileCellIDs(geom: geometry, zoomLevel: Int)`
+
+SQL example:
+
+```sql
+SELECT ST_BingTileCellIDs(ST_GeomFromText('POINT(60 30.12)'), 10)
+```
+
+Output:
+
+```
+[1230301230]
+```
+
+## ST_BingTilePolygon
+
+Introduction: Returns the bounding polygon (Geometry) of the Bing Tile
identified by the given quadkey.
+
+Format: `ST_BingTilePolygon(quadKey: String)`
+
+SQL example:
+
+```sql
+SELECT ST_AsText(ST_BingTilePolygon('213'))
+```
+
+Output:
+
+```
+POLYGON ((0 0, 0 -40.97989806962013, 45 -40.97989806962013, 45 0, 0 0))
+```
+
+## ST_BingTilesAround
+
+Introduction: Returns an array of Bing Tile quadkey strings representing the
neighborhood tiles around the tile that contains the given point (longitude,
latitude) at the specified zoom level. Returns the 3×3 neighborhood (up to 9
tiles), or fewer tiles at the edges/corners of the map.
+
+Format: `ST_BingTilesAround(longitude: Double, latitude: Double, zoomLevel:
Int)`
+
+SQL example:
+
+```sql
+SELECT ST_BingTilesAround(60, 30.12, 1)
+```
+
+Output:
+
+```
+[0, 2, 1, 3]
+```
+
+## ST_BingTileToGeom
+
+Introduction: Returns a GeometryCollection of Polygons for the corresponding
Bing Tile quadkeys.
+
+Format: `ST_BingTileToGeom(quadKeys: Array)`
+
+SQL example:
+
+```sql
+SELECT ST_BingTileToGeom(ARRAY_CONSTRUCT('0', '1', '2', '3'))
+```
+
+Output:
+
+```
+GEOMETRYCOLLECTION (POLYGON ((-180 85.05112877980659, -180 0, 0 0, 0
85.05112877980659, -180 85.05112877980659)), ...)
+```
+
+## ST_BingTileX
+
+Introduction: Returns the tile X coordinate of the Bing Tile identified by the
given quadkey.
+
+Format: `ST_BingTileX(quadKey: String)`
+
+SQL example:
+
+```sql
+SELECT ST_BingTileX('213')
+```
+
+Output:
+
+```
+3
+```
+
+## ST_BingTileY
+
+Introduction: Returns the tile Y coordinate of the Bing Tile identified by the
given quadkey.
+
+Format: `ST_BingTileY(quadKey: String)`
+
+SQL example:
+
+```sql
+SELECT ST_BingTileY('213')
+```
+
+Output:
+
+```
+5
+```
+
+## ST_BingTileZoomLevel
+
+Introduction: Returns the zoom level of the Bing Tile identified by the given
quadkey.
+
+Format: `ST_BingTileZoomLevel(quadKey: String)`
+
+SQL example:
+
+```sql
+SELECT ST_BingTileZoomLevel('213')
+```
+
+Output:
+
+```
+3
+```
+
## ST_Boundary
Introduction: Returns the closure of the combinatorial boundary of this
Geometry.
diff --git a/docs/api/sql/Function.md b/docs/api/sql/Function.md
index 31a8f3d682..7afea7b69d 100644
--- a/docs/api/sql/Function.md
+++ b/docs/api/sql/Function.md
@@ -736,6 +736,189 @@ Output:
32618
```
+## ST_BingTile
+
+Introduction: Creates a Bing Tile quadkey from tile XY coordinates and a zoom
level.
+
+Format: `ST_BingTile(tileX: Integer, tileY: Integer, zoomLevel: Integer)`
+
+Since: `v1.9.0`
+
+SQL Example
+
+```sql
+SELECT ST_BingTile(3, 5, 3)
+```
+
+Output:
+
+```
+213
+```
+
+## ST_BingTileAt
+
+Introduction: Returns the Bing Tile quadkey for a given point (longitude,
latitude) at a specified zoom level.
+
+Format: `ST_BingTileAt(longitude: Double, latitude: Double, zoomLevel:
Integer)`
+
+Since: `v1.9.0`
+
+SQL Example
+
+```sql
+SELECT ST_BingTileAt(60, 30.12, 15)
+```
+
+Output:
+
+```
+123030123010121
+```
+
+## ST_BingTileCellIDs
+
+Introduction: Returns an array of Bing Tile quadkey strings that cover the
given geometry at the specified zoom level.
+
+Format: `ST_BingTileCellIDs(geom: Geometry, zoomLevel: Integer)`
+
+Since: `v1.9.0`
+
+SQL Example
+
+```sql
+SELECT ST_BingTileCellIDs(ST_GeomFromText('POINT(60 30.12)'), 10)
+```
+
+Output:
+
+```
+[1230301230]
+```
+
+## ST_BingTilePolygon
+
+Introduction: Returns the bounding polygon (Geometry) of the Bing Tile
identified by the given quadkey.
+
+Format: `ST_BingTilePolygon(quadKey: String)`
+
+Since: `v1.9.0`
+
+SQL Example
+
+```sql
+SELECT ST_AsText(ST_BingTilePolygon('213'))
+```
+
+Output:
+
+```
+POLYGON ((0 0, 0 -40.97989806962013, 45 -40.97989806962013, 45 0, 0 0))
+```
+
+## ST_BingTilesAround
+
+Introduction: Returns an array of Bing Tile quadkey strings representing the
neighborhood tiles around the tile that contains the given point (longitude,
latitude) at the specified zoom level. Returns the 3×3 neighborhood (up to 9
tiles), or fewer tiles at the edges/corners of the map.
+
+Format: `ST_BingTilesAround(longitude: Double, latitude: Double, zoomLevel:
Integer)`
+
+Since: `v1.9.0`
+
+SQL Example
+
+```sql
+SELECT ST_BingTilesAround(60, 30.12, 1)
+```
+
+Output:
+
+```
+[0, 2, 1, 3]
+```
+
+## ST_BingTileToGeom
+
+Introduction: Returns an array of Polygons for the corresponding Bing Tile
quadkeys.
+
+!!!Hint
+ To convert a Polygon array to a single geometry, use
[ST_Collect](#st_collect).
+
+Format: `ST_BingTileToGeom(quadKeys: Array[String])`
+
+Since: `v1.9.0`
+
+SQL Example
+
+```sql
+SELECT ST_BingTileToGeom(array('0', '1', '2', '3'))
+```
+
+Output:
+
+```
+[POLYGON ((-180 85.05112877980659, -180 0, 0 0, 0 85.05112877980659, -180
85.05112877980659)), ...]
+```
+
+## ST_BingTileX
+
+Introduction: Returns the tile X coordinate of the Bing Tile identified by the
given quadkey.
+
+Format: `ST_BingTileX(quadKey: String)`
+
+Since: `v1.9.0`
+
+SQL Example
+
+```sql
+SELECT ST_BingTileX('213')
+```
+
+Output:
+
+```
+3
+```
+
+## ST_BingTileY
+
+Introduction: Returns the tile Y coordinate of the Bing Tile identified by the
given quadkey.
+
+Format: `ST_BingTileY(quadKey: String)`
+
+Since: `v1.9.0`
+
+SQL Example
+
+```sql
+SELECT ST_BingTileY('213')
+```
+
+Output:
+
+```
+5
+```
+
+## ST_BingTileZoomLevel
+
+Introduction: Returns the zoom level of the Bing Tile identified by the given
quadkey.
+
+Format: `ST_BingTileZoomLevel(quadKey: String)`
+
+Since: `v1.9.0`
+
+SQL Example
+
+```sql
+SELECT ST_BingTileZoomLevel('213')
+```
+
+Output:
+
+```
+3
+```
+
## ST_BinaryDistanceBandColumn
Introduction: Introduction: Returns a `weights` column containing every record
in a dataframe within a specified `threshold` distance.
diff --git a/flink/src/main/java/org/apache/sedona/flink/Catalog.java
b/flink/src/main/java/org/apache/sedona/flink/Catalog.java
index 5a02e2efdb..2d3646c43c 100644
--- a/flink/src/main/java/org/apache/sedona/flink/Catalog.java
+++ b/flink/src/main/java/org/apache/sedona/flink/Catalog.java
@@ -88,6 +88,15 @@ public class Catalog {
new Functions.ST_H3CellIDs(),
new Functions.ST_H3KRing(),
new Functions.ST_H3ToGeom(),
+ new Functions.ST_BingTile(),
+ new Functions.ST_BingTileAt(),
+ new Functions.ST_BingTilesAround(),
+ new Functions.ST_BingTileZoomLevel(),
+ new Functions.ST_BingTileX(),
+ new Functions.ST_BingTileY(),
+ new Functions.ST_BingTilePolygon(),
+ new Functions.ST_BingTileCellIDs(),
+ new Functions.ST_BingTileToGeom(),
new Functions.ST_Dump(),
new Functions.ST_DumpPoints(),
new Functions.ST_DelaunayTriangles(),
diff --git
a/flink/src/main/java/org/apache/sedona/flink/expressions/Functions.java
b/flink/src/main/java/org/apache/sedona/flink/expressions/Functions.java
index c4567a6a98..c5610fe1d6 100644
--- a/flink/src/main/java/org/apache/sedona/flink/expressions/Functions.java
+++ b/flink/src/main/java/org/apache/sedona/flink/expressions/Functions.java
@@ -2338,6 +2338,95 @@ public class Functions {
}
}
+ // =========================================================================
+ // Bing Tile functions
+ // =========================================================================
+
+ public static class ST_BingTile extends ScalarFunction {
+ @DataTypeHint("STRING")
+ public String eval(
+ @DataTypeHint("INT") Integer tileX,
+ @DataTypeHint("INT") Integer tileY,
+ @DataTypeHint("INT") Integer zoomLevel) {
+ return org.apache.sedona.common.Functions.bingTile(tileX, tileY,
zoomLevel);
+ }
+ }
+
+ public static class ST_BingTileAt extends ScalarFunction {
+ @DataTypeHint("STRING")
+ public String eval(
+ @DataTypeHint("DOUBLE") Double longitude,
+ @DataTypeHint("DOUBLE") Double latitude,
+ @DataTypeHint("INT") Integer zoomLevel) {
+ return org.apache.sedona.common.Functions.bingTileAt(longitude,
latitude, zoomLevel);
+ }
+ }
+
+ public static class ST_BingTilesAround extends ScalarFunction {
+ @DataTypeHint("ARRAY<STRING>")
+ public String[] eval(
+ @DataTypeHint("DOUBLE") Double longitude,
+ @DataTypeHint("DOUBLE") Double latitude,
+ @DataTypeHint("INT") Integer zoomLevel) {
+ return org.apache.sedona.common.Functions.bingTilesAround(longitude,
latitude, zoomLevel);
+ }
+ }
+
+ public static class ST_BingTileZoomLevel extends ScalarFunction {
+ @DataTypeHint("INT")
+ public Integer eval(@DataTypeHint("STRING") String quadKey) {
+ return org.apache.sedona.common.Functions.bingTileZoomLevel(quadKey);
+ }
+ }
+
+ public static class ST_BingTileX extends ScalarFunction {
+ @DataTypeHint("INT")
+ public Integer eval(@DataTypeHint("STRING") String quadKey) {
+ return org.apache.sedona.common.Functions.bingTileX(quadKey);
+ }
+ }
+
+ public static class ST_BingTileY extends ScalarFunction {
+ @DataTypeHint("INT")
+ public Integer eval(@DataTypeHint("STRING") String quadKey) {
+ return org.apache.sedona.common.Functions.bingTileY(quadKey);
+ }
+ }
+
+ public static class ST_BingTilePolygon extends ScalarFunction {
+ @DataTypeHint(
+ value = "RAW",
+ rawSerializer = GeometryTypeSerializer.class,
+ bridgedTo = Geometry.class)
+ public Geometry eval(@DataTypeHint("STRING") String quadKey) {
+ return org.apache.sedona.common.Functions.bingTilePolygon(quadKey);
+ }
+ }
+
+ public static class ST_BingTileCellIDs extends ScalarFunction {
+ @DataTypeHint("ARRAY<STRING>")
+ public String[] eval(
+ @DataTypeHint(
+ value = "RAW",
+ rawSerializer = GeometryTypeSerializer.class,
+ bridgedTo = Geometry.class)
+ Object o,
+ @DataTypeHint("INT") Integer zoomLevel) {
+ Geometry geom = (Geometry) o;
+ return org.apache.sedona.common.Functions.bingTileCellIDs(geom,
zoomLevel);
+ }
+ }
+
+ public static class ST_BingTileToGeom extends ScalarFunction {
+ @DataTypeHint(
+ value = "RAW",
+ rawSerializer = GeometryArrayTypeSerializer.class,
+ bridgedTo = Geometry[].class)
+ public Geometry[] eval(@DataTypeHint("ARRAY<STRING>") String[] quadKeys) {
+ return org.apache.sedona.common.Functions.bingTileToGeom(quadKeys);
+ }
+ }
+
public static class ST_Simplify extends ScalarFunction {
@DataTypeHint(
value = "RAW",
diff --git a/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
b/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
index 3c9b271941..a348c074d7 100644
--- a/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
+++ b/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
@@ -3077,4 +3077,182 @@ public class FunctionTest extends TestBase {
.getField(0);
assertEquals("ST_MultiLineString", actual);
}
+
+ // =========================================================================
+ // Bing Tile function tests
+ // =========================================================================
+
+ @Test
+ public void testBingTile() {
+ // bing_tile(3, 5, 3) = "213"
+ String result = (String) first(tableEnv.sqlQuery("SELECT ST_BingTile(3, 5,
3)")).getField(0);
+ assertEquals("213", result);
+
+ // bing_tile(21845, 13506, 15) = "123030123010121"
+ result = (String) first(tableEnv.sqlQuery("SELECT ST_BingTile(21845,
13506, 15)")).getField(0);
+ assertEquals("123030123010121", result);
+ }
+
+ @Test
+ public void testBingTileAt() {
+ // ST_BingTileAt(lon, lat, zoom) — longitude first
+ // bingTileAt(lon=60, lat=30.12, zoom=15) = tile(21845, 13506, 15) =
"123030123010121"
+ String result =
+ (String) first(tableEnv.sqlQuery("SELECT ST_BingTileAt(60.0, 30.12,
15)")).getField(0);
+ assertEquals("123030123010121", result);
+
+ // bingTileAt(lon=-0.002, lat=0, zoom=1) = tile(0, 1, 1)
+ result = (String) first(tableEnv.sqlQuery("SELECT ST_BingTileAt(-0.002,
0.0, 1)")).getField(0);
+ int x = (int) first(tableEnv.sqlQuery("SELECT ST_BingTileX('" + result +
"')")).getField(0);
+ int y = (int) first(tableEnv.sqlQuery("SELECT ST_BingTileY('" + result +
"')")).getField(0);
+ assertEquals(0, x);
+ assertEquals(1, y);
+ }
+
+ @Test
+ public void testBingTilesAround() {
+ // ST_BingTilesAround(lon, lat, zoom) — longitude first
+ // bingTilesAround(lon=60, lat=30.12, zoom=1) = ["0", "2", "1", "3"]
+ String[] result =
+ (String[])
+ first(tableEnv.sqlQuery("SELECT ST_BingTilesAround(60.0, 30.12,
1)")).getField(0);
+ assertArrayEquals(new String[] {"0", "2", "1", "3"}, result);
+
+ // bingTilesAround(lon=60, lat=30.12, zoom=15) = 9 tiles
+ result =
+ (String[])
+ first(tableEnv.sqlQuery("SELECT ST_BingTilesAround(60.0, 30.12,
15)")).getField(0);
+ assertArrayEquals(
+ new String[] {
+ "123030123010102",
+ "123030123010120",
+ "123030123010122",
+ "123030123010103",
+ "123030123010121",
+ "123030123010123",
+ "123030123010112",
+ "123030123010130",
+ "123030123010132"
+ },
+ result);
+
+ // corner at (lon=-180, lat=-85.05112878, zoom=3) = 4 tiles
+ result =
+ (String[])
+ first(tableEnv.sqlQuery("SELECT ST_BingTilesAround(-180.0,
-85.05112878, 3)"))
+ .getField(0);
+ assertArrayEquals(new String[] {"220", "222", "221", "223"}, result);
+
+ // edge at (lon=0, lat=-85.05112878, zoom=2) = 6 tiles
+ result =
+ (String[])
+ first(tableEnv.sqlQuery("SELECT ST_BingTilesAround(0.0,
-85.05112878, 2)")).getField(0);
+ assertArrayEquals(new String[] {"21", "23", "30", "32", "31", "33"},
result);
+ }
+
+ @Test
+ public void testBingTileZoomLevel() {
+ int result = (int) first(tableEnv.sqlQuery("SELECT
ST_BingTileZoomLevel('213')")).getField(0);
+ assertEquals(3, result);
+
+ result =
+ (int)
+ first(tableEnv.sqlQuery("SELECT
ST_BingTileZoomLevel('123030123010121')")).getField(0);
+ assertEquals(15, result);
+ }
+
+ @Test
+ public void testBingTileXY() {
+ int x = (int) first(tableEnv.sqlQuery("SELECT
ST_BingTileX('213')")).getField(0);
+ int y = (int) first(tableEnv.sqlQuery("SELECT
ST_BingTileY('213')")).getField(0);
+ assertEquals(3, x);
+ assertEquals(5, y);
+
+ x = (int) first(tableEnv.sqlQuery("SELECT
ST_BingTileX('123030123010121')")).getField(0);
+ y = (int) first(tableEnv.sqlQuery("SELECT
ST_BingTileY('123030123010121')")).getField(0);
+ assertEquals(21845, x);
+ assertEquals(13506, y);
+
+ // round-trip bing_tile(x, y, zoom) = original quadkey
+ String reconstructed =
+ (String)
+ first(
+ tableEnv.sqlQuery(
+ "SELECT ST_BingTile(ST_BingTileX('123030123010121'),
ST_BingTileY('123030123010121'), ST_BingTileZoomLevel('123030123010121'))"))
+ .getField(0);
+ assertEquals("123030123010121", reconstructed);
+ }
+
+ @Test
+ public void testBingTilePolygon() {
+ Geometry result =
+ (Geometry)
+ first(tableEnv.sqlQuery("SELECT
ST_BingTilePolygon('123030123010121')")).getField(0);
+ assertNotNull(result);
+ assertTrue(result instanceof Polygon);
+ Envelope env = result.getEnvelopeInternal();
+ assertEquals(59.996337890625, env.getMinX(), 1e-10);
+ assertEquals(60.00732421875, env.getMaxX(), 1e-10);
+ assertEquals(30.11662158281937, env.getMinY(), 1e-10);
+ assertEquals(30.12612436422458, env.getMaxY(), 1e-10);
+ }
+
+ @Test
+ public void testBingTileCellIDs() {
+ // geometry_to_bing_tiles(POINT(60 30.12), 10) = ["1230301230"]
+ String[] result =
+ (String[])
+ first(
+ tableEnv.sqlQuery(
+ "SELECT ST_BingTileCellIDs(ST_GeomFromWKT('POINT (60
30.12)'), 10)"))
+ .getField(0);
+ assertArrayEquals(new String[] {"1230301230"}, result);
+
+ // geometry_to_bing_tiles(POINT(60 30.12), 15) = ["123030123010121"]
+ result =
+ (String[])
+ first(
+ tableEnv.sqlQuery(
+ "SELECT ST_BingTileCellIDs(ST_GeomFromWKT('POINT (60
30.12)'), 15)"))
+ .getField(0);
+ assertArrayEquals(new String[] {"123030123010121"}, result);
+
+ // geometry_to_bing_tiles(POLYGON((0 0, 0 10, 10 10, 10 0)), 6)
+ result =
+ (String[])
+ first(
+ tableEnv.sqlQuery(
+ "SELECT ST_BingTileCellIDs(ST_GeomFromWKT('POLYGON ((0
0, 0 10, 10 10, 10 0, 0 0))'), 6)"))
+ .getField(0);
+ assertArrayEquals(new String[] {"122220", "122222", "122221", "122223"},
result);
+
+ // POINT EMPTY → empty list
+ result =
+ (String[])
+ first(tableEnv.sqlQuery("SELECT
ST_BingTileCellIDs(ST_GeomFromWKT('POINT EMPTY'), 10)"))
+ .getField(0);
+ assertEquals(0, result.length);
+
+ // round-trip tile polygon back to the same tile
+ result =
+ (String[])
+ first(
+ tableEnv.sqlQuery(
+ "SELECT
ST_BingTileCellIDs(ST_BingTilePolygon('1230301230'), 10)"))
+ .getField(0);
+ assertArrayEquals(new String[] {"1230301230"}, result);
+ }
+
+ @Test
+ public void testBingTileToGeom() {
+ Geometry[] result =
+ (Geometry[])
+ first(tableEnv.sqlQuery("SELECT ST_BingTileToGeom(ARRAY['0', '1',
'2', '3'])"))
+ .getField(0);
+ assertEquals(4, result.length);
+ for (Geometry g : result) {
+ assertTrue(g instanceof Polygon);
+ assertEquals(5, g.getCoordinates().length);
+ }
+ }
}
diff --git a/python/sedona/spark/sql/st_functions.py
b/python/sedona/spark/sql/st_functions.py
index 34afd113b0..0a52d8c4c2 100644
--- a/python/sedona/spark/sql/st_functions.py
+++ b/python/sedona/spark/sql/st_functions.py
@@ -917,6 +917,143 @@ def ST_H3ToGeom(cells: Union[ColumnOrName, list]) ->
Column:
return _call_st_function("ST_H3ToGeom", cells)
+# =========================================================================
+# Bing Tile functions
+# =========================================================================
+
+
+@validate_argument_types
+def ST_BingTile(
+ tile_x: Union[ColumnOrName, int],
+ tile_y: Union[ColumnOrName, int],
+ zoom_level: Union[ColumnOrName, int],
+) -> Column:
+ """Create a Bing tile quadkey from XY coordinates and zoom level.
+ :param tile_x: Tile X coordinate
+ :type tile_x: Union[ColumnOrName, int]
+ :param tile_y: Tile Y coordinate
+ :type tile_y: Union[ColumnOrName, int]
+ :param zoom_level: Zoom level (1 to 23)
+ :type zoom_level: Union[ColumnOrName, int]
+ :return: Quadkey string
+ :rtype: Column
+ """
+ args = (tile_x, tile_y, zoom_level)
+ return _call_st_function("ST_BingTile", args)
+
+
+@validate_argument_types
+def ST_BingTileAt(
+ longitude: Union[ColumnOrName, float],
+ latitude: Union[ColumnOrName, float],
+ zoom_level: Union[ColumnOrName, int],
+) -> Column:
+ """Return the Bing tile quadkey containing the given point at the
specified zoom level.
+ :param longitude: Longitude (-180 to 180)
+ :type longitude: Union[ColumnOrName, float]
+ :param latitude: Latitude (-85.05112878 to 85.05112878)
+ :type latitude: Union[ColumnOrName, float]
+ :param zoom_level: Zoom level (1 to 23)
+ :type zoom_level: Union[ColumnOrName, int]
+ :return: Quadkey string
+ :rtype: Column
+ """
+ args = (longitude, latitude, zoom_level)
+ return _call_st_function("ST_BingTileAt", args)
+
+
+@validate_argument_types
+def ST_BingTilesAround(
+ longitude: Union[ColumnOrName, float],
+ latitude: Union[ColumnOrName, float],
+ zoom_level: Union[ColumnOrName, int],
+) -> Column:
+ """Return the 3x3 neighborhood of Bing tiles around the tile containing
the specified point.
+ :param longitude: Longitude
+ :type longitude: Union[ColumnOrName, float]
+ :param latitude: Latitude
+ :type latitude: Union[ColumnOrName, float]
+ :param zoom_level: Zoom level (1 to 23)
+ :type zoom_level: Union[ColumnOrName, int]
+ :return: Array of quadkey strings
+ :rtype: Column
+ """
+ args = (longitude, latitude, zoom_level)
+ return _call_st_function("ST_BingTilesAround", args)
+
+
+@validate_argument_types
+def ST_BingTileZoomLevel(quad_key: ColumnOrName) -> Column:
+ """Return the zoom level of a Bing tile quadkey.
+ :param quad_key: Quadkey string
+ :type quad_key: ColumnOrName
+ :return: Zoom level
+ :rtype: Column
+ """
+ return _call_st_function("ST_BingTileZoomLevel", quad_key)
+
+
+@validate_argument_types
+def ST_BingTileX(quad_key: ColumnOrName) -> Column:
+ """Return the X coordinate of a Bing tile from its quadkey.
+ :param quad_key: Quadkey string
+ :type quad_key: ColumnOrName
+ :return: Tile X coordinate
+ :rtype: Column
+ """
+ return _call_st_function("ST_BingTileX", quad_key)
+
+
+@validate_argument_types
+def ST_BingTileY(quad_key: ColumnOrName) -> Column:
+ """Return the Y coordinate of a Bing tile from its quadkey.
+ :param quad_key: Quadkey string
+ :type quad_key: ColumnOrName
+ :return: Tile Y coordinate
+ :rtype: Column
+ """
+ return _call_st_function("ST_BingTileY", quad_key)
+
+
+@validate_argument_types
+def ST_BingTilePolygon(quad_key: ColumnOrName) -> Column:
+ """Return the polygon representation of a Bing tile given its quadkey.
+ :param quad_key: Quadkey string
+ :type quad_key: ColumnOrName
+ :return: Tile polygon geometry
+ :rtype: Column
+ """
+ return _call_st_function("ST_BingTilePolygon", quad_key)
+
+
+@validate_argument_types
+def ST_BingTileCellIDs(
+ geometry: ColumnOrName,
+ zoom_level: Union[ColumnOrName, int],
+) -> Column:
+ """Return the minimum set of Bing tile quadkeys that fully cover a given
geometry at the specified zoom level.
+ :param geometry: Geometry to cover
+ :type geometry: ColumnOrName
+ :param zoom_level: Zoom level (1 to 23)
+ :type zoom_level: Union[ColumnOrName, int]
+ :return: Array of quadkey strings
+ :rtype: Column
+ """
+ args = (geometry, zoom_level)
+ return _call_st_function("ST_BingTileCellIDs", args)
+
+
+@validate_argument_types
+def ST_BingTileToGeom(quad_keys: Union[ColumnOrName, list]) -> Column:
+ """Convert an array of Bing tile quadkeys to their polygon geometries.
+ :param quad_keys: Array of quadkey strings
+ :type quad_keys: Union[ColumnOrName, list]
+ :return: Array of polygon geometries
+ :rtype: Column
+ """
+ return _call_st_function("ST_BingTileToGeom", quad_keys)
+
+
@validate_argument_types
def ST_InteriorRingN(polygon: ColumnOrName, n: Union[ColumnOrName, int]) ->
Column:
"""Return the index n (0-th based) interior ring of a polygon geometry
column.
diff --git
a/snowflake-tester/src/test/java/org/apache/sedona/snowflake/snowsql/TestFunctions.java
b/snowflake-tester/src/test/java/org/apache/sedona/snowflake/snowsql/TestFunctions.java
index b1f24e15f1..2c226f70ba 100644
---
a/snowflake-tester/src/test/java/org/apache/sedona/snowflake/snowsql/TestFunctions.java
+++
b/snowflake-tester/src/test/java/org/apache/sedona/snowflake/snowsql/TestFunctions.java
@@ -1403,4 +1403,64 @@ public class TestFunctions extends TestBase {
"SELECT
sedona.GeometryType(sedona.ST_ApproximateMedialAxis(sedona.ST_GeomFromWKT('POLYGON
((0 0, 10 0, 10 5, 5 5, 5 10, 0 10, 0 0))'), 100))",
"MULTILINESTRING");
}
+
+ @Test
+ public void test_ST_BingTile() {
+ registerUDF("ST_BingTile", int.class, int.class, int.class);
+ verifySqlSingleRes("select sedona.ST_BingTile(3, 5, 3)", "213");
+ }
+
+ @Test
+ public void test_ST_BingTileAt() {
+ registerUDF("ST_BingTileAt", double.class, double.class, int.class);
+ verifySqlSingleRes("select sedona.ST_BingTileAt(60, 30.12, 15)",
"123030123010121");
+ }
+
+ @Test
+ public void test_ST_BingTilesAround() {
+ registerUDF("ST_BingTilesAround", double.class, double.class, int.class);
+ verifySqlSingleRes("select ARRAY_SIZE(sedona.ST_BingTilesAround(60, 30.12,
15))", 9);
+ }
+
+ @Test
+ public void test_ST_BingTileZoomLevel() {
+ registerUDF("ST_BingTileZoomLevel", String.class);
+ verifySqlSingleRes("select sedona.ST_BingTileZoomLevel('213')", 3);
+ }
+
+ @Test
+ public void test_ST_BingTileX() {
+ registerUDF("ST_BingTileX", String.class);
+ verifySqlSingleRes("select sedona.ST_BingTileX('213')", 3);
+ }
+
+ @Test
+ public void test_ST_BingTileY() {
+ registerUDF("ST_BingTileY", String.class);
+ verifySqlSingleRes("select sedona.ST_BingTileY('213')", 5);
+ }
+
+ @Test
+ public void test_ST_BingTilePolygon() {
+ registerUDF("ST_BingTilePolygon", String.class);
+ registerUDF("GeometryType", byte[].class);
+ verifySqlSingleRes("select
sedona.GeometryType(sedona.ST_BingTilePolygon('213'))", "POLYGON");
+ }
+
+ @Test
+ public void test_ST_BingTileCellIDs() {
+ registerUDF("ST_BingTileCellIDs", byte[].class, int.class);
+ verifySqlSingleRes(
+ "select
ARRAY_SIZE(sedona.ST_BingTileCellIDs(sedona.ST_GeomFromText('POINT(60 30.12)'),
10))",
+ 1);
+ }
+
+ @Test
+ public void test_ST_BingTileToGeom() {
+ registerUDF("ST_BingTileToGeom", String[].class);
+ registerUDF("GeometryType", byte[].class);
+ verifySqlSingleRes(
+ "select
sedona.GeometryType(sedona.ST_BingTileToGeom(ARRAY_CONSTRUCT('0', '1')))",
+ "GEOMETRYCOLLECTION");
+ }
}
diff --git
a/snowflake-tester/src/test/java/org/apache/sedona/snowflake/snowsql/TestFunctionsV2.java
b/snowflake-tester/src/test/java/org/apache/sedona/snowflake/snowsql/TestFunctionsV2.java
index ae26fedad2..67bc09b187 100644
---
a/snowflake-tester/src/test/java/org/apache/sedona/snowflake/snowsql/TestFunctionsV2.java
+++
b/snowflake-tester/src/test/java/org/apache/sedona/snowflake/snowsql/TestFunctionsV2.java
@@ -1344,4 +1344,64 @@ public class TestFunctionsV2 extends TestBase {
"select
sedona.GeometryType(sedona.ST_ApproximateMedialAxis(ST_GeometryFromWKT('POLYGON
((0 0, 10 0, 10 5, 5 5, 5 10, 0 10, 0 0))'), 100))",
"MULTILINESTRING");
}
+
+ @Test
+ public void test_ST_BingTile() {
+ registerUDFV2("ST_BingTile", int.class, int.class, int.class);
+ verifySqlSingleRes("select sedona.ST_BingTile(3, 5, 3)", "213");
+ }
+
+ @Test
+ public void test_ST_BingTileAt() {
+ registerUDFV2("ST_BingTileAt", double.class, double.class, int.class);
+ verifySqlSingleRes("select sedona.ST_BingTileAt(60, 30.12, 15)",
"123030123010121");
+ }
+
+ @Test
+ public void test_ST_BingTilesAround() {
+ registerUDFV2("ST_BingTilesAround", double.class, double.class, int.class);
+ verifySqlSingleRes("select ARRAY_SIZE(sedona.ST_BingTilesAround(60, 30.12,
15))", 9);
+ }
+
+ @Test
+ public void test_ST_BingTileZoomLevel() {
+ registerUDFV2("ST_BingTileZoomLevel", String.class);
+ verifySqlSingleRes("select sedona.ST_BingTileZoomLevel('213')", 3);
+ }
+
+ @Test
+ public void test_ST_BingTileX() {
+ registerUDFV2("ST_BingTileX", String.class);
+ verifySqlSingleRes("select sedona.ST_BingTileX('213')", 3);
+ }
+
+ @Test
+ public void test_ST_BingTileY() {
+ registerUDFV2("ST_BingTileY", String.class);
+ verifySqlSingleRes("select sedona.ST_BingTileY('213')", 5);
+ }
+
+ @Test
+ public void test_ST_BingTilePolygon() {
+ registerUDFV2("ST_BingTilePolygon", String.class);
+ registerUDFV2("GeometryType", String.class);
+ verifySqlSingleRes("select
sedona.GeometryType(sedona.ST_BingTilePolygon('213'))", "POLYGON");
+ }
+
+ @Test
+ public void test_ST_BingTileCellIDs() {
+ registerUDFV2("ST_BingTileCellIDs", String.class, int.class);
+ verifySqlSingleRes(
+ "select
ARRAY_SIZE(sedona.ST_BingTileCellIDs(ST_GeometryFromWKT('POINT(60 30.12)'),
10))",
+ 1);
+ }
+
+ @Test
+ public void test_ST_BingTileToGeom() {
+ registerUDFV2("ST_BingTileToGeom", String[].class);
+ registerUDFV2("GeometryType", String.class);
+ verifySqlSingleRes(
+ "select
sedona.GeometryType(sedona.ST_BingTileToGeom(ARRAY_CONSTRUCT('0', '1')))",
+ "GEOMETRYCOLLECTION");
+ }
}
diff --git
a/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/UDFs.java
b/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/UDFs.java
index b4cde280c4..a7d98082ea 100644
--- a/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/UDFs.java
+++ b/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/UDFs.java
@@ -1418,4 +1418,52 @@ public class UDFs {
return GeometrySerde.serialize(
Functions.rotate(GeometrySerde.deserialize(geom), angle, originX,
originY));
}
+
+ // Bing Tile functions
+
+ @UDFAnnotations.ParamMeta(argNames = {"tileX", "tileY", "zoomLevel"})
+ public static String ST_BingTile(int tileX, int tileY, int zoomLevel) {
+ return Functions.bingTile(tileX, tileY, zoomLevel);
+ }
+
+ @UDFAnnotations.ParamMeta(argNames = {"longitude", "latitude", "zoomLevel"})
+ public static String ST_BingTileAt(double longitude, double latitude, int
zoomLevel) {
+ return Functions.bingTileAt(longitude, latitude, zoomLevel);
+ }
+
+ @UDFAnnotations.ParamMeta(argNames = {"longitude", "latitude", "zoomLevel"})
+ public static String[] ST_BingTilesAround(double longitude, double latitude,
int zoomLevel) {
+ return Functions.bingTilesAround(longitude, latitude, zoomLevel);
+ }
+
+ @UDFAnnotations.ParamMeta(argNames = {"quadKey"})
+ public static int ST_BingTileZoomLevel(String quadKey) {
+ return Functions.bingTileZoomLevel(quadKey);
+ }
+
+ @UDFAnnotations.ParamMeta(argNames = {"quadKey"})
+ public static int ST_BingTileX(String quadKey) {
+ return Functions.bingTileX(quadKey);
+ }
+
+ @UDFAnnotations.ParamMeta(argNames = {"quadKey"})
+ public static int ST_BingTileY(String quadKey) {
+ return Functions.bingTileY(quadKey);
+ }
+
+ @UDFAnnotations.ParamMeta(argNames = {"quadKey"})
+ public static byte[] ST_BingTilePolygon(String quadKey) {
+ return GeometrySerde.serialize(Functions.bingTilePolygon(quadKey));
+ }
+
+ @UDFAnnotations.ParamMeta(argNames = {"geometry", "zoomLevel"})
+ public static String[] ST_BingTileCellIDs(byte[] geometry, int zoomLevel) {
+ return Functions.bingTileCellIDs(GeometrySerde.deserialize(geometry),
zoomLevel);
+ }
+
+ @UDFAnnotations.ParamMeta(argNames = {"quadKeys"})
+ public static byte[] ST_BingTileToGeom(String[] quadKeys) {
+ Geometry[] geoms = Functions.bingTileToGeom(quadKeys);
+ return
GeometrySerde.serialize(GeometrySerde.GEOMETRY_FACTORY.createGeometryCollection(geoms));
+ }
}
diff --git
a/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/UDFsV2.java
b/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/UDFsV2.java
index 7cff529ed3..8fcc980d8a 100644
--- a/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/UDFsV2.java
+++ b/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/UDFsV2.java
@@ -1751,4 +1751,65 @@ public class UDFsV2 {
return GeometrySerde.serGeoJson(
Functions.rotate(GeometrySerde.deserGeoJson(geom), angle, originX,
originY));
}
+
+ // Bing Tile functions
+
+ @UDFAnnotations.ParamMeta(argNames = {"tileX", "tileY", "zoomLevel"})
+ public static String ST_BingTile(int tileX, int tileY, int zoomLevel) {
+ return Functions.bingTile(tileX, tileY, zoomLevel);
+ }
+
+ @UDFAnnotations.ParamMeta(argNames = {"longitude", "latitude", "zoomLevel"})
+ public static String ST_BingTileAt(double longitude, double latitude, int
zoomLevel) {
+ return Functions.bingTileAt(longitude, latitude, zoomLevel);
+ }
+
+ @UDFAnnotations.ParamMeta(argNames = {"longitude", "latitude", "zoomLevel"})
+ public static String[] ST_BingTilesAround(double longitude, double latitude,
int zoomLevel) {
+ return Functions.bingTilesAround(longitude, latitude, zoomLevel);
+ }
+
+ @UDFAnnotations.ParamMeta(
+ argNames = {"quadKey"},
+ argTypes = {"String"})
+ public static int ST_BingTileZoomLevel(String quadKey) {
+ return Functions.bingTileZoomLevel(quadKey);
+ }
+
+ @UDFAnnotations.ParamMeta(
+ argNames = {"quadKey"},
+ argTypes = {"String"})
+ public static int ST_BingTileX(String quadKey) {
+ return Functions.bingTileX(quadKey);
+ }
+
+ @UDFAnnotations.ParamMeta(
+ argNames = {"quadKey"},
+ argTypes = {"String"})
+ public static int ST_BingTileY(String quadKey) {
+ return Functions.bingTileY(quadKey);
+ }
+
+ @UDFAnnotations.ParamMeta(
+ argNames = {"quadKey"},
+ argTypes = {"String"},
+ returnTypes = "Geometry")
+ public static String ST_BingTilePolygon(String quadKey) {
+ return GeometrySerde.serGeoJson(Functions.bingTilePolygon(quadKey));
+ }
+
+ @UDFAnnotations.ParamMeta(
+ argNames = {"geometry", "zoomLevel"},
+ argTypes = {"Geometry", "int"})
+ public static String[] ST_BingTileCellIDs(String geometry, int zoomLevel) {
+ return Functions.bingTileCellIDs(GeometrySerde.deserGeoJson(geometry),
zoomLevel);
+ }
+
+ @UDFAnnotations.ParamMeta(
+ argNames = {"quadKeys"},
+ returnTypes = "Geometry")
+ public static String ST_BingTileToGeom(String[] quadKeys) {
+ Geometry[] geoms = Functions.bingTileToGeom(quadKeys);
+ return
GeometrySerde.serGeoJson(GeometrySerde.GEOMETRY_FACTORY.createGeometryCollection(geoms));
+ }
}
diff --git
a/spark/common/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala
b/spark/common/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala
index a2d483398a..1c20dc5577 100644
--- a/spark/common/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala
+++ b/spark/common/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala
@@ -153,6 +153,15 @@ object Catalog extends AbstractCatalog with Logging {
function[ST_H3CellIDs](),
function[ST_H3ToGeom](),
function[ST_H3KRing](),
+ function[ST_BingTile](),
+ function[ST_BingTileAt](),
+ function[ST_BingTilesAround](),
+ function[ST_BingTileZoomLevel](),
+ function[ST_BingTileX](),
+ function[ST_BingTileY](),
+ function[ST_BingTilePolygon](),
+ function[ST_BingTileCellIDs](),
+ function[ST_BingTileToGeom](),
function[ST_InteriorRingN](),
function[ST_InterpolatePoint](),
function[ST_Dump](),
diff --git
a/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala
b/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala
index da470ef6ff..31beddc720 100644
---
a/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala
+++
b/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala
@@ -1547,6 +1547,83 @@ private[apache] case class ST_H3ToGeom(inputExpressions:
Seq[Expression])
}
}
+// =========================================================================
+// Bing Tile expressions
+// =========================================================================
+
+private[apache] case class ST_BingTile(inputExpressions: Seq[Expression])
+ extends InferredExpression(Functions.bingTile _) {
+
+ protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) =
{
+ copy(inputExpressions = newChildren)
+ }
+}
+
+private[apache] case class ST_BingTileAt(inputExpressions: Seq[Expression])
+ extends InferredExpression(Functions.bingTileAt _) {
+
+ protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) =
{
+ copy(inputExpressions = newChildren)
+ }
+}
+
+private[apache] case class ST_BingTilesAround(inputExpressions:
Seq[Expression])
+ extends InferredExpression(Functions.bingTilesAround _) {
+
+ protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) =
{
+ copy(inputExpressions = newChildren)
+ }
+}
+
+private[apache] case class ST_BingTileZoomLevel(inputExpressions:
Seq[Expression])
+ extends InferredExpression(Functions.bingTileZoomLevel _) {
+
+ protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) =
{
+ copy(inputExpressions = newChildren)
+ }
+}
+
+private[apache] case class ST_BingTileX(inputExpressions: Seq[Expression])
+ extends InferredExpression(Functions.bingTileX _) {
+
+ protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) =
{
+ copy(inputExpressions = newChildren)
+ }
+}
+
+private[apache] case class ST_BingTileY(inputExpressions: Seq[Expression])
+ extends InferredExpression(Functions.bingTileY _) {
+
+ protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) =
{
+ copy(inputExpressions = newChildren)
+ }
+}
+
+private[apache] case class ST_BingTilePolygon(inputExpressions:
Seq[Expression])
+ extends InferredExpression(Functions.bingTilePolygon _) {
+
+ protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) =
{
+ copy(inputExpressions = newChildren)
+ }
+}
+
+private[apache] case class ST_BingTileCellIDs(inputExpressions:
Seq[Expression])
+ extends InferredExpression(Functions.bingTileCellIDs _) {
+
+ protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) =
{
+ copy(inputExpressions = newChildren)
+ }
+}
+
+private[apache] case class ST_BingTileToGeom(inputExpressions: Seq[Expression])
+ extends InferredExpression(Functions.bingTileToGeom _)
+ with FoldableExpression {
+
+ protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) =
{
+ copy(inputExpressions = newChildren)
+ }
+}
+
private[apache] case class ST_CollectionExtract(inputExpressions:
Seq[Expression])
extends
InferredExpression(InferrableFunction.allowRightNull(Functions.collectionExtract))
{
diff --git
a/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_functions.scala
b/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_functions.scala
index 818055b906..f79fc78be5 100644
---
a/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_functions.scala
+++
b/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_functions.scala
@@ -291,6 +291,46 @@ object st_functions {
def ST_H3ToGeom(cellIds: Array[Long]): Column =
wrapExpression[ST_H3ToGeom](cellIds)
+ // Bing Tile functions
+ def ST_BingTile(tileX: Column, tileY: Column, zoomLevel: Column): Column =
+ wrapExpression[ST_BingTile](tileX, tileY, zoomLevel)
+ def ST_BingTile(tileX: Int, tileY: Int, zoomLevel: Int): Column =
+ wrapExpression[ST_BingTile](tileX, tileY, zoomLevel)
+
+ def ST_BingTileAt(longitude: Column, latitude: Column, zoomLevel: Column):
Column =
+ wrapExpression[ST_BingTileAt](longitude, latitude, zoomLevel)
+ def ST_BingTileAt(longitude: Double, latitude: Double, zoomLevel: Int):
Column =
+ wrapExpression[ST_BingTileAt](longitude, latitude, zoomLevel)
+
+ def ST_BingTilesAround(longitude: Column, latitude: Column, zoomLevel:
Column): Column =
+ wrapExpression[ST_BingTilesAround](longitude, latitude, zoomLevel)
+ def ST_BingTilesAround(longitude: Double, latitude: Double, zoomLevel: Int):
Column =
+ wrapExpression[ST_BingTilesAround](longitude, latitude, zoomLevel)
+
+ def ST_BingTileZoomLevel(quadKey: Column): Column =
+ wrapExpression[ST_BingTileZoomLevel](quadKey)
+ def ST_BingTileZoomLevel(quadKey: String): Column =
+ wrapExpression[ST_BingTileZoomLevel](quadKey)
+
+ def ST_BingTileX(quadKey: Column): Column =
wrapExpression[ST_BingTileX](quadKey)
+ def ST_BingTileX(quadKey: String): Column =
wrapExpression[ST_BingTileX](quadKey)
+
+ def ST_BingTileY(quadKey: Column): Column =
wrapExpression[ST_BingTileY](quadKey)
+ def ST_BingTileY(quadKey: String): Column =
wrapExpression[ST_BingTileY](quadKey)
+
+ def ST_BingTilePolygon(quadKey: Column): Column =
+ wrapExpression[ST_BingTilePolygon](quadKey)
+ def ST_BingTilePolygon(quadKey: String): Column =
+ wrapExpression[ST_BingTilePolygon](quadKey)
+
+ def ST_BingTileCellIDs(geometry: Column, zoomLevel: Column): Column =
+ wrapExpression[ST_BingTileCellIDs](geometry, zoomLevel)
+ def ST_BingTileCellIDs(geometry: String, zoomLevel: Int): Column =
+ wrapExpression[ST_BingTileCellIDs](geometry, zoomLevel)
+
+ def ST_BingTileToGeom(quadKeys: Column): Column =
+ wrapExpression[ST_BingTileToGeom](quadKeys)
+
def ST_InteriorRingN(polygon: Column, n: Column): Column =
wrapExpression[ST_InteriorRingN](polygon, n)
def ST_InteriorRingN(polygon: String, n: Int): Column =
diff --git
a/spark/common/src/test/scala/org/apache/sedona/sql/dataFrameAPITestScala.scala
b/spark/common/src/test/scala/org/apache/sedona/sql/dataFrameAPITestScala.scala
index 8cdd05da15..d31da962dc 100644
---
a/spark/common/src/test/scala/org/apache/sedona/sql/dataFrameAPITestScala.scala
+++
b/spark/common/src/test/scala/org/apache/sedona/sql/dataFrameAPITestScala.scala
@@ -2659,5 +2659,112 @@ class dataFrameAPITestScala extends TestBaseScala {
val medialAxis = result.asInstanceOf[Geometry]
assert(medialAxis.getGeometryType == "MultiLineString")
}
+
+ // Bing Tile functions
+ it("Passed ST_BingTile with literal overload") {
+ val df = sparkSession.sql("SELECT 1 AS dummy").select(ST_BingTile(3, 5,
3))
+ assert(df.first().getString(0) == "213")
+ }
+
+ it("Passed ST_BingTile with Column overload") {
+ val df = sparkSession
+ .sql("SELECT 3 AS tileX, 5 AS tileY, 3 AS zoom")
+ .select(ST_BingTile(col("tileX"), col("tileY"), col("zoom")))
+ assert(df.first().getString(0) == "213")
+ }
+
+ it("Passed ST_BingTileAt with literal overload") {
+ val df = sparkSession.sql("SELECT 1 AS
dummy").select(ST_BingTileAt(60.0, 30.12, 15))
+ assert(df.first().getString(0) == "123030123010121")
+ }
+
+ it("Passed ST_BingTileAt with Column overload") {
+ val df = sparkSession
+ .sql("SELECT 60.0 AS lon, 30.12 AS lat, 15 AS zoom")
+ .select(ST_BingTileAt(col("lon"), col("lat"), col("zoom")))
+ assert(df.first().getString(0) == "123030123010121")
+ }
+
+ it("Passed ST_BingTilesAround with literal overload") {
+ val df = sparkSession.sql("SELECT 1 AS
dummy").select(ST_BingTilesAround(60.0, 30.12, 1))
+ val result = df.first().getAs[WrappedArray[String]](0)
+ assert(result == WrappedArray.make(Array("0", "2", "1", "3")))
+ }
+
+ it("Passed ST_BingTilesAround with Column overload") {
+ val df = sparkSession
+ .sql("SELECT 60.0 AS lon, 30.12 AS lat, 1 AS zoom")
+ .select(ST_BingTilesAround(col("lon"), col("lat"), col("zoom")))
+ val result = df.first().getAs[WrappedArray[String]](0)
+ assert(result == WrappedArray.make(Array("0", "2", "1", "3")))
+ }
+
+ it("Passed ST_BingTileZoomLevel with String column name") {
+ val df = sparkSession
+ .sql("SELECT '123030123010121' AS qk")
+ .select(ST_BingTileZoomLevel("qk"))
+ assert(df.first().getInt(0) == 15)
+ }
+
+ it("Passed ST_BingTileZoomLevel with Column overload") {
+ val df = sparkSession
+ .sql("SELECT '123030123010121' AS qk")
+ .select(ST_BingTileZoomLevel(col("qk")))
+ assert(df.first().getInt(0) == 15)
+ }
+
+ it("Passed ST_BingTileX with String column name") {
+ val df = sparkSession.sql("SELECT '213' AS
qk").select(ST_BingTileX("qk"))
+ assert(df.first().getInt(0) == 3)
+ }
+
+ it("Passed ST_BingTileX with Column overload") {
+ val df = sparkSession.sql("SELECT '213' AS
qk").select(ST_BingTileX(col("qk")))
+ assert(df.first().getInt(0) == 3)
+ }
+
+ it("Passed ST_BingTileY with String column name") {
+ val df = sparkSession.sql("SELECT '213' AS
qk").select(ST_BingTileY("qk"))
+ assert(df.first().getInt(0) == 5)
+ }
+
+ it("Passed ST_BingTileY with Column overload") {
+ val df = sparkSession.sql("SELECT '213' AS
qk").select(ST_BingTileY(col("qk")))
+ assert(df.first().getInt(0) == 5)
+ }
+
+ it("Passed ST_BingTilePolygon with String column name") {
+ val df = sparkSession
+ .sql("SELECT '123030123010121' AS qk")
+ .select(ST_BingTilePolygon("qk"))
+ val geom = df.first().get(0).asInstanceOf[Geometry]
+ assert(geom != null)
+ assert(geom.toText.startsWith("POLYGON"))
+ }
+
+ it("Passed ST_BingTilePolygon with Column overload") {
+ val df = sparkSession
+ .sql("SELECT '123030123010121' AS qk")
+ .select(ST_BingTilePolygon(col("qk")))
+ val geom = df.first().get(0).asInstanceOf[Geometry]
+ assert(geom != null)
+ assert(geom.toText.startsWith("POLYGON"))
+ }
+
+ it("Passed ST_BingTileCellIDs with Column overload") {
+ val df = sparkSession
+ .sql("SELECT ST_GeomFromWKT('POINT (60 30.12)') AS geom, 10 AS zoom")
+ .select(ST_BingTileCellIDs(col("geom"), col("zoom")))
+ val result = df.first().getAs[WrappedArray[String]](0)
+ assert(result == WrappedArray.make(Array("1230301230")))
+ }
+
+ it("Passed ST_BingTileToGeom with Column overload") {
+ val df = sparkSession
+ .sql("SELECT array('0', '1', '2', '3') AS qks")
+ .select(ST_BingTileToGeom(col("qks")))
+ val result = df.first().getAs[WrappedArray[Any]](0)
+ assert(result.length == 4)
+ }
}
}
diff --git
a/spark/common/src/test/scala/org/apache/sedona/sql/functions/STBingTileFunctions.scala
b/spark/common/src/test/scala/org/apache/sedona/sql/functions/STBingTileFunctions.scala
new file mode 100644
index 0000000000..ba49646a7f
--- /dev/null
+++
b/spark/common/src/test/scala/org/apache/sedona/sql/functions/STBingTileFunctions.scala
@@ -0,0 +1,254 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sedona.sql.functions
+
+import org.apache.sedona.sql.{GeometrySample, TestBaseScala}
+import org.apache.spark.sql.functions.expr
+import org.scalatest.{GivenWhenThen, Matchers}
+
+import scala.collection.mutable
+
+// Bing Tile function tests
+class STBingTileFunctions
+ extends TestBaseScala
+ with Matchers
+ with GeometrySample
+ with GivenWhenThen {
+ import sparkSession.implicits._
+
+ describe("should pass ST_BingTile") {
+
+ it("should create tile (3,5,3) as quadkey '213'") {
+ val result = sparkSession.sql("SELECT ST_BingTile(3, 5,
3)").collect()(0).getString(0)
+ result should equal("213")
+ }
+
+ it("should create tile (21845,13506,15) as quadkey '123030123010121'") {
+ val result =
+ sparkSession.sql("SELECT ST_BingTile(21845, 13506,
15)").collect()(0).getString(0)
+ result should equal("123030123010121")
+ }
+ }
+
+ describe("should pass ST_BingTileAt") {
+
+ // ST_BingTileAt(lon, lat, zoom) — longitude first
+ it("should return quadkey '123030123010121' for (lon=60, lat=30.12,
zoom=15)") {
+ val result =
+ sparkSession.sql("SELECT ST_BingTileAt(60.0, 30.12,
15)").collect()(0).getString(0)
+ result should equal("123030123010121")
+ }
+
+ it("should return tile(0,1,1) for (lon=-0.002, lat=0, zoom=1)") {
+ val qk = sparkSession.sql("SELECT ST_BingTileAt(-0.002, 0.0,
1)").collect()(0).getString(0)
+ val x = sparkSession.sql(s"SELECT
ST_BingTileX('$qk')").collect()(0).getInt(0)
+ val y = sparkSession.sql(s"SELECT
ST_BingTileY('$qk')").collect()(0).getInt(0)
+ x should equal(0)
+ y should equal(1)
+ }
+ }
+
+ describe("should pass ST_BingTilesAround") {
+
+ // ST_BingTilesAround(lon, lat, zoom) — longitude first
+ it("should return all 4 tiles at zoom 1 for (lon=60, lat=30.12)") {
+ val result = sparkSession
+ .sql("SELECT ST_BingTilesAround(60.0, 30.12, 1)")
+ .collect()(0)
+ .getAs[mutable.WrappedArray[String]](0)
+ result should equal(mutable.WrappedArray.make(Array("0", "2", "1", "3")))
+ }
+
+ it("should return 9 tiles at zoom 15 for (lon=60, lat=30.12)") {
+ val result = sparkSession
+ .sql("SELECT ST_BingTilesAround(60.0, 30.12, 15)")
+ .collect()(0)
+ .getAs[mutable.WrappedArray[String]](0)
+ result should equal(
+ mutable.WrappedArray.make(Array(
+ "123030123010102",
+ "123030123010120",
+ "123030123010122",
+ "123030123010103",
+ "123030123010121",
+ "123030123010123",
+ "123030123010112",
+ "123030123010130",
+ "123030123010132")))
+ }
+
+ it("should return 4 tiles at corner (lon=-180, lat=-85.05112878, zoom=3)")
{
+ val result = sparkSession
+ .sql("SELECT ST_BingTilesAround(-180.0, -85.05112878, 3)")
+ .collect()(0)
+ .getAs[mutable.WrappedArray[String]](0)
+ result should equal(mutable.WrappedArray.make(Array("220", "222", "221",
"223")))
+ }
+
+ it("should return 6 tiles at edge (lon=0, lat=-85.05112878, zoom=2)") {
+ val result = sparkSession
+ .sql("SELECT ST_BingTilesAround(0.0, -85.05112878, 2)")
+ .collect()(0)
+ .getAs[mutable.WrappedArray[String]](0)
+ result should equal(mutable.WrappedArray.make(Array("21", "23", "30",
"32", "31", "33")))
+ }
+ }
+
+ describe("should pass ST_BingTileZoomLevel") {
+
+ it("should return zoom level 3 for quadkey '213'") {
+ val result =
+ sparkSession.sql("SELECT
ST_BingTileZoomLevel('213')").collect()(0).getInt(0)
+ result should equal(3)
+ }
+
+ it("should return zoom level 15 for quadkey '123030123010121'") {
+ val result = sparkSession
+ .sql("SELECT ST_BingTileZoomLevel('123030123010121')")
+ .collect()(0)
+ .getInt(0)
+ result should equal(15)
+ }
+ }
+
+ describe("should pass ST_BingTileX and ST_BingTileY") {
+
+ it("should return X=3 and Y=5 for quadkey '213'") {
+ val x = sparkSession.sql("SELECT
ST_BingTileX('213')").collect()(0).getInt(0)
+ val y = sparkSession.sql("SELECT
ST_BingTileY('213')").collect()(0).getInt(0)
+ x should equal(3)
+ y should equal(5)
+ }
+
+ it("should return X=21845 and Y=13506 for quadkey '123030123010121'") {
+ val x =
+ sparkSession.sql("SELECT
ST_BingTileX('123030123010121')").collect()(0).getInt(0)
+ val y =
+ sparkSession.sql("SELECT
ST_BingTileY('123030123010121')").collect()(0).getInt(0)
+ x should equal(21845)
+ y should equal(13506)
+ }
+
+ it("should round-trip via ST_BingTile and ST_BingTileX/Y") {
+ val result = sparkSession
+ .sql("SELECT ST_BingTile(ST_BingTileX('123030123010121'),
ST_BingTileY('123030123010121'), ST_BingTileZoomLevel('123030123010121'))")
+ .collect()(0)
+ .getString(0)
+ result should equal("123030123010121")
+ }
+ }
+
+ describe("should pass ST_BingTilePolygon") {
+
+ it("should return correct envelope for quadkey '123030123010121'") {
+ val result = sparkSession
+ .sql("SELECT ST_AsText(ST_BingTilePolygon('123030123010121'))")
+ .collect()(0)
+ .getString(0)
+ result should not be null
+ result should startWith("POLYGON")
+ result should include("59.996337890625")
+ result should include("60.00732421875")
+ }
+
+ it("should return correct envelope for tile(0,0,1)") {
+ val result = sparkSession
+ .sql("SELECT ST_AsText(ST_BingTilePolygon(ST_BingTile(0, 0, 1)))")
+ .collect()(0)
+ .getString(0)
+ result should include("-180")
+ result should include("85.051128")
+ }
+ }
+
+ describe("should pass ST_BingTileCellIDs") {
+
+ it("should return one tile for a point at zoom 10") {
+ val result = sparkSession
+ .sql("SELECT ST_BingTileCellIDs(ST_GeomFromWKT('POINT (60 30.12)'),
10)")
+ .collect()(0)
+ .getAs[mutable.WrappedArray[String]](0)
+ result should equal(mutable.WrappedArray.make(Array("1230301230")))
+ }
+
+ it("should return one tile for a point at zoom 15") {
+ val result = sparkSession
+ .sql("SELECT ST_BingTileCellIDs(ST_GeomFromWKT('POINT (60 30.12)'),
15)")
+ .collect()(0)
+ .getAs[mutable.WrappedArray[String]](0)
+ result should equal(mutable.WrappedArray.make(Array("123030123010121")))
+ }
+
+ it("should return 4 tiles for a 10x10 polygon at zoom 6") {
+ val result = sparkSession
+ .sql("SELECT ST_BingTileCellIDs(ST_GeomFromWKT('POLYGON ((0 0, 0 10,
10 10, 10 0, 0 0))'), 6)")
+ .collect()(0)
+ .getAs[mutable.WrappedArray[String]](0)
+ result should equal(
+ mutable.WrappedArray.make(Array("122220", "122222", "122221",
"122223")))
+ }
+
+ // geometry_to_bing_tiles(POLYGON((10 10, -10 10, -20 -15, 10 10)), 3)
+ it("should return 3 tiles for a triangle at zoom 3") {
+ val result = sparkSession
+ .sql("SELECT ST_BingTileCellIDs(ST_GeomFromWKT('POLYGON ((10 10, -10
10, -20 -15, 10 10))'), 3)")
+ .collect()(0)
+ .getAs[mutable.WrappedArray[String]](0)
+ result should equal(mutable.WrappedArray.make(Array("033", "211",
"122")))
+ }
+
+ it("should return empty array for empty geometry") {
+ val result = sparkSession
+ .sql("SELECT ST_BingTileCellIDs(ST_GeomFromWKT('POINT EMPTY'), 10)")
+ .collect()(0)
+ .getAs[mutable.WrappedArray[String]](0)
+ result.length should equal(0)
+ }
+
+ it("should round-trip tile polygon back to the same tile") {
+ val result = sparkSession
+ .sql("SELECT ST_BingTileCellIDs(ST_BingTilePolygon('1230301230'), 10)")
+ .collect()(0)
+ .getAs[mutable.WrappedArray[String]](0)
+ result should equal(mutable.WrappedArray.make(Array("1230301230")))
+ }
+
+ it("should return 4 child tiles when expanding tile to zoom+1") {
+ val result = sparkSession
+ .sql("SELECT ST_BingTileCellIDs(ST_BingTilePolygon('1230301230'), 11)")
+ .collect()(0)
+ .getAs[mutable.WrappedArray[String]](0)
+ result should equal(
+ mutable.WrappedArray.make(
+ Array("12303012300", "12303012302", "12303012301", "12303012303")))
+ }
+ }
+
+ describe("should pass ST_BingTileToGeom") {
+
+ it("should convert quadkeys to polygon geometries") {
+ val result = sparkSession
+ .sql("SELECT ST_BingTileToGeom(array('0', '1', '2', '3'))")
+ .collect()(0)
+ .getAs[mutable.WrappedArray[Any]](0)
+ result.length should equal(4)
+ }
+ }
+
+}