You are here

weather.module in Weather 5

Display <acronym title="METeorological Aerodrome Report">METAR</acronym> weather data from anywhere in the world

The module is compatible with Drupal 5.

@author Tobias Toedter

File

weather.module
View source
<?php

/*
 * Copyright (C) 2006 Tobias Toedter <t.toedter@gmx.net>
 *
 * This file is part of the Drupal Weather module.
 * It is based on the Weather module which was written in 2004 by
 * Gerard Ryan <gerardryan@canada.com>.
 *
 * Weather is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Weather is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Weather; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

/**
 * @file
 * Display <acronym title="METeorological Aerodrome Report">METAR</acronym>
 * weather data from anywhere in the world
 *
 * The module is compatible with Drupal 5.
 *
 * @author Tobias Toedter
 */

// Include the parser for METAR data
require_once drupal_get_path('module', 'weather') . '/weather_parser.inc';

/*********************************************************************
 * General Drupal hooks for registering the module
 ********************************************************************/

/**
 * Implementation of hook_perm().
 */
function weather_perm() {
  return array(
    'administer custom weather block',
  );
}

/**
 * Implementation of hook_menu().
 */
function weather_menu($may_cache) {
  global $user;
  $items = array();
  if ($may_cache) {
    $items[] = array(
      'path' => 'admin/settings/weather',
      'title' => t('Weather'),
      'description' => t('Configure the system-wide weather block.'),
      'callback' => 'weather_custom_block',
      'callback arguments' => array(
        0,
      ),
      'access' => user_access('access administration pages'),
      'type' => MENU_NORMAL_ITEM,
    );
    $items[] = array(
      'path' => 'user/' . $user->uid . '/weather',
      'title' => t('My weather'),
      'description' => t('Configure your custom weather block.'),
      'callback' => 'weather_custom_block',
      'callback arguments' => array(
        $user->uid,
      ),
      'access' => user_access('administer custom weather block'),
      'type' => MENU_LOCAL_TASK,
    );
  }
  return $items;
}

/*********************************************************************
 * Drupal hooks for the block content
 ********************************************************************/

/**
 * Generate HTML for the weather block
 * @param op operation from the URL
 * @param delta offset
 * @returns block HTML 
 */
function weather_block($op = 'list', $delta = 0) {
  global $user;
  if ($op == 'list') {
    $block[0]['info'] = t('Weather: system-wide');
    $block[1]['info'] = t('Weather: custom user');
    return $block;
  }
  else {
    if ($op == 'view') {
      if ($delta == 0 and user_access('access content')) {

        // show the system weather block
        $block['subject'] = t('Current weather');
        $block['content'] = '';
        $configs_in_use = _weather_get_configs_in_use(0);
        if (count($configs_in_use) == 0) {
          $configs_in_use = array(
            'cid' => 1,
          );
        }
        foreach ($configs_in_use as $index) {
          $config = _weather_get_config(0, $index['cid']);
          $metar = weather_get_metar($config['icao']);
          $block['content'] .= theme('weather', $config, $metar);
        }
        return $block;
      }
      else {
        if ($delta == 1 and user_access('administer custom weather block')) {

          // show the user's custom weather block
          $block['subject'] = t('Current weather');
          $block['content'] = '';
          $configs_in_use = _weather_get_configs_in_use($user->uid);
          if (count($configs_in_use) == 0) {
            $configs_in_use = array(
              'cid' => 1,
            );
          }
          foreach ($configs_in_use as $index) {
            $config = _weather_get_config($user->uid, $index['cid']);
            $metar = weather_get_metar($config['icao']);
            $block['content'] .= theme('weather', $config, $metar);
          }
          return $block;
        }
      }
    }
  }
}

/**
 * Custom theme function for the weather block output
 */
