<?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;
    }
}
