Skip to content

Commit b169939

Browse files
committed
quadtile implementation
1 parent 8fd5915 commit b169939

File tree

6 files changed

+172
-1
lines changed

6 files changed

+172
-1
lines changed
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
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.util.BitUtil;
22+
import org.elasticsearch.search.aggregations.bucket.geogrid.GeoHashTypeProvider;
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 implements GeoHashTypeProvider {
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+
* Another consideration is that index optimizes lat/lng storage, loosing some precision.
39+
* E.g. hash lng=140.74779717298918D lat=45.61884022447444D == "18/233561/93659", but shown as "18/233561/93658"
40+
*/
41+
public static final int MAX_ZOOM = 29;
42+
43+
/**
44+
* Bit position of the zoom value within hash. Must be >= 2*MAX_ZOOM
45+
* Keeping it at a constant place allows MAX_ZOOM to be increased
46+
* without breaking serialization binary compatibility
47+
* (still, the newer version should not use higher MAX_ZOOM in the mixed cases)
48+
*/
49+
private static final int ZOOM_SHIFT = 29 * 2;
50+
51+
/**
52+
* Mask of all the bits used by the quadkey in a hash
53+
*/
54+
private static final long QUADKEY_MASK = (1L << ZOOM_SHIFT) - 1;
55+
56+
private static int validatePrecisionInt(int precision) {
57+
if (precision < 0 || precision > MAX_ZOOM) {
58+
throw new IllegalArgumentException("Invalid geohash quadkey aggregation precision of " +
59+
precision + ". Must be between 0 and " + MAX_ZOOM + ".");
60+
}
61+
return precision;
62+
}
63+
64+
private static int[] parseHash(final long hash) {
65+
final int zoom = validatePrecisionInt((int) (hash >>> ZOOM_SHIFT));
66+
final int tiles = 1 << zoom;
67+
68+
// decode the quadkey bits as interleaved xtile and ytile
69+
long val = hash & QUADKEY_MASK;
70+
int xtile = (int) BitUtil.deinterleave(val);
71+
int ytile = (int) BitUtil.deinterleave(val >>> 1);
72+
if (xtile < 0 || ytile < 0 || xtile >= tiles || ytile >= tiles) {
73+
throw new IllegalArgumentException("hash-tile");
74+
}
75+
76+
return new int[]{zoom, xtile, ytile};
77+
}
78+
79+
private static double tile2lon(final double x, final double tiles) {
80+
return x / tiles * 360.0 - 180;
81+
}
82+
83+
private static double tile2lat(final double y, final double tiles) {
84+
double n = Math.PI - (2.0 * Math.PI * y) / tiles;
85+
return Math.toDegrees(Math.atan(Math.sinh(n)));
86+
}
87+
88+
@Override
89+
public int getDefaultPrecision() {
90+
return 5;
91+
}
92+
93+
@Override
94+
public int parsePrecisionString(String precision) {
95+
try {
96+
// we want to treat simple integer strings as precision levels, not distances
97+
return validatePrecision(Integer.parseInt(precision));
98+
// Do not catch IllegalArgumentException here
99+
} catch (NumberFormatException e) {
100+
// try to parse as a distance value
101+
final int parsedPrecision = GeoUtils.quadTreeLevelsForPrecision(precision);
102+
try {
103+
return validatePrecision(parsedPrecision);
104+
} catch (IllegalArgumentException e2) {
105+
// this happens when distance too small, so precision > .
106+
// We'd like to see the original string
107+
throw new IllegalArgumentException("precision too high [" + precision + "]", e2);
108+
}
109+
}
110+
}
111+
112+
@Override
113+
public int validatePrecision(int precision) {
114+
return validatePrecisionInt(precision);
115+
}
116+
117+
@Override
118+
public long calculateHash(double longitude, double latitude, int precision) {
119+
// Adapted from https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Java
120+
121+
// How many tiles in X and in Y
122+
final int tiles = 1 << validatePrecision(precision);
123+
final double lon = normalizeLon(longitude);
124+
final double lat = normalizeLat(latitude);
125+
126+
int xtile = (int) Math.floor((lon + 180) / 360 * tiles);
127+
int ytile = (int) Math.floor(
128+
(1 - Math.log(
129+
Math.tan(Math.toRadians(lat)) + 1 / Math.cos(Math.toRadians(lat))
130+
) / Math.PI) / 2 * tiles);
131+
if (xtile < 0)
132+
xtile = 0;
133+
if (xtile >= tiles)
134+
xtile = (tiles - 1);
135+
if (ytile < 0)
136+
ytile = 0;
137+
if (ytile >= tiles)
138+
ytile = (tiles - 1);
139+
140+
// Zoom value is placed in front of all the bits used for the quadkey
141+
// e.g. if max zoom is 26, the largest index would use 52 bits (51st..0th),
142+
// leaving 12 bits unused for zoom. See MAX_ZOOM comment above.
143+
return BitUtil.interleave(xtile, ytile) | ((long) precision << ZOOM_SHIFT);
144+
}
145+
146+
@Override
147+
public String hashAsString(long hash) {
148+
int[] res = parseHash(hash);
149+
return "" + res[0] + "/" + res[1] + "/" + res[2];
150+
}
151+
152+
@Override
153+
public GeoPoint hashAsObject(long hash) {
154+
int[] res = parseHash(hash);
155+
double tiles = Math.pow(2.0, res[0]);
156+
return new GeoPoint(tile2lat(res[2] + 0.5, tiles), tile2lon(res[1] + 0.5, tiles));
157+
}
158+
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
package org.elasticsearch.search.aggregations.bucket.geogrid;
2121

2222
import org.elasticsearch.Version;
23+
import org.elasticsearch.common.geo.QuadKeyHash;
2324
import org.elasticsearch.common.io.stream.StreamInput;
2425
import org.elasticsearch.common.io.stream.StreamOutput;
2526
import org.elasticsearch.common.io.stream.Writeable;
@@ -28,7 +29,8 @@
2829
import java.util.Locale;
2930

3031
public enum GeoHashType implements Writeable {
31-
GEOHASH(new GeoHashHandler());
32+
GEOHASH(new GeoHashHandler()),
33+
QUADKEY(new QuadKeyHash());
3234

3335
public static final GeoHashType DEFAULT = GEOHASH;
3436

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

Lines changed: 1 addition & 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;

server/src/test/java/org/elasticsearch/search/aggregations/bucket/GeoHashGridTests.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
package org.elasticsearch.search.aggregations.bucket;
2121

22+
import org.elasticsearch.common.geo.QuadKeyHash;
2223
import org.elasticsearch.search.aggregations.BaseAggregationTestCase;
2324
import org.elasticsearch.search.aggregations.bucket.geogrid.GeoGridAggregationBuilder;
2425
import org.elasticsearch.search.aggregations.bucket.geogrid.GeoHashType;
@@ -41,6 +42,9 @@ public static int randomPrecision(final GeoHashType type) {
4142
case GEOHASH:
4243
precision = randomIntBetween(1, 12);
4344
break;
45+
case QUADKEY:
46+
precision = randomIntBetween(0, QuadKeyHash.MAX_ZOOM);
47+
break;
4448
default:
4549
throw new IllegalArgumentException("GeoHashType." + type.name() + " was not added to the test");
4650
}
@@ -51,6 +55,8 @@ public static int maxPrecision(GeoHashType type) {
5155
switch (type) {
5256
case GEOHASH:
5357
return 12;
58+
case QUADKEY:
59+
return QuadKeyHash.MAX_ZOOM;
5460
default:
5561
throw new IllegalArgumentException("GeoHashType." + type.name() + " was not added to the test");
5662
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.apache.lucene.search.Query;
2929
import org.apache.lucene.store.Directory;
3030
import org.elasticsearch.common.CheckedConsumer;
31+
import org.elasticsearch.common.geo.QuadKeyHash;
3132
import org.elasticsearch.index.mapper.GeoPointFieldMapper;
3233
import org.elasticsearch.index.mapper.MappedFieldType;
3334
import org.elasticsearch.search.aggregations.Aggregator;

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,9 @@ public void testParseErrorOnPrecisionOutOfRange() throws Exception {
127127
case GEOHASH:
128128
expectedMsg = "Invalid geohash aggregation precision of 13. Must be between 1 and 12.";
129129
break;
130+
case QUADKEY:
131+
expectedMsg = "Invalid geohash quadkey aggregation precision of 30. Must be between 0 and 29.";
132+
break;
130133
default:
131134
throw new IllegalArgumentException("GeoHashType." + type.name() + " was not added to the test");
132135
}

0 commit comments

Comments
 (0)