Current File : //opt/RZphp83/includes/Services/GeoNames.php
<?php

/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */

/**
 * This file is part of the PEAR Services_GeoNames package.
 *
 * PHP version 5
 *
 * LICENSE: This source file is subject to the MIT license that is available
 * through the world-wide-web at the following URI:
 * http://opensource.org/licenses/mit-license.php
 *
 * @category  Services
 * @package   Services_GeoNames
 * @author    David Jean Louis <izi@php.net>
 * @copyright 2008-2009 David Jean Louis
 * @license   http://opensource.org/licenses/mit-license.php MIT License 
 * @version   CVS: $Id: GeoNames.php 303916 2010-10-01 10:54:24Z izi $
 * @link      http://pear.php.net/package/Services_GeoNames
 * @link      http://www.geonames.org/export/web-services.html
 * @since     File available since release 0.1.0
 * @filesource
 */

/**
 * Dependencies.
 */
require_once 'Services/GeoNames/Exception.php';
require_once 'HTTP/Request2.php';
 
/**
 * Main interface to the GeoNames API:
 * {@link http://www.geonames.org/export/web-services.html}
 *
 * @category  Services
 * @package   Services_GeoNames
 * @author    David Jean Louis <izi@php.net>
 * @copyright 2008-2009 David Jean Louis
 * @license   http://opensource.org/licenses/mit-license.php MIT License 
 * @version   Release: 1.0.1
 * @link      http://pear.php.net/package/Services_GeoNames
 * @link      http://www.geonames.org/export/web-services.html
 * @link      http://www.geonames.org/export/ws-overview.html
 * @since     Class available since release 0.1.0
 *
 * @method array    children()                children(array $params)
 * @method array    cities()                  cities(array $params)
 * @method stdclass countryCode()             countryCode(array $params)
 * @method array    countryInfo()             countryInfo(array $params)
 * @method stdclass countrySubdivision()      countrySubdivision(array $params)
 * @method array    earthquakes()             earthquakes(array $params)
 * @method array    findNearby()              findNearby(array $params)
 * @method array    findNearbyPlaceName()     findNearbyPlaceName(array $params)
 * @method array    findNearbyPostalCodes()   findNearbyPostalCodes(array $params)
 * @method array    findNearbyStreets()       findNearbyStreets(array $params)
 * @method stdclass findNearByWeather()       findNearByWeather(array $params)
 * @method array    findNearbyWikipedia()     findNearbyWikipedia(array $params)
 * @method stdclass findNearestAddress()      findNearestAddress(array $params)
 * @method stdclass findNearestIntersection() findNearestIntersection(array $params)
 * @method stdclass get()                     get(array $params)
 * @method stdclass gtopo30()                 gtopo30(array $params)
 * @method array    hierarchy()               hierarchy(array $params)
 * @method stdclass neighbourhood()           neighbourhood(array $params)
 * @method array    neighbours()              neighbours(array $params)
 * @method array    postalCodeCountryInfo()   postalCodeCountryInfo(array $params)
 * @method array    postalCodeLookup()        postalCodeLookup(array $params)
 * @method array    postalCodeSearch()        postalCodeSearch(array $params)
 * @method array    search()                  search(array $params)
 * @method array    siblings()                siblings(array $params)
 * @method array    weather()                 weather(array $params)
 * @method stdclass weatherIcao()             weatherIcao(array $params)
 * @method stdclass srtm3()                   srtm3(array $params)
 * @method stdclass timezone()                timezone(array $params)
 * @method array    wikipediaBoundingBox()    wikipediaBoundingBox(array $params)
 * @method array    wikipediaSearch()         wikipediaSearch(array $params)
 */
class Services_GeoNames
{
    // constants {{{

    /**
     * Exception code constant defined by this package.
     */
    const UNSUPPORTED_ENDPOINT = 1;
     
    /**#@+
     * Exception codes constants from:
     * {@link http://www.geonames.org/export/webservice-exception.html}
     */
    const AUTHORIZATION_EXCEPTION          = 10;
    const RECORD_DOES_NOT_EXIST            = 11;
    const OTHER_ERROR                      = 12;
    const DATABASE_TIMEOUT                 = 13;
    const INVALID_PARAMETER                = 14;
    const NO_RESULT_FOUND                  = 15;
    const DUPLICATE_EXCEPTION              = 16;
    const POSTAL_CODE_NOT_FOUND            = 17;
    const DAILY_LIMIT_OF_CREDITS_EXCEEDED  = 18;
    const HOURLY_LIMIT_OF_CREDITS_EXCEEDED = 19;
    const WEEKLY_LIMIT_OF_CREDITS_EXCEEDED = 20;
    /**#@-*/
    
    // }}}
    // properties {{{

    /**
     * Url of the GeoNames web service.
     *
     * @var string $url
     */
    public $url = 'http://ws.geonames.org';

    /**
     * Array of failover servers.
     *
     * @var array $failoverServers
     * @see Services_GeoNames::sendRequest()
     */
    public $failoverServers = array();

