<?php // **** FUNCTIONS ****
function get_metar($station, &$wxInfo) {
// This function retrieves METAR information for a given station from the
// National Weather Service. It assumes that the station exists.
// A slower URL is "ftp://weather.noaa.gov/data/observations/metar/stations/$station.TXT"
$fileName = "[URL unfurl="true"]http://weather.noaa.gov/pub/data/observations/metar/stations/$station.TXT";[/URL]
$metar = '';
$fileData = @file($fileName); // or die('Data not available');
if ($fileData != false) {
list($i, $date) = each($fileData);
$utc = strtotime(trim($date));
set_time_data($utc,$wxInfo);
while (list($i, $line) = each($fileData)) {
$metar .= ' ' . trim($line);
}
$metar = trim(str_replace(' ', ' ', $metar));
}
return $metar;
}
function set_time_data($utc, &$wxInfo) {
// This function formats observation time in the local time zone of server, the
// current local time on server, and time difference since observation. $utc is a
// UNIX timestamp for Universal Coordinated Time (Greenwich Mean Time or Zulu Time).
$timeZoneOffset = date('Z');
$local = $utc + $timeZoneOffset;
$wxInfo['OBSERVED'] = date('D M j, H:i T',$local);
$now = time();
$wxInfo['NOW'] = date('D M j, H:i T',$now);
$timeDiff = floor(($now - $local) / 60);
if ($timeDiff < 91) $wxInfo['AGE'] = "$timeDiff min ago";
else {
$min = $timeDiff % 60;
if ($min < 10) $min = '0' . $min;
$wxInfo['AGE'] = floor($timeDiff / 60) . ":$min hr ago";
}
}
function process_metar($metar, &$wxInfo) {
// This function directs the examination of each group of the METAR. The problem
// with a METAR is that not all the groups have to be there. Some groups could be
// missing. Fortunately, the groups must be in a specific order. (This function
// also assumes that a METAR is well-formed, that is, no typographical mistakes.)
// This function uses a function variable to organize the sequence in which to
// decode each group. Each function checks to see if it can decode the current
// METAR part. If not, then the group pointer is advanced for the next function
// to try. If yes, the function decodes that part of the METAR and advances the
// METAR pointer and group pointer. (If the function can be called again to
// decode similar information, then the group pointer does not get advanced.)
if ($metar != '') {
$metarParts = explode(' ',$metar);
$groupName = array('get_station','get_time','get_station_type','get_wind','get_var_wind','get_visibility','get_runway','get_conditions','get_cloud_cover','get_temperature','get_altimeter');
$metarPtr = 1; // get_station identity is ignored
$group = 1;
while ($group < count($groupName)) {
$part = $metarParts[$metarPtr];
$groupName[$group]($part,$metarPtr,$group,$wxInfo); // $groupName is a function variable
}
}
else $wxInfo['ERROR'] = 'Data not available';
}
function get_station($part, &$metarPtr, &$group, &$wxInfo) {
// Ignore station code. Script assumes this matches requesting $station.
// This function is never called. It is here for completeness of documentation.
if (strlen($part) == 4 and $group == 0) {
$group++;
$metarPtr++;
}
}
function get_time($part, &$metarPtr, &$group, &$wxInfo) {
// Ignore observation time. This information is found in the first line of the NWS file.
// Format is ddhhmmZ where dd = day, hh = hours, mm = minutes in UTC time.
if (substr($part,-1) == 'Z') $metarPtr++;
$group++;
}
function get_station_type($part, &$metarPtr, &$group, &$wxInfo) {
// Ignore station type if present.
if ($part == 'AUTO' || $part == 'COR') $metarPtr++;
$group++;
}
function get_wind($part, &$metarPtr, &$group, &$wxInfo) {
// Decodes wind direction and speed information.
// Format is dddssKT where ddd = degrees from North, ss = speed, KT for knots,
// or dddssGggKT where G stands for gust and gg = gust speed. (ss or gg can be a 3-digit number.)
// KT can be replaced with MPH for meters per second or KMH for kilometers per hour.
function speed($part, $unit) {
// Convert wind speed into miles per hour.
// Some other common conversion factors (to 6 significant digits):
// 1 mi/hr = 1.15080 knots = 0.621371 km/hr = 2.23694 m/s
// 1 ft/s = 1.68781 knots = 0.911344 km/hr = 3.28084 m/s
// 1 knot = 0.539957 km/hr = 1.94384 m/s
// 1 km/hr = 1.852 knots = 3.6 m/s
// 1 m/s = 0.514444 knots = 0.277778 km/s
if ($unit == 'KT') $speed = round(1.1508 * $part); // from knots
elseif ($unit == 'MPS') $speed = round(2.23694 * $part); // from meters per second
else $speed = round(0.621371 * $part); // from km per hour
$speed = "$speed mph";
return $speed;
}
if (ereg('^([0-9G]{5,10}|VRB[0-9]{2,3})(KT|MPS|KMH)$',$part,$pieces)) {
$part = $pieces[1];
$unit = $pieces[2];
if ($part == '00000') {
$wxInfo['WIND'] = 'calm'; // no wind
}
else {
ereg('([0-9]{3}|VRB)([0-9]{2,3})G?([0-9]{2,3})?',$part,$pieces);
if ($pieces[1] == 'VRB') $direction = 'varies';
else {
$angle = (integer) $pieces[1];
$compass = array('N','NNE','NE','ENE','E','ESE','SE','SSE','S','SSW','SW','WSW','W','WNW','NW','NNW');
$direction = $compass[round($angle / 22.5) % 16];
}
if ($pieces[3] == 0) $gust = '';
else $gust = ', gusting to ' . speed($pieces[3], $unit);
$wxInfo['WIND'] = $direction . ' at ' . speed($pieces[2], $unit) . $gust;
}
$metarPtr++;
}
$group++;
}
function get_var_wind($part, &$metarPtr, &$group, &$wxInfo) {
// Ignore variable wind direction information if present.
// Format is fffVttt where V stands for varies from fff degrees to ttt degrees.
if (ereg('([0-9]{3})V([0-9]{3})',$part,$pieces)) $metarPtr++;
$group++;
}
function get_visibility($part, &$metarPtr, &$group, &$wxInfo) {
// Decodes visibility information. This function will be called a second time
// if visibility is limited to an integer mile plus a fraction part.
// Format is mmSM for mm = statute miles, or m n/dSM for m = mile and n/d = fraction of a mile,
// or just a 4-digit number nnnn (with leading zeros) for nnnn = meters.
static $integerMile = '';
if (strlen($part) == 1) { // visibility is limited to a whole mile plus a fraction part
$integerMile = $part . ' ';
$metarPtr++;
}
elseif (substr($part,-2) == 'SM') { // visibility is in miles
$part = substr($part,0,strlen($part)-2);
if (substr($part,0,1) == 'M') {
$prefix = 'less than ';
$part = substr($part, 1);
}
else $prefix = '';
if (($integerMile == '' && ereg('[/]',$part,$pieces)) || $part == '1') $unit = ' mile';
else $unit = ' miles';
$wxInfo['VISIBILITY'] = $prefix . $integerMile . $part . $unit;
$metarPtr++;
$group++;
}
elseif (substr($part,-2) == 'KM') { // unknown (Reported by NFFN in Fiji)
$metarPtr++;
$group++;
}
elseif (ereg('^([0-9]{4})$',$part,$pieces)) { // visibility is in meters
$distance = round($part/ 621.4, 1); // convert to miles
if ($distance > 5) $distance = round($distance);
if ($distance <= 1) $unit = ' mile';
else $unit = ' miles';
$wxInfo['VISIBILITY'] = $distance . $unit;
$metarPtr++;
$group++;
}
elseif ($part == 'CAVOK') { // good weather
$wxInfo['VISIBILITY'] = 'greater than 7 miles'; // or 10 km
$wxInfo['CONDITIONS'] = '';
$wxInfo['CLOUDS'] = 'clear skies';
$metarPtr++;
$group += 4; // can skip the next 3 groups
}
else {
$group++;
}
}
function get_runway($part, &$metarPtr, &$group, &$wxInfo) {
// Ignore runway information if present. Maybe called a second time.
// Format is Rrrr/vvvvFT where rrr = runway number and vvvv = visibility in feet.
if (substr($part,0,1) == 'R') $metarPtr++;
else $group++;
}
function get_conditions($part, &$metarPtr, &$group, &$wxInfo) {
// Decodes current weather conditions. This function maybe called several times
// to decode all conditions. To learn more about weather condition codes, visit section
// 12.6.8 - Present Weather Group of the Federal Meteorological Handbook No. 1 at
// [URL unfurl="true"]www.nws.noaa.gov/oso/oso1/oso12/fmh1/fmh1ch12.htm[/URL]
static $conditions = '';
static $wxCode = array(
'VC' => 'nearby',
'MI' => 'shallow',
'PR' => 'partial',
'BC' => 'patches of',
'DR' => 'low drifting',
'BL' => 'blowing',
'SH' => 'showers',
'TS' => 'thunderstorm',
'FZ' => 'freezing',
'DZ' => 'drizzle',
'RA' => 'rain',
'SN' => 'snow',
'SG' => 'snow grains',
'IC' => 'ice crystals',
'PE' => 'ice pellets',
'GR' => 'hail',
'GS' => 'small hail', // and/or snow pellets
'UP' => 'unknown',
'BR' => 'mist',
'FG' => 'fog',
'FU' => 'smoke',
'VA' => 'volcanic ash',
'DU' => 'widespread dust',
'SA' => 'sand',
'HZ' => 'haze',
'PY' => 'spray',
'PO' => 'well-developed dust/sand whirls',
'SQ' => 'squalls',
'FC' => 'funnel cloud, tornado, or waterspout',
'SS' => 'sandstorm/duststorm');
if (ereg('^(-|\+|VC)?(TS|SH|FZ|BL|DR|MI|BC|PR|RA|DZ|SN|SG|GR|GS|PE|IC|UP|BR|FG|FU|VA|DU|SA|HZ|PY|PO|SQ|FC|SS|DS)+$',$part,$pieces)) {
if (strlen($conditions) == 0) $join = '';
else $join = ' & ';
if (substr($part,0,1) == '-') {
$prefix = 'light ';
$part = substr($part,1);
}
elseif (substr($part,0,1) == '+') {
$prefix = 'heavy ';
$part = substr($part,1);
}
else $prefix = ''; // moderate conditions have no descriptor
$conditions .= $join . $prefix;
// The 'showers' code 'SH' is moved behind the next 2-letter code to make the English translation read better.
if (substr($part,0,2) == 'SH') $part = substr($part,2,2) . substr($part,0,2). substr($part, 4);
while ($code = substr($part,0,2)) {
$conditions .= $wxCode[$code] . ' ';
$part = substr($part,2);
}
$wxInfo['CONDITIONS'] = $conditions;
$metarPtr++;
}
else {
$wxInfo['CONDITIONS'] = $conditions;
$group++;
}
}
function get_cloud_cover($part, &$metarPtr, &$group, &$wxInfo) {
// Decodes cloud cover information. This function maybe called several times
// to decode all cloud layer observations. Only the last layer is saved.
// Format is SKC or CLR for clear skies, or cccnnn where ccc = 3-letter code and
// nnn = altitude of cloud layer in hundreds of feet. 'VV' seems to be used for
// very low cloud layers. (Other conversion factor: 1 m = 3.28084 ft)
static $cloudCode = array(
'SKC' => 'clear skies',
'CLR' => 'clear skies',
'FEW' => 'partly cloudy',
'SCT' => 'scattered clouds',
'BKN' => 'mostly cloudy',
'OVC' => 'overcast',
'VV' => 'vertical visibility');
if ($part == 'SKC' || $part == 'CLR') {
$wxInfo['CLOUDS'] = $cloudCode[$part];
$metarPtr++;
$group++;
}
else {
if (ereg('^([A-Z]{2,3})([0-9]{3})',$part,$pieces)) { // codes for CB and TCU are ignored
$wxInfo['CLOUDS'] = $cloudCode[$pieces[1]];
if ($pieces[1] == 'VV') {
$altitude = (integer) 100 * $pieces[2]; // units are feet
$wxInfo['CLOUDS'] .= " to $altitude ft";
}
$metarPtr++;
}
else {
$group++;
}
}
}
function get_temperature($part, &$metarPtr, &$group, &$wxInfo) {
// Decodes temperature and dew point information. Relative humidity is calculated. Also,
// depending on the temperature, Heat Index or Wind Chill Temperature is calculated.
// Format is tt/dd where tt = temperature and dd = dew point temperature. All units are
// in Celsius. A 'M' preceeding the tt or dd indicates a negative temperature. Some
// stations do not report dew point, so the format is tt/ or tt/XX.
function get_heat_index($tempF, $rh, &$wxInfo) {
// Calculate Heat Index based on temperature in F and relative humidity (65 = 65%)
if ($tempF > 79 && $rh > 39) {
$hiF = -42.379 + 2.04901523 * $tempF + 10.14333127 * $rh - 0.22475541 * $tempF * $rh;
$hiF += -0.00683783 * pow($tempF, 2) - 0.05481717 * pow($rh, 2);
$hiF += 0.00122874 * pow($tempF, 2) * $rh + 0.00085282 * $tempF * pow($rh, 2);
$hiF += -0.00000199 * pow($tempF, 2) * pow($rh, 2);
$hiF = round($hiF);
$hiC = round(($hiF - 32) / 1.8);
$wxInfo['HEAT INDEX'] = "$hiF°F ($hiC°C)";
}
}
function get_wind_chill($tempF, &$wxInfo) {
// Calculate Wind Chill Temperature based on temperature in F and
// wind speed in miles per hour
if ($tempF < 51 && $wxInfo['WIND'] != 'calm') {
$pieces = explode(' ', $wxInfo['WIND']);
$windspeed = (integer) $pieces[2]; // wind speed must be in miles per hour
if ($windspeed > 3) {
$chillF = 35.74 + 0.6215 * $tempF - 35.75 * pow($windspeed, 0.16) + 0.4275 * $tempF * pow($windspeed, 0.16);
$chillF = round($chillF);
$chillC = round(($chillF - 32) / 1.8);
$wxInfo['WIND CHILL'] = "$chillF°F ($chillC°C)";
}
}
}
if (ereg('^(M?[0-9]{2})/(M?[0-9]{2}|[X]{2})?$',$part,$pieces)) {
$tempC = (integer) strtr($pieces[1], 'M', '-');
$tempF = round(1.8 * $tempC + 32);
$wxInfo['TEMP'] = "$tempF°F ($tempC°C)";
get_wind_chill($tempF, $wxInfo);
if (strlen($pieces[2]) != 0 && $pieces[2] != 'XX') {
$dewC = (integer) strtr($pieces[2], 'M', '-');
$dewF = round(1.8 * $dewC + 32);
$wxInfo['DEWPT'] = "$dewF°F ($dewC°C)";
$rh = round(100 * pow((112 - (0.1 * $tempC) + $dewC) / (112 + (0.9 * $tempC)), 8));
$wxInfo['HUMIDITY'] = $rh . '%';
get_heat_index($tempF, $rh, $wxInfo);
}
$metarPtr++;
$group++;
}
else {
$group++;
}
}
function get_altimeter($part, &$metarPtr, &$group, &$wxInfo) {
// Decodes altimeter or barometer information.
// Format is Annnn where nnnn represents a real number as nn.nn in inches of Hg,
// or Qpppp where pppp = hectoPascals.
// Some other common conversion factors:
// 1 millibar = 1 hPa
// 1 in Hg = 0.02953 hPa
// 1 mm Hg = 25.4 in Hg = 0.750062 hPa
// 1 lb/sq in = 0.491154 in Hg = 0.014504 hPa
// 1 atm = 0.33421 in Hg = 0.0009869 hPa
if (ereg('^(A|Q)([0-9]{4})',$part,$pieces)) {
if ($pieces[1] == 'A') {
$pressureIN = substr($pieces[2],0,2) . '.' . substr($pieces[2],2); // units are inches Hg
$pressureHPA = round($pressureIN / 0.02953); // convert to hectoPascals
}
else {
$pressureHPA = (integer) $pieces[2]; // units are hectoPascals
$pressureIN = round(0.02953 * $pressureHPA,2); // convert to inches Hg
}
$wxInfo['BAROMETER'] = "$pressureHPA hPa ($pressureIN in Hg)";
$metarPtr++;
$group++;
}
else {
$group++;
}
}
function print_wxInfo($wxInfo) {
// Prints each piece of information stored in the array.
// If an error occurs in retrieving a METAR file, check for index called 'ERROR'.
// (Modify this function to fit your needs.)
$dots = '...............';
foreach($wxInfo As $wxIndex => $wx) {
if (strlen($wx) != 0) echo $wxIndex . substr($dots,0,strlen($dots)-strlen($wxIndex)) . " $wx<BR>\n";
}
}
?>