zoukankan      html  css  js  c++  java
  • 跟据经纬度实现附近搜索Java实现

    现在很多手机软件都用附近搜索功能,但具体是怎么实现的呢》
    在网上查了很多资料,mysql空间数据库、矩形算法、geohash我都用过了,当数据上了百万之后mysql空间数据库方法是最强最精确的(查询前100条数据只需5秒左右)。

    接下来推出一个原创计算方法,查询速度是mysql空间数据库算法的2倍

    $lng是你的经度,$lat是你的纬度  

    SELECT lng,lat,
    (POWER(MOD(ABS(lng - $lng),360),2) + POWER(ABS(lat - $lat),2)) AS distance
    FROM `user_location`
    ORDER BY distance LIMIT 100

     经测试,在100万数据中取前100条数据只需2.5秒左右。


    ###########################################


    另外的几种算法还是在这里展示一下:

     ps:赤道半径 6378.137km

    平均地球半径 6371.004km

    一、距形算法

     1 define(EARTH_RADIUS, 6371);//地球半径,平均半径为6371km
     2  /**
     3  *计算某个经纬度的周围某段距离的正方形的四个点
     4  *
     5  *@param lng float 经度
     6  *@param lat float 纬度
     7  *@param distance float 该点所在圆的半径,该圆与此正方形内切,默认值为0.5千米
     8  *@return array 正方形的四个点的经纬度坐标
     9  */
    10  function returnSquarePoint($lng, $lat,$distance = 0.5){
    11  
    12     $dlng =  2 * asin(sin($distance / (2 * EARTH_RADIUS)) / cos(deg2rad($lat)));
    13     $dlng = rad2deg($dlng);
    14  
    15     $dlat = $distance/EARTH_RADIUS;
    16     $dlat = rad2deg($dlat);
    17  
    18     return array(
    19                 'left-top'=>array('lat'=>$lat + $dlat,'lng'=>$lng-$dlng),
    20                 'right-top'=>array('lat'=>$lat + $dlat, 'lng'=>$lng + $dlng),
    21                 'left-bottom'=>array('lat'=>$lat - $dlat, 'lng'=>$lng - $dlng),
    22                 'right-bottom'=>array('lat'=>$lat - $dlat, 'lng'=>$lng + $dlng)
    23                 );
    24  }
    25 //使用此函数计算得到结果后,带入sql查询。
    26 $squares = returnSquarePoint($lng, $lat);
    27 $info_sql = "select id,locateinfo,lat,lng from `lbs_info` where lat<>0 and lat>{$squares['right-bottom']['lat']} and lat<{$squares['left-top']['lat']} and lng>{$squares['left-top']['lng']} and lng<{$squares['right-bottom']['lng']} ";

    java代码如下:

     1 /**
     2  * 默认地球半径
     3  */
     4 private static double EARTH_RADIUS = 6371;
     5  
     6 /**
     7  * 计算经纬度点对应正方形4个点的坐标
     8  *
     9  * @param longitude
    10  * @param latitude
    11  * @param distance
    12  * @return
    13  */
    14 public static Map<String, double[]> returnLLSquarePoint(double longitude,
    15         double latitude, double distance) {
    16     Map<String, double[]> squareMap = new HashMap<String, double[]>();
    17     // 计算经度弧度,从弧度转换为角度
    18     double dLongitude = 2 * (Math.asin(Math.sin(distance
    19             / (2 * EARTH_RADIUS))
    20             / Math.cos(Math.toRadians(latitude))));
    21     dLongitude = Math.toDegrees(dLongitude);
    22     // 计算纬度角度
    23     double dLatitude = distance / EARTH_RADIUS;
    24     dLatitude = Math.toDegrees(dLatitude);
    25     // 正方形
    26     double[] leftTopPoint = { latitude + dLatitude, longitude - dLongitude };
    27     double[] rightTopPoint = { latitude + dLatitude, longitude + dLongitude };
    28     double[] leftBottomPoint = { latitude - dLatitude,
    29             longitude - dLongitude };
    30     double[] rightBottomPoint = { latitude - dLatitude,
    31             longitude + dLongitude };
    32     squareMap.put("leftTopPoint", leftTopPoint);
    33     squareMap.put("rightTopPoint", rightTopPoint);
    34     squareMap.put("leftBottomPoint", leftBottomPoint);
    35     squareMap.put("rightBottomPoint", rightBottomPoint);
    36     return squareMap;
    37 }

    二、 空间数据库算法

    以下location字段是跟据经纬度来生成的空间数据,如:
    location字段的type设为point
    "update feed set location=GEOMFROMTEXT('point({$lat} {$lng})') where id='{$id}'"

    mysql空间数据查询

     1 SET @center = GEOMFROMTEXT('POINT(35.801559 -10.501577)');
     2         SET @radius = 4000;
     3         SET @bbox = CONCAT('POLYGON((',
     4         X(@center) - @radius, ' ', Y(@center) - @radius, ',',
     5         X(@center) + @radius, ' ', Y(@center) - @radius, ',',
     6         X(@center) + @radius, ' ', Y(@center) + @radius, ',',
     7         X(@center) - @radius, ' ', Y(@center) + @radius, ',',
     8         X(@center) - @radius, ' ', Y(@center) - @radius, '))'
     9         );
    10 SELECT id,lng,lat,
    11         SQRT(POW( ABS( X(location) - X(@center)), 2) + POW( ABS(Y(location) - Y(@center)), 2 )) AS distance
    12         FROM `user_location` WHERE 1=1
    13         AND INTERSECTS( location, GEOMFROMTEXT(@bbox) )
    14         AND SQRT(POW( ABS( X(location) - X(@center)), 2) + POW( ABS(Y(location) - Y(@center)), 2 )) < @radius
    15         ORDER BY distance LIMIT 20

    三、geo算法

     参考文档:

    http://blog.csdn.net/wangxiafghj/article/details/9014363geohash  算法原理及实现方式
    http://blog.charlee.li/geohash-intro/  geohash:用字符串实现附近地点搜索
    http://blog.sina.com.cn/s/blog_7c05385f0101eofb.html    查找附近点--Geohash方案讨论
    http://www.wubiao.info/372        查找附近的xxx 球面距离以及Geohash方案探讨
    http://en.wikipedia.org/wiki/Haversine_formula       Haversine formula球面距离公式
    http://www.codecodex.com/wiki/Calculate_Distance_Between_Two_Points_on_a_Globe   球面距离公式代码实现
    http://developer.baidu.com/map/jsdemo.htm#a6_1   球面距离公式验证  
    http://www.wubiao.info/470     Mysql or Mongodb LBS快速实现方案


    geohash有以下几个特点:

    首先,geohash用一个字符串表示经度和纬度两个坐标。某些情况下无法在两列上同时应用索引 (例如MySQL 4之前的版本,Google App Engine的数据层等),利用geohash,只需在一列上应用索引即可。

    其次,geohash表示的并不是一个点,而是一个矩形区域。比如编码wx4g0ec19,它表示的是一个矩形区域。 使用者可以发布地址编码,既能表明自己位于北海公园附近,又不至于暴露自己的精确坐标,有助于隐私保护。

    第三,编码的前缀可以表示更大的区域。例如wx4g0ec1,它的前缀wx4g0e表示包含编码wx4g0ec1在内的更大范围。 这个特性可以用于附近地点搜索。首先根据用户当前坐标计算geohash(例如wx4g0ec1)然后取其前缀进行查询 (SELECT * FROM place WHERE geohash LIKE 'wx4g0e%'),即可查询附近的所有地点。

    查找附近网点geohash算法及实现 (Java版本),geohashjava


    Geohash比直接用经纬度的高效很多。

    Geohash算法实现(Java版本)

      1 package com.DistTest;
      2 import java.util.BitSet;
      3 import java.util.HashMap;
      4  
      5 public class Geohash {
      6  
      7         private static int numbits = 6 * 5;
      8         final static char[] digits = { '0', '1', '2', '3', '4', '5', '6', '7', '8',
      9                         '9', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n', 'p',
     10                         'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' };
     11         
     12         final static HashMap<Character, Integer> lookup = new HashMap<Character, Integer>();
     13         static {
     14                 int i = 0;
     15                 for (char c : digits)
     16                         lookup.put(c, i++);
     17         }
     18  
     19         public double[] decode(String geohash) {
     20                 StringBuilder buffer = new StringBuilder();
     21                 for (char c : geohash.toCharArray()) {
     22  
     23                         int i = lookup.get(c) + 32;
     24                         buffer.append( Integer.toString(i, 2).substring(1) );
     25                 }
     26                 
     27                 BitSet lonset = new BitSet();
     28                 BitSet latset = new BitSet();
     29                 
     30                 //even bits
     31                 int j =0;
     32                 for (int i=0; i< numbits*2;i+=2) {
     33                         boolean isSet = false;
     34                         if ( i < buffer.length() )
     35                           isSet = buffer.charAt(i) == '1';
     36                         lonset.set(j++, isSet);
     37                 }
     38                 
     39                 //odd bits
     40                 j=0;
     41                 for (int i=1; i< numbits*2;i+=2) {
     42                         boolean isSet = false;
     43                         if ( i < buffer.length() )
     44                           isSet = buffer.charAt(i) == '1';
     45                         latset.set(j++, isSet);
     46                 }
     47                //中国地理坐标:东经73°至东经135°,北纬4°至北纬53°
     48                 double lon = decode(lonset, 70, 140);
     49                 double lat = decode(latset, 0, 60);
     50                 
     51                 return new double[] {lat, lon};       
     52         }
     53         
     54         private double decode(BitSet bs, double floor, double ceiling) {
     55                 double mid = 0;
     56                 for (int i=0; i<bs.length(); i++) {
     57                         mid = (floor + ceiling) / 2;
     58                         if (bs.get(i))
     59                                 floor = mid;
     60                         else
     61                                 ceiling = mid;
     62                 }
     63                 return mid;
     64         }
     65         
     66         
     67         public String encode(double lat, double lon) {
     68                 BitSet latbits = getBits(lat, 0, 60);
     69                 BitSet lonbits = getBits(lon, 70, 140);
     70                 StringBuilder buffer = new StringBuilder();
     71                 for (int i = 0; i < numbits; i++) {
     72                         buffer.append( (lonbits.get(i))?'1':'0');
     73                         buffer.append( (latbits.get(i))?'1':'0');
     74                 }
     75                 return base32(Long.parseLong(buffer.toString(), 2));
     76         }
     77  
     78         private BitSet getBits(double lat, double floor, double ceiling) {
     79                 BitSet buffer = new BitSet(numbits);
     80                 for (int i = 0; i < numbits; i++) {
     81                         double mid = (floor + ceiling) / 2;
     82                         if (lat >= mid) {
     83                                 buffer.set(i);
     84                                 floor = mid;
     85                         } else {
     86                                 ceiling = mid;
     87                         }
     88                 }
     89                 return buffer;
     90         }
     91  
     92         public static String base32(long i) {
     93                 char[] buf = new char[65];
     94                 int charPos = 64;
     95                 boolean negative = (i < 0);
     96                 if (!negative)
     97                         i = -i;
     98                 while (i <= -32) {
     99                         buf[charPos--] = digits[(int) (-(i % 32))];
    100                         i /= 32;
    101                 }
    102                 buf[charPos] = digits[(int) (-i)];
    103  
    104                 if (negative)
    105                         buf[--charPos] = '-';
    106                 return new String(buf, charPos, (65 - charPos));
    107         }
    108  
    109 }
    View Code

    球面距离公式:(个人喜欢这个算法)

     1 /**
     2      * @method 以下用于计算两个经纬度之间的距离
     3      * */
     4     private double EARTH_RADIUS = 6378.137;//地球半径
     5     private static double rad(double d)
     6     {
     7        return d * Math.PI / 180.0;
     8     }
     9 
    10     public double GetDistance(double lat1, double lng1, double lat2, double lng2)
    11     {
    12        double radLat1 = rad(lat1);
    13        double radLat2 = rad(lat2);
    14        double a = radLat1 - radLat2;
    15        double b = rad(lng1) - rad(lng2);
    16 
    17        double s = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a/2),2) +
    18         Math.cos(radLat1)*Math.cos(radLat2)*Math.pow(Math.sin(b/2),2)));
    19        s = s * EARTH_RADIUS;
    20        s = Math.round(s * 100) / 100.0;
    21        return s;
    22     }
    View Code

    附近网点距离排序

     1 package com.DistTest;
     2   
     3 import java.sql.DriverManager;
     4 import java.sql.ResultSet;
     5 import java.sql.SQLException;
     6 import java.sql.Connection;
     7 import java.sql.Statement;
     8   
     9   
    10 public class sqlTest {
    11      
    12     public static void main(String[] args) throws Exception {
    13         Connection conn = null;
    14         String sql;
    15         String url = "jdbc:mysql://132.97.**.**/test?"
    16                 + "user=***&password=****&useUnicode=true&characterEncoding=UTF8";
    17   
    18         try {
    19             Class.forName("com.mysql.jdbc.Driver");// 动态加载mysql驱动
    20             // System.out.println("成功加载MySQL驱动程序");
    21             // 一个Connection代表一个数据库连接
    22             conn = DriverManager.getConnection(url);
    23             // Statement里面带有很多方法,比如executeUpdate可以实现插入,更新和删除等
    24             Statement stmt = conn.createStatement();
    25             sql = "select * from retailersinfotable limit 1,10";
    26             ResultSet rs = stmt.executeQuery(sql);// executeQuery会返回结果的集合,否则返回空值
    27               double lon1=109.0145193757; 
    28               double lat1=34.236080797698;
    29             System.out.println("当前位置:");
    30             int i=0;
    31             String[][] array = new String[10][3];
    32             while (rs.next()){
    33                     //从数据库取出地理坐标
    34                     double lon2=Double.parseDouble(rs.getString("Longitude"));
    35                     double lat2=Double.parseDouble(rs.getString("Latitude"));
    36                      
    37                     //根据地理坐标,生成geohash编码
    38                       Geohash geohash = new Geohash();
    39                     String geocode=geohash.encode(lat2, lon2).substring(0, 9);
    40                      
    41                     //计算两点间的距离
    42                       int dist=(int) Test.GetDistance(lon1, lat1, lon2, lat2);
    43                        
    44                       array[i][0]=String.valueOf(i);
    45                     array[i][1]=geocode;
    46                     array[i][2]=Integer.toString(dist);
    47                        
    48                       i++;
    49          
    50                 //    System.out.println(lon2+"---"+lat2+"---"+geocode+"---"+dist);   
    51                 }
    52  
    53             array=sqlTest.getOrder(array); //二维数组排序
    54             sqlTest.showArray(array);        //打印数组
    55  
    56              
    57              
    58              
    59         } catch (SQLException e) {
    60             System.out.println("MySQL操作错误");
    61             e.printStackTrace();
    62         } finally {
    63             conn.close();
    64         }
    65   
    66     }
    67     /*
    68      * 二维数组排序,比较array[][2]的值,返回二维数组
    69      * */
    70     public static String[][] getOrder(String[][] array){
    71         for (int j = 0; j < array.length ; j++) {
    72             for (int bb = 0; bb < array.length - 1; bb++) {
    73                 String[] ss;
    74                 int a1=Integer.valueOf(array[bb][2]);  //转化成int型比较大小
    75                 int a2=Integer.valueOf(array[bb+1][2]);
    76                 if (a1>a2) {
    77                     ss = array[bb];
    78                     array[bb] = array[bb + 1];
    79                     array[bb + 1] = ss;
    80                      
    81                 }
    82             }
    83         }
    84         return array;
    85     }
    86      
    87     /*打印数组*/
    88     public static void showArray(String[][] array){
    89           for(int a=0;a<array.length;a++){
    90               for(int j=0;j<array[0].length;j++)
    91                   System.out.print(array[a][j]+" ");
    92               System.out.println();
    93           }
    94     }
    95  
    96 }
    View Code

    一直在琢磨LBS,期待可以发现更好的方案。现在纠结了。

    简单列举一下已经了解到的方案:
    1.sphinx geo索引
    2.mongodb geo索引
    3.mysql sql查询
    4.mysql+geohash
    5.redis+geohash

    然后列举一下需求:
    1.实时性要高,有频繁的更新和读取
    2.可按距离排序支持分页
    3.支持多条件筛选(一个经纬度数据还包含其他属性,比如社交系统的性别、年龄)

    方案简单介绍:
    1.sphinx geo索引
    支持按照距离排序,并支持分页。但是尝试mva+geo失败,还在找原因。
    无法满足高实时性需求。(可能是不了解实时增量索引配置有误)
    资源占用小,速度快

    2.mongodb geo索引
    支持按照距离排序,并支持分页。支持多条件筛选。
    可满足实时性需求。
    资源占用大,数据量达到百万级请流量在10w左右查询速度明显下降。

    3.mysql+geohash/ mysql sql查询
    不支持按照距离排序(代价太大)。支持分页。支持多条件筛选。
    可满足实时性需求。
    资源占用中等,查询速度不及mongodb。
    且geohash按照区块将球面转化平面并切割。暂时没有找到跨区块查询方法(不太了解)。

    4.redis+geohash
    geohash缺点不再赘述
    不支持距离排序。支持分页查询。不支持多条件筛选。
    可满足实时性需求。
    资源占用最小。查询速度很快。

    ------update
    补充一下测试机配置:
    1TB SATA硬盘。8GB RAM。I3 2350 双核四线程

    转自http://www.open-open.com/lib/view/open1421650750328.html

    一、距形算法

  • 相关阅读:
    矩阵管理——本质是职能分工,例如所有部门都执行财务部门制定的财务制度而不会各自为政
    linkedin databus介绍——监听数据库变化,有新数据到来时通知其他消费者app,新数据存在内存里,多份快照
    ES忽略TF-IDF评分——使用constant_score
    ES设置字段搜索权重——Query-Time Boosting
    lucene内置的评分函数
    ES搜索排序,文档相关度评分介绍——Vector Space Model
    ES搜索排序,文档相关度评分介绍——TF-IDF—term frequency, inverse document frequency, and field-length norm—are calculated and stored at index time.
    ES搜索排序,文档相关度评分介绍——Field-length norm
    ES 搜索结果expalain 可以类似数据库性能调优来看排序算法的选择
    设计模式之多例模式
  • 原文地址:https://www.cnblogs.com/xiaoliu66007/p/4676280.html
Copyright © 2011-2022 走看看