forked from lino/radar-wp
250 lines
7.3 KiB
PHP
250 lines
7.3 KiB
PHP
|
<?php
|
||
|
/**
|
||
|
* PHP Geometry GeoHash encoder/decoder.
|
||
|
*
|
||
|
* @author prinsmc
|
||
|
* @see http://en.wikipedia.org/wiki/Geohash
|
||
|
*
|
||
|
*/
|
||
|
class GeoHash extends GeoAdapter{
|
||
|
|
||
|
/**
|
||
|
* base32 encoding character map.
|
||
|
*/
|
||
|
private $table = "0123456789bcdefghjkmnpqrstuvwxyz";
|
||
|
|
||
|
/**
|
||
|
* array of neighbouring hash character maps.
|
||
|
*/
|
||
|
private $neighbours = array (
|
||
|
// north
|
||
|
'top' => array (
|
||
|
'even' => 'p0r21436x8zb9dcf5h7kjnmqesgutwvy',
|
||
|
'odd' => 'bc01fg45238967deuvhjyznpkmstqrwx'
|
||
|
),
|
||
|
// east
|
||
|
'right' => array (
|
||
|
'even' => 'bc01fg45238967deuvhjyznpkmstqrwx',
|
||
|
'odd' => 'p0r21436x8zb9dcf5h7kjnmqesgutwvy'
|
||
|
),
|
||
|
// west
|
||
|
'left' => array (
|
||
|
'even' => '238967debc01fg45kmstqrwxuvhjyznp',
|
||
|
'odd' => '14365h7k9dcfesgujnmqp0r2twvyx8zb'
|
||
|
),
|
||
|
// south
|
||
|
'bottom' => array (
|
||
|
'even' => '14365h7k9dcfesgujnmqp0r2twvyx8zb',
|
||
|
'odd' => '238967debc01fg45kmstqrwxuvhjyznp'
|
||
|
)
|
||
|
);
|
||
|
|
||
|
/**
|
||
|
* array of bordering hash character maps.
|
||
|
*/
|
||
|
private $borders = array (
|
||
|
// north
|
||
|
'top' => array (
|
||
|
'even' => 'prxz',
|
||
|
'odd' => 'bcfguvyz'
|
||
|
),
|
||
|
// east
|
||
|
'right' => array (
|
||
|
'even' => 'bcfguvyz',
|
||
|
'odd' => 'prxz'
|
||
|
),
|
||
|
// west
|
||
|
'left' => array (
|
||
|
'even' => '0145hjnp',
|
||
|
'odd' => '028b'
|
||
|
),
|
||
|
// south
|
||
|
'bottom' => array (
|
||
|
'even' => '028b',
|
||
|
'odd' => '0145hjnp'
|
||
|
)
|
||
|
);
|
||
|
|
||
|
/**
|
||
|
* Convert the geohash to a Point. The point is 2-dimensional.
|
||
|
* @return Point the converted geohash
|
||
|
* @param string $hash a geohash
|
||
|
* @see GeoAdapter::read()
|
||
|
*/
|
||
|
public function read($hash, $as_grid = FALSE) {
|
||
|
$ll = $this->decode($hash);
|
||
|
if (!$as_grid) {
|
||
|
return new Point($ll['medlon'], $ll['medlat']);
|
||
|
}
|
||
|
else {
|
||
|
return new Polygon(array(
|
||
|
new LineString(array(
|
||
|
new Point($ll['minlon'], $ll['maxlat']),
|
||
|
new Point($ll['maxlon'], $ll['maxlat']),
|
||
|
new Point($ll['maxlon'], $ll['minlat']),
|
||
|
new Point($ll['minlon'], $ll['minlat']),
|
||
|
new Point($ll['minlon'], $ll['maxlat']),
|
||
|
))
|
||
|
));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Convert the geometry to geohash.
|
||
|
* @return string the geohash or null when the $geometry is not a Point
|
||
|
* @param Point $geometry
|
||
|
* @see GeoAdapter::write()
|
||
|
*/
|
||
|
public function write(Geometry $geometry, $precision = NULL){
|
||
|
if ($geometry->isEmpty()) return '';
|
||
|
|
||
|
if($geometry->geometryType() === 'Point'){
|
||
|
return $this->encodePoint($geometry, $precision);
|
||
|
}
|
||
|
else {
|
||
|
// The geohash is the hash grid ID that fits the envelope
|
||
|
$envelope = $geometry->envelope();
|
||
|
$geohashes = array();
|
||
|
$geohash = '';
|
||
|
foreach ($envelope->getPoints() as $point) {
|
||
|
$geohashes[] = $this->encodePoint($point, 0.0000001);
|
||
|
}
|
||
|
$i = 0;
|
||
|
while ($i < strlen($geohashes[0])) {
|
||
|
$char = $geohashes[0][$i];
|
||
|
foreach ($geohashes as $hash) {
|
||
|
if ($hash[$i] != $char) {
|
||
|
return $geohash;
|
||
|
}
|
||
|
}
|
||
|
$geohash .= $char;
|
||
|
$i++;
|
||
|
}
|
||
|
return $geohash;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return string geohash
|
||
|
* @param Point $point
|
||
|
* @author algorithm based on code by Alexander Songe <a@songe.me>
|
||
|
* @see https://github.com/asonge/php-geohash/issues/1
|
||
|
*/
|
||
|
private function encodePoint($point, $precision = NULL){
|
||
|
if ($precision === NULL) {
|
||
|
$lap = strlen($point->y())-strpos($point->y(),".");
|
||
|
$lop = strlen($point->x())-strpos($point->x(),".");
|
||
|
$precision = pow(10,-max($lap-1,$lop-1,0))/2;
|
||
|
}
|
||
|
|
||
|
$minlat = -90;
|
||
|
$maxlat = 90;
|
||
|
$minlon = -180;
|
||
|
$maxlon = 180;
|
||
|
$latE = 90;
|
||
|
$lonE = 180;
|
||
|
$i = 0;
|
||
|
$error = 180;
|
||
|
$hash='';
|
||
|
while($error>=$precision) {
|
||
|
$chr = 0;
|
||
|
for($b=4;$b>=0;--$b) {
|
||
|
if((1&$b) == (1&$i)) {
|
||
|
// even char, even bit OR odd char, odd bit...a lon
|
||
|
$next = ($minlon+$maxlon)/2;
|
||
|
if($point->x()>$next) {
|
||
|
$chr |= pow(2,$b);
|
||
|
$minlon = $next;
|
||
|
} else {
|
||
|
$maxlon = $next;
|
||
|
}
|
||
|
$lonE /= 2;
|
||
|
} else {
|
||
|
// odd char, even bit OR even char, odd bit...a lat
|
||
|
$next = ($minlat+$maxlat)/2;
|
||
|
if($point->y()>$next) {
|
||
|
$chr |= pow(2,$b);
|
||
|
$minlat = $next;
|
||
|
} else {
|
||
|
$maxlat = $next;
|
||
|
}
|
||
|
$latE /= 2;
|
||
|
}
|
||
|
}
|
||
|
$hash .= $this->table[$chr];
|
||
|
$i++;
|
||
|
$error = min($latE,$lonE);
|
||
|
}
|
||
|
return $hash;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param string $hash a geohash
|
||
|
* @author algorithm based on code by Alexander Songe <a@songe.me>
|
||
|
* @see https://github.com/asonge/php-geohash/issues/1
|
||
|
*/
|
||
|
private function decode($hash){
|
||
|
$ll = array();
|
||
|
$minlat = -90;
|
||
|
$maxlat = 90;
|
||
|
$minlon = -180;
|
||
|
$maxlon = 180;
|
||
|
$latE = 90;
|
||
|
$lonE = 180;
|
||
|
for($i=0,$c=strlen($hash);$i<$c;$i++) {
|
||
|
$v = strpos($this->table,$hash[$i]);
|
||
|
if(1&$i) {
|
||
|
if(16&$v)$minlat = ($minlat+$maxlat)/2; else $maxlat = ($minlat+$maxlat)/2;
|
||
|
if(8&$v) $minlon = ($minlon+$maxlon)/2; else $maxlon = ($minlon+$maxlon)/2;
|
||
|
if(4&$v) $minlat = ($minlat+$maxlat)/2; else $maxlat = ($minlat+$maxlat)/2;
|
||
|
if(2&$v) $minlon = ($minlon+$maxlon)/2; else $maxlon = ($minlon+$maxlon)/2;
|
||
|
if(1&$v) $minlat = ($minlat+$maxlat)/2; else $maxlat = ($minlat+$maxlat)/2;
|
||
|
$latE /= 8;
|
||
|
$lonE /= 4;
|
||
|
} else {
|
||
|
if(16&$v)$minlon = ($minlon+$maxlon)/2; else $maxlon = ($minlon+$maxlon)/2;
|
||
|
if(8&$v) $minlat = ($minlat+$maxlat)/2; else $maxlat = ($minlat+$maxlat)/2;
|
||
|
if(4&$v) $minlon = ($minlon+$maxlon)/2; else $maxlon = ($minlon+$maxlon)/2;
|
||
|
if(2&$v) $minlat = ($minlat+$maxlat)/2; else $maxlat = ($minlat+$maxlat)/2;
|
||
|
if(1&$v) $minlon = ($minlon+$maxlon)/2; else $maxlon = ($minlon+$maxlon)/2;
|
||
|
$latE /= 4;
|
||
|
$lonE /= 8;
|
||
|
}
|
||
|
}
|
||
|
$ll['minlat'] = $minlat;
|
||
|
$ll['minlon'] = $minlon;
|
||
|
$ll['maxlat'] = $maxlat;
|
||
|
$ll['maxlon'] = $maxlon;
|
||
|
$ll['medlat'] = round(($minlat+$maxlat)/2, max(1, -round(log10($latE)))-1);
|
||
|
$ll['medlon'] = round(($minlon+$maxlon)/2, max(1, -round(log10($lonE)))-1);
|
||
|
return $ll;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Calculates the adjacent geohash of the geohash in the specified direction.
|
||
|
* This algorithm is available in various ports that seem to point back to
|
||
|
* geohash-js by David Troy under MIT notice.
|
||
|
*
|
||
|
*
|
||
|
* @see https://github.com/davetroy/geohash-js
|
||
|
* @see https://github.com/lyokato/objc-geohash
|
||
|
* @see https://github.com/lyokato/libgeohash
|
||
|
* @see https://github.com/masuidrive/pr_geohash
|
||
|
* @see https://github.com/sunng87/node-geohash
|
||
|
* @see https://github.com/davidmoten/geo
|
||
|
*
|
||
|
* @param string $hash the geohash (lowercase)
|
||
|
* @param string $direction the direction of the neighbor (top, bottom, left or right)
|
||
|
* @return string the geohash of the adjacent cell
|
||
|
*/
|
||
|
public function adjacent($hash, $direction){
|
||
|
$last = substr($hash, -1);
|
||
|
$type = (strlen($hash) % 2)? 'odd': 'even';
|
||
|
$base = substr($hash, 0, strlen($hash) - 1);
|
||
|
if(strpos(($this->borders[$direction][$type]), $last) !== false){
|
||
|
$base = $this->adjacent($base, $direction);
|
||
|
}
|
||
|
return $base.$this->table[strpos($this->neighbours[$direction][$type], $last)];
|
||
|
}
|
||
|
}
|