function theme_weather($config, $metar) {
  $content .= '<p><strong>' . check_plain($config['real_name']) . '</strong></p>';
  $image = _weather_get_image($metar);
  $content .= '<div style="text-align:center;"><img src="';
  $content .= $image['filename'];
  $content .= '" alt="' . $image['alt_text'] . '" width="64" height="64" /></div>';
  $content .= '<ul><li>';
  $content .= _weather_format_condition($metar);
  $content .= '</li><li>';
  $content .= t('Temperature: !temperature', array(
    '!temperature' => _weather_format_temperature($metar['temperature'], $config['units']),
  ));
  $content .= '</li><li>';
  $content .= t('Wind: !wind', array(
    '!wind' => _weather_format_wind($metar['wind'], $config['units']),
  ));
  $content .= '</li><li>';
  $content .= t('Pressure: !pressure', array(
    '!pressure' => _weather_format_pressure($metar['pressure'], $config['units']),
  ));
  $content .= '</li><li>';
  $content .= t('Rel. Humidity: !rel_humidity', array(
    '!rel_humidity' => _weather_format_relative_humidity($metar['temperature'], $metar['dewpoint']),
  ));
  $content .= '</li><li>';
  $content .= t('Visibility: !visibility', array(
    '!visibility' => _weather_format_visibility($metar['visibility'], $config['units']),
  ));
  $content .= '</li></ul>';
  $content .= '<small>';
  $content .= t("Reported on:");
  $content .= '<br />' . format_date($metar['reported_on']);
  $content .= '</small>';
  return $content;
}
function _weather_get_image($metar) {

  // is there any data available?
  if (!isset($metar['condition_text'])) {
    $name = 'nodata';
  }
  else {

    // handle special case: NSC, we just use few for the display
    if ($metar['condition_text'] == 'no-significant-clouds') {
      $metar['condition_text'] = 'few';
    }

    // calculate the sunrise and sunset times for day/night images
    $day_night = _weather_sunrise_sunset($metar);
    $name = $day_night . '-' . $metar['condition_text'];

    // handle rain images
    if (isset($metar['phenomena']['rain'])) {
      $rain = $metar['phenomena']['rain'];
    }
    else {
      if (isset($metar['phenomena']['drizzle'])) {
        $rain = $metar['phenomena']['drizzle'];
      }
    }
    if (isset($rain)) {
      if ($rain['#light']) {
        $name .= '-light-rain';
      }
      else {
        if ($rain['#heavy']) {
          $name .= '-heavy-rain';
        }
        else {
          $name .= '-moderate-rain';
        }
      }
    }
  }

  // set up final return array
  $image['filename'] .= base_path() . drupal_get_path('module', 'weather') . '/images/' . $name . '.png';
  $image['alt_text'] = $name;
  return $image;
}

/**
 * Internal helper function to calculate the sunrise and sunset times
 * for a given location
 */
