美文网首页
PHP-Geohash算法查找附近的人

PHP-Geohash算法查找附近的人

作者: 独居焚香 | 来源:发表于2020-04-22 19:38 被阅读0次

    最近需要做一个需求,查看动态时需要查找附近的人动态,因此呢就研究了下Geohash算法。
    一、优点:
    1、利用一个字段,即可存储经纬度;搜索时,只需一条索引,效率较高
    2、编码的前缀可以表示更大的区域,查找附近的,非常方便。 SQL中,LIKE 'wm3yr3%',即可查询附近的所有地点。
    二、直接上代码(Geohash算法具体可自行百度)

    <?php
    /**
     * Geohash
     * @author PwordC
     */
    
    class Geohash {
    
        private $bitss = array(16, 8, 4, 2, 1);
        private $neighbors = array();
        private $borders = array();
        
        private $coding = "0123456789bcdefghjkmnpqrstuvwxyz";
        private $codingMap = array();
    
        /**
         * 位数范围(单位km)
         * 1    ±2500
         * 2    ±630
         * 3    ±78
         * 4    ±20
         * 5    ±2.4
         * 6    ±0.61
         * 7    ±0.076
         * 8    ±0.01911
         * 9    ±0.00478
         * 10   ±0.0005971
         * 11   ±0.0001492
         * 12   ±0.0000186
         */
        
        public function Geohash() {
            
            $this->neighbors['right']['even'] = 'bc01fg45238967deuvhjyznpkmstqrwx';
            $this->neighbors['left']['even'] = '238967debc01fg45kmstqrwxuvhjyznp';
            $this->neighbors['top']['even'] = 'p0r21436x8zb9dcf5h7kjnmqesgutwvy';
            $this->neighbors['bottom']['even'] = '14365h7k9dcfesgujnmqp0r2twvyx8zb';
            
            $this->borders['right']['even'] = 'bcfguvyz';
            $this->borders['left']['even'] = '0145hjnp';
            $this->borders['top']['even'] = 'prxz';
            $this->borders['bottom']['even'] = '028b';
            
            $this->neighbors['bottom']['odd'] = $this->neighbors['left']['even'];
            $this->neighbors['top']['odd'] = $this->neighbors['right']['even'];
            $this->neighbors['left']['odd'] = $this->neighbors['bottom']['even'];
            $this->neighbors['right']['odd'] = $this->neighbors['top']['even'];
            
            $this->borders['bottom']['odd'] = $this->borders['left']['even'];
            $this->borders['top']['odd'] = $this->borders['right']['even'];
            $this->borders['left']['odd'] = $this->borders['bottom']['even'];
            $this->borders['right']['odd'] = $this->borders['top']['even'];
            
            for($i=0; $i<32; $i++) {
            
                $this->codingMap[substr($this->coding, $i, 1)] = str_pad(decbin($i), 5, "0", STR_PAD_LEFT);
            }
            
        }
    
        public function decode($hash) {
        
            $binary = "";
            $hl = strlen($hash);
            for ($i=0; $i<$hl; $i++) {
            
                $binary .= $this->codingMap[substr($hash, $i, 1)];
            }
            
            $bl = strlen($binary);
            $blat = "";
            $blng = "";
            for ($i=0; $i<$bl; $i++) {
            
                if ($i%2)
                    $blat=$blat.substr($binary, $i, 1);
                else
                    $blng=$blng.substr($binary, $i, 1);
                
            }
            
            $lat = $this->binDecode($blat, -90, 90);
            $lng = $this->binDecode($blng, -180, 180);
            
            $latErr = $this->calcError(strlen($blat), -90, 90);
            $lngErr = $this->calcError(strlen($blng), -180, 180);
                    
            $latPlaces = max(1, -round(log10($latErr))) - 1;
            $lngPlaces = max(1, -round(log10($lngErr))) - 1;
            
            $lat = round($lat, $latPlaces);
            $lng = round($lng, $lngPlaces);
    
            return array($lat, $lng);
        }
    
        
        private function calculateAdjacent($srcHash, $dir) {
        
            $srcHash = strtolower($srcHash);
            $lastChr = $srcHash[strlen($srcHash) - 1];
            $type = (strlen($srcHash) % 2) ? 'odd' : 'even';
            $base = substr($srcHash, 0, strlen($srcHash) - 1);
            
            if (strpos($this->borders[$dir][$type], $lastChr) !== false) {
                
                $base = $this->calculateAdjacent($base, $dir);  
            }
                
            return $base . $this->coding[strpos($this->neighbors[$dir][$type], $lastChr)];
        }
        
        
        public function neighbors($srcHash) {
        
            $geohashPrefix = substr($srcHash, 0, strlen($srcHash) - 1);
         
            $neighbors['top'] = $this->calculateAdjacent($srcHash, 'top');
            $neighbors['bottom'] = $this->calculateAdjacent($srcHash, 'bottom');
            $neighbors['right'] = $this->calculateAdjacent($srcHash, 'right');
            $neighbors['left'] = $this->calculateAdjacent($srcHash, 'left');
            
            $neighbors['topleft'] = $this->calculateAdjacent($neighbors['left'], 'top');
            $neighbors['topright'] = $this->calculateAdjacent($neighbors['right'], 'top');
            $neighbors['bottomright'] = $this->calculateAdjacent($neighbors['right'], 'bottom');
            $neighbors['bottomleft'] = $this->calculateAdjacent($neighbors['left'], 'bottom');
         
            return $neighbors;
        }
    
        public function encode($lat, $lng) {
        
            $plat = $this->precision($lat);
            $latbits = 1;
            $err = 45;
            while($err > $plat) {
            
                $latbits++;
                $err /= 2;
            }
            
            $plng = $this->precision($lng);
            $lngbits = 1;
            $err = 90;
            while($err > $plng) {
            
                $lngbits++;
                $err /= 2;
            }
            
            $bits = max($latbits, $lngbits);
    
            $lngbits = $bits;
            $latbits = $bits;
            $addlng = 1;
            while (($lngbits + $latbits) % 5 != 0) {
            
                $lngbits += $addlng;
                $latbits += !$addlng;
                $addlng = !$addlng;
            }
            
            
            $blat = $this->binEncode($lat, -90, 90, $latbits);
            $blng = $this->binEncode($lng, -180, 180, $lngbits);
            
            $binary = "";
            $uselng = 1;
            while (strlen($blat) + strlen($blng)) {
            
                if ($uselng) {
                
                    $binary = $binary.substr($blng, 0, 1);
                    $blng = substr($blng, 1);
                
                } else {
                
                    $binary = $binary.substr($blat, 0, 1);
                    $blat = substr($blat, 1);
                }
                
                $uselng = !$uselng;
            }
            
            $hash = "";
            for ($i=0; $i<strlen($binary); $i+=5) {
            
                $n = bindec(substr($binary, $i, 5));
                $hash = $hash.$this->coding[$n];
            }
            
            return $hash;
        }
    
        private function calcError($bits, $min, $max) {
        
            $err = ($max - $min) / 2;
            while ($bits--)
                $err /= 2;
            return $err;
        }
    
        private function precision($number) {
        
            $precision = 0;
            $pt = strpos($number,'.');
            if ($pt !== false) {
            
                $precision = -(strlen($number) - $pt - 1);
            }
            
            return pow(10, $precision) / 2;
        }
        
        private function binEncode($number, $min, $max, $bitcount) {
        
            if ($bitcount == 0)
                return "";
    
            $mid = ($min + $max) / 2;
            if ($number > $mid)
                return "1" . $this->binEncode($number, $mid, $max, $bitcount - 1);
            else
                return "0" . $this->binEncode($number, $min, $mid, $bitcount - 1);
        }
        
        private function binDecode($binary, $min, $max) {
        
            $mid = ($min + $max) / 2;
            
            if (strlen($binary) == 0)
                return $mid;
                
            $bit = substr($binary, 0, 1);
            $binary = substr($binary, 1);
            
            if ($bit == 1)
                return $this->binDecode($binary, $mid, $max);
            else
                return $this->binDecode($binary, $min, $mid);
        }
    }
    
    
    
    
    
    
    

    代码较长,不关心算法的情况下可以不关注具体内容,因此没写注释。
    使用时需要特别注意的一点就是距离范围
    具体使用方法如下:

    $geo = new Geohash();
    $hash = $geo->encode($lat,$lng);
    //默认取4位
    $prefix = substr($hash, 0, 4);
    //取出相邻区域,此处为一个数组区域
    $neighbors = $geo->neighbors($prefix);
    //需要将当前区域放进数组中
    array_push($neighbors, $prefix);
    

    特别说明一点,距离范围问题,上述代码注释中有说明 ↑↑↑
    之后使用sql like 既可以查询出附近的人的记录
    然后再计算距离即可;
    附距离算法:

    /**
     * 根据起点坐标和终点坐标测距离
     * @param  [array]   $from  [起点坐标(经纬度),例如:array(118.012951,36.810024)]
     * @param  [array]   $to    [终点坐标(经纬度)]
     * @param  [bool]    $km        是否以公里为单位 false:米 true:公里(千米)
     * @param  [int]     $decimal   精度 保留小数位数
     * @return [string]  距离数值
     * @author PwordC
     */
    function get_distance($from,$to,$km=true,$decimal=2){
        sort($from);
        sort($to);
        $EARTH_RADIUS = 6370.996; // 地球半径系数
    
        $distance = $EARTH_RADIUS*2*asin(sqrt(pow(sin( ($from[0]*pi()/180-$to[0]*pi()/180)/2),2)+cos($from[0]*pi()/180)*cos($to[0]*pi()/180)* pow(sin( ($from[1]*pi()/180-$to[1]*pi()/180)/2),2)))*1000;
    
        if($km){
            $distance = $distance / 1000;
        }
    
        return round($distance, $decimal);
    }
    

    具体有问题可以给我留言,看到会回复。

    相关文章

      网友评论

          本文标题:PHP-Geohash算法查找附近的人

          本文链接:https://www.haomeiwen.com/subject/angwihtx.html