new Weather block

Dan Chlopek <[email protected]> Mon, 15 Dec 2003 15:13:44 -0600
Newsgroups gmane.comp.horde.jonah
Message-ID <[email protected]>
Rick Emery wrote:

> Quoting Dan Chlopek <[email protected]>:
>
> > I've been trying to port the ejse driver from Jonah into a standalone Horde
> > block that looks up weather by zip code.  I'm having some difficulty trying
> > to
> > get it all working, but once I've managed to get something that works, I'll
> > post it to the appropriate list..
>
> Great!

Well, I finally have something that works.  Its my first attempt at php, so it
may not be too pretty.  It's basically a port of the ejse driver from jonah,
along with some functions needed to parse the xml. It works standalone from
horde/lib/Block.  The block uses the Horde cache interface to cache the weather
data.  All you need to do is specify your zip code, and a refresh time in the
layout preferences. It also does metric/standard conversion.   Enjoy.

Regards,
Dan


-- 
Jonah mailing list
Frequently Asked Questions: http://horde.org/faq/
To unsubscribe, mail: [email protected]
weather.php (application/x-php, 21 KB)
<?php

class Horde_Block_weather extends Horde_Block {


    var $_app = 'horde';
    var $_type = 'weather';

    /**
     * The XML parser.
     * @var resource $_parser
     */
    var $_parser;

    /**
     * The current conditions.
     * @var array $_conditions
     */
    var $_conditions;

    /**
     * The forecast.
     * @var array $_forecast
     */
    var $_forecast;

    /**
     * The current day
     * @var integer $_currentDay
     */
    var $_currentDay;

    /**
     * Where we are in the XML document.
     * @var string $_currentTag
     */
    var $_currentTag;

    /**
     * Keys that tell us where we are in the XML document.
     * @var array $_keys
     */
    var $_keys = array('cityKey' => '^EXTENDEDWEATHERINFO^INFO^LOCATION',
                       'tempKey' => '^EXTENDEDWEATHERINFO^INFO^TEMPRATURE',
                       'realtempKey' => '^EXTENDEDWEATHERINFO^INFO^FEELSLIKE',
                       'conditionsKey' => '^EXTENDEDWEATHERINFO^INFO^FORECAST',
                       'visibilityKey' => '^EXTENDEDWEATHERINFO^INFO^VISIBILITY',
                       'barometerKey' => '^EXTENDEDWEATHERINFO^INFO^PRESSURE',
                       'uvKey' => '^EXTENDEDWEATHERINFO^INFO^UVINDEX',
                       'humidityKey' => '^EXTENDEDWEATHERINFO^INFO^HUMIDITY',
                       'windDirKey' => '^EXTENDEDWEATHERINFO^INFO^WIND',
                       'regionKey' => '^EXTENDEDWEATHERINFO^INFO^REPORTEDAT',
                       'dateKey' => '^EXTENDEDWEATHERINFO^INFO^LASTUPDATED',
                       'forecastDay1Key' => '^EXTENDEDWEATHERINFO^DAY1^DAY',
                       'forecastDay2Key' => '^EXTENDEDWEATHERINFO^DAY2^DAY',
                       'forecastDay3Key' => '^EXTENDEDWEATHERINFO^DAY3^DAY',
                       'forecastDay4Key' => '^EXTENDEDWEATHERINFO^DAY4^DAY',
                       'forecastDay5Key' => '^EXTENDEDWEATHERINFO^DAY5^DAY',
                       'forecastDay6Key' => '^EXTENDEDWEATHERINFO^DAY6^DAY',
                       'forecastDay7Key' => '^EXTENDEDWEATHERINFO^DAY7^DAY',
                       'forecastHigh1Key' => '^EXTENDEDWEATHERINFO^DAY1^HIGH',
                       'forecastHigh2Key' => '^EXTENDEDWEATHERINFO^DAY2^HIGH',
                       'forecastHigh3Key' => '^EXTENDEDWEATHERINFO^DAY3^HIGH',
                       'forecastHigh4Key' => '^EXTENDEDWEATHERINFO^DAY4^HIGH',
                       'forecastHigh5Key' => '^EXTENDEDWEATHERINFO^DAY5^HIGH',
                       'forecastHigh6Key' => '^EXTENDEDWEATHERINFO^DAY6^HIGH',
                       'forecastHigh7Key' => '^EXTENDEDWEATHERINFO^DAY7^HIGH',
                       'forecastLow1Key' => '^EXTENDEDWEATHERINFO^DAY1^LOW',
                       'forecastLow2Key' => '^EXTENDEDWEATHERINFO^DAY2^LOW',
                       'forecastLow3Key' => '^EXTENDEDWEATHERINFO^DAY3^LOW',
                       'forecastLow4Key' => '^EXTENDEDWEATHERINFO^DAY4^LOW',
                       'forecastLow5Key' => '^EXTENDEDWEATHERINFO^DAY5^LOW',
                       'forecastLow6Key' => '^EXTENDEDWEATHERINFO^DAY6^LOW',
                       'forecastLow7Key' => '^EXTENDEDWEATHERINFO^DAY7^LOW',
                       'forecastConditions1Key' => '^EXTENDEDWEATHERINFO^DAY1^FORECAST',
                       'forecastConditions2Key' => '^EXTENDEDWEATHERINFO^DAY2^FORECAST',
                       'forecastConditions3Key' => '^EXTENDEDWEATHERINFO^DAY3^FORECAST',
                       'forecastConditions4Key' => '^EXTENDEDWEATHERINFO^DAY4^FORECAST',
                       'forecastConditions5Key' => '^EXTENDEDWEATHERINFO^DAY5^FORECAST',
                       'forecastConditions6Key' => '^EXTENDEDWEATHERINFO^DAY6^FORECAST',
                       'forecastConditions7Key' => '^EXTENDEDWEATHERINFO^DAY7^FORECAST',
                       'forecast1Wid' => '^EXTENDEDWEATHERINFO^DAY1^ICONINDEX',
                       'forecast2Wid' => '^EXTENDEDWEATHERINFO^DAY2^ICONINDEX',
                       'forecast3Wid' => '^EXTENDEDWEATHERINFO^DAY3^ICONINDEX',
                       'forecast4Wid' => '^EXTENDEDWEATHERINFO^DAY4^ICONINDEX',
                       'forecast5Wid' => '^EXTENDEDWEATHERINFO^DAY5^ICONINDEX',
                       'forecast6Wid' => '^EXTENDEDWEATHERINFO^DAY6^ICONINDEX',
                       'forecast7Wid' => '^EXTENDEDWEATHERINFO^DAY7^ICONINDEX');

