Skip to content

Commit 2c99c16

Browse files
committed
Reimplemented based on reorg PR
1 parent d29cff2 commit 2c99c16

File tree

5 files changed

+180
-1
lines changed

5 files changed

+180
-1
lines changed
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.elasticsearch.common.geo;
20+
21+
import org.apache.lucene.geo.Rectangle;
22+
import org.apache.lucene.util.BitUtil;
23+
24+
import static org.elasticsearch.common.geo.GeoUtils.normalizeLat;
25+
import static org.elasticsearch.common.geo.GeoUtils.normalizeLon;
26+
27+
/**
28+
* Implements quad key hashing, same as used by map tiles.
29+
* The string key is formatted as "zoom/x/y"
30+
* The hash value (long) contains all three of those values.
31+
*/
32+
public class QuadKeyHash {
33+
34+
/**
35+
* Largest number of tiles (precision) to use.
36+
* This value cannot be more than (64-5)/2 = 29, because 5 bits are used for zoom level itself
37+
* If zoom is not stored inside hash, it would be possible to use up to 32
38+
*/
39+
private static final int MAX_ZOOM = 29;
40+
41+
/** Maximum number of bits used by quadkey in a hash */
42+
private static final int BITS_IN_HASH = MAX_ZOOM * 2;
43+
44+
/** Maximum number of bits used by quadkey in a hash */
45+
private static final long QUADKEY_MASK = (1L << BITS_IN_HASH) - 1;
46+
47+
/**
48+
* Convert [longitude, latitude] to a hash tha combines zoom, x, and y of the tile.
49+
*/
50+
public static long geoToHash(final double longitude, final double latitude, final int zoom) {
51+
// Adapted from https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Java
52+
53+
// How many tiles in X and in Y
54+
final int tiles = 1 << validatePrecision(zoom);
55+
final double lon = normalizeLon(longitude);
56+
final double lat = normalizeLat(latitude);
57+
58+
int xtile = (int) Math.floor((lon + 180) / 360 * tiles);
59+
int ytile = (int) Math.floor(
60+
(1 - Math.log(
61+
Math.tan(Math.toRadians(lat)) + 1 / Math.cos(Math.toRadians(lat))
62+
) / Math.PI) / 2 * tiles);
63+
if (xtile < 0)
64+
xtile = 0;
65+
if (xtile >= tiles)
66+
xtile = (tiles - 1);
67+
if (ytile < 0)
68+
ytile = 0;
69+
if (ytile >= tiles)
70+
ytile = (tiles - 1);
71+
72+
// Zoom value is placed in front of all the bits used for the quadkey
73+
// e.g. if max zoom is 26, the largest index would use 52 bits (51st..0th),
74+
// leaving 12 bits unused. Zoom cannot be >26, so it can fit into 5 bits (56th..52nd)
75+
return BitUtil.interleave(xtile, ytile) | ((long) zoom << BITS_IN_HASH);
76+
}
77+
78+
private static int[] parseHash(final long hash) {
79+
final int zoom = validatePrecision((int) (hash >>> BITS_IN_HASH));
80+
final int tiles = 1 << zoom;
81+
82+
// decode the quadkey bits as interleaved xtile and ytile
83+
long val = hash & QUADKEY_MASK;
84+
int xtile = (int) BitUtil.deinterleave(val);
85+
int ytile = (int) BitUtil.deinterleave(val >>> 1);
86+
if (xtile < 0 || ytile < 0 || xtile >= tiles || ytile >= tiles) {
87+
throw new IllegalArgumentException("hash-tile");
88+
}
89+
90+
return new int[]{zoom, xtile, ytile};
91+
}
92+
93+
public static String hashToKey(final long geohashAsLong) {
94+
int[] res = parseHash(geohashAsLong);
95+
return "" + res[0] + "/" + res[1] + "/" + res[2];
96+
}
97+
98+
private static double tile2lon(final int x, final double tiles) {
99+
return x / tiles * 360.0 - 180;
100+
}
101+
102+
private static double tile2lat(final int y, final double tiles) {
103+
double n = Math.PI - (2.0 * Math.PI * y) / tiles;
104+
return Math.toDegrees(Math.atan(Math.sinh(n)));
105+
}
106+
107+
public static Rectangle bboxFromTileIndex(long geohashAsLong) {
108+
int[] res = parseHash(geohashAsLong);
109+
int zoom = res[0], x = res[1], y = res[2];
110+
double tiles = Math.pow(2.0, zoom); // optimization
111+
112+
return new Rectangle(
113+
tile2lat(y, tiles), tile2lat(y + 1, tiles),
114+
tile2lon(x, tiles), tile2lon(x + 1, tiles));
115+
}
116+
117+
/**
118+
* Validate precision parameter
119+
* @param precision as submitted by the user
120+
*/
121+
private static int validatePrecision(int precision) {
122+
if (precision < 0 || precision > MAX_ZOOM) {
123+
throw new IllegalArgumentException("Invalid geohash quadkey aggregation precision of " +
124+
precision + ". Must be between 0 and " + MAX_ZOOM + ".");
125+
}
126+
return precision;
127+
}
128+
129+
/**
130+
* Attempt to parse precision string into an integer value
131+
*/
132+
public static int parsePrecisionString(String precision) {
133+
try {
134+
// we want to treat simple integer strings as precision levels, not distances
135+
return validatePrecision(Integer.parseInt(precision));
136+
// Do not catch IllegalArgumentException here
137+
} catch (NumberFormatException e) {
138+
// try to parse as a distance value
139+
final int parsedPrecision = GeoUtils.quadTreeLevelsForPrecision(precision);
140+
try {
141+
return validatePrecision(parsedPrecision);
142+
} catch (IllegalArgumentException e2) {
143+
// this happens when distance too small, so precision > .
144+
// We'd like to see the original string
145+
throw new IllegalArgumentException("precision too high [" + precision + "]", e2);
146+
}
147+
}
148+
}
149+
150+
private QuadKeyHash() {
151+
}
152+
153+
}

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.elasticsearch.common.geo.GeoHashUtils;
2626
import org.elasticsearch.common.geo.GeoPoint;
2727
import org.elasticsearch.common.geo.GeoUtils;
28+
import org.elasticsearch.common.geo.QuadKeyHash;
2829
import org.elasticsearch.common.io.stream.StreamInput;
2930
import org.elasticsearch.common.io.stream.StreamOutput;
3031
import org.elasticsearch.common.xcontent.ObjectParser;
@@ -196,6 +197,13 @@ public int shardSize() {
196197
precision = 5;
197198
}
198199
break;
200+
case QUADKEY:
201+
if (this.precision != null) {
202+
precision = QuadKeyHash.parsePrecisionString(this.precision);
203+
} else {
204+
precision = 5;
205+
}
206+
break;
199207
default:
200208
throw new IllegalArgumentException("Unknown type " + type.toString());
201209
}
@@ -283,6 +291,9 @@ public boolean advanceExact(int docId) throws IOException {
283291
case GEOHASH:
284292
values[i] = GeoHashUtils.longEncode(target.getLon(), target.getLat(), precision);
285293
break;
294+
case QUADKEY:
295+
values[i] = QuadKeyHash.geoToHash(target.getLon(), target.getLat(), precision);
296+
break;
286297
default:
287298
throw new IllegalArgumentException();
288299
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
import java.util.Locale;
2828

2929
public enum GeoHashType implements Writeable {
30-
GEOHASH;
30+
GEOHASH,
31+
QUADKEY;
3132

3233
/**
3334
* Case-insensitive from string method.

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.apache.lucene.util.PriorityQueue;
2222
import org.elasticsearch.common.geo.GeoHashUtils;
2323
import org.elasticsearch.common.geo.GeoPoint;
24+
import org.elasticsearch.common.geo.QuadKeyHash;
2425
import org.elasticsearch.common.io.stream.StreamInput;
2526
import org.elasticsearch.common.io.stream.StreamOutput;
2627
import org.elasticsearch.common.util.LongObjectPagedHashMap;
@@ -84,6 +85,8 @@ public String getKeyAsString() {
8485
switch(type) {
8586
case GEOHASH:
8687
return GeoHashUtils.stringEncode(geohashAsLong);
88+
case QUADKEY:
89+
return QuadKeyHash.hashToKey(geohashAsLong);
8790
default:
8891
throw new IllegalArgumentException();
8992
}
@@ -94,6 +97,8 @@ public Object getKey() {
9497
switch(type) {
9598
case GEOHASH:
9699
return GeoPoint.fromGeohash(geohashAsLong);
100+
case QUADKEY:
101+
return QuadKeyHash.bboxFromTileIndex(geohashAsLong);
97102
default:
98103
throw new IllegalArgumentException();
99104
}

server/src/test/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoHashGridAggregatorTests.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.apache.lucene.search.Query;
2828
import org.apache.lucene.store.Directory;
2929
import org.elasticsearch.common.CheckedConsumer;
30+
import org.elasticsearch.common.geo.QuadKeyHash;
3031
import org.elasticsearch.index.mapper.GeoPointFieldMapper;
3132
import org.elasticsearch.index.mapper.MappedFieldType;
3233
import org.elasticsearch.search.aggregations.Aggregator;
@@ -72,6 +73,14 @@ public void testHashcodeWithSeveralDocs() throws IOException {
7273
});
7374
}
7475

76+
public void testMaptileWithSeveralDocs() throws IOException {
77+
final int precision = randomIntBetween(0, 26);
78+
79+
testWithSeveralDocs(GeoHashType.QUADKEY, precision, (lng, lat) -> {
80+
return QuadKeyHash.hashToKey(QuadKeyHash.geoToHash(lng, lat, precision));
81+
});
82+
}
83+
7584
private void testWithSeveralDocs(GeoHashType type, int precision, BiFunction<Double, Double, String> hasher)
7685
throws IOException {
7786
int numPoints = randomIntBetween(8, 128);

0 commit comments

Comments
 (0)