You are here

weather.module in Weather 6.5

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

The module is compatible with Drupal 6.x

@author Tobias Quathamer

File

weather.module
View source
<?php

/*
 *
 * Copyright © 2006-2012 Tobias Quathamer <t.quathamer@gmx.net>
 *
 * This file is part of the Drupal Weather module.
 *
 * It was inspired by 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 6.x
 *
 * @author Tobias Quathamer
 */

// Define the start of block deltas for system-wide blocks
define('SYSTEM_BLOCK_DELTA_START', 3);

// 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',
    'access weather pages',
  );
}

/**
 * Implementation of hook_menu().
 */
function weather_menu() {
  $items['admin/settings/weather'] = array(
    'title' => 'Weather',
    'description' => 'Configure system-wide weather blocks and the default configuration for new locations.',
    'page callback' => 'weather_admin_main_page',
    'access arguments' => array(
      'administer site configuration',
    ),
    'type' => MENU_NORMAL_ITEM,
  );
  $items['admin/settings/weather/edit/%/%'] = array(
    'title' => 'Edit location',
    'description' => 'Configure a system-wide weather block.',
    'page callback' => 'weather_custom_block',
    'page arguments' => array(
      4,
      5,
    ),
    'access arguments' => array(
      'administer site configuration',
    ),
    'type' => MENU_CALLBACK,
  );
  $items['admin/settings/weather/delete/%/%'] = array(
    'title' => 'Delete location',
    'description' => 'Delete a location from a system-wide weather block.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'weather_custom_block_delete_confirm',
      4,
      5,
    ),
    'access arguments' => array(
      'administer site configuration',
    ),
    'type' => MENU_CALLBACK,
  );
  $items['admin/settings/weather/default'] = array(
    'title' => 'Default configuration',
    'description' => 'Setup the default configuration for new locations.',
    'page callback' => 'weather_custom_block',
    'page arguments' => array(
      '0',
    ),
    'access arguments' => array(
      'administer site configuration',
    ),
    'type' => MENU_CALLBACK,
  );
  $items['user/%/weather'] = array(
    'title' => 'My weather',
    'description' => 'Configure your custom weather block.',
    'page callback' => 'weather_user_main_page',
    'page arguments' => array(
      1,
    ),
    'access callback' => 'weather_custom_block_access',
    'access arguments' => array(
      1,
    ),
    'type' => MENU_LOCAL_TASK,
  );
  $items['user/%/weather/edit/%'] = array(
    'title' => 'Edit location',
    'description' => 'Configure your custom weather block.',
    'page callback' => 'weather_custom_block',
    'page arguments' => array(
      1,
      4,
    ),
    'access callback' => 'weather_custom_block_access',
    'access arguments' => array(
      1,
    ),
    'type' => MENU_CALLBACK,
  );
  $items['user/%/weather/delete/%'] = array(
    'title' => 'Delete location',
    'description' => 'Delete a location from your custom weather block.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'weather_custom_block_delete_confirm',
      1,
      4,
    ),
    'access callback' => 'weather_custom_block_access',
    'access arguments' => array(
      1,
    ),
    'type' => MENU_CALLBACK,
  );
  $items['weather/js'] = array(
    'page callback' => 'weather_js',
    'access arguments' => array(
      'access content',
    ),
    'type' => MENU_CALLBACK,
  );
  $items['weather'] = array(
    'title' => 'Weather',
    'description' => 'Search for locations and display their current weather.',
    'page callback' => 'weather_search_location',
    'access arguments' => array(
      'access weather pages',
    ),
    'type' => MENU_NORMAL_ITEM,
  );
  $items['weather/autocomplete'] = array(
    'page callback' => 'weather_search_autocomplete',
    'access arguments' => array(
      'access weather pages',
    ),
    'type' => MENU_CALLBACK,
  );
  return $items;
}

/**
 * Implementation of hook_help().
 */
function weather_help($path, $arg) {
  $output = '';
  switch ($path) {
    case 'admin/settings/weather':
      $output .= '<p>';
      $output .= t('You can add, edit, and delete locations from system-wide weather blocks. Moreover, you can specify default values for newly created locations.');
      $output .= '</p>';
      break;
    case 'user/%/weather':
      $output = '<p>';
      $output .= t('You can add, edit, and delete locations from your custom weather block.');
      $output .= "\n";
      $output .= t('Please note that the block will not be shown until you configure at least one location.');
      $output .= '</p>';
      break;
  }
  return $output;
}

/*********************************************************************
 * General Drupal hooks for maintenance tasks
 ********************************************************************/

/**
 * Implementation of hook_cron().
 *
 * If the site uses caching for anonymous users, the cached weather
 * blocks are not updated until the page cache is flushed.
 * We rely on cron to perform necessary updates (and flushing the cache)
 * for anonymous users. In order to not clear the cache for user-defined
 * weather blocks (which are not shown to anonymous users), we only
 * check for system weather blocks.
 */
function weather_cron() {
  if (variable_get('weather_use_cron', FALSE)) {
    $sql = "SELECT * FROM {weather} LEFT JOIN {weather_config}\n      ON {weather}.icao={weather_config}.icao\n      WHERE {weather_config}.uid < 0 ORDER BY next_update_on ASC";
    $result = db_query($sql);
    $row = db_fetch_array($result);
    if (isset($row['next_update_on'])) {
      if ($row['next_update_on'] <= time()) {
        cache_clear_all();
      }
    }
  }
}

/*********************************************************************
 * 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: custom user');
    $block[1]['info'] = t('Weather: location of nodes (requires Location or Node Map module)');
    $current_blocks = _weather_get_blocks_in_use();
    if (!empty($current_blocks)) {
      foreach ($current_blocks as $block_id) {

        // $block_id is at least 1, so make sure the delta is at least 2
        $block[SYSTEM_BLOCK_DELTA_START + $block_id - 1]['info'] = t('Weather: system-wide !number', array(
          '!number' => $block_id,
        ));
      }
    }
    return $block;
  }
  else {
    if ($op == 'view') {
      if ($delta == 0 and weather_custom_block_access()) {

        // Show the user's custom weather block, if there is already
        // a location configured. Otherwise, do not show the block.
        $configs_in_use = _weather_get_configs_in_use($user->uid);
        if (count($configs_in_use) == 0) {
          return;
        }
        $block['subject'] = t('Current weather');
        $block['content'] = '';
        foreach ($configs_in_use as $index) {
          $config = _weather_get_config($user->uid, $index['cid']);
          $metar = weather_get_metar($config['icao']);
          $block['content'] .= theme('weather_theming', $config, $metar);
        }
        return $block;
      }
      else {
        if ($delta == 1 and user_access('access content')) {

          // show the node location weather block
          if (arg(0) == 'node' and is_numeric(arg(1))) {
            $node = node_load(arg(1));
            $block['content'] = '';

            // This checks the location module
            if (isset($node->locations)) {

              // Iterate through all available locations and check
              // for lat/long information. If there is no information,
              // the location module return 0.0/0.0 instead of NULL values
              foreach ($node->locations as $location) {
                if ($location['latitude'] != 0 or $location['longitude'] != 0) {
                  $nearest_station = weather_get_icao_from_lat_lon($location['latitude'], $location['longitude']);
                  $config = _weather_get_config(0, 1);
                  $config = array_merge($config, $nearest_station);
                  $config['real_name'] = $config['name'];
                  $metar = weather_get_metar($config['icao']);
                  $block['content'] .= theme('weather_theming', $config, $metar);
                }
              }
            }

            // Handle CCK location fields
            // First, determine all names of location fields
            $sql = "SELECT field_name FROM {content_node_field} WHERE type='location'";
            $result = db_query($sql);

            // Cycle through all found field names.
            while ($field_name = db_result($result)) {

              // Iterate through all available locations and check
              // for lat/long information. If there is no information,
              // the location module return 0.0/0.0 instead of NULL values
              foreach ($node->{$field_name} as $location) {
                if ($location['latitude'] != 0 or $location['longitude'] != 0) {
                  $nearest_station = weather_get_icao_from_lat_lon($location['latitude'], $location['longitude']);
                  $config = _weather_get_config(0, 1);
                  $config = array_merge($config, $nearest_station);
                  $config['real_name'] = $config['name'];
                  $metar = weather_get_metar($config['icao']);
                  $block['content'] .= theme('weather_theming', $config, $metar);
                }
              }
            }

            // Handle nodemap module
            if (isset($node->nodemap_latitude_field) and isset($node->nodemap_longitude_field) and ($node->nodemap_latitude_field != 0 or $node->nodemap_longitude_field != 0)) {
              $nearest_station = weather_get_icao_from_lat_lon($node->nodemap_latitude_field, $node->nodemap_longitude_field);
              $config = _weather_get_config(0, 1);
              $config = array_merge($config, $nearest_station);
              $config['real_name'] = $config['name'];
              $metar = weather_get_metar($config['icao']);
              $block['content'] .= theme('weather_theming', $config, $metar);
            }

            // Do not show block if no lat/long information has been found
            if ($block['content'] != '') {
              $block['subject'] = t('Current weather nearby');
              return $block;
            }
          }
        }
        else {
          if ($delta >= SYSTEM_BLOCK_DELTA_START and user_access('access content')) {

            // show a system-wide weather block
            $system_block_id = SYSTEM_BLOCK_DELTA_START - $delta - 1;
            $block['subject'] = t('Current weather');
            $block['content'] = '';
            $configs_in_use = _weather_get_configs_in_use($system_block_id);
            if (count($configs_in_use) == 0) {
              $configs_in_use[] = array(
                'cid' => 1,
              );
            }
            foreach ($configs_in_use as $index) {
              $config = _weather_get_config($system_block_id, $index['cid']);
              $metar = weather_get_metar($config['icao']);
              $block['content'] .= theme('weather_theming', $config, $metar);
            }
            return $block;
          }
        }
      }
    }
  }
}

/**
 * Check whether the user has access to their own custom weather block
 */
