View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  // NOTE: we keep the header as it came from ASF; it did not originate in Spatial4j
19  
20  package org.locationtech.spatial4j.io;
21  
22  import org.locationtech.spatial4j.context.SpatialContext;
23  import org.locationtech.spatial4j.shape.Point;
24  import org.locationtech.spatial4j.shape.Rectangle;
25  
26  import java.util.Arrays;
27  
28  /**
29   * Utilities for encoding and decoding <a href="http://en.wikipedia.org/wiki/Geohash">geohashes</a>.
30   * <p>
31   * This class isn't used by any other part of Spatial4j; it's included largely for convenience of
32   * software using Spatial4j. There are other open-source libraries that have more comprehensive
33   * geohash utilities but providing this one avoids an additional dependency for what's a small
34   * amount of code.  <em>If you're using Spatial4j just for this class, consider alternatives.</em>
35   * <p>
36   * This code originally came from <a href="https://issues.apache.org/jira/browse/LUCENE-1512">
37   * Apache Lucene, LUCENE-1512</a>.
38   */
39  public class GeohashUtils {
40  
41    private static final char[] BASE_32 = {'0', '1', '2', '3', '4', '5', '6',
42        '7', '8', '9', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n',
43        'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'};//note: this is sorted
44  
45    private static final int[] BASE_32_IDX;//sparse array of indexes from '0' to 'z'
46  
47    public static final int MAX_PRECISION = 24;//DWS: I forget what level results in needless more precision but it's about this
48    private static final int[] BITS = {16, 8, 4, 2, 1};
49  
50    static {
51      BASE_32_IDX = new int[BASE_32[BASE_32.length-1] - BASE_32[0] + 1];
52      assert BASE_32_IDX.length < 100;//reasonable length
53      Arrays.fill(BASE_32_IDX,-500);
54      for (int i = 0; i < BASE_32.length; i++) {
55        BASE_32_IDX[BASE_32[i] - BASE_32[0]] = i;
56      }
57    }
58  
59    private GeohashUtils() {
60    }
61  
62    /**
63     * Encodes the given latitude and longitude into a geohash
64     *
65     * @param latitude Latitude to encode
66     * @param longitude Longitude to encode
67     * @return Geohash encoding of the longitude and latitude
68     */
69    public static String encodeLatLon(double latitude, double longitude) {
70      return encodeLatLon(latitude, longitude, 12);
71    }
72  
73    public static String encodeLatLon(double latitude, double longitude, int precision) {
74      double[] latInterval = {-90.0, 90.0};
75      double[] lngInterval = {-180.0, 180.0};
76  
77      final StringBuilder geohash = new StringBuilder(precision);
78      boolean isEven = true;
79  
80      int bit = 0;
81      int ch = 0;
82  
83      while (geohash.length() < precision) {
84        double mid = 0.0;
85        if (isEven) {
86          mid = (lngInterval[0] + lngInterval[1]) / 2D;
87          if (longitude > mid) {
88            ch |= BITS[bit];
89            lngInterval[0] = mid;
90          } else {
91            lngInterval[1] = mid;
92          }
93        } else {
94          mid = (latInterval[0] + latInterval[1]) / 2D;
95          if (latitude > mid) {
96            ch |= BITS[bit];
97            latInterval[0] = mid;
98          } else {
99            latInterval[1] = mid;
100         }
101       }
102 
103       isEven = !isEven;
104 
105       if (bit < 4) {
106         bit++;
107       } else {
108         geohash.append(BASE_32[ch]);
109         bit = 0;
110         ch = 0;
111       }
112     }
113 
114     return geohash.toString();
115   }
116 
117   /**
118    * Decodes the given geohash into a longitude (X) and latitude (Y)
119    */
120   public static Point decode(String geohash, SpatialContext ctx) {
121     Rectangle rect = decodeBoundary(geohash,ctx);
122     double latitude = (rect.getMinY() + rect.getMaxY()) / 2D;
123     double longitude = (rect.getMinX() + rect.getMaxX()) / 2D;
124     return ctx.makePoint(longitude,latitude);
125 	}
126 
127   /** Returns min-max lon (X), min-max lat (Y). */
128   public static Rectangle decodeBoundary(String geohash, SpatialContext ctx) {
129     double minY = -90, maxY = 90, minX = -180, maxX = 180;
130     boolean isEven = true;
131 
132     for (int i = 0; i < geohash.length(); i++) {
133       char c = geohash.charAt(i);
134       if (c >= 'A' && c <= 'Z')
135         c -= ('A' - 'a');
136       final int cd = BASE_32_IDX[c - BASE_32[0]];//TODO check successful?
137 
138       for (int mask : BITS) {
139         if (isEven) {
140           if ((cd & mask) != 0) {
141             minX = (minX + maxX) / 2D;
142           } else {
143             maxX = (minX + maxX) / 2D;
144           }
145         } else {
146           if ((cd & mask) != 0) {
147             minY = (minY + maxY) / 2D;
148           } else {
149             maxY = (minY + maxY) / 2D;
150           }
151         }
152         isEven = !isEven;
153       }
154 
155     }
156     return ctx.makeRectangle(minX, maxX, minY, maxY);
157   }
158 
159   /** Array of geohashes 1 level below the baseGeohash. Sorted. */
160   public static String[] getSubGeohashes(String baseGeohash) {
161     String[] hashes = new String[BASE_32.length];
162     for (int i = 0; i < BASE_32.length; i++) {//note: already sorted
163       char c = BASE_32[i];
164       hashes[i] = baseGeohash+c;
165     }
166     return hashes;
167   }
168 
169   public static double[] lookupDegreesSizeForHashLen(int hashLen) {
170     return new double[]{hashLenToLatHeight[hashLen], hashLenToLonWidth[hashLen]};
171   }
172 
173   /**
174    * Return the shortest geohash length that will have a width &amp; height &gt;= specified arguments.
175    */
176   public static int lookupHashLenForWidthHeight(double lonErr, double latErr) {
177     //loop through hash length arrays from beginning till we find one.
178     for(int len = 1; len < MAX_PRECISION; len++) {
179       double latHeight = hashLenToLatHeight[len];
180       double lonWidth = hashLenToLonWidth[len];
181       if (latHeight < latErr && lonWidth < lonErr)
182         return len;
183     }
184     return MAX_PRECISION;
185   }
186 
187   /** See the table at http://en.wikipedia.org/wiki/Geohash */
188   private static final double[] hashLenToLatHeight, hashLenToLonWidth;
189   static {
190     hashLenToLatHeight = new double[MAX_PRECISION +1];
191     hashLenToLonWidth = new double[MAX_PRECISION +1];
192     hashLenToLatHeight[0] = 90*2;
193     hashLenToLonWidth[0] = 180*2;
194     boolean even = false;
195     for(int i = 1; i <= MAX_PRECISION; i++) {
196       hashLenToLatHeight[i] = hashLenToLatHeight[i-1]/(even?8:4);
197       hashLenToLonWidth[i] = hashLenToLonWidth[i-1]/(even?4:8);
198       even = ! even;
199     }
200   }
201 
202 }