GeoHash::decode()   A
last analyzed

Complexity

Conditions 5
Paths 7

Size

Total Lines 31
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 1 Features 1
Metric Value
cc 5
eloc 22
c 5
b 1
f 1
nc 7
nop 1
dl 0
loc 31
rs 9.2568
1
<?php
2
3
4
namespace MuCTS\GeoHash;
5
6
7
use MuCTS\GeoHash\Exceptions\InvalidArgumentException;
8
9
class GeoHash
10
{
11
    public const MIN_LAT = -90;
12
    public const MAX_LAT = 90;
13
    public const MIN_LON = -180;
14
    public const MAX_LON = 180;
15
16
    public const ERR_LNG = 90;
17
    public const ERR_LAT = 45;
18
19
    /** @var int $bits */
20
    private $bits;
21
    /** @var int $lonBits */
22
    private $lonBits;
23
    /** @var int $latBits */
24
    private $latBits;
25
26
    private $digits = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'];
27
28
    /**
29
     * Longitude and latitude separately encoded length
30
     * @param null|int $bits
31
     * @return $this
32
     */
33
    public function setBits(?int $bits)
34
    {
35
        $this->bits = $this->lonBits = $this->latBits = $bits;
36
        return $this;
37
    }
38
39
    /**
40
     * Geo Hash to Latitude and longitude
41
     * @param string $geoHash
42
     * @return array
43
     */
44
    public function decode(string $geoHash): array
45
    {
46
        $binary = '';
47
        for ($i = 0; $i < strlen($geoHash); $i++) {
48
            $digit = array_search($geoHash[$i], $this->digits);
49
            if (!is_int($digit)) {
50
                throw new InvalidArgumentException('%s is not a valid geo hash format text.');
51
            }
52
            $binary .= str_pad(decbin($digit), 5, '0', STR_PAD_LEFT);
53
        }
54
        $lonSet = [];
55
        $latSet = [];
56
        for ($i = 0; $i < strlen($binary); $i++) {
57
            if ($i % 2) {
58
                array_push($latSet, $binary[$i]);
59
            } else {
60
                array_push($lonSet, $binary[$i]);
61
            }
62
        }
63
        $lon = $this->bitsDecode($lonSet, self::MIN_LON, self::MAX_LON);
64
        $lat = $this->bitsDecode($latSet, self::MIN_LAT, self::MAX_LAT);
65
        $latErr = $this->calcError(count($latSet), self::MIN_LAT, self::MAX_LAT);
66
        $lonErr = $this->calcError(count($lonSet), self::MIN_LON, self::MAX_LON);
67
68
        $latPlaces = max(1, -round(log10($latErr))) - 1;
69
        $lonPlaces = max(1, -round(log10($lonErr))) - 1;
70
71
        $lat = round($lat, $latPlaces);
72
        $lon = round($lon, $lonPlaces);
73
74
        return [$lat, $lon];
75
    }
76
77
    /**
78
     * Encode a hash from given lat and long
79
     *
80
     * @param float $lat
81
     * @param float $lon
82
     * @return string
83
     */
84
    public function encode(float $lat, float $lon)
85
    {
86
        $this->adjustBits($lat, $lon);
87
        $latBits = $this->getBits($lat, self::MIN_LAT, self::MAX_LAT, $this->latBits);
88
        $lonBits = $this->getBits($lon, self::MIN_LON, self::MAX_LON, $this->lonBits);
89
        $binary = '';
90
        while (!empty($latBits) || !empty($lonBits)) {
91
            $binary .= array_shift($lonBits) ?? '';
92
            $binary .= array_shift($latBits) ?? '';
93
        }
94
        return $this->base32($binary);
95
    }
96
97
    /**
98
     * According to latitude and longitude and range, obtain the corresponding binary
99
     *
100
     * @param float $number
101
     * @param float $floor
102
     * @param float $ceiling
103
     * @param int $bits
104
     * @return array
105
     */
106
    private function getBits(float $number, float $floor, float $ceiling, int $bits): array
107
    {
108
        $buffer = [];
109
        for ($i = 0; $i < $bits; $i++) {
110
            $mid = ($floor + $ceiling) / 2;
111
            if ($number >= $mid) {
112
                array_push($buffer, 1);
113
                $floor = $mid;
114
            } else {
115
                array_push($buffer, 0);
116
                $ceiling = $mid;
117
            }
118
        }
119
        return $buffer;
120
    }
121
122
    /**
123
     * Binary to 32
124
     *
125
     * @param string $binary
126
     * @return string
127
     */
128
    private function base32(string $binary)
129
    {
130
        $hash = '';
131
        for ($i = 0; $i < strlen($binary); $i += 5) {
132
            $n = bindec(substr($binary, $i, 5));
133
            $hash = $hash . $this->digits[$n];
134
        }
135
        return $hash;
136
    }
137
138
    /**
139
     * Latitude Or longitude decoding
140
     *
141
     * @param array $bs
142
     * @param float $floor
143
     * @param float $ceiling
144
     * @return float
145
     */
146
    private function bitsDecode(array $bs, float $floor, float $ceiling): float
147
    {
148
        $mid = 0;
149
        $len = count($bs);
150
        for ($i = 0; $i < $len; $i++) {
151
            $mid = ($floor + $ceiling) / 2;
152
            if ($bs[$i] == 1)
153
                $floor = $mid;
154
            else
155
                $ceiling = $mid;
156
        }
157
        return $mid;
158
    }
159
160
    /**
161
     * get precision
162
     * @param string $number
163
     * @return float
164
     */
165
    private function precision(string $number): float
166
    {
167
        $precision = 0;
168
        $pt = strpos($number, '.');
169
        if ($pt !== false) {
170
            $precision = -(strlen($number) - $pt - 1);
171
        }
172
        return pow(10, $precision) / 2;
173
    }
174
175
    private function preBits(float $number, int $err)
176
    {
177
        $pre = $this->precision(strval($number));
178
        $bits = 1;
179
        while ($err > $pre) {
180
            $bits++;
181
            $err /= 2;
182
        }
183
        return $bits;
184
    }
185
186
    private function adjustBits(float $lat, float $lon): void
187
    {
188
        if (!$this->bits) {
189
            $this->setBits(max($this->preBits($lat, self::ERR_LAT), $this->preBits($lon, self::ERR_LNG)));
190
        }
191
        $bit = 1;
192
        while (($this->lonBits + $this->latBits) % 5 != 0) {
193
            $this->lonBits += $bit;
194
            $this->latBits += !$bit;
195
            $bit = !$bit;
196
        }
197
    }
198
199
    private function calcError(int $bits, float $floor, float $ceiling): float
200
    {
201
        $err = ($ceiling - $floor) / 2;
202
        while ($bits--)
203
            $err /= 2;
204
        return $err;
205
    }
206
}