<?php
/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4 foldmethod=marker: */

/**
 * PEAR::Services_Weather_Weatherdotcom
 *
 * PHP versions 4 and 5
 *
 * <LICENSE>
 * Copyright (c) 2005-2007, Alexander Wirtz
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * o Redistributions of source code must retain the above copyright notice,
 *   this list of conditions and the following disclaimer.
 * o Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 * o Neither the name of the software nor the names of its contributors
 *   may be used to endorse or promote products derived from this software
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 * </LICENSE>
 *
 * @category    Web Services
 * @package     Services_Weather
 * @author      Alexander Wirtz <alex@pc4p.net>
 * @copyright   2005-2007 Alexander Wirtz
 * @license     http://www.opensource.org/licenses/bsd-license.php  BSD License
 * @version     CVS: $Id: Weatherdotcom.php,v 1.63 2008/05/10 20:43:36 eru Exp $
 * @link        http://pear.php.net/package/Services_Weather
 * @link        http://www.weather.com/services/xmloap.html
 * @example     examples/weather.com-basic.php      weather.com-basic.php
 * @example     examples/weather.com-extensive.php  weather.com-extensive.php
 * @filesource
 */

require_once "Services/Weather/Common.php";

// {{{ class Services_Weather_Weatherdotcom
/**
 * This class acts as an interface to the xml service of weather.com. It
 * searches for given locations and retrieves current weather data as well
 * as forecast for up to 5 days.
 *
 * For using the weather.com xml-service please visit
 *     http://www.weather.com/services/xmloap.html
 * and follow the link to sign up, it's free! You will receive an email
 * where to download the SDK with the needed images and guidelines how to
 * publish live data from weather.com. Unfortunately the guidelines are a
 * bit harsh, that's why there's no actual data-representation in this
 * class, just the raw data. Also weather.com demands active caching, so I'd
 * strongly recommend enabling the caching implemented in this class. It
 * obeys to the times as written down in the guidelines.
 *
 * For working examples, please take a look at
 *     docs/Services_Weather/examples/weather.com-basic.php
 *     docs/Services_Weather/examples/weather.com-extensive.php
 *
 * @category    Web Services
 * @package     Services_Weather
 * @author      Alexander Wirtz <alex@pc4p.net>
 * @copyright   2005-2007 Alexander Wirtz
 * @license     http://www.opensource.org/licenses/bsd-license.php  BSD License
 * @version     Release: 1.4.3
 * @link        http://pear.php.net/package/Services_Weather
 * @link        http://www.weather.com/services/xmloap.html
 * @example     examples/weather.com-basic.php      weather.com-basic.php
 * @example     examples/weather.com-extensive.php  weather.com-extensive.php
 */
class Services_Weather_Weatherdotcom extends Services_Weather_Common {

    // {{{ properties
    /**
     * Partner-ID at weather.com
     *
     * @var     string                      $_partnerID
     * @access  private
     */
    var $_partnerID = "";

    /**
     * License key at weather.com
     *
     * @var     string                      $_licenseKey
     * @access  private
     */
    var $_licenseKey = "";

    /**
     * Switch to toggle pre-fetching of data in one single request
     *
     * @var     bool                        $_preFetch
     * @access  private
     */
     var $_preFetch = false;

    /**
     * Object containing the promotional links-data
     *
     * @var     object stdClass             $_links
     * @access  private
     */
    var $_links;

    /**
     * XML_Unserializer, used for processing the xml
     *
     * @var     object XML_Unserializer     $_unserializer
     * @access  private
     */
    var $_unserializer;
    // }}}

    // {{{ constructor
    /**
     * Constructor
     *
     * Requires XML_Serializer to be installed
     *
     * @param   array                       $options
     * @param   mixed                       $error
     * @throws  PEAR_Error
     * @access  private
     */
    function Services_Weather_Weatherdotcom($options, &$error)
    {
        $perror = null;
        $this->Services_Weather_Common($options, $perror);
        if (Services_Weather::isError($perror)) {
            $error = $perror;
            return;
        }

        // Set options accordingly
        if (isset($options["partnerID"])) {
            $this->setAccountData($options["partnerID"]);
        }
        if (isset($options["licenseKey"])) {
            $this->setAccountData("", $options["licenseKey"]);
        }
        if (isset($options["preFetch"])) {
            $this->enablePreFetch($options["preFetch"]);
        }

        include_once "XML/Unserializer.php";
        $unserializer = &new XML_Unserializer(array("complexType" => "object", "keyAttribute" => "type"));
        if (Services_Weather::isError($unserializer)) {
            $error = $unserializer;
            return;
        } else {
            $this->_unserializer = $unserializer;
        }

        // Can't acquire an object here, has to be clean on every request
        include_once "HTTP/Request.php";
    }
    // }}}

