GeoHash的原理讲的很多了,
GeoHash 核心原理解析 https://baijiahao.baidu.com/s?id=1684213349530757049&wfr=spider&for=pc
大多数实现都是用JAVA写的,今天给个c++版本:
考虑综合性能,可能使用7字符编码是一个合适的考虑:
static const char digits[] = { '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' };
///
// 附加一个35比特,7字符的哈希;纬度17级,经度18级;
// 备注:从低到高,索引index为0-----34,每位都是等于0x1移动了 index位
// 位移操作默认1是32bit的,如要使用64bit需要强制声明1ULL。
string GeoHashStr7(double lon, double lat)
{
uint64_t d = 0ULL;
int n = 34;
double lon_left = -180;
double lon_right = 180;
double lon_mid = 0;
double lat_left = -90;
double lat_right = 90;
double lat_mid;
// 每个循环处理2个比特,
for (int i = 0; i< 17; i++)
{
// 先处理经度
lon_mid = (lon_left + lon_right) / 2;
if (lon > lon_mid) // 右侧,1
{
lon_left = lon_mid;
d = d | (1ULL << (34 - 2*i));
}
else // 左侧0
{
lon_right = lon_mid;
}
// 处理纬度,
lat_mid = (lat_left + lat_right) / 2;
if (lat > lat_mid) // 右侧
{
lat_left = lat_mid ;
d = d | (1ULL << (34 - 2 * i - 1));
}
else
{
lat_right = lat_mid;
}
}
//剩下最低位留给经度
lon_mid = (lon_left + lon_right) / 2;
if (lon > lon_mid) // 右侧,1
{
d = d | 0x1;
}
else // 左侧0
{
}
// 开始转哈希字符
assert(sizeof(digits) / sizeof(char) == 32);
n = 7; // 字符个数
string temp = "";
for (int i = 0; i < n; i++)
{
uint64_t t = d >> 5 * (n - 1 - i);
t = t & 0X1FULL;
cout << t << endl;
assert(t < 32);
char a = digits[t];
temp += a;
}
std::cout << "经度范围:" << lon_left << "," << lon_right << ",纬度范围:" << lat_left << "," << lat_right << endl;
std::cout << "宽-高(m):" << (lon_right - lon_left) / 0.00001 * 1.1 << ", " << (lat_right - lat_left) / 0.00001 << endl;
std::cout << "7字符编码:"<<temp.c_str() << endl;
return temp;
}
bool GeoHashDecode7(const string & str, double &lon, double &lat)
{
static std::map<char, int> digitsMap;
if (digitsMap.size() == 0)
{
for (int i = 0; i < 32; i++)
{
digitsMap.insert(make_pair(digits[i], i));
}
}
// 拼接
uint64_t d = 0ULL;
int i = 0;
for (int i = 0; i < 7; i++)
{
auto it = digitsMap.find(str[i]);
if (it == digitsMap.end())
{
return false; // 错误
}
uint64_t t = static_cast<uint64_t>(it->second) << (6 - i) * 5;
d = d | t;
}
lon = 0;
lat = 0;
int n = 34;
double lon_left = -180;
double lon_right = 180;
double lon_mid = 0;
double lat_left = -90;
double lat_right = 90;
double lat_mid;
// 每个循环处理2个比特,
uint64_t t = 0UI64;
for (int i = 0; i < 17; i++)
{
// 先处理经度
t = d & (1ULL << (34 - 2 * i));
lon_mid = (lon_left + lon_right) / 2;
if (t) // 右侧,1
{
lon_left = lon_mid;
}
else // 左侧0
{
lon_right = lon_mid;
}
// 处理纬度,
t = d & (1ULL << (34 - 2 * i - 1));
lat_mid = (lat_left + lat_right) / 2;
if (t) // 右侧
{
lat_left = lat_mid;
}
else
{
lat_right = lat_mid;
}
}
// 处理最后一bit经度
t = d & 1ULL;
if (t) // 右侧,1
{
lon_left = lon_mid;
}
else // 左侧0
{
lon_right = lon_mid;
}
// 此时此刻,锁定了编码所在的区域;
std::cout << "经度范围:" << lon_left << "," << lon_right << ",纬度范围:" << lat_left << "," << lat_right << endl;
lon = (lon_left + lon_right) / 2;
lat = (lat_left + lat_right) / 2;
std::cout << lon << ", " << lat << endl;
return true;
}
而我自己用的是经纬度分别编码为2字节,15比特,同时需要在显示时候编码为6字节:
/*
* LinkUtils.h
*
* Created on: 2021年4月25日
* Author: 390017268@qq.com
*/
#ifndef LINK4A_LINKUTILS_H_
#define LINK4A_LINKUTILS_H_
#include <stdint.h>
#include <string>
using namespace std;
class LinkUtils {
public:
LinkUtils();
virtual ~LinkUtils();
// 浮点数与二进制哈希互转
static uint32_t GeoHash(double lonlat, double left, double right, int level);
static double GeoHash2LonLat(uint32_t d, double left, double right, int level);
// 字符串相关
static std::string GeoHashEncode(uint32_t & d, int level);
static int GeoHashDecode(const string & str, uint32_t & d);
static double GeoStrDecodeLontitude(const string & str);
static double GeoStrDecodeLatitude(const string & str);
// 二进制哈希与2字节互转
static void GeoHashEncode2Bytes(uint32_t d, uint8_t & d1, uint8_t & d2);
static void GeoHashDecode2Byte(uint32_t & d, uint8_t d1, uint8_t d2);
static void GeoHashEncode2Bytes(double lon, double lat, uint8_t & d1, uint8_t & d2, uint8_t & d3, uint8_t & d4);
static void GeoHashDecode2Byte(double &lon, double &lat, uint8_t d1, uint8_t d2, uint8_t d3, uint8_t d4);
};
#endif /* LINK4A_LINKUTILS_H_ */
CPP文件:
/*
* LinkUtils.cpp
*
* Created on: 2021年4月25日
* Author: 39001
*/
#include "LinkUtils.h"
#include <assert.h>
#include <map>
static const char digits[] = { '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' };
LinkUtils::LinkUtils() {
// TODO Auto-generated constructor stub
}
LinkUtils::~LinkUtils() {
// TODO Auto-generated destructor stub
}
//--------------------------------------------------------------
// 精度比较好的是纬度使用17比特,经度使用18比特,一共35比特,7个字符编码;
// 混合编码经度使用奇数位,这样就比维度多一位;
// 这里主要考虑分别编码
// 经纬度单独编码,二分法计算编码,将经纬度转为二进制
// index表示编码级别15,或10,
// d = GeoHash(39.9232, -90, 90, 15);
// d = GeoHash(116.389550, -180, 180, 15);
uint32_t LinkUtils::GeoHash(double lonlat, double left, double right, int level)
{
uint32_t d = 0;
double mid = 0;
while (true)
{
mid = (left + right) / 2;
if (lonlat > mid)
{
d = d | 0x01;
left = mid;
//std::cout << "1 ";
}
else
{
// 该位为0
right = mid;
//std::cout << "0 ";
}
level --;
if (level > 0)
{
d = d << 1;
continue;
}
else
break;
}
return d;
}
// 将二进制编码转为double
// 反向的转换一定会有精度损失,与之前就不一样了
//
//
double LinkUtils::GeoHash2LonLat(uint32_t d, double left, double right, int level)
{
double mid = 0;
for (int i=0; i<level; i++)
{
mid = (left + right) / 2;
uint32_t mask = 1 << (level - i - 1);
if (d & mask) // 1,计算右侧
{
left = mid;
}
else // 计算左侧
{
right = mid;
}
}
// 最后缩小到区域,再计算一次,取中间
mid = (left + right) / 2;
//cout << "确定范围:" << left << ", " << right << ", 精度" << right -left <<endl;
return mid;
}
//-------------------------------------------------------
// 32位哈希编码转字符
string LinkUtils::GeoHashEncode(uint32_t & d, int level)
{
assert(sizeof(digits) / sizeof(char) == 32);
int n = (level + 4) / 5;
string temp = "";
for (int i = 0; i < n; i++)
{
uint32_t t = d >> 5 * (n - 1 -i);
t = t & 0x1f;
assert(t < 32);
char a = digits[t];
temp += a;
}
// std::cout << temp.c_str() << endl;
return temp;
}
// 从单独的经纬度字符串解码为二进制哈希
int LinkUtils::GeoHashDecode(const string & str, uint32_t & d)
{
static std::map<char, int> digitsMap;
size_t n = str.length();
if (digitsMap.size() == 0) // 初始化
{
for (int i = 0; i < (sizeof(digits) / sizeof(char)); i++)
{
digitsMap.insert(make_pair(digits[i], i));
}
// 遍历一遍
/*for (auto &it : digitsMap)
{
std::cout << it.first << it.second << endl;
}*/
}
d = 0;
for (int i=0; i< n; i++)
{
auto it = digitsMap.find(str[i]);
if (it == digitsMap.end())
{
return 0;
}
uint32_t t = it->second << (n - 1 - i) * 5;
d = d | t;
}
return n * 5;
}
//
// 单独的经纬度字符编码直接转为double
double LinkUtils::GeoStrDecodeLontitude(const string & str)
{
uint32_t lon;
GeoHashDecode(str, lon);
return GeoHash2LonLat(lon, -180, 180, 15);
}
double LinkUtils::GeoStrDecodeLatitude(const string & str)
{
uint32_t lat;
GeoHashDecode(str, lat);
return GeoHash2LonLat(lat, -90, 90, 15);
}
//-------------------------------------------------------
// 另外
// 10位编码转为2个char
// d1是低8位,d2是高7 位
void LinkUtils::GeoHashEncode2Bytes(uint32_t d, uint8_t & d1, uint8_t & d2)
{
d1 = d & 0xff;
d2 = (d >> 8) & 0xff;
}
void LinkUtils::GeoHashDecode2Byte(uint32_t & d, uint8_t d1, uint8_t d2)
{
d = 0;
d = d1 | ((uint32_t)d2 << 8);
}
void LinkUtils::GeoHashEncode2Bytes(double lon, double lat, uint8_t & d1, uint8_t & d2, uint8_t & d3, uint8_t & d4)
{
uint32_t temp = GeoHash(lat, -90, 90, 15);
GeoHashEncode2Bytes(temp, d1, d2);
temp= GeoHash(lon, -180, 180, 15);
GeoHashEncode2Bytes(temp, d3, d4);
}
void LinkUtils::GeoHashDecode2Byte(double &lon, double &lat, uint8_t d1, uint8_t d2, uint8_t d3, uint8_t d4)
{
uint32_t temp;
GeoHashDecode2Byte(temp, d1, d2);
lat = GeoHash2LonLat(temp, -90, 90, 15);
GeoHashDecode2Byte(temp, d3, d4);
lon = GeoHash2LonLat(temp, -180, 180, 15);
}
希望对大家有用。