123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224 |
- <?php
- /**
- * Geohash 空间距离服务类
- * > 算法过程简述
- * 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;
- }
- }
|