Skip to content

Commit d4de471

Browse files
committed
Implement maptile geo grid hashing
Add support for map aggregation based on a very common "tile" pattern - given a zoom level, hashes are computed in the same way as tile indexes (zoom/x/y)
1 parent cf14d29 commit d4de471

File tree

5 files changed

+96
-0
lines changed

5 files changed

+96
-0
lines changed

server/src/main/java/org/elasticsearch/common/geo/GeoHashUtils.java

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
import java.util.ArrayList;
2626
import java.util.Collection;
2727

28+
import static org.elasticsearch.common.geo.GeoUtils.normalizeLat;
29+
import static org.elasticsearch.common.geo.GeoUtils.normalizeLon;
30+
2831
/**
2932
* Utilities for converting to/from the GeoHash standard
3033
*
@@ -351,4 +354,83 @@ public static final double decodeLatitude(final String geohash) {
351354
public static final double decodeLongitude(final String geohash) {
352355
return decodeLongitude(mortonEncode(geohash));
353356
}
357+
358+
359+
/* ****** Converting geopoint to Spherical Mercator as tiles ****** */
360+
361+
public static final int MAX_ZOOM = 26;
362+
363+
/**
364+
* Convert [longitude, latitude] to a hash tha combines zoom, x, and y of the tile.
365+
*/
366+
public static long geoToMapTileHash(final double longitude, final double latitude, final int zoom) {
367+
// Adapted from https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Java
368+
369+
if (zoom < 0 || zoom > MAX_ZOOM) {
370+
throw new IllegalArgumentException("zoom");
371+
}
372+
// How many tiles in X and in Y
373+
final int tiles = 1 << zoom;
374+
final double lon = normalizeLon(longitude);
375+
final double lat = normalizeLat(latitude);
376+
377+
int xtile = (int) Math.floor((lon + 180) / 360 * tiles);
378+
int ytile = (int) Math.floor(
379+
(1 - Math.log(
380+
Math.tan(Math.toRadians(lat)) + 1 / Math.cos(Math.toRadians(lat))
381+
) / Math.PI) / 2 * tiles);
382+
if (xtile < 0)
383+
xtile = 0;
384+
if (xtile >= tiles)
385+
xtile = (tiles - 1);
386+
if (ytile < 0)
387+
ytile = 0;
388+
if (ytile >= tiles)
389+
ytile = (tiles - 1);
390+
391+
// with max zoom being 26, the largest index would be 2^52 (51st..0th),
392+
// leaving 12 bits unused. Zoom cannot be >26, so it can fit into 5 bits (56th..52nd)
393+
return BitUtil.interleave(xtile, ytile) | ((long) zoom << 52);
394+
}
395+
396+
private static int[] parseMapTileHash(final long hash) {
397+
int zoom = (int) (hash >> 52);
398+
if (zoom < 0 || zoom > MAX_ZOOM) {
399+
throw new IllegalArgumentException("hash-zoom");
400+
}
401+
402+
final int tiles = 1 << zoom;
403+
// decode last 52 bits as xtile and ytile
404+
long val = hash & 0x000FFFFFFFFFFFFFL;
405+
int xtile = (int) BitUtil.deinterleave(val);
406+
int ytile = (int) BitUtil.deinterleave(val >> 1);
407+
if (xtile < 0 || ytile < 0 || xtile >= tiles || ytile >= tiles) {
408+
throw new IllegalArgumentException("hash-tile");
409+
}
410+
411+
return new int[]{zoom, xtile, ytile};
412+
}
413+
414+
public static String geoTileMapHashToKey(final long geohashAsLong) {
415+
int[] res = parseMapTileHash(geohashAsLong);
416+
return "" + res[0] + "/" + res[1] + "/" + res[2];
417+
}
418+
419+
private static double tile2lon(final int x, final double tiles) {
420+
return x / tiles * 360.0 - 180;
421+
}
422+
423+
private static double tile2lat(final int y, final double tiles) {
424+
double n = Math.PI - (2.0 * Math.PI * y) / tiles;
425+
return Math.toDegrees(Math.atan(Math.sinh(n)));
426+
}
427+
428+
public static Rectangle bboxFromTileIndex(long geohashAsLong) {
429+
int[] res = parseMapTileHash(geohashAsLong);
430+
int zoom = res[0], x = res[1], y = res[2];
431+
double tiles = Math.pow(2.0, zoom); // optimization
432+
433+
return new Rectangle(tile2lat(y, tiles), tile2lat(y + 1, tiles),
434+
tile2lon(x, tiles), tile2lon(x + 1, tiles));
435+
}
354436
}

server/src/main/java/org/elasticsearch/common/geo/GeoUtils.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,12 @@ public static int checkPrecisionRange(int precision, GeoHashType type) {
544544
case pluscode:
545545
PluscodeHash.validatePrecision(precision);
546546
break;
547+
case maptile:
548+
if (precision < 0 || precision > GeoHashUtils.MAX_ZOOM) {
549+
throw new IllegalArgumentException("Invalid geohash maptile aggregation precision of " + precision +
550+
". Must be between 0 and " + GeoHashUtils.MAX_ZOOM + ".");
551+
}
552+
break;
547553
default:
548554
throw new IllegalArgumentException("Unknown type " + type.toString());
549555
}

server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoGridAggregationBuilder.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,9 @@ public boolean advanceExact(int docId) throws IOException {
257257
case pluscode:
258258
values[i] = PluscodeHash.latLngToPluscodeHash(target.getLon(), target.getLat(), precision);
259259
break;
260+
case maptile:
261+
values[i] = GeoHashUtils.geoToMapTileHash(target.getLon(), target.getLat(), precision);
262+
break;
260263
default:
261264
throw new IllegalArgumentException();
262265
}

server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoHashType.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,5 @@
2222
public enum GeoHashType {
2323
geohash,
2424
pluscode,
25+
maptile,
2526
}

server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/InternalGeoHashGrid.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ public String getKeyAsString() {
8787
return GeoHashUtils.stringEncode(geohashAsLong);
8888
case pluscode:
8989
return PluscodeHash.decodePluscode(geohashAsLong);
90+
case maptile:
91+
return GeoHashUtils.geoTileMapHashToKey(geohashAsLong);
9092
default:
9193
throw new IllegalArgumentException();
9294
}
@@ -99,6 +101,8 @@ public Object getKey() {
99101
return GeoPoint.fromGeohash(geohashAsLong);
100102
case pluscode:
101103
return PluscodeHash.bboxFromPluscode(geohashAsLong);
104+
case maptile:
105+
return GeoHashUtils.bboxFromTileIndex(geohashAsLong);
102106
default:
103107
throw new IllegalArgumentException();
104108
}

0 commit comments

Comments
 (0)