GeoHash::decode()   C
last analyzed

Complexity

Conditions 13
Paths 65

Size

Total Lines 77
Code Lines 62

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 62
c 0
b 0
f 0
dl 0
loc 77
rs 6.1224
cc 13
nc 65
nop 1

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
namespace geoPHP\Adapter;
3
4
use geoPHP\Geometry\Geometry;
5
use geoPHP\Geometry\Point;
6
use geoPHP\Geometry\LineString;
7
use geoPHP\Geometry\Polygon;
8
9
/**
10
 * PHP Geometry GeoHash encoder/decoder.
11
 *
12
 * @author prinsmc
13
 * @see http://en.wikipedia.org/wiki/Geohash
14
 *
15
 */
16
class GeoHash implements GeoAdapter
17
{
18
    /**
19
     * @var string
20
     * @noinspection SpellCheckingInspection */
21
    public static $characterTable = "0123456789bcdefghjkmnpqrstuvwxyz";
22
23
    /**
24
     * @var array<string, array<string,string>> of neighbouring hash character maps.
25
     */
26
    private static $neighbours = [
27
        // north
28
        'top' => [
29
            'even' => 'p0r21436x8zb9dcf5h7kjnmqesgutwvy',
30
            'odd' => 'bc01fg45238967deuvhjyznpkmstqrwx'
31
        ],
32
        // east
33
        'right' => [
34
            'even' => 'bc01fg45238967deuvhjyznpkmstqrwx',
35
            'odd' => 'p0r21436x8zb9dcf5h7kjnmqesgutwvy'
36
        ],
37
        // west
38
        'left' => [
39
            'even' => '238967debc01fg45kmstqrwxuvhjyznp',
40
            'odd' => '14365h7k9dcfesgujnmqp0r2twvyx8zb'
41
        ],
42
        // south
43
        'bottom' => [
44
            'even' => '14365h7k9dcfesgujnmqp0r2twvyx8zb',
45
            'odd' => '238967debc01fg45kmstqrwxuvhjyznp'
46
        ]
47
    ];
48
49
    /**
50
     * @var array<string, array<string,string>> of bordering hash character maps.
51
     */
52
    private static $borders = [
53
        // north
54
        'top' => [
55
            'even' => 'prxz',
56
            'odd' => 'bcfguvyz'
57
        ],
58
        // east
59
        'right' => [
60
            'even' => 'bcfguvyz',
61
            'odd' => 'prxz'
62
        ],
63
        // west
64
        'left' => [
65
            'even' => '0145hjnp',
66
            'odd' => '028b'
67
        ],
68
        // south
69
        'bottom' => [
70
            'even' => '028b',
71
            'odd' => '0145hjnp'
72
        ]
73
    ];
74
75
    /**
76
     * Convert the geoHash to a Point. The point is 2-dimensional.
77
     *
78
     * @param string $hash a GeoHash
79
     * @param bool $asGrid Return the center point of hash grid or the grid cell as Polygon
80
     * @return Point|Polygon the converted GeoHash
81
     */
82
    public function read(string $hash, bool $asGrid = false): Geometry
83
    {
84
        $decodedHash = $this->decode($hash);
85
        if (!$asGrid) {
86
            return new Point($decodedHash['centerLongitude'], $decodedHash['centerLatitude']);
87
        }
88
        
89
        return new Polygon(
90
            [
91
                new LineString(
92
                    [
93
                        new Point($decodedHash['minLongitude'], $decodedHash['maxLatitude']),
94
                        new Point($decodedHash['maxLongitude'], $decodedHash['maxLatitude']),
95
                        new Point($decodedHash['maxLongitude'], $decodedHash['minLatitude']),
96
                        new Point($decodedHash['minLongitude'], $decodedHash['minLatitude']),
97
                        new Point($decodedHash['minLongitude'], $decodedHash['maxLatitude']),
98
                    ]
99
                )
100
            ]
101
        );
102
    }
103
104
    /**
105
     * Convert the geometry to geohash.
106
     *
107
     * @param Geometry $geometry
108
     * @param float|null $precision default 0.0000001
109
     * @return string the GeoHash or null when the $geometry is not a Point
110
     */
111
    public function write(Geometry $geometry, $precision = 0.0000001): string
112
    {
113
        if ($geometry->isEmpty()) {
114
            return '';
115
        }
116
117
        if ($geometry->geometryType() === Geometry::POINT) {
118
            /** @var Point $geometry */
119
            return $this->encodePoint($geometry, $precision);
120
        }
121
        
122
        // The GeoHash is the smallest hash grid ID that fits the envelope
123
        $envelope = $geometry->envelope();
124
        
125
        if ($envelope === null) {
126
            return '';
127
        }
128
        
129
        $geoHashes = [];
130
        $geohash = '';
131
        foreach ($envelope->getPoints() as $point) {
132
            $geoHashes[] = $this->encodePoint($point, $precision);
133
        }
134
135
        $i = 0;
136
        while ($i < strlen($geoHashes[0])) {
137
            $char = $geoHashes[0][$i];
138
139
            foreach ($geoHashes as $hash) {
140
                if ($hash[$i] != $char) {
141
                    return $geohash;
142
                }
143
            }
144
            $geohash .= $char;
145
            ++$i;
146
        }
147
148
        return $geohash;
149
    }
150
151
    /**
152
     * algorithm based on code by Alexander Songe
153
     * @author Alexander Songe <[email protected]>
154
     * @see https://github.com/asonge/php-geohash/issues/1
155
     *
156
     * @param Point $point
157
     * @param float|null $precision
158
     * @return string The GeoHash
159
     * @throws \Exception
160
     */
161
    private function encodePoint($point, $precision = null)
162
    {
163
        $minLatitude = -90.0000000000001;
164
        $maxLatitude = 90.0000000000001;
165
        $minLongitude = -180.0000000000001;
166
        $maxLongitude = 180.0000000000001;
167
        $latitudeError = 90;
168
        $longitudeError = 180;
169
        $i = 0;
170
        $error = 180;
171
        $hash = '';
172
        $x = $point->getX();
173
        $y = $point->getY();
174
        
175
        if (!is_numeric($precision)) {
176
            $lap = strlen(strval($y)) - strpos(strval($y), ".");
177
            $lop = strlen(strval($x)) - strpos(strval($x), ".");
178
            $precision = pow(10, -max($lap - 1, $lop - 1, 0)) / 2;
179
        }
180
181
        if ($x < $minLongitude || $y < $minLatitude ||
182
            $x > $maxLongitude || $y > $maxLatitude
183
        ) {
184
            throw new \Exception("Point coordinates (" . $x . ", " . $y . ") are out of lat/lon range");
185
        }
186
187
        while ($error >= $precision) {
188
            $chr = 0;
189
            for ($b = 4; $b >= 0; --$b) {
190
                if ((1 & $b) == (1 & $i)) {
191
                    // even char, even bit OR odd char, odd bit...a lon
192
                    $next = ($minLongitude + $maxLongitude) / 2;
193
                    if ($x > $next) {
194
                        $chr |= pow(2, $b);
195
                        $minLongitude = $next;
196
                    } else {
197
                        $maxLongitude = $next;
198
                    }
199
                    $longitudeError /= 2;
200
                } else {
201
                    // odd char, even bit OR even char, odd bit...a lat
202
                    $next = ($minLatitude + $maxLatitude) / 2;
203
                    if ($y > $next) {
204
                        $chr |= pow(2, $b);
205
                        $minLatitude = $next;
206
                    } else {
207
                        $maxLatitude = $next;
208
                    }
209
                    $latitudeError /= 2;
210
                }
211
            }
212
            $hash .= self::$characterTable[$chr];
213
            $i++;
214
            $error = min($latitudeError, $longitudeError);
215
        }
216
        return $hash;
217
    }
218
219
    /**
220
     * algorithm based on code by Alexander Songe
221
     * @author Alexander Songe <[email protected]>
222
     * @see https://github.com/asonge/php-geohash/issues/1
223
     *
224
     * @param string $hash a GeoHash
225
     * @return array<string, int|float> Associative array.
226
     */
227
    private function decode($hash): array
228
    {
229
        $result = [];
230
        $minLatitude = -90;
231
        $maxLatitude = 90;
232
        $minLongitude = -180;
233
        $maxLongitude = 180;
234
        $latitudeError = 90;
235
        $longitudeError = 180;
236
        for ($i = 0, $c = strlen($hash); $i < $c; $i++) {
237
            $v = strpos(self::$characterTable, $hash[$i]);
238
            if (1 & $i) {
239
                if (16 & $v) {
240
                    $minLatitude = ($minLatitude + $maxLatitude) / 2;
241
                } else {
242
                    $maxLatitude = ($minLatitude + $maxLatitude) / 2;
243
                }
244
                if (8 & $v) {
245
                    $minLongitude = ($minLongitude + $maxLongitude) / 2;
246
                } else {
247
                    $maxLongitude = ($minLongitude + $maxLongitude) / 2;
248
                }
249
                if (4 & $v) {
250
                    $minLatitude = ($minLatitude + $maxLatitude) / 2;
251
                } else {
252
                    $maxLatitude = ($minLatitude + $maxLatitude) / 2;
253
                }
254
                if (2 & $v) {
255
                    $minLongitude = ($minLongitude + $maxLongitude) / 2;
256
                } else {
257
                    $maxLongitude = ($minLongitude + $maxLongitude) / 2;
258
                }
259
                if (1 & $v) {
260
                    $minLatitude = ($minLatitude + $maxLatitude) / 2;
261
                } else {
262
                    $maxLatitude = ($minLatitude + $maxLatitude) / 2;
263
                }
264
                $latitudeError /= 8;
265
                $longitudeError /= 4;
266
            } else {
267
                if (16 & $v) {
268
                    $minLongitude = ($minLongitude + $maxLongitude) / 2;
269
                } else {
270
                    $maxLongitude = ($minLongitude + $maxLongitude) / 2;
271
                }
272
                if (8 & $v) {
273
                    $minLatitude = ($minLatitude + $maxLatitude) / 2;
274
                } else {
275
                    $maxLatitude = ($minLatitude + $maxLatitude) / 2;
276
                }
277
                if (4 & $v) {
278
                    $minLongitude = ($minLongitude + $maxLongitude) / 2;
279
                } else {
280
                    $maxLongitude = ($minLongitude + $maxLongitude) / 2;
281
                }
282
                if (2 & $v) {
283
                    $minLatitude = ($minLatitude + $maxLatitude) / 2;
284
                } else {
285
                    $maxLatitude = ($minLatitude + $maxLatitude) / 2;
286
                }
287
                if (1 & $v) {
288
                    $minLongitude = ($minLongitude + $maxLongitude) / 2;
289
                } else {
290
                    $maxLongitude = ($minLongitude + $maxLongitude) / 2;
291
                }
292
                $latitudeError /= 4;
293
                $longitudeError /= 8;
294
            }
295
        }
296
        $result['minLatitude'] = $minLatitude;
297
        $result['minLongitude'] = $minLongitude;
298
        $result['maxLatitude'] = $maxLatitude;
299
        $result['maxLongitude'] = $maxLongitude;
300
        $result['centerLatitude'] = round(($minLatitude + $maxLatitude) / 2, (int) max(1, -round(log10($latitudeError))) - 1);
301
        $result['centerLongitude'] = round(($minLongitude + $maxLongitude) / 2, (int) max(1, -round(log10($longitudeError))) - 1);
302
        
303
        return $result;
304
    }
305
306
    /**
307
     * Calculates the adjacent geohash of the geohash in the specified direction.
308
     * This algorithm is available in various ports that seem to point back to
309
     * geohash-js by David Troy under MIT notice.
310
     *
311
     *
312
     * @see https://github.com/davetroy/geohash-js
313
     * @see https://github.com/lyokato/objc-geohash
314
     * @see https://github.com/lyokato/libgeohash
315
     * @see https://github.com/masuidrive/pr_geohash
316
     * @see https://github.com/sunng87/node-geohash
317
     * @see https://github.com/davidmoten/geo
318
     *
319
     * @param string $hash the geohash (lowercase)
320
     * @param string $direction the direction of the neighbor (top, bottom, left or right)
321
     * @return string the geohash of the adjacent cell
322
     */
323
    public static function adjacent($hash, $direction)
324
    {
325
        $last = substr($hash, -1);
326
        $type = (strlen($hash) % 2) ? 'odd' : 'even';
327
        $base = substr($hash, 0, strlen($hash) - 1);
328
        if (strpos((self::$borders[$direction][$type]), $last) !== false) {
329
            $base = self::adjacent($base, $direction);
330
        }
331
        return $base . self::$characterTable[strpos(self::$neighbours[$direction][$type], $last)];
332
    }
333
}
334