GeoHash 算法_gephi算法
myzbx 2025-09-01 09:50 65 浏览
明天的你会感谢今天努力的你
举手之劳,加个关注
简介
对于很多初学者来说,“附近的人”或者类似功能,在技术实现上还有点摸不着头脑。本文将简要的为你讲解“附近的人”的基本理论原理,并以Redis的GEO系列地理位置操作指令为例,理论联系实际地为你讲解它们是如何被高效实现的。
阅读提示:本文适合有一定Redis使用经验和经纬度知识的服务器后端开发人员阅读。
经纬度常识
- 经线是纵的,经度是横的,用于表示不同的经线,纬线是横的,纬度是纵的,用于表示不同的纬线,如下图
- 纬线:地球仪上的横线,lat,赤道是最大的纬线,从赤道开始分为北纬和南纬,都是0-90°,纬线是角度数值,并不是米;
- 经线:地球仪上的竖线,lng,子午线为0°,分为西经和东经,都是0-180°,经线也是角度数值;
- 经纬线和米的换算:经度或者纬度0.00001度,约等于1米,这个在GPS测算距离的时候可以体会到,GPS只要精确到小数点后五位,就是10米范围内的精度
- 经度0度的位置为本初子午线,在180度的位置转为西经,数字由大到小依次经过北美洲到达西欧.纬度0度的位置为赤道
- 为了便于理解,将地球看成一个基于经纬度线的坐标系。纬线就是平行于赤道平面的那些平面的周线,经线就是连接南北两极的大圆线的半圆弧。纬度分为北纬(正),南纬(负),赤道所在的纬度值为0。经度以本初子午线界(本初子午线经度为0),分为东经(正),西经(负)。故纬度范围可表示为[-90o, 0o),(0o, 90o],经度范围可表示为[-180o, 0o),(0o, 180o]
什么是geohash
- GeoHash将二维的经纬度转换成字符串,比如下图展示了北京9个区域的GeoHash字符串,分别是WX4ER,WX4G2、WX4G3等等,每一个字符串代表了某一矩形区域。也就是说,这个矩形区域内所有的点(经纬度坐标)都共享相同的GeoHash字符串,这样既可以保护隐私(只表示大概区域位置而不是具体的点),又比较容易做缓存。
- 不同的编码长度,表示不同的范围区间,字符串越长,表示的范围越精确
- 字符串相似的表示距离相近(特殊情况后文阐述),这样可以利用字符串的前缀匹配来查询附近的POI信息。如下两个图所示,一个在城区,一个在郊区,城区的GeoHash字符串之间比较相似,郊区的字符串之间也比较相似,而城区和郊区的GeoHash字符串相似程度要低些
- 总结:GeoHash就是一种将经纬度转换成字符串的方法,并且使得在大部分情况下,字符串前缀匹配越多的距离越近
GeoHash 算法
以经纬度值:(116.389550, 39.928167)进行算法说明,对纬度39.928167进行逼近编码 (地球纬度区间是[-90,90])
- 区间[-90,90]进行二分为[-90,0),[0,90],称为左右区间,可以确定39.928167属于右区间[0,90],给标记为1
- 接着将区间[0,90]进行二分为 [0,45),[45,90],可以确定39.928167属于左区间 [0,45),给标记为0
- 递归上述过程39.928167总是属于某个区间[a,b]。随着每次迭代区间[a,b]总在缩小,并越来越逼近39.928167
- 如果给定的纬度x(39.928167)属于左区间,则记录0,如果属于右区间则记录1,序列的长度跟给定的区间划分次数有关,如下图
- 同理,地球经度区间是[-180,180],可以对经度116.389550进行编码
- 通过上述计算,纬度产生的编码为1 1 0 1 0 0 1 0 1 1 0 0 0 1 0,经度产生的编码为1 0 1 1 1 0 0 0 1 1 0 0 0 1 1
- 合并:偶数位放经度,奇数位放纬度,把2串编码组合生成新串如下图:
- 首先将11100 11101 00100 01111 0000 01101转成十进制,对应着28、29、4、15,0,13 十进制对应的base32编码就是wx4g0e,如下图
- O同理,将编码转换成经纬度的解码算法与之相反
GeoHash 原理
- Geohash其实就是将整个地图或者某个分割所得的区域进行一次划分,由于采用的是base32编码方式,即Geohash中的每一个字母或者数字(如wx4g0e中的w)都是由5bits组成(2^5 = 32,base32),这5bits可以有32中不同的组合(0~31),这样我们可以将整个地图区域分为32个区域,通过00000 ~ 11111来标识这32个区域。第一次对地图划分后的情况如下图所示(每个区域中的编号对应于该区域所对应的编码):
- Geohash的0、1串序列是经度0、1序列和纬度0、1序列中的数字交替进行排列的,偶数位对应的序列为经度序列,奇数位对应的序列为纬度序列,在进行第一次划分时,Geohash0、1序列中的前5个bits(11100),那么这5bits中有3bits是表示经度,2bits表示纬度,所以第一次划分时,是将经度划分成8个区段(2^3 = 8),将纬度划分为4个区段(2^2 = 4),这样就形成了32个区域。如下图
- 同理,可以按照第一次划分所采用的方式对第一次划分所得的32个区域各自再次划分.
对照
附近的人,附近的加油站如何实现
它需要做以下两件事情:
1)在使用“附近的人”功能前提交自己的地理位置;
2)根据“我”的地理位置,计算出别人跟我的距离;
3)将第2步中计算出的距离由近及远,进行排序。
具体在产品技术上的实现原理和技术难点
1)现在移动端(ios、android等),通过系统的API很容易抓到用户当前的位置(即经纬度数据);
2)根据第1步中的经纬度数据,很容易计算出两个点之间的距离
3)对第2步中的计算结果排序就更简单了。
技术难点
1)如何高效地进行两点距离的计算
2)如何高效地进行地理围栏的圈定
public class GeoHash {
public static final double MINLAT = -90;
public static final double MAXLAT = 90;
public static final double MINLNG = -180;
public static final double MAXLNG = 180;
private static int numbits = 3 * 5; //经纬度单独编码长度
private static double minLat;
private static double minLng;
private final static 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' };
//定义编码映射关系
final static HashMap<Character, Integer> lookup = new HashMap<Character, Integer>();
//初始化编码映射内容
static {
int i = 0;
for (char c : digits)
lookup.put(c, i++);
}
public GeoHash(){
setMinLatLng();
}
public String encode(double lat, double lon) {
BitSet latbits = getBits(lat, -90, 90);
BitSet lonbits = getBits(lon, -180, 180);
StringBuilder buffer = new StringBuilder();
for (int i = 0; i < numbits; i++) {
buffer.append( (lonbits.get(i))?'1':'0');
buffer.append( (latbits.get(i))?'1':'0');
}
String code = base32(Long.parseLong(buffer.toString(), 2));
//Log.i("okunu", "encode lat = " + lat + " lng = " + lon + " code = " + code);
return code;
}
public ArrayList<String> getArroundGeoHash(double lat, double lon){
//Log.i("okunu", "getArroundGeoHash lat = " + lat + " lng = " + lon);
ArrayList<String> list = new ArrayList<>();
double uplat = lat + minLat;
double downLat = lat - minLat;
double leftlng = lon - minLng;
double rightLng = lon + minLng;
String leftUp = encode(uplat, leftlng);
list.add(leftUp);
String leftMid = encode(lat, leftlng);
list.add(leftMid);
String leftDown = encode(downLat, leftlng);
list.add(leftDown);
String midUp = encode(uplat, lon);
list.add(midUp);
String midMid = encode(lat, lon);
list.add(midMid);
String midDown = encode(downLat, lon);
list.add(midDown);
String rightUp = encode(uplat, rightLng);
list.add(rightUp);
String rightMid = encode(lat, rightLng);
list.add(rightMid);
String rightDown = encode(downLat, rightLng);
list.add(rightDown);
//Log.i("okunu", "getArroundGeoHash list = " + list.toString());
return list;
}
//根据经纬度和范围,获取对应的二进制
private BitSet getBits(double lat, double floor, double ceiling) {
BitSet buffer = new BitSet(numbits);
for (int i = 0; i < numbits; i++) {
double mid = (floor + ceiling) / 2;
if (lat >= mid) {
buffer.set(i);
floor = mid;
} else {
ceiling = mid;
}
}
return buffer;
}
//将经纬度合并后的二进制进行指定的32位编码
private String base32(long i) {
char[] buf = new char[65];
int charPos = 64;
boolean negative = (i < 0);
if (!negative){
i = -i;
}
while (i <= -32) {
buf[charPos--] = digits[(int) (-(i % 32))];
i /= 32;
}
buf[charPos] = digits[(int) (-i)];
if (negative){
buf[--charPos] = '-';
}
return new String(buf, charPos, (65 - charPos));
}
private void setMinLatLng() {
minLat = MAXLAT - MINLAT;
for (int i = 0; i < numbits; i++) {
minLat /= 2.0;
}
minLng = MAXLNG - MINLNG;
for (int i = 0; i < numbits; i++) {
minLng /= 2.0;
}
}
//根据二进制和范围解码
private double decode(BitSet bs, double floor, double ceiling) {
double mid = 0;
for (int i=0; i<bs.length(); i++) {
mid = (floor + ceiling) / 2;
if (bs.get(i))
floor = mid;
else
ceiling = mid;
}
return mid;
}
//对编码后的字符串解码
public double[] decode(String geohash) {
StringBuilder buffer = new StringBuilder();
for (char c : geohash.toCharArray()) {
int i = lookup.get(c) + 32;
buffer.append( Integer.toString(i, 2).substring(1) );
}
BitSet lonset = new BitSet();
BitSet latset = new BitSet();
//偶数位,经度
int j =0;
for (int i=0; i< numbits*2;i+=2) {
boolean isSet = false;
if ( i < buffer.length() )
isSet = buffer.charAt(i) == '1';
lonset.set(j++, isSet);
}
//奇数位,纬度
j=0;
for (int i=1; i< numbits*2;i+=2) {
boolean isSet = false;
if ( i < buffer.length() )
isSet = buffer.charAt(i) == '1';
latset.set(j++, isSet);
}
double lon = decode(lonset, -180, 180);
double lat = decode(latset, -90, 90);
return new double[] {lat, lon};
}
public static void main(String[] args) throws Exception{
GeoHash geohash = new GeoHash();
// String s = geohash.encode(40.222012, 116.248283);
// System.out.println(s);
geohash.getArroundGeoHash(40.222012, 116.248283);
// double[] geo = geohash.decode(s);
// System.out.println(geo[0]+" "+geo[1]);
}
}xxxxxxxxxxbr public class GeoHash {brpublic static final double MINLAT = -90;brpublic static final double MAXLAT = 90;brpublic static final double MINLNG = -180;brpublic static final double MAXLNG = 180;brbrprivate static int numbits = 3 * 5; //经纬度单独编码长度brbrprivate static double minLat;brprivate static double minLng;brbrprivate final static char[] digits = { '0', '1', '2', '3', '4', '5', '6', '7', '8',br '9', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n', 'p',br 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' };brbr//定义编码映射关系brfinal static HashMap<Character, Integer> lookup = new HashMap<Character, Integer>();br//初始化编码映射内容brstatic {br int i = 0;br for (char c : digits)br lookup.put(c, i++);br}brbrpublic GeoHash(){br setMinLatLng();br}brbrpublic String encode(double lat, double lon) {br BitSet latbits = getBits(lat, -90, 90);br BitSet lonbits = getBits(lon, -180, 180);br StringBuilder buffer = new StringBuilder();br for (int i = 0; i < numbits; i++) {br buffer.append( (lonbits.get(i))?'1':'0');br buffer.append( (latbits.get(i))?'1':'0');br }br String code = base32(Long.parseLong(buffer.toString(), 2));br //Log.i("okunu", "encode lat = " + lat + " lng = " + lon + " code = " + code);br return code;br}brbrpublic ArrayList<String> getArroundGeoHash(double lat, double lon){br //Log.i("okunu", "getArroundGeoHash lat = " + lat + " lng = " + lon);br ArrayList<String> list = new ArrayList<>();br double uplat = lat + minLat;br double downLat = lat - minLat;brbr double leftlng = lon - minLng;br double rightLng = lon + minLng;brbr String leftUp = encode(uplat, leftlng);br list.add(leftUp);brbr String leftMid = encode(lat, leftlng);br list.add(leftMid);brbr String leftDown = encode(downLat, leftlng);br list.add(leftDown);brbr String midUp = encode(uplat, lon);br list.add(midUp);brbr String midMid = encode(lat, lon);br list.add(midMid);brbr String midDown = encode(downLat, lon);br list.add(midDown);brbr String rightUp = encode(uplat, rightLng);br list.add(rightUp);brbr String rightMid = encode(lat, rightLng);br list.add(rightMid);brbr String rightDown = encode(downLat, rightLng);br list.add(rightDown);brbr //Log.i("okunu", "getArroundGeoHash list = " + list.toString());br return list;br}brbr//根据经纬度和范围,获取对应的二进制brprivate BitSet getBits(double lat, double floor, double ceiling) {br BitSet buffer = new BitSet(numbits);br for (int i = 0; i < numbits; i++) {br double mid = (floor + ceiling) / 2;br if (lat >= mid) {br buffer.set(i);br floor = mid;br } else {br ceiling = mid;br }br }br return buffer;br}brbr//将经纬度合并后的二进制进行指定的32位编码brprivate String base32(long i) {br char[] buf = new char[65];br int charPos = 64;br boolean negative = (i < 0);br if (!negative){br i = -i;br }br while (i <= -32) {br buf[charPos--] = digits[(int) (-(i % 32))];br i /= 32;br }br buf[charPos] = digits[(int) (-i)];br if (negative){br buf[--charPos] = '-';br }br return new String(buf, charPos, (65 - charPos));br}brbrprivate void setMinLatLng() {br minLat = MAXLAT - MINLAT;br for (int i = 0; i < numbits; i++) {br minLat /= 2.0;br }br minLng = MAXLNG - MINLNG;br for (int i = 0; i < numbits; i++) {br minLng /= 2.0;br }br}brbr//根据二进制和范围解码brprivate double decode(BitSet bs, double floor, double ceiling) {br double mid = 0;br for (int i=0; i<bs.length(); i++) {br mid = (floor + ceiling) / 2;br if (bs.get(i))br floor = mid;br elsebr ceiling = mid;br }br return mid;br}brbr//对编码后的字符串解码brpublic double[] decode(String geohash) {br StringBuilder buffer = new StringBuilder();br for (char c : geohash.toCharArray()) {br int i = lookup.get(c) + 32;br buffer.append( Integer.toString(i, 2).substring(1) );br }brbr BitSet lonset = new BitSet();br BitSet latset = new BitSet();brbr //偶数位,经度br int j =0;br for (int i=0; i< numbits*2;i+=2) {br boolean isSet = false;br if ( i < buffer.length() )br isSet = buffer.charAt(i) == '1';br lonset.set(j++, isSet);br }brbr //奇数位,纬度br j=0;br for (int i=1; i< numbits*2;i+=2) {br boolean isSet = false;br if ( i < buffer.length() )br isSet = buffer.charAt(i) == '1';br latset.set(j++, isSet);br }brbr double lon = decode(lonset, -180, 180);br double lat = decode(latset, -90, 90);brbr return new double[] {lat, lon};br}brbrpublic static void main(String[] args) throws Exception{br GeoHash geohash = new GeoHash();br// String s = geohash.encode(40.222012, 116.248283);br// System.out.println(s);br geohash.getArroundGeoHash(40.222012, 116.248283);br// double[] geo = geohash.decode(s);br// System.out.println(geo[0]+" "+geo[1]);br}b核心算法获取任意两点距离核心算法获取任意两点距离
/**
* 计算地球上任意两点(经纬度)距离
*
* @param long1 第一点经度
* @param lat1 第一点纬度
* @param long2 第二点经度
* @param lat2 第二点纬度
* @return 返回距离 单位:米
*/
public static double Distance(double long1, double lat1, double long2, double lat2)
{
double a, b, R;
R = 6378137; // 地球半径
lat1 = lat1 * Math.PI / 180.0;
lat2 = lat2 * Math.PI / 180.0;
a = lat1 - lat2;
b = (long1 - long2) * Math.PI / 180.0;
double d;
double sa2, sb2;
sa2 = Math.sin(a / 2.0);
sb2 = Math.sin(b / 2.0);
d = 2* R * Math.asin(Math.sqrt(sa2 * sa2 + Math.cos(lat1) * Math.cos(lat2) * sb2 * sb2));
return d;
}xxxxxxxxxxbr /**brbr * 计算地球上任意两点(经纬度)距离 brbr * brbr * @param long1 第一点经度 brbr * @param lat1 第一点纬度 brbr * @param long2 第二点经度 brbr * @param lat2 第二点纬度 brbr * @return 返回距离 单位:米brbr */brbrpublic static double Distance(double long1, double lat1, double long2, double lat2)brbr{brbr double a, b, R;brbr R = 6378137; // 地球半径 brbr lat1 = lat1 * Math.PI / 180.0;brbr lat2 = lat2 * Math.PI / 180.0;brbr a = lat1 - lat2;brbr b = (long1 - long2) * Math.PI / 180.0;brbr double d;brbr double sa2, sb2;brbr sa2 = Math.sin(a / 2.0);brbr sb2 = Math.sin(b / 2.0);brbr d = 2* R * Math.asin(Math.sqrt(sa2 * sa2 + Math.cos(lat1) * Math.cos(lat2) * sb2 * sb2));brbr return d;brbr}关注下我的公众号吧,只为开源精神
相关推荐
- 如何设计一个优秀的电子商务产品详情页
-
加入人人都是产品经理【起点学院】产品经理实战训练营,BAT产品总监手把手带你学产品电子商务网站的产品详情页面无疑是设计师和开发人员关注的最重要的网页之一。产品详情页面是客户作出“加入购物车”决定的页面...
- 怎么在JS中使用Ajax进行异步请求?
-
大家好,今天我来分享一项JavaScript的实战技巧,即如何在JS中使用Ajax进行异步请求,让你的网页速度瞬间提升。Ajax是一种在不刷新整个网页的情况下与服务器进行数据交互的技术,可以实现异步加...
- 中小企业如何组建,管理团队_中小企业应当如何开展组织结构设计变革
-
前言写了太多关于产品的东西觉得应该换换口味.从码农到架构师,从前端到平面再到UI、UE,最后走向了产品这条不归路,其实以前一直再给你们讲.产品经理跟项目经理区别没有特别大,两个岗位之间有很...
- 前端监控 SDK 开发分享_前端监控系统 开源
-
一、前言随着前端的发展和被重视,慢慢的行业内对于前端监控系统的重视程度也在增加。这里不对为什么需要监控再做解释。那我们先直接说说需求。对于中小型公司来说,可以直接使用三方的监控,比如自己搭建一套免费的...
- Ajax 会被 fetch 取代吗?Axios 怎么办?
-
大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!今天给大家带来的主题是ajax、fetch...
- 前端面试题《AJAX》_前端面试ajax考点汇总
-
1.什么是ajax?ajax作用是什么?AJAX=异步JavaScript和XML。AJAX是一种用于创建快速动态网页的技术。通过在后台与服务器进行少量数据交换,AJAX可以使网页实...
- Ajax 详细介绍_ajax
-
1、ajax是什么?asynchronousjavascriptandxml:异步的javascript和xml。ajax是用来改善用户体验的一种技术,其本质是利用浏览器内置的一个特殊的...
- 6款可替代dreamweaver的工具_替代powerdesigner的工具
-
dreamweaver对一个web前端工作者来说,再熟悉不过了,像我07年接触web前端开发就是用的dreamweaver,一直用到现在,身边的朋友有跟我推荐过各种更好用的可替代dreamweaver...
- 我敢保证,全网没有再比这更详细的Java知识点总结了,送你啊
-
接下来你看到的将是全网最详细的Java知识点总结,全文分为三大部分:Java基础、Java框架、Java+云数据小编将为大家仔细讲解每大部分里面的详细知识点,别眨眼,从小白到大佬、零基础到精通,你绝...
- 福斯《死侍》发布新剧照 "小贱贱"韦德被改造前造型曝光
-
时光网讯福斯出品的科幻片《死侍》今天发布新剧照,其中一张是较为罕见的死侍在被改造之前的剧照,其余两张剧照都是死侍在执行任务中的状态。据外媒推测,片方此时发布剧照,预计是为了给不久之后影片发布首款正式预...
- 2021年超详细的java学习路线总结—纯干货分享
-
本文整理了java开发的学习路线和相关的学习资源,非常适合零基础入门java的同学,希望大家在学习的时候,能够节省时间。纯干货,良心推荐!第一阶段:Java基础重点知识点:数据类型、核心语法、面向对象...
- 不用海淘,真黑五来到你身边:亚马逊15件热卖爆款推荐!
-
Fujifilm富士instaxMini8小黄人拍立得相机(黄色/蓝色)扫二维码进入购物页面黑五是入手一个轻巧可爱的拍立得相机的好时机,此款是mini8的小黄人特别版,除了颜色涂装成小黄人...
- 2025 年 Python 爬虫四大前沿技术:从异步到 AI
-
作为互联网大厂的后端Python爬虫开发,你是否也曾遇到过这些痛点:面对海量目标URL,单线程爬虫爬取一周还没完成任务;动态渲染的SPA页面,requests库返回的全是空白代码;好不容易...
- 最贱超级英雄《死侍》来了!_死侍超燃
-
死侍Deadpool(2016)导演:蒂姆·米勒编剧:略特·里斯/保罗·沃尼克主演:瑞恩·雷诺兹/莫蕾娜·巴卡林/吉娜·卡拉诺/艾德·斯克林/T·J·米勒类型:动作/...
- 停止javascript的ajax请求,取消axios请求,取消reactfetch请求
-
一、Ajax原生里可以通过XMLHttpRequest对象上的abort方法来中断ajax。注意abort方法不能阻止向服务器发送请求,只能停止当前ajax请求。停止javascript的ajax请求...
- 一周热门
- 最近发表
- 标签列表
-
- HTML 简介 (30)
- HTML 响应式设计 (31)
- HTML URL 编码 (32)
- HTML Web 服务器 (31)
- HTML 表单属性 (32)
- HTML 音频 (31)
- HTML5 支持 (33)
- HTML API (36)
- HTML 总结 (32)
- HTML 全局属性 (32)
- HTML 事件 (31)
- HTML 画布 (32)
- HTTP 方法 (30)
- 键盘快捷键 (30)
- CSS 语法 (35)
- CSS 轮廓宽度 (31)
- CSS 谷歌字体 (33)
- CSS 链接 (31)
- CSS 定位 (31)
- CSS 图片库 (32)
- CSS 图像精灵 (31)
- SVG 文本 (32)
- 时钟启动 (33)
- HTML 游戏 (34)
- JS Loop For (32)
