最近需要做一個需求,查看動態(tài)時需要查找附近的人動態(tài)督禽,因此呢就研究了下Geohash算法脆霎。
一、優(yōu)點:
1赂蠢、利用一個字段绪穆,即可存儲經(jīng)緯度;搜索時虱岂,只需一條索引玖院,效率較高
2、編碼的前綴可以表示更大的區(qū)域第岖,查找附近的难菌,非常方便。 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();
/**
* 位數(shù)范圍(單位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);
}
}
代碼較長燎窘,不關(guān)心算法的情況下可以不關(guān)注具體內(nèi)容,因此沒寫注釋蹄咖。
使用時需要特別注意的一點就是距離范圍
具體使用方法如下:
$geo = new Geohash();
$hash = $geo->encode($lat,$lng);
//默認(rèn)取4位
$prefix = substr($hash, 0, 4);
//取出相鄰區(qū)域,此處為一個數(shù)組區(qū)域
$neighbors = $geo->neighbors($prefix);
//需要將當(dāng)前區(qū)域放進(jìn)數(shù)組中
array_push($neighbors, $prefix);
特別說明一點褐健,距離范圍問題,上述代碼注釋中有說明 ↑↑↑
之后使用sql like 既可以查詢出附近的人的記錄
然后再計算距離即可澜汤;
附距離算法:
/**
* 根據(jù)起點坐標(biāo)和終點坐標(biāo)測距離
* @param [array] $from [起點坐標(biāo)(經(jīng)緯度),例如:array(118.012951,36.810024)]
* @param [array] $to [終點坐標(biāo)(經(jīng)緯度)]
* @param [bool] $km 是否以公里為單位 false:米 true:公里(千米)
* @param [int] $decimal 精度 保留小數(shù)位數(shù)
* @return [string] 距離數(shù)值
* @author PwordC
*/
function get_distance($from,$to,$km=true,$decimal=2){
sort($from);
sort($to);
$EARTH_RADIUS = 6370.996; // 地球半徑系數(shù)
$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);
}
具體有問題可以給我留言蚜迅,看到會回復(fù)。