|
import static java.lang.Math.abs; |
|
import static java.lang.Math.acos; |
|
import static java.lang.Math.cos; |
|
import static java.lang.Math.sin; |
|
|
|
import java.util.Arrays; |
|
import java.util.LinkedList; |
|
import java.util.List; |
|
|
|
public class GeographyUtil { |
|
public static final double RADIUS_OF_EARTH = 6371.0088; //kilometers |
|
public static final double ANGLE_TO_RADIAN = Math.PI / 180; |
|
|
|
private static final int BITS = 6 * 5; |
|
private static final char[] BASE32 = "0123456789bcdefghjkmnpqrstuvwxyz".toCharArray(); |
|
|
|
private GeographyUtil() { |
|
} |
|
|
|
public static boolean isValid(double lat, double lon) { |
|
return (-90 <= lat && lat <= 90) && (-180 <= lon && lon <= 180); |
|
} |
|
|
|
public static String geohash(double lat, double lon) throws IllegalArgumentException { |
|
if (!isValid(lat, lon)) { |
|
throw new IllegalArgumentException( |
|
"invalid coordinate pair. lat:" + lat + " , lon:" + lon); |
|
} |
|
return encode(lat, lon); |
|
} |
|
|
|
public static List<String> neighbours(String geohash) { |
|
return NeighbourProvider.expand(geohash); |
|
} |
|
|
|
private static String encode(double lat, double lon) { |
|
byte[] latBits = getBits(lat, -90, 90); |
|
byte[] lonBits = getBits(lon, -180, 180); |
|
|
|
StringBuilder result = new StringBuilder(); |
|
int idx = 0, latBit, lonBit; |
|
for (int i = 0, j = 0; i < BITS; i++) { |
|
lonBit = lonBits[i]; |
|
latBit = latBits[i]; |
|
idx = (idx << 2) | (lonBit << 1 | latBit); |
|
j += 2; |
|
if (j >= 5) { |
|
j -= 5; |
|
result.append(BASE32[(idx & (31 << j)) >>> j]); |
|
idx &= ((1 << j) - 1); |
|
} |
|
} |
|
|
|
return result.toString(); |
|
} |
|
|
|
private static byte[] getBits(double value, double floor, |
|
double ceiling) throws IllegalArgumentException { |
|
byte[] buffer = new byte[BITS]; |
|
for (int i = 0; i < BITS; i++) { |
|
double mid = (floor + ceiling) / 2; |
|
if (value > mid) { |
|
buffer[i] = 1; |
|
floor = mid; |
|
continue; |
|
} |
|
|
|
ceiling = mid; |
|
} |
|
return buffer; |
|
} |
|
|
|
/** |
|
* Calculate the shortest distance between two points on the surface of Earth. |
|
* |
|
* @param lat1 latitude of first point |
|
* @param lon1 longitude of first point |
|
* @param lat2 latitude of second point |
|
* @param lon2 longitude of second point |
|
* @return distance between two points |
|
*/ |
|
public static double getDistance(double lat1, double lon1, double lat2, double lon2) { |
|
if (!isValid(lat1, lon1) || !isValid(lat2, lon2)) { |
|
throw new IllegalArgumentException("invalid geographic coordinate"); |
|
} |
|
|
|
return calculate(lat1, lon1, lat2, lon2); |
|
} |
|
|
|
private static double calculate(double lat1, double lon1, double lat2, double lon2) { |
|
double radLat1 = lat1 * ANGLE_TO_RADIAN; |
|
double radLon1 = lon1 * ANGLE_TO_RADIAN; |
|
|
|
double radLat2 = lat2 * ANGLE_TO_RADIAN; |
|
double radLon2 = lon2 * ANGLE_TO_RADIAN; |
|
|
|
return RADIUS_OF_EARTH * acos( |
|
sin(radLat1) * sin(radLat2) + |
|
cos(radLat1) * cos(radLat2) * cos(abs(radLon1 - radLon2))); |
|
} |
|
|
|
private static class NeighbourProvider { |
|
private static final int TOP = 0; |
|
private static final int RIGHT = 1; |
|
private static final int BOTTOM = 2; |
|
private static final int LEFT = 3; |
|
private static final int EVEN = 0; |
|
private static final int ODD = 1; |
|
|
|
private static char[][][] NEIGHBOURS; |
|
private static char[][][] BORDERS; |
|
|
|
static { |
|
NEIGHBOURS = new char[2][4][]; |
|
BORDERS = new char[2][4][]; |
|
|
|
BORDERS[ODD][TOP] = "bcfguvyz".toCharArray(); |
|
BORDERS[ODD][RIGHT] = "prxz".toCharArray(); |
|
BORDERS[ODD][BOTTOM] = "0145hjnp".toCharArray(); |
|
BORDERS[ODD][LEFT] = "028b".toCharArray(); |
|
|
|
BORDERS[EVEN][TOP] = BORDERS[ODD][RIGHT]; |
|
BORDERS[EVEN][RIGHT] = BORDERS[ODD][TOP]; |
|
BORDERS[EVEN][BOTTOM] = BORDERS[ODD][LEFT]; |
|
BORDERS[EVEN][LEFT] = BORDERS[ODD][BOTTOM]; |
|
|
|
NEIGHBOURS[ODD][TOP] = "238967debc01fg45kmstqrwxuvhjyznp".toCharArray(); |
|
NEIGHBOURS[ODD][RIGHT] = "14365h7k9dcfesgujnmqp0r2twvyx8zb".toCharArray(); |
|
NEIGHBOURS[ODD][BOTTOM] = "bc01fg45238967deuvhjyznpkmstqrwx".toCharArray(); |
|
NEIGHBOURS[ODD][LEFT] = "p0r21436x8zb9dcf5h7kjnmqesgutwvy".toCharArray(); |
|
|
|
NEIGHBOURS[EVEN][TOP] = NEIGHBOURS[ODD][RIGHT]; |
|
NEIGHBOURS[EVEN][RIGHT] = NEIGHBOURS[ODD][TOP]; |
|
NEIGHBOURS[EVEN][BOTTOM] = NEIGHBOURS[ODD][LEFT]; |
|
NEIGHBOURS[EVEN][LEFT] = NEIGHBOURS[ODD][BOTTOM]; |
|
} |
|
|
|
static List<String> expand(String geohash) { |
|
List<String> list = new LinkedList<>(); |
|
|
|
String top = calculate(geohash, TOP); |
|
String bottom = calculate(geohash, BOTTOM); |
|
|
|
addNonNull(list, calculate(geohash, LEFT)); // left |
|
addNonNull(list, calculate(geohash, RIGHT)); // right |
|
addNonNull(list, top); |
|
addNonNull(list, bottom); |
|
|
|
addNonNull(list, calculate(top, LEFT)); |
|
addNonNull(list, calculate(top, RIGHT)); |
|
addNonNull(list, calculate(bottom, LEFT)); |
|
addNonNull(list, calculate(bottom, RIGHT)); |
|
|
|
return list; |
|
} |
|
|
|
private static String calculate(String geohash, int direction) { |
|
if (geohash == null) { |
|
return null; |
|
} |
|
|
|
char[] chars = geohash.toCharArray(); |
|
final int len = geohash.length(); |
|
|
|
int parity = len & 1; |
|
int lastIdx = len - 1; |
|
|
|
while (lastIdx >= 0 && Arrays.binarySearch(BORDERS[parity][direction], chars[lastIdx]) > -1) { |
|
lastIdx--; |
|
parity ^= 1; |
|
} |
|
|
|
if (lastIdx == -1) { |
|
return null; // out of range |
|
} |
|
|
|
char lastChar; |
|
while (lastIdx < len) { |
|
lastChar = chars[lastIdx]; |
|
chars[lastIdx++] = NEIGHBOURS[parity][direction][Arrays.binarySearch(BASE32, lastChar)]; |
|
parity ^= 1; |
|
} |
|
|
|
return new String(chars); |
|
} |
|
|
|
private static <T> void addNonNull(List<? super T> list, T obj) { |
|
if (obj != null) { |
|
list.add(obj); |
|
} |
|
} |
|
} |
|
} |