function weather_custom_block_access($uid = NULL) {
  global $user;

  // If $uid is not set, just check for the access permission
  if (is_null($uid) || $user->uid == $uid) {
    return user_access('administer custom weather block');
  }
  return FALSE;
}

/**
 * Implementation of hook_theme().
 */
function weather_theme() {
  return array(
    // Custom theme function for preprocessing variables
    'weather_theming' => array(
      'arguments' => array(
        'config' => NULL,
        'metar' => NULL,
      ),
    ),
    // Default block layout
    'weather' => array(
      'template' => 'weather',
      'arguments' => array(
        'weather' => NULL,
      ),
    ),
    // Compact block layout
    'weather_compact' => array(
      'template' => 'weather_compact',
      'arguments' => array(
        'weather' => NULL,
      ),
    ),
  );
}

/**
 * Custom theme function for preprocessing the weather block output
 */
function theme_weather_theming($config, $metar) {

  // Set up variables which might be needed in the templates
  $weather['real_name'] = check_plain($config['real_name']);
  $weather['condition'] = _weather_format_condition($metar);
  $weather['image'] = _weather_get_image($metar);
  if (isset($metar['temperature']) and $config['units']['temperature'] != 'dont-display') {
    $weather = array_merge($weather, _weather_format_temperature($metar['temperature'], $metar['wind'], $config['units'], $config['settings']));
  }
  if (isset($metar['wind']) and $config['units']['windspeed'] != 'dont-display') {
    $weather['wind'] = _weather_format_wind($metar['wind'], $config['units'], $config['settings']);
  }
  if (isset($metar['pressure']) and $config['units']['pressure'] != 'dont-display') {
    $weather['pressure'] = _weather_format_pressure($metar['pressure'], $config['units']);
  }
  if (isset($metar['temperature']) and isset($metar['dewpoint']) and $config['units']['humidity'] != 'dont-display') {
    $weather['rel_humidity'] = _weather_format_relative_humidity($metar['temperature'], $metar['dewpoint']);
  }
  if (isset($metar['visibility']['kilometers']) and $config['units']['visibility'] != 'dont-display') {
    $weather['visibility'] = _weather_format_visibility($metar['visibility'], $config['units']);
  }
  if ($config['settings']['show_sunrise_sunset']) {

    // Check if there is a sunrise or sunset
    if ($metar['daytime']['no_sunrise']) {
      $weather['sunrise'] = t('No sunrise today');
    }
    else {
      if ($metar['daytime']['no_sunset']) {
        $weather['sunset'] = t('No sunset today');
      }
      else {

        // Set up timezone with a sensible default value
        $timezone = $config['settings']['sunrise_sunset_timezone'];

        // If the timezone is numeric, just use that value
        if (!is_numeric($timezone)) {
          if ($timezone == 'drupal') {

            // Use Drupal's default timezone or user's timezone
            $timezone = NULL;
          }
          else {

            // Fall back to using GMT
            $timezone = 0;
          }
        }

        // Try to extract a time format from the system wide date format
        $date_format_short = variable_get('date_format_short', 'm/d/Y - H:i');
        preg_match("/[GgHh].*?i(.*?[Aa])?/", $date_format_short, $matches);
        if (isset($matches[0])) {
          $format = $matches[0];
        }
        else {
          $format = 'G:i';
        }

        // If the selected timezone is "drupal", just show the time.
        // Otherwise, we append either "GMT" or the current GMT offset.
        if (is_null($timezone)) {

          // This is Drupal's default timezone, so do nothing.
        }
        else {
          if ($timezone == 0) {

            // This is GMT
            $format .= ' T';
          }
          else {

            // Append the GMT offset
            $format .= ' O';
          }
        }
        $text = format_date($metar['daytime']['sunrise_on'], 'custom', $format, $timezone);
        $weather['sunrise'] = t('Sunrise: !sunrise', array(
          '!sunrise' => $text,
        ));
        $text = format_date($metar['daytime']['sunset_on'], 'custom', $format, $timezone);
        $weather['sunset'] = t('Sunset: !sunset', array(
          '!sunset' => $text,
        ));
      }
    }
  }
  if (isset($metar['#raw']) and $config['settings']['show_unconverted_metar']) {
    $weather['metar'] = $metar['#raw'];
  }

  // If this is displayed as location block, show information about
  // which METAR station has been used for weather data
  if (isset($config['distance'])) {
    $weather['location'] = _weather_format_closest_station($config['distance'], $config['units'], $config['settings']);
  }
  if (isset($metar['reported_on'])) {
    $weather['reported_on'] = format_date($metar['reported_on']);
  }

  // Use compact block, if desired
  if ($config['settings']['show_compact_block']) {
    return theme('weather_compact', $weather);
  }
  else {
    return theme('weather', $weather);
  }
}
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
    $name = $metar['daytime']['condition'] . '-' . $metar['condition_text'];

    // Use fog image, if needed
    if (isset($metar['phenomena']['#mist']) or isset($metar['phenomena']['fog']) or isset($metar['phenomena']['#smoke'])) {
      $name .= '-fog';
    }

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

  // Support a custom image directory. If the variable is not set or the specified
  // file is not available, fall back to the default images of the module.
  $path = variable_get('weather_image_directory', '');
  $filename = file_directory_path() . '/' . $path . '/' . $name . '.png';
  if (!is_readable($filename)) {
    $filename = drupal_get_path('module', 'weather') . '/images/' . $name . '.png';
  }

  // Set up final return array
  $image['filename'] = base_path() . $filename;
  $size = getimagesize($filename);
  $image['size'] = $size[3];
  return $image;
}

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

/**
 * Show an overview of configured locations
 */
function weather_user_main_page($uid) {
  $header = array(
    t('Real name'),
    t('Weight'),
    array(
      'data' => t('Operations'),
      'colspan' => 2,
    ),
  );
  $path = 'user/' . $uid . '/weather/';
  $rows = array();
  $sql = "SELECT * FROM {weather_config}\n    WHERE uid=%d ORDER BY weight ASC, real_name ASC";
  $result = db_query($sql, $uid);
  while ($row = db_fetch_array($result)) {
    $rows[] = array(
      $row['real_name'],
      $row['weight'],
      l(t('edit'), $path . 'edit/' . $row['cid']),
      l(t('delete'), $path . 'delete/' . $row['cid']),
    );
  }
  if (count($rows) == 0) {
    $rows[] = array(
      array(
        'data' => '<em>' . t('There are currently no locations.') . '</em>',
        'colspan' => 4,
      ),
    );
  }
  $output = theme('table', $header, $rows);
  if (isset($form['pager']['#value'])) {
    $output .= drupal_render($form['pager']);
  }
  $free_cid = _weather_get_free_config($uid);
  $output .= '<p>' . l(t('Create new location'), $path . 'edit/' . $free_cid) . '</p>';
  return $output;
}

