实现“附近的人”的方式原理

  • 前言
    • GeoHash算法
    • GeoHash算法在实际应用场景中遇到的问题及其解决方案
    • 基于mysql实现“附近的人”功能
    • 基于Mysql + GeoHash实现“附近的人”功能
    • 基于Redis + GeoHash实现“附近的人”功能

前言

前提:本文提供3种方式实现“附近的人”功能,在“附近的人” 功能的具体实现之前,先了解一下GeoHash 算法。(会使用较长的篇幅解析GeoHash 算法)

GeoHash算法

GeoHash算法就是将经纬度编码,将二维变一维,把二维的空间经纬度数据编码成一个字符串从而实现给地址位置分区的一种算法。实现GeoHash算法分为三个步骤:

  1. 首先将经纬度变成二进制;比如这样一个点(39.923201, 116.390705)
    纬度的范围是(-90,90),其中间值为0。对于纬度39.923201,在区间(0,90)中,因此得到一个1;(0,90)区间的中间值为45度,纬度39.923201小于45,因此得到一个0,依次计算下去,即可得到纬度的二进制表示,如下表:
  2. 将经纬度合并。
  3. 按照Base32进行编码。
  4. Geohash比直接用经纬度的高效很多,而且使用者可以发布地址编码,既能表明自己位于北海公园附近,又不至于暴露自己的精确坐标,有助于隐私保护。GeoHash表示的并不是一个点,而是一个矩形区域。
  5. GeoHash字符串越长,表示的位置越精确,字符串长度越长代表在距离上的误差越小。

GeoHash算法在实际应用场景中遇到的问题及其解决方案

  1. 边缘问题。在下图示例中,如果车在红点位置,区域内还有一个黄点。相邻区域内的绿点明显离红点更近。但因为黄点的编码和红点一样,最终找到的将是黄点。这就有问题了。所以要解决这个问题,只要再查找周边8个区域内的点,看哪个离自己更近即可。
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;elseceiling = 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]);
}
}
  1. 曲线突变问题。具体指两块区域转换后的字符串的编码可能非常的相近,但在实际中可能这两个区域距离非常远。

基于mysql实现“附近的人”功能

以用户为中心,假设给定一个500米的距离作为半径画一个圆,这个圆型区域内的所有用户就是符合用户要求的 “附近的人”。在圆形外套上一个正方形,通过获取用户经、纬度的最大最小值(经、纬度 + 距离),再根据最大最小值作为筛选条件,就很容易将正方形内的用户信息搜索出来。

但是这样就会有一个多出来的正方形四个角和圆之间空隙的用户,所以我们可以根据到圆点的距离一定比圆的半径要大,那么我们就计算用户中心点与正方形内所有用户的距离,筛选出所有距离小于等于半径的用户,圆形区域内的所用户即符合要求的“附近的人”。

弊端:纯基于 mysql 实现 “附近的人”,优点显而易见就是简单,只要建一张表存下用户的经、纬度信息即可。缺点也很明显,需要大量的计算两个点之间的距离,非常影响性能。