function _weather_sunrise_sunset($metar) {

  // Include the ICAO codes
  require_once drupal_get_path('module', 'weather') . '/icao_codes.inc';
  $info = _weather_get_lat_long($metar['icao']);

  // reported_on is already GMT, so don't use gmdate()
  $day_of_year = date('z', $metar['reported_on']);
  $year = date('Y', $metar['reported_on']);
  $month = date('m', $metar['reported_on']);
  $day = date('d', $metar['reported_on']);

  // setup pi constants: 0.5*pi, 1.0*pi, 1.5*pi, 2.0*pi
  $pi_05 = 1.570796;
  $pi_10 = 3.141593;
  $pi_15 = 4.712389;
  $pi_20 = 6.283185;

  // convert latitude and longitude degree into radian
  // x rad = y° * pi / 180 = 0.017453 * y°
  $latitude = 0.017453 * $info['lat'];
  $longitude = 0.017453 * $info['long'];

  // we want always GMT time, so set to 0. Otherwise,
  // the timezone can be calculated as follows:
  // $timezone = 0.261799 * offset;
  $timezone = 0;

  // the sunrise/sunset altitude in radian (-0.833°)
  $altitude = -0.014539;
  $sunrise = 0;
  $sunset = 0;
  foreach (array(
    'sunrise' => $pi_05,
    'sunset' => $pi_15,
  ) as $type => $factor) {
    $a = $day_of_year + ($factor - $longitude) / $pi_20;

    // solar mean anomaly
    $sma = $a * 0.017202 - 0.0574039;

    // solar true longitude
    $stl = $sma + 0.0334405 * sin($sma);
    $stl += 4.93289 + 0.000349066 * sin(2 * $sma);

    // normalize the longitude to be between >= 0 and < 2.0*pi
    while ($stl < 0) {
      $stl += $pi_20;
    }
    while ($stl >= $pi_20) {
      $stl -= $pi_20;
    }
    if ($stl / $pi_05 - intval($stl / $pi_05) == 0) {
      $stl += 4.84814E-6;
    }

    // solar right ascension
    $sra = sin($stl) / cos($stl);
    $sra = atan2(0.9174600000000001 * $sra, 1);

    // adjust quadrant
    if ($stl > $pi_15) {
      $sra += $pi_20;
    }
    else {
      if ($stl > $pi_05) {
        $sra += $pi_10;
      }
    }

    // solar declination
    $sd = 0.39782 * sin($stl);
    $sd = $sd / sqrt(1 - $sd * $sd);
    $sd = atan2($sd, 1);
    $diurnal_arc = ($altitude - sin($sd) * sin($latitude)) / (cos($sd) * cos($latitude));

    // is there a sunrise or sunset at all?
    if ($diurnal_arc >= 1) {

      // no sunrise
      $no_sunrise = TRUE;
      break;
    }
    if ($diurnal_arc <= -1) {

      // no sunset
      $no_sunset = TRUE;
      break;
    }
    $diurnal_arc = $diurnal_arc / sqrt(1 - $diurnal_arc * $diurnal_arc);
    $diurnal_arc = $pi_05 - atan2($diurnal_arc, 1);
    if ($type == 'sunrise') {
      $diurnal_arc = $pi_20 - $diurnal_arc;
    }

    // calculate the time
    $localtime = $diurnal_arc + $sra - 0.0172028 * $a - 1.73364;

    // wall clock time
    $wallclock = $localtime - $longitude + $timezone;

    // normalize wallclock to be between >= 0 and < 2.0*pi
    while ($wallclock < 0) {
      $wallclock += $pi_20;
    }
    while ($wallclock >= $pi_20) {
      $wallclock -= $pi_20;
    }
    $wallclock = $wallclock * 3.81972;
    $hour = intval($wallclock);
    $minute = round(($wallclock - $hour) * 60, 0);
    if ($type == 'sunrise') {
      $sunrise = gmmktime($hour, $minute, 0, $month, $day, $year);
    }
    else {
      $sunset = gmmktime($hour, $minute, 0, $month, $day, $year);
    }
  }

  // handle special cases like no sunrise / sunset at all
  if ($no_sunset) {
    $condition = 'day';
  }
  else {
    if ($no_sunrise) {
      $condition = 'night';
    }
    else {

      // correctly handle northern and southern hemisphere
      if ($sunrise <= $sunset) {

        // this should be on the northern hemisphere
        if ($metar['reported_on'] >= $sunrise and $metar['reported_on'] < $sunset) {
          $condition = 'day';
        }
        else {
          $condition = 'night';
        }
      }
      else {

        // this should be on the southern hemisphere
        if ($metar['reported_on'] >= $sunrise or $metar['reported_on'] <= $sunset) {
          $condition = 'day';
        }
        else {
          $condition = 'night';
        }
      }
    }
  }
  return $condition;
}

/*********************************************************************
 * Internal functions for custom weather blocks
 ********************************************************************/

/**
 * Show a configuration page for a custom weather block
 */