/**
 * Show an overview of configured locations and the default location
 */
function weather_admin_main_page() {
  $output = '';
  $blocks = _weather_get_blocks_in_use();
  $path = 'admin/settings/weather/';
  if (!empty($blocks)) {
    foreach ($blocks as $block_id) {
      $header = array(
        t('System-wide block !number', array(
          '!number' => $block_id,
        )),
        t('Weight'),
        array(
          'data' => t('Operations'),
          'colspan' => 2,
        ),
      );
      $rows = array();
      $sql = "SELECT * FROM {weather_config}\n        WHERE uid=%d ORDER BY weight ASC, real_name ASC";
      $result = db_query($sql, -$block_id);
      while ($row = db_fetch_array($result)) {
        $rows[] = array(
          $row['real_name'],
          $row['weight'],
          l(t('edit'), $path . 'edit/' . -$block_id . '/' . $row['cid']),
          l(t('delete'), $path . 'delete/' . -$block_id . '/' . $row['cid']),
        );
      }
      $output .= theme('table', $header, $rows);
      if (isset($form['pager']['#value'])) {
        $output .= drupal_render($form['pager']);
      }
      $free_cid = _weather_get_free_config(-$block_id);
      $output .= '<p>' . l(t('Create new location in block !number', array(
        '!number' => $block_id,
      )), $path . 'edit/' . -$block_id . '/' . $free_cid) . '</p>';
    }
  }

  // Allow creation of another system-wide block
  $free_block_id = _weather_get_free_block_id();
  $output .= '<p>' . l(t('Create new system-wide block'), $path . 'edit/' . $free_block_id . '/1') . '</p>';

  // Add the default location
  $output .= '<p>' . l(t('Configure the default location'), $path . 'default') . '</p>';
  $output .= drupal_get_form('weather_main_page_form');
  return $output;
}

/**
 * Construct a form for general settings of the Weather module
 */
function weather_main_page_form() {
  $form['use_cron'] = array(
    '#type' => 'checkbox',
    '#title' => t('Use cron to clear the cache once per hour'),
    '#description' => t('If you use Drupal\'s cache, the system weather blocks will not be updated for anonymous users unless the cache is cleared. This happens e.g. when new nodes are created. If you want the system weather blocks to be updated when new weather data is available, you can clear the cache once per hour. Please note that this might slow down your site.'),
    '#default_value' => variable_get('weather_use_cron', FALSE),
  );
  $form['weather_image_directory'] = array(
    '#type' => 'textfield',
    '#title' => t('Directory for custom images'),
    '#description' => t('Override the default image directory. This directory must be a subdirectory of the Drupal \'files\' path.'),
    '#default_value' => variable_get('weather_image_directory', ''),
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Save configuration'),
  );
  return $form;
}

/**
 * Handle the submission for general settings of the Weather module
 */
function weather_main_page_form_submit($form, &$form_state) {
  variable_set('weather_use_cron', $form_state['values']['use_cron']);
  $directory = $form_state['values']['weather_image_directory'];

  // Remove whitespace and directory separators from the string.
  $directory = trim(trim($directory, '/\\'));

  // Replace Windows-style directory separators with Unix separators
  $directory = implode('/', explode('\\', $directory));
  variable_set('weather_image_directory', $directory);
  drupal_set_message(t('The configuration has been saved.'));
}

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

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

  // 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);
  return $output;
}

/**
 * Return a new place selection box based on the country selection
 */
function weather_js() {

  // Get the current selected country for the new places
  $form_state = array(
    'values' => $_POST,
  );
  $new_places = weather_get_places($form_state['values']['country']);

  // Get form from cache and store modified place selection
  $form = form_get_cache($_POST['form_build_id'], $form_state);
  $form['place'] = array(
    '#type' => 'select',
    '#title' => t('Place'),
    '#description' => t('Select a place in that country for the weather display.'),
    '#options' => $new_places,
  );
  form_set_cache($_POST['form_build_id'], $form, $form_state);
  $form += array(
    '#post' => $_POST,
    '#programmed' => FALSE,
  );

  // Rebuild the form.
  $form_state = array(
    'submitted' => FALSE,
  );
  $form = form_builder('weather_custom_block_form', $form, $form_state);

  // Render the new output.
  $output = theme('status_messages') . drupal_render($form['place']);

  // Don't call drupal_json(). ahah.js uses an iframe and
  // the header output by drupal_json() causes problems in some browsers.
  print drupal_to_js(array(
    'status' => TRUE,
    'data' => $output,
  ));
  exit;
}

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

  // 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']),
    '#ahah' => array(
      'path' => 'weather/js',
      'wrapper' => 'edit-place-wrapper',
    ),
  );

  // set up a selection box with all place names of the selected country
  $form['place'] = 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'],
  );
  $form['icao'] = array(
    '#type' => 'textfield',
    '#title' => t('ICAO code'),
    '#default_value' => $config['icao'],
    '#description' => t('Enter the 4-letter ICAO code of the weather station. If you first need to look up the code, you can use !url_1 or !url_2. Please note that not all stations listed at those URLs are providing weather data and thus may not be supported by this module.', array(
      '!url_1' => l('airlinecodes.co.uk', 'http://www.airlinecodes.co.uk/aptcodesearch.asp'),
      '!url_2' => l('notams.jcs.mil', 'https://www.notams.jcs.mil/common/icao/index.html'),
    )),
    '#required' => true,
    '#size' => '5',
  );
  $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' => 'fieldset',
    '#title' => t('Display units'),
    '#description' => t('You can specify which units should be used for displaying the data.'),
    '#collapsible' => TRUE,
    '#collapsed' => FALSE,
    '#tree' => TRUE,
  );
  $form['units']['temperature'] = array(
    '#type' => 'select',
    '#title' => t('Temperature'),
    '#default_value' => $config['units']['temperature'],
    '#options' => array(
      'celsius' => t('Celsius'),
      'fahrenheit' => t('Fahrenheit'),
      'celsiusfahrenheit' => t('Celsius / Fahrenheit'),
      'fahrenheitcelsius' => t('Fahrenheit / Celsius'),
      'dont-display' => t('Don\'t display'),
    ),
  );
  $form['units']['windspeed'] = array(
    '#type' => 'select',
    '#title' => t('Wind speed'),
    '#default_value' => $config['units']['windspeed'],
    '#options' => array(
      'kmh' => t('km/h'),
      'mph' => t('mph'),
      'knots' => t('Knots'),
      'mps' => t('meter/s'),
      'beaufort' => t('Beaufort'),
      'dont-display' => t('Don\'t display'),
    ),
  );
  $form['units']['pressure'] = array(
    '#type' => 'select',
    '#title' => t('Pressure'),
    '#default_value' => $config['units']['pressure'],
    '#options' => array(
      'hpa' => t('hPa'),
      'kpa' => t('kPa'),
      'inhg' => t('inHg'),
      'mmhg' => t('mmHg'),
      'dont-display' => t('Don\'t display'),
    ),
  );
  $form['units']['humidity'] = array(
    '#type' => 'select',
    '#title' => t('Rel. Humidity'),
    '#default_value' => $config['units']['humidity'],
    '#options' => array(
      'display' => t('Display'),
      'dont-display' => t('Don\'t display'),
    ),
  );
  $form['units']['visibility'] = array(
    '#type' => 'select',
    '#title' => t('Visibility'),
    '#default_value' => $config['units']['visibility'],
    '#options' => array(
      'kilometers' => t('kilometers'),
      'miles' => t('UK miles'),
      'dont-display' => t('Don\'t display'),
    ),
  );
  $form['settings'] = array(
    '#type' => 'fieldset',
    '#title' => t('Display settings'),
    '#description' => t('You can customize the display of the block.'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
    '#tree' => TRUE,
  );
  $form['settings']['show_windchill'] = array(
    '#type' => 'checkbox',
    '#title' => t('Show windchill temperature'),
    '#default_value' => $config['settings']['show_windchill'],
    '#description' => t('Calculates the temperature resulting from windchill. This is how the temperature <q>feels like</q>.'),
  );
  $form['settings']['show_unconverted_metar'] = array(
    '#type' => 'checkbox',
    '#title' => t('Show unconverted METAR data'),
    '#default_value' => $config['settings']['show_unconverted_metar'],
    '#description' => t('Displays the original data of the METAR report.'),
  );
  $form['settings']['show_abbreviated_directions'] = array(
    '#type' => 'checkbox',
    '#title' => t('Show abbreviated wind directions'),
    '#default_value' => $config['settings']['show_abbreviated_directions'],
    '#description' => t('Displays abbreviated wind directions like N, SE, or W instead of North, Southeast, or West.'),
  );
  $form['settings']['show_directions_degree'] = array(
    '#type' => 'checkbox',
    '#title' => t('Show degrees of wind directions'),
    '#default_value' => $config['settings']['show_directions_degree'],
    '#description' => t('Displays the degrees of wind directions, e.g. North (20°).'),
  );
  $form['settings']['show_sunrise_sunset'] = array(
    '#type' => 'checkbox',
    '#title' => t('Show time of sunrise and sunset'),
    '#default_value' => $config['settings']['show_sunrise_sunset'],
    '#description' => t('Displays the time of sunrise and sunset.'),
  );

  // Construct timezones
  $timezones = array(
    'gmt' => t('GMT'),
    'drupal' => t('Drupal'),
  );
  $system_timezones = _system_zonelist();
  foreach ($system_timezones as $seconds_offset => $date) {
    $timezones[$seconds_offset] = sprintf("%+03d:%02d", $seconds_offset / 3600, abs($seconds_offset % 3600) / 60);
  }
  $form['settings']['sunrise_sunset_timezone'] = array(
    '#type' => 'select',
    '#title' => t('Timezone for sunrise and sunset'),
    '#default_value' => $config['settings']['sunrise_sunset_timezone'],
    '#description' => t('Choose either Greenwich Mean Time (GMT), Drupal\'s standard timezone as set in the configuration, or a custom timezone.'),
    '#options' => $timezones,
  );
  $form['settings']['show_compact_block'] = array(
    '#type' => 'checkbox',
    '#title' => t('Show compact block'),
    '#default_value' => $config['settings']['show_compact_block'],
    '#description' => t('Displays only the name, condition, and temperature of the weather station.'),
  );
  $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'),
  );
  return $form;
}

