GeoHash.class.php 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. <?php
  2. /**
  3. * PHP Geometry GeoHash encoder/decoder.
  4. *
  5. * @author prinsmc
  6. * @see http://en.wikipedia.org/wiki/Geohash
  7. *
  8. */
  9. class GeoHash extends GeoAdapter{
  10. /**
  11. * base32 encoding character map.
  12. */
  13. private $table = "0123456789bcdefghjkmnpqrstuvwxyz";
  14. /**
  15. * array of neighbouring hash character maps.
  16. */
  17. private $neighbours = array (
  18. // north
  19. 'top' => array (
  20. 'even' => 'p0r21436x8zb9dcf5h7kjnmqesgutwvy',
  21. 'odd' => 'bc01fg45238967deuvhjyznpkmstqrwx'
  22. ),
  23. // east
  24. 'right' => array (
  25. 'even' => 'bc01fg45238967deuvhjyznpkmstqrwx',
  26. 'odd' => 'p0r21436x8zb9dcf5h7kjnmqesgutwvy'
  27. ),
  28. // west
  29. 'left' => array (
  30. 'even' => '238967debc01fg45kmstqrwxuvhjyznp',
  31. 'odd' => '14365h7k9dcfesgujnmqp0r2twvyx8zb'
  32. ),
  33. // south
  34. 'bottom' => array (
  35. 'even' => '14365h7k9dcfesgujnmqp0r2twvyx8zb',
  36. 'odd' => '238967debc01fg45kmstqrwxuvhjyznp'
  37. )
  38. );
  39. /**
  40. * array of bordering hash character maps.
  41. */
  42. private $borders = array (
  43. // north
  44. 'top' => array (
  45. 'even' => 'prxz',
  46. 'odd' => 'bcfguvyz'
  47. ),
  48. // east
  49. 'right' => array (
  50. 'even' => 'bcfguvyz',
  51. 'odd' => 'prxz'
  52. ),
  53. // west
  54. 'left' => array (
  55. 'even' => '0145hjnp',
  56. 'odd' => '028b'
  57. ),
  58. // south
  59. 'bottom' => array (
  60. 'even' => '028b',
  61. 'odd' => '0145hjnp'
  62. )
  63. );
  64. /**
  65. * Convert the geohash to a Point. The point is 2-dimensional.
  66. * @return Point the converted geohash
  67. * @param string $hash a geohash
  68. * @see GeoAdapter::read()
  69. */
  70. public function read($hash, $as_grid = FALSE) {
  71. $ll = $this->decode($hash);
  72. if (!$as_grid) {
  73. return new Point($ll['medlon'], $ll['medlat']);
  74. }
  75. else {
  76. return new Polygon(array(
  77. new LineString(array(
  78. new Point($ll['minlon'], $ll['maxlat']),
  79. new Point($ll['maxlon'], $ll['maxlat']),
  80. new Point($ll['maxlon'], $ll['minlat']),
  81. new Point($ll['minlon'], $ll['minlat']),
  82. new Point($ll['minlon'], $ll['maxlat']),
  83. ))
  84. ));
  85. }
  86. }
  87. /**
  88. * Convert the geometry to geohash.
  89. * @return string the geohash or null when the $geometry is not a Point
  90. * @param Point $geometry
  91. * @see GeoAdapter::write()
  92. */
  93. public function write(Geometry $geometry, $precision = NULL){
  94. if ($geometry->isEmpty()) return '';
  95. if($geometry->geometryType() === 'Point'){
  96. return $this->encodePoint($geometry, $precision);
  97. }
  98. else {
  99. // The geohash is the hash grid ID that fits the envelope
  100. $envelope = $geometry->envelope();
  101. $geohashes = array();
  102. $geohash = '';
  103. foreach ($envelope->getPoints() as $point) {
  104. $geohashes[] = $this->encodePoint($point, 0.0000001);
  105. }
  106. $i = 0;
  107. while ($i < strlen($geohashes[0])) {
  108. $char = $geohashes[0][$i];
  109. foreach ($geohashes as $hash) {
  110. if ($hash[$i] != $char) {
  111. return $geohash;
  112. }
  113. }
  114. $geohash .= $char;
  115. $i++;
  116. }
  117. return $geohash;
  118. }
  119. }
  120. /**
  121. * @return string geohash
  122. * @param Point $point
  123. * @author algorithm based on code by Alexander Songe <a@songe.me>
  124. * @see https://github.com/asonge/php-geohash/issues/1
  125. */
  126. private function encodePoint($point, $precision = NULL){
  127. if ($precision === NULL) {
  128. $lap = strlen($point->y())-strpos($point->y(),".");
  129. $lop = strlen($point->x())-strpos($point->x(),".");
  130. $precision = pow(10,-max($lap-1,$lop-1,0))/2;
  131. }
  132. $minlat = -90;
  133. $maxlat = 90;
  134. $minlon = -180;
  135. $maxlon = 180;
  136. $latE = 90;
  137. $lonE = 180;
  138. $i = 0;
  139. $error = 180;
  140. $hash='';
  141. while($error>=$precision) {
  142. $chr = 0;
  143. for($b=4;$b>=0;--$b) {
  144. if((1&$b) == (1&$i)) {
  145. // even char, even bit OR odd char, odd bit...a lon
  146. $next = ($minlon+$maxlon)/2;
  147. if($point->x()>$next) {
  148. $chr |= pow(2,$b);
  149. $minlon = $next;
  150. } else {
  151. $maxlon = $next;
  152. }
  153. $lonE /= 2;
  154. } else {
  155. // odd char, even bit OR even char, odd bit...a lat
  156. $next = ($minlat+$maxlat)/2;
  157. if($point->y()>$next) {
  158. $chr |= pow(2,$b);
  159. $minlat = $next;
  160. } else {
  161. $maxlat = $next;
  162. }
  163. $latE /= 2;
  164. }
  165. }
  166. $hash .= $this->table[$chr];
  167. $i++;
  168. $error = min($latE,$lonE);
  169. }
  170. return $hash;
  171. }
  172. /**
  173. * @param string $hash a geohash
  174. * @author algorithm based on code by Alexander Songe <a@songe.me>
  175. * @see https://github.com/asonge/php-geohash/issues/1
  176. */
  177. private function decode($hash){
  178. $ll = array();
  179. $minlat = -90;
  180. $maxlat = 90;
  181. $minlon = -180;
  182. $maxlon = 180;
  183. $latE = 90;
  184. $lonE = 180;
  185. for($i=0,$c=strlen($hash);$i<$c;$i++) {
  186. $v = strpos($this->table,$hash[$i]);
  187. if(1&$i) {
  188. if(16&$v)$minlat = ($minlat+$maxlat)/2; else $maxlat = ($minlat+$maxlat)/2;
  189. if(8&$v) $minlon = ($minlon+$maxlon)/2; else $maxlon = ($minlon+$maxlon)/2;
  190. if(4&$v) $minlat = ($minlat+$maxlat)/2; else $maxlat = ($minlat+$maxlat)/2;
  191. if(2&$v) $minlon = ($minlon+$maxlon)/2; else $maxlon = ($minlon+$maxlon)/2;
  192. if(1&$v) $minlat = ($minlat+$maxlat)/2; else $maxlat = ($minlat+$maxlat)/2;
  193. $latE /= 8;
  194. $lonE /= 4;
  195. } else {
  196. if(16&$v)$minlon = ($minlon+$maxlon)/2; else $maxlon = ($minlon+$maxlon)/2;
  197. if(8&$v) $minlat = ($minlat+$maxlat)/2; else $maxlat = ($minlat+$maxlat)/2;
  198. if(4&$v) $minlon = ($minlon+$maxlon)/2; else $maxlon = ($minlon+$maxlon)/2;
  199. if(2&$v) $minlat = ($minlat+$maxlat)/2; else $maxlat = ($minlat+$maxlat)/2;
  200. if(1&$v) $minlon = ($minlon+$maxlon)/2; else $maxlon = ($minlon+$maxlon)/2;
  201. $latE /= 4;
  202. $lonE /= 8;
  203. }
  204. }
  205. $ll['minlat'] = $minlat;
  206. $ll['minlon'] = $minlon;
  207. $ll['maxlat'] = $maxlat;
  208. $ll['maxlon'] = $maxlon;
  209. $ll['medlat'] = round(($minlat+$maxlat)/2, max(1, -round(log10($latE)))-1);
  210. $ll['medlon'] = round(($minlon+$maxlon)/2, max(1, -round(log10($lonE)))-1);
  211. return $ll;
  212. }
  213. /**
  214. * Calculates the adjacent geohash of the geohash in the specified direction.
  215. * This algorithm is available in various ports that seem to point back to
  216. * geohash-js by David Troy under MIT notice.
  217. *
  218. *
  219. * @see https://github.com/davetroy/geohash-js
  220. * @see https://github.com/lyokato/objc-geohash
  221. * @see https://github.com/lyokato/libgeohash
  222. * @see https://github.com/masuidrive/pr_geohash
  223. * @see https://github.com/sunng87/node-geohash
  224. * @see https://github.com/davidmoten/geo
  225. *
  226. * @param string $hash the geohash (lowercase)
  227. * @param string $direction the direction of the neighbor (top, bottom, left or right)
  228. * @return string the geohash of the adjacent cell
  229. */
  230. public function adjacent($hash, $direction){
  231. $last = substr($hash, -1);
  232. $type = (strlen($hash) % 2)? 'odd': 'even';
  233. $base = substr($hash, 0, strlen($hash) - 1);
  234. if(strpos(($this->borders[$direction][$type]), $last) !== false){
  235. $base = $this->adjacent($base, $direction);
  236. }
  237. return $base.$this->table[strpos($this->neighbours[$direction][$type], $last)];
  238. }
  239. }