weather_parser.inc in Weather 7
Same filename and directory in other branches
Retrieves and parses raw METAR data and stores result in database.
Copyright © 2006-2013 Tobias Quathamer <t.quathamer@gmx.net>
This program 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.
This program 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 this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
File
weather_parser.incView source
<?php
/**
* @file
* Retrieves and parses raw METAR data and stores result in database.
*
* Copyright © 2006-2013 Tobias Quathamer <t.quathamer@gmx.net>
*
* This program 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.
*
* This program 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 this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
/**
* Parses raw METAR data string and stores results in database.
*
* @param string $icao
* ICAO code.
* @param object $metar
* METAR data object, may be altered.
*
* @return object
* METAR data object or FALSE.
*/
function weather_refresh_data($icao, &$metar) {
$metar_raw_string = weather_retrieve_data($icao);
// If there's new data available, parse it.
if (!empty($metar_raw_string)) {
$metar = weather_parse_metar($metar_raw_string);
// Calculate the next scheduled update. Use 62 minutes after the
// reported timestamp, to allow the data to propagate to the server.
$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 ($next_update_on < REQUEST_TIME) {
$last_update = $metar->reported_on;
$hours = 3 * 60 * 60;
while ($last_update + $hours + 120 < REQUEST_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->next_update_on = $next_update_on;
}
else {
// The download has not been successful. Calculate the time of next update
// according to last tries.
if (empty($metar)) {
// There is no entry yet, so this is the first download attempt.
// Create a new entry with 'nodata' and give some minutes to retry.
$metar = new stdClass();
$metar->icao = $icao;
$metar->reported_on = REQUEST_TIME;
$metar->next_update_on = REQUEST_TIME + 10 * 60;
$metar->image = 'nodata';
}
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.
$last_update = $metar->reported_on;
$hours = 3 * 60 * 60;
while ($last_update + $hours + 120 < REQUEST_TIME) {
if ($hours < 86400) {
$hours = $hours * 2;
}
else {
$hours = $hours + 86400;
}
}
// Add 2 minutes to allow the data to propagate to the server.
$metar->next_update_on = $last_update + $hours + 120;
}
}
// Wrap drupal_write_record() into try/catch block, see #1412352.
try {
// Make sure there's no stale data around
db_delete('weather_metar')
->condition('icao', $icao)
->execute();
drupal_write_record('weather_metar', $metar);
} catch (Exception $e) {
// Nothing to do here. If the writing fails and an exception
// is raised, the writing of the data will happen on the next
// update.
watchdog_exception('weather', $e);
}
}
/**
* Retrieve data from http://www.aviationweather.gov/
*
* @param string $icao
* ICAO code
*
* @return string
* Raw METAR string or FALSE
*/
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 (!empty($response->data) && 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;
}
/**
* Parses a raw METAR data string.
*
* @param string $metar_raw_string
* Raw METAR data string.
*
* @return object
* METAR data object.
*/
function weather_parse_metar($metar_raw_string) {
// Setup the METAR data object.
$metar = new stdClass();
$metar->raw = $metar_raw_string;
// Initialize a helper property, see bug #1894646.
$metar->visibility_miles = 0;
// Extract the date and time in UTC.
$metar->reported_on = _weather_parse_timestamp($metar_raw_string);
// Some stations insert a space between the cloud conditions
// and the altitude, for example, "FEW 025" instead of "FEW025".
// Therefore, we scan for such occurences and remove the space.
$metar_raw_string = preg_replace("/(FEW|SCT|BKN|OVC)\\s+([0-9]{3})/", '$1$2', $metar->raw);
// Split string for parsing routines
$raw_items = preg_split('/\\s+/', drupal_strtoupper($metar_raw_string));
// Run the data through the METAR routines
foreach ($raw_items as $metar_raw) {
if (_weather_parse_stop($metar_raw)) {
break;
}
_weather_parse_icao($metar_raw, $metar);
_weather_parse_sky_condition($metar_raw, $metar);
_weather_parse_phenomena($metar_raw, $metar);
_weather_parse_temperature($metar_raw, $metar);
_weather_parse_wind($metar_raw, $metar);
_weather_parse_pressure($metar_raw, $metar);
_weather_parse_visibility($metar_raw, $metar);
}
if (isset($metar->phenomena)) {
$metar->phenomena = implode(', ', $metar->phenomena);
}
// Calculate sunrise and sunset times
_weather_calculate_sunrise_sunset($metar);
// Set up the image filename.
_weather_construct_image_filename($metar);
return $metar;
}
/**
* Extract timestamp.
*
* @param string $metar_raw
* Raw METAR data to parse
*
* @return string
* Datetime string (UTC) for database storage
*/
function _weather_parse_timestamp($metar_raw) {
// The format to match is DDHHMMZ, for example, 251720Z
if (preg_match('/ ([0-9]{2})([0-9]{2})([0-9]{2})Z /', $metar_raw, $matches)) {
// Construct the date
$month = gmdate("n");
$year = gmdate("Y");
// If the day is larger than the current day, it's most probably
// a day in the last month.
if ($matches[1] > gmdate("d")) {
$month -= 1;
}
return gmmktime($matches[2], $matches[3], 0, $month, $matches[1], $year);
}
}
/**
* Decide whether to stop parsing.
*
* @param string $metar_raw
* Raw METAR data to parse.
*
* @return boolean
* TRUE or FALSE.
*/
function _weather_parse_stop($metar_raw) {
if (preg_match('/^(BECMG|TEMPO|NOSIG|RMK)$/', $metar_raw)) {
return TRUE;
}
else {
return FALSE;
}
}
/**
* Extract ICAO code.
*
* ICAO = International Civil Aviation Organization, this is a four
* letter airport code, e. g. EDDH
*
* @param string $metar_raw
* Raw METAR data to parse.
* @param object $metar
* METAR data object, may be altered.
*/
function _weather_parse_icao($metar_raw, &$metar) {
if (preg_match('/^([A-Z]{4}|K[A-Z0-9]{3})$/', $metar_raw) and !isset($metar->icao)) {
$metar->icao = $metar_raw;
}
}
/**
* Extract sky condition information.
*
* @param string $metar_raw
* Raw METAR data to parse.
* @param object $metar
* METAR data object, may be altered.
*/
function _weather_parse_sky_condition($metar_raw, &$metar) {
// The ordering of cloud covering is as follows:
// 0, CLR: clear
// 1, FEW: few
// 2, SCT: scattered
// 3, BKN: broken
// 4, OVC: overcast
if (preg_match('/^' . '(FEW|SCT|BKN|OVC)([0-9]{3})' . '(CB|TCU)?(\\/\\/\\/)?' . '$/', $metar_raw, $matches)) {
switch ($matches[1]) {
case 'CLR':
$metar->sky_condition_order[] = 0;
$metar->sky_condition_text[] = 'clear';
break;
case 'FEW':
$metar->sky_condition_order[] = 1;
$metar->sky_condition_text[] = 'few';
break;
case 'SCT':
$metar->sky_condition_order[] = 2;
$metar->sky_condition_text[] = 'scattered';
break;
case 'BKN':
$metar->sky_condition_order[] = 3;
$metar->sky_condition_text[] = 'broken';
break;
case 'OVC':
$metar->sky_condition_order[] = 4;
$metar->sky_condition_text[] = 'overcast';
break;
}
}
elseif (preg_match('/^' . '(CLR|SKC|CAVOK)' . '$/', $metar_raw, $matches)) {
$metar->sky_condition_order[] = 0;
$metar->sky_condition_text[] = 'clear';
// CAVOK implies a visibility of 10 km, there's no extra visibility section.
if ($matches[1] == 'CAVOK') {
$metar->visibility = 10000;
}
}
elseif (preg_match('/^' . '(NSC|NCD)' . '$/', $metar_raw, $matches)) {
// NSC means no significant clouds,
// NCD is from automatic stations, no cloud detected
// The ordering is equivalent to "FEW"
$metar->sky_condition_order[] = 1;
$metar->sky_condition_text[] = 'no significant clouds';
}
elseif (preg_match('/^' . 'VV[0-9\\/]{3}' . '$/', $metar_raw, $matches)) {
// VV is the vertical visibility, this should be shown as overcast.
// If no information is available, VV/// will be used.
$metar->sky_condition_order[] = 4;
$metar->sky_condition_text[] = 'overcast';
}
if (isset($metar->sky_condition_order)) {
if (isset($metar->current_sky_condition_order)) {
foreach ($metar->sky_condition_order as $index => $order) {
if ($order > $metar->current_sky_condition_order) {
$metar->current_sky_condition_order = $order;
$metar->sky_condition = $metar->sky_condition_text[$index];
}
}
}
else {
$metar->current_sky_condition_order = $metar->sky_condition_order[0];
$metar->sky_condition = $metar->sky_condition_text[0];
}
}
}
/**
* Extract phenomena information.
*
* @param string $metar_raw
* Raw METAR data to parse.
* @param object $metar
* METAR data object, may be altered.
*/
function _weather_parse_phenomena($metar_raw, &$metar) {
// Handle rain
if (preg_match('/^' . '(-|\\+|VC)?' . '(SH|TS|FZ)?' . 'RA' . '$/', $metar_raw, $matches)) {
// Construct a result array of the following form:
// [light/heavy] [freezing] [rain] [showers]
$result[2] = 'rain';
$intensity = 'moderate';
if (isset($matches[1])) {
switch ($matches[1]) {
case '-':
$result[0] = 'light';
$intensity = 'light';
break;
case '+':
$result[0] = 'heavy';
$intensity = 'heavy';
break;
}
}
if (isset($matches[2])) {
switch ($matches[2]) {
case 'SH':
$result[3] = 'showers';
break;
case 'FZ':
$result[1] = 'freezing';
break;
}
}
ksort($result);
$metar->phenomena[] = implode(' ', $result);
// Store parts of the weather phenomena for image name construction.
$metar->image_part['precipitation'] = "{$intensity}-rain";
}
elseif (preg_match('/^' . '(-|\\+|VC)?' . '(FZ)?' . 'DZ' . '$/', $metar_raw, $matches)) {
// Construct a result array of the following form:
// [light/heavy] [freezing] [drizzle]
$result[2] = 'drizzle';
$intensity = 'moderate';
if (isset($matches[1])) {
switch ($matches[1]) {
case '-':
$result[0] = 'light';
$intensity = 'light';
break;
case '+':
$result[0] = 'heavy';
$intensity = 'heavy';
break;
}
}
if (isset($matches[2])) {
switch ($matches[2]) {
case 'FZ':
$result[1] = 'freezing';
break;
}
}
ksort($result);
$metar->phenomena[] = implode(' ', $result);
// Store parts of the weather phenomena for image name construction.
$metar->image_part['precipitation'] = "{$intensity}-rain";
}
elseif (preg_match('/^' . '(-|\\+|VC)?' . '(BL|DR|SH)?' . 'SN' . '$/', $metar_raw, $matches)) {
// Construct a result array of the following form:
// [light/heavy] [blowing/low driftig] [snow] [showers]
$result[2] = 'snow';
$intensity = 'moderate';
if (isset($matches[1])) {
switch ($matches[1]) {
case '-':
$result[0] = 'light';
$intensity = 'light';
break;
case '+':
$result[0] = 'heavy';
$intensity = 'heavy';
break;
}
}
if (isset($matches[2])) {
switch ($matches[2]) {
case 'BL':
$result[1] = 'blowing';
break;
case 'DR':
$result[1] = 'low drifting';
break;
case 'SH':
$result[3] = 'showers';
break;
}
}
ksort($result);
$metar->phenomena[] = implode(' ', $result);
// Store parts of the weather phenomena for image name construction.
$metar->image_part['precipitation'] = "{$intensity}-snow";
}
elseif (preg_match('/^' . 'BR' . '$/', $metar_raw, $matches)) {
$metar->phenomena[] = 'mist';
// Store parts of the weather phenomena for image name construction.
$metar->image_part['fog'] = 'fog';
}
elseif (preg_match('/^' . '(VC|MI|PR|BC|FZ)?' . 'FG' . '$/', $metar_raw, $matches)) {
// Construct a result array of the following form:
// [shallow/partial/patches of/freezing] [fog]
$result[1] = 'fog';
if (isset($matches[1])) {
switch ($matches[1]) {
case 'MI':
$result[0] = 'shallow';
break;
case 'PR':
$result[0] = 'partial';
break;
case 'BC':
$result[0] = 'patches of';
break;
case 'FZ':
$result[0] = 'freezing';
break;
}
}
ksort($result);
$metar->phenomena[] = implode(' ', $result);
// Store parts of the weather phenomena for image name construction.
$metar->image_part['fog'] = 'fog';
}
elseif (preg_match('/^' . 'FU' . '$/', $metar_raw, $matches)) {
$metar->phenomena[] = 'smoke';
// Store parts of the weather phenomena for image name construction.
$metar->image_part['fog'] = 'fog';
}
}
/**
* Extract temperature information.
*
* @param string $metar_raw
* Raw METAR data to parse.
* @param object $metar
* METAR data object, may be altered.
*/
function _weather_parse_temperature($metar_raw, &$metar) {
if (preg_match('/^' . '(M?[0-9]{2})' . '\\/' . '(M?[0-9]{2}|XX)?' . '$/', $metar_raw, $matches)) {
$metar->temperature = (int) strtr($matches[1], 'M', '-');
if (isset($matches[2]) and $matches[2] != 'XX') {
$metar->dewpoint = (int) strtr($matches[2], 'M', '-');
}
else {
$metar->dewpoint = NULL;
}
}
}
/**
* Extract wind information.
*
* @param string $metar_raw
* Raw METAR data to parse.
* @param object $metar
* METAR data object, may be altered.
*/
function _weather_parse_wind($metar_raw, &$metar) {
if (preg_match('/^' . '([0-9]{3}|VRB)' . '([0-9]{2,3})' . '(G([0-9]{2,3}))?' . '(KT|MPS|KMH)' . '$/', $metar_raw, $matches)) {
if ($matches[1] == 'VRB') {
$metar->wind_direction = 'variable';
}
else {
$metar->wind_direction = (int) $matches[1];
}
$wind_speed = (int) $matches[2];
$wind_gusts = (int) $matches[4];
$wind_unit = $matches[5];
// Ensure that windspeed is in km/h.
switch ($wind_unit) {
case 'KT':
// Convert from knots (1 knot = 1.852 km/h)
$metar->wind_speed = round($wind_speed * 1.852, 1);
if ($wind_gusts > 0) {
$metar->wind_gusts = round($wind_gusts * 1.852, 1);
}
break;
case 'MPS':
// Convert from meter/s (1 m/s = 3.6 km/h)
$metar->wind_speed = round($wind_speed * 3.6, 1);
if ($wind_gusts > 0) {
$metar->wind_gusts = round($wind_gusts * 3.6, 1);
}
break;
case 'KMH':
$metar->wind_speed = $wind_speed;
if ($wind_gusts > 0) {
$metar->wind_gusts = $wind_gusts;
}
break;
}
}
elseif (preg_match('/^' . '([0-9]{3})' . 'V' . '([0-9]{3})' . '$/', $metar_raw, $matches)) {
$metar->wind_direction = $matches[1] . '-' . $matches[2];
}
}
/**
* Extract pressure information.
*
* @param string $metar_raw
* Raw METAR data to parse.
* @param object $metar
* METAR data object, may be altered.
*/
function _weather_parse_pressure($metar_raw, &$metar) {
if (preg_match('/^' . '(A|Q)([0-9]{4})' . '$/', $metar_raw, $matches)) {
if ($matches[1] == 'A') {
// Pressure is given in inch Hg
$metar->pressure = round($matches[2] / 100 * 33.8639, 0);
}
else {
// Pressure is given in HektoPascal, hPa
$metar->pressure = (int) $matches[2];
}
}
}
/**
* Extract visibility information.
*
* @param string $metar_raw
* Raw METAR data to parse.
* @param object $metar
* METAR data object, may be altered.
*/
function _weather_parse_visibility($metar_raw, &$metar) {
if (preg_match('/^([0-9])$/', $metar_raw, $matches)) {
// Special case: A single digit, for example, in 1 1/2SM
$metar->visibility_miles = $matches[1];
}
elseif (preg_match('/^' . '(M?)([0-9])(\\/?)([0-9]*)' . 'SM' . '$/', $metar_raw, $matches)) {
if ($matches[3] == '/') {
// This is a fractional visibility, we need to convert this.
$visibility = $metar->visibility_miles + $matches[2] / $matches[4];
}
else {
$visibility = $matches[2] . $matches[4];
}
$metar->visibility = round($visibility * 1609.344, 0);
}
elseif (preg_match('/^([0-9]{4})(NDV)?$/', $metar_raw, $matches)) {
// NDV means "no directional variation", used by automatic stations
$metar->visibility = (int) $matches[1];
}
}
/**
* Calculate the times of sunrise and sunset.
*
* The times are GMT, so it's possible for the sunrise being
* at 16:48 while the sun sets at 7:06.
*
* @param object $metar
* METAR data object, may be altered.
*/
function _weather_calculate_sunrise_sunset(&$metar) {
// Get the coordinates for this weather station.
$location = db_query("SELECT latitude, longitude FROM {weather_icao} WHERE icao=:icao", array(
':icao' => $metar->icao,
))
->fetchAssoc();
$reported_on = $metar->reported_on;
// Initialize sunrise and sunset times.
$metar->sunrise_on = NULL;
$metar->sunset_on = NULL;
$suninfo = date_sun_info($reported_on, $location['latitude'], $location['longitude']);
// Handle special cases (no sunrise or sunset at all).
if ($suninfo['sunrise'] == 0 and $suninfo['sunset'] == 0) {
// Sun is always below the horizon. To indicate that the sun
// does not rise, let sunrise_on be NULL and set sunset_on to today.
$condition = 'night';
$metar->sunset_on = $metar->reported_on;
}
elseif ($suninfo['sunrise'] == 1 and $suninfo['sunset'] == 1) {
// Sun is always above the horizon. To indicate that the sun
// does not set, let sunset_on be NULL and set sunrise_on to today.
$condition = 'day';
$metar->sunrise_on = $metar->reported_on;
}
else {
// There is a sunrise and a sunset.
// We don't need the exact second of the sunrise and sunset. Therefore, the
// times are rounded to the next minute. We add 30 seconds and cut off the
// seconds part.
$metar->sunrise_on = round($suninfo['sunrise'] / 60) * 60;
$metar->sunset_on = round($suninfo['sunset'] / 60) * 60;
// Correctly handle northern and southern hemisphere.
if ($suninfo['sunrise'] <= $suninfo['sunset']) {
// This should be on the northern hemisphere.
if ($reported_on >= $suninfo['sunrise'] and $reported_on < $suninfo['sunset']) {
$condition = 'day';
}
else {
$condition = 'night';
}
}
else {
// This should be on the southern hemisphere.
if ($reported_on >= $suninfo['sunrise'] or $reported_on <= $suninfo['sunset']) {
$condition = 'day';
}
else {
$condition = 'night';
}
}
}
$metar->daytime_condition = $condition;
}
/**
* Construct filename of the weather image.
*
* @param object $metar
* METAR data object, may be altered.
*/
function _weather_construct_image_filename(&$metar) {
// Is there any data available?
if (empty($metar->sky_condition)) {
$metar->image = 'nodata';
}
else {
// First part: daytime (day/night).
$image_part[] = $metar->daytime_condition;
// Next part: sky condition
// Handle special case: NSC, we just use few for the display.
if ($metar->sky_condition == 'no significant clouds') {
$image_part[] = 'few';
}
else {
$image_part[] = $metar->sky_condition;
}
// Next part: fog.
if (isset($metar->image_part['fog'])) {
$image_part[] = $metar->image_part['fog'];
}
// Next part: precipitation.
if (isset($metar->image_part['precipitation'])) {
$image_part[] = $metar->image_part['precipitation'];
}
$metar->image = implode('-', $image_part);
}
}
Functions
Name | Description |
---|---|
weather_parse_metar | Parses a raw METAR data string. |
weather_refresh_data | Parses raw METAR data string and stores results in database. |
weather_retrieve_data | Retrieve data from http://www.aviationweather.gov/ |
_weather_calculate_sunrise_sunset | Calculate the times of sunrise and sunset. |
_weather_construct_image_filename | Construct filename of the weather image. |
_weather_parse_icao | Extract ICAO code. |
_weather_parse_phenomena | Extract phenomena information. |
_weather_parse_pressure | Extract pressure information. |
_weather_parse_sky_condition | Extract sky condition information. |
_weather_parse_stop | Decide whether to stop parsing. |
_weather_parse_temperature | Extract temperature information. |
_weather_parse_timestamp | Extract timestamp. |
_weather_parse_visibility | Extract visibility information. |
_weather_parse_wind | Extract wind information. |