php通过geohash算法实现查找附近的商铺
geohash有以下几个特点:
首先,geohash用一个字符串表示经度和纬度两个坐标。利用geohash,只需在一列上应用索引即可。
其次,geohash表示的并不是一个点,而是一个矩形区域。比如编码wx4g0ec19,它表示的是一个矩形区域。 使用者可以发布地址编码,既能表明自己位于北海公园附近,又不至于暴露自己的精确坐标,有助于隐私保护。
第三,编码的前缀可以表示更大的区域。例如wx4g0ec1,它的前缀wx4g0e表示包含编码wx4g0ec1在内的更大范围。 这个特性可以用于附近地点搜索。首先根据用户当前坐标计算geohash(例如wx4g0ec1)然后取其前缀进行查询 (SELECT * FROM place WHERE geohash LIKE 'wx4g0e%'),即可查询附近的所有地点。
Geohash比直接用经纬度的高效很多。
Geohash基础类:
<?php /** * Geohash generation class for php */ /** * * Encode and decode geohashes * * Find neighbors * */ class Geohash { private $bitss = [16, 8, 4, 2, 1]; private $neighbors = []; private $borders = []; private $coding = "0123456789bcdefghjkmnpqrstuvwxyz"; private $codingMap = []; public function __construct() { $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']; //build map from encoding char to 0 padded bitfield for($i=0; $i<32; $i++) { $this->codingMap[substr($this->coding, $i, 1)] = str_pad(decbin($i), 5, "0", STR_PAD_LEFT); } } /** * Decode a geohash and return an array with decimal lat,long in it */ public function decode($hash) { //decode hash into binary string $binary = ""; $hl = strlen($hash); for ($i=0; $i<$hl; $i++) { $binary .= $this->codingMap[substr($hash, $i, 1)]; } //split the binary into lat and log binary strings $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); } } //now concert to decimal $lat = $this->binDecode($blat, -90, 90); $long = $this->binDecode($blong, -180, 180); //figure out how precise the bit count makes this calculation $latErr = $this->calcError(strlen($blat), -90, 90); $longErr = $this->calcError(strlen($blong), -180, 180); //how many decimal places should we use? There's a little art to //this to ensure I get the same roundings as geohash.org $latPlaces = max(1, -round(log10($latErr))) - 1; $longPlaces = max(1, -round(log10($longErr))) - 1; //round it $lat = round($lat, $latPlaces); $long = round($long, $longPlaces); return array($lat, $long); } 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; } /** * Encode a hash from given lat and long */ public function encode($lat, $long) { //how many bits does latitude need? $plat = $this->precision($lat); $latbits = 1; $err = 45; while ($err > $plat) { $latbits++; $err /= 2; } //how many bits does longitude need? $plong = $this->precision($long); $longbits = 1; $err = 90; while ($err > $plong) { $longbits++; $err /= 2; } //bit counts need to be equal $bits = max($latbits, $longbits); //as the hash create bits in groups of 5, lets not //waste any bits - lets bulk it up to a multiple of 5 //and favour the longitude for any odd bits $longbits = $bits; $latbits = $bits; $addlong = 1; while (($longbits + $latbits) % 5 != 0) { $longbits += $addlong; $latbits += !$addlong; $addlong = !$addlong; } //encode each as binary string $blat = $this->binEncode($lat, -90, 90, $latbits); $blong = $this->binEncode($long, -180, 180, $longbits); //merge lat and long together $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; } //convert binary string to hash $hash = ""; for ($i=0; $i<strlen($binary); $i+=5) { $n = bindec(substr($binary, $i, 5)); $hash = $hash.$this->coding[$n]; } return $hash; } /** * What's the maximum error for $bits bits covering a range $min to $max */ private function calcError($bits, $min, $max) { $err = ($max - $min) / 2; while ($bits--) { $err /= 2; } return $err; } /* * returns precision of number * precision of 42 is 0.5 * precision of 42.4 is 0.05 * precision of 42.41 is 0.005 etc * */ private function precision($number) { $precision = 0; $pt = strpos($number,'.'); if ($pt !== false) { $precision = -(strlen($number) - $pt - 1); } return pow(10, $precision) / 2; } /** * create binary encoding of number as detailed in http://en.wikipedia.org/wiki/Geohash#Example * removing the tail recursion is left an exercise for the reader * * Author: Bruce Chen (weibo: @一个开发者) */ private function binEncode($number, $min, $max, $bitcount) { if ($bitcount == 0) return ""; #echo "$bitcount: $min $max<br>"; //this is our mid point - we will produce a bit to say //whether $number is above or below this mid point $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); } } /** * decodes binary encoding of number as detailed in http://en.wikipedia.org/wiki/Geohash#Example * removing the tail recursion is left an exercise for the reader * */ 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); } } }
测试实例:
$geohash = new GeoHash(); $hash = $geohash->encode($latitude, $longitude); //var_dump($hash);exit; //决定查询范围,值越大,获取的范围越小 //当geohash base32编码长度为8时,精度在19米左右,而当编码长度为9时,精度在2米左右,编码长度需要根据数据情况进行选择。 $pre_hash = substr($hash, 0, 5); //取出相邻八个区域 $neighbors = $geohash->neighbors($pre_hash); array_push($neighbors, $pre_hash); $values = ''; foreach ($neighbors as $key=>$val) { $values .= '\'' . $val . '\'' .','; } $values = substr($values, 0, -1); // var_dump($values); $stores=\DB::select("SELECT * FROM `stores` WHERE LEFT(`geohash`,5) IN ($values)"); foreach ($stores as $key => $value) { $geohash_arr=$geohash->decode($value->geohash); $stores[$key]->latitude=$geohash_arr[0];//纬度 $stores[$key]->longitude=$geohash_arr[1];//经度 $distance=$this->getDistance($request['latitude'],$request['longitude'],$value->latitude,$value->longitude); $stores[$key]->distance=$distance; $sortdistance[$key] = $distance; } array_multisort($sortdistance,SORT_ASC,$stores); // var_dump($stores); return response()->json(['status_code'=>0,'nearby_stores'=>$stores]); /** * @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); //四舍五入 }