You are here

GeoHash.class.php in geoPHP 7

Same filename and directory in other branches
  1. 8 geoPHP/lib/adapters/GeoHash.class.php

File

geoPHP/lib/adapters/GeoHash.class.php
View source
<?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, 1.0E-7);
      }
      $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)];
  }

}

Classes

Namesort descending Description
GeoHash PHP Geometry GeoHash encoder/decoder.