/**
 * Check the submission of the custom weather block
 */
function weather_custom_block_form_validate($form, &$form_state) {
  if (weather_get_country_from_icao($form_state['values']['icao']) == '') {
    form_set_error('icao', t('The ICAO code is not supported by this module.'));
  }
}

/**
 * Handle the submission of the custom weather block
 */
function weather_custom_block_form_submit($form, &$form_state) {

  // delete the previous entry
  $sql = "DELETE FROM {weather_config} WHERE uid=%d AND cid=%d";
  db_query($sql, $form_state['values']['uid'], $form_state['values']['cid']);

  // insert the new configuration values into the DB
  $sql = "INSERT INTO {weather_config}\n    (uid, cid, icao, real_name, units, settings, weight)\n    VALUES(%d, %d, '%s', '%s', '%s', '%s', %d)";
  db_query($sql, $form_state['values']['uid'], $form_state['values']['cid'], strtoupper($form_state['values']['icao']), $form_state['values']['real_name'], serialize($form_state['values']['units']), serialize($form_state['values']['settings']), $form_state['values']['weight']);
  if ($form_state['values']['uid'] == 0) {
    drupal_set_message(t('The default configuration has been saved.'));
  }
  else {
    drupal_set_message(t('The location has been saved.'));
  }
  if ($form_state['values']['uid'] <= 0) {

    // go back to the administration of the system weather block,
    // if this is the default configuration or a system-wide block
    $form_state['redirect'] = 'admin/settings/weather';

    /** TODO
     * Rehashing is not needed on every submission, only if the block
     * is newly created. On the other hand, this happens only
     * rarely and surely is not a performance bottleneck.
     */
    _block_rehash();
  }
  else {
    $form_state['redirect'] = 'user/' . $form_state['values']['uid'] . '/weather';
  }
}

/**
 * Confirmation page before deleting a location
 */
function weather_custom_block_delete_confirm($form, $uid, $cid) {
  if ($uid < 0) {
    $abort_path = 'admin/settings/weather';
  }
  else {
    $abort_path = 'user/' . $uid . '/weather';
  }
  $sql = "SELECT * FROM {weather_config} WHERE uid=%d and cid=%d";
  $result = db_query($sql, $uid, $cid);
  $row = db_fetch_array($result);
  $form = array();
  $form['uid'] = array(
    '#type' => 'hidden',
    '#value' => $uid,
  );
  $form['cid'] = array(
    '#type' => 'hidden',
    '#value' => $cid,
  );
  return confirm_form($form, t('Are you sure you want to delete the location %name?', array(
    '%name' => $row['real_name'],
  )), $abort_path, t('This action cannot be undone.'), t('Delete'), t('Cancel'));
}

/**
 * Handle the deletion of a location
 */
function weather_custom_block_delete_confirm_submit($form, &$form_state) {

  // delete the entry
  $sql = "DELETE FROM {weather_config} WHERE uid=%d AND cid=%d";
  db_query($sql, $form_state['values']['uid'], $form_state['values']['cid']);
  drupal_set_message(t('The location has been deleted.'));
  if ($form_state['values']['uid'] < 0) {

    // go back to the administration of system-wide weather blocks
    $form_state['redirect'] = 'admin/settings/weather';

    /** TODO
     * Rehashing is not needed on every submission, only if the block
     * is totally empty. On the other hand, this happens only
     * rarely and surely is not a performance bottleneck.
     */
    _block_rehash();
  }
  else {
    $form_state['redirect'] = 'user/' . $form_state['values']['uid'] . '/weather';
  }
}

/**
 * Searches for the specified location, whether it is a place name or
 * an ICAO code. For example, weather/fuhlsbüttel will display the weather
 * for Hamburg-Fuhlsbüttel.
 *
 * @param string The argument passed in the URL that specifies the
 *               location which should be searched for.
 */
function weather_search_location($search = NULL) {
  if ($search == NULL) {

    // The user did not enter a search string in the URL, so just
    // display the search form.
    return drupal_get_form('weather_search_form');
  }
  else {
    $search = urldecode($search);

    // Do some sanity checks first
    if (strlen($search) < 3 || strlen($search) > 64) {
      drupal_set_message(t('The string to search for must be between 3 and 64 characters.'), 'error');
      drupal_goto('weather');
    }

    // Try to match the ICAO code
    if (strlen($search) == 4) {
      $sql = "SELECT icao, country, name FROM {weather_icao} WHERE icao = '%s'";
      $result = db_query($sql, strtoupper($search));
      if ($location = db_fetch_object($result)) {

        // Use the default configuration for display
        $config = _weather_get_config(0, 0);
        $config['icao'] = $location->icao;
        $config['real_name'] = $location->name;
        $metar = weather_get_metar($location->icao);
        $output = theme('weather_theming', $config, $metar);
        $output .= drupal_get_form('weather_search_form');
        return $output;
      }
    }

    // Try to match on icao, name, or country
    $locations = array();
    $sql = "SELECT icao, country, name FROM {weather_icao}\n      WHERE icao LIKE UPPER('%%%s%%')\n      OR UPPER(country) LIKE UPPER('%%%s%%')\n      OR UPPER(name) LIKE UPPER('%%%s%%')\n      ORDER BY name ASC";
    $result = db_query($sql, $search, $search, $search);
    while ($location = db_fetch_object($result)) {
      $locations[] = $location;
    }

    // If there are no results, notify user
    if (empty($locations)) {
      drupal_set_message(t('Your search did not return any results.'), 'error');
      drupal_goto('weather');
    }
    else {
      if (count($locations) == 1) {
        $location = $locations[0];

        // There's only one search result, so show the weather directly
        // using the default configuration for display
        $config = _weather_get_config(0, 0);
        $config['icao'] = $location->icao;
        $config['real_name'] = $location->name;
        $metar = weather_get_metar($location->icao);
        $output = theme('weather_theming', $config, $metar);
        $output .= drupal_get_form('weather_search_form');
        return $output;
      }
      else {

        // There is more than one result, so show all of them
        // to let the user decide
        $links = array();
        foreach ($locations as $location) {
          $links[] = l($location->name, 'weather/' . $location->icao);
        }
        $title = t('Search results for <q>@search</q>', array(
          '@search' => $search,
        ));
        $output = theme('item_list', $links, $title);
        $output .= drupal_get_form('weather_search_form');
        return $output;
      }
    }
  }
}

