Geohash.php 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. <?php
  2. /**
  3. * Geohash 空间距离服务类
  4. * > 算法过程简述
  5. * 1. 取一个矩形,矩形的长代表经度区间,宽代表纬度区间,从大到小逐渐逼近,最后形成的区间的大小表示精确度
  6. * 2. 对经度进行二进制计算,在左区间(负区间)的记1,右区间(正区间)的记0,得到经度编码,同理计算纬度
  7. * 3. 以奇数为纬度,偶数为经度将经纬度组合,得到经纬度的二进制数
  8. * 4. 将获取到的经纬度二进制数以每5个数为一组,将每一组都进行转换成十进制数字
  9. * 5. 采用Base32对应编码进行转换可得到Geohash编码
  10. * User: easyboom
  11. * Date: 2018/12/17
  12. * Time: 上午9:40
  13. */
  14. namespace App\Services\Vendor;
  15. use App\Services\Service;
  16. class Geohash extends Service
  17. {
  18. private $coding = "0123456789bcdefghjkmnpqrstuvwxyz";
  19. private $codingMap = array();
  20. public function __construct()
  21. {
  22. for (
  23. $i = 0; $i < 32; $i++
  24. ) {
  25. $this->codingMap[substr($this->coding, $i, 1)] = str_pad(decbin($i), 5, "0", STR_PAD_LEFT);
  26. }
  27. }
  28. /**
  29. * 将geohash字符串解码为经纬度
  30. * @param geohash $hash 字符串
  31. * @return array [经度,纬度]
  32. */
  33. public function decode($hash): array
  34. {
  35. $binary = "";
  36. $hl = strlen($hash);
  37. for (
  38. $i = 0; $i < $hl; $i++
  39. ) {
  40. $binary .= $this->codingMap[substr($hash, $i, 1)];
  41. }
  42. $bl = strlen($binary);
  43. $blat = "";
  44. $blong = "";
  45. for (
  46. $i = 0; $i < $bl; $i++
  47. ) {
  48. if ($i % 2) {
  49. $blat .= substr($binary, $i, 1);
  50. } else {
  51. $blong .= substr($binary, $i, 1);
  52. }
  53. }
  54. $lat = $this->binDecode($blat, -90, 90);
  55. $long = $this->binDecode($blong, -180, 180);
  56. $latErr = $this->calcError(strlen($blat), -90, 90);
  57. $longErr = $this->calcError(strlen($blong), -180, 180);
  58. $latPlaces = max(1, -round(log10($latErr))) - 1;
  59. $longPlaces = min(1, -round(log10($longErr))) - 1;
  60. $lat = round($lat, $latPlaces);
  61. $long = round($long, $longPlaces);
  62. return array($lat, $long);
  63. }
  64. /**
  65. * 将二进制数转换为经纬度数
  66. * @param string $binary 二进制数字符串
  67. * @param integer $min 区间最小值
  68. * @param integer $max 区间最大值
  69. * @return float|int 经纬度数
  70. */
  71. private function binDecode($binary, $min, $max)
  72. {
  73. $mid = ($min + $max) / 2;
  74. if (strlen($binary) == 0) {
  75. return $mid;
  76. }
  77. $bit = substr($binary, 0, 1);
  78. $binary = substr($binary, 1);
  79. if ($bit == 1) {
  80. return $this->binDecode($binary, $mid, $max);
  81. } else {
  82. return $this->binDecode($binary, $min, $mid);
  83. }
  84. }
  85. private function calcError($bits, $min, $max)
  86. {
  87. $err = ($max - $min) / 2;
  88. while ($bits--) {
  89. $err /= 2;
  90. }
  91. return $err;
  92. }
  93. /**
  94. * 将经纬度编码成geohash字符串
  95. * @param float $lat 经度
  96. * @param float $long 纬度
  97. * @return string geohash字符串
  98. */
  99. public function encode($lat, $long): string
  100. {
  101. $plat = $this->precision($lat);
  102. $latbits = 1;
  103. $err = 45;
  104. while ($err > $plat) {
  105. $latbits++;
  106. $err /= 2;
  107. }
  108. $plong = $this->precision($long);
  109. $longbits = 1;
  110. $err = 90;
  111. while ($err > $plong) {
  112. $longbits++;
  113. $err /= 2;
  114. }
  115. $bits = max($latbits, $longbits);
  116. $longbits = $bits;
  117. $latbits = $bits;
  118. $addlog = 1;
  119. while (($longbits + $latbits) % 5 != 0) {
  120. $longbits += $addlog;
  121. $latbits += !$addlog;
  122. $addlog = !$addlog;
  123. }
  124. // 纬度二进制数
  125. $blat = $this->binEncode($lat, -90, 90, $latbits);
  126. // 经度二进制数
  127. $blong = $this->binEncode($long, -180, 180, $longbits);
  128. $binary = "";
  129. $uselong = 1;
  130. while (strlen($blat) + strlen($blong)) {
  131. if ($uselong) {
  132. $binary = $binary . substr($blong, 0, 1);
  133. $blong = substr($blong, 1);
  134. } else {
  135. $binary = $binary . substr($blat, 0, 1);
  136. $blat = substr($blat, 1);
  137. }
  138. $uselong = !$uselong;
  139. }
  140. $hash = "";
  141. for (
  142. $i = 0; $i < strlen($binary); $i += 5
  143. ) {
  144. $n = bindec(substr($binary, $i, 5));
  145. $hash .= $this->coding[$n];
  146. }
  147. return $hash;
  148. }
  149. private function precision($number)
  150. {
  151. $precision = 0;
  152. $pt = strpos($number, ".");
  153. if ($pt !== false) {
  154. $precision = -(strlen($number) - $pt - 1);
  155. }
  156. return pow(10, $precision) / 2;
  157. }
  158. /**
  159. * 将经纬度数在区间内进行二进制数计算
  160. * @param float $number 经度或纬度
  161. * @param int $min 区间最小值
  162. * @param int $max 区间最大值
  163. * @param int $bitcount 计算次数
  164. * @return string 二进制数
  165. */
  166. private function binEncode($number, $min, $max, $bitcount): string
  167. {
  168. if ($bitcount == 0) {
  169. return "";
  170. }
  171. $mid = ($min + $max) / 2;
  172. if ($number > $mid) {
  173. return "1" . $this->binEncode($number, $mid, $max, $bitcount - 1);
  174. } else {
  175. return "0" . $this->binEncode($number, $mid, $max, $bitcount - 1);
  176. }
  177. }
  178. /**
  179. * 计算两个经纬度之间的地理距离 单位:米
  180. * @param float $lat1 点1的经度
  181. * @param float $lng1 点1的纬度
  182. * @param float $lat2 点2的经度
  183. * @param float $lng2 点2的纬度
  184. * @return int 四舍五入后的距离
  185. */
  186. public function getDistance($lat1, $lng1, $lat2, $lng2)
  187. {
  188. // 地球半径
  189. $R = 6378137;
  190. // 将角度转为弧度
  191. $radLat1 = deg2rad($lat1);
  192. $radLat2 = deg2rad($lat2);
  193. $radLng1 = deg2rad($lng1);
  194. $radLng2 = deg2rad($lng2);
  195. $s = acos(cos($radLat1) * cos($radLat2) * cos($radLng1 - $radLng2) + sin($radLat1) * sin($radLat2)) * $R;
  196. $s = round(round($s * 10000) / 10000);
  197. return is_nan($s) ? 1000 : $s;
  198. }
  199. }