geohash.php 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. <?php
  2. /**
  3. * Geohash generation class for php
  4. *
  5. * This file copyright (C) 2013 Bruce Chen (http://weibo.com/smcz)
  6. *
  7. * Author: Bruce Chen (weibo: @一个开发者)
  8. *
  9. */
  10. /**
  11. *
  12. * Encode and decode geohashes
  13. *
  14. * Find neighbors
  15. *
  16. */
  17. class geohash {
  18. private $bitss = array(16, 8, 4, 2, 1);
  19. private $neighbors = array();
  20. private $borders = array();
  21. private $coding = "0123456789bcdefghjkmnpqrstuvwxyz";
  22. private $codingMap = array();
  23. public function geohash()
  24. {
  25. $this->neighbors['right']['even'] = 'bc01fg45238967deuvhjyznpkmstqrwx';
  26. $this->neighbors['left']['even'] = '238967debc01fg45kmstqrwxuvhjyznp';
  27. $this->neighbors['top']['even'] = 'p0r21436x8zb9dcf5h7kjnmqesgutwvy';
  28. $this->neighbors['bottom']['even'] = '14365h7k9dcfesgujnmqp0r2twvyx8zb';
  29. $this->borders['right']['even'] = 'bcfguvyz';
  30. $this->borders['left']['even'] = '0145hjnp';
  31. $this->borders['top']['even'] = 'prxz';
  32. $this->borders['bottom']['even'] = '028b';
  33. $this->neighbors['bottom']['odd'] = $this->neighbors['left']['even'];
  34. $this->neighbors['top']['odd'] = $this->neighbors['right']['even'];
  35. $this->neighbors['left']['odd'] = $this->neighbors['bottom']['even'];
  36. $this->neighbors['right']['odd'] = $this->neighbors['top']['even'];
  37. $this->borders['bottom']['odd'] = $this->borders['left']['even'];
  38. $this->borders['top']['odd'] = $this->borders['right']['even'];
  39. $this->borders['left']['odd'] = $this->borders['bottom']['even'];
  40. $this->borders['right']['odd'] = $this->borders['top']['even'];
  41. //build map from encoding char to 0 padded bitfield
  42. for($i=0; $i<32; $i++) {
  43. $this->codingMap[substr($this->coding, $i, 1)] = str_pad(decbin($i), 5, "0", STR_PAD_LEFT);
  44. }
  45. }
  46. /**
  47. * Decode a geohash and return an array with decimal lat,long in it
  48. * Author: Bruce Chen (weibo: @一个开发者)
  49. */
  50. public function decode($hash) {
  51. //decode hash into binary string
  52. $binary = "";
  53. $hl = strlen($hash);
  54. for ($i=0; $i<$hl; $i++) {
  55. $binary .= $this->codingMap[substr($hash, $i, 1)];
  56. }
  57. //split the binary into lat and log binary strings
  58. $bl = strlen($binary);
  59. $blat = "";
  60. $blong = "";
  61. for ($i=0; $i<$bl; $i++) {
  62. if ($i%2)
  63. $blat=$blat.substr($binary, $i, 1);
  64. else
  65. $blong=$blong.substr($binary, $i, 1);
  66. }
  67. //now concert to decimal
  68. $lat = $this->binDecode($blat, -90, 90);
  69. $long = $this->binDecode($blong, -180, 180);
  70. //figure out how precise the bit count makes this calculation
  71. $latErr = $this->calcError(strlen($blat), -90, 90);
  72. $longErr = $this->calcError(strlen($blong), -180, 180);
  73. //how many decimal places should we use? There's a little art to
  74. //this to ensure I get the same roundings as geohash.org
  75. $latPlaces = max(1, -round(log10($latErr))) - 1;
  76. $longPlaces = max(1, -round(log10($longErr))) - 1;
  77. //round it
  78. $lat = round($lat, $latPlaces);
  79. $long = round($long, $longPlaces);
  80. return array($lat, $long);
  81. }
  82. private function calculateAdjacent($srcHash, $dir) {
  83. $srcHash = strtolower($srcHash);
  84. $lastChr = $srcHash[strlen($srcHash) - 1];
  85. $type = (strlen($srcHash) % 2) ? 'odd' : 'even';
  86. $base = substr($srcHash, 0, strlen($srcHash) - 1);
  87. if (strpos($this->borders[$dir][$type], $lastChr) !== false) {
  88. $base = $this->calculateAdjacent($base, $dir);
  89. }
  90. return $base . $this->coding[strpos($this->neighbors[$dir][$type], $lastChr)];
  91. }
  92. public function neighbors($srcHash) {
  93. $geohashPrefix = substr($srcHash, 0, strlen($srcHash) - 1);
  94. $neighbors['top'] = $this->calculateAdjacent($srcHash, 'top');
  95. $neighbors['bottom'] = $this->calculateAdjacent($srcHash, 'bottom');
  96. $neighbors['right'] = $this->calculateAdjacent($srcHash, 'right');
  97. $neighbors['left'] = $this->calculateAdjacent($srcHash, 'left');
  98. $neighbors['topleft'] = $this->calculateAdjacent($neighbors['left'], 'top');
  99. $neighbors['topright'] = $this->calculateAdjacent($neighbors['right'], 'top');
  100. $neighbors['bottomright'] = $this->calculateAdjacent($neighbors['right'], 'bottom');
  101. $neighbors['bottomleft'] = $this->calculateAdjacent($neighbors['left'], 'bottom');
  102. return $neighbors;
  103. }
  104. /**
  105. * Encode a hash from given lat and long
  106. * Author: Bruce Chen (weibo: @一个开发者)
  107. */
  108. public function encode($lat, $long) {
  109. //how many bits does latitude need?
  110. $plat = $this->precision($lat);
  111. $latbits = 1;
  112. $err = 45;
  113. while($err > $plat) {
  114. $latbits++;
  115. $err /= 2;
  116. }
  117. //how many bits does longitude need?
  118. $plong = $this->precision($long);
  119. $longbits = 1;
  120. $err = 90;
  121. while($err > $plong) {
  122. $longbits++;
  123. $err /= 2;
  124. }
  125. //bit counts need to be equal
  126. $bits = max($latbits, $longbits);
  127. //as the hash create bits in groups of 5, lets not
  128. //waste any bits - lets bulk it up to a multiple of 5
  129. //and favour the longitude for any odd bits
  130. $longbits = $bits;
  131. $latbits = $bits;
  132. $addlong = 1;
  133. while (($longbits + $latbits) % 5 != 0) {
  134. $longbits += $addlong;
  135. $latbits += !$addlong;
  136. $addlong = !$addlong;
  137. }
  138. //encode each as binary string
  139. $blat = $this->binEncode($lat, -90, 90, $latbits);
  140. $blong = $this->binEncode($long, -180, 180, $longbits);
  141. //merge lat and long together
  142. $binary = "";
  143. $uselong = 1;
  144. while (strlen($blat) + strlen($blong)) {
  145. if ($uselong) {
  146. $binary = $binary.substr($blong, 0, 1);
  147. $blong = substr($blong, 1);
  148. } else {
  149. $binary = $binary.substr($blat, 0, 1);
  150. $blat = substr($blat, 1);
  151. }
  152. $uselong = !$uselong;
  153. }
  154. //convert binary string to hash
  155. $hash = "";
  156. for ($i=0; $i<strlen($binary); $i+=5) {
  157. $n = bindec(substr($binary, $i, 5));
  158. $hash = $hash.$this->coding[$n];
  159. }
  160. return $hash;
  161. }
  162. /**
  163. * What's the maximum error for $bits bits covering a range $min to $max
  164. */
  165. private function calcError($bits, $min, $max) {
  166. $err = ($max - $min) / 2;
  167. while ($bits--)
  168. $err /= 2;
  169. return $err;
  170. }
  171. /*
  172. * returns precision of number
  173. * precision of 42 is 0.5
  174. * precision of 42.4 is 0.05
  175. * precision of 42.41 is 0.005 etc
  176. *
  177. * Author: Bruce Chen (weibo: @一个开发者)
  178. */
  179. private function precision($number) {
  180. $precision = 0;
  181. $pt = strpos($number,'.');
  182. if ($pt !== false) {
  183. $precision = -(strlen($number) - $pt - 1);
  184. }
  185. return pow(10, $precision) / 2;
  186. }
  187. /**
  188. * create binary encoding of number as detailed in http://en.wikipedia.org/wiki/Geohash#Example
  189. * removing the tail recursion is left an exercise for the reader
  190. *
  191. * Author: Bruce Chen (weibo: @一个开发者)
  192. */
  193. private function binEncode($number, $min, $max, $bitcount) {
  194. if ($bitcount == 0)
  195. return "";
  196. #echo "$bitcount: $min $max<br>";
  197. //this is our mid point - we will produce a bit to say
  198. //whether $number is above or below this mid point
  199. $mid = ($min + $max) / 2;
  200. if ($number > $mid)
  201. return "1" . $this->binEncode($number, $mid, $max, $bitcount - 1);
  202. else
  203. return "0" . $this->binEncode($number, $min, $mid, $bitcount - 1);
  204. }
  205. /**
  206. * decodes binary encoding of number as detailed in http://en.wikipedia.org/wiki/Geohash#Example
  207. * removing the tail recursion is left an exercise for the reader
  208. *
  209. * Author: Bruce Chen (weibo: @一个开发者)
  210. */
  211. private function binDecode($binary, $min, $max) {
  212. $mid = ($min + $max) / 2;
  213. if (strlen($binary) == 0)
  214. return $mid;
  215. $bit = substr($binary, 0, 1);
  216. $binary = substr($binary, 1);
  217. if ($bit == 1)
  218. return $this->binDecode($binary, $mid, $max);
  219. else
  220. return $this->binDecode($binary, $min, $mid);
  221. }
  222. }