    function getParams()
    {

        return array(
            'location' => array(
                'type' => 'string',
                'name' => _("ZIP Code"),
                'default' => ''
            ),
            'expire' => array(
                'type' => 'string',
                'name' => _("Reload Period"),
                'default' => '900'
            ),
            'metric' => array(
                'type' => 'enum',
                'name' => _("Units"),
                'default' => 0,
                'values' => array(
                    0 => _("Standard"),
                    1 => _("Metric")
                )
            )

        );

    }


	
    /**
     * The title to go in this block.
     *
     * @return string   The title text.
     */
    function _title()
    {

	return _("Current Weather for zip ") . $this->_params['location'];

    }

    /**
     * The content to go in this block.
     *
     * @return string   The content
     */
    function _content()
    {

        // Get the location and refresh info from the paramaters
	$location = $this->_params['location'];
        $expire = $this->_params['expire'];


        $html = '<table border="0" cellspacing="0" cellpadding="0"><tr><td class="text" valign="top">';

            /* Get weekday names and weather info into the .po files. */
            array(_("Monday"), _("Tuesday"), _("Wednesday"), _("Thursday"), _("Friday"), _("Saturday"), _("Sunday"));
            array(_("windy"), _("thunder storms"), _("snow"), _("rain"),
                  _("heavy rain"), _("sleet"), _("light flurries"),
                  _("flurries"), _("snow and wind"), _("dust"), _("fog"),
                  _("haze"), _("smoke"), _("cold"), _("cloudy"), _("mostly cloudy"),
                  _("partly cloudy"), _("sunny"), _("mostly sunny"), _("hot"));

            

            // Get the weather info
	    $weather = $this->_getWeather('http://www.ejse.com/WeatherService/Service.asmx/GetExtendedWeatherInfo?zipCode=' . $location,
	    $expire);


            $html .= '<table border="0" cellspacing="0" cellpadding="1"><tr><td class="control"><b>';
            $sep = '';
            if (!empty($weather['conditions']['city'])) {
                $html .= $weather['conditions']['city'];
                $sep = ', ';
            }
            if (!empty($weather['conditions']['country']) && ($weather['conditions']['country'] != 'USA')) {
                $html .= $sep . $weather['conditions']['country'];
            } elseif (!empty($weather['conditions']['state'])) {
                $html .= $sep . $weather['conditions']['state'];
            }
            $html .= '</b>';
            if (!empty($weather['conditions']['date'])) {
                $html = sprintf(_("%s at %s"), $html, strftime('%X', strtotime($weather['conditions']['date'])));
            }
            $html .= '</td></tr><tr><td>';
            if (isset($weather['conditions']['temp'])) {
                $html .= sprintf(_("The temperature is <b>%s</b>, "),
                                 $this->_params['metric'] ?
                                 round((($weather['conditions']['temp'] - 32) * 5) / 9) . '&deg;C' : 
                                 $weather['conditions']['temp'] . '&deg;F');
            }
            if (isset($weather['conditions']['humidity'])) {
                $html .= sprintf(_("humidity is at %s"), $weather['conditions']['humidity'] . '% ');
            }
            if (isset($weather['conditions']['windDir'])) {
                if ($weather['conditions']['windDir'] == 'CALM') {
                    $html .= _("and there is no wind measured. ");
                } elseif (!empty($weather['conditions']['windDir'])) {
                    if (isset($weather['conditions']['windStrength'])) {
                        $html .= sprintf(_("and the wind is %s at %s. "),
                                         $weather['conditions']['windDir'],
                                         $this->_params['metric'] ?
                                         round($weather['conditions']['windStrength'] * 1609 / 1000) . _(" km/h") :
                                         $weather['conditions']['windStrength'] . _("mph"));
                    } else {
                        $html .= sprintf(_("and the wind is %s. "), $weather['conditions']['windDir']);
                    }
                }
            }
            if (isset($weather['conditions']['realtemp'])) {
                $html .= sprintf(_("It feels like %s. "),
                                 $this->_params['metric'] ?
                                 round((($weather['conditions']['realtemp'] - 32) * 5) / 9) . '&deg;C' :
                                 $weather['conditions']['realtemp'] . '&deg;F');
            }
            if (isset($weather['conditions']['barometer'])) {
                $html .= sprintf(_("Barometer shows %s. "),
                                 $this->_params['metric'] ?
                                 round($weather['conditions']['barometer'] * 3386 / 100) . ' hP' :
                                 $weather['conditions']['barometer']);
            }
            $visibility = '';
            if (isset($weather['conditions']['visibility'])) {
                $visibility = sprintf(_("Visibility is %s"),
                                      $weather['conditions']['visibility'] == 999 ?
                                      _("infinite") :
                                      ($this->_params['metric'] ?
                                       round(1609 * $weather['conditions']['visibility'] / 1000) . _(" km") :
                                       $weather['conditions']['visibility'] . _(" miles")));
            }
            $html .= $visibility;
            if (isset($weather['conditions']['uv'])) {
                if ($weather['conditions']['uv'] <= 2.5) {
                    $uv = sprintf(_("minimal %s"), $weather['conditions']['uv']);
                } elseif ($weather['conditions']['uv'] <= 4.5) {
                    $uv = sprintf(_("low %s"), $weather['conditions']['uv']);
                } elseif ($weather['conditions']['uv'] <= 6.5) {
                    $uv = sprintf(_("moderate %s"), $weather['conditions']['uv']);
                } elseif ($weather['conditions']['uv'] <= 9.5) {
                    $uv = sprintf(_("high %s. Use sunscreen"), $weather['conditions']['uv']);
                } else {
                    $uv = sprintf(_("very high %s. Stay indoors"), $weather['conditions']['uv']);
                }
                $html .= sprintf(empty($visibility) ? _("The UV index is a %s. ") : _(" and the UV index is a %s. "), $uv);
            } elseif (!empty($visibility)) {
                $html .= '. ';
            }
            $html .= '</td></tr>';
            if (isset($weather['forecast']) && (count($weather['forecast']) > 1)) {
                $html .= '<tr><td class="control"><b>' . sprintf(_("%d-Day Forecast"), count($weather['forecast'])) . '</b></td></tr><tr><td><table border="0" align="center"><tr>';
                foreach ($weather['forecast'] as $day => $forecast) {
                    $html .= '<td align="center" width="75"><b>' . _($day) . '</b><br /><span style="color:red">';
                    if ($this->_params['metric']) {
                        $html .= round((($forecast['high'] - 32) * 5) / 9);
                    } else {
                        $html .= $forecast['high'];
                    }
                    $html .= '&deg;</span>/<span style="color:blue">';
                    if ($this->_params['metric']) {
                        $html .= round((($forecast['low'] - 32) * 5) / 9);
                    } else {
                        $html .= $forecast['low'];
                    }
                    $html .= '&deg;</span><br />';
                    if (isset($forecast['conditions'])) {
                        $html .= Horde::img('weather/' . $forecast['wid'] . '.gif', gettext(String::lower($forecast['conditions'])), 'align="middle"', '/horde3/jonah/graphics');
                    }
                    $html .= '</td>';
                }
                $html .= '</tr></table></td></tr>';
            }
            $html .= '</table>';
        

        $html .= '</td></tr></table>';
        return $html; 

    }