function weather_custom_block($uid, $cid = 0) {

  // Include the ICAO codes
  require_once drupal_get_path('module', 'weather') . '/icao_codes.inc';

  // Include a nice Javascript which makes the settings easier
  drupal_add_js(drupal_get_path('module', 'weather') . '/helper.js');
  $places_url .= base_path() . drupal_get_path('module', 'weather');

  // if there's no configuration id provided, we get the first in use or 1
  if ($cid == 0) {
    $cid = _weather_get_first_valid_config($uid);
  }

  // if the provided config id is not currently used, we want the lowest
  // free configuration id. This avoid cases like 56271 for config ids
  $config_in_use = _weather_get_configs_in_use($uid);
  $cid_found = FALSE;
  foreach ($config_in_use as $index) {
    if ($index['cid'] == $cid) {
      $cid_found = TRUE;
      break;
    }
  }
  if (!$cid_found) {
    $cid = _weather_get_free_config($uid);
  }

  // get the previously determined configuration
  $config = _weather_get_config($uid, $cid);
  $config['country'] = _weather_get_country_from_icao($config['icao']);
  $config['countries'] = _weather_get_countries();
  $config['places'] = _weather_get_places($config['country']);
  $output = drupal_get_form('weather_custom_block_form', $uid, $cid, $config, $places_url);

  // Add another location
  if ($uid == 0) {
    $url = 'admin/settings/weather/' . _weather_get_free_config($uid);
  }
  else {
    $url = 'user/' . $uid . '/weather/' . _weather_get_free_config($uid);
  }
  $output .= '<p>';
  $output .= l(t('Add another location to the block'), $url);
  $output .= '</p>';
  if (count($config_in_use)) {
    $output .= '<p>';
    $output .= t('Current locations in the block:');
    $output .= '</p>';
    $output .= '<ul>';
    foreach ($config_in_use as $index) {

      // do not add a link to the current configuration
      $output .= '<li>';
      if ($index['cid'] == $cid) {
        $output .= check_plain($index['real_name']);
      }
      else {
        if ($uid == 0) {
          $url = 'admin/settings/weather/' . $index['cid'];
        }
        else {
          $url = 'user/' . $uid . '/weather/' . $index['cid'];
        }
        $output .= l($index['real_name'], $url);
      }
      $output .= '</li>';
    }
    $output .= '</ul>';
  }
  return $output;
}

/**
 * Construct the configuration form for a weather block
 */
function weather_custom_block_form($uid, $cid, $config, $places_url) {

  // set up a selection box with all countries
  $form['country'] = array(
    '#type' => 'select',
    '#title' => t('Country'),
    '#description' => t('Select a country to narrow down your search.'),
    '#default_value' => $config['country'],
    '#options' => drupal_map_assoc($config['countries']),
    '#attributes' => array(
      'onchange' => "update_place('{$places_url}')",
    ),
  );

  // set up a selection box with all place names of the selected country
  $form['icao'] = array(
    '#type' => 'select',
    '#title' => t('Place'),
    '#description' => t('Select a place in that country for the weather display.'),
    '#default_value' => $config['icao'],
    '#options' => $config['places'],
    '#attributes' => array(
      'onchange' => 'update_icao()',
    ),
    // we need to skip the check of valid options, because they get
    // modified with AJAX after a new country has been selected.
    '#DANGEROUS_SKIP_CHECK' => true,
  );
  $form['real_name'] = array(
    '#type' => 'textfield',
    '#title' => t('Real name for the selected place'),
    '#default_value' => $config['real_name'],
    '#description' => t('You may enter another name for the place selected above.'),
    '#required' => true,
    '#size' => '30',
  );
  $form['units'] = array(
    '#type' => 'select',
    '#title' => t('Display units'),
    '#default_value' => $config['units'],
    '#description' => t('Show values of temperature, speed etc. in metric or imperial units.'),
    '#options' => array(
      'metric' => t('metric'),
      'imperial' => t('imperial'),
    ),
  );
  $form['weight'] = array(
    '#type' => 'weight',
    '#title' => t('Weight'),
    '#default_value' => $config['weight'],
    '#description' => t('Optional. In the block, the heavier locations will sink and the lighter locations will be positioned nearer the top. Locations with equal weights are sorted alphabetically.'),
  );
  $form['uid'] = array(
    '#type' => 'value',
    '#value' => $uid,
  );
  $form['cid'] = array(
    '#type' => 'value',
    '#value' => $cid,
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Save configuration'),
  );
  $form['delete'] = array(
    '#type' => 'submit',
    '#value' => t('Delete configuration'),
  );
  return $form;
}

/**
 * Check the submission of the custom weather block
 */
function weather_custom_block_form_validate($form_id, $form_values) {

  // do not validate for deletion action
  if ($form_values['op'] == t('Delete configuration')) {
    return;
  }
  if (_weather_get_country_from_icao($form_values['icao']) == '') {
    form_set_error('icao', t('The ICAO code is not valid.'));
  }
}

