LUCENE-6724: add utility APIs to compute neighboring geohash cells

git-svn-id: https://svn.apache.org/repos/asf/lucene/dev/trunk@1694747 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Michael McCandless 2015-08-07 20:48:49 +00:00
parent c561e142bc
commit a050d3accb
3 changed files with 233 additions and 0 deletions

View File

@ -50,6 +50,9 @@ New Features
* LUCENE-6720: New FunctionRangeQuery wrapper around ValueSourceScorer
(returned from ValueSource/FunctionValues.getRangeScorer()). (David Smiley)
* LUCENE-6724: Add utility APIs to GeoHashUtils to compute neighbor
geohash cells (Nick Knize via Mike McCandless).
Optimizations
* LUCENE-6708: TopFieldCollector does not compute the score several times on the

View File

@ -17,6 +17,9 @@ package org.apache.lucene.util;
* limitations under the License.
*/
import java.util.ArrayList;
import java.util.Collection;
/**
* Utilities for converting to/from the GeoHash standard
*
@ -150,4 +153,112 @@ public class GeoHashUtils {
return BitUtil.flipFlop((geoHashLong >>> 4) << odd) << (((12 - level) * 5) + (MORTON_OFFSET - odd));
}
private static final char encode(int x, int y) {
return BASE_32[((x & 1) + ((y & 1) * 2) + ((x & 2) * 2) + ((y & 2) * 4) + ((x & 4) * 4)) % 32];
}
/**
* Calculate all neighbors of a given geohash cell.
*
* @param geohash Geohash of the defined cell
* @return geohashes of all neighbor cells
*/
public static Collection<? extends CharSequence> neighbors(String geohash) {
return addNeighbors(geohash, geohash.length(), new ArrayList<CharSequence>(8));
}
/**
* Calculate the geohash of a neighbor of a geohash
*
* @param geohash the geohash of a cell
* @param level level of the geohash
* @param dx delta of the first grid coordinate (must be -1, 0 or +1)
* @param dy delta of the second grid coordinate (must be -1, 0 or +1)
* @return geohash of the defined cell
*/
public final static String neighbor(String geohash, int level, int dx, int dy) {
int cell = BASE_32_STRING.indexOf(geohash.charAt(level -1));
// Decoding the Geohash bit pattern to determine grid coordinates
int x0 = cell & 1; // first bit of x
int y0 = cell & 2; // first bit of y
int x1 = cell & 4; // second bit of x
int y1 = cell & 8; // second bit of y
int x2 = cell & 16; // third bit of x
// combine the bitpattern to grid coordinates.
// note that the semantics of x and y are swapping
// on each level
int x = x0 + (x1 / 2) + (x2 / 4);
int y = (y0 / 2) + (y1 / 4);
if (level == 1) {
// Root cells at north (namely "bcfguvyz") or at
// south (namely "0145hjnp") do not have neighbors
// in north/south direction
if ((dy < 0 && y == 0) || (dy > 0 && y == 3)) {
return null;
} else {
return Character.toString(encode(x + dx, y + dy));
}
} else {
// define grid coordinates for next level
final int nx = ((level % 2) == 1) ? (x + dx) : (x + dy);
final int ny = ((level % 2) == 1) ? (y + dy) : (y + dx);
// if the defined neighbor has the same parent a the current cell
// encode the cell directly. Otherwise find the cell next to this
// cell recursively. Since encoding wraps around within a cell
// it can be encoded here.
// xLimit and YLimit must always be respectively 7 and 3
// since x and y semantics are swapping on each level.
if (nx >= 0 && nx <= 7 && ny >= 0 && ny <= 3) {
return geohash.substring(0, level - 1) + encode(nx, ny);
} else {
String neighbor = neighbor(geohash, level - 1, dx, dy);
return (neighbor != null) ? neighbor + encode(nx, ny) : neighbor;
}
}
}
/**
* Add all geohashes of the cells next to a given geohash to a list.
*
* @param geohash Geohash of a specified cell
* @param neighbors list to add the neighbors to
* @return the given list
*/
public static final <E extends Collection<? super String>> E addNeighbors(String geohash, E neighbors) {
return addNeighbors(geohash, geohash.length(), neighbors);
}
/**
* Add all geohashes of the cells next to a given geohash to a list.
*
* @param geohash Geohash of a specified cell
* @param length level of the given geohash
* @param neighbors list to add the neighbors to
* @return the given list
*/
public static final <E extends Collection<? super String>> E addNeighbors(String geohash, int length, E neighbors) {
String south = neighbor(geohash, length, 0, -1);
String north = neighbor(geohash, length, 0, +1);
if (north != null) {
neighbors.add(neighbor(north, length, -1, 0));
neighbors.add(north);
neighbors.add(neighbor(north, length, +1, 0));
}
neighbors.add(neighbor(geohash, length, -1, 0));
neighbors.add(neighbor(geohash, length, +1, 0));
if (south != null) {
neighbors.add(neighbor(south, length, -1, 0));
neighbors.add(south);
neighbors.add(neighbor(south, length, +1, 0));
}
return neighbors;
}
}

View File