//maven依赖
<dependency><groupId>com.spatial4j</groupId><artifactId>spatial4j</artifactId><version>0.5</version>
</dependency>
/*** 获取附近x米的人** 自行计算外接正方形坐标及距离判断!!!!!!!!!!!!!!** @param distance 距离范围 单位km* @param userLng  当前经度* @param userLat  当前纬度* @return json*/@GetMapping("/nearby")public String nearBySearch(@RequestParam("distance") double distance,@RequestParam("userLng") double userLng,@RequestParam("userLat") double userLat) {double[] point = getGpsRange(userLng, userLat, distance);//1.获取位置在正方形内的所有用户List<User> users = userMapper.selectUser(point[0], point[1], point[2], point[3]);//2.剔除半径超过指定距离的多余用户users = users.stream().filter(a -> getDistance(a.getLongitude(), a.getLatitude(), userLng, userLat) <= distance).collect(Collectors.toList());return JSON.toJSONString(users);}/*** 获取附近x米的人* 使用第三方库计算外接正方形和距离!!!!!!!!* @param distance 距离范围 单位km* @param userLng  当前经度* @param userLat  当前纬度* @return json*/@GetMapping("/nearby1")public String nearBySearch1(@RequestParam("distance") double distance,@RequestParam("userLng") double userLng,@RequestParam("userLat") double userLat) {Rectangle rectangle = getRectangle(distance, userLng, userLat);//1.获取位置在正方形内的所有用户List<User> users = userMapper.selectUser(rectangle.getMinX(), rectangle.getMaxX(), rectangle.getMinY(), rectangle.getMaxY());//2.剔除半径超过指定距离的多余用户users = users.stream().filter(a -> getDistance1(a.getLongitude(), a.getLatitude(), userLng, userLat) <= distance).collect(Collectors.toList());return JSON.toJSONString(users);}/*********************************************  手动实现的工具方法 ***************************************************************///地球半径常量,kmprivate static final double EARTH_RADIUS = 6378.137;/*** 查询出某个范围内的最大经纬度和最小经纬度** @param longitude 当前位置经度* @param latitude  当前位置纬度* @param rangeDis  距离范围,单位km* @return*/public static double[] getGpsRange(double longitude, double latitude, double rangeDis) {double dlng = 2 * Math.asin(Math.sin(rangeDis / (2 * EARTH_RADIUS)) / Math.cos(latitude * Math.PI / 180));//角度转为弧度dlng = dlng * 180 / Math.PI;double dlat = rangeDis / EARTH_RADIUS;dlat = dlat * 180 / Math.PI;double minlng = longitude - dlng;double maxlng = longitude + dlng;double minlat = latitude - dlat;double maxlat = latitude + dlat;return new double[]{minlng, maxlng, minlat, maxlat};}/*** 根据地球上任意两点的经纬度计算两点间的距离,返回距离单位:km** @param longitude1 坐标1 经度* @param latitude1  坐标1 纬度* @param longitude2 坐标2 经度* @param latitude2  坐标2 纬度* @return 返回km*/public static double getDistance(double longitude1, double latitude1, double longitude2, double latitude2) {double radLat1 = rad(latitude1);double radLat2 = rad(latitude2);double a = radLat1 - radLat2;double b = rad(longitude1) - rad(longitude2);double distance = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2) +Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2)));distance = distance * EARTH_RADIUS;distance = Math.round(distance * 10000) / 10000.0;return distance;}/*** 角度转弧度** @param d* @return*/private static double rad(double d) {return d * Math.PI / 180.0;}/*********************************************  第三方工具方法 ***************************************************************//**** 球面中,两点间的距离(第三方库方法)* @param longitude 经度1* @param latitude  纬度1* @param userLng   经度2* @param userLat   纬度2* @return 返回距离,单位km*/private double getDistance1(Double longitude, Double latitude, double userLng, double userLat) {return spatialContext.calcDistance(spatialContext.makePoint(userLng, userLat),spatialContext.makePoint(longitude, latitude)) * DistanceUtils.DEG_TO_KM;}/*** 利用开源库计算外接正方形坐标* @param distance* @param userLng* @param userLat* @return*/private Rectangle getRectangle(double distance, double userLng, double userLat) {return spatialContext.getDistCalc().calcBoxByDistFromPt(spatialContext.makePoint(userLng, userLat), distance * DistanceUtils.KM_TO_DEG, spatialContext, null);}

基于Mysql + GeoHash实现“附近的人”功能

这种方式的设计思路更简单,在存用户位置信息时,根据用户经、纬度属性计算出相应的geohash字符串。注意:在计算geohash字符串时,需要指定geohash字符串的精度,也就是geohash字符串的长度,参考上边的geohash精度表。

当需要获取附近的人,只需用当前用户geohash字符串,数据库通过WHERE geohash Like ‘geocode%’ 来查询geohash字符串相似的用户,然后计算当前用户与搜索出的用户距离,筛选出所有距离小于等于指定距离(附近500米)的,即附近的人。

* 获取附近指定范围的人** @param distance 距离范围(附近多远的用户) 单位km* @param len      geoHash的精度(几位的字符串)* @param userLng  当前用户的经度* @param userLat  当前用户的纬度* @return json*/@GetMapping("/nearby")public String nearBySearch(@RequestParam("distance") double distance,@RequestParam("len") int len,@RequestParam("userLng") double userLng,@RequestParam("userLat") double userLat) {//1.根据要求的范围,确定geoHash码的精度,获取到当前用户坐标的geoHash码GeoHash geoHash = GeoHash.withCharacterPrecision(userLat, userLng, len);//2.获取到用户周边8个方位的geoHash码GeoHash[] adjacent = geoHash.getAdjacent();QueryWrapper<UserGeohash> queryWrapper = new QueryWrapper<UserGeohash>().likeRight("geo_code",geoHash.toBase32());Stream.of(adjacent).forEach(a -> queryWrapper.or().likeRight("geo_code",a.toBase32()));//3.匹配指定精度的geoHash码List<UserGeohash> users = userGeohashService.list(queryWrapper);//4.过滤超出距离的users = users.stream().filter(a ->getDistance(a.getLongitude(),a.getLatitude(),userLng,userLat)<= distance).collect(Collectors.toList());return JSON.toJSONString(users);}/**** 球面中,两点间的距离* @param longitude 经度1* @param latitude  纬度1* @param userLng   经度2* @param userLat   纬度2* @return 返回距离,单位km*/private double getDistance(Double longitude, Double latitude, double userLng, double userLat) {return spatialContext.calcDistance(spatialContext.makePoint(userLng, userLat),spatialContext.makePoint(longitude, latitude)) * DistanceUtils.DEG_TO_KM;}

基于Redis + GeoHash实现“附近的人”功能

Redis 3.2版本以后,基于geohash和数据结构Zset提供了地理位置相关功能。

 /*** 根据当前位置获取附近指定范围内的用户* @param distance 指定范围 单位km ,可根据{@link org.springframework.data.geo.Metrics} 进行设置* @param userLng 用户经度* @param userLat 用户纬度* @return*/public String nearBySearch(double distance, double userLng, double userLat) {List<User> users = new ArrayList<>();// 1.GEORADIUS获取附近范围内的信息GeoResults<RedisGeoCommands.GeoLocation<Object>> reslut = redisTemplate.opsForGeo().radius(KEY, new Circle(new Point(userLng, userLat), new Distance(distance, Metrics.KILOMETERS)),RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().includeCoordinates().sortAscending());//2.收集信息,存入listList<GeoResult<RedisGeoCommands.GeoLocation<Object>>> content = reslut.getContent();//3.过滤掉超过距离的数据content.forEach(a-> users.add(new User().setDistance(a.getDistance().getValue()).setLatitude(a.getContent().getPoint().getX()).setLongitude(a.getContent().getPoint().getY())));return JSON.toJSONString(users);}

实现“附近的人”的方式原理相关推荐

  1. python 编码解码原理_Python JSON编解码方式原理详解

    这篇文章主要介绍了Python JSON编解码方式原理详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 概念 JSON(JavaScript Ob ...

  2. 辽宁启迪教育:拼多多千人千面的原理是什么?

    现在有很多人都想自己创业开网店,但是却对拼多多很多原理都不了解,那么拼多多千人千面的原理是什么?就是很多人都想要知道的,下面就和辽宁启迪教育一起来看看吧! 千人千面是一种流量的展示规则,又包含了两种场 ...

  3. 三种Cache写入方式原理简介

    三种Cache写入方式原理简介 在386以上档次的微机中,为了提高系统效率,普遍采用Cache(高速缓冲存储器),现在的系统甚至可以拥有多级Cache.Cache实际上是位于CPU与DRAM主存储器之 ...

  4. Github上多人协作方式之一

    Github上多人协作方式之一 @(Github学习笔记) A拥有一个仓库RepoA, B看着不错,想着自己可以为这个仓库贡献一点东西,于是Fork这个仓库到自己账户上来. 这时候仓库还是在远程仓库里 ...

  5. 一天十道Java面试题----第四天(线程池复用的原理------>spring事务的实现方式原理以及隔离级别)

    这里是参考B站上的大佬做的面试题笔记.大家也可以去看视频讲解!!! 文章目录 31.线程池复用的原理 32.spring是什么? 33.对Aop的理解 34.对IOC的理解 35.BeanFactor ...

  6. 云顶之弈怎么防止被机器人拉_云顶之奕机器人勾人规律和原理-云顶之奕机器人勾人技巧讲解...

    云顶之奕机器人勾人规律和原理-云顶之奕机器人勾人技巧讲解 2019-08-08 作者:佚名  来源:本站整理  评论:0 lol云顶之奕游戏中新增加一个机器人角色,它有一个长勾子可以把敌人给勾到自己家 ...

  7. git分支合并之Fast-forword(快进方式)原理剖析

    git分支合并之Fast-forword(快进方式)原理剖析 git与svn创建分支差别 svn创建一个分支是将文件全部拷贝一份,而git则为其新的分支创建一个指针,其性能及效率相比与svn更加高效. ...

  8. mysql lbs 附近的人_一口气说出 4种 LBS “附近的人” 实现方式,面试官笑了

    引言 昨天一位公众号粉丝和我讨论了一道面试题,个人觉得比较有意义,这里整理了一下分享给大家,愿小伙伴们面试路上少踩坑.面试题目比较简单:"让你实现一个附近的人功能,你有什么方案?" ...

  9. 一口气说出 4种 “附近的人” 实现方式,面试官笑了

    引言 昨天一位公众号粉丝和我讨论了一道面试题,个人觉得比较有意义,这里整理了一下分享给大家,愿小伙伴们面试路上少踩坑.面试题目比较简单:"让你实现一个附近的人功能,你有什么方案?" ...

最新文章

  1. 《数学之美》第25章 条件随机场、文法分析及其他
  2. pythonChallenge:第1关
  3. 阿里百万级规模开源容器 PouchContainer GA 版本发布,邀您参与上海 Meetup 共话容器未来
  4. 机器人演唱邓丽君是真的吗_体验官|炒菜机器人真的实用吗
  5. ITK:将ITK灰度图像转换为CV :: Mat
  6. JCheckBox使用示例
  7. 【Nginx】截取URL中某个参数Parameter
  8. windows 环境怎样恢复 (oracle 11g grid) ocr voting 损坏的集群
  9. Cisco网络设备搭建×××服务器的全部过程
  10. Struts2请求处理的内部流程说明(版本二)
  11. Linux上构建一个RADIUS服务器详解
  12. 迁移实战:一次AntDB(基于pgxl分布式架构的数据库)数据库迁移经验分享
  13. iOS开发之NSData和NSString相互转换
  14. 五一劳动节,你在加班劳动吗?
  15. 《不只是美:信息图表设计原理与经典案例》—— 第1章 为何需要可视化:信息到智慧的升华...
  16. srvany.exe和instsrv.exe打包exe为windows服务趟的坑
  17. 怎么屏蔽还有照片_在朋友圈发男神的裸照,却忘了屏蔽父母,老妈的回应亮了…...
  18. 人力资源管理-各类激励理论
  19. Swing - 简单入门
  20. JAVA简单计算器(简单实现两数加减乘除)

热门文章

  1. 多传感器融合综述---FOV与BEV
  2. git 常用的命令行
  3. 扫码枪回车键条码_扫描枪怎么设置自动换行 条码扫描枪不自动回车怎么设置...
  4. dex字符串解密_某Xposed微信群发工具dex解密分析
  5. 微信公众平台开发——新增素材
  6. 数据增强-亮度-对比度-色彩饱和度-色调-锐度 不改变图像大小
  7. 服务器搭建——ftp
  8. Git 学习进展 (补发)
  9. 修改域服务器同步时间,配置Windows Server 2008 R2 域控制器的时间同步
  10. 关于java中输出流flush()方法