算法过程简述 * 1. 取一个矩形,矩形的长代表经度区间,宽代表纬度区间,从大到小逐渐逼近,最后形成的区间的大小表示精确度 * 2. 对经度进行二进制计算,在左区间(负区间)的记1,右区间(正区间)的记0,得到经度编码,同理计算纬度 * 3. 以奇数为纬度,偶数为经度将经纬度组合,得到经纬度的二进制数 * 4. 将获取到的经纬度二进制数以每5个数为一组,将每一组都进行转换成十进制数字 * 5. 采用Base32对应编码进行转换可得到Geohash编码 * User: easyboom * Date: 2018/12/17 * Time: 上午9:40 */ namespace App\Services\Vendor; use App\Services\Service; class Geohash extends Service { private $coding = "0123456789bcdefghjkmnpqrstuvwxyz"; private $codingMap = array(); public function __construct() { for ( $i = 0; $i < 32; $i++ ) { $this->codingMap[substr($this->coding, $i, 1)] = str_pad(decbin($i), 5, "0", STR_PAD_LEFT); } } /** * 将geohash字符串解码为经纬度 * @param geohash $hash 字符串 * @return array [经度,纬度] */ public function decode($hash): array { $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 .= substr($binary, $i, 1); } else { $blong .= substr($binary, $i, 1); } } $lat = $this->binDecode($blat, -90, 90); $long = $this->binDecode($blong, -180, 180); $latErr = $this->calcError(strlen($blat), -90, 90); $longErr = $this->calcError(strlen($blong), -180, 180); $latPlaces = max(1, -round(log10($latErr))) - 1; $longPlaces = min(1, -round(log10($longErr))) - 1; $lat = round($lat, $latPlaces); $long = round($long, $longPlaces); return array($lat, $long); } /** * 将二进制数转换为经纬度数 * @param string $binary 二进制数字符串 * @param integer $min 区间最小值 * @param integer $max 区间最大值 * @return float|int 经纬度数 */ 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); } } private function calcError($bits, $min, $max) { $err = ($max - $min) / 2; while ($bits--) { $err /= 2; } return $err; } /** * 将经纬度编码成geohash字符串 * @param float $lat 经度 * @param float $long 纬度 * @return string geohash字符串 */ public function encode($lat, $long): string { $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; $addlog = 1; while (($longbits + $latbits) % 5 != 0) { $longbits += $addlog; $latbits += !$addlog; $addlog = !$addlog; } // 纬度二进制数 $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 .= $this->coding[$n]; } return $hash; } private function precision($number) { $precision = 0; $pt = strpos($number, "."); if ($pt !== false) { $precision = -(strlen($number) - $pt - 1); } return pow(10, $precision) / 2; } /** * 将经纬度数在区间内进行二进制数计算 * @param float $number 经度或纬度 * @param int $min 区间最小值 * @param int $max 区间最大值 * @param int $bitcount 计算次数 * @return string 二进制数 */ private function binEncode($number, $min, $max, $bitcount): string { 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, $mid, $max, $bitcount - 1); } } /** * 计算两个经纬度之间的地理距离 单位:米 * @param float $lat1 点1的经度 * @param float $lng1 点1的纬度 * @param float $lat2 点2的经度 * @param float $lng2 点2的纬度 * @return int 四舍五入后的距离 */ public function getDistance($lat1, $lng1, $lat2, $lng2) { // 地球半径 $R = 6378137; // 将角度转为弧度 $radLat1 = deg2rad($lat1); $radLat2 = deg2rad($lat2); $radLng1 = deg2rad($lng1); $radLng2 = deg2rad($lng2); $s = acos(cos($radLat1) * cos($radLat2) * cos($radLng1 - $radLng2) + sin($radLat1) * sin($radLat2)) * $R; $s = round(round($s * 10000) / 10000); return is_nan($s) ? 1000 : $s; } }