/*
* 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.
*/
// NOTE: we keep the header as it came from ASF; it did not originate in Spatial4j
package org.locationtech.spatial4j.io;
import org.locationtech.spatial4j.context.SpatialContext;
import org.locationtech.spatial4j.shape.Point;
import org.locationtech.spatial4j.shape.Rectangle;
import java.util.Arrays;
/**
* Utilities for encoding and decoding <a href="http://en.wikipedia.org/wiki/Geohash">geohashes</a>.
* <p>
* This class isn't used by any other part of Spatial4j; it's included largely for convenience of
* software using Spatial4j. There are other open-source libraries that have more comprehensive
* geohash utilities but providing this one avoids an additional dependency for what's a small
* amount of code. <em>If you're using Spatial4j just for this class, consider alternatives.</em>
* <p>
* This code originally came from <a href="https://issues.apache.org/jira/browse/LUCENE-1512">
* Apache Lucene, LUCENE-1512</a>.
*/
public class GeohashUtils {
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'};//note: this is sorted
private static final int[] BASE_32_IDX;//sparse array of indexes from '0' to 'z'
public static final int MAX_PRECISION = 24;//DWS: I forget what level results in needless more precision but it's about this
private static final int[] BITS = {16, 8, 4, 2, 1};
static {
BASE_32_IDX = new int[BASE_32[BASE_32.length-1] - BASE_32[0] + 1];
assert BASE_32_IDX.length < 100;//reasonable length
Arrays.fill(BASE_32_IDX,-500);
for (int i = 0; i < BASE_32.length; i++) {
BASE_32_IDX[BASE_32[i] - BASE_32[0]] = 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 encodeLatLon(double latitude, double longitude) {
return encodeLatLon(latitude, longitude, 12);
}
public static String encodeLatLon(double latitude, double longitude, int precision) {
double[] latInterval = {-90.0, 90.0};
double[] lngInterval = {-180.0, 180.0};
final StringBuilder geohash = new StringBuilder(precision);
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 longitude (X) and latitude (Y)
*/
public static Point decode(String geohash, SpatialContext ctx) {
Rectangle rect = decodeBoundary(geohash,ctx);
double latitude = (rect.getMinY() + rect.getMaxY()) / 2D;
double longitude = (rect.getMinX() + rect.getMaxX()) / 2D;
return ctx.makePoint(longitude,latitude);
}
/** Returns min-max lon (X), min-max lat (Y). */
public static Rectangle decodeBoundary(String geohash, SpatialContext ctx) {
double minY = -90, maxY = 90, minX = -180, maxX = 180;
boolean isEven = true;
for (int i = 0; i < geohash.length(); i++) {
char c = geohash.charAt(i);
if (c >= 'A' && c <= 'Z')
c -= ('A' - 'a');
final int cd = BASE_32_IDX[c - BASE_32[0]];//TODO check successful?
for (int mask : BITS) {
if (isEven) {
if ((cd & mask) != 0) {
minX = (minX + maxX) / 2D;
} else {
maxX = (minX + maxX) / 2D;
}
} else {
if ((cd & mask) != 0) {
minY = (minY + maxY) / 2D;
} else {
maxY = (minY + maxY) / 2D;
}
}
isEven = !isEven;
}
}
return ctx.makeRectangle(minX, maxX, minY, maxY);
}
/** Array of geohashes 1 level below the baseGeohash. Sorted. */
public static String[] getSubGeohashes(String baseGeohash) {
String[] hashes = new String[BASE_32.length];
for (int i = 0; i < BASE_32.length; i++) {//note: already sorted
char c = BASE_32[i];
hashes[i] = baseGeohash+c;
}
return hashes;
}
public static double[] lookupDegreesSizeForHashLen(int hashLen) {
return new double[]{hashLenToLatHeight[hashLen], hashLenToLonWidth[hashLen]};
}
/**
* Return the shortest geohash length that will have a width & height >= specified arguments.
*/
public static int lookupHashLenForWidthHeight(double lonErr, double latErr) {
//loop through hash length arrays from beginning till we find one.
for(int len = 1; len < MAX_PRECISION; len++) {
double latHeight = hashLenToLatHeight[len];
double lonWidth = hashLenToLonWidth[len];
if (latHeight < latErr && lonWidth < lonErr)
return len;
}
return MAX_PRECISION;
}
/** See the table at http://en.wikipedia.org/wiki/Geohash */
private static final double[] hashLenToLatHeight, hashLenToLonWidth;
static {
hashLenToLatHeight = new double[MAX_PRECISION +1];
hashLenToLonWidth = new double[MAX_PRECISION +1];
hashLenToLatHeight[0] = 90*2;
hashLenToLonWidth[0] = 180*2;
boolean even = false;
for(int i = 1; i <= MAX_PRECISION; i++) {
hashLenToLatHeight[i] = hashLenToLatHeight[i-1]/(even?8:4);
hashLenToLonWidth[i] = hashLenToLonWidth[i-1]/(even?4:8);
even = ! even;
}
}
}