/**
 * Handle the submission of the custom weather block
 */
function weather_custom_block_form_submit($form_id, $form_values) {

  // delete the previous entry
  $sql = "DELETE FROM {weather_config} WHERE uid=%d AND cid=%d";
  db_query($sql, $form_values['uid'], $form_values['cid']);
  if ($form_values['op'] == t('Save configuration')) {

    // insert the new configuration values into the DB
    $sql = "INSERT INTO {weather_config}\n      (uid, cid, icao, real_name, units, weight)\n      VALUES(%d, %d, '%s', '%s', '%s', %d)";
    db_query($sql, $form_values['uid'], $form_values['cid'], $form_values['icao'], $form_values['real_name'], $form_values['units'], $form_values['weight']);
    drupal_set_message(t('The configuration options have been saved.'));
  }
  else {
    drupal_set_message(t('The configuration has been deleted.'));
    $form_values['cid'] = _weather_get_first_valid_config($form_values['uid']);
  }
  if ($form_values['uid'] == 0) {

    // go back to the administration of the system weather block
    drupal_goto('admin/settings/weather/' . $form_values['cid']);
  }
  else {
    drupal_goto('user/' . $form_values['uid'] . '/weather/' . $form_values['cid']);
  }
}

/**
 * Return the configuration for the given user id.
 * 
 * If there is no configuration yet, get the default
 * configuration instead. The user is 0 is used for
 * the system weather block.
 */
function _weather_get_config($uid, $cid) {
  $sql = "SELECT * FROM {weather_config} WHERE uid=%d AND cid=%d";
  $result = db_query($sql, $uid, $cid);
  if (db_num_rows($result) != 1) {

    // there was no configuration found. Provide it here.
    $config['icao'] = 'EDDH';
    $config['real_name'] = 'Hamburg-Fuhlsbuettel';
    $config['units'] = 'metric';
  }
  else {

    // get the user's configuration
    $config = db_fetch_array($result);
  }
  return $config;
}

/**
 * Return the first valid configuration or 1 if there is none
 */
function _weather_get_first_valid_config($uid) {
  $sql = 'SELECT cid FROM {weather_config} WHERE uid=%d ORDER BY weight ASC, real_name ASC';
  return max(db_result(db_query($sql, $uid)), 1);
}

/**
 * Return the first unused configuration number
 */
function _weather_get_free_config($uid) {
  $result = db_query("SELECT * FROM {weather_config}\n    WHERE uid=%d\n    ORDER BY cid ASC", $uid);
  $free_config = 1;
  while ($config = db_fetch_array($result)) {
    if ($config['cid'] > $free_config) {
      break;
    }
    $free_config++;
  }
  return $free_config;
}

/**
 * Determine how many configurations exist for the given user id.
 */
function _weather_get_configs_in_use($uid) {
  $configs = array();
  $result = db_query("SELECT * FROM {weather_config}\n    WHERE uid=%d\n    ORDER BY weight ASC, real_name ASC", $uid);
  while ($row = db_fetch_array($result)) {
    $configs[] = array(
      'cid' => $row['cid'],
      'real_name' => $row['real_name'],
    );
  }
  return $configs;
}

/*********************************************************************
 * Internal functions for converting data
 ********************************************************************/

/**
 * Format the weather condition and phenomena (rain, drizzle, ...)
 */