    function _getWeather($location, $expire)
    {
        global $conf;

        require_once HORDE_LIBS . 'Horde/Cache.php';
        $cache = &Horde_Cache::singleton($conf['cache']['driver'], Horde::getDriverConfig('cache', $conf['cache']['driver']));
        if (is_a($cache, 'PEAR_Error')) {
            Horde::fatal($cache, __FILE__, __LINE__);
        }

        $weather = @unserialize($cache->getData('Block.ejse_weather.' . $location, 'false;', $expire));

        if (!$weather) {
            $this->_updateCache($location, $this->_parseUrl($location));
            $weather = @unserialize($cache->getData('Block.ejse_weather.' . $location, "Horde_Block_weather::_updateCache('$location');", 1));
        }
	
        return $weather;
    }

    function _updateCache($location, $data = null)
    {
        static $weather;

        if (is_null($data)) {
            return isset($weather[$location]) ?
                (is_a($weather[$location], 'PEAR_Error') ? '' : $weather[$location])
                : null;
        }

        $weather[$location] = $data;
	 
    	Horde::logMessage($weather[$location], __FILE__, __LINE__, PEAR_LOG_ERR);

    }

    function _parseUrl($url)
    {
        $this->_initialize();
        if (!$this->_parser) {
            return PEAR::raiseError(_("Could not find XML parser handle."));
        }

        $data = $this->_readUrl($url);
        if (is_a($data, 'PEAR_Error')) {
            Horde::logMessage($data, __FILE__, __LINE__, PEAR_LOG_ERR);
            return '';
        }

        $data = preg_replace(array('|<br />|', '|<b>.*</b>|'), '', $data);
        if (!xml_parse($this->_parser, $data)) {
            return PEAR::raiseError(sprintf(_("XML error: %s at line %d"), xml_error_string(xml_get_error_code($this->_parser)), xml_get_current_line_number($this->_parser)));
        }

        xml_parser_free($this->_parser);

        return serialize(array('conditions' => $this->_conditions,
                               'forecast' => $this->_forecast));
    }