    /**
     * The HTTP_Request2 instance, you can customize the request if you want to 
     * (proxy, auth etc...) with the get/setRequest() methods.
     *
     * @var HTTP_Request2 $request
     * @see Services_GeoNames::getRequest()
     * @see Services_GeoNames::setRequest()
     */
    protected $request;

    /**
     * Auth username, only relevant for the geonames commercial web services:
     * {@link http://www.geonames.org/commercial-webservices.html}
     *
     * @var string $username
     * @see Services_GeoNames::__construct()
     */
    protected $username;

    /**
     * Auth token, only relevant for the geonames commercial web services:
     * {@link http://www.geonames.org/commercial-webservices.html}
     *
     * @var string $token
     * @see Services_GeoNames::__construct()
     */
    protected $token;

    /**
     * Array of supported endpoints (listed alphabetically) and their 
     * corresponding root property (if any). You can retrieve the list of 
     * endpoints (only the keys of this array) with the 
     * Services_GeoNames::getSupportedEndpoints() method.
     * 
     * Note that we only support json endpoints, so the following endpoints are
     * not supported:
     * - extendedFindNearby (JSON not available for now)
     * - rssToGeo (RSS/KML only)
     *
     * For a full documentation of the available endpoints services, please 
     * see: {@link http://www.geonames.org/export/ws-overview.html}.
     *
     * @var array $endpoints
     * @see Services_GeoNames::getSupportedEndpoints()
     */
    protected $endpoints = array(
        'children'                => 'geonames',
        'cities'                  => 'geonames',
        'countryCode'             => false,
        'countryInfo'             => 'geonames',
        'countrySubdivision'      => false,
        'earthquakes'             => 'earthquakes',
        'findNearby'              => 'geonames',
        'findNearbyPlaceName'     => 'geonames',
        'findNearbyPostalCodes'   => 'postalCodes',
        'findNearbyStreets'       => 'streetSegment',
        'findNearByWeather'       => 'weatherObservation',
        'findNearbyWikipedia'     => 'geonames',
        'findNearestAddress'      => 'address',
        'findNearestIntersection' => 'intersection',
        'get'                     => false,
        'gtopo30'                 => false,
        'hierarchy'               => 'geonames',
        'neighbourhood'           => 'neighbourhood',
        'neighbours'              => 'geonames',
        'postalCodeCountryInfo'   => 'geonames',
        'postalCodeLookup'        => 'postalcodes', // not a typo
        'postalCodeSearch'        => 'postalCodes',
        'search'                  => 'geonames',
        'siblings'                => 'geonames',
        'weather'                 => 'weatherObservations',
        'weatherIcao'             => 'weatherObservation',
        'srtm3'                   => false,
        'timezone'                => false,
        'wikipediaBoundingBox'    => 'geonames',
        'wikipediaSearch'         => 'geonames',
    );

    // }}}
    // __construct() {{{
    
    /**
     * Constructor, if you're using a commercial account (optional), you must 
     * pass your "username" and "token".
     * 
     * @param string $username Username for commercial webservice (optional)
     * @param string $token    Token for commercial webservice (optional)
     * 
     * @return void
     * @access public
     */
    public function __construct($username = null, $token = null)
    {
        if ($username !== null) {
            $this->username = $username;
        }
        if ($token !== null) {
            $this->token = $token;
        }
    }
    
    // }}}
    // __call() {{{
    
    /**
     * Method interceptor that retrieves the corresponding endpoint and return
     * a json decoded object or throw a Services_GeoNames_Exception.
     * 
     * @param string $endpoint The endpoint to call
     * @param array  $params   Array of parameters to pass to the endpoint
     * 
     * @return mixed stdclass|array The JSON decoded response or an array
     * @throws Services_GeoNames_Exception When an invalid method is called or 
     *                                     when the websercices returns an error
     */
    public function __call($endpoint, $params = array())
    {
        // check that endpoint is supported
        if (!in_array($endpoint, $this->getSupportedEndpoints())) {
            throw new Services_GeoNames_Exception(
                'Unknown service endpoint "' . $endpoint . '"',
                self::UNSUPPORTED_ENDPOINT
            );
        }

        // handle params
        if (isset($params[0])) {
            $params = is_array($params[0]) 
                ? $params[0] 
                : array('geonameId' => $params[0]);
        } else {
            $params = array();
        }
        if (isset($params['type'])) {
            // we only do json
            unset($params['type']);
        }

        // manage authentication to commercial webservice
        if ($this->username !== null) {
            $params['username'] = $this->username;
        }
        if ($this->token !== null) {
            $params['token'] = $this->token;
        }

        // build the url and retrieve the result
        $qString = $this->formatQueryString($params);
        $urlPath = '/' . $endpoint . 'JSON?' . $qString;
        $ret     = json_decode($this->sendRequest($urlPath));

        // check if we have a error response
        if (isset($ret->status->message) && isset($ret->status->value)) {
            throw new Services_GeoNames_Exception(
                $ret->status->message,
                (int)$ret->status->value
            );
        }
        
        // remove useless root property, to make the result more user friendly
        if ($this->endpoints[$endpoint] !== false && $ret instanceof stdclass) {
            $prop = $this->endpoints[$endpoint];
            $ret  = $ret->$prop;
        }
        return $ret;
    }
    