function _weather_format_condition($metar) {
  if (!isset($metar) or !isset($metar['condition_text'])) {
    return t('No data');
  }

  // sky conditions
  switch ($metar['condition_text']) {
    case 'clear':
      $result[] = t('Clear sky');
      break;
    case 'few':
      $result[] = t('Few clouds');
      break;
    case 'scattered':
      $result[] = t('Scattered clouds');
      break;
    case 'broken':
      $result[] = t('Broken clouds');
      break;
    case 'overcast':
      $result[] = t('Overcast');
      break;
    case 'no-significant-clouds':
      $result[] = t('No significant clouds');
      break;
  }

  // weather phenomena
  $rain = $metar['phenomena']['rain'];
  if ($rain) {
    if ($rain['#light']) {
      if ($rain['#showers']) {
        $result[] = t('light rain showers');
      }
      else {
        if ($rain['#freezing']) {
          $result[] = t('light freezing rain');
        }
        else {
          $result[] = t('light rain');
        }
      }
    }
    else {
      if ($rain['#heavy']) {
        if ($rain['#showers']) {
          $result[] = t('heavy rain showers');
        }
        else {
          if ($rain['#freezing']) {
            $result[] = t('heavy freezing rain');
          }
          else {
            $result[] = t('heavy rain');
          }
        }
      }
      else {
        if ($rain['#showers']) {
          $result[] = t('rain showers');
        }
        else {
          if ($rain['#freezing']) {
            $result[] = t('freezing rain');
          }
          else {
            $result[] = t('rain');
          }
        }
      }
    }
  }
  return join(", ", $result);
}

/**
 * Convert temperatures
 * 
 * @param int Temperature, always in degree celsius
 * @param string The unit to be returned (metric, imperial)
 * @return string Formatted representation, either in celsius or fahrenheit
 */
function _weather_format_temperature($temperature, $unit) {
  if (!isset($temperature)) {
    return t('No data');
  }
  if ($unit == 'imperial') {
    return t('!temperature &deg;F', array(
      '!temperature' => $temperature['fahrenheit'],
    ));
  }
  else {

    // default to metric units
    return t('!temperature &deg;C', array(
      '!temperature' => $temperature['celsius'],
    ));
  }
}

/**
 * Convert wind
 * 
 * @param int Wind
 * @param string The unit to be returned (metric, imperial)
 * @return string Formatted representation
 */
function _weather_format_wind($wind, $unit) {
  if (!isset($wind)) {
    return t('No data');
  }

  // shortcut for a special case
  if ($wind['direction'] == 0 and $wind['speed_kmh'] == 0) {
    return t('Calm');
  }

  // translate the wind direction
  $directions = array(
    t('North') => array(
      'start' => -23,
      'end' => 22,
    ),
    t('Northeast') => array(
      'start' => 22,
      'end' => 67,
    ),
    t('East') => array(
      'start' => 67,
      'end' => 112,
    ),
    t('Southeast') => array(
      'start' => 112,
      'end' => 157,
    ),
    t('South') => array(
      'start' => 157,
      'end' => 202,
    ),
    t('Southwest') => array(
      'start' => 202,
      'end' => 247,
    ),
    t('West') => array(
      'start' => 247,
      'end' => 292,
    ),
    t('Northwest') => array(
      'start' => 292,
      'end' => 337,
    ),
  );

  // perform the wrap for North
  if ($wind['direction'] >= 337) {
    $wind['direction'] -= 360;
  }
  if ($wind['variable_start'] >= 337) {
    $wind['variable_start'] -= 360;
  }
  if ($wind['variable_end'] >= 337) {
    $wind['variable_end'] -= 360;
  }
  foreach ($directions as $description => $range) {
    if ($wind['direction'] >= $range['start'] and $wind['direction'] < $range['end']) {
      $wind['direction_text'] = $description;
    }
    if ($wind['variable_start'] >= $range['start'] and $wind['variable_start'] < $range['end']) {
      $wind['variable_start_text'] = $description;
    }
    if ($wind['variable_end'] >= $range['start'] and $wind['variable_end'] < $range['end']) {
      $wind['variable_end_text'] = $description;
    }
  }

  // handle variable wind directions
  if ($wind['direction'] == 'VRB' or isset($wind['variable_start'])) {
    if (isset($wind['variable_start'])) {
      $result[] = t('Variable from !direction_a to !direction_b', array(
        '!direction_a' => $wind['variable_start_text'],
        '!direction_b' => $wind['variable_end_text'],
      ));
    }
    else {
      $result[] = t('Variable');
    }
  }
  else {

    // no variable wind direction, but an exact one
    $result[] = $wind['direction_text'];
  }

  // set up the wind speed
  if ($unit == 'imperial') {
    $result[] = t('!speed mph', array(
      '!speed' => $wind['speed_mph'],
    ));
  }
  else {

    // default to metric units
    $result[] = t('!speed km/h', array(
      '!speed' => $wind['speed_kmh'],
    ));
  }

  // set up gusts, if applicable
  if ($wind['gusts_kmh'] > 0) {
    if ($unit == 'imperial') {
      $result[] = t('gusts up to !speed mph', array(
        '!speed' => $wind['gusts_mph'],
      ));
    }
    else {

      // default to metric units
      $result[] = t('gusts up to !speed km/h', array(
        '!speed' => $wind['gusts_kmh'],
      ));
    }
  }
  return join(', ', $result);
}

