18
18
*/
19
19
package org .elasticsearch .search .aggregations .bucket .geogrid ;
20
20
21
+ import org .apache .lucene .geo .GeoEncodingUtils ;
22
+ import org .apache .lucene .util .SloppyMath ;
21
23
import org .elasticsearch .ElasticsearchParseException ;
22
24
import org .elasticsearch .common .geo .GeoPoint ;
23
25
import org .elasticsearch .common .util .ESSloppyMath ;
24
26
import org .elasticsearch .common .xcontent .ObjectParser .ValueType ;
25
27
import org .elasticsearch .common .xcontent .XContentParser ;
26
28
import org .elasticsearch .common .xcontent .support .XContentMapValues ;
29
+ import org .elasticsearch .geometry .Rectangle ;
27
30
28
31
import java .io .IOException ;
29
32
import java .util .Locale ;
@@ -43,6 +46,8 @@ public final class GeoTileUtils {
43
46
44
47
private GeoTileUtils () {}
45
48
49
+ private static final double PI_DIV_2 = Math .PI / 2 ;
50
+
46
51
/**
47
52
* Largest number of tiles (precision) to use.
48
53
* This value cannot be more than (64-5)/2 = 29, because 5 bits are used for zoom level itself (0-31)
@@ -53,6 +58,18 @@ private GeoTileUtils() {}
53
58
*/
54
59
public static final int MAX_ZOOM = 29 ;
55
60
61
+ /**
62
+ * The geo-tile map is clipped at 85.05112878 to 90 and -85.05112878 to -90
63
+ */
64
+ public static final double LATITUDE_MASK = 85.0511287798066 ;
65
+
66
+ /**
67
+ * Since shapes are encoded, their boundaries are to be compared to against the encoded/decoded values of <code>LATITUDE_MASK</code>
68
+ */
69
+ public static final double NORMALIZED_LATITUDE_MASK = GeoEncodingUtils .decodeLatitude (GeoEncodingUtils .encodeLatitude (LATITUDE_MASK ));
70
+ public static final double NORMALIZED_NEGATIVE_LATITUDE_MASK =
71
+ GeoEncodingUtils .decodeLatitude (GeoEncodingUtils .encodeLatitude (-LATITUDE_MASK ));
72
+
56
73
/**
57
74
* Bit position of the zoom value within hash - zoom is stored in the most significant 6 bits of a long number.
58
75
*/
@@ -63,6 +80,7 @@ private GeoTileUtils() {}
63
80
*/
64
81
private static final long X_Y_VALUE_MASK = (1L << MAX_ZOOM ) - 1 ;
65
82
83
+
66
84
/**
67
85
* Parse an integer precision (zoom level). The {@link ValueType#INT} allows it to be a number or a string.
68
86
*
@@ -90,37 +108,65 @@ public static int checkPrecisionRange(int precision) {
90
108
}
91
109
92
110
/**
93
- * Encode lon/lat to the geotile based long format.
94
- * The resulting hash contains interleaved tile X and Y coordinates.
95
- * The precision itself is also encoded as a few high bits.
111
+ * Calculates the x-coordinate in the tile grid for the specified longitude given
112
+ * the number of tile columns for a pre-determined zoom-level.
113
+ *
114
+ * @param longitude the longitude to use when determining the tile x-coordinate
115
+ * @param tiles the number of tiles per row for a pre-determined zoom-level
96
116
*/
97
- public static long longEncode (double longitude , double latitude , int precision ) {
98
- // Mathematics for this code was adapted from https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Java
99
-
100
- // Number of tiles for the current zoom level along the X and Y axis
101
- final long tiles = 1 << checkPrecisionRange (precision );
102
-
103
- long xTile = (long ) Math .floor ((normalizeLon (longitude ) + 180 ) / 360 * tiles );
117
+ public static int getXTile (double longitude , long tiles ) {
118
+ // normalizeLon treats this as 180, which is not friendly for tile mapping
119
+ if (longitude == -180 ) {
120
+ return 0 ;
121
+ }
104
122
105
- double latSin = Math .sin (Math .toRadians (normalizeLat (latitude )));
106
- long yTile = (long ) Math .floor ((0.5 - (Math .log ((1 + latSin ) / (1 - latSin )) / (4 * Math .PI ))) * tiles );
123
+ int xTile = (int ) Math .floor ((normalizeLon (longitude ) + 180 ) / 360 * tiles );
107
124
108
125
// Edge values may generate invalid values, and need to be clipped.
109
126
// For example, polar regions (above/below lat 85.05112878) get normalized.
110
127
if (xTile < 0 ) {
111
- xTile = 0 ;
128
+ return 0 ;
112
129
}
113
130
if (xTile >= tiles ) {
114
- xTile = tiles - 1 ;
131
+ return ( int ) tiles - 1 ;
115
132
}
133
+
134
+ return xTile ;
135
+ }
136
+
137
+ /**
138
+ * Calculates the y-coordinate in the tile grid for the specified longitude given
139
+ * the number of tile rows for pre-determined zoom-level.
140
+ *
141
+ * @param latitude the latitude to use when determining the tile y-coordinate
142
+ * @param tiles the number of tiles per column for a pre-determined zoom-level
143
+ */
144
+ public static int getYTile (double latitude , long tiles ) {
145
+ double latSin = SloppyMath .cos (PI_DIV_2 - Math .toRadians (normalizeLat (latitude )));
146
+ int yTile = (int ) Math .floor ((0.5 - (Math .log ((1 + latSin ) / (1 - latSin )) / (4 * Math .PI ))) * tiles );
147
+
116
148
if (yTile < 0 ) {
117
149
yTile = 0 ;
118
150
}
119
151
if (yTile >= tiles ) {
120
- yTile = tiles - 1 ;
152
+ return ( int ) tiles - 1 ;
121
153
}
122
154
123
- return longEncode ((long ) precision , xTile , yTile );
155
+ return yTile ;
156
+ }
157
+
158
+ /**
159
+ * Encode lon/lat to the geotile based long format.
160
+ * The resulting hash contains interleaved tile X and Y coordinates.
161
+ * The precision itself is also encoded as a few high bits.
162
+ */
163
+ public static long longEncode (double longitude , double latitude , int precision ) {
164
+ // Mathematics for this code was adapted from https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Java
165
+ // Number of tiles for the current zoom level along the X and Y axis
166
+ final long tiles = 1 << checkPrecisionRange (precision );
167
+ long xTile = getXTile (longitude , tiles );
168
+ long yTile = getYTile (latitude , tiles );
169
+ return longEncodeTiles (precision , xTile , yTile );
124
170
}
125
171
126
172
/**
@@ -131,7 +177,14 @@ public static long longEncode(double longitude, double latitude, int precision)
131
177
*/
132
178
public static long longEncode (String hashAsString ) {
133
179
int [] parsed = parseHash (hashAsString );
134
- return longEncode ((long )parsed [0 ], (long )parsed [1 ], (long )parsed [2 ]);
180
+ return longEncode ((long ) parsed [0 ], (long ) parsed [1 ], (long ) parsed [2 ]);
181
+ }
182
+
183
+ public static long longEncodeTiles (int precision , long xTile , long yTile ) {
184
+ // Zoom value is placed in front of all the bits used for the geotile
185
+ // e.g. when max zoom is 29, the largest index would use 58 bits (57th..0th),
186
+ // leaving 5 bits unused for zoom. See MAX_ZOOM comment above.
187
+ return ((long ) precision << ZOOM_SHIFT ) | (xTile << MAX_ZOOM ) | yTile ;
135
188
}
136
189
137
190
/**
@@ -193,6 +246,23 @@ static GeoPoint keyToGeoPoint(String hashAsString) {
193
246
return zxyToGeoPoint (hashAsInts [0 ], hashAsInts [1 ], hashAsInts [2 ]);
194
247
}
195
248
249
+ public static Rectangle toBoundingBox (long hash ) {
250
+ int [] hashAsInts = parseHash (hash );
251
+ return toBoundingBox (hashAsInts [1 ], hashAsInts [2 ], hashAsInts [0 ]);
252
+ }
253
+
254
+ public static Rectangle toBoundingBox (int xTile , int yTile , int precision ) {
255
+ final double tiles = validateZXY (precision , xTile , yTile );
256
+ final double minN = Math .PI - (2.0 * Math .PI * (yTile + 1 )) / tiles ;
257
+ final double maxN = Math .PI - (2.0 * Math .PI * (yTile )) / tiles ;
258
+ final double minY = Math .toDegrees (ESSloppyMath .atan (ESSloppyMath .sinh (minN )));
259
+ final double minX = ((xTile ) / tiles * 360.0 ) - 180 ;
260
+ final double maxY = Math .toDegrees (ESSloppyMath .atan (ESSloppyMath .sinh (maxN )));
261
+ final double maxX = ((xTile + 1 ) / tiles * 360.0 ) - 180 ;
262
+
263
+ return new Rectangle (minX , maxX , maxY , minY );
264
+ }
265
+
196
266
/**
197
267
* Validates Zoom, X, and Y values, and returns the total number of allowed tiles along the x/y axis.
198
268
*/
0 commit comments