    // }}}
    // sendRequest() {{{
    
    /**
     * Sends the request to the server using HTTP_Request2. 
     * 
     * @param string $urlPath The url path *without* the scheme://host
     * 
     * @return string The response body
     * @throws HTTP_Request2_Exception
     * @throws Services_GeoNames_HTTPException When something goes wrong when 
     *                                         building the request or 
     *                                         requesting the server.
     */
    protected function sendRequest($urlPath)
    {
        $exceptionStack = array();
        $response       = null;
        array_unshift($this->failoverServers, $this->url);

        foreach ($this->failoverServers as $server) { 
            try {
                $request = clone $this->getRequest();
                $request->setUrl(rtrim($server, '/') . $urlPath);
                $response = $request->send();
            } catch (Exception $exc) {
                $exceptionStack[] =  new Services_GeoNames_HTTPException(
                    $exc->getMessage(),
                    $exc
                );
                continue;
            }
            if ($response->getStatus() != 200) {
                $exceptionStack[] = new Services_GeoNames_HTTPException(
                    $response->getReasonPhrase(),
                    $response->getStatus(),
                    $response
                );
                // reset the response variable since it's not a valid one
                $response = null;
            } else {
                break;
            }
        }

        if ($response == null && !empty($exceptionStack)) {
            $lastException = $exceptionStack[count($exceptionStack)-1];
            if (count($exceptionStack) == 1) {
                throw $lastException;
            } else {
                throw new Services_GeoNames_HTTPException(
                    $lastException->getMessage(),
                    $exceptionStack
                );
            }
        }

        return $response->getBody();
    }
    
    // }}}
    // formatQueryString() {{{
    
    /**
     * Builds a valid query string (url and utf8 encoded) to pass to the 
     * endpoint and returns it.
     * 
     * @param array $params Associative array of query parameters (name=>val)
     * 
     * @return string The formatted query string
     */
    protected function formatQueryString($params = array())
    {
        $qString = array();
        foreach ($params as $name => $value) {
            if (is_array($value)) {
                foreach ($value as $val) {
                    $val = $this->isUtf8($val) ? $val : utf8_encode($val);
                    $qString[] = $name . '=' . urlencode($val);
                }
            } else {
                $value = $this->isUtf8($value) ? $value : utf8_encode($value);
                $qString[] = $name . '=' . urlencode($value);
            }
        }
        return implode('&', $qString);
    }
    
    // }}}
    // getRequest() {{{
    
    /**
     * Returns the HTTP_Request2 instance, if it's not yet set it is 
     * instanciated on the fly.
     * 
     * @return HTTP_Request2 The request
     * @see Services_GeoNames::$request
     */
    public function getRequest()
    {
        if (!$this->request instanceof HTTP_Request2) {
            $this->request = new HTTP_Request2();
        }
        return $this->request;
    }
    
    // }}}
    // setRequest() {{{
    
    /**
     * Sets the HTTP_Request2 instance.
     * 
     * @param HTTP_Request2 $request The request to set
     *
     * @return void
     * @see Services_GeoNames::$request
     */
    public function setRequest(HTTP_Request2 $request)
    {
        $this->request = $request;
    }
    
    // }}}
    // getSupportedEndpoints() {{{
    
    /**
     * Returns an array of supported services endpoints.
     * 
     * @return array The endpoints array
     * @see Services_GeoNames::$endpoints
     */
    public function getSupportedEndpoints()
    {
        return array_keys($this->endpoints);
    }
    
    // }}}
    // isUtf8() {{{

    /**
     * Check if the given string is a UTF-8 string or an iso-8859-1 one.
     *
     * @param string $str The string to check
     *
     * @return boolean Wether the string is unicode or not
     */
    protected function isUtf8($str)
    {
        return (bool)preg_match(
            '%^(?:
                  [\x09\x0A\x0D\x20-\x7E]            # ASCII
                | [\xC2-\xDF][\x80-\xBF]             # non-overlong 2-byte
                |  \xE0[\xA0-\xBF][\x80-\xBF]        # excluding overlongs
                | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}  # straight 3-byte
                |  \xED[\x80-\x9F][\x80-\xBF]        # excluding surrogates
                |  \xF0[\x90-\xBF][\x80-\xBF]{2}     # planes 1-3
                | [\xF1-\xF3][\x80-\xBF]{3}          # planes 4-15
                |  \xF4[\x80-\x8F][\x80-\xBF]{2}     # plane 16
            )*$%xs',
            $str
        );
    }

    // }}}
}