diff --git a/contrib/CHANGES.txt b/contrib/CHANGES.txt index 735e8751928..e97657bd7ca 100644 --- a/contrib/CHANGES.txt +++ b/contrib/CHANGES.txt @@ -32,7 +32,11 @@ API Changes * LUCENE-2108: Add SpellChecker.close, to close the underlying reader. (Eirik Bjørsnøs via Mike McCandless) - + + * LUCENE-2147: Spatial GeoHashUtils now always decode GeoHash strings + with full precision. GeoHash#decode_exactly(String) was merged into + GeoHash#decode(String). (Chris Male, Simon Willnauer) + * LUCENE-2165: Add a constructor to SnowballAnalyzer that takes a Set of stopwords, and deprecate the String[] one. (Nick Burch via Robert Muir) diff --git a/contrib/spatial/src/java/org/apache/lucene/spatial/geohash/GeoHashUtils.java b/contrib/spatial/src/java/org/apache/lucene/spatial/geohash/GeoHashUtils.java index 2462a47ccfc..be7c1433d54 100644 --- a/contrib/spatial/src/java/org/apache/lucene/spatial/geohash/GeoHashUtils.java +++ b/contrib/spatial/src/java/org/apache/lucene/spatial/geohash/GeoHashUtils.java @@ -21,154 +21,119 @@ import java.util.HashMap; import java.util.Map; /** - * Based on http://en.wikipedia.org/wiki/Geohash - * - *

