GeoHash
▶经纬度坐标
地球的经度范围为西经180到东经180,纬度范围为北纬90到南纬90,按照通常所说的上北下南左西右东,结合二维直角坐标系来理解,经度区间可以划分为左区间[-180,0)和右区间[0,180)两部分,同理纬度区间可以划分为左区间[-90,0)和[0,90)两部分
▶1、是什么
一种地理编码,空间点索引算法,分级分数据结构,将地图划分为网格,可以提供任意精度的分段级别,一般分级为1-12级
▶2、作用:
对二维的有经度和维度的地理坐标进行编码,将二维坐标映射为一个字符串,一个GeoHash字符串表示经度和纬度两个坐标,每个字符串代表特定的矩形,该矩形范围内的所有坐标都共用这个字符串
▶3、特点
GeoHash字符串的长度n即n级网格大小,下图所示即为各级网格大小的范围偏差
字符串长度越长,表示网格级别越大,所表示的区域就越小,精度越高,对应的矩形范围越小。此外,一个点附近的区域的GeoHash字符串总有公共前缀,公共前缀的长度越长,表明两个点的距离越近
▶4、实现(经纬度->字符串)
GeoHash的实现比较简单,主要包括 根据需要得到的n级网格大小分别确定经、纬度的二进制编码串,之后合并经纬度二进制编码串,接着按每五个二进制数截取,转换为一个十进制数,通过base32编码即可得到相应的GeoHash编码。
4.1二进制编码串
由于使用的base32编码,每5个二进制数确定一个base32编码,我们这里截取每5个二进制数转换为一个十进制数。首先,我们需要通过n级的网格大小确定合并后的经纬度二进制编码串的长度即为:网格大小*5。之后分别确定经纬度的二进制编码串长度,若网格大小为奇数,则经度二进制编码长度为(5n/2)+1,纬度二进制编码长度为5n/2;若网格大小为偶数则均分都为5n/2。这里我们可以分别得到经、纬度二进制编码的长度,而二进制编码的获取,则是根据前面对经纬度区间的划分,判断目标的经纬度分别落在左区间还是右区间,落在左边则取0,落在右边则取1。然后根据经纬度的长度,使用二分查找法,接着对上一步得到的区间继续按照同样的方法获取下一位二进制编码,逐个拼接得到最终的二进制编码。
以坐标(31.245105,121.506377)为例,计算它的5级网格大小。纬度二进制编码串的长度应该为5*5/2=12,经度二进制编码串的长度应为(5X5/2)+1=13。首先对纬度进行编码:
[-90,90]可以分为左区间[-90,0]和右区间(0,90],纬度31.245105在右区间(0,90],因此取1
(0,90]可以分为左区间[0,45]和右区间(45,90],维度31.245105在左区间,因此取0
。。。。。。
不断重复这些步骤,得到的目标的区间会越来越小,趋近于31.245105
同理,对经度进行二进制编码可以得到:1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0
/**
* @param min 区间最小经纬度
* @param max 区间最大经纬度
* @param value 经纬度的值
* @param count 经纬度位数
* @param list 有序集合存储
*/
public static void binary(double min, double max, double value, int count, ArrayList list){
if (list.size()>count-1){
return;
}
double mid = (max + min)/2;
if (value < mid){
list.add('0');
binary(min,mid,value,count,list);
} else{
list.add('1');
binary(mid,max,value,count,list);
}
}
4.2经纬度合并
得到经、纬度的二进制编码串之后,要做的就是进行经纬度的合并,根据希尔伯特曲线,编码时候我们经纬度交替,奇数位放纬度,偶数位放经度,能够让空间上临近的经纬度在编码时也靠的更近。
拼接时要注意网格的奇数偶数问题,若经度编码串与纬度编码串的长度不等,则说明网格为奇数,经度最后要再拼接一次,避免合并时的漏拼接问题。这里就得到合并后的新的二进制编码串
对上面的经纬度二进制编码串合并后得到1110011001111000001111000
StringBuilder sb = new StringBuilder();
//纬度编码串长度
int latLength = latList.size();
for (int i = 0; i < latLength; i++) {
sb.append(lngList.get(i)).append(latList.get(i));
}
if (lngList.size()!=latLength){
sb.append(lngList.get(latLength));
}
4.3base32编码
用0-9,b-z(去掉a、i、l、o)这32个进行编码
private static final String[] 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"};
首先需要将二进制编码串按每五位截取为一组,转为一个十进制数,之后根据索引下标去base32数组中获取对应的字符编码,将其转换,拼接得到的最后结果即为目标字符串。
截取可得到 11100 11001 11100 00011 11000
转为十进制 28 25 28 3 24
base32编码 w t w 3 s
StringBuilder resStr = new StringBuilder();
String fiveNum = "";
for (int i = 0; i < sb.length(); i+=5) {
fiveNum = sb.substring(i,i+5);
//转十进制
long tenChange = Long.parseLong(fiveNum, 2);
System.out.println("tenChange = " + tenChange);
resStr.append(BASE32[(int) tenChange]);
}
System.out.println("GeoHash编码为:" + resStr);
return resStr.toString();
▶5、字符串->经纬度
字符串到经纬度的转换也相对简单,就是一个逆向的过程,首先需要将获取到的base32编码转为十进制数,即相关字符在base32数组中的索引位置
int strLength = str.length();
String baseStr = ArrayUtil.arrayToString(BASE32).replaceAll(" ","");
//1、base32编码转为十进制字符串
ArrayList<Integer> tenList = new ArrayList<>(strLength);
for (int i = 0; i < strLength; i++) {
tenList.add(baseStr.indexOf(str.charAt(i)));
}
接着需要将获取到的十进制数集合转为二进制编码串,一个十进制数对应五个二进制编码数,不满5位的前面补0
StringBuilder sbBinary = new StringBuilder();
for (Integer list : tenList) {
String num = Integer.toBinaryString(list);
int length = num.length();
if (length < 5){
for (int i = 0; i < 5 - length; i++) {
num = "0" + num;
}
}
sbBinary.append(num);
}
接着依旧按照之前的奇数位放纬度,偶数位放经度的方式实现经、纬度二进制编码的拆分,得到两个经纬度的二进制编码串
ArrayList<Character> latList = new ArrayList<>();
ArrayList<Character> lngList = new ArrayList<>();
for (int i = 0; i < sbBinary.length(); i++) {
if (i%2==1){
latList.add(sbBinary.charAt(i));
}else {
lngList.add(sbBinary.charAt(i));
}
}
之后就是根据得到的经纬度二进制编码串进行计算得到经纬度,依旧是根据之前区间的划分,和之前的处理是一个相反的过程,遍历经度或纬度的二进制编码,如果是0,则说明在原经度或纬度在左区间,改变区间的最大值为中间值,如果是1,则说明原经度或纬度在右区间,改变区间的最小值为中间值,根据二进制编码重复更改区间值求中间值,直至遍历到最后一个元素,最终得到的即为我们需要的经度或纬度
/**
* 二进制编码转换为经纬度
* @param min 经纬度最小区间
* @param max 经纬度最大区间
* @param list 二进制编码集合
* @return 经纬度值
*/
public static double getLngAndLat(double min, double max, List<Character> list){
double mid;
double resValue = 0;
for (char status : list) {
mid = (max + min)/2;
if (status == '0'){
max = mid;
}
if (status == '1'){
min = mid;
}
resValue = (max + min)/2;
}
return Double.parseDouble(String.format("%.6f",resValue));
}