    function _initialize()
    {
        $this->_conditions = array();
        $this->_forecast = array();
        $this->_currentDay = '';
        $this->_currentTag = '';

        // Create the XML parser.
        $this->_parser = xml_parser_create();
        xml_set_object($this->_parser, $this);
        xml_parser_set_option($this->_parser, XML_OPTION_CASE_FOLDING, true);
        xml_set_element_handler($this->_parser, '_start', '_end');
        xml_set_character_data_handler($this->_parser, '_data');
        xml_set_default_handler($this->_parser, '_default');
        xml_set_processing_instruction_handler($this->_parser, '');
        xml_set_external_entity_ref_handler($this->_parser, '');
    }

    function _readUrl($url)
    {
        $options['method'] = 'GET';
        $options['timeout'] = 5;
        $options['allowRedirects'] = true;

        require_once 'HTTP/Request.php';
        $http = &new HTTP_Request($url, $options);
        @$http->sendRequest();
        if ($http->getResponseCode() != 200) {
            return PEAR::raiseError(sprintf(_("Could not open %s."), $url));
        }

        return $http->getResponseBody();
    }

    /**
     * Start collecting data about a new element.
     */
    function _start($parser, $name, $attribs)
    {
        $this->_currentTag .= "^$name";
    }

    /**
     * Handle the ends of XML elements - wrap up whatever we've been
     * putting together and store it for safekeeping.
     */
    function _end($parser, $name)
    {
        $currentPosition = strrpos($this->_currentTag, '^');
        $this->_currentTag = substr($this->_currentTag, 0, $currentPosition);
    }