@ -17,6 +17,10 @@ package org.apache.lucene.util;
* limitations under the License.
*/
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.junit.BeforeClass;
import org.junit.Test;
@ -82,6 +86,121 @@ public class TestGeoUtils extends LuceneTestCase {
}
}
/**
* Pass condition: lat=42.6, lng=-5.6 should be encoded as "ezs42e44yx96",
* lat=57.64911 lng=10.40744 should be encoded as "u4pruydqqvj8"
*/
@Test
public void testEncode() {
String hash = GeoHashUtils.stringEncode(-5.6, 42.6, 12);
assertEquals("ezs42e44yx96", hash);
hash = GeoHashUtils.stringEncode(10.40744, 57.64911, 12);
assertEquals("u4pruydqqvj8", hash);
}
/**
* Pass condition: lat=52.3738007, lng=4.8909347 should be encoded and then
* decoded within 0.00001 of the original value
*/
@Test
public void testDecodePreciseLongitudeLatitude() {
final String geohash = GeoHashUtils.stringEncode(4.8909347, 52.3738007);
final long hash = GeoHashUtils.mortonEncode(geohash);
assertEquals(52.3738007, GeoUtils.mortonUnhashLat(hash), 0.00001D);
assertEquals(4.8909347, GeoUtils.mortonUnhashLon(hash), 0.00001D);
}
/**
* Pass condition: lat=84.6, lng=10.5 should be encoded and then decoded
* within 0.00001 of the original value
*/
@Test
public void testDecodeImpreciseLongitudeLatitude() {
final String geohash = GeoHashUtils.stringEncode(10.5, 84.6);
final long hash = GeoHashUtils.mortonEncode(geohash);
assertEquals(84.6, GeoUtils.mortonUnhashLat(hash), 0.00001D);
assertEquals(10.5, GeoUtils.mortonUnhashLon(hash), 0.00001D);
}
@Test
public void testDecodeEncode() {
final String geoHash = "u173zq37x014";
assertEquals(geoHash, GeoHashUtils.stringEncode(4.8909347, 52.3738007));
final long mortonHash = GeoHashUtils.mortonEncode(geoHash);
final double lon = GeoUtils.mortonUnhashLon(mortonHash);
final double lat = GeoUtils.mortonUnhashLat(mortonHash);
assertEquals(52.37380061d, GeoUtils.mortonUnhashLat(mortonHash), 0.000001d);
assertEquals(4.8909343d, GeoUtils.mortonUnhashLon(mortonHash), 0.000001d);
assertEquals(geoHash, GeoHashUtils.stringEncode(lon, lat));
}
@Test
public void testNeighbors() {
String geohash = "gcpv";
List<String> expectedNeighbors = new ArrayList<>();
expectedNeighbors.add("gcpw");
expectedNeighbors.add("gcpy");
expectedNeighbors.add("u10n");
expectedNeighbors.add("gcpt");
expectedNeighbors.add("u10j");
expectedNeighbors.add("gcps");
expectedNeighbors.add("gcpu");
expectedNeighbors.add("u10h");
Collection<? super String> neighbors = new ArrayList<>();
GeoHashUtils.addNeighbors(geohash, neighbors );
assertEquals(expectedNeighbors, neighbors);
// Border odd geohash
geohash = "u09x";
expectedNeighbors = new ArrayList<>();
expectedNeighbors.add("u0c2");
expectedNeighbors.add("u0c8");
expectedNeighbors.add("u0cb");
expectedNeighbors.add("u09r");
expectedNeighbors.add("u09z");
expectedNeighbors.add("u09q");
expectedNeighbors.add("u09w");
expectedNeighbors.add("u09y");
neighbors = new ArrayList<>();
GeoHashUtils.addNeighbors(geohash, neighbors);
assertEquals(expectedNeighbors, neighbors);
// Border even geohash
geohash = "u09tv";
expectedNeighbors = new ArrayList<>();
expectedNeighbors.add("u09wh");
expectedNeighbors.add("u09wj");
expectedNeighbors.add("u09wn");
expectedNeighbors.add("u09tu");
expectedNeighbors.add("u09ty");
expectedNeighbors.add("u09ts");
expectedNeighbors.add("u09tt");
expectedNeighbors.add("u09tw");
neighbors = new ArrayList<>();
GeoHashUtils.addNeighbors(geohash, neighbors );
assertEquals(expectedNeighbors, neighbors);
// Border even and odd geohash
geohash = "ezzzz";
expectedNeighbors = new ArrayList<>();
expectedNeighbors.add("gbpbn");
expectedNeighbors.add("gbpbp");
expectedNeighbors.add("u0000");
expectedNeighbors.add("ezzzy");
expectedNeighbors.add("spbpb");
expectedNeighbors.add("ezzzw");
expectedNeighbors.add("ezzzx");
expectedNeighbors.add("spbp8");
neighbors = new ArrayList<>();
GeoHashUtils.addNeighbors(geohash, neighbors );
assertEquals(expectedNeighbors, neighbors);
}
public static double randomLatFullRange() {
return (180d * random().nextDouble()) - 90d;
}