/**
 * Convert pressure
 * 
 * The function uses celsius as input
 */
function _weather_format_pressure($pressure, $unit) {
  if (!isset($pressure)) {
    return t('No data');
  }
  if ($unit == 'imperial') {
    return t('!pressure inHg', array(
      '!pressure' => $pressure['inHg'],
    ));
  }
  else {

    // default to metric units
    return t('!pressure hPa', array(
      '!pressure' => $pressure['hPa'],
    ));
  }
}

/**
 * Calculate the relative humidity
 * 
 * The function uses celsius as input
 */
function _weather_format_relative_humidity($temperature, $dewpoint) {
  if (!isset($dewpoint)) {
    return t('No data');
  }
  $e = 6.11 * pow(10, 7.5 * $dewpoint['celsius'] / (237.7 + $dewpoint['celsius']));
  $es = 6.11 * pow(10, 7.5 * $temperature['celsius'] / (237.7 + $temperature['celsius']));
  return max(0, min(100, round(100 * ($e / $es), 0))) . "%";
}

/**
 * Convert the visibility
 */
function _weather_format_visibility($visibility, $unit) {
  if (!isset($visibility['kilometers'])) {
    return t('No data');
  }
  if ($unit == 'imperial') {
    return t('!visibility miles', array(
      '!visibility' => $visibility['miles'],
    ));
  }
  else {

    // default to metric units
    return t('!visibility kilometers', array(
      '!visibility' => $visibility['kilometers'],
    ));
  }
}

/*********************************************************************
 * Internal function for retrieving data
 ********************************************************************/

/**
 * Fetches the latest METAR data from the database or internet
 */
function weather_get_metar($icao) {

  // see if there's a report in the database
  $icao = strtoupper($icao);
  $sql = "SELECT * FROM {weather} WHERE icao='%s'";
  $result = db_query($sql, $icao);
  $data = db_fetch_array($result);

  // if there is no report, initialize the array
  if (!isset($data['metar_raw']) or !isset($data['next_update_on'])) {
    $data['next_update_on'] = 0;
    $data['metar_raw'] = '';
  }

  // if the time has come, download again
  if ($data['next_update_on'] <= time()) {
    $data['metar_raw'] = '';
  }

  // fetch data from the internet
  if ($data['metar_raw'] == '') {
    $data['metar_raw'] = _weather_retrieve_data($icao);
    if ($data['metar_raw']) {
      $metar = weather_parse_metar($data['metar_raw']);
      weather_store_metar($metar);
    }
    else {

      // the internet retrieval has not been successful.
      // try again in 10 minutes
      $sql = "UPDATE {weather} SET next_update_on=%d WHERE icao='%s'";
      db_query($sql, time() + 10 * 60, $icao);
    }
  }
  else {
    $metar = weather_parse_metar($data['metar_raw']);
  }
  return $metar;
}

/**
 * Stores parsed METAR data in the database
 */
function weather_store_metar($metar) {

  // if there's already a record in the database with the same ICAO
  // overwrite it
  $sql = "DELETE FROM {weather} WHERE icao='%s'";
  db_query($sql, $metar['icao']);

  // insert the new data
  $sql = "INSERT INTO {weather}\n    (icao, next_update_on, metar_raw)\n    VALUES ('%s', %d, '%s')";

  // calculate the next scheduled update: normally, we use 62
  // minutes after the reported timestamp, to allow the data
  // to propagate to the server servers.
  $next_update_on = $metar['reported_on'] + 62 * 60;

  // However, if the current time is more than 62 minutes
  // over the reported timestamp, allow ten more minutes
  // to not fetch the data on each page request.
  if ($next_update_on < time()) {
    $next_update_on = time() + 10 * 60;
  }
  db_query($sql, $metar['icao'], $next_update_on, $metar['#raw']);
}

