Skip to content

Commit

Permalink
Merge pull request phayes#42 from phayes/geohash-revamp
Browse files Browse the repository at this point in the history
Geohash Revamp
  • Loading branch information
phayes committed Aug 27, 2012
2 parents d7abcc1 + 7cf6a75 commit f3fc207
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 131 deletions.
289 changes: 163 additions & 126 deletions lib/adapters/GeoHash.class.php
Original file line number Diff line number Diff line change
@@ -1,126 +1,163 @@
<?php
/**
* PHP Geometry GeoHash encoder/decoder.
*
* @author prinsmc
* @see http://en.wikipedia.org/wiki/Geohash
*
*/
class GeoHash extends GeoAdapter{
private $table = "0123456789bcdefghjkmnpqrstuvwxyz";

/**
* 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) {
$ll = $this->decode($hash);
return new Point($ll['lon'], $ll['lat'], NULL);
}

/**
* 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){
if($geometry->geometryType()==='Point'){
return $this->encode($geometry);
} else {
return NULL;
}
}

/**
* @return string geohash
* @param Point $point
* @author algorithm based on code by Alexander Songe <[email protected]>
* @see https://github.com/asonge/php-geohash/issues/1
*/
private function encode($point){
$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;
$minlng = -180;
$maxlng = 180;
$latE = 90;
$lngE = 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 lng
$next = ($minlng+$maxlng)/2;
if($point->x()>$next) {
$chr |= pow(2,$b);
$minlng = $next;
} else {
$maxlng = $next;
}
$lngE /= 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,$lngE);
}
return $hash;
}

/**
* @param string $hash a geohash
* @author algorithm based on code by Alexander Songe <[email protected]>
* @see https://github.com/asonge/php-geohash/issues/1
*/
private function decode($hash){
$ll = array('lat'=>NULL,'lon'=>NULL);
$minlat = -90;
$maxlat = 90;
$minlng = -180;
$maxlng = 180;
$latE = 90;
$lngE = 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) $minlng = ($minlng+$maxlng)/2; else $maxlng = ($minlng+$maxlng)/2;
if(4&$v) $minlat = ($minlat+$maxlat)/2; else $maxlat = ($minlat+$maxlat)/2;
if(2&$v) $minlng = ($minlng+$maxlng)/2; else $maxlng = ($minlng+$maxlng)/2;
if(1&$v) $minlat = ($minlat+$maxlat)/2; else $maxlat = ($minlat+$maxlat)/2;
$latE /= 8;
$lngE /= 4;
} else {
if(16&$v)$minlng = ($minlng+$maxlng)/2; else $maxlng = ($minlng+$maxlng)/2;
if(8&$v) $minlat = ($minlat+$maxlat)/2; else $maxlat = ($minlat+$maxlat)/2;
if(4&$v) $minlng = ($minlng+$maxlng)/2; else $maxlng = ($minlng+$maxlng)/2;
if(2&$v) $minlat = ($minlat+$maxlat)/2; else $maxlat = ($minlat+$maxlat)/2;
if(1&$v) $minlng = ($minlng+$maxlng)/2; else $maxlng = ($minlng+$maxlng)/2;
$latE /= 4;
$lngE /= 8;
}
}
$ll['lat'] = round(($minlat+$maxlat)/2, max(1, -round(log10($latE)))-1);
$ll['lon'] = round(($minlng+$maxlng)/2, max(1, -round(log10($lngE)))-1);
return $ll;
}
}
<?php
/**
* PHP Geometry GeoHash encoder/decoder.
*
* @author prinsmc
* @see http://en.wikipedia.org/wiki/Geohash
*
*/
class GeoHash extends GeoAdapter{
private $table = "0123456789bcdefghjkmnpqrstuvwxyz";

/**
* 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->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 <[email protected]>
* @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 <[email protected]>
* @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;
}
}
1 change: 1 addition & 0 deletions tests/input/long.geohash
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
xn76urx4epb0
1 change: 1 addition & 0 deletions tests/input/short.geohash
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
xpssc0
11 changes: 6 additions & 5 deletions tests/test.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?php

// Uncomment to test
// run_test();
# run_test();

function run_test() {
header("Content-type: text");
Expand Down Expand Up @@ -120,8 +120,11 @@ function test_adapters($geometry, $format, $input) {
$test_geom_2 = $adapter_loader->read($test_geom_1->out($adapter_key));

// Check to make sure a round-trip results in the same geometry
if ($test_geom_1->out('wkt') != $test_geom_2->out('wkt')) {
print "Mismatched adapter output in ".$adapter_class."\n";
// We don't check round-trip accuracy on geohash as it's not a storage format that preserves information
if ($adapter_key != 'geohash') {
if ($test_geom_1->out('wkt') != $test_geom_2->out('wkt')) {
print "Mismatched adapter output in ".$adapter_class."\n";
}
}
}
else {
Expand Down Expand Up @@ -197,8 +200,6 @@ function test_methods($geometry) {

if ($geos_type != $norm_type) {
print 'Type mismatch on '.$method."\n";
var_dump($geos_type);
var_dump($norm_type);
continue;
}

Expand Down

0 comments on commit f3fc207

Please sign in to comment.