/**
 * Display a form for the user to search for weather locations.
 */
function weather_search_form() {
  $form = array();
  $form['search'] = array(
    '#type' => 'textfield',
    '#title' => t('Search for a location'),
    '#description' => t('Type in an ICAO code, a name, or a country to search for weather conditions at that location.'),
    '#autocomplete_path' => 'weather/autocomplete',
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Search'),
  );
  return $form;
}

/**
 * Validate the input from the weather search form
 */
function weather_search_form_validate($form, &$form_state) {
  if (strlen($form_state['values']['search']) < 3 || strlen($form_state['values']['search']) > 64) {
    form_set_error('search', t('The string to search for must be between 3 and 64 characters.'));
  }
}

/**
 * Submission handler for the weather search form.
 *
 * Just redirect the user to the weather URL with the search term stuffed
 * on the end of it. We've been through validation but make sure the
 * search contains no dodgy characters here.
 */
function weather_search_form_submit($form, &$form_state) {
  drupal_goto('weather/' . urlencode($form_state['values']['search']));
}

/**
 * Given a partial string, search for a location or ICAO code matching that
 * string.
 *
 * @param string $input The partial text to search for.
 */
function weather_search_autocomplete($input) {
  $matches = array();

  // In this query we search for ICAO code, country, and name of locations.
  $sql = "SELECT icao, country, name FROM {weather_icao}\n    WHERE icao LIKE UPPER('%%%s%%')\n    OR UPPER(country) LIKE UPPER('%%%s%%')\n    OR UPPER(name) LIKE UPPER('%%%s%%')\n    ORDER BY name ASC";
  $result = db_query_range($sql, $input, $input, $input, 0, 10);
  while ($match = db_fetch_object($result)) {
    $matches[$match->icao] = check_plain(sprintf("%s, %s (%s)", $match->name, $match->country, $match->icao));
  }
  print drupal_to_js($matches);
  exit;
}

/**
 * Return the configuration for the given user id.
 *
 * If there is no configuration yet, get the default
 * configuration instead.
 */
function _weather_get_config($uid, $cid) {
  $sql = "SELECT * FROM {weather_config} WHERE uid=%d AND cid=%d";
  $result = db_query($sql, $uid, $cid);
  $config = db_fetch_array($result);
  if (!isset($config['icao'])) {

    // There was no configuration found. See if there is a custom
    // default configuration.
    $sql = "SELECT * FROM {weather_config} WHERE uid=%d AND cid=%d";
    $result = db_query($sql, 0, 1);
    $config = db_fetch_array($result);
    if (!isset($config['icao'])) {

      // There is no custom default configuration, provide the
      // module's default
      $config['icao'] = 'EDDH';
      $config['real_name'] = 'Hamburg-Fuhlsbüttel';
      $config['units'] = array(
        'temperature' => 'celsius',
        'windspeed' => 'kmh',
        'pressure' => 'hpa',
        'humidity' => 'display',
        'visibility' => 'kilometers',
      );
      $config['settings'] = array(
        'show_windchill' => FALSE,
        'show_unconverted_metar' => FALSE,
        'show_abbreviated_directions' => FALSE,
        'show_directions_degree' => FALSE,
        'show_sunrise_sunset' => FALSE,
        'sunrise_sunset_timezone' => 'gmt',
        'show_compact_block' => FALSE,
      );
      $config['weight'] = 0;
    }
    else {

      // get the custom default configuration
      $config['units'] = unserialize($config['units']);
      $config['settings'] = unserialize($config['settings']);
    }
  }
  else {

    // get the user's configuration
    $config['units'] = unserialize($config['units']);
    $config['settings'] = unserialize($config['settings']);
  }
  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;
}

/**
 * Return the first unused sytem-wide block id
 */
function _weather_get_free_block_id() {
  $result = db_query("SELECT DISTINCT uid FROM {weather_config}\n    WHERE uid < 0 ORDER BY uid DESC");
  $free_block_id = -1;
  while ($block_id = db_fetch_array($result)) {
    if ($block_id['uid'] < $free_block_id) {
      break;
    }
    $free_block_id--;
  }
  return $free_block_id;
}

/**
 * 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;
}

/**
 * Return a list of current blocks in use
 */
function _weather_get_blocks_in_use() {
  $blocks = array();
  $result = db_query("SELECT DISTINCT uid FROM {weather_config} WHERE uid < 0\n    ORDER BY uid DESC");
  while ($row = db_fetch_array($result)) {

    // Convert negative UIDs to positive
    $blocks[] = abs($row['uid']);
  }
  return $blocks;
}

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

/**
 * Format the weather condition and phenomena (rain, drizzle, snow, ...)
 */
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, obscuration
  if (isset($metar['phenomena']['#mist'])) {
    $result[] = t('mist');
  }
  if (isset($metar['phenomena']['fog'])) {
    $fog = $metar['phenomena']['fog'];
    if (isset($fog['#shallow'])) {
      $result[] = t('shallow fog');
    }
    else {
      if (isset($fog['#partial'])) {
        $result[] = t('partial fog');
      }
      else {
        if (isset($fog['#patches'])) {
          $result[] = t('patches of fog');
        }
        else {
          $result[] = t('fog');
        }
      }
    }
  }
  if (isset($metar['phenomena']['#smoke'])) {
    $result[] = t('smoke');
  }

  // weather phenomena, precipitation
  if (isset($metar['phenomena']['rain'])) {
    $rain = $metar['phenomena']['rain'];
    if (isset($rain['#light'])) {
      if (isset($rain['#showers'])) {
        $result[] = t('light rain showers');
      }
      else {
        if (isset($rain['#freezing'])) {
          $result[] = t('light freezing rain');
        }
        else {
          $result[] = t('light rain');
        }
      }
    }
    else {
      if (isset($rain['#heavy'])) {
        if (isset($rain['#showers'])) {
          $result[] = t('heavy rain showers');
        }
        else {
          if (isset($rain['#freezing'])) {
            $result[] = t('heavy freezing rain');
          }
          else {
            $result[] = t('heavy rain');
          }
        }
      }
      else {
        if (isset($rain['#showers'])) {
          $result[] = t('rain showers');
        }
        else {
          if (isset($rain['#freezing'])) {
            $result[] = t('freezing rain');
          }
          else {
            $result[] = t('rain');
          }
        }
      }
    }
  }
  else {
    if (isset($metar['phenomena']['drizzle'])) {
      $drizzle = $metar['phenomena']['drizzle'];
      if (isset($drizzle['#light'])) {
        if (isset($drizzle['#freezing'])) {
          $result[] = t('light freezing drizzle');
        }
        else {
          $result[] = t('light drizzle');
        }
      }
      else {
        if (isset($drizzle['#heavy'])) {
          if (isset($drizzle['#freezing'])) {
            $result[] = t('heavy freezing drizzle');
          }
          else {
            $result[] = t('heavy drizzle');
          }
        }
        else {
          if (isset($drizzle['#freezing'])) {
            $result[] = t('freezing drizzle');
          }
          else {
            $result[] = t('drizzle');
          }
        }
      }
    }
    else {
      if (isset($metar['phenomena']['snow'])) {
        $snow = $metar['phenomena']['snow'];
        if (isset($snow['#light'])) {
          if (isset($snow['#blowing'])) {
            $result[] = t('light blowing snow');
          }
          else {
            if (isset($snow['#low_drifting'])) {
              $result[] = t('light low drifting snow');
            }
            else {
              if (isset($snow['#showers'])) {
                $result[] = t('light snow showers');
              }
              else {
                $result[] = t('light snow');
              }
            }
          }
        }
        else {
          if (isset($snow['#heavy'])) {
            if (isset($snow['#blowing'])) {
              $result[] = t('heavy blowing snow');
            }
            else {
              if (isset($snow['#low_drifting'])) {
                $result[] = t('heavy low drifting snow');
              }
              else {
                if (isset($snow['#showers'])) {
                  $result[] = t('heavy snow showers');
                }
                else {
                  $result[] = t('heavy snow');
                }
              }
            }
          }
          else {
            if (isset($snow['#blowing'])) {
              $result[] = t('blowing snow');
            }
            else {
              if (isset($snow['#low_drifting'])) {
                $result[] = t('low drifting snow');
              }
              else {
                if (isset($snow['#showers'])) {
                  $result[] = t('snow showers');
                }
                else {
                  $result[] = t('snow');
                }
              }
            }
          }
        }
      }
    }
  }
  return join(", ", $result);
}