/**
 * Retrieve data from weather.noaa.gov
 */
function _weather_retrieve_data($icao) {
  $icao = strtoupper($icao);
  $metar_raw = FALSE;

  // get information about the last successful data download
  //
  // we want to fetch via FTP, because it's much less bandwidth
  // for downloads. if the FTP access did not work, we use
  // HTTP POST instead.
  $fetch = variable_get('weather_fetch', 'FTP');

  // try alternative methods for fetching the METAR data via FTP
  if ($fetch == 'FTP') {
    $url = 'ftp://tgftp.nws.noaa.gov/data/observations/metar/stations/';
    $url .= $icao . '.TXT';
    if (function_exists('file_get_contents') and ini_get('allow_url_fopen')) {
      $metar_raw = file_get_contents($url);
    }
    else {
      if (function_exists('curl_init')) {
        $curl = curl_init($url);
        curl_setopt($curl, CURLOPT_TRANSFERTEXT, TRUE);
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, TRUE);
        $metar_raw = curl_exec($curl);
        curl_close($curl);
      }
      else {
        if (function_exists('exec')) {
          exec("wget --quiet -O- {$url}", $output, $return_val);
          if ($return_val == 0) {
            $metar_raw = join("\n", $output);
          }
        }
      }
    }

    // if we had success, store the method
    if ($metar_raw !== FALSE) {
      variable_set('weather_fetch', 'FTP');
    }
  }

  // if the FTP access does not work, try POSTing the ICAO via HTTP
  if ($metar_raw === FALSE) {
    if (function_exists('fsockopen')) {
      $handle = fsockopen('weather.noaa.gov', 80);
      if ($handle !== FALSE) {
        $request = "POST /cgi-bin/mgetmetar.pl HTTP/1.1\r\n";
        $request .= "Host: weather.noaa.gov\r\n";
        $request .= "Content-Type: application/x-www-form-urlencoded\r\n";
        $request .= "Content-Length: 9\r\n\r\n";
        $request .= "cccc={$icao}\r\n";
        fwrite($handle, $request);
        while (!feof($handle)) {
          $response .= fgets($handle, 1024);
        }
        fclose($handle);

        // extract the valid METAR data from the received webpage
        if (preg_match("/({$icao} [0-9]{6}Z .+)/", $response, $matches)) {
          $metar_raw = $matches[1];

          // store the method
          variable_set('weather_fetch', 'HTTP');
        }
      }
    }
  }

  // check on errors
  if ($metar_raw === FALSE) {
    drupal_set_message(t('Download location for METAR data is not accessible.'), 'error');
  }
  return $metar_raw;
}

Functions

Namesort descending Description
theme_weather Custom theme function for the weather block output
weather_block Generate HTML for the weather block
weather_custom_block Show a configuration page for a custom weather block
weather_custom_block_form Construct the configuration form for a weather block
weather_custom_block_form_submit Handle the submission of the custom weather block
weather_custom_block_form_validate Check the submission of the custom weather block
weather_get_metar Fetches the latest METAR data from the database or internet
weather_menu Implementation of hook_menu().
weather_perm Implementation of hook_perm().
weather_store_metar Stores parsed METAR data in the database
_weather_format_condition Format the weather condition and phenomena (rain, drizzle, ...)
_weather_format_pressure Convert pressure
_weather_format_relative_humidity Calculate the relative humidity
_weather_format_temperature Convert temperatures
_weather_format_visibility Convert the visibility
_weather_format_wind Convert wind
_weather_get_config Return the configuration for the given user id.
_weather_get_configs_in_use Determine how many configurations exist for the given user id.
_weather_get_first_valid_config Return the first valid configuration or 1 if there is none
_weather_get_free_config Return the first unused configuration number
_weather_get_image
_weather_retrieve_data Retrieve data from weather.noaa.gov
_weather_sunrise_sunset Internal helper function to calculate the sunrise and sunset times for a given location