NOTE: This API is still in - * flux and might change in incompatible ways in the next - * release. + * Utilities for encoding and decoding geohashes. Based on + * http://en.wikipedia.org/wiki/Geohash. */ public class GeoHashUtils { - // geohash's char map - // no a's i's l's o's - // old MacDonal wouldn't be happy - private static char[] _base32 = {'0','1','2','3','4','5','6','7','8','9', - 'b','c','d','e','f','g','h','j','k','m', - 'n','p','q','r','s','t','u','v','w','x', - 'y','z'} ; - - private final static Map _decodemap = new HashMap(); - static { - int sz = _base32.length; - for (int i = 0; i < sz; i++ ){ - _decodemap.put(_base32[i], i); - } - } - - private static int precision = 12; - private static int[] bits = {16, 8, 4, 2, 1}; - - public static void main(String[] args) { - GeoHashUtils ghf = new GeoHashUtils(); - String gc1 = ghf.encode(30, -90.0); - String gc2 = ghf.encode(51.4797, -0.0124); - - System.out.println(gc1); - System.out.println(gc2); - - double [] gd1 = ghf.decode(gc1); - double [] gd2 = ghf.decode(gc2); - System.out.println(gd1[0]+ ", "+ gd1[1]); - System.out.println(gd2[0]+ ", "+ gd2[1]); - - } - - public static String encode(double latitude, double longitude){ - double[] lat_interval = {-90.0 , 90.0}; - double[] lon_interval = {-180.0, 180.0}; - - StringBuilder geohash = new StringBuilder(); - boolean is_even = true; - int bit = 0, ch = 0; - - while(geohash.length() < precision){ - double mid = 0.0; - if(is_even){ - mid = (lon_interval[0] + lon_interval[1]) / 2; - if (longitude > mid){ - ch |= bits[bit]; - lon_interval[0] = mid; - } else { - lon_interval[1] = mid; - } - - } else { - mid = (lat_interval[0] + lat_interval[1]) / 2; - if(latitude > mid){ - ch |= bits[bit]; - lat_interval[0] = mid; - } else { - lat_interval[1] = mid; - } - } - - is_even = is_even ? false : true; - - if (bit < 4){ - bit ++; - } else { - geohash.append(_base32[ch]); - bit =0; - ch = 0; - } - } - - return geohash.toString(); - } - - public static double[] decode(String geohash) { - double[] ge = decode_exactly(geohash); - double lat, lon, lat_err, lon_err; - lat = ge[0]; - lon = ge[1]; - lat_err = ge[2]; - lon_err = ge[3]; - - double lat_precision = Math.max(1, Math.round(- Math.log10(lat_err))) - 1; - double lon_precision = Math.max(1, Math.round(- Math.log10(lon_err))) - 1; - - lat = getPrecision(lat, lat_precision); - lon = getPrecision(lon, lon_precision); - - return new double[] {lat, lon}; - } - - public static double[] decode_exactly (String geohash){ - double[] lat_interval = {-90.0 , 90.0}; - double[] lon_interval = {-180.0, 180.0}; - - double lat_err = 90.0; - double lon_err = 180.0; - boolean is_even = true; - int sz = geohash.length(); - int bsz = bits.length; - double latitude, longitude; - for (int i = 0; i < sz; i++){ - - int cd = _decodemap.get(geohash.charAt(i)); - - for (int z = 0; z< bsz; z++){ - int mask = bits[z]; - if (is_even){ - lon_err /= 2; - if ((cd & mask) != 0){ - lon_interval[0] = (lon_interval[0]+lon_interval[1])/2; - } else { - lon_interval[1] = (lon_interval[0]+lon_interval[1])/2; - } - - } else { - lat_err /=2; - - if ( (cd & mask) != 0){ - lat_interval[0] = (lat_interval[0]+lat_interval[1])/2; - } else { - lat_interval[1] = (lat_interval[0]+lat_interval[1])/2; - } - } - is_even = is_even ? false : true; - } - - } - latitude = (lat_interval[0] + lat_interval[1]) / 2; - longitude = (lon_interval[0] + lon_interval[1]) / 2; + private static final char[] BASE_32 = {'0', '1', '2', '3', '4', '5', '6', + '7', '8', '9', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n', + 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'}; - return new double []{latitude, longitude, lat_err, lon_err}; + private final static Map DECODE_MAP = new HashMap(); + + private static final int PRECISION = 12; + private static final int[] BITS = {16, 8, 4, 2, 1}; + + static { + for (int i = 0; i < BASE_32.length; i++) { + DECODE_MAP.put(Character.valueOf(BASE_32[i]), Integer.valueOf(i)); + } + } + + private GeoHashUtils() { + } + + /** + * Encodes the given latitude and longitude into a geohash + * + * @param latitude Latitude to encode + * @param longitude Longitude to encode + * @return Geohash encoding of the longitude and latitude + */ + public static String encode(double latitude, double longitude) { + double[] latInterval = {-90.0, 90.0}; + double[] lngInterval = {-180.0, 180.0}; + + final StringBuilder geohash = new StringBuilder(); + boolean isEven = true; + + int bit = 0; + int ch = 0; + + while (geohash.length() < PRECISION) { + double mid = 0.0; + if (isEven) { + mid = (lngInterval[0] + lngInterval[1]) / 2D; + if (longitude > mid) { + ch |= BITS[bit]; + lngInterval[0] = mid; + } else { + lngInterval[1] = mid; + } + } else { + mid = (latInterval[0] + latInterval[1]) / 2D; + if (latitude > mid) { + ch |= BITS[bit]; + latInterval[0] = mid; + } else { + latInterval[1] = mid; + } + } + + isEven = !isEven; + + if (bit < 4) { + bit++; + } else { + geohash.append(BASE_32[ch]); + bit = 0; + ch = 0; + } + } + + return geohash.toString(); + } + + /** + * Decodes the given geohash into a latitude and longitude + * + * @param geohash Geohash to deocde + * @return Array with the latitude at index 0, and longitude at index 1 + */ + public static double[] decode(String geohash) { + final double[] latInterval = {-90.0, 90.0}; + final double[] lngInterval = {-180.0, 180.0}; + + boolean isEven = true; + + double latitude; + double longitude; + for (int i = 0; i < geohash.length(); i++) { + final int cd = DECODE_MAP.get(Character.valueOf( + geohash.charAt(i))).intValue(); + + for (int mask : BITS) { + if (isEven) { + if ((cd & mask) != 0) { + lngInterval[0] = (lngInterval[0] + lngInterval[1]) / 2D; + } else { + lngInterval[1] = (lngInterval[0] + lngInterval[1]) / 2D; + } + } else { + if ((cd & mask) != 0) { + latInterval[0] = (latInterval[0] + latInterval[1]) / 2D; + } else { + latInterval[1] = (latInterval[0] + latInterval[1]) / 2D; + } + } + isEven = !isEven; + } + + } + latitude = (latInterval[0] + latInterval[1]) / 2D; + longitude = (lngInterval[0] + lngInterval[1]) / 2D; + + return new double[] {latitude, longitude}; } - - static double getPrecision(double x, double precision) { - double base = Math.pow(10,- precision); - double diff = x % base; - return x - diff; - } -} +} \ No newline at end of file diff --git a/contrib/spatial/src/java/org/apache/lucene/spatial/geometry/DistanceUnits.java b/contrib/spatial/src/java/org/apache/lucene/spatial/geometry/DistanceUnits.java index 3648ec122de..e72800f4e17 100644 --- a/contrib/spatial/src/java/org/apache/lucene/spatial/geometry/DistanceUnits.java +++ b/contrib/spatial/src/java/org/apache/lucene/spatial/geometry/DistanceUnits.java @@ -18,11 +18,93 @@ package org.apache.lucene.spatial.geometry; /** - *