/**
 * Convert temperatures and calculate wind chill
 *
 * Windchill temperature is only defined for temperatures at or below 50 °F
 * and wind speeds above 3 mph. Bright sunshine may increase the wind chill
 * temperature by 10 to 18 degrees F.
 * @link http://www.weather.gov/os/windchill/windchillglossary.shtml
 *
 * @param array Temperature data
 * @param array Wind data
 * @param array The unit to be returned (celsius, fahrenheit)
 * @param array The display setting, whether to show windchill
 * @return array Formatted representation, either in celsius or fahrenheit
 *               with the wind chill calculated
 */
function _weather_format_temperature($temperature, $wind, $unit, $settings) {
  if (isset($settings['show_windchill']) and $settings['show_windchill'] == TRUE and $temperature['fahrenheit'] <= 50 and $wind['speed_mph'] >= 3) {

    // Calculate windchill (in degree Fahrenheit)
    $windchill['fahrenheit'] = round(35.74 + 0.6215000000000001 * $temperature['fahrenheit'] - 35.75 * pow($wind['speed_mph'], 0.16) + 0.4275 * $temperature['fahrenheit'] * pow($wind['speed_mph'], 0.16), 1);
    $windchill['celsius'] = round(($windchill['fahrenheit'] - 32) * 5 / 9, 1);
    if ($unit['temperature'] == 'fahrenheit') {
      $result['temperature_windchill'] = t('!temperature&thinsp;°F', array(
        '!temperature' => $windchill['fahrenheit'],
      ));
    }
    elseif ($unit['temperature'] == 'celsiusfahrenheit') {
      $result['temperature_windchill'] = t('!temperature_c&thinsp;°C / !temperature_f&thinsp;°F', array(
        '!temperature_c' => $windchill['celsius'],
        '!temperature_f' => $windchill['fahrenheit'],
      ));
    }
    elseif ($unit['temperature'] == 'fahrenheitcelsius') {
      $result['temperature_windchill'] = t('!temperature_f&thinsp;°F / !temperature_c&thinsp;°C', array(
        '!temperature_f' => $windchill['fahrenheit'],
        '!temperature_c' => $windchill['celsius'],
      ));
    }
    else {

      // default to metric units
      $result['temperature_windchill'] = t('!temperature&thinsp;°C', array(
        '!temperature' => $windchill['celsius'],
      ));
    }
  }

  // Format the temperature
  if ($unit['temperature'] == 'fahrenheit') {
    $result['temperature'] = t('!temperature&thinsp;°F', array(
      '!temperature' => $temperature['fahrenheit'],
    ));
  }
  elseif ($unit['temperature'] == 'celsiusfahrenheit') {
    $result['temperature'] = t('!temperature_c&thinsp;°C / !temperature_f&thinsp;°F', array(
      '!temperature_c' => $temperature['celsius'],
      '!temperature_f' => $temperature['fahrenheit'],
    ));
  }
  elseif ($unit['temperature'] == 'fahrenheitcelsius') {
    $result['temperature'] = t('!temperature_f&thinsp;°F / !temperature_c&thinsp;°C', array(
      '!temperature_f' => $temperature['fahrenheit'],
      '!temperature_c' => $temperature['celsius'],
    ));
  }
  else {

    // default to metric units
    $result['temperature'] = t('!temperature&thinsp;°C', array(
      '!temperature' => $temperature['celsius'],
    ));
  }
  return preg_replace("/([^ ]*)&thinsp;([^ ]*)/", '<span style="white-space:nowrap;">\\1&thinsp;\\2</span>', $result);
}

/**
 * Convert wind
 *
 * @param int Wind
 * @param string The unit to be returned (km/h, knots, meter/s, mph)
 * @param array Settings, used for abbreviated wind directions
 * @return string Formatted representation
 */
