1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* This file is part of the Geotools library. |
5
|
|
|
* |
6
|
|
|
* (c) Antoine Corcy <[email protected]> |
7
|
|
|
* |
8
|
|
|
* For the full copyright and license information, please view the LICENSE |
9
|
|
|
* file that was distributed with this source code. |
10
|
|
|
*/ |
11
|
|
|
|
12
|
|
|
namespace League\Geotools\Geohash; |
13
|
|
|
|
14
|
|
|
use League\Geotools\Coordinate\Coordinate; |
15
|
|
|
use League\Geotools\Coordinate\CoordinateInterface; |
16
|
|
|
use League\Geotools\Exception\InvalidArgumentException; |
17
|
|
|
use League\Geotools\Exception\RuntimeException; |
18
|
|
|
|
19
|
|
|
/** |
20
|
|
|
* Geohash class |
21
|
|
|
* |
22
|
|
|
* @author Antoine Corcy <[email protected]> |
23
|
|
|
*/ |
24
|
|
|
class Geohash implements GeohashInterface |
25
|
|
|
{ |
26
|
|
|
/** |
27
|
|
|
* The minimum length of the geo hash. |
28
|
|
|
* |
29
|
|
|
* @var integer |
30
|
|
|
*/ |
31
|
|
|
const MIN_LENGTH = 1; |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* The maximum length of the geo hash. |
35
|
|
|
* |
36
|
|
|
* @var integer |
37
|
|
|
*/ |
38
|
|
|
const MAX_LENGTH = 12; |
39
|
|
|
|
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* The geo hash. |
43
|
|
|
* |
44
|
|
|
* @var string |
45
|
|
|
*/ |
46
|
|
|
protected $geohash; |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* The interval of latitudes in degrees. |
50
|
|
|
* |
51
|
|
|
* @var array |
52
|
|
|
*/ |
53
|
|
|
protected $latitudeInterval = array(-90.0, 90.0); |
54
|
|
|
|
55
|
|
|
/** |
56
|
|
|
* The interval of longitudes in degrees. |
57
|
|
|
* |
58
|
|
|
* @var array |
59
|
|
|
*/ |
60
|
|
|
protected $longitudeInterval = array(-180.0, 180.0); |
61
|
|
|
|
62
|
|
|
/** |
63
|
|
|
* The interval of bits. |
64
|
|
|
* |
65
|
|
|
* @var array |
66
|
|
|
*/ |
67
|
|
|
protected $bits = array(16, 8, 4, 2, 1); |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* The array of chars in base 32. |
71
|
|
|
* |
72
|
|
|
* @var array |
73
|
|
|
*/ |
74
|
|
|
protected $base32Chars = array( |
75
|
|
|
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'b', 'c', 'd', 'e', 'f', 'g', |
76
|
|
|
'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' |
77
|
|
|
); |
78
|
|
|
|
79
|
|
|
|
80
|
|
|
/** |
81
|
|
|
* Returns the geo hash. |
82
|
|
|
* |
83
|
|
|
* @return string |
84
|
|
|
*/ |
85
|
5 |
|
public function getGeohash() |
86
|
|
|
{ |
87
|
5 |
|
return $this->geohash; |
88
|
|
|
} |
89
|
|
|
|
90
|
|
|
/** |
91
|
|
|
* Returns the decoded coordinate (The center of the bounding box). |
92
|
|
|
* |
93
|
|
|
* @return CoordinateInterface |
94
|
|
|
*/ |
95
|
5 |
|
public function getCoordinate() |
96
|
|
|
{ |
97
|
5 |
|
return new Coordinate(array( |
98
|
5 |
|
($this->latitudeInterval[0] + $this->latitudeInterval[1]) / 2, |
99
|
5 |
|
($this->longitudeInterval[0] + $this->longitudeInterval[1]) / 2 |
100
|
|
|
)); |
101
|
|
|
} |
102
|
|
|
|
103
|
|
|
/** |
104
|
|
|
* Returns the bounding box which is an array of coordinates (SouthWest & NorthEast). |
105
|
|
|
* |
106
|
|
|
* @return CoordinateInterface[] |
107
|
|
|
*/ |
108
|
6 |
|
public function getBoundingBox() |
109
|
|
|
{ |
110
|
|
|
return array( |
111
|
6 |
|
new Coordinate(array( |
112
|
6 |
|
$this->latitudeInterval[0], |
113
|
6 |
|
$this->longitudeInterval[0] |
114
|
|
|
)), |
115
|
6 |
|
new Coordinate(array( |
116
|
6 |
|
$this->latitudeInterval[1], |
117
|
6 |
|
$this->longitudeInterval[1] |
118
|
|
|
)) |
119
|
|
|
); |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
/** |
123
|
|
|
* {@inheritDoc} |
124
|
|
|
* |
125
|
|
|
* @see http://en.wikipedia.org/wiki/Geohash |
126
|
|
|
* @see http://geohash.org/ |
127
|
|
|
*/ |
128
|
19 |
|
public function encode(CoordinateInterface $coordinate, $length = self::MAX_LENGTH) |
129
|
|
|
{ |
130
|
19 |
|
if ((int) $length < self::MIN_LENGTH || (int) $length > self::MAX_LENGTH) { |
131
|
10 |
|
throw new InvalidArgumentException('The length should be between 1 and 12.'); |
132
|
|
|
} |
133
|
|
|
|
134
|
9 |
|
$latitudeInterval = $this->latitudeInterval; |
135
|
9 |
|
$longitudeInterval = $this->longitudeInterval; |
136
|
9 |
|
$isEven = true; |
137
|
9 |
|
$bit = 0; |
138
|
9 |
|
$charIndex = 0; |
139
|
|
|
|
140
|
9 |
|
while (strlen($this->geohash) < $length) { |
141
|
9 |
|
if ($isEven) { |
142
|
9 |
|
$middle = ($longitudeInterval[0] + $longitudeInterval[1]) / 2; |
143
|
9 |
|
if ($coordinate->getLongitude() > $middle) { |
144
|
8 |
|
$charIndex |= $this->bits[$bit]; |
145
|
8 |
|
$longitudeInterval[0] = $middle; |
146
|
|
|
} else { |
147
|
9 |
|
$longitudeInterval[1] = $middle; |
148
|
|
|
} |
149
|
|
|
} else { |
150
|
9 |
|
$middle = ($latitudeInterval[0] + $latitudeInterval[1]) / 2; |
151
|
9 |
|
if ($coordinate->getLatitude() > $middle) { |
152
|
8 |
|
$charIndex |= $this->bits[$bit]; |
153
|
8 |
|
$latitudeInterval[0] = $middle; |
154
|
|
|
} else { |
155
|
9 |
|
$latitudeInterval[1] = $middle; |
156
|
|
|
} |
157
|
|
|
} |
158
|
|
|
|
159
|
9 |
|
if ($bit < 4) { |
160
|
9 |
|
$bit++; |
161
|
|
|
} else { |
162
|
9 |
|
$this->geohash = $this->geohash . $this->base32Chars[$charIndex]; |
163
|
9 |
|
$bit = 0; |
164
|
9 |
|
$charIndex = 0; |
165
|
|
|
} |
166
|
|
|
|
167
|
9 |
|
$isEven = $isEven ? false : true; |
168
|
|
|
} |
169
|
|
|
|
170
|
9 |
|
$this->latitudeInterval = $latitudeInterval; |
171
|
9 |
|
$this->longitudeInterval = $longitudeInterval; |
172
|
|
|
|
173
|
9 |
|
return $this; |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
/** |
177
|
|
|
* {@inheritDoc} |
178
|
|
|
*/ |
179
|
25 |
|
public function decode($geohash) |
180
|
|
|
{ |
181
|
25 |
|
if (!is_string($geohash)) { |
182
|
4 |
|
throw new InvalidArgumentException('The geo hash should be a string.'); |
183
|
|
|
} |
184
|
|
|
|
185
|
21 |
|
if (strlen($geohash) < self::MIN_LENGTH || strlen($geohash) > self::MAX_LENGTH) { |
186
|
4 |
|
throw new InvalidArgumentException('The length of the geo hash should be between 1 and 12.'); |
187
|
|
|
} |
188
|
|
|
|
189
|
17 |
|
$base32DecodeMap = array(); |
190
|
17 |
|
$base32CharsTotal = count($this->base32Chars); |
191
|
17 |
|
for ($i = 0; $i < $base32CharsTotal; $i++) { |
192
|
17 |
|
$base32DecodeMap[$this->base32Chars[$i]] = $i; |
193
|
|
|
} |
194
|
|
|
|
195
|
17 |
|
$latitudeInterval = $this->latitudeInterval; |
196
|
17 |
|
$longitudeInterval = $this->longitudeInterval; |
197
|
17 |
|
$isEven = true; |
198
|
|
|
|
199
|
17 |
|
$geohashLength = strlen($geohash); |
200
|
17 |
|
for ($i = 0; $i < $geohashLength; $i++) { |
201
|
|
|
|
202
|
17 |
|
if (!isset($base32DecodeMap[$geohash[$i]])) { |
203
|
8 |
|
throw new RuntimeException('This geo hash is invalid.'); |
204
|
|
|
} |
205
|
|
|
|
206
|
10 |
|
$currentChar = $base32DecodeMap[$geohash[$i]]; |
207
|
|
|
|
208
|
10 |
|
$bitsTotal = count($this->bits); |
209
|
10 |
|
for ($j = 0; $j < $bitsTotal; $j++) { |
210
|
10 |
|
$mask = $this->bits[$j]; |
211
|
|
|
|
212
|
10 |
|
if ($isEven) { |
213
|
10 |
|
if (($currentChar & $mask) !== 0) { |
214
|
10 |
|
$longitudeInterval[0] = ($longitudeInterval[0] + $longitudeInterval[1]) / 2; |
215
|
|
|
} else { |
216
|
10 |
|
$longitudeInterval[1] = ($longitudeInterval[0] + $longitudeInterval[1]) / 2; |
217
|
|
|
} |
218
|
|
|
} else { |
219
|
10 |
|
if (($currentChar & $mask) !== 0) { |
220
|
10 |
|
$latitudeInterval[0] = ($latitudeInterval[0] + $latitudeInterval[1]) / 2; |
221
|
|
|
} else { |
222
|
9 |
|
$latitudeInterval[1] = ($latitudeInterval[0] + $latitudeInterval[1]) / 2; |
223
|
|
|
} |
224
|
|
|
} |
225
|
|
|
|
226
|
10 |
|
$isEven = $isEven ? false : true; |
227
|
|
|
} |
228
|
|
|
} |
229
|
|
|
|
230
|
9 |
|
$this->latitudeInterval = $latitudeInterval; |
231
|
9 |
|
$this->longitudeInterval = $longitudeInterval; |
232
|
|
|
|
233
|
9 |
|
return $this; |
234
|
|
|
} |
235
|
|
|
} |
236
|
|
|
|