[ 'even' => 'p0r21436x8zb9dcf5h7kjnmqesgutwvy', 'odd' => 'bc01fg45238967deuvhjyznpkmstqrwx' ], // east 'right' => [ 'even' => 'bc01fg45238967deuvhjyznpkmstqrwx', 'odd' => 'p0r21436x8zb9dcf5h7kjnmqesgutwvy' ], // west 'left' => [ 'even' => '238967debc01fg45kmstqrwxuvhjyznp', 'odd' => '14365h7k9dcfesgujnmqp0r2twvyx8zb' ], // south 'bottom' => [ 'even' => '14365h7k9dcfesgujnmqp0r2twvyx8zb', 'odd' => '238967debc01fg45kmstqrwxuvhjyznp' ] ]; /** * array of bordering hash character maps. */ private static $borders = [ // north 'top' => [ 'even' => 'prxz', 'odd' => 'bcfguvyz' ], // east 'right' => [ 'even' => 'bcfguvyz', 'odd' => 'prxz' ], // west 'left' => [ 'even' => '0145hjnp', 'odd' => '028b' ], // south 'bottom' => [ 'even' => '028b', 'odd' => '0145hjnp' ] ]; /** * Convert the geoHash to a Point. The point is 2-dimensional. * * @param string $hash a GeoHash * @param boolean $asGrid Return the center point of hash grid or the grid cell as Polygon * * @return Point|Polygon the converted GeoHash */ public function read($hash, $asGrid = false) { $decodedHash = $this->decode($hash); if (!$asGrid) { return new Point($decodedHash['centerLongitude'], $decodedHash['centerLatitude']); } else { return new Polygon( [ new LineString( [ new Point($decodedHash['minLongitude'], $decodedHash['maxLatitude']), new Point($decodedHash['maxLongitude'], $decodedHash['maxLatitude']), new Point($decodedHash['maxLongitude'], $decodedHash['minLatitude']), new Point($decodedHash['minLongitude'], $decodedHash['minLatitude']), new Point($decodedHash['minLongitude'], $decodedHash['maxLatitude']), ] ) ] ); } } /** * Convert the geometry to geohash. * * @param Geometry $geometry * @param float|null $precision * @return string the GeoHash or null when the $geometry is not a Point */ public function write(Geometry $geometry, $precision = null) { if ($geometry->isEmpty()) { return ''; } if ($geometry->geometryType() === Geometry::POINT) { /** @var Point $geometry */ return $this->encodePoint($geometry, $precision); } else { // The GeoHash is the smallest hash grid ID that fits the envelope $envelope = $geometry->envelope(); $geoHashes = []; $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; } } /** * @author algorithm based on code by Alexander Songe * @see https://github.com/asonge/php-geohash/issues/1 * * @param Point $point * @param float|null $precision * @return string The GeoHash * @throws \Exception */ private function encodePoint($point, $precision = null) { $minLatitude = -90.0000000000001; $maxLatitude = 90.0000000000001; $minLongitude = -180.0000000000001; $maxLongitude = 180.0000000000001; $latitudeError = 90; $longitudeError = 180; $i = 0; $error = 180; $hash = ''; if (!is_numeric($precision)) { $lap = strlen($point->y()) - strpos($point->y(), "."); $lop = strlen($point->x()) - strpos($point->x(), "."); $precision = pow(10, -max($lap - 1, $lop - 1, 0)) / 2; } if ( $point->x() < $minLongitude || $point->y() < $minLatitude || $point->x() > $maxLongitude || $point->y() > $maxLatitude ) { throw new \Exception("Point coordinates ({$point->x()}, {$point->y()}) are out of lat/lon range"); } 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 = ($minLongitude + $maxLongitude) / 2; if ($point->x() > $next) { $chr |= pow(2, $b); $minLongitude = $next; } else { $maxLongitude = $next; } $longitudeError /= 2; } else { // odd char, even bit OR even char, odd bit...a lat $next = ($minLatitude + $maxLatitude) / 2; if ($point->y() > $next) { $chr |= pow(2, $b); $minLatitude = $next; } else { $maxLatitude = $next; } $latitudeError /= 2; } } $hash .= self::$characterTable[$chr]; $i++; $error = min($latitudeError, $longitudeError); } return $hash; } /** * @author algorithm based on code by Alexander Songe * @see https://github.com/asonge/php-geohash/issues/1 * * @param string $hash a GeoHash * @return array Associative array. */ private function decode($hash) { $result = []; $minLatitude = -90; $maxLatitude = 90; $minLongitude = -180; $maxLongitude = 180; $latitudeError = 90; $longitudeError = 180; for ($i = 0, $c = strlen($hash); $i < $c; $i++) { $v = strpos(self::$characterTable, $hash[$i]); if (1 & $i) { if (16 & $v) { $minLatitude = ($minLatitude + $maxLatitude) / 2; } else { $maxLatitude = ($minLatitude + $maxLatitude) / 2; } if (8 & $v) { $minLongitude = ($minLongitude + $maxLongitude) / 2; } else { $maxLongitude = ($minLongitude + $maxLongitude) / 2; } if (4 & $v) { $minLatitude = ($minLatitude + $maxLatitude) / 2; } else { $maxLatitude = ($minLatitude + $maxLatitude) / 2; } if (2 & $v) { $minLongitude = ($minLongitude + $maxLongitude) / 2; } else { $maxLongitude = ($minLongitude + $maxLongitude) / 2; } if (1 & $v) { $minLatitude = ($minLatitude + $maxLatitude) / 2; } else { $maxLatitude = ($minLatitude + $maxLatitude) / 2; } $latitudeError /= 8; $longitudeError /= 4; } else { if (16 & $v) { $minLongitude = ($minLongitude + $maxLongitude) / 2; } else { $maxLongitude = ($minLongitude + $maxLongitude) / 2; } if (8 & $v) { $minLatitude = ($minLatitude + $maxLatitude) / 2; } else { $maxLatitude = ($minLatitude + $maxLatitude) / 2; } if (4 & $v) { $minLongitude = ($minLongitude + $maxLongitude) / 2; } else { $maxLongitude = ($minLongitude + $maxLongitude) / 2; } if (2 & $v) { $minLatitude = ($minLatitude + $maxLatitude) / 2; } else { $maxLatitude = ($minLatitude + $maxLatitude) / 2; } if (1 & $v) { $minLongitude = ($minLongitude + $maxLongitude) / 2; } else { $maxLongitude = ($minLongitude + $maxLongitude) / 2; } $latitudeError /= 4; $longitudeError /= 8; } } $result['minLatitude'] = $minLatitude; $result['minLongitude'] = $minLongitude; $result['maxLatitude'] = $maxLatitude; $result['maxLongitude'] = $maxLongitude; $result['centerLatitude'] = round(($minLatitude + $maxLatitude) / 2, max(1, -round(log10($latitudeError))) - 1); $result['centerLongitude'] = round(($minLongitude + $maxLongitude) / 2, max(1, -round(log10($longitudeError))) - 1); return $result; } /** * 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 static function adjacent($hash, $direction) { $last = substr($hash, -1); $type = (strlen($hash) % 2) ? 'odd' : 'even'; $base = substr($hash, 0, strlen($hash) - 1); if (strpos((self::$borders[$direction][$type]), $last) !== false) { $base = self::adjacent($base, $direction); } return $base . self::$characterTable[strpos(self::$neighbours[$direction][$type], $last)]; } }