查找附近網點geohash算法及實現 (PHP版本)

 

geohash有如下幾個特色:php

首先,geohash用一個字符串表示經度和緯度兩個座標。某些狀況下沒法在兩列上同時應用索引 (例如MySQL 4以前的版本,Google App Engine的數據層等),利用geohash,只需在一列上應用索引便可。mysql

其次,geohash表示的並非一個點,而是一個矩形區域。好比編碼wx4g0ec19,它表示的是一個矩形區域。 使用者能夠發佈地址編碼,既能代表本身位於北海公園附近,又不至於暴露本身的精確座標,有助於隱私保護。git

第三,編碼的前綴能夠表示更大的區域。例如wx4g0ec1,它的前綴wx4g0e表示包含編碼wx4g0ec1在內的更大範圍。 這個特性能夠用於附近地點搜索。首先根據用戶當前座標計算geohash(例如wx4g0ec1)而後取其前綴進行查詢 (SELECT * FROM place WHERE geohash LIKE 'wx4g0e%'),便可查詢附近的全部地點。算法

Geohash比直接用經緯度的高效不少。sql

 

Geohash算法實現(PHP版本)數據庫

<?php
class Geohash
{
    private $coding="0123456789bcdefghjkmnpqrstuvwxyz";
    private $codingMap=array();
  
    public function Geohash()
    {
        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="";
        $blong="";
        for ($i=0; $i<$bl; $i++)
        {
            if ($i%2)
                $blat=$blat.substr($binary,$i,1);
            else
                $blong=$blong.substr($binary,$i,1);
  
        }
  
         $lat=$this->binDecode($blat,-90,90);
         $long=$this->binDecode($blong,-180,180);
        // $lat=$this->binDecode($blat,2,54);
        // $long=$this->binDecode($blong,72,136);
  
        $latErr=$this->calcError(strlen($blat),-90,90);
        $longErr=$this->calcError(strlen($blong),-180,180);
  
        $latPlaces=max(1, -round(log10($latErr))) - 1;
        $longPlaces=max(1, -round(log10($longErr))) - 1;
  
        $lat=round($lat, $latPlaces);
        $long=round($long, $longPlaces);
  
        return array($lat,$long);
    }
  
    public function encode($lat,$long)
    {
        $plat=$this->precision($lat);
        $latbits=1;
        $err=45;
        while($err>$plat)
        {
            $latbits++;
            $err/=2;
        }
  
        $plong=$this->precision($long);
        $longbits=1;
        $err=90;
        while($err>$plong)
        {
            $longbits++;
            $err/=2;
        }
  
        $bits=max($latbits,$longbits);
  
        $longbits=$bits;
        $latbits=$bits;
        $addlong=1;
        while (($longbits+$latbits)%5 != 0)
        {
            $longbits+=$addlong;
            $latbits+=!$addlong;
            $addlong=!$addlong;
        }
  
        $blat=$this->binEncode($lat,-90,90, $latbits);
  
        $blong=$this->binEncode($long,-180,180,$longbits);
  
        $binary="";
        $uselong=1;
        while (strlen($blat)+strlen($blong))
        {
            if ($uselong)
            {
                $binary=$binary.substr($blong,0,1);
                $blong=substr($blong,1);
            }
            else
            {
                $binary=$binary.substr($blat,0,1);
                $blat=substr($blat,1);
            }
            $uselong=!$uselong;
        }
  
        $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);
    }
}
  
?>

測試app

<?php
  
require_once("db_config.php");
require_once('geohash.class.php');
  
 
$geohash=new Geohash;
  
//經緯度轉換成Geohash
  
//獲取附近的信息
 
$n_latitude  =  34.236080797698;
$n_longitude = 109.0145193757;
 
echo "當前位置爲:經度108.7455,緯度34.3608<br/><br/>
如下網點離我最近:";
  
//開始
$b_time = microtime(true);
  
//方案A,直接利用數據庫存儲函數,遍歷排序
  
//方案B geohash求出附近,而後排序
  
//當前 geohash值
 $n_geohash = $geohash->encode($n_latitude,$n_longitude);
  
//附近
$n = 3;
$like_geohash = substr($n_geohash, 0, $n);
 
$sql = 'select * from retailersinfotable where geohash like "'.$like_geohash.'%"';
  
 
 
$query = mysql_query($sql);
	if(mysql_num_rows($query))
	{
		while($row=mysql_fetch_array($query))
		{
			$data[] = array (
							"latitude" =>$row["Latitude"],
							"longitude"=>$row["Longitude"],
							"name"     =>$row["RetailersName"],
					);
		}
}
  
//算出實際距離
 foreach($data as $key=>$val)
{
    $distance = getDistance($n_latitude,$n_longitude,$val['latitude'],$val['longitude']);
    $data[$key]['distance'] = $distance;
  
    //排序列
    $sortdistance[$key] = $distance;
} 
  
 //距離排序
array_multisort($sortdistance,SORT_ASC,$data);
  
//結束
$e_time = microtime(true);
 
echo "(計算耗時:" ;
echo $e_time - $b_time; 
echo "秒)<br/>";
  
//var_dump($data);
 
  foreach($data as $key=>$val)
{
	echo "</br>";
	echo $val['distance']. " 米-------".$val['name'];
 
} 
 
/**
*  @desc 根據兩點間的經緯度計算距離
*  @param float $latitude 緯度值
*  @param float $longitude 經度值
*/
function getDistance($latitude1, $longitude1, $latitude2, $longitude2) 
{
	$earth_radius = 6371000;   //approximate radius of earth in meters
	
	$dLat = deg2rad($latitude2 - $latitude1);
	$dLon = deg2rad($longitude2 - $longitude1);
     /*
       Using the
       Haversine formula
 
       http://en.wikipedia.org/wiki/Haversine_formula
	   http://www.codecodex.com/wiki/Calculate_Distance_Between_Two_Points_on_a_Globe
       驗證:百度地圖  http://developer.baidu.com/map/jsdemo.htm#a6_1
       calculate the distance
     */	
	$a = sin($dLat/2) * sin($dLat/2) + cos(deg2rad($latitude1)) * cos(deg2rad($latitude2)) * sin($dLon/2) * sin($dLon/2);
	$c = 2 * asin(sqrt($a));
	$d = $earth_radius * $c;
	
	return round($d);   //四捨五入
}
  
?>
相關文章
相關標籤/搜索