NOTE: This API is still in - * flux and might change in incompatible ways in the next - * release. + * Enum representing difference distance units, currently only kilometers and + * miles */ public enum DistanceUnits { - MILES, - KILOMETERS; + + MILES("miles", 3959, 24902), + KILOMETERS("km", 6371, 40076); + + private static final double MILES_KILOMETRES_RATIO = 1.609344; + + private final String unit; + + private final double earthCircumference; + + private final double earthRadius; + + /** + * Creates a new DistanceUnit that represents the given unit + * + * @param unit Distance unit in String form + * @param earthRadius Radius of the Earth in the specific distance unit + * @param earthCircumfence Circumference of the Earth in the specific distance unit + */ + DistanceUnits(String unit, double earthRadius, double earthCircumfence) { + this.unit = unit; + this.earthCircumference = earthCircumfence; + this.earthRadius = earthRadius; + } + + /** + * Returns the DistanceUnit which represents the given unit + * + * @param unit Unit whose DistanceUnit should be found + * @return DistanceUnit representing the unit + * @throws IllegalArgumentException if no DistanceUnit which represents the given unit is found + */ + public static DistanceUnits findDistanceUnit(String unit) { + if (MILES.getUnit().equals(unit)) { + return MILES; + } + + if (KILOMETERS.getUnit().equals(unit)) { + return KILOMETERS; + } + + throw new IllegalArgumentException("Unknown distance unit " + unit); + } + + /** + * Converts the given distance in given DistanceUnit, to a distance in the unit represented by {@code this} + * + * @param distance Distance to convert + * @param from Unit to convert the distance from + * @return Given distance converted to the distance in the given unit + */ + public double convert(double distance, DistanceUnits from) { + if (from == this) { + return distance; + } + return (this == MILES) ? distance / MILES_KILOMETRES_RATIO : distance * MILES_KILOMETRES_RATIO; + } + + /** + * Returns the string representation of the distance unit + * + * @return String representation of the distance unit + */ + public String getUnit() { + return unit; + } + + /** + * Returns the average earth radius + * + * @return the average earth radius + */ + public double earthRadius() { + return earthRadius; + } + + /** + * Returns the circumference of the Earth + * + * @return the circumference of the Earth + */ + public double earthCircumference() { + return earthCircumference; + } } + diff --git a/contrib/spatial/src/test/org/apache/lucene/spatial/geohash/TestGeoHashUtils.java b/contrib/spatial/src/test/org/apache/lucene/spatial/geohash/TestGeoHashUtils.java new file mode 100644 index 00000000000..3bf531185e8 --- /dev/null +++ b/contrib/spatial/src/test/org/apache/lucene/spatial/geohash/TestGeoHashUtils.java @@ -0,0 +1,89 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.lucene.spatial.geohash; + +import static junit.framework.Assert.*; + +import org.junit.Test; + +/** + * Tests for {@link GeoHashUtils} + */ +public class TestGeoHashUtils { + + /** + * 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.encode(42.6, -5.6); + assertEquals("ezs42e44yx96", hash); + + hash = GeoHashUtils.encode(57.64911, 10.40744); + 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() { + String hash = GeoHashUtils.encode(52.3738007, 4.8909347); + + double[] latitudeLongitude = GeoHashUtils.decode(hash); + + assertEquals(52.3738007, latitudeLongitude[0], 0.00001D); + assertEquals(4.8909347, latitudeLongitude[1], 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() { + String hash = GeoHashUtils.encode(84.6, 10.5); + + double[] latitudeLongitude = GeoHashUtils.decode(hash); + + assertEquals(84.6, latitudeLongitude[0], 0.00001D); + assertEquals(10.5, latitudeLongitude[1], 0.00001D); + } + + /* + * see https://issues.apache.org/jira/browse/LUCENE-1815 for details + */ + @Test + public void testDecodeEncode() { + String geoHash = "u173zq37x014"; + assertEquals(geoHash, GeoHashUtils.encode(52.3738007, 4.8909347)); + double[] decode = GeoHashUtils.decode(geoHash); + assertEquals(52.37380061d, decode[0], 0.000001d); + assertEquals(4.8909343d, decode[1], 0.000001d); + + assertEquals(geoHash, GeoHashUtils.encode(decode[0], decode[1])); + + geoHash = "u173"; + decode = GeoHashUtils.decode("u173"); + geoHash = GeoHashUtils.encode(decode[0], decode[1]); + assertEquals(decode[0], GeoHashUtils.decode(geoHash)[0], 0.000001d); + assertEquals(decode[1], GeoHashUtils.decode(geoHash)[1], 0.000001d); + } +} diff --git a/contrib/spatial/src/test/org/apache/lucene/spatial/geometry/TestDistanceUnits.java b/contrib/spatial/src/test/org/apache/lucene/spatial/geometry/TestDistanceUnits.java new file mode 100644 index 00000000000..114d88d387b --- /dev/null +++ b/contrib/spatial/src/test/org/apache/lucene/spatial/geometry/TestDistanceUnits.java @@ -0,0 +1,46 @@ +package org.apache.lucene.spatial.geometry; + +import static junit.framework.Assert.*; + +import org.junit.Test; + +/** + * Tests for {@link org.apache.lucene.spatial.geometry.DistanceUnits} + */ +public class TestDistanceUnits { + + /** + * Pass condition: When finding the DistanceUnit for "km", KILOMETRES is found. When finding the DistanceUnit for + * "miles", MILES is found. + */ + @Test + public void testFindDistanceUnit() { + assertEquals(DistanceUnits.KILOMETERS, DistanceUnits.findDistanceUnit("km")); + assertEquals(DistanceUnits.MILES, DistanceUnits.findDistanceUnit("miles")); + } + + /** + * Pass condition: Searching for the DistanceUnit of an unknown unit "mls" should throw an IllegalArgumentException. + */ + @Test + public void testFindDistanceUnit_unknownUnit() { + try { + DistanceUnits.findDistanceUnit("mls"); + assertTrue("IllegalArgumentException should have been thrown", false); + } catch (IllegalArgumentException iae) { + // Expected + } + } + + /** + * Pass condition: Converting between the same units should not change the value. Converting from MILES to KILOMETRES + * involves multiplying the distance by the ratio, and converting from KILOMETRES to MILES involves dividing by the ratio + */ + @Test + public void testConvert() { + assertEquals(10.5, DistanceUnits.MILES.convert(10.5, DistanceUnits.MILES), 0D); + assertEquals(10.5, DistanceUnits.KILOMETERS.convert(10.5, DistanceUnits.KILOMETERS), 0D); + assertEquals(10.5 * 1.609344, DistanceUnits.KILOMETERS.convert(10.5, DistanceUnits.MILES), 0D); + assertEquals(10.5 / 1.609344, DistanceUnits.MILES.convert(10.5, DistanceUnits.KILOMETERS), 0D); + } +}