    // {{{ setAccountData()
    /**
     * Sets the neccessary account-information for weather.com, you'll
     * receive them after registering for the XML-stream
     *
     * @param   string                      $partnerID
     * @param   string                      $licenseKey
     * @access  public
     */
    function setAccountData($partnerID, $licenseKey)
    {
        if (strlen($partnerID) && ctype_digit($partnerID)) {
            $this->_partnerID  = $partnerID;
        }
        if (strlen($licenseKey) && ctype_alnum($licenseKey)) {
            $this->_licenseKey = $licenseKey;
        }
    }
    // }}}

    // {{{ enablePreFetch()
    /**
     * Enables pre-fetching of data in one single request
     *
     * @param   bool                        $preFetch
     * @access  public
     */
    function enablePreFetch($preFetch)
    {
        if ($preFetch == true) {
            $this->_preFetch = true;
        }
    }
    // }}}

    // {{{ _checkLocationID()
    /**
     * Checks the id for valid values and thus prevents silly requests to
     * weather.com server
     *
     * @param   string                      $id
     * @return  PEAR_Error|bool
     * @throws  PEAR_Error::SERVICES_WEATHER_ERROR_NO_LOCATION
     * @throws  PEAR_Error::SERVICES_WEATHER_ERROR_INVALID_LOCATION
     * @access  private
     */
    function _checkLocationID($id)
    {
        if (is_array($id) || is_object($id) || !strlen($id)) {
            return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_NO_LOCATION, __FILE__, __LINE__);
        } elseif (!ctype_alnum($id) || (strlen($id) > 8)) {
            return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_INVALID_LOCATION, __FILE__, __LINE__);
        }

        return true;
    }
    // }}}

    // {{{ _parseWeatherData()
    /**
     * Fetches the data based on the requested type and caches it
     *
     * @param   string                      $id
     * @param   string                      $reqType
     * @return  PEAR_Error|bool
     * @throws  PEAR_Error::SERVICES_WEATHER_ERROR_WRONG_SERVER_DATA
     * @throws  PEAR_Error
     * @access  private
     */
    function _parseWeatherData($id, $reqType)
    {
        if ($this->_preFetch) {
            $reqType = "all";
        }

        $url = "http://xoap.weather.com/weather/local/".$id."?link=xoap&prod=xoap&par=".$this->_partnerID."&key=".$this->_licenseKey;

        switch ($reqType) {
            case "links":
                $url .= "&link=xoap";
                break;
            case "weather":
                $url .= "&cc=*&unit=s";
                break;
            case "forecast":
                $url .= "&dayf=5&unit=s";
                break;
            case "all":
                $url .= "&link=xoap&cc=*&dayf=5&unit=s";
                break;
        }

        // Get data from URL...
        $request = &new HTTP_Request($url, $this->_httpOptions);
        $status = $request->sendRequest();
        if (Services_Weather::isError($status) || (int) $request->getResponseCode() <> 200) {
            return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_WRONG_SERVER_DATA, __FILE__, __LINE__);
        }
        $data = $request->getResponseBody();

        // ...and unserialize
        $status = $this->_unserializer->unserialize($data);

        if (Services_Weather::isError($status)) {
            return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_WRONG_SERVER_DATA, __FILE__, __LINE__);
        } else {
            $root = $this->_unserializer->getRootName();
            $data = $this->_unserializer->getUnserializedData();

            if (Services_Weather::isError($root) || $root == "HTML") {
                // Something wrong here, maybe not XML retrieved...
                return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_WRONG_SERVER_DATA, __FILE__, __LINE__);
            } elseif ($root == "error") {
                // We got an error back from weather.com
                $errno  = key(get_object_vars($data));
                return Services_Weather::raiseError($errno, __FILE__, __LINE__);
            } else {
                // Valid data, lets get started
                // Loop through the different sub-parts of the data fro processing
                foreach (get_object_vars($data) as $key => $val) {
                    switch ($key) {
                        case "head":
                            continue 2;
                        case "prmo":
                            $varname  = "links";
                            break;
                        case "loc":
                            $varname  = "location";
                            break;
                        case "cc":
                            $varname  = "weather";
                            break;
                        case "dayf":
                            $varname  = "forecast";
                            break;
                    }
                    // Save data in object
                    $this->{"_".$varname} = $val;
                    if ($this->_cacheEnabled) {
                        // ...and cache if possible
                        $expire = constant("SERVICES_WEATHER_EXPIRES_".strtoupper($varname));
                        $this->_cache->extSave($id, $val, "", $expire, $varname);
                    }
                }
            }
        }

        return true;
    }
    // }}}

    // {{{ searchLocation()
    /**
     * Searches IDs for given location, returns array of possible locations
     * or single ID
     *
     * @param   string                      $location
     * @param   bool                        $useFirst       If set, first ID of result-array is returned
     * @return  PEAR_Error|array|string
     * @throws  PEAR_Error::SERVICES_WEATHER_ERROR_WRONG_SERVER_DATA
     * @throws  PEAR_Error::SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION
     * @access  public
     */
    function searchLocation($location, $useFirst = false)
    {
        $location = trim($location);
        $locLow   = strtolower($location);
        
        // Check on cached data: MD5-hash of location has to be correct and the userdata has to be the same as the given location 
        if ($this->_cacheEnabled && $locLow == $this->_cache->getUserData(md5($locLow), "search")) {
            $search = $this->_cache->get(md5($locLow), "search");
        } else {
            // Get search data from server and unserialize
            $request = &new HTTP_Request("http://xoap.weather.com/search/search?where=".urlencode($location), $this->_httpOptions);
            $status = $request->sendRequest();
            if (Services_Weather::isError($status) || (int) $request->getResponseCode() <> 200) {
                return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_WRONG_SERVER_DATA, __FILE__, __LINE__);
            }
            $data = $request->getResponseBody();
    
            // ...and unserialize
            $status = $this->_unserializer->unserialize($data, false, array("overrideOptions" => true, "complexType" => "array", "keyAttribute" => "id"));
    
            if (Services_Weather::isError($status)) {
                return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_WRONG_SERVER_DATA, __FILE__, __LINE__);
            }
    
            $root = $this->_unserializer->getRootName();
            $search = $this->_unserializer->getUnserializedData();
    
            if (Services_Weather::isError($search) || $root == "HTML") {
                return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_WRONG_SERVER_DATA, __FILE__, __LINE__);
            } elseif (!is_array($search) || !sizeof($search)) {
                return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION, __FILE__, __LINE__);
            }

            if ($this->_cacheEnabled) {
                // ...and cache if possible
                $expire = constant("SERVICES_WEATHER_EXPIRES_SEARCH");
                $this->_cache->extSave(md5($locLow), $search, $locLow, $expire, "search");
            }
        }

        if (!$useFirst && (sizeof($search) > 1)) {
            $searchReturn = $search;
        } elseif ($useFirst || (sizeof($search) == 1)) {
            $searchReturn = key($search);
        } else {
            $searchReturn = array();
        }

        return $searchReturn;
    }
    // }}}

    // {{{ searchLocationByCountry()
    /**
     * Returns only false, as weather.com offers no country listing via
     * its XML services
     *
     * @param   string                      $country
     * @return  bool
     * @access  public
     * @deprecated
     */
    function searchLocationByCountry($country = "")
    {
        return false;
    }
    // }}}

    // {{{ getLinks()
    /**
     * Returns the data for the promotional links belonging to the ID
     *
     * @param   string                      $id
     * @return  PEAR_Error|array
     * @throws  PEAR_Error
     * @access  public
     */
    function getLinks($id = "")
    {
        $status = $this->_checkLocationID($id);

        if (Services_Weather::isError($status)) {
            return $status;
        }

        $linksReturn = array();

        if ($this->_cacheEnabled && ($links = $this->_cache->get($id, "links"))) {
            // Get data from cache
            $this->_links = $links;
            $linksReturn["cache"] = "HIT";
        } else {
            // Same as in the function above...
            $status = $this->_parseWeatherData($id, "links");

            if (Services_Weather::isError($status)) {
                return $status;
            }
            $linksReturn["cache"] = "MISS";
        }

        $linksReturn["promo"] = array();
        for ($i = 0; $i < sizeof($this->_links->link); $i++) {
            $linksReturn["promo"][$i] = array();
            $linksReturn["promo"][$i]["title"] = $this->_links->link[$i]->t;
            // B0rked response (returned is &par=xoap, should be &prod=xoap), fix it
            $linksReturn["promo"][$i]["link"]  = str_replace("par=", "prod=", $this->_links->link[$i]->l);
            $linksReturn["promo"][$i]["link"] .= "&par=".$this->_partnerID;
        }

        return $linksReturn;
    }
    // }}}

    // {{{ getLocation()
    /**
     * Returns the data for the location belonging to the ID
     *
     * @param   string                      $id
     * @return  PEAR_Error|array
     * @throws  PEAR_Error
     * @access  public
     */
    function getLocation($id = "")
    {
        $status = $this->_checkLocationID($id);

        if (Services_Weather::isError($status)) {
            return $status;
        }

        $locationReturn = array();

        if ($this->_cacheEnabled && ($location = $this->_cache->get($id, "location"))) {
            // Get data from cache
            $this->_location = $location;
            $locationReturn["cache"] = "HIT";
        } else {
            // Same as in the function above...
            $status = $this->_parseWeatherData($id, "location");

            if (Services_Weather::isError($status)) {
                return $status;
            }
            $locationReturn["cache"] = "MISS";
        }

        $locationReturn["name"]      = $this->_location->dnam;
        $locationReturn["time"]      = date($this->_timeFormat, strtotime($this->_location->tm));
        $locationReturn["latitude"]  = $this->_location->lat;
        $locationReturn["longitude"] = $this->_location->lon;
        $locationReturn["sunrise"]   = date($this->_timeFormat, strtotime($this->_location->sunr));
        $locationReturn["sunset"]    = date($this->_timeFormat, strtotime($this->_location->suns));
        $locationReturn["timezone"]  = $this->_location->zone;

        return $locationReturn;
    }
    // }}}

    // {{{ getWeather()
    /**
     * Returns the weather-data for the supplied location
     *
     * @param   string                      $id
     * @param   string                      $unitsFormat
     * @return  PEAR_Error|array
     * @throws  PEAR_Error
     * @access  public
     */
    function getWeather($id = "", $unitsFormat = "")
    {
        $status = $this->_checkLocationID($id);

        if (Services_Weather::isError($status)) {
            return $status;
        }

        // Get other data
        $units    = $this->getUnitsFormat($unitsFormat);

        $weatherReturn = array();

        if ($this->_cacheEnabled && ($weather = $this->_cache->get($id, "weather"))) {
            // Same procedure...
            $this->_weather = $weather;
            $weatherReturn["cache"] = "HIT";
        } else {
            // ...as last function
            $status = $this->_parseWeatherData($id, "weather");

            if (Services_Weather::isError($status)) {
                return $status;
            }
            $weatherReturn["cache"] = "MISS";
        }

        // Some explanation for the next two lines:
        // weather.com isn't always supplying the timezone in the update string, but
        // uses "Local Time" as reference, which is imho utterly stupid, because it's
        // inconsistent. Well, what I do here is check for this string and if I can
        // find it, I calculate the difference between the timezone at the location
        // and this computers timezone. This amount of seconds is then subtracted from
        // the time the update-string has delivered.
        $update   = str_replace("Local Time", "", $this->_weather->lsup);
        $adjustTZ = ($update == $this->_weather->lsup) ? 0 : $this->_location->zone * 3600 - date("Z");
        $weatherReturn["update"]            = gmdate(trim($this->_dateFormat." ".$this->_timeFormat), strtotime($update) - $adjustTZ);
        $weatherReturn["updateRaw"]         = $this->_weather->lsup;
        $weatherReturn["station"]           = $this->_weather->obst;
        $weatherReturn["temperature"]       = $this->convertTemperature($this->_weather->tmp, "f", $units["temp"]);
        $weatherReturn["feltTemperature"]   = $this->convertTemperature($this->_weather->flik, "f", $units["temp"]);
        $weatherReturn["condition"]         = $this->_weather->t;
        $weatherReturn["conditionIcon"]     = $this->_weather->icon;
        $weatherReturn["pressure"]          = $this->convertPressure($this->_weather->bar->r, "in", $units["pres"]);
        $weatherReturn["pressureTrend"]     = $this->_weather->bar->d;
        $weatherReturn["wind"]              = $this->convertSpeed($this->_weather->wind->s, "mph", $units["wind"]);
        $weatherReturn["windGust"]          = $this->convertSpeed($this->_weather->wind->gust, "mph", $units["wind"]);
        $weatherReturn["windDegrees"]       = $this->_weather->wind->d;
        $weatherReturn["windDirection"]     = $this->_weather->wind->t;
        $weatherReturn["humidity"]          = $this->_weather->hmid;
        if (is_numeric($this->_weather->vis)) {
            $weatherReturn["visibility"]    = $this->convertDistance($this->_weather->vis, "sm", $units["vis"]);
        } else {
            $weatherReturn["visibility"]    = $this->_weather->vis;
        }
        $weatherReturn["uvIndex"]           = $this->_weather->uv->i;
        $weatherReturn["uvText"]            = $this->_weather->uv->t;
        $weatherReturn["dewPoint"]          = $this->convertTemperature($this->_weather->dewp, "f", $units["temp"]);

        return $weatherReturn;
    }
    // }}}

    // {{{ getForecast()
    /**
     * Get the forecast for the next days
     *
     * @param   string                      $id
     * @param   int                         $days           Values between 1 and 5
     * @param   string                      $unitsFormat
     * @return  PEAR_Error|array
     * @throws  PEAR_Error
     * @access  public
     */
    function getForecast($id = "", $days = 2, $unitsFormat = "")
    {
        $status = $this->_checkLocationID($id);

        if (Services_Weather::isError($status)) {
            return $status;
        }
        if (!in_array($days, range(1, 5))) {
            $days = 2;
        }

        // Get other data
        $units    = $this->getUnitsFormat($unitsFormat);

        $forecastReturn = array();

        if ($this->_cacheEnabled && ($forecast = $this->_cache->get($id, "forecast"))) {
            // Encore...
            $this->_forecast = $forecast;
            $forecastReturn["cache"] = "HIT";
        } else {
            // ...
            $status = $this->_parseWeatherData($id, "forecast");

            if (Services_Weather::isError($status)) {
                return $status;
            }
            $forecastReturn["cache"] = "MISS";
        }

        // Some explanation for the next two lines: (same as above)
        // weather.com isn't always supplying the timezone in the update string, but
        // uses "Local Time" as reference, which is imho utterly stupid, because it's
        // inconsistent. Well, what I do here is check for this string and if I can
        // find it, I calculate the difference between the timezone at the location
        // and this computers timezone. This amount of seconds is then subtracted from
        // the time the update-string has delivered.
        $update   = str_replace("Local Time", "", $this->_forecast->lsup);
        $adjustTZ = ($update == $this->_forecast->lsup) ? 0 : $this->_location->zone * 3600 - date("Z");
        $forecastReturn["update"]    = gmdate($this->_dateFormat." ".$this->_timeFormat, strtotime($update) - $adjustTZ);
        $forecastReturn["updateRaw"] = $this->_forecast->lsup;
        $forecastReturn["days"]      = array();

        for ($i = 0; $i < $days; $i++) {
            $day = array(
                "temperatureHigh" => $this->convertTemperature($this->_forecast->day[$i]->hi, "f", $units["temp"]),
                "temperatureLow"  => $this->convertTemperature($this->_forecast->day[$i]->low, "f", $units["temp"]),
                "sunrise"         => date($this->_timeFormat, strtotime($this->_forecast->day[$i]->sunr)),
                "sunset"          => date($this->_timeFormat, strtotime($this->_forecast->day[$i]->suns)),
                "day" => array(
                    "condition"     => $this->_forecast->day[$i]->part[0]->t,
                    "conditionIcon" => $this->_forecast->day[$i]->part[0]->icon,
                    "wind"          => $this->convertSpeed($this->_forecast->day[$i]->part[0]->wind->s, "mph", $units["wind"]),
                    "windGust"      => $this->convertSpeed($this->_forecast->day[$i]->part[0]->wind->gust, "mph", $units["wind"]),
                    "windDegrees"   => $this->_forecast->day[$i]->part[0]->wind->d,
                    "windDirection" => $this->_forecast->day[$i]->part[0]->wind->t,
                    "precipitation" => $this->_forecast->day[$i]->part[0]->ppcp,
                    "humidity"      => $this->_forecast->day[$i]->part[0]->hmid
                ),
                "night" => array (
                    "condition"     => $this->_forecast->day[$i]->part[1]->t,
                    "conditionIcon" => $this->_forecast->day[$i]->part[1]->icon,
                    "wind"          => $this->convertSpeed($this->_forecast->day[$i]->part[1]->wind->s, "mph", $units["wind"]),
                    "windGust"      => $this->convertSpeed($this->_forecast->day[$i]->part[1]->wind->gust, "mph", $units["wind"]),
                    "windDegrees"   => $this->_forecast->day[$i]->part[1]->wind->d,
                    "windDirection" => $this->_forecast->day[$i]->part[1]->wind->t,
                    "precipitation" => $this->_forecast->day[$i]->part[1]->ppcp,
                    "humidity"      => $this->_forecast->day[$i]->part[1]->hmid
                )
            );

            $forecastReturn["days"][] = $day;
        }

        return $forecastReturn;
    }
    // }}}
}
// }}}
?>
