Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add geo_shape support for geotile_grid and geohash_grid #55966

Merged
merged 11 commits into from
May 5, 2020
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,20 @@ public class Geohash {
/** Bit encoded representation of the latitude of north pole */
private static final long MAX_LAT_BITS = (0x1L << (PRECISION * 5 / 2)) - 1;

// Below code is adapted from the spatial4j library (GeohashUtils.java) Apache 2.0 Licensed
private static final double[] precisionToLatHeight, precisionToLonWidth;
static {
precisionToLatHeight = new double[PRECISION + 1];
precisionToLonWidth = new double[PRECISION + 1];
precisionToLatHeight[0] = 90*2;
precisionToLonWidth[0] = 180*2;
boolean even = false;
for(int i = 1; i <= PRECISION; i++) {
precisionToLatHeight[i] = precisionToLatHeight[i-1] / (even ? 8 : 4);
precisionToLonWidth[i] = precisionToLonWidth[i-1] / (even ? 4 : 8);
even = ! even;
}
}

// no instance:
private Geohash() {
Expand Down Expand Up @@ -97,6 +111,16 @@ public static Rectangle toBoundingBox(final String geohash) {
}
}

/** Array of geohashes one level below the baseGeohash. Sorted. */
public static String[] getSubGeohashes(String baseGeohash) {
String[] hashes = new String[BASE_32.length];
for (int i = 0; i < BASE_32.length; i++) {//note: already sorted
char c = BASE_32[i];
hashes[i] = baseGeohash+c;
}
return hashes;
}

/**
* Calculate all neighbors of a given geohash cell.
*
Expand Down Expand Up @@ -201,6 +225,13 @@ public static final String getNeighbor(String geohash, int level, int dx, int dy
}
}

/**
* Encode a string geohash to the geohash based long format (lon/lat interleaved, 4 least significant bits = level)
*/
public static final long longEncode(String hash) {
return longEncode(hash, hash.length());
}

/**
* Encode lon/lat to the geohash based long format (lon/lat interleaved, 4 least significant bits = level)
*/
Expand Down Expand Up @@ -290,6 +321,16 @@ public static long mortonEncode(final String hash) {
return BitUtil.flipFlop(l);
}

/** approximate width of geohash tile for a specific precision in degrees */
public static double lonWidthInDegrees(int precision) {
return precisionToLonWidth[precision];
}

/** approximate height of geohash tile for a specific precision in degrees */
public static double latHeightInDegrees(int precision) {
return precisionToLatHeight[precision];
}

private static long encodeLatLon(final double lat, final double lon) {
// encode lat/lon flipping the sign bit so negative ints sort before positive ints
final int latEnc = encodeLatitude(lat) ^ 0x80000000;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.elasticsearch.search.aggregations.LeafBucketCollector;
import org.elasticsearch.search.aggregations.LeafBucketCollectorBase;
import org.elasticsearch.search.aggregations.bucket.BucketsAggregator;
import org.elasticsearch.search.aggregations.support.ValuesSource;
import org.elasticsearch.search.internal.SearchContext;

import java.io.IOException;
Expand All @@ -43,10 +44,10 @@ public abstract class GeoGridAggregator<T extends InternalGeoGrid> extends Bucke

protected final int requiredSize;
protected final int shardSize;
protected final CellIdSource valuesSource;
protected final ValuesSource.Numeric valuesSource;
protected final LongHash bucketOrds;

GeoGridAggregator(String name, AggregatorFactories factories, CellIdSource valuesSource,
GeoGridAggregator(String name, AggregatorFactories factories, ValuesSource.Numeric valuesSource,
int requiredSize, int shardSize, SearchContext aggregationContext,
Aggregator parent, Map<String, Object> metadata) throws IOException {
super(name, factories, aggregationContext, parent, metadata);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import org.elasticsearch.search.aggregations.Aggregator;
import org.elasticsearch.search.aggregations.AggregatorFactories;
import org.elasticsearch.search.aggregations.support.ValuesSource;
import org.elasticsearch.search.internal.SearchContext;

import java.io.IOException;
Expand All @@ -32,9 +33,9 @@
*/
public class GeoHashGridAggregator extends GeoGridAggregator<InternalGeoHashGrid> {

GeoHashGridAggregator(String name, AggregatorFactories factories, CellIdSource valuesSource,
int requiredSize, int shardSize, SearchContext aggregationContext,
Aggregator parent, Map<String, Object> metadata) throws IOException {
public GeoHashGridAggregator(String name, AggregatorFactories factories, ValuesSource.Numeric valuesSource,
int requiredSize, int shardSize, SearchContext aggregationContext,
Aggregator parent, Map<String, Object> metadata) throws IOException {
super(name, factories, valuesSource, requiredSize, shardSize, aggregationContext, parent, metadata);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import org.elasticsearch.search.aggregations.Aggregator;
import org.elasticsearch.search.aggregations.AggregatorFactories;
import org.elasticsearch.search.aggregations.support.ValuesSource;
import org.elasticsearch.search.internal.SearchContext;

import java.io.IOException;
Expand All @@ -33,9 +34,9 @@
*/
public class GeoTileGridAggregator extends GeoGridAggregator<InternalGeoTileGrid> {

GeoTileGridAggregator(String name, AggregatorFactories factories, CellIdSource valuesSource,
int requiredSize, int shardSize, SearchContext aggregationContext,
Aggregator parent, Map<String, Object> metadata) throws IOException {
public GeoTileGridAggregator(String name, AggregatorFactories factories, ValuesSource.Numeric valuesSource,
int requiredSize, int shardSize, SearchContext aggregationContext,
Aggregator parent, Map<String, Object> metadata) throws IOException {
super(name, factories, valuesSource, requiredSize, shardSize, aggregationContext, parent, metadata);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,15 @@
*/
package org.elasticsearch.search.aggregations.bucket.geogrid;

import org.apache.lucene.geo.GeoEncodingUtils;
import org.apache.lucene.util.SloppyMath;
import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.util.ESSloppyMath;
import org.elasticsearch.common.xcontent.ObjectParser.ValueType;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.support.XContentMapValues;
import org.elasticsearch.geometry.Rectangle;

import java.io.IOException;
import java.util.Locale;
Expand All @@ -43,6 +46,8 @@ public final class GeoTileUtils {

private GeoTileUtils() {}

private static final double PI_DIV_2 = Math.PI / 2;

/**
* Largest number of tiles (precision) to use.
* This value cannot be more than (64-5)/2 = 29, because 5 bits are used for zoom level itself (0-31)
Expand All @@ -53,6 +58,18 @@ private GeoTileUtils() {}
*/
public static final int MAX_ZOOM = 29;

/**
* The geo-tile map is clipped at 85.05112878 to 90 and -85.05112878 to -90
*/
public static final double LATITUDE_MASK = 85.0511287798066;

/**
* Since shapes are encoded, their boundaries are to be compared to against the encoded/decoded values of <code>LATITUDE_MASK</code>
*/
public static final double NORMALIZED_LATITUDE_MASK = GeoEncodingUtils.decodeLatitude(GeoEncodingUtils.encodeLatitude(LATITUDE_MASK));
public static final double NORMALIZED_NEGATIVE_LATITUDE_MASK =
GeoEncodingUtils.decodeLatitude(GeoEncodingUtils.encodeLatitude(-LATITUDE_MASK));

/**
* Bit position of the zoom value within hash - zoom is stored in the most significant 6 bits of a long number.
*/
Expand All @@ -63,6 +80,7 @@ private GeoTileUtils() {}
*/
private static final long X_Y_VALUE_MASK = (1L << MAX_ZOOM) - 1;


/**
* Parse an integer precision (zoom level). The {@link ValueType#INT} allows it to be a number or a string.
*
Expand Down Expand Up @@ -90,37 +108,65 @@ public static int checkPrecisionRange(int precision) {
}

/**
* Encode lon/lat to the geotile based long format.
* The resulting hash contains interleaved tile X and Y coordinates.
* The precision itself is also encoded as a few high bits.
* Calculates the x-coordinate in the tile grid for the specified longitude given
* the number of tile columns for a pre-determined zoom-level.
*
* @param longitude the longitude to use when determining the tile x-coordinate
* @param tiles the number of tiles per row for a pre-determined zoom-level
*/
public static long longEncode(double longitude, double latitude, int precision) {
// Mathematics for this code was adapted from https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Java

// Number of tiles for the current zoom level along the X and Y axis
final long tiles = 1 << checkPrecisionRange(precision);

long xTile = (long) Math.floor((normalizeLon(longitude) + 180) / 360 * tiles);
public static int getXTile(double longitude, long tiles) {
// normalizeLon treats this as 180, which is not friendly for tile mapping
if (longitude == -180) {
return 0;
}

double latSin = Math.sin(Math.toRadians(normalizeLat(latitude)));
long yTile = (long) Math.floor((0.5 - (Math.log((1 + latSin) / (1 - latSin)) / (4 * Math.PI))) * tiles);
int xTile = (int) Math.floor((normalizeLon(longitude) + 180) / 360 * tiles);

// Edge values may generate invalid values, and need to be clipped.
// For example, polar regions (above/below lat 85.05112878) get normalized.
if (xTile < 0) {
xTile = 0;
return 0;
}
if (xTile >= tiles) {
xTile = tiles - 1;
return (int) tiles - 1;
}

return xTile;
}

/**
* Calculates the y-coordinate in the tile grid for the specified longitude given
* the number of tile rows for pre-determined zoom-level.
*
* @param latitude the latitude to use when determining the tile y-coordinate
* @param tiles the number of tiles per column for a pre-determined zoom-level
*/
public static int getYTile(double latitude, long tiles) {
double latSin = SloppyMath.cos(PI_DIV_2 - Math.toRadians(normalizeLat(latitude)));
int yTile = (int) Math.floor((0.5 - (Math.log((1 + latSin) / (1 - latSin)) / (4 * Math.PI))) * tiles);

if (yTile < 0) {
yTile = 0;
}
if (yTile >= tiles) {
yTile = tiles - 1;
return (int) tiles - 1;
}

return longEncode((long) precision, xTile, yTile);
return yTile;
}

/**
* Encode lon/lat to the geotile based long format.
* The resulting hash contains interleaved tile X and Y coordinates.
* The precision itself is also encoded as a few high bits.
*/
public static long longEncode(double longitude, double latitude, int precision) {
// Mathematics for this code was adapted from https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Java
// Number of tiles for the current zoom level along the X and Y axis
final long tiles = 1 << checkPrecisionRange(precision);
long xTile = getXTile(longitude, tiles);
long yTile = getYTile(latitude, tiles);
return longEncodeTiles(precision, xTile, yTile);
}

/**
Expand All @@ -131,7 +177,14 @@ public static long longEncode(double longitude, double latitude, int precision)
*/
public static long longEncode(String hashAsString) {
int[] parsed = parseHash(hashAsString);
return longEncode((long)parsed[0], (long)parsed[1], (long)parsed[2]);
return longEncode((long) parsed[0], (long) parsed[1], (long) parsed[2]);
}

public static long longEncodeTiles(int precision, long xTile, long yTile) {
// Zoom value is placed in front of all the bits used for the geotile
// e.g. when max zoom is 29, the largest index would use 58 bits (57th..0th),
// leaving 5 bits unused for zoom. See MAX_ZOOM comment above.
return ((long) precision << ZOOM_SHIFT) | (xTile << MAX_ZOOM) | yTile;
}

/**
Expand Down Expand Up @@ -193,6 +246,23 @@ static GeoPoint keyToGeoPoint(String hashAsString) {
return zxyToGeoPoint(hashAsInts[0], hashAsInts[1], hashAsInts[2]);
}

public static Rectangle toBoundingBox(long hash) {
int[] hashAsInts = parseHash(hash);
return toBoundingBox(hashAsInts[1], hashAsInts[2], hashAsInts[0]);
}

public static Rectangle toBoundingBox(int xTile, int yTile, int precision) {
final double tiles = validateZXY(precision, xTile, yTile);
final double minN = Math.PI - (2.0 * Math.PI * (yTile + 1)) / tiles;
final double maxN = Math.PI - (2.0 * Math.PI * (yTile)) / tiles;
final double minY = Math.toDegrees(ESSloppyMath.atan(ESSloppyMath.sinh(minN)));
final double minX = ((xTile) / tiles * 360.0) - 180;
final double maxY = Math.toDegrees(ESSloppyMath.atan(ESSloppyMath.sinh(maxN)));
final double maxX = ((xTile + 1) / tiles * 360.0) - 180;

return new Rectangle(minX, maxX, maxY, minY);
}

/**
* Validates Zoom, X, and Y values, and returns the total number of allowed tiles along the x/y axis.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
package org.elasticsearch.search.aggregations.bucket.geogrid;

import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.geometry.Rectangle;
import org.elasticsearch.test.ESTestCase;

import static org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileUtils.MAX_ZOOM;
Expand All @@ -28,8 +29,10 @@
import static org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileUtils.keyToGeoPoint;
import static org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileUtils.longEncode;
import static org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileUtils.stringEncode;
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.closeTo;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;

public class GeoTileUtilsTests extends ESTestCase {

Expand Down Expand Up @@ -219,8 +222,8 @@ public void testGeoTileAsLongRoutines() {
* so ensure they are clipped correctly.
*/
public void testSingularityAtPoles() {
double minLat = -85.05112878;
double maxLat = 85.05112878;
double minLat = -GeoTileUtils.LATITUDE_MASK;
double maxLat = GeoTileUtils.LATITUDE_MASK;
double lon = randomIntBetween(-180, 180);
double lat = randomBoolean()
? randomDoubleBetween(-90, minLat, true)
Expand All @@ -231,4 +234,23 @@ public void testSingularityAtPoles() {
String clippedTileIndex = stringEncode(longEncode(lon, clippedLat, zoom));
assertEquals(tileIndex, clippedTileIndex);
}

public void testPointToTile() {
int zoom = randomIntBetween(0, MAX_ZOOM);
int tiles = 1 << zoom;
int xTile = randomIntBetween(0, zoom);
int yTile = randomIntBetween(0, zoom);
Rectangle rectangle = GeoTileUtils.toBoundingBox(xTile, yTile, zoom);
// check corners
assertThat(GeoTileUtils.getXTile(rectangle.getMinX(), tiles), equalTo(xTile));
assertThat(GeoTileUtils.getXTile(rectangle.getMaxX(), tiles), equalTo(Math.min(tiles - 1, xTile + 1)));
assertThat(GeoTileUtils.getYTile(rectangle.getMaxY(), tiles), anyOf(equalTo(yTile - 1), equalTo(yTile)));
assertThat(GeoTileUtils.getYTile(rectangle.getMinY(), tiles), anyOf(equalTo(yTile + 1), equalTo(yTile)));
// check point inside
double x = randomDoubleBetween(rectangle.getMinX(), rectangle.getMaxX(), false);
double y = randomDoubleBetween(rectangle.getMinY(), rectangle.getMaxY(), false);
assertThat(GeoTileUtils.getXTile(x, tiles), equalTo(xTile));
assertThat(GeoTileUtils.getYTile(y, tiles), equalTo(yTile));

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ public enum Feature {

SPATIAL_GEO_CENTROID(OperationMode.GOLD, true),

SPATIAL_GEO_GRID(OperationMode.GOLD, true),

ANALYTICS(OperationMode.MISSING, true);

final OperationMode minimumOperationMode;
Expand Down
Loading