function _weather_format_wind($wind, $unit, $settings) {

  // shortcut for a special case
  if ($wind['direction'] == 0 and $wind['speed_kmh'] == 0) {
    return t('Calm');
  }
  $wind['direction_text'] = weather_bearing_to_text($wind['direction']);
  $wind['direction_text_short'] = weather_bearing_to_text($wind['direction'], TRUE);
  if (isset($wind['variable_start'])) {
    $wind['variable_start_text'] = weather_bearing_to_text($wind['variable_start']);
    $wind['variable_start_text_short'] = weather_bearing_to_text($wind['variable_start'], TRUE);
  }
  if (isset($wind['variable_end'])) {
    $wind['variable_end_text'] = weather_bearing_to_text($wind['variable_end']);
    $wind['variable_end_text_short'] = weather_bearing_to_text($wind['variable_end'], TRUE);
  }

  // handle variable wind directions
  if ($wind['direction'] == 'VRB' or isset($wind['variable_start'])) {
    if (isset($wind['variable_start'])) {
      if ($settings['show_abbreviated_directions']) {
        if ($settings['show_directions_degree']) {
          $result[] = t('Variable from !direction_a (!degree_a°) to !direction_b (!degree_b°)', array(
            '!direction_a' => $wind['variable_start_text_short'],
            '!degree_a' => $wind['variable_start'],
            '!direction_b' => $wind['variable_end_text_short'],
            '!degree_b' => $wind['variable_end'],
          ));
        }
        else {
          $result[] = t('Variable from !direction_a to !direction_b', array(
            '!direction_a' => $wind['variable_start_text_short'],
            '!direction_b' => $wind['variable_end_text_short'],
          ));
        }
      }
      else {
        if ($settings['show_directions_degree']) {
          $result[] = t('Variable from !direction_a (!degree_a°) to !direction_b (!degree_b°)', array(
            '!direction_a' => $wind['variable_start_text'],
            '!degree_a' => $wind['variable_start'],
            '!direction_b' => $wind['variable_end_text'],
            '!degree_b' => $wind['variable_end'],
          ));
        }
        else {
          $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
    if ($settings['show_abbreviated_directions']) {
      if ($settings['show_directions_degree']) {
        $result[] = t('!direction (!degree°)', array(
          '!direction' => $wind['direction_text_short'],
          '!degree' => $wind['direction'],
        ));
      }
      else {
        $result[] = $wind['direction_text_short'];
      }
    }
    else {
      if ($settings['show_directions_degree']) {
        $result[] = t('!direction (!degree°)', array(
          '!direction' => $wind['direction_text'],
          '!degree' => $wind['direction'],
        ));
      }
      else {
        $result[] = $wind['direction_text'];
      }
    }
  }

  // Set up the wind speed
  // In order to reduce the number of strings to translate, the
  // gust speed is converted here as well. Later on, we just need
  // to set up the gust translation.
  if ($unit['windspeed'] == 'mph') {
    $result[] = t('!speed&thinsp;mph', array(
      '!speed' => $wind['speed_mph'],
    ));
    $gust_speed = t('!speed&thinsp;mph', array(
      '!speed' => $wind['gusts_mph'],
    ));
  }
  else {
    if ($unit['windspeed'] == 'knots') {
      $result[] = t('!speed&thinsp;knots', array(
        '!speed' => $wind['speed_knots'],
      ));
      $gust_speed = t('!speed&thinsp;knots', array(
        '!speed' => $wind['gusts_knots'],
      ));
    }
    else {
      if ($unit['windspeed'] == 'mps') {
        $result[] = t('!speed&thinsp;meter/s', array(
          '!speed' => $wind['speed_mps'],
        ));
        $gust_speed = t('!speed&thinsp;meter/s', array(
          '!speed' => $wind['gusts_mps'],
        ));
      }
      else {
        if ($unit['windspeed'] == 'beaufort') {
          $result[] = t('Beaufort !number', array(
            '!number' => $wind['speed_beaufort'],
          ));
          $gust_speed = t('Beaufort !number', array(
            '!number' => $wind['gusts_beaufort'],
          ));
        }
        else {

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

  // set up gusts, if applicable
  if ($wind['gusts_kmh'] > 0) {
    $result[] = t('gusts up to !speed', array(
      '!speed' => $gust_speed,
    ));
  }
  $tmp = preg_replace("/([^ ]*)&thinsp;([^ ]*)/", '<span style="white-space:nowrap;">\\1&thinsp;\\2</span>', $result);
  return join(', ', $tmp);
}

/**
 * Convert pressure
 *
 * @param int Pressure
 * @param string The unit to be returned (inHg, mmHg, hPa)
 * @return string Formatted representation
 */
function _weather_format_pressure($pressure, $unit) {
  if ($unit['pressure'] == 'inhg') {
    $result = t('!pressure&thinsp;inHg', array(
      '!pressure' => $pressure['inHg'],
    ));
  }
  else {
    if ($unit['pressure'] == 'mmhg') {
      $result = t('!pressure&thinsp;mmHg', array(
        '!pressure' => $pressure['mmHg'],
      ));
    }
    else {
      if ($unit['pressure'] == 'kpa') {
        $result = t('!pressure&thinsp;kPa', array(
          '!pressure' => $pressure['kPa'],
        ));
      }
      else {

        // default to metric units
        $result = t('!pressure&thinsp;hPa', array(
          '!pressure' => $pressure['hPa'],
        ));
      }
    }
  }
  return preg_replace("/([^ ]*)&thinsp;([^ ]*)/", '<span style="white-space:nowrap;">\\1&thinsp;\\2</span>', $result);
}

/**
 * Calculate the relative humidity
 *
 * @param float Temperature (must be Celsius)
 * @param float Dewpoint (must be Celsius)
 * @return string Formatted representation
 */
function _weather_format_relative_humidity($temperature, $dewpoint) {
  $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']));
  $result = t('!rel_humidity&thinsp;%', array(
    '!rel_humidity' => max(0, min(100, round(100 * ($e / $es), 0))),
  ));
  return preg_replace("/([^ ]*)&thinsp;([^ ]*)/", '<span style="white-space:nowrap;">\\1&thinsp;\\2</span>', $result);
}

/**
 * Convert the visibility
 */
function _weather_format_visibility($visibility, $unit) {
  if ($unit['visibility'] == 'miles') {
    $result = t('!visibility&thinsp;mi', array(
      '!visibility' => $visibility['miles'],
    ));
  }
  else {

    // default to metric units
    $result = t('!visibility&thinsp;km', array(
      '!visibility' => $visibility['kilometers'],
    ));
  }
  return preg_replace("/([^ ]*)&thinsp;([^ ]*)/", '<span style="white-space:nowrap;">\\1&thinsp;\\2</span>', $result);
}

/**
 * Convert information about nearest METAR station in the location block
 */
function _weather_format_closest_station($distance, $unit, $settings) {
  while ($distance['direction'] < 0) {
    $distance['direction'] += 360;
  }
  while ($distance['direction'] >= 360) {
    $distance['direction'] -= 360;
  }
  if ($settings['show_abbreviated_directions']) {
    $direction = weather_bearing_to_text($distance['direction'], TRUE);
  }
  else {
    $direction = weather_bearing_to_text($distance['direction']);
  }
  if ($unit['visibility'] == 'miles') {
    if ($settings['show_directions_degree']) {
      $result = t('!distance&thinsp;mi !direction (!degree°)', array(
        '!distance' => $distance['miles'],
        '!direction' => $direction,
        '!degree' => $distance['direction'],
      ));
    }
    else {
      $result = t('!distance&thinsp;mi !direction', array(
        '!distance' => $distance['miles'],
        '!direction' => $direction,
      ));
    }
  }
  else {

    // default to metric units
    if ($settings['show_directions_degree']) {
      $result = t('!distance&thinsp;km !direction (!degree°)', array(
        '!distance' => $distance['kilometers'],
        '!direction' => $direction,
        '!degree' => $distance['direction'],
      ));
    }
    else {
      $result = t('!distance&thinsp;km !direction', array(
        '!distance' => $distance['kilometers'],
        '!direction' => $direction,
      ));
    }
  }
  return preg_replace("/([^ ]*)&thinsp;([^ ]*)/", '<span style="white-space:nowrap;">\\1&thinsp;\\2</span>', $result);
}

/**
 * Converts a compass bearing to a text direction (e.g. 0° North,
 * 86° East, ...)
 *
 * @param int Compass bearing in degrees
 * @param boolean If true, return abbreviated directions (N, NNW)
 *                instead of full text (North, North-Northwest)
 *                Defaults to full text directions
 * @return string The translated text direction
 */
function weather_bearing_to_text($bearing, $abbreviated = FALSE) {

  // Ensure the bearing to be from 0° to 359°
  while ($bearing < 0) {
    $bearing += 360;
  }
  while ($bearing >= 360) {
    $bearing -= 360;
  }

  // Determine the sector. This works for 0° up to 348.75°
  // If the bearing was greater than 348.75°, perform a wrap (%16)
  $sector = floor(($bearing + 11.25) / 22.5) % 16;
  if (!$abbreviated) {
    $direction = array(
      t('North'),
      t('North-Northeast'),
      t('Northeast'),
      t('East-Northeast'),
      t('East'),
      t('East-Southeast'),
      t('Southeast'),
      t('South-Southeast'),
      t('South'),
      t('South-Southwest'),
      t('Southwest'),
      t('West-Southwest'),
      t('West'),
      t('West-Northwest'),
      t('Northwest'),
      t('North-Northwest'),
    );
  }
  else {
    $direction = array(
      t('N'),
      t('NNE'),
      t('NE'),
      t('ENE'),
      t('E'),
      t('ESE'),
      t('SE'),
      t('SSE'),
      t('S'),
      t('SSW'),
      t('SW'),
      t('WSW'),
      t('W'),
      t('WNW'),
      t('NW'),
      t('NNW'),
    );
  }
  return $direction[$sector];
}

/*********************************************************************
 * Internal function for retrieving data
 ********************************************************************/
function weather_get_countries() {
  $sql = "SELECT country FROM {weather_icao}\n";
  $sql .= "GROUP BY country ORDER BY country ASC";
  $result = db_query($sql);
  while ($row = db_fetch_array($result)) {
    $countries[] = $row['country'];
  }
  return $countries;
}
function weather_get_places($country) {
  $sql = "SELECT icao, name FROM {weather_icao}\n";
  $sql .= "WHERE country='%s' ORDER BY name ASC";
  $result = db_query($sql, $country);
  while ($row = db_fetch_array($result)) {
    $places[$row['icao']] = $row['name'];
  }
  return $places;
}
function weather_get_country_from_icao($wanted_icao) {
  $sql = "SELECT country FROM {weather_icao} WHERE icao='%s'";
  $result = db_query($sql, $wanted_icao);
  $row = db_fetch_array($result);
  return $row['country'];
}
function weather_get_latitude_longitude($wanted_icao) {
  $sql = "SELECT latitude, longitude FROM {weather_icao} WHERE icao='%s'";
  $result = db_query($sql, $wanted_icao);
  $row = db_fetch_array($result);
  return $row;
}

/**
 * Return ICAO code of the nearest weather station.
 *
 * The distance calculation is based on the spherical law of cosines.
 * The bearing is converted from radians to degress and normalized to
 * be between 0 and 360 degress. The returned value will range from
 * -180° to 180°.
 * All angles must be passed in radians for the trigonometry functions.
 *
 * R = Earth's radius (using a mean radius of 6371 km)
 * distance = R * acos(sin(lat1) * sin(lat2) + cos(lat1) * cos(lat2) * cos(long2 - long1))
 * bearing = atan2(sin(long2 - long1) * cos(lat2), cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(long2- long1))
 *
 * @param Latitude to be searched.
 * @param Longitude to be searched.
 *
 * @return array METAR station with two additional fields (distance and
 *               direction)
 */
function weather_get_icao_from_lat_lon($latitude, $longitude) {
  $sql = "SELECT icao, name,\n    ROUND(6371 *\n      ACOS(\n        SIN(RADIANS(%f)) * SIN(RADIANS(latitude)) +\n        COS(RADIANS(%f)) * COS(RADIANS(latitude)) * COS(RADIANS(longitude - %f))\n      ),\n    1) AS distance_in_km,\n    MOD(\n      ROUND(\n        DEGREES(\n          ATAN2(\n            SIN(RADIANS(longitude - %f)) * COS(RADIANS(latitude)),\n            COS(RADIANS(%f)) * SIN(RADIANS(latitude)) - SIN(RADIANS(%f)) * COS(RADIANS(latitude)) * COS(RADIANS(longitude - %f))\n          )\n        )\n      ) + 360, 360\n    ) AS bearing\n    FROM {weather_icao} ORDER BY distance_in_km";
  $result = db_query($sql, array(
    $latitude,
    $latitude,
    $longitude,
    $longitude,
    $latitude,
    $latitude,
    $longitude,
  ));
  $station = db_fetch_array($result);
  $station['distance']['kilometers'] = $station['distance_in_km'];
  $station['distance']['miles'] = round($station['distance_in_km'] / 1.609344, 1);
  $station['distance']['direction'] = $station['bearing'];
  return $station;
}

/**
 * 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['next_update_on'])) {
    $data['next_update_on'] = 0;
  }

  // If the time has come, try to download again
  if ($data['next_update_on'] <= time()) {
    $metar_raw = _weather_retrieve_data($icao);
    if ($metar_raw) {
      $metar = weather_parse_metar($metar_raw);

      // Calculate the next scheduled update. Use 62 minutes after the
      // reported timestamp, to allow the data to propagate to the server.
      $data['next_update_on'] = $metar['reported_on'] + 62 * 60;

      // However, if the current time is more than 62 minutes
      // over the reported timestamp, do not download on every page request.
      // Therefore, we use 3, 6, 12, and 24 hours to check for updates.
      // From then on, we check once a day for updates.
      if ($data['next_update_on'] < time()) {
        $last_update = $metar['reported_on'];
        $hours = 3 * 60 * 60;
        while ($last_update + $hours + 120 < time()) {
          if ($hours < 86400) {
            $hours = $hours * 2;
          }
          else {
            $hours = $hours + 86400;
          }
        }

        // Add 2 minutes to allow the data to propagate to the server.
        $data['next_update_on'] = $last_update + $hours + 120;
      }
      weather_store_metar($metar, $data['next_update_on']);
    }
    else {

      // The download has not been successful. Calculate the time of next update
      // according to last tries.
      if (empty($data['metar_raw'])) {

        // There is no entry yet, so this is the first download attempt.
        // Create a new entry and store the current time in the metar_raw column.
        $metar['icao'] = $icao;
        $metar['#raw'] = gmdate("dHi") . "Z";
        $next_update_on = time() + 10 * 60;
        weather_store_metar($metar, $next_update_on);
      }
      else {

        // There has been at least one download attempt. Increase the time of
        // next update to not download every few minutes.
        // If 24 hours are reached, we check once a day for updates.
        // This way, we gracefully handle ICAO codes which do no longer exist.
        // The time of the last download attempt is stored in the metar_raw column.
        $metar = weather_parse_metar($data['metar_raw']);
        $last_update = $metar['reported_on'];
        $hours = 3 * 60 * 60;
        while ($last_update + $hours + 120 < time()) {
          if ($hours < 86400) {
            $hours = $hours * 2;
          }
          else {
            $hours = $hours + 86400;
          }
        }

        // Add 2 minutes to allow the data to propagate to the server.
        $next_update_on = $last_update + $hours + 120;
        $metar['icao'] = $icao;
        $metar['#raw'] = $data['metar_raw'];
        weather_store_metar($metar, $next_update_on);
      }
    }
  }
  else {
    $metar = weather_parse_metar($data['metar_raw']);
  }
  return $metar;
}

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

  // 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')";
  db_query($sql, $metar['icao'], $next_update_on, $metar['#raw']);
}

/**
 * Retrieve data from http://www.aviationweather.gov/
 */
function _weather_retrieve_data($icao) {
  $metar_raw = FALSE;

  // Specify timeout in seconds
  $timeout = 10;
  $url = 'http://www.aviationweather.gov/adds/metars/?chk_metars=on&station_ids=' . $icao;
  $response = drupal_http_request($url, array(
    'timeout' => $timeout,
  ));

  // Extract the valid METAR data from the received webpage.
  if (preg_match("/({$icao} [0-9]{6}Z [^<]+)/m", $response->data, $matches)) {
    $metar_raw = str_replace("\n", "", $matches[1]);
  }

  // Check for errors.
  if ($metar_raw === FALSE) {

    // Make an entry about this error into the watchdog table.
    watchdog('weather', 'Download location for METAR data is not accessible.', array(), WATCHDOG_ERROR);

    // Show a message to users with administration priviledges
    if (user_access('administer custom weather block') or user_access('administer site configuration')) {
      drupal_set_message(t('Download location for METAR data is not accessible.'), 'error');
    }
  }
  return $metar_raw;
}

Functions

Namesort descending Description
theme_weather_theming Custom theme function for preprocessing the weather block output
weather_admin_main_page Show an overview of configured locations and the default location
weather_bearing_to_text Converts a compass bearing to a text direction (e.g. 0° North, 86° East, ...)
weather_block Generate HTML for the weather block
weather_cron Implementation of hook_cron().
weather_custom_block Show a configuration page for a custom weather block
weather_custom_block_access Check whether the user has access to their own custom weather block
weather_custom_block_delete_confirm Confirmation page before deleting a location
weather_custom_block_delete_confirm_submit Handle the deletion of a location
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_countries
weather_get_country_from_icao
weather_get_icao_from_lat_lon Return ICAO code of the nearest weather station.
weather_get_latitude_longitude
weather_get_metar Fetches the latest METAR data from the database or internet
weather_get_places
weather_help Implementation of hook_help().
weather_js Return a new place selection box based on the country selection
weather_main_page_form Construct a form for general settings of the Weather module
weather_main_page_form_submit Handle the submission for general settings of the Weather module
weather_menu Implementation of hook_menu().
weather_perm Implementation of hook_perm().
weather_search_autocomplete Given a partial string, search for a location or ICAO code matching that string.
weather_search_form Display a form for the user to search for weather locations.
weather_search_form_submit Submission handler for the weather search form.
weather_search_form_validate Validate the input from the weather search form
weather_search_location Searches for the specified location, whether it is a place name or an ICAO code. For example, weather/fuhlsbüttel will display the weather for Hamburg-Fuhlsbüttel.
weather_store_metar Stores parsed METAR data in the database
weather_theme Implementation of hook_theme().
weather_user_main_page Show an overview of configured locations
_weather_format_closest_station Convert information about nearest METAR station in the location block
_weather_format_condition Format the weather condition and phenomena (rain, drizzle, snow, ...)
_weather_format_pressure Convert pressure
_weather_format_relative_humidity Calculate the relative humidity
_weather_format_temperature Convert temperatures and calculate wind chill
_weather_format_visibility Convert the visibility
_weather_format_wind Convert wind
_weather_get_blocks_in_use Return a list of current blocks in use
_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_block_id Return the first unused sytem-wide block id
_weather_get_free_config Return the first unused configuration number
_weather_get_image
_weather_retrieve_data Retrieve data from http://www.aviationweather.gov/

Constants