    /**
     * The handler for character data encountered in the XML file.
     */
    function _data($parser, $data)
    {
        $data = trim($data);
        switch ($this->_currentTag) {
        case $this->_keys['cityKey']:
            list($this->_conditions['city'], $this->_conditions['state']) = split(',', $data, 2);
            break;

        case $this->_keys['regionKey']:
            $this->_conditions['region'] = $data;
            break;

        case $this->_keys['dateKey']:
            list($time, $ampm) = preg_split('/^\S+, \S+ \S+, \S+, at ([0-9:]+) (\S+) .*/', $data, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
            $this->_conditions['date'] = "$time $ampm";
            break;

        case $this->_keys['tempKey']:
            $this->_conditions['temp'] = round($data);
            break;

        case $this->_keys['windDirKey']:
            list($this->_conditions['windDir'], $this->_conditions['windStrength']) = split(' at ', $data, 2);
            $this->_conditions['windStrength'] = substr($this->_conditions['windStrength'], 0, -3);
            break;

        case $this->_keys['barometerKey']:
            $this->_conditions['barometer'] = substr($data,0,5);
            $this->_conditions['barometer'] = $data;
            break;

        case $this->_keys['humidityKey']:
            $this->_conditions['humidity'] = round($data);
            break;

        case $this->_keys['realtempKey']:
            $this->_conditions['realtemp'] = round($data);
            break;

        case $this->_keys['uvKey']:
            $this->_conditions['uv'] = round($data);
            break;

        case $this->_keys['visibilityKey']:
            $this->_conditions['visibility'] = $data;
            break;

        case $this->_keys['conditionsKey']:
            $this->_conditions['conditions'] = $data;
            break;

        case $this->_keys['forecastDay1Key']:
        case $this->_keys['forecastDay2Key']:
        case $this->_keys['forecastDay3Key']:
        case $this->_keys['forecastDay4Key']:
        case $this->_keys['forecastDay5Key']:
        case $this->_keys['forecastDay6Key']:
        case $this->_keys['forecastDay7Key']:
            $this->_currentDay = $data;
            $this->_forecast[$this->_currentDay] = array();
            break;

        case $this->_keys['forecastHigh1Key']:
        case $this->_keys['forecastHigh2Key']:
        case $this->_keys['forecastHigh3Key']:
        case $this->_keys['forecastHigh4Key']:
        case $this->_keys['forecastHigh5Key']:
        case $this->_keys['forecastHigh6Key']:
        case $this->_keys['forecastHigh7Key']:
            $this->_forecast[$this->_currentDay]['high'] = round($data);
            break;

        case $this->_keys['forecastLow1Key']:
        case $this->_keys['forecastLow2Key']:
        case $this->_keys['forecastLow3Key']:
        case $this->_keys['forecastLow4Key']:
        case $this->_keys['forecastLow5Key']:
        case $this->_keys['forecastLow6Key']:
        case $this->_keys['forecastLow7Key']:
            $this->_forecast[$this->_currentDay]['low'] = round($data);
            break;

        case $this->_keys['forecastConditions1Key']:
        case $this->_keys['forecastConditions2Key']:
        case $this->_keys['forecastConditions3Key']:
        case $this->_keys['forecastConditions4Key']:
        case $this->_keys['forecastConditions5Key']:
        case $this->_keys['forecastConditions6Key']:
        case $this->_keys['forecastConditions7Key']:
            $this->_forecast[$this->_currentDay]['conditions'] = $data;
            break;

        case $this->_keys['forecast1Wid']:
        case $this->_keys['forecast2Wid']:
        case $this->_keys['forecast3Wid']:
        case $this->_keys['forecast4Wid']:
        case $this->_keys['forecast5Wid']:
        case $this->_keys['forecast6Wid']:
        case $this->_keys['forecast7Wid']:
            switch ($data) {
            case 23:
            case 24:
                $data = 1;
                break;

            case 4:
            case 17:
            case 35:
            case 37:
            case 38:
                $data = 3;
                break;

            case 9:
            case 11:
                $data = 6;
                break;

            case 39:
            case 40:
                $data = 18;
                break;

            case 8:
            case 10:
                $data = 7;
                break;

            case 16:
            case 41:
            case 42:
                $data = 15;
                break;

            case 28:
                $data = 27;
                break;

            case 30:
            case 44:
                $data = 29;
                break;

            case 32:
                $data = 31;
                break;

            case 34:
                $data = 33;
                break;
            }

            $this->_forecast[$this->_currentDay]['wid'] = $data;
            break;
        }
    }

    /**
     * Handles things that we don't recognize. A no-op.
     */
    function _default($parser, $data)
    {
    }


}