script= 'decodeMETAR_sub_funct.php', © author= SPAWS, version= 1.1.4, "; // Full version history at end /* /* =============================================================================================================================================== SPAWS Script for translating METeorological Aviation Routine (METAR) Weather Reports in the format of traditional alphanumerical characters (TAC) into British English =============================================================================================================================================== A METAR weather report consists of abbreviations, contractions, numbers, plain language, and symbols to provide a uniform means of disseminating surface weather reports. They conform to WMO code forms FM 15-X Ext. for METAR where X is a Roman format number incremented for each revision. ----------------------------------------------------------------------------------- This is the second of five scripts that make up the SPAWS decoding suite. All scripts are to be placed in same directory on your web server. Outside that suite, another script 'sourceViewC.php', whilst not essential, should be in same directory. It includes three sub-functions which are not called if the script does not exist, two of these 'add_source()' and 'display_source()' appearing on lines 55 and 64, are standard functions for enabling the displaying of PHP sources used by all SPAWS scripts, and the third sub-function 'calculateBeaufort()', is called from function novel_decode_wind() in this "decodeMETAR_sub_funct.php". Without that script, you can't see sources, and there will be less information available for decoding wind speed, but other aspects of this suite will still work fully. Thus 'sourceViewC.php' together with 'decodeMETAR.php' and this script "decodeMETAR_sub_funct.php" are the three scripts that are minimum recommended to run the decoder. The first script "decodeMETAR.php" (the top-level navigation for translating/decoding the METAR supplied as an input parameter), uses the PHP instruction "Requires_once()" to load this second script, and this second script uses "include_once("decodeMETAR_Rmk.php");" to optionally load the decoding for the Remarks Group, and in turn that script will optionally load the two remaining scripts in the decoding suite. This second script is entirely sub-functions, all of which are called either from the first script, or from another sub-function in this script, or from one of the scripts dealing with the Remarks Group. This script also decodes Colour State codes, this is treated by the script as if it is a separate Group, although actually the Colour State is represented by a single segment added to end of one of three possible standard Groups - either the WMO standard Cloud and Visibility Groups, the WMO standard Trend Group or the Remarks Group. ------------------------------------------------------------------------------------------------------------------------------------------------------------------ A lot of work has gone into development and (even more work involving finding over a thousand METAR reports) for testing this particular script, consequently the intellectual ownership of this suite resides with the operator of a Simple Personal Automatic Weather Station (SPAWS), who does not give away any rights for anybody else to gain financially, in any way, directly or in-directly, from their use of this suite. ----------------------------------------------------------------------------------------------------------------- © Script Editor SPAWS: This work is licensed under the Creative Commons Attribution-NonCommercial 4.0 International License. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. Permission will be given for others to use this script on amateur weather sites, or to incorporate parts in their own freely distributed scripts used on such amateur sites, provided due acknowledgement is made to the work put in by SPAWS, just as SPAWS pays acknowledgement in this script to snippets written by other programmers that have inspired coding here or even have parts of their work incorporated in this script. The other decoders that were examined are listed in the accompanying Portable Document Format paper "METAR". Where there is some similarity, between code in another decoder and code in this script, the source from the other script has been acknowledged, and in some cases even listed, in an adjacent comment. ----------------------------------------------------------------------------------------------------------------- */ # An optional code snippet (for PHP source displaying), remove if you want! #--------------------------------------------------------------------------# # Just list the PHP source? Start of common SPAWS snippet. # # Modify URL of calling web page by adding a query-string such as # # ?viewSource='xxxxx'" or "?src='xxxxx'" or "?sce='xxxxx'" # # to see source for any file xxxxx.php whether web page or included file # #--------------------------------------------------------------------------# $home = substr(__FILE__ , 0,14); if(file_exists('sourceViewC.php')) include_once 'sourceViewC.php'; elseif(file_exists($home . 'forbidden\sourceView.php')) include_once $home . 'forbidden\sourceView.php'; /* 'require' does a fatal error if the requested file is not found, rest of code ignored; 'include' issues a warning if the requested file is not found, rest of HTML obeyed; 'require_once' and 'include_once' have same file not found action, but the once clause ensures that only one attempt is made to load the procedure. The once option is used here because some variables declared in the procedure may be used in this script, and such variables cannot have multiple declarations of them. */ # IMPORTANT - This included script provides functions 'calculateBeaufort', 'calculateTime($timeStamp)', 'add_source' and 'display_source' # but this script tests for their existence and will not fail if they are not present. if(function_exists('add_source')) add_source(__FILE__, basename(__FILE__, '.php')); # NB use of this magic constant represents the file in which it appears if( // $_GET only returns parameters as part of a GET REQUEST, this is the one where (when you start (re-)loading the file), you read information from the query-string // $_POST only returns parameters as part of a POST request, this is when a form has been completed its contents are kept in HTTP for when page reloaded // $_REQUEST will return parameters found in COOKIE, GET or POST isset($_GET['viewSource']) && $_GET['viewSource'] == basename(__FILE__, ".php") or isset($_GET['sce']) && $_REQUEST['sce'] == basename(__FILE__, '.php') or isset($_REQUEST['src']) && $_REQUEST['src'] == basename(__FILE__, '.php') ){ if(function_exists('display_source')) display_source (basename(__FILE__, '.php')); } #-----------------------------------------------------------# # End of common snippet to list source call # #-----------------------------------------------------------# //---------------------------------------------------------------- // The sub-functions that follow are called from the sub-functions // in 'decodeMETAR.php' directly or are called by another one of the sub-functions below. // For each sub-function, there are notes explaining origin of that code sequence. //---------------------------------------------------------------- function mtr_speed($part, $unit) // apart from function name, exactly as original function by Mark Woodward, retained purely for Saratoga compatibility { // Convert wind speed into miles per hour. 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 return $speed; } /* // original function by Mark Woodward for USA conversions follows: function speed($part, $unit) { // Convert wind speed into miles per hour. 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; } // 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 */ //------------------------------------------------------------------------- ################################################################################################################################################################# # Novel function written by SPAWS to decode surface wind information. Decodes wind direction and speed information specified in first parameter. # # Designed looking at ICAO Annex 3, WMO METAR form, UK Civil Aviation Authority chapter 4 in 'CAP 746' rules, and Federal Meteorological Handbook # # No. 1 (FCM-H1-2017) standard, resulting in much original coding, but still influenced heavily by Ken True's code for some output compatibility. # # # # 1. For Surface Wind Group called directly from main function with second parameter set to 'swg'. Applies only to average since last METAR. # # # # 2. For Change Group called directly from main function with second parameter set to 'change'. Applies to forecast speed after METAR issued. # # # # For both the above groups, allowed formats are 'cccccUU(U)' for calm, 'dddss(s)UU(U)' if not reporting gusts, or 'dddss(s)Ggg(g)UU(U)' if reporting # # gust speed. However, some operators include a slash after 'ddd' i.e. 'ddd/'. For all formats, the key to their components is: ccccc = '00000' # # for calm (no speed nor direction), ddd = degrees from North, ss = average speed last 10 minutes, G stands for gust and gg = gust speed, (brackets # # indicate ss and gg can be a 2 or 3-digit number), UU(U) is a two or three character abbreviation for units. WMO say units should be MPS for metres per # # second. ICAO say units should be KMH for kilometres per hour. WMO from June 2011 removed ability to use KMH! # # UK and USA say units should be KT for knots. # # Again, in some METAR reports there is carelessness by operators and the 'K' can be omitted causing another format of 'dddss(s)T' for this script. # # # # 3. For Peak Wind Report called from sub-function new_get_remarks with second parameter set to false. Format is dddff(f)/(hh)mm. # # # # This last format only used by various nations within Remarks Group - e.g. USA paragraph 12.7.1 (d). For this format, the key to the components is: # # ddd = degrees from North, ff(f) = 2 or 3 digit peak wind speed, hh = hour (if not inferred from report time), mm = minutes past hour. # ################################################################################################################################################################# function novel_decode_wind($part, $call) { global $metar_toDecode, $decodeInfo, $decodeGroupCount, $decodeGroupProcessed, $show_diagnostics, $show_diagnosticsR, $home, $compass, $compass_expanded; $output['matched'] = false; // not yet matched $part to any recognised format if($show_diagnostics or (isset($show_diagnosticsR) and $show_diagnosticsR and isset($decodeGroupProcessed['REMARK'][0]))) { echo "
---- Starting 'novel_decode_wind' function within 'decodeMETAR_sub_funct.php' ---- examining segment containing " . $part; echo "
---- novel_decode_wind function ---- requested for ". $call . "
"; } if (strpos($call, 'Peak wind') !== false) { if(isset($show_diagnosticsR) and $show_diagnosticsR) echo '
Remarks Group - Peak Wind specification'; // Called from USA Peak Wind Specification 4 : 12.7.1 Automated, Manual, and Plain Language Remarks. d. Peak Wind // (or New Zealand Peak Wind?) // 2017/12/12 21:55 METAR NZFX 122155Z 18020KT 0800 -SN BLSN SCT010 BKN020 BKN060 M04/M07 A2927 RMK PK WND GRID32028/44 SLP910 WND DATA ESTMD GRID35020KT SDN/HDN LAST # This code snippet for peak wind report is totally original coding by SPAWS, with no influences from other people ////////////////////////////////// // remarks - peak wind report // ////////////////////////////////// // Federal Meteorological Handbook No. 1 - paragraph 12.7.1 Automated, Manual, and Plain Language Remarks. d) Peak Wind (PK_WND_dddff(f)/(hh)mm) if(preg_match('/([G][R][I][D])?([0-3][0-9][0])([0-9]{2,3})([\/])([0,1,2][0-9])?([0-5][0-9])$/',$part,$pieces) === 1) // NB Bearing is to nearest 10 degrees // NB speed can be 2 or 3 digits (knots), // mandatory slash // hour portion UTC optional // final portion is time in minutes { if($show_diagnostics or (isset($show_diagnosticsR) and $show_diagnosticsR)) print_array($pieces, 'peak wind'); # --------------------------- # Is it reported as GRID direction? # --------------------------- $grid = false; if(strlen($pieces[1])) { $grid = true; $output['grid'] = "As it is North in all directions from the South Pole, a Grid Direction is used as an alternative to the conventional direction.   "; $output['grid'] .= "In the Grid Direction, North is only used if pointing towards Greenwich along the Meridian Line, East is used if the longitude is 90° to the right,"; $output['grid'] .= " South is used for a longitude of 180°, and West is used for a longitude of 270° from the Greenwich Meridian."; } # --------------------------- # Extract peak wind direction # --------------------------- $output['bearing'] = (integer) $pieces[2]; $output['direction'] = $compass_expanded[$compass[1 + round($output['bearing'] / 22.5) % 16]]; if($grid) $output['direction'] .= " (Grid direction)"; $output['bearing'] .= " °"; if($grid) $output['bearing'] .= " longitude"; if($show_diagnostics or (isset($show_diagnosticsR) and $show_diagnosticsR /* and isset($decodeGroupProcessed['REMARK'][0])*/ )) echo "
---- novel_decode_wind function ---- peak wind bearing is " . $output['bearing']; # ---------------------------------------- # Extract peak wind speed in various units # ---------------------------------------- $output['knots'] = 1 * $pieces[3]; $output['mps'] = round (0.1544 * $pieces[3]); $output['mph'] = round (1.1508 * $pieces[2]); if($show_diagnostics or (isset($show_diagnosticsR) and $show_diagnosticsR /* and isset($decodeGroupProcessed['REMARK'][0])*/ )) echo "
---- novel_decode_wind function ---- peak wind speed is " . $output['knots'] . " knots"; # ------------------------- # Extract time of peak wind # Format suits function adjustTime() # ------------------------- if(strlen($pieces[5]) > 1) { $timeArray = adjustTime($pieces[5] . $pieces[6]); if($show_diagnostics or (isset($show_diagnosticsR) and $show_diagnosticsR and isset($decodeGroupProcessed['REMARK'][0]))) echo "
---- novel_decode_wind function ---- peak wind hour=" . $pieces[4]; $output['time'] = $timeArray['hour'] . ":" . $timeArray['minute'] . " (local time)" ; }else{ $timeArray = adjustTime("99" . $pieces[6]); //echo "no hour"; $output['time'] = $timeArray['minute'] . " minutes past current hour (local time)"; } if($show_diagnostics or (isset($show_diagnosticsR) and $show_diagnosticsR and isset($decodeGroupProcessed['REMARK'][0]))) echo "
---- novel_decode_wind function ---- peak wind minutes=" . $pieces[6]; } return $output; }else{ /////////////////////////////////// // average surface wind report // // and // // change forecast surface wind // // and // // runway wind speed (RMK Group) // // and // // other surface wind (RMK Group)// /////////////////////////////////// // Called from "Surface Wind Group" (WMO standard or New Zealand alternative) or called from "Change Group" or called from Italian wind at specific location if($show_diagnostics) { echo "
---- novel_decode_wind function [bearing, speed and gust] ---- " . $call . "
"; switch($call) { case 'swg': echo "WMO standard - Surface Wind Group"; break; case 'change': echo "WMO supplementary - Change Group"; break; case 'decode_ITA_remarks - Location specific wind': echo "decode_ITA_remarks - Runway Wind specification"; break; case 'decode_NZ_remarks': echo "decode_NZ_remarks - alternative Surface Wind report"; break; case "decode_NZ_remarks - wind": echo "decode_NZ_remarks - Grid Surface Wind report"; break; } } switch($part) // WMO regulations format is 'cccccUU(U)' i.e. that 2 or 3 letter code for units must follow those five zeroes without a space. { # Use of Switch condition above makes this code snippet for 'calm' into totally original coding by SPAWS, // calm case '00000MPS': case '00000KT': // the Saratoga "My_Files\Me\Downloads\metar-display package v1 23 May 2016\get-metar-conditions-inc.php" version uses a less efficient regular expression approach if($show_diagnostics) echo "
---- novel_decode_wind function ---- calm "; $output['mph'] = $output['speed'] = 'calm'; // no wind $output['knots'] = 0; $output['bearing'] = ""; $output['direction'] = 'calm'; $output['matched'] = true; // $part matched to recognised format and decoded if(file_exists("../gauges_images/beaufort_0.png")) $output['house'] = "../gauges_images/beaufort_0.png"; return $output; // not calm default: # This code snippet for 'not calm' by SPAWS uses a different regular expression, # although otherwise this an approach like Saratoga "My_Files\Me\Downloads\metar-display package v1 23 May 2016\get-metar-conditions-inc.php" version if(preg_match('/([0-3][0-9]{2}|VRB)[\/]?([0-9]{2,3})G?([0-9]{2,3})?(T|KT|MPS|KMH)$/',$part,$pieces) === 1) /* Included 'T' as possible unit after malformed METAR seen from Cranfield (extracts from failure to decode log follows): 2017/07/18 16:50 METAR EGTC 181650Z 100013T CAVOK 25/10 Q1013 Unknown=100013T 2017/07/19 06:20 METAR EGTC 190620Z 08010T 7000 FEW005 18/17 Q1006 Unknown=08010T */ { if($show_diagnostics) echo "
---- novel_decode_wind function ---- not calm "; # ---------------------- # Extract wind direction # - Degrees and Compass # ---------------------- if ($pieces[1] == 'VRB') // Canada - some automatic stations do not use 'VRB' { # This code snippet for variable wind direction is totally original coding by SPAWS, with no compatibility to Saratoga output // WMO say wind direction can be coded as 'VRB' if speed is less than 1.5 m per second or 3 knots, // (WMO regulations changed this in June 2011 from previous threshold of 2 metres per second) // WMO say if speed is 1.5 metres per second or more and direction varies by 60 degrees or more, // report variability using conditional 'Wind Bearing Extremes Group' as coded in main function if($show_diagnostics) echo "
---- novel_decode_wind function ---- variable direction"; $output['direction'] = 'VRB'; $output['bearing'] = 'Direction varies '; $output['matched'] = true; // $part matched to recognised format and decoded }else{ # This code snippet for 'wind direction' by SPAWS uses different names, # although otherwise approach same as Saratoga in "My_Files\Me\Downloads\metar-display package v1 23 May 2016\get-metar-conditions-inc.php" version $output['bearing'] = (integer) $pieces[1]; $directionIndex = 1 + round($output['bearing'] / 22.5) % 16; if($show_diagnostics) echo "
---- novel_decode_wind function ---- steady direction " . $compass[$directionIndex]; if($call == 'swg' or substr($call, -3) == 'AIR') $output['direction'] = $compass[$directionIndex]; // short compass direction needed for graphic in surface wind group else $output['direction'] = $compass_expanded[$compass[$directionIndex]];// long compass direction otherwise $output['bearing'] .= " ° "; $output['matched'] = true; // $part matched to recognised format and decoded } # ---------------------- # Extract wind speed # - average and gust # ---------------------- if (!array_key_exists(3, $pieces) or (array_key_exists(3, $pieces) && $pieces[3] == 0 ) ) { $output['gust'] = '';// This empty string is required by Saratoga scripts $output['mphGust'] = ''; } if($show_diagnostics) print_array($pieces, 'novel_decode_wind function'); // average is over 10 minutes in WMO specification, but over just 2 minutes in USA standard, averaging period is 2 minutes for Canada and some places in Estonia // wind gust reported is always maximum instantaneous speed in last 10 minutes switch($pieces[4]) // WMO did previously leave choice of units to national decision, but this was deleted from their regulations in June 2011. // WMO standard of FM 15-XIV (effective June 2011) says can use knots only until a yet to be agreed termination date, // making metres per second their preferred unit. Meanwhile ICAO continued to accept knots (international or UK versions) with no termination date. // In 2013, WMO aligned its standard with that of ICAO, as that is a later date so I'm assuming that knots are an acceptable unit for future and // that is why using Knots is specified in UK CAA rules (March 2017) paragraph 4.8, and in USA FAA (September 2017) standard paragraph 12.6.5 // Canada according to WMO (volume 2 page 10) use (international?) nautical miles per hour (Knots) { case 'KT': // ICAO say that Knots may be used, as an alternative unit to their (July 2007) preference of km per hour; # METAR example from King Mswati III International Airport, Swaziland: FDSK 011200Z 04015KT 9999 FEW035 26/16 Q1018 NOSIG $output['knots'] = 1 * $pieces[2]; if(substr($decodeGroupProcessed['AIG'], 0, 2) == 'EG' and !in_array('RMK', $metar_toDecode)) { $output['knots'] .= ' UK'; // If UK stations, convert larger UK knots to smaller International knots $pieces[2] *= 0.9993610995993922; } $output['mps'] = round(0.1544 * $pieces[2], 1); $output['mph'] = round(1.1508 * $pieces[2], 1); $output['kph'] = round(1.852 * $pieces[2]); // ICAO conversion figure - outside UK - 16 Feb 2018 if(array_key_exists(3, $pieces) and $pieces[3] > 0) { // 2017/11/23 16:00 METAR CWGD 231600Z AUTO 23012G17KT 02/M02 RMK AO1 SOG 00 6016 PK WND 22019/1501 SLP165 T00241018 $output['knotsGust'] = 1 * $pieces[3]; if(substr($decodeGroupProcessed['AIG'], 0, 2) == 'EG' and !in_array('RMK', $metar_toDecode)) { $output['knotsGust'] .= ' UK'; // If UK stations, convert larger UK knots to smaller International knots $pieces[3] *= 0.9993610995993922; } $output['mpsGust'] = round(0.1544 * $pieces[3], 1); $output['mphGust'] = round(1.1508 * $pieces[3], 1); $output['kphGust'] = round(1.852 * $pieces[3]); // ICAO conversion figure - outside UK - 16 Feb 2018 } $output['speed'] = $pieces[2] . " knots"; // This array element is required by Saratoga scripts break; case 'MPS': // WMO regulations say that metres per second is the primary unit to be used for wind speeds in the two Groups. # Example from Urum-Qi Diwopu International Airport, China: METAR ZWWW 011230Z 05001MPS CAVOK 09/02 Q1030 NOSIG # note units for wind speed $output['mps'] = 1 * $pieces[2]; $output['knots'] = round(1.9440122 * $pieces[2]); $output['mph'] = round(2.23694 * $pieces[2]); $output['kph'] = round(3.6 * $pieces[2]); if(array_key_exists(3, $pieces) and $pieces[3] > 0) { $output['mpsGust'] = 1 * $pieces[3]; $output['knotsGust'] = round(1.9440122 * $pieces[3]); $output['mphGust'] = round(2.23694 * $pieces[3]); $output['kphGust'] = round(3.6 * $pieces[3]); // ICAO conversion figure - outside UK - 16 Feb 2018 } $output['speed'] = $pieces[2] . " m s-1"; break; case 'KMH': // Not currently expected because in WMO standard of FM 15 XIV (agreed June 2011) there was proposal to delete use of KMH (for km per hour). // ICAO say (July 2007) preference of km per hour; $output['kph'] = 1 * $pieces[2]; $output['mph'] = round(0.621427 * $pieces[2]); $output['knots'] = round($pieces[2] / 1.852); // ICAO conversion figure - outside UK - 16 Feb 2018 $output['mps'] = round($pieces[2] * 0.277778, 1); if(array_key_exists(3, $pieces) and $pieces[3] > 0) { $output['kphGust'] = 1 * $pieces[3]; $output['knotsGust'] = round($pieces[3] / 1.852); // ICAO conversion figure - outside UK - 16 Feb 2018 $output['mphGust'] = round(0.621427 * $pieces[3]); $output['mpsGust'] = round($pieces[3] * 0.277778, 1); } $output['speed'] = $pieces[2] . " km hr-1"; break; default: $output['wind'] = "ERROR re UNITS"; $output['speed'] = "Assumed " . $pieces[2] . " knots"; } if(isset($output['knots']) ) { // ------------------------------------------------ # these two lines are addition by SPAWS to select drawing # IMPORTANT - If you don't have the external script 'sourceViewC.php', you won't have function 'calculateBeaufort' if(function_exists('calculateBeaufort')) { $beaufort_force = calculateBeaufort($output['knots'],'kts')[1]; if(file_exists("../gauges_images/beaufort_" . $beaufort_force . ".png")) $output['house'] = "../gauges_images/beaufort_" . $beaufort_force . ".png"; if(file_exists("gauges_images/beaufort_" . $beaufort_force . ".png")) $output['house'] = "gauges_images/beaufort_" . $beaufort_force . ".png"; } } # ----------------------------------- # Compatibility with Saratoga scripts # ----------------------------------- if (array_key_exists(3, $pieces) and $pieces[3] != 0 ) { if($show_diagnostics) echo '
---- novel_decode_wind function ---- wind gust=' . $output['knotsGust'] . ' knots
'; $output['gust'] = " Gust " . (1*$pieces[3]) . ' ' . strtolower($pieces[4]) . ', ' . mtr_speed($pieces[3], $pieces[4]); } } } if($output['matched'] and $call == "svg") $output['wind'] = $output['direction'] . ' at ' . mtr_speed( $output['knots'], $pieces[4] ) . $output['gust']; // Used for Ken True's "Downloads\metar-display package v1 23 May 2016\include-metar-display-insert.php" //------------------------------------------------- } # --------------------------- # ['wind'], ['gust'] - complex strings for Saratoga script compatibility # --------------------------- # Extract direction as elements of returning array # ['direction'] - variable or calm or compass # ['bearing'] - direction varies or angle with unit of degrees suffix # --------------------------- # Extract average wind speed as elements of returning array: # ['speed'] - for text content # ['knots'] - traditional reporting unit # ['mph'] - for external scripts # ['mps'] - WMO preferred units (metres per second) # ['house'] - Beaufort force image # ['kph'] - alternative units (km per hour) # --------------------------- # Extract (optional) gust wind speed as elements of returning array: # ['knotsGust'] - traditional reporting unit # ['mphGust'] - for external scripts # ['mpsGust'] - WMO preferred units (metres per second) # ['kphGust'] - alternative units (km per hour) # --------------------------- return $output; } //---------------------------------------------------------------- // The sub-functions that follow are called from the sub-functions // in 'decodeMETAR.php' directly or are called by another one of the sub-functions below. // For each sub-function, there are notes explaining origin of that code sequence. //---------------------------------------------------------------- ################################################################################################################################################## # Novel (and working) function written by SPAWS to decode Prevailing and Minimum Visibility Groups. Works with several formats: # # Two parameters - First segment is normally 'Prevailing Visibility', it is mandatory if not preceded by 'CAVOK Group'. # # - Second segment for N. America can be continuation of first, as fractions consume two segments! # # - Generally, internationally, the second segment is 'Minimum Directional Visibility', with a compass direction # # included either at the end of that second segment, or in a third segment. # # - However, a few countries report the Minimum Directional Visibility before the Prevailing Visibility! # # # # Standard international format for 'Prevailing Visibility' is optional 'M', then 4 digits for distance in metres with leading zeroes. # # Allowed variants: Use 1 or 2 digits to represent distance as integer followed by two characters describing the units. # # (In USA/Canada can have fractional part requires space between integer and fraction, hence consumes 2 segments i.e. both parameters). # # (Units used in USA and Canada are 'SM' for American miles, other units seen in Fiji and other countries are 'KM' for kilometres). # # # # Standard international format for 'Minimum Visibility' is 4 digits representing distance in metres followed by 'N', 'NE', 'E'... # # # # Also decodes (Canada only) Variable visibility in RMK where two figures (possibly using fractions) are separated by a hyphen. # # Also decodes (Italy only) Minimum Visibility in RMK Group with standard 4 digit visibility. # ################################################################################################################################################## function novel_allVisibility($partLocal1, $partLocal2 = "", $partLocal3 = "", $spec) { global $metar_toDecode, $decodeInfo, $decodeGroupCount, $decodeGroupProcessed, $compass, $compass_expanded; // arrays declared at start of this script global $faa2017, $show_diagnostics, $show_diagnosticsR, $dM_Rmk; // shared with other scripts if($show_diagnostics or $show_diagnosticsR) { echo "
~~~~~ Sub-function 'novel_allVisibility' ~~~~~~ within 'decodeMETAR_sub_funct.php' begins with passed in parameters of
segments " . $partLocal1; if(strlen($partLocal2)) echo " and " . $partLocal2; if(strlen($partLocal3)) echo " and " . $partLocal3; echo " for specification of " . $spec . '
'; } // Initialise with what to return if not recognised as specifying visibility $output['match'] = false; // nothing matched $output['pV_match'] = false; $output['segments'] = 0; // no "to decode" segments recognised yet by this sub-function /* ICAO Amendment 73 (WMO FM 15-XII Ext. METAR) applicable from 25 November 2004 changed requirement to report "Minimum Visibility" into requirement to report "Prevailing Visibility". REGIONAL STANDARDS: Australian BOM use metres, they always report prevailing visibility, but can also report minimum visibility by using second segment e.g. '9000 2000N' means prevailing 9km, but 2km towards North. Canada - Environment and Climate in Canada ManObs paragraph 16.3.6 - Visibility shall be reported at land stations in statute miles (SM), and at ocean stations in nautical miles. This decoder only accepts SM as it is unclear whether the format for nautical miles still uses same suffix of SM or changes suffix to NM. Presumably Canada uses USA (aka international) nautical miles as these are shorter than UK nautical miles. Every site that produces a METAR in Canada was checked on the official Canadian page, none of them seemed to actually be Ocean stations. None of the thousands of METAR inspected have included a 'NM' suffix, but that does not answer the question. Confusingly, Canada also reports variable visibility in some cases, for specification 16.3.6.3 (ii) Variable visibility it uses statute miles, and there is another visibility 2) General aviation remarks. b) 16.3.13.2.2 Visibility Remarks (v) 10.2.19.3 Visibility (Remarks) that uses hundreds of feet as the units, as it is a vertical visibility. UK CAA METAR Coding Rules paragraphs 4.25 to 4.37 and 4.164 - units used are metres and prevailing visibility is officially always given as a 4 digit integer, like Australia, UK can report minimum directional visibility in next segment. USA - visibility reported in both mandatory standard 'Prevailing Visibility' Group and in various Groups within the Reamarks Group The units used in most places are statute miles according to FAA standard handbook in both chapters 6 (whole chapter deals with Visibility) and 12 (paragraphs 12.4 a. (6) and 12.6.6). Confusingly, if the automatic prevailing visibility sensor fails, USA observers report the prevailing visibility in metres, not the units specified in their handbook! However, this manual (observer) report using metres appears in the Remarks Group, not the WMO Standard Prevailing Visibility Group, in the raw METAR, although I report both in the same output element. Statute miles are also specified in the description of the content of a METAR found at http://www.met.tamu.edu/class/metar/metar-pg7-vis.html Automatic-generated reporting is to the nearest quarter mile and manual (observer) reporting is to nearest sixteenth of a mile according to handbook table 6.1. For visibilities of 3 miles to 15 miles, reporting is to nearest mile (although some automatic stations don't report 6, 8, or 9) and above that to nearest 5 miles. To specify a number (below 3) with a fractional part a space is inserted between the whole integer and any fractional part, thus need to pass two of the three segments in parameters passed to this function onto the "statute_miles" sub-function so it has access to both the whole and the fractional parts of a single figure. For example, a visibility of one and a half statute miles would be coded '1 1/2SM'. Also the number can be prefixed with 'M' to indicate 'less than'. USA also reports variable visibility in some cases, when reported it appears in the Remarks Group of the METAR, the USA never uses the WMO Minimum Visibility Group. Note the variable visibility still uses statute miles as the units, but without the letters 'SM' appearing. In this case, all three of the segments passed to this sub-function are interpreted as there are both minimum and maximum visibilities and each may have whole and fractional parts. For example, a visibility that was varying between 1 and 2 statute miles would be coded VIS 1/2V2 and one varying between 1 3/4 and 2 1/2 statute miles would be coded as "VIS 1 3/4V2 1/2". */ // Canada ####################################################################################################################################################### # Novel script snippet written by SPAWS to produce a single output from decoding the one or more numerical segments in the Remarks Group. # # For Canada the CANADA MANOBS: 16.3.6.3 Variable visibility specification in case 7 uses 3 or 4 segments processed: e.g. "VIS VRB 3/4-1 1/4" # # The 'VIS' and the 'VRB' are not passed to this sub-function, it only deals with the numbers separated by the hyphen # # It uses 1 or 2 segments for the min. and max. separated by a '-', the only usage of a hyphen that I have found in all the METAR I have seen. # ####################################################################################################################################################### /* If the prevailing visibility is observed to be fluctuating rapidly and increasing and decreasing from a mean value by 1/4 or more of the mean value, the range of variation shall be entered in Remarks beginning with the lowest visibility value. The mean value shall be entered as the prevailing visibility in the WMO standard Group. Example (1): Prevailing visibility of 1 SM is varying between 3/4 and 1 1/4 SM. METAR CYHZ 241800Z 35009KT 1SM BR OVC008 16/14 A2986 RMK SF8 VIS VRB 3/4-1 1/4 SLP112 Example (2): Prevailing visibility of 3 SM is varying between 1 SM and 5 SM. METAR CYTS 251800Z 06010G25KT 3SM BLSN SKC M12/M15 A3041 RMK VIS VRB 1-5 SLP311 */ if($spec == 'VRB') { $output['count'] = 3; ####################################################################################################################################################### # Unfortunately sometimes there is an extra space inserted before and after the hyphen, so my script has to then merge into correct format. # if($partLocal2 = '-') $partLocal1 .= '-' . $partLocal3; $output['count'] += 2; $partLocal2 = $partLocal3 = null; ####################################################################################################################################################### if(strpos($partLocal1,"-") !== false) // segment has whole number or fraction before hyphen containing minimum visibility { $first = substr($partLocal1, 0, strpos($partLocal1, "V")); $reply = statuteMiles($first, ''); if($show_diagnosticsR) print_array($reply,"C1-min"); $output['min'] = $reply['value'] . " statute miles"; if($show_diagnostics) echo "
minimum visibility = " . $output['min'] . "
"; $whole = substr($partLocal1, 1 + strpos($partLocal1, "V")); $fraction = $partLocal2; if(strpos($whole, '/')) { $whole = substr($whole, 0, 1); $fraction = substr($whole, 1); } $reply = statuteMiles($whole, $fraction); if($show_diagnostics) print_array($reply,"C1-max"); if(!array_key_exists(1, $reply)) $output['max'] = $reply[0] . " statute miles"; else{ $output['count']++; $output['max'] = $reply[1] . " statute miles"; } $output['vV_match'] = true; // successfully matched }elseif(strpos($partLocal2,"-") !== false) // minimum visibility has whole part in first segment and a fractional part before hyphen in second segment { $output['count']++; $fraction = substr($partLocal2, 0, strpos($partLocal2,"V")); $reply = statuteMiles($partLocal1, $fraction); if($show_diagnosticsR) print_array($reply,"C2-min"); $output['min'] = $reply['value']; $whole = substr($partLocal2, 1 + strpos($partLocal2,"V")); if(strpos($partLocal3, '/')) $fraction = $partLocal3; else $fraction = null; $reply = statuteMiles($whole, $fraction); if($show_diagnostics) print_array($reply,"C2-max"); if(!array_key_exists(1, $reply)) $output['max'] = $reply[0]; else{ $output['count']++; $output['max'] = $reply[1] . " statute miles"; } $output['vV_match'] = true; // successfully matched } return $output; // no more processing } if(in_array($partLocal1, $compass) and statuteMiles($partLocal2, $partLocal3) !== false) { /* Canada examples: MANOBS: 16.3.6.2 Sector visibilities Sector Visibilities that are half or less, or double or more of the prevailing visibility shall be reported in Remarks. Example (1): The prevailing visibility is 15 SM; visibility to the north quadrant is observed to be 3 SM. METAR CYTH 241800Z 13017KT 15SM FEW020 FEW220 15/07 A3011 RMK SF1CI1 VIS N 3 SLP205 Example (2): The prevailing visibility is 3 SM; visibility to the south quadrant is observed to be 6 SM. METAR CYGK 201600Z 11003KT 3SM BR FEW020 08/07 A2948 RMK FG1SC1 VIS S 6 SLP980 */ $output['mdv'] = "Minimum sector visibility of " . statuteMiles($partLocal2, $partLocal3) . " statute miles towards "; $output['mdv'] .= $compass_expanded[$partLocal1]; $output['dV_match'] = true; // successfully matched minimum sector directional visibility $output['segments'] = (strpos($partLocal3, '/') !== false) ? 3 : 2; return $output; // no more processing } /* WMO regulations volume 1 paragraph 15.6.2 - additional Minimum Directional Visibility Group to be used only ... ... When the horizontal visibility is not the same in different directions and preceding Group reports prevailing visibility, when the minimum directional visibility is different from the prevailing visibility, and less than 1 500 metres or less than 50% of the prevailing visibility, and less than 5 000 metres UK CAP 746: pages 31-32: paragraph 4.25: VVVV = prevailing meteorological visibility VNVNVNVN = minimum meteorological visibility DV = one or two letters indicating one of the eight points of the compass that best describes the direction of the meteorological visibility, relative to the aerodrome meteorological observer's station. 4.26 Prevailing visibility is defined as the greatest visibility value that is reached within at least half the horizon circle or within at least half of the surface of the aerodrome. These areas could comprise contiguous or non- contiguous sectors. 4.27 In the METAR, the visibility reported is the prevailing visibility and, under certain circumstances, the minimum visibility. In order to determine the prevailing visibility and any requirement to report the minimum visibility, the variation of visibility in all directions around the aerodrome should be considered. 4.28 The visibility reported in the METAR should be assessed at a height of about 1.5 m above the ground at the observing site. Observers should be aware of possible errors generated by reporting slant visibility when meteorological visibility is assessed at heights greater than 1.5 m above the ground. 4.29 If the visibility in one direction which is not the prevailing visibility, is less than 1500 m or less than 50% of the prevailing visibility, the lowest visibility observed should be reported after the prevailing visibility and its general direction in relation to the aerodrome indicated by reference to one of the eight points of the compass. If the lowest visibility is observed in more than one direction, then the most operationally significant direction should be reported. When the visibility is fluctuating rapidly and the prevailing visibility cannot be determined, only the lowest visibility should be reported, with no indication of direction. 4.30 There is no requirement to report the lowest visibility if it is 10 km or more. USA Federal Reporting Handbook number 1 (November 2017 edition) 6.5 Visibility Reporting Standards 6.5.1 Unit of Measure. Visibility shall be reported in statute miles. 6.5.2 Prevailing Visibility. Prevailing visibility shall be reported in all weather observations. The reportable values for visibility are listed in Table 6-1. If the actual visibility falls halfway between two reportable values, the lower value shall be reported (see paragraph 12.6.6). 6.5.3 Variable Prevailing Visibility. Variable prevailing visibility shall be reported if the prevailing visibility is less than 3 miles and rapidly increases or decreases by 1/2 statute mile or more during the time of observation. The minimum and maximum visibility values observed shall be reported in remarks section (see paragraph 12.7.1.g). 6.5.4 Tower Visibility. Tower visibility shall be reported, in accordance with agency procedures (see paragraph 12.7.1.f). 6.5.5 Surface Visibility. Surface visibility shall be the prevailing visibility from the surface at manual stations or the visibility derived from sensors at automated stations (see paragraph 12.7.1.f). 6.5.6 Visibility at Second Location. When an automated station uses a meteorological discontinuity visibility sensor, remarks shall be added to identify visibility at the second location which differ from the visibility in the body of the report (see paragraph 12.7.1.i). 6.5.7 Sector Visibility. Sector visibility shall be reported in remarks when it differs from the prevailing visibility by one or more reportable values and either the prevailing or sector visibility is less than 3 miles. WMO volume II (my) page 10 - Canada does not report direction for minimum visibility. Canada MANOBS: 16.3.6 Prevailing visibility (VVVVSM) The prevailing visibility shall be reported in statute miles and fractions of statute miles followed by the letters "SM" to indicate units. If the observed prevailing visibility is between two reportable values, the "lower" value shall be used. */ re_start: // don't know if minimal or prevailing comes first in WMO standard Groups if(strlen($partLocal1) > 4 and is_numeric(substr($partLocal1, 0, 4)) and in_array(substr($partLocal1, 4), $compass)) { //////////////////////////////////////////////////////////////////////// // a) Minimum Directional Visibility Group (International metric) // // Either 4 digits for units of metres, // // Or 2 digits then 'KM' for units of kilometres. // // Ends with compass direction in same segment // //////////////////////////////////////////////////////////////////////// # An example from Marcos A. Gelabert, Panama where minimum directional comes before prevailing visibility - not in accordance with WMO regulations # 2017/12/10 20:20 METAR MPMG 102020Z 32006KT 280V360 2000E 7000 TSRA FEW008 SCT015CB BKN080 26/24 Q1010 RWY WET TEMPO 5000 if($show_diagnostics) echo '
WMO Minimum Directional Visibility Group: Sub-function novel_allVisibility - found compass direction'; $output['mdv'] = "Minimum directional visibility of " . 1 * substr($partLocal1, 0, 4) . " metres looking towards " . $compass_expanded[substr($partLocal1, 4)]; $output['dV_match'] = true; // successfully matched minimum directional visibility $output['segments'] ++; // one segment consumed $partLocal1 = $partLocal2; $partLocal2 = $partLocal3; $partLocal3 = null; goto re_start; }elseif(is_numeric(substr($partLocal2,0,2)) and substr($partLocal2, 2 ,2) == 'KM' and in_array(substr($partLocal2,4),$compass)) { $output['mdv'] = "Minimum directional visibility of " . substr($partLocal2, 0, 2) . " kilometres towards "; $output['mdv'] .= $compass_expanded[substr($partLocal2,4)]; $output['dV_match'] = true; // successfully matched minimum directional visibility $output['segments'] ++; // one segment consumed return $output; // no more processing }else{ switch($partLocal1) { ///////////////////////////////////////////////////////// // b) Prevailing Visibility Group (failed sensor) // //////////////////////////////////////////////////////// case '////': case '////SM': // added 2nd test from version= 1.0.6 (20 Jan 2018) // An example from Saglek Bay, Newfoundland and Labrador, Canada // METAR CWZZ 201200Z AUTO 02038G43KT ////SM //// OVC003 M13/M15 A2908= if($show_diagnostics) echo '
Sub-function novel_allVisibility - found slashes'; $output['prevailVisibility'] = "Automatic visibility sensor failed"; // UK CAA METAR Coding Rules paragraph 4.164, WMO paragraph 15.4 $output['pV_match'] = true; // successfully matched $output['segments'] ++; // one segment consumed return $output; // no more processing /////////////////////////////////////////////////////////////////////////////////////////// // c) Prevailing Visibility Group (good visibility applying in most directions) // /////////////////////////////////////////////////////////////////////////////////////////// case '9999': // Saint-Pierre, Saint Pierre and Miquelon example METAR LFVP 201200Z 25014KT 9999 SCT016 OVC030 M03/M07 Q1009 NOSIG= case '9999NDV': if($show_diagnostics) echo "
Sub-function novel_allVisibility - found '9999'"; $output['prevailVisibility'] = 'Good visibility, at least 10 kilometres (6 miles)'; // WMO regulations paragraph 15.6.3 (d) // The next lowest amount to report is 9500 metres, and that should be reported for visibility less than 10 km // some references say 9999 code used for any visibility over 9500 km (i.e. rounding always down to nearest multiple of 1/2 km) // and some decoders wrongly report that, but I am sticking to WMO whose nearest multiple of 1/2 km would be conventional rounding // hopefully METAR generators get it right to whatever precision they use $output['pV_match'] = true; // successfully matched $output['segments'] ++; // one segment consumed return $output; // no more processing break; // logic flow should never get here ////////////////////////////////////////////////////////////////////////////////////// // d) Remarks Group: USA only Group // // Tower or Surface Visibility (TWR_VIS_vv_vvv or SFC_VIS_vv_vvv) // ////////////////////////////////////////////////////////////////////////////////////// /* Federal Meteorological Handbook No.1 - November 2017 edition (FCM-H1-2017): Surface Weather Observations and Reports, paragraphs 6.3 Visibility Parameters The visibility parameters are: a. Prevailing visibility. The visibility that is considered representative of visibility conditions at the station; the greatest distance that can be seen throughout at least half the horizon circle, not necessarily continuous. b. Sector visibility. The visibility in a specified direction that represents at least a 45 degree arc of the horizon circle. c. Surface visibility. The prevailing visibility determined from the usual point of observation. d. Tower visibility. The prevailing visibility determined from the airport traffic control tower (ATCT) at stations that also report surface visibility. 6.4 Visibility Observing Standards. Visibility may be manually determined at either the surface, the tower level, or both. If visibility observations are made from just one level, e.g., the airport traffic control tower, that level shall be considered the "usual point of observation" and that visibility shall be reported as surface visibility. If visibility observations are made from both levels, the visibility at the tower level may be reported as tower visibility. */ case 'TWR': case 'SFC': ####################################################################################################################################################### # Surface Weather Observations: Air Force Manual 15-111 [U.S. Air Force] Remark Number 6 - Tower Visibility TWR_VIS_vv # ####################################################################################################################################################### # Federal Meteorological Handbook No.1 - November 2017 edition (FCM-H1-2017): Surface Weather Observations and Reports, # # paragraph 6.5.4 Tower Visibility. Tower visibility shall be reported, in accordance with agency procedures (see paragraph 12.7.1.f). # # 12.7.1 Automated, Manual, and Plain Language Remarks. f. Tower or Surface Visibility (TWR_VIS_vv_vvv or SFC_VIS_vv_vvv) # ####################################################################################################################################################### # In the function decode_USA_remarks() this is processed as a case 9 specification and the segments before and after 'VIS' are passed into here. # # However, the visibility is expressed in statute miles with fractions and so both 3 and 4 segments versions above can be seen as there is # # a space between whole and fractional parts. For example, a visibility of 2 1/2 statute miles from Tower would be coded TWR VIS 2 1/2 # ####################################################################################################################################################### if($show_diagnosticsR) echo '
Remark Group: Sub-function novel_allVisibility - ' . $partLocal1 . ' visibility found'; $reply = statuteMiles($partLocal2, $partLocal3); $output['mdv'] = 'The prevailing visibility determined from the '; $output['mdv'] .= $partLocal1 == 'SFC' ? 'usual point of observation is ' : 'Tower is '; $output['mdv'] .= $reply['value']; $output['dV_match'] = true; // successfully matched minimum directional visibility $output['segments'] = 2 + $reply['segments']; return $output; ////////////////////////////////////////////////////////////////////////////////////// // e) Remarks Group: USA Variable Prevailing Visibility Group // ////////////////////////////////////////////////////////////////////////////////////// # Revised 13 March 2018 case 'VIS': // 2018/02/28 12:22 METAR EGUN 281222Z 10011KT 0800 R11/0700V1300 BLSN VV008 M03/M03 A3010 RMK AO2A VIS 0800V3600 CIG 008V011 SLP197 $ ####################################################################################################################################################### # Surface Weather Observations: Air Force Manual 15-111 [U.S. Air Force] Remark Number 7, Variable Prevailing Visibility # # Encode variable prevailing visibility (visibility < 3 statute miles (4800 meters) increases/decreases by ½ statute miles(800 meters) during # # observation) in format VIS_vnvnvnvnvnVvxvxvxvxvx, where VIS is the remark identifier, vnvnvnvnvn is the lowest visibility evaluated, # # V denotes variability between two values, and vxvxvxvxvx is the highest visibility evaluated. # # There is one space following the remark identifier; no spaces between the letter V and the lowest/highest values. # # For example, a visibility that was varying between ½ and 2 statute miles would be encoded "VIS 1/2V2." # ####################################################################################################################################################### # Federal Meteorological Handbook No.1 - November 2017 edition (FCM-H1-2017): Surface Weather Observations and Reports, # # paragraph 6.5.3 Variable Prevailing Visibility. Variable prevailing visibility shall be reported if the prevailing visibility is less than # # 3 miles and rapidly increases or decreases by 1/2 statute mile or more during the time of observation. The minimum and maximum visibility values # # observed shall be reported in remarks section (see paragraph 12.7.1.g). # # 12.7.1 Automated, Manual, and Plain Language Remarks. g. Variable Prevailing Visibility (VIS_vvvvvVvvvv) # ####################################################################################################################################################### # In the function decode_USA_remarks() this is processed as a case 10 specification and the 2 segments 'VIS' and 'vvvvvVvvvv' passed into here. # # However, the minimum and maximum visibilities are also expressed in statute miles with fractions and so could extend into 3 segments as there is a # # space between whole and fractional parts. For example, a visibility that was varying between ½ and 2 statute miles would be encoded "VIS 1/2V2." # ####################################################################################################################################################### if($show_diagnosticsR) echo '
"VIS" found in ' . $partLocal1 . ' identifying USA Variable Prevailing Visibility Group'; if(strpos($partLocal2,'V') !== false) // fraction or whole before 'V' { $wholeOrFraction = substr($partLocal2, 0, strpos($partLocal2,'V')); if($show_diagnosticsR) echo '
"V" found in ' . $partLocal2 . ' passing ' . $wholeOrFraction . ' to function statuteMiles()'; $reply = statuteMiles($wholeOrFraction,''); $output['segments'] += $reply['segments']; if($show_diagnosticsR) print_array($reply,"1-min"); $output['min'] = $reply['value'] . " statute miles"; if($show_diagnosticsR) echo '
Minimum visibility ' . $output['min']; if(strpos($partLocal3,"/") !== false) { // whole after 'V' in segment 2, then fraction in segment 3 $whole = substr($partLocal2, strpos($partLocal2, 'V')); $fraction = $partLocal3; $reply = statuteMiles($whole, $fraction); $output['segments'] += $reply['segments']; if($show_diagnosticsR) print_array($reply,'2 segment-max'); $metres = $whole * 1.609 * 1000; $numerator = 1 * substr($partLocal3, 0, strpos($partLocal3, '/')); $denominator = 1 * substr($partLocal3, -1); $metres += ($numerator / $denominator) * 1.609 * 1000; $metres = round($metres, -2); $output['max'] = number_format($metres) . " metres (" . $reply['value'] . " statute miles)"; }else{ // whole integer after 'V' in segment 2, irrelevant content in segment 3 $maxValue = substr($partLocal2, 1 + strpos($partLocal2, 'V')); $metres = $maxValue * 1.609 * 1000; $metres = round($metres, -2); $output['max'] = number_format($metres) . " metres (" . $maxValue . " statute miles)"; if($show_diagnosticsR) echo '
Maximum visibility ' . $output['max']; } $output['segments'] += 1; return $output; // no more processing }elseif(strpos($partLocal3,"V") !== false) { // 2018/03/13 21:53 METAR KMNN 132153Z AUTO 02007KT 2SM -SN BKN020 BKN029 OVC038 M01/M04 A2997 RMK AO2 VIS 1 1/2V3 SNB29 SLP163 P0000 T10111044 $whole = $partLocal2; $fraction = substr($partLocal3, 0, strpos($partLocal3,'V')); if($show_diagnosticsR) echo '
"V" found in ' . $partLocal3 . ' passing ' . $whole . ' and ' . $fraction . ' to function statuteMiles()'; $reply = statuteMiles($whole, $fraction); if($show_diagnosticsR) print_array($reply,'2 segment-min'); $output['segments'] += 1 + $reply['segments']; $output['min'] = $reply['value']; $maxValue = substr($partLocal3, 1 + strpos($partLocal3, 'V')); if($show_diagnosticsR) echo '
separated off for maximum ' . $maxValue; $metres = substr($partLocal3, 1 + strpos($partLocal3,"V")) * 1.609 * 1000; $metres = round($metres, -2); $output['max'] = number_format($metres) . " metres (" . $maxValue . " statute miles)"; return $output; // no more processing } break; // logic flow should never get here ////////////////////////////////////////////////////////////////////////////////////// // f) Remarks Group: USA Sector Visibility Group // ////////////////////////////////////////////////////////////////////////////////////// /* 6.4.6 Sector Visibility. When the manually-derived visibility is not uniform in all directions, the horizon circle shall be divided into arcs that have uniform visibility and represent at least one eighth of the horizon circle (45 degrees). The visibility that is evaluated in each sector is sector visibility. ... 6.5.7 Sector Visibility. Sector visibility shall be reported in remarks when it differs from the prevailing visibility by one or more reportable values and either the prevailing or sector visibility is less than 3 miles (see paragraph 12.7.1.h). */ # Revised 13 March 2018 case 'Sector': ####################################################################################################################################################### # Surface Weather Observations: Air Force Manual 15-111 [U.S. Air Force] Remark Number 8, Sector Visibility # # Encode sector visibility (visibility < 3 statute miles (4800 meters) increases/decreases by ½ statute miles(800 meters) during # # observation) in format VIS_[DIR]_vvvvv, where VIS is the remark identifier, DIR is the optional compass direction, and vvvv is the visibility. # # There is one space following the remark identifier; one space between the compass direction and the visibility value. # ####################################################################################################################################################### # Federal Meteorological Handbook No.1 - November 2017 edition (FCM-H1-2017): Surface Weather Observations and Reports, # # paragraph 6.5.7 Sector Visibility. Sector visibility shall be reported in remarks when it differs from the prevailing visibility by one or # # more reportable values and either the prevailing or sector visibility is less than 3 miles (see paragraph 12.7.1.h). # # 12.7.1 Automated, Manual, and Plain Language Remarks. h. Sector Visibility (VIS_[DIR]_vvvvv) The compass direction is optional. # ####################################################################################################################################################### # In the function decode_USA_remarks() this is processed as a case 11 specification and the segments after 'VIS' only are passed into here. # # However, the visibility is expressed in statute miles with fractions and so could extend into 3 or 4 segments as there is a space between whole # # and fractional parts. For example, a visibility of 2 1/2 statute miles in the north-eastern octant would be coded VIS NE 2 1/2 # ####################################################################################################################################################### if($show_diagnosticsR) echo '
Remark Group: Sub-function novel_allVisibility - found sector visibility'; $reply = statuteMiles($partLocal2, $partLocal3); return $reply; } // end switch ////////////////////////////////////////////////////////////////////////////////////////////////////// // g) Prevailing Visibility Group of Standard Groups (USA & Canada quoting figure and 'SM') // // In USA/Canada, only prevailing visibility is reported in main part of METAR, // ///////////////////////////////////////////////////////////////////////////////////////////////////// // Sector and variable visibilities are reported within Remarks Group - See d) and e) above here. // ///////////////////////////////////////////////////////////////////////////////////////////////////// if(strpos($partLocal1, "SM") !== false or strpos($partLocal2, "SM") !== false) { if($show_diagnostics) echo '
Sub-function novel_allVisibility - found "SM"'; $output['pV_match'] = false; switch($partLocal1) { case 'M1/4SM': $output['prevailVisibility'] = "Less than 400 metres (less than ¼ statute mile)"; $output['pV_match'] = true; // successfully matched $output['segments'] = 1; // one segment consumed break; case '1/16SM': case '1/8SM': case '3/16SM': case '3/8SM': case '5/16SM': case '5/8SM': case '7/16SM': case '7/8SM': case '9/16SM': case '11/16SM': case '13/16SM': case '15/16SM': // 2018/01/21 13:00 CYXU 211300Z 08004KT 1/8SM R15/0700FT/N FZFG OVC001 M05/M06 A3007 RMK FG4ST4 FROIN SLP198 // 2018/02/15 22:00 METAR CYZR 152200Z AUTO 19010KT 1/8SM R33/1200V1800FT/ -RA FG BKN002 BKN095 OVC140 06/06 A2961 RMK SLP032 $numerator = 1 * substr($partLocal1, 0, strpos($partLocal1, '/')); $denominator = strpos($partLocal1,'8SM') ? 8 : 16; if($show_diagnostics) echo '
Recognised numerator=' . $numerator . ' and denominator=' . $denominator . ' for visibility'; $metres = ($numerator / $denominator) * 1.609 * 1000; $metres = round($metres, -2); $output['prevailVisibility'] = number_format($metres) . " metres (" . $numerator . '' . $denominator . " statute miles)"; // Division sign ➗ $output['pV_match'] = true; // successfully matched $output['segments'] = 1; // one segment consumed break; case '1/4SM': $output['prevailVisibility'] ='400 metres (¼ statute mile)'; $output['pV_match'] = true; // successfully matched $output['segments'] = 1; // one segment consumed break; case '1/2SM': $output['prevailVisibility'] ='800 metres (½ statute mile)'; $output['pV_match'] = true; // successfully matched $output['segments'] = 1; // one segment consumed break; case '3/4SM': $output['prevailVisibility'] ='1,200 metres (¾ statute mile)'; $output['pV_match'] = true; // successfully matched $output['segments'] = 1; // one segment consumed break; case '0SM': case '1SM': case '2SM': case '3SM': case '4SM': case '5SM': case '6SM': case '7SM': case '8SM': case '9SM': case '10SM': case '11SM': case '12SM': case '13SM': case '14SM': case '15SM': case '20SM': case '25SM': case '30SM': case '35SM': case '40SM': case '45SM': case '50SM': $metres = substr($partLocal1,0,-2) * 1.609 * 1000; $metres = round($metres, -2); $output['prevailVisibility'] = number_format($metres) . " metres (" . substr($partLocal1,0,-2) . " statute miles)"; $output['pV_match'] = true; // successfully matched $output['segments'] = 1; // one segment consumed break; case 1: case 2: switch ($partLocal2) { case '1/4SM': $output['prevailVisibility'] = round(($partLocal1 . '.25') * 1.609, 2) * 1000 . " metres (" . $partLocal1 . '¼' . " statute miles)"; break; case '1/2SM': $output['prevailVisibility'] = round(($partLocal1 . '.5') * 1.609, 2) * 1000 . " metres (" . $partLocal1 . '½' . " statute miles)"; break; case '3/4SM': $output['prevailVisibility'] = round(($partLocal1 . '.75') * 1.609, 2) * 1000 . " metres (" . $partLocal1 . '¾' . " statute miles)"; break; } $output['pV_match'] = true; // successfully matched $output['segments'] = 2; // two segments consumed break; } // end switch return $output; // no more processing ////////////////////////////////////////////////////////////// // h) Prevailing Visibility Group (International metric) // // 1 to 4 digits for units of metres. // // May be Minimum Directional Visibility in next segment. // ////////////////////////////////////////////////////////////// }elseif(is_numeric($partLocal1) and strlen($partLocal1) < 5) { // UK CAA METAR Coding Rules paragraph 4.29 use metres, they always report prevailing visibility, but can also report minimum visibility. // Australian BOM use metres, they always report prevailing visibility, but can also report minimum visibility. if($show_diagnostics or $show_diagnosticsR) echo '
Sub-function novel_allVisibility - found '. strlen($partLocal1) . ' digits, and no units suffixed
'; $yards = 1.093613 * $partLocal1; $miles = round(($yards - ($yards % 1760)) / 1760); $yards = number_format($yards % 1760); $output['prevailVisibility'] = number_format($partLocal1) . ' metres (' . $miles . ' miles ' . $yards . ' yards)'; $output['segments'] ++; // one segment consumed function_continue: $output['pV_match'] = true; // successfully matched prevailing visibility if(array_key_exists('dV_match', $output) and $output['dV_match']) return $output; // no more processing $partLocal1 = $partLocal2; $partLocal2 = $partLocal3; $partLocal3 = null; goto re_start; /////////////////////////////////////////////////////////////// // i) Prevailing Visibility Group (International metric) // // 1 or 2 digits then 'KM' for units of kilometres. // // May be Minimum Directional Visibility in next segment. // /////////////////////////////////////////////////////////////// }elseif (preg_match('~^(M)?([0-9][0-9]?)([K][M])$~', $partLocal1, $pieces) === 1) { if($show_diagnostics) echo "
Sub-function novel_allVisibility - found 1 or 2 digits and 'KM'"; $miles = $partLocal1 * 0.621; if(!strlen($pieces[1]) and $pieces[3] == "KM") { $output['prevailVisibility'] = 1 * $pieces[2] . " kilometres (" . number_format($miles, 2) . " miles)"; goto function_continue; }elseif($pieces[1] == "M" and $pieces[3] == "KM") { $output['prevailVisibility'] = "Less than " . 1 * $pieces[2] . " kilometres (less than " . round($miles, 2) . " miles)"; goto function_continue; } ///////////////////////////////////////////////////////// // j) Prevailing Visibility Group (Obsolete) km // // No Minimum Directional Visibility reported // //////////////////////////////////////////////////////// }elseif(preg_match('~^([0-9]+)([K][M])?([N][D][V])$~',$partLocal1,$pieces) === 1) { # New Zealand, Switzerland etc. if($show_diagnostics) echo "
Sub-function novel_allVisibility - found 1 or 2 digits then 'KM', and obsolete suffix 'NDV'"; $miles = $pieces[1] * 0.621; $output['prevailVisibility'] = 1 * $pieces[1] . " kilometres (" . number_format($miles, 2) . " miles)"; $output['pV_match'] = true; // successfully matched prevailing visibility // the segment consumed by prevailing visibility is shared with directional visibility $output['mdv'] = "This automatic stations does not identify directional visibility variation"; $output['dV_match'] = true; // successfully matched minimum directional visibility $output['segments'] = -1; // no second segment consumed by minimum directional visibility, so treat as 1 for both groups return $output; // no more processing }else return $output; } // end else } // end sub-function novel_allVisibility() function statuteMiles($whole, $fraction) { global $show_diagnosticsR; $return['value'] = false; $return['segments'] = 0; if(is_numeric($whole)) $whole = 1 * $whole; if($show_diagnosticsR) echo '
function statuteMiles() ... parameter 1= "' . $whole . '" and parameter 2= "' . $fraction . '"
'; switch($whole) { case '1': case '2': if($show_diagnosticsR) echo '
function statuteMiles() ... recognised integer part ' . $whole; $metres = $whole * 1.609 * 1000; switch ($fraction) { case '1/4': $metres += 400; $metres = round($metres, -2); $return['value'] = number_format($metres) . ' metres (' . $whole . ' ¼ statute miles)'; $return['segments'] = 2; if($show_diagnosticsR) echo '
function statuteMiles() ... recognised fraction part ' . $fraction; break; case '1/2': $metres += 800; $metres = round($metres, -2); $return['value'] = number_format($metres) . ' metres (' . $whole . ' ½ statute miles)'; $return['segments'] = 2; if($show_diagnosticsR) echo '
function statuteMiles() ... recognised fraction part ' . $fraction; break; case '3/4': $metres += 1200; $metres = round($metres, -2); $return['value'] = number_format($metres) . ' metres (' . $whole . ' ¾ statute miles)'; $return['segments'] = 2; if($show_diagnosticsR) echo '
function statuteMiles() ... recognised fraction part ' . $fraction; break; default: $metres = round($metres, -2); $return['value'] = number_format($metres) . ' metres (' . $whole . ' statute miles)'; $return['segments'] = 1; } break; case 'M1/4': $return['value'] = 'Less than 400 metres (< ¼ statute mile)'; $return['segments'] = 1; if($show_diagnosticsR) echo '
function statuteMiles() ... recognised fraction ' . $whole; break; case '1/4': $return['value'] = '400 metres (¼ statute mile)'; $return['segments'] = 1; if($show_diagnosticsR) echo '
function statuteMiles() ... recognised fraction ' . $whole; break; case '1/2': $return['value'] = '800 metres (½ statute mile)'; $return['segments'] = 1; if($show_diagnosticsR) echo '
function statuteMiles() ... recognised fraction ' . $whole; break; case '3/4': $return['value'] = '1,200 metres (¾ statute mile)'; $return['segments'] = 1; if($show_diagnosticsR) echo '
function statuteMiles() ... recognised fraction ' . $whole; break; case 3: case 4: case 5: case 6: case 7: case 8: case 9: case 10: case 11: case 12: case 13: case 14: case 15: case 20: case 25: case 30: case 35: case 40: case 45: case 50: default: $metres = $whole * 1.609 * 1000; $metres = round($metres, -2); $return['value'] = number_format($metres) . " metres (" . $whole . " statute miles)"; $return['segments'] = 1; if($show_diagnosticsR) echo '
function statuteMiles() ... recognised integer part ' . $whole; break; } return $return; } // end sub-function - statuteMiles() //---------------------------------------------------------------- // The sub-functions that follow are called from the sub-functions // in 'decodeMETAR.php' directly or are called by another one of the sub-functions below. // For each sub-function, there are notes explaining origin of that code sequence. //---------------------------------------------------------------- ############################################################################################################################## # Novel function written by SPAWS to decode a segment that might be part of the following types of segments # # 'present' weather, 'recent' weather, and 'change'. # # This can be reported in up to 3 segments each of up to 9 characters representing weather conditions/phenomena. # # This decoder has been written based on the 'iiddppooxx' format in the specification for input of phenomena; # # i.e. intensity qualifier, descriptor, precipitation type, obscuration type, and miscellaneous. # # The WMO recognise a grand total of 402 different combinations (see http://codes.wmo.int/306/_4678) of the possible codes # # so an alternative approach would be to list all those combinations instead of the 'iiddppooxx'approach taken here. # # For present or forecast weather there are 399 combinations http://codes.wmo.int/49-2/_AerodromePresentOrForecastWeather # # Similarly, the WMO recognise just 25 combinations for recent weather http://codes.wmo.int/49-2/_AerodromeRecentWeather # ############################################################################################################################## function novel_decode_phenomena($part,$call) { global $metar_toDecode, $decodeInfo, $decodeGroupCount, $show_diagnostics, $show_diagnosticsR; // shared with calling function in this same script global $precipitationTypes; // used in another sub-function (Remarks) and in 'selectMETAR.php' global $descriptorArray, $obscurationTypes, $miscellaneousTypes; // used in 'selectMETAR.php' static $component_list = ''; // used only within this sub-function // UK CAA METAR Coding Rules paragraphs as referenced in main function $output =array(); $output['segment'] = 0; // 2017/12/13 10:00 METAR CYZR 131000Z AUTO 29006KT 9SM SCT034 M12/M14 A2982 RMK ICG INTMT SLP113 if($part == "ICG") { if(array_key_exists(1, $metar_toDecode) and $metar_toDecode[1] == "PAST" and array_key_exists(2, $metar_toDecode) and $metar_toDecode[1] == "HOUR") { // 2018/01/28 16:04 METAR CYZR 281604Z AUTO 35008KT 9SM CLR 03/M02 A3032 RMK ICG PAST HR SLP276 $output[0] = "In past hour, seen Icing"; $output['segment'] = 3; $part .= '_' . $metar_toDecode[1] . '_' . $metar_toDecode[2]; }elseif(array_key_exists(1, $metar_toDecode) and $metar_toDecode[1] == "INTMT") { // 2018/01/28 12:00 METAR CYZR 281200Z AUTO 33002KT 9SM CLR M02/M04 A3025 RMK ICG INTMT SLP251 $output[0] = "Intermittent Icing"; $output['segment'] = 2; $part .= '_' . $metar_toDecode[1]; }else{ // 2018/01/28 19:00 METAR CYZR 281900Z AUTO 03012G19KT 9SM CLR 02/M01 A3030 RMK ICG SLP269 $output[0] = "Icing"; $output['segment'] = 1; } if($show_diagnostics or $show_diagnosticsR) echo "
Found 'phenomena' " . $output[0] . " using novel_decode_phenomena matching " . $part; goto continuePoint; } $partLocal = $part; // local variable that can change its content, initialised to what passed to sub-function if($show_diagnostics) echo "
======================== Starting novel_decode_phenomena function within 'decodeMETAR_sub_funct.php' examining " . $partLocal; if(substr($part,0,1 == '/')) goto continuePoint; if($call == 'change' and $partLocal == 'NSW' and array_key_exists('CONDITIONS', $decodeInfo)) { /* See UK CAA 'Requirements for meteorological observations at aerodromes' 4.151 Present weather: All codes given in the first two columns of Table 2 shown earlier in this chapter may be used, including changes to intensity where applicable. Note that weather forecast to be in the vicinity of the aerodrome is not included in the TREND forecast, only that which is expected to affect the aerodrome. In addition, if certain present weather is occurring in the observation and is expected to cease during the TREND forecast period, the letters ‘NSW’ (no significant weather) may be used in the TREND forecast to indicate this. */ $output['segment'] = 1; $output[0] = 'The reported ' . $decodeInfo['CONDITIONS'] . ' is expected to be replaced by "No Significant Weather" in Trend period defined above.'; goto continuePoint; }elseif($call == 'change' and $partLocal == 'NSW') { $output['segment'] = 1; $output[0] = "All Significant Weather is expected to cease in Trend period defined above"; goto continuePoint; } if($call != 'recent'){ // paragraph 4.131 applied here, but 4.130 is not tested # Test if an intensity/proximity qualifier is present if (substr($partLocal,0,1) == '-') { $intensity = 'Light '; // No international agreement on what is called 'Light intensity' // In UK, precipitation is considered light if it does not immediately form puddles // In Canada - Light: if rate of fall is 2.5 mm/h or less (Usually determined over a period of 5 minutes prior to METAR) (Alternative ways in manual for Observers) $partLocal = substr($partLocal,1); // remove processed part of code }elseif (substr($partLocal,0,1) == '+') { $intensity = 'Heavy '; // No international agreement on what is called 'Heavy intensity' // In UK, precipitation is described as heavy when the rain rate is at least high (MET OFFICE definition), or when the precipitation makes a roaring noise (observers are urged to listen) // In Canada - Heavy: if rate of fall is 7.6 mm/h or more # Note that code later in this script will convert the meaning of '+' to 'Well developed' if needed as that is its meaning when qualifying certain phenomenon $partLocal = substr($partLocal,1); // remove processed part of code }elseif (substr($partLocal,0,2) == 'VC' and $call != 'change') // only allowed with 'present weather' see paragraph 4.151, so this checked in 2nd part of condition { $intensity = 'Nearby '; // VC is 'very close' i.e. weather observed within 8km, but not anywhere on aerodrome $partLocal = substr($partLocal,2); // remove processed part of code }else $intensity = ''; // moderate conditions have no intensity qualifier // In Canada - Moderate: if rate of fall is 2.6 to 7.5 mm/h if($show_diagnostics and $partLocal == $part) echo "
Did not find 'intensity/proximity' in novel_decode_phenomena"; if($show_diagnostics and $partLocal != $part) echo "
Found 'intensity/proximity' " . $intensity . " using novel_decode_phenomena matching '" . substr($part,0,strcspn($part,$partLocal)) . "'"; }else $intensity = ''; // recent conditions have no intensity qualifier # Test if descriptor is present $descriptor = array_search(substr($partLocal,0,2),$descriptorArray,true); if($show_diagnostics and $descriptor) echo "
Found 'descriptor' " . $descriptor . " using novel_decode_phenomena matching " . substr($partLocal,0,2); if($show_diagnostics and !$descriptor) echo "
Did not find 'descriptor' in novel_decode_phenomena with " . substr($partLocal,0,2); if($descriptor) $partLocal = substr($partLocal,2); # Test if precipitation type is present # Example METAR from Hof, Germany: METAR EDQM 011320Z 27005KT 9999 -DZ BKN010 OVC026 11/09 Q1022 REDZ while (array_search(substr($partLocal,0,2),$precipitationTypes,true)) // can be multiple precipitation types (USA permit maximum of 3 precipitation types in single group) { if(isset($precipitation)) $store = $precipitation .", "; // Store any precipitation type already found else { $store = ""; } $precipitation_found = array_search(substr($partLocal,0,2),$precipitationTypes,true); if(!is_null($precipitation_found)){ if($show_diagnostics) echo "
Found 'precipitation' " . $precipitation_found . " using novel_decode_phenomena matching " . substr($partLocal,0,2); $partLocal = substr($partLocal,2); // move to next 2 characters $precipitation = $store . " " . $precipitation_found; }elseif($show_diagnostics) echo "
Did not find 'precipitation' in novel_decode_phenomena looking at " . substr($partLocal,0,2); } if($descriptor == 'Shower' and isset($precipitation)){ if($call == 'recent') // plural only applies to 'recent' $descriptor .= 's of '; else $descriptor .= ' of '; } # Test if obscuration type is present # Example METAR from Djanet Inedbirene Airport, Djanet, Algeria with sand blowing in a thunderstorm: METAR DAAJ 011330Z 32026 0500 TS BLSA FEW040CB BKN046 35/06 1015 if($call != 'recent'){ $obscuration = array_search(substr($partLocal,0,2), $obscurationTypes, true); if($obscuration) { if($show_diagnostics) echo "
Found 'obscuration' " . $obscuration . " using novel_decode_phenomena matching " . substr($partLocal,0,2); $partLocal = substr($partLocal,2); } if($obscuration == "Fog" and $descriptor == "Partial") $descriptor = "Bank of"; }else $obscuration =false; # Test for miscellaneous codes that can appear at end of recent weather segment if($call != 'recent') { $miscellaneous = array_search(substr($partLocal,0,2),$miscellaneousTypes,true); if ($intensity == '+' and substr($partLocal,0,2) == 'FC') $miscellaneous = "Tornado or Waterspout"; } else $miscellaneous = false; if($show_diagnostics and $miscellaneous) echo "
Found 'miscellaneous' " . $miscellaneous . " using novel_decode_phenomena matching " . $partLocal; if($show_diagnostics and !$miscellaneous) echo "
Did not find 'miscellaneous' in novel_decode_phenomena"; // correct decoded meaning of '+' if necessary if(($miscellaneous or $obscuration) and !isset($precipitation) and !$descriptor){ if($intensity == 'Heavy ') $intensity = 'Well developed ';// apply correct meaning given context } # This script can translate any code that is permitted under UK or USA standards. # This script does not validate the combinations used in the various components are permitted under the rules, # the purpose of this script is to display what is found in the METAR (even if it has been completed to break the rules) if($intensity or $descriptor or isset($precipitation) or $obscuration or $miscellaneous) { $component_list = $intensity; // always initialised even if null. if($descriptor) $component_list .= $descriptor; // add only if present if(isset($precipitation)) $component_list .= $precipitation . ","; // add only if present, add also separator if($obscuration) $component_list .= $obscuration . ","; // add only if present if($miscellaneous) $component_list .= $miscellaneous; // add only if present $component_list .= " "; // tidy up by adding space $component_list = preg_replace('| , |is',', ',$component_list); // tidy - remove any space before comma $component_list = preg_replace('|\s+|is',' ',$component_list); // tidy - remove multiple spaces $output[0] = $component_list; // return tidied output if(substr($output[0],-1) == ',') $output[0] = substr($component_list,0,-1); // tidy remove trailing comma, if any, before return } $output['segment'] = 1; continuePoint: if(array_key_exists(0, $output)) { if(substr($call,0,3) != "Rem" and $output['segment'] == 1) $output[1] = trim(array_shift($metar_toDecode)); // take out processed segment else $output[1] = $metar_toDecode[0]; // RMK if($show_diagnostics) echo "

Matched into " . $call . ' Weather Phenomena Group the content of ' . $output[1] . ' giving ' . $output[0] . '

'; // conditional de-bugging section continues return $output; } if($show_diagnostics) echo "
Nothing found by novel_decode_phenomena function examining " . $part; $output[0] = null; return $output; } //---------------------------------------------------------------- // The sub-functions that follow are called from the sub-functions // in 'decodeMETAR.php' directly or are called by another one of the sub-functions below. // For each sub-function, there are notes explaining origin of that code sequence. //---------------------------------------------------------------- ############################################################################################################### # Sub-Function to decode what in CAA guide is called 'Cloud Cover' Group. In FAA guide called 'Sky Conditions' Group. # # Written by SPAWS using decoding text based on CAA 'CAP 746' guide. Also used for decoding in Recent weather, Trend and Remarks Groups. # # This sub-function can be called up to 4 times during decoding any of those 4 Groups listed above, i.e. maximum 16 times per METAR. # # WMO Format of Group segments is NNNhhh(CC) or NSC or NCD or VVhhh. # # UK standard allows automatic stations to replace any of 3 parts of first format type with the corresponding number of '/' symbols. # # Hence complexity of 'preg_match' expression, and that was refined based on the UK and USA METAR actually experienced during testing. # # Did also try using series of conditional tests, but regular expression proved the easier to code in this case, due to rule complexity. # ############################################################################################################### function novel_decode_cloud($part) { global $metar_toDecode, $decodeInfo, $decodeGroupCount, $show_diagnostics, $cloudAmountBandCode, $cloud_type_array; // last variable made global because used in a few other places // UK CAA METAR Coding Rules paragraphs 4.99 to 4.109 # Example from Tokyo Heliport, Japan: METAR RJTI 010700Z 13013KT 9999 FEW030 BKN045 BKN/// 24/15 Q1022 if(isset($output)) $removed = array_splice($output, 0, count($output)); // initialise output array as empty at start as might be re-entry into this sub-function $output[4] = false; // initialise as current segment not yet matched to cloud Group if(is_numeric(substr($part,1,1))) return $output; // The format for cloud Group always starts with letters as defined in array $cloudAmountBandCode, so if numeric found, skip this Group if($show_diagnostics ) echo "
In 'novel_decode_cloud' within 'decodeMETAR_sub_funct.php' decoding pass " . $decodeGroupCount['SKY'] . ", looking at " . $part; // conditional de-bugging section continues # Is it just a cloud type code? if(in_array($part, $cloud_type_array)) { $output[4] = true; $output[3] = array_search($part, $cloud_type_array); if($show_diagnostics) echo "
Matched pass number " . (1 + $decodeGroupCount['SKY']) . ' in Group with ' . $part . "
"; # no amount of cloud cover quantified as not normal weather cloud, but seen on horizon $output[2] = "very high"; }else # Is it clear or no clouds? if ($part == 'SKC' || $part == 'CLR' || // (Tested for here, although neither in UK CAA METAR Coding Rules, CLR is used by USAF in UK) // WMO vol II page 12 says Canada does not report 'NSC' as it reports clouds at all heights, but Canada says 'CLR' used if nothing below 12,000 feet $part == 'NSC' || $part == 'NCD') // (UK CAA paragraph 4.103) { # example from Indira Gandhi International Airport, New Delhi, India: # METAR VIDP 011230Z 32007KT 4500 HZ NSC 34/15 Q1006 NOSIG $output[4] = true; $output[0] = $output[7] = ($part == 'SKC' || $part == 'CLR') ? 'Clear' : 'No significant clouds'; if($show_diagnostics) echo "
Matched pass number " . (1 + $decodeGroupCount['SKY']) . ' in Group with ' . $part; }else{ # Decode details of clouds # Multiple pieces to code: 1 -> oktas, 2 -> height, 3 -> cloud type // e.g. Aigen-Fiala Fernbrugg Air Base 2012/09/10 08:20 METAR LOXA 100820Z VRB02KT 50KM FEW120AC SCT300CI 17/13 Q1021 NOSIG RMK SCT if (preg_match('/^([A-Z]{2,3}|[\/]{3})([0-9]{2,3}|[\/]{3})([ACST][BCIT]?([CU])?|[\/]{3})?$/', $part, $pieces) === 1) # included '///' and '//////' here although former probably already excluded { $output[4] = true; if($show_diagnostics) { echo "
Processing cloud Group specification in pass number ". (1 + $decodeGroupCount['SKY']) . ' in Group for oktas and height
'; print_array($pieces, "Cloud-details->"); } # First part = amount of cloud (UK CAA paragraph 4.101, WMO paragraph 15.9.1.4) // Script does not validate that for first layer FEW, SCT, BKN or OVC allowed // Script does not validate that for second layer SCT, BKN or OVC allowed (over 2 oktas) // Script does not validate that for third layer BKN or OVC allowed (over 4 oktas) # N America often reports more than 3 layers if(array_key_exists(1, $pieces)){ switch($pieces[1]){ case 'OVC': // 2018/01/29 08:00 METAR CYKF 290800Z AUTO 03003KT OVC110 M04/M07 A3030 RMK WX MISG VIS MISG SLP275 $output[1] = $cloudAmountBandCode[$pieces[1]]; $output[0] = $output [7] = "Overcast"; break; case '///': $output[0] = "Cloudy"; $output[1] = ' unidentified cloud amount;'; $output[7] = "Clouds"; break; case 'VV': $output[0] = "Obscured"; $output[1] = $cloudAmountBandCode[$pieces[1]]; break; default: $output[0] = $output [7] = "Cloudy"; $output[1] = $cloudAmountBandCode[$pieces[1]]; // array $cloudAmountBandCode translates to international standard oktas (eighths), // note that Canada up to May 2016 worked in tenths, and Canada does not use sky obscured code } if($show_diagnostics) echo "
Matched cloud amount decoded " . $pieces[1] . " into '" . $output[1] . "' in Group pass number " . (1 + $decodeGroupCount['SKY']); // conditional de-bugging section continues } # Second part = height of cloud (WMO regulation paragraph 15.9.1.5 states steps in 30m of height for coding, that is approximately 100 feet) // (UK CAA paragraph 4.102) and USA http://www.met.tamu.edu/class/metar/metar-pg10-sky.html if(array_key_exists(2, $pieces)){ if($pieces[2] == '///') { if($pieces[1] == "VV") $output[2] = "
[Code 'VV///' indicates that vertical visibility height is limited, but not quantifed (e.g. UK practice), or sky obscured due to fog, falling or blowing snow]"; // UK CAA rule 4.108 // Meteorologists use code 9 when reporting an obscured sky (one that cannot be seen to estimate number of oktas e.g in fog). // METAR can use VV instead for such obscuring, but the aviation industry don't use 9 even in Remarks Group where oktas can be reported. else $output[2] = ' either height not detected or station on mountain above cloud level'; }elseif($pieces[2] == '000') $output[2] = ' less than 100 feet (30.5 m)'; // USA less than or equal to 50 feet, but UK threshold assumed by this script else{ if(strlen($pieces[2]) < 3) $pieces[2] = 10 * $pieces[2]; # assumption to deal with malformed METAR with insufficient digits $altitudeFT = (integer) 100 * $pieces[2]; // units are to nearest hundreds of feet in UK and USA specs // (WMO rules require report heights in m, but for cloud heights ICAO insist all pilots work in feet internationally) // - My script assumes ICAO obeyed and all figures are in ft (as no way to tell if metres used because format does not allow inclusion of units) // (Uses conversion factor: 1 m = 3.28084 ft, but gives range equivalent to plus or minus 50 feet) $altitudeM = "Height between " . 10 * round(($altitudeFT - 50)/32.8084) . " and " . 10 * round(($altitudeFT + 50)/32.8084); $output[2] = $altitudeM . ' m (reported as ' . $altitudeFT . 'ft)'; /* The ceiling is defined as the lesser of: - The height above ground or water of the base of the lowest layer of cloud where the summation amount exceeds half the sky (more than 5/10 or 4/8); or - the vertical visibility in a surface-based layer that completely obscures the whole sky. */ # Since UK policy is not to quantify vertical visibility, that figure not part of following condition if(!array_key_exists(6, $output) and ($pieces[1] == "OVC" or $pieces[1] == "BKN")){ $output[5] = $pieces[0]; $altitudeFT = 100 * $pieces[2]; $output[6] = number_format($altitudeFT) . ' feet a.g.l. (' . 10 * round(($altitudeFT + 50)/32.8084) . ' metres)'; } } if($show_diagnostics) echo "
Matched cloud Group height decoded " . $pieces[2] . " into '" . $output[2] . "' in Group pass number " . (1 + $decodeGroupCount['SKY']); // conditional de-bugging section continues } if(array_key_exists(3, $pieces)) { $output[4] = true; # Is it a recognised cloud type? $type = array_search($pieces[3], $cloud_type_array); if(!is_null($type)) { $output[3] = $type; if($show_diagnostics) echo "
Matched cloud Group type decoded " . $pieces[3] . " into '" . $output[0] . "' in Group pass number " . (1 + $decodeGroupCount['SKY']); // conditional de-bugging section continues }else{ # Is it storm clouds? switch ($pieces[3]){ # script must cope with thunderstorm cloud name with (see UK paragraph 4.104) or without other details - see UK paragraphs 4.104, 4.155, 4.160, and 4.162, 4.163 case 'CB': // UK CAA paragraph 4.109 - this script does not cross-check with present weather # example METAR from Port Bouet Airport, Abidjan, Ivory coast: METAR DIAP 011400Z 19010KT 160V220 9999 SCT012 FEW020CB SCT025 29/24 Q1010 NOSIG case 'TCU': $output[0] = 'Storm cloud'; // WMO volume II page 12 says Canada does not forecast 'TCU' if($pieces[1] == '///')unset($output[1]); if($pieces[2] == '///')unset($output[2]); if($show_diagnostics) echo "
Matched cloud type decoded " . $pieces[3] . " into '" . $output[0] . "' in Group pass number " . (1 + $decodeGroupCount['SKY']); // conditional de-bugging section continues $output[3] = $cloudAmountBandCode[$pieces[3]]; break; } } }else unset($output[3]); } } if(isset($output)) { if($output[4]) { $output[5] = trim(array_shift($metar_toDecode)); // take out processed segment }else{ unset($output[0], $output[1], $output[2], $output[3]); if($show_diagnostics) echo "
No match in Cloud Group decode for pass " . $decodeGroupCount['SKY'] . ", looking at " . $part; // conditional de-bugging section continues } return $output; } //---------------------------------------------------------------- } // End of 'novel_decode_cloud' function by SPAWS - returning array: // [0] = Sky summary - Clear, Cloudy, Overcast, Obscured, 'No significant clouds' // [1] = Cloud Amount, [2] = Cloud Height, [3] = Cloud Type Code // [4] = Boolean indicating whether METAR segment recognised as 'Cloud Cover' Group // [5] = METAR segment that has been decoded // [6] = Cloud Ceiling // [7] = Ken True condition - 'Clear', 'No significant clouds', 'Cloud', 'Cloudy' //---------------------------------------------------------------- //---------------------------------------------------------------- // The sub-functions that follow are called from the sub-functions // in 'decodeMETAR.php' directly or are called by another one of the sub-functions below. // For each sub-function, there are notes explaining origin of that code sequence. //---------------------------------------------------------------- function rev_get_heat_index($tempF, $rh) // minor change from function by Marshall Roch and Mark Woodward that follows within comments { global $decodeInfo; // Calculate Heat Index based on temperature in F and relative humidity (65 = 65%) if ($tempF > 69 && $rh > 39) // temperature comparison value reduced for UK from 79 to 69 { $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); $decodeInfo['HEAT INDEX'] = "$hiF°F ($hiC°C)"; } } /* 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 rev_get_wind_chill($tempC) // inspired by original function by Marshall Roch and Mark Woodward that follows within comments { global $decodeInfo; // Calculate Wind Chill Temperature based on Environment Canada formula https://en.wikipedia.org/wiki/Wind_chill # UK Met Office Wind Chill also compensates for humidity, so they use different calculation, more similar to apparent temperature below // First formula applied by Environment Canada to all outputs for year 2000 and earlier // From 2001, applies to air temps less than or equal to zero, and wind speeds of 5 or more km/hr $chillC = 13.12 + 0.6215 * $tempC - 11.37 * pow($decodeInfo['WINDKPH'], 0.16) + 0.3965 * $tempC * pow($decodeInfo['WINDKPH'], 0.16); // second formula used by Environment Canada for same temperatures, but lower wind speeds. if($decodeInfo['WINDKPH'] < 5) $chillC = $tempC + ((-1.59+(0.1345*$tempC))/5)*$decodeInfo['WINDKPH']; $chillC = round($chillC); $chillF = round($chillC * 1.8 + 32); $decodeInfo['WIND CHILL'] = "$chillF°F ($chillC°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)"; } } } */ //------------------------------------------------------------------------- # Function added by SPAWS and is working //------------------------------------------------------------------------- function calculate_Australian_apparent_temperature($tempC, $rh) { # Australian Apparent Temperature global $decodeInfo; # calculate AT based on temperature (Celsius), humidity (%) and wind speed (metres per second) // v = rh / 100 X 6.105 X exp ( 17.27 X Ta / ( 237.7 + Ta ) ) // AT = Ta + 0.33 X v - 0.70 X ws - 4.00 # from PHP 5.6 onwards can use "base ** number" instead of "pow(base, number)" for exponential expressions # not used ** here, so script compatible with earlier PHP versions $v = $rh / 100 * 6.105 * pow (0.16, 17.27 * $tempC / (237.7 + $tempC) ); $decodeInfo['APPARENT'] = round($tempC + (0.33 * $v) - (0.7 * $decodeInfo['WINDMPS']) - 4.0) . "°C"; } function calculate_humidex($tempC, $dewC) { # Canadian Humidity Index global $decodeInfo; // 5417.7530 is a rounded constant based on the molecular weight of water, latent heat of evaporation, and the universal gas constant. $e = 5417.7530 * ((1/273.16)-(1/(273.16 + $dewC))); $vapour_pressure = 6.11 * exp($e); // vapour pressure in hPa (or mbar) $decodeInfo['HUMIDEX'] = round($tempC + (0.5555 * $vapour_pressure -10)); } //------------------------------------------------------------------------- # Function added by SPAWS and is working //------------------------------------------------------------------------- function calculate_wet_bulb_temperature($tempC,$dewC,$atmos) { global $decodeInfo; // This is done by repeated iteration until the guesses at each // pass of the loop vary by less than 0.005 $tHigher = $tempC; $tLower = $dewC; $wetGuess = $dewC; while(true) { $previousGuess = $wetGuess; // new guess - half way between previous values $wetGuess = ($tHigher + $tLower) / 2; // exit if difference is small if(abs($previousGuess - $wetGuess) < 0.005) break; // use the guessed wet bulb temp to calculate vapour pressure $guessedVapourPressure = 6.11 * pow(10,7.5*$wetGuess/(237.7+$wetGuess)); $actualVapourPressure = 6.112 * pow(10,7.5*$dewC/(237.7+$dewC)); $guessedVapourPressure > $actualVapourPressure ? $tHigher = $wetGuess : $tLower = $wetGuess; } $wbF = number_format(1.8 * $wetGuess + 32, 1); $decodeInfo['WET_BULB'] = round($wetGuess) . " °Celsius (" . $wbF . " °Fahrenheit)"; } /* Source: http://www.ringbell.co.uk/info/humid.htm Some JavaScript to calculate wet bulb when given temp and dew-point in F and Atmos pressure in mb // display wet bulb if not supplied by user // This is done by repeated iteration until the guesses at each // pass of the loop vary by less than 0.005 var tHigher = temp; var tLower = DewPoint; var WetGuess = DewPoint; while(true) { var PreviousGuess = WetGuess; // half way between previous values WetGuess = (tLower + tHigher)/2; if(Math.abs(PreviousGuess - WetGuess) < 0.005) break; // use the guessed wet bulb temp to calculate actual vapour pressure var EwGuess = 6.112 * (Math.pow(Math.E, ((17.67 * WetGuess) / (243.5 + WetGuess)))); var ActVpGuess = EwGuess - (0.00066 * (1 + 0.00115 * WetGuess) * (DryBulb - WetGuess) * Atmos); // how does this compare with E from the dew point var EDelta = E - ActVpGuess; if(EDelta < 0) { tHigher = WetGuess; }else { tLower = WetGuess; } } TextOut += 'Wet Bulb temperature as '; if(FahrFlag){ TextOut += RoundIt(Ce2Fr(WetGuess)) + 'ºF'; }else{ TextOut += RoundIt(WetGuess) + 'ºC'; } */ //------------------------------------------------------------------------- // Decodes temperature and dew point information. Relative humidity and apparent temperature 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' preceding the tt or dd indicates a // negative temperature. Some stations do not report dew point, so the // format is tt/ or tt/XX. function rev_get_temperature($temp, $dewPoint) // Extended version of Ken True's function of similar name { global $decodeInfo, $decodeGroupCount, $tempC, $dewC; // last two made global as used for wet bulb calculation $tempC = 1 * strtr($temp, 'M', '-'); $tempF = number_format(1.8 * $tempC + 32, 1); $decodeInfo['TEMP'] = $tempC . " °Celsius (" . $tempF . " °Fahrenheit)"; $decodeGroupCount['TEMP'] ++; // Numerical count used as this sub-function called from mandatory Groups, but can also be called from RMK Group if(array_key_exists('WINDKPH', $decodeInfo)&& $decodeInfo['WINDKPH'] !== '0') rev_get_wind_chill($tempC); if ($dewPoint == '00' or is_numeric(substr($dewPoint, 1))) { $dewC = 1 * strtr($dewPoint, 'M', '-'); $actualVapourPressure = 6.112 * pow(10,7.5*$dewC/(237.7+$dewC)); $decodeInfo['ABS_HUMIDITY'] = round(($actualVapourPressure * 100000.0) /(($dewC + 273.16)*461.5)); $dewF = number_format(1.8 * $dewC + 32, 1); $decodeInfo['DEWPT'] = $dewC . " °C (" . $dewF . " °F)"; $rh = round(100 * pow((112 - (0.1 * $tempC) + $dewC) / (112 + (0.9 * $tempC)), 8)); rev_get_heat_index($tempF, $rh); calculate_humidex($tempC, $dewC); $decodeInfo['REL_HUMIDITY'] = $rh; if(array_key_exists('WINDMPS', $decodeInfo)) calculate_Australian_apparent_temperature($tempC, $rh); }else // USA FH1: 12.6.10 Temperature/Dew Point Group (T'T'/T'dT'd). An example, a temperature of 1.5°C and a missing dew point would be coded as "02/". $decodeInfo['DEWPT'] = 'Not measured'; } //---------------------------------------------------------------- // The sub-functions that follow are called from the sub-function for supplementary Group // in 'decodeMETAR.php' directly or are called by another one of the sub-functions below. // For each sub-function, there are notes explaining origin of that code sequence. //---------------------------------------------------------------- // There is one sub-function per supplementary Group, the relevant sub-functions are listed in the order that they appear in the standard function test_recent() { # define error handling for development environment, feel free to comment out (or adjust) next line if you use this script in a production environment # error_reporting(E_ALL); // show all errors is requested as although there should not be any errors in this finalised script, highlighting any that do occur rather than hiding them shows confidence! global $metar_toDecode, $decodeInfo, $decodeGroupProcessed, $decodeGroupCount, $compass, $pressureHPA, $show_diagnostics, $dM, $dMsf; // Global Variables can be shared with calling script and with other functions or scripts; // the usage description for these global arrays and variables follows: // # The METAR contents are loaded into $metar_toDecode, each space found in the (cleaned) METAR is treated as the separator between elements in this still to be decoded array. # As each element of that array is matched to a standard METAR Group and decoded, it is removed from this still to be decoded array. This script will identify if multiple array elements # belong to the same METAR group, whether more than one array element is required to define a single instance of the METAR Group, or because there are multiple instances of that METAR Group. // # The text created from decoding the METAR is assigned to elements of the array $decodeInfo, each Group in the METAR can assign values to one or more elements in this information array. # Any external script calling this script can either use "if(array_key_exists('element_name', $decodeInfo))" to check for existence of text for that element of the METAR group after the decode, # (more than one element_name can be used for a particular Group to allow the external calling script to have a choice on what it reports), or the external script can check for non-zero # $decodeGroupCount['group_name'] as that indicates whether a particular Group has been decoded or not, and it also counts how many instances were decoded in that Group. #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# # Parse METAR for (conditional) Recent Phenomena Group # # (up to 3 segments permitted) # #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# /* Canada Manual of Surface Weather Observations Seventh edition (April 2015) has removed the standard for the reporting of Recent Weather (RE) phenomena in METAR/SPECI to comply with ICAO Annex 3 Eighteenth Edition July 2013, Appendix 3 Section 4.8.1.1 removal of the requirement for Recent Weather (RE) phenomena where METAR/SPECI are issued. */ // WMO regulations paragraph 15.13.2 // UK CAA METAR Coding Rules paragraphs 4.125 to 4.131 cover this. The same sub-function 'novel_decode_phenomena' as used for present weather is used. // http://www.met.tamu.edu/class/metar/metar-pg14-rmk-inter.html // Australian BOM do optionally use this // Netherlands use a single 'RE//' to indicate that no recent weather details are available. $matched[0] = false; $decodeGroupCount['RECENT'] = 0; if($metar_toDecode[0] == "RE//") { $decodeInfo['RECENT'] = "No recent weather details are available"; $decodeGroupProcessed['RECENT'] = trim(array_shift($metar_toDecode)); // take out processed segment $decodeGroupCount['RECENT'] ++; $returnArray = array(); $returnArray['Info'] = $decodeInfo; $returnArray['GroupProcessed'] = $decodeGroupProcessed; $returnArray['mtr_toDecode'] = $metar_toDecode; $returnArray['GroupCount'] = $decodeGroupCount; $returnArray['Others']= 'RECENT'; return $returnArray; } while (count($metar_toDecode) > 0 and substr($metar_toDecode[0],0,2) == "RE" and $decodeGroupCount['RECENT'] < 4){ if($show_diagnostics) echo "
Examining $metar_toDecode[0] against recent phenomena Group
"; // conditional de-bugging section continues # if($show_diagnostics) print_array($metar_toDecode);// print each element of array to show segments left to be decoded for de-bugging purposes $recent = novel_decode_phenomena(substr($metar_toDecode[0],2),'recent'); if(array_key_exists(0, $recent) and strlen($recent[0]) > 1) { if(!array_key_exists('RECENT', $decodeInfo)) { $decodeGroupProcessed['RECENT'] = $decodeInfo['RECENT'] = ""; }else{ $decodeGroupProcessed['RECENT'] .= " + "; } $decodeGroupCount['RECENT'] ++; $decodeInfo['RECENT'] .= $recent[0]; $decodeGroupProcessed['RECENT'] .= $recent[1]; $matched[0]= 'RECENT'; } } $returnArray = array(); $returnArray['Info'] = $decodeInfo; $returnArray['GroupProcessed'] = $decodeGroupProcessed; $returnArray['mtr_toDecode'] = $metar_toDecode; $returnArray['GroupCount'] = $decodeGroupCount; $returnArray['Others']= $matched[0]; return $returnArray; } // There is one sub-function per supplementary Group, the relevant sub-functions are listed in the order that they appear in the standard function test_wind_shear() { # define error handling for development environment, feel free to comment out (or adjust) next line if you use this script in a production environment # error_reporting(E_ALL); // show all errors is requested as although there should not be any errors in this finalised script, highlighting any that do occur rather than hiding them shows confidence! global $metar_toDecode, $decodeInfo, $decodeGroupProcessed, $decodeGroupCount, $compass, $pressureHPA, $show_diagnostics, $dM, $dMsf; // Global Variables can be shared with calling script and with other functions or scripts; // the usage description for these global arrays and variables follows: // # The METAR contents are loaded into $metar_toDecode, each space found in the (cleaned) METAR is treated as the separator between elements in this still to be decoded array. # As each element of that array is matched to a standard METAR Group and decoded, it is removed from this still to be decoded array. This script will identify if multiple array elements # belong to the same METAR group, whether more than one array element is required to define a single instance of the METAR Group, or because there are multiple instances of that METAR Group. // # The text created from decoding the METAR is assigned to elements of the array $decodeInfo, each Group in the METAR can assign values to one or more elements in this information array. # Any external script calling this script can either use "if(array_key_exists('element_name', $decodeInfo))" to check for existence of text for that element of the METAR group after the decode, # (more than one element_name can be used for a particular Group to allow the external calling script to have a choice on what it reports), or the external script can check for non-zero # $decodeGroupCount['group_name'] as that indicates whether a particular Group has been decoded or not, and it also counts how many instances were decoded in that Group. #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# # Parse METAR for (conditional) Wind Shear Group (single segment) # # A change in wind speed and/or direction over a short distance # #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# // WMO regulations paragraph 15.13.3 Format either WS RDrDr or WS ALL RWY // This Group not used in METAR issued by UK nor USA nor Belgium nor Luxembourg // Australian BOM do optionally use this Group - Wind Shear €“ reports of wind shear experienced on take-off or landing are given after the indicator WS, e.g. WS R16. // Environment and Climate in Canada ManObs paragraph 16.3.12 /* ICAO defines the vertical and horizontal components of wind shear as follows: Vertical wind shear is defined as change of horizontal wind direction and/or speed with height, as would be determined by means of two or more anemometers mounted at different heights on a single mast. Horizontal wind shear is defined as change of horizontal wind direction and/or speed with horizontal distance, as would be determined by two or more anemometers mounted at the same height along a runway. */ $matched[0] = false; if($metar_toDecode[0] == "WS"){ $matched[0] = "WS"; if($metar_toDecode[1] == 'ALL' and $metar_toDecode[2] == 'RWY') { $decodeGroupProcessed['WIND_SHEAR'] = "WS_ALL_RWY"; $decodeInfo['WIND_SHEAR'] = "Wind Shear warning applies to all runways"; $metar_toDecode = array_slice($metar_toDecode, 3); // take out processed segments }elseif(substr($metar_toDecode[1],0,1) == "R") { $decodeGroupProcessed['WIND_SHEAR'] = "WS_" . $metar_toDecode[1]; $decodeInfo['WIND_SHEAR'] = "Wind Shear reported on runway " . substr($metar_toDecode[1],1,2); $metar_toDecode = array_slice($metar_toDecode, 2); // take out processed segments } if($show_diagnostics) echo "Matched against Wind Shear Group content is " . $decodeGroupProcessed['WIND_SHEAR']; // conditional de-bugging section continues } $returnArray = array(); $returnArray['Info'] = $decodeInfo; $returnArray['GroupProcessed'] = $decodeGroupProcessed; $returnArray['mtr_toDecode'] = $metar_toDecode; $returnArray['GroupCount'] = $decodeGroupCount; $returnArray['Others']= $matched[0]; return $returnArray; } // There is one sub-function per supplementary Group, the relevant sub-functions are listed in the order that they appear in the standard function test_sea_state() { # define error handling for development environment, feel free to comment out (or adjust) next line if you use this script in a production environment # error_reporting(E_ALL); // show all errors is requested as although there should not be any errors in this finalised script, highlighting any that do occur rather than hiding them shows confidence! global $metar_toDecode, $decodeInfo, $decodeGroupProcessed, $decodeGroupCount, $compass, $pressureHPA, $show_diagnostics, $dM, $dMsf; // Global Variables can be shared with calling script and with other functions or scripts; // the usage description for these global arrays and variables follows: // # The METAR contents are loaded into $metar_toDecode, each space found in the (cleaned) METAR is treated as the separator between elements in this still to be decoded array. # As each element of that array is matched to a standard METAR Group and decoded, it is removed from this still to be decoded array. This script will identify if multiple array elements # belong to the same METAR group, whether more than one array element is required to define a single instance of the METAR Group, or because there are multiple instances of that METAR Group. // # The text created from decoding the METAR is assigned to elements of the array $decodeInfo, each Group in the METAR can assign values to one or more elements in this information array. # Any external script calling this script can either use "if(array_key_exists('element_name', $decodeInfo))" to check for existence of text for that element of the METAR group after the decode, # (more than one element_name can be used for a particular Group to allow the external calling script to have a choice on what it reports), or the external script can check for non-zero # $decodeGroupCount['group_name'] as that indicates whether a particular Group has been decoded or not, and it also counts how many instances were decoded in that Group. #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# # Parse METAR for (conditional) Sea surface temperature and state of # # sea Group (single segment) Also known as ('Buoy' sites) # # 'Water temperature and significant wave height (North Sea platforms)' # # See 'http://www.ndbc.noaa.gov/' # #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# // WMO regulations paragraph 15.13.5 // The sea-surface temperature shall, by regional agreement, be reported according to the ICAO Regulation 15.11 if($show_diagnostics) echo "
Examining against sea surface Group "; // conditional de-bugging section continues if($show_diagnostics) print_array($metar_toDecode);// print each element of array to show segments left to be decoded for de-bugging purposes $matched[0] = false; if(substr($metar_toDecode[0],0,1) == "W" and substr($metar_toDecode[0],3,1) == "/") { # issued by offshore installations only - seen at EHSA e.g. W19/H9 # also reported by UK Met Office run lightships e.g. 'F3' in North Sea (Dover Straits end) marine location id=62170 # see 'https://www.metoffice.gov.uk/public/weather/marine-observations/#?tab=last24hoursMarine&marineLocId=162170 (air temp, sea temp, dew-point in columns on right) # format WtT/SS (W prefix mandatory, tT sea surface temperature as integer Celsius, SS sea state) or WtT/HHH (what is this?) $decodeGroupCount['SEA_S'] = 1; $decodeInfo['SEA_S'] = "Sea Surface Temperature=" . substr($metar_toDecode[0],1,2) . " °C"; if(substr($metar_toDecode[0],4,1) != "H"){ $decodeInfo['SEA_S'] .= "; Sea State is "; switch(substr($metar_toDecode[0],4,1)) // Douglas Sea Scale 'wind sea' definition adopted by WMO - The state of the sea shall be reported in accordance with Code table 3700. { case 0: $decodeInfo['SEA_S'] .= "Calm (glassy)"; break; case 1: $decodeInfo['SEA_S'] .= "Calm (rippled)"; break; case 2: $decodeInfo['SEA_S'] .= "Smooth (wavelets)"; break; case 3: $decodeInfo['SEA_S'] .= "Slight"; break; case 4: $decodeInfo['SEA_S'] .= "Moderate"; break; case 5: $decodeInfo['SEA_S'] .= "Rough"; break; case 6: $decodeInfo['SEA_S'] .= "Very Rough"; break; case 7: $decodeInfo['SEA_S'] .= "High"; break; case 8: $decodeInfo['SEA_S'] .= "Very High"; break; case 9: $decodeInfo['SEA_S'] .= "Phenomenal"; break; case '/': $decodeInfo['SEA_S'] = " (Water temperature only)"; } $decodeInfo['SEA_S'] .= "; Character of sea swell="; switch(substr($metar_toDecode[0],5,1)) { case 0: $decodeInfo['SEA_S'] .= "None"; break; case 1: $decodeInfo['SEA_S'] .= "Low: Short or average"; break; case 2: $decodeInfo['SEA_S'] .= "Low: Long"; break; case 3: $decodeInfo['SEA_S'] .= "Moderate: Short"; break; case 4: $decodeInfo['SEA_S'] .= "Moderate: Average"; break; case 5: $decodeInfo['SEA_S'] .= "Moderate: Long"; break; case 6: $decodeInfo['SEA_S'] .= "High: Short"; break; case 7: $decodeInfo['SEA_S'] .= "High: Average"; break; case 8: $decodeInfo['SEA_S'] .= "High: Long"; break; case 9: $decodeInfo['SEA_S'] .= "Confused"; break; case '/': $decodeInfo['SEA_S'] .= "not measured"; } }else{ switch(substr($metar_toDecode[0],5)){ // Wave height (modified from WMO code table 1555) case 0: $decodeInfo['SEA_S'] .= "; Significant Wave Height= calm"; break; case 1: $decodeInfo['SEA_S'] .= "; Significant Wave Height=" . 0.5 . " metres"; break; case 2: $decodeInfo['SEA_S'] .= "; Significant Wave Height=" . 1 . " metres"; break; case 3: $decodeInfo['SEA_S'] .= "; Significant Wave Height=" . 1.5 . " metres"; break; case 4: $decodeInfo['SEA_S'] .= "; Significant Wave Height=" . 2 . " metres"; break; case 5: $decodeInfo['SEA_S'] .= "; Significant Wave Height=" . 2.5 . " metres"; break; case 6: $decodeInfo['SEA_S'] .= "; Significant Wave Height=" . 3 . " metres"; break; case 7: $decodeInfo['SEA_S'] .= "; Significant Wave Height=" . 3.5 . " metres"; break; case 8: $decodeInfo['SEA_S'] .= "; Significant Wave Height=" . 4 . " metres"; break; case 9: $decodeInfo['SEA_S'] .= "; Significant Wave Height=" . 4.5 . " metres"; break; case 10: $decodeInfo['SEA_S'] .= "; Significant Wave Height=" . 5 . " metres"; break; case 11: $decodeInfo['SEA_S'] .= "; Significant Wave Height=" . 5.5 . " metres"; break; case 12: $decodeInfo['SEA_S'] .= "; Significant Wave Height=" . 6 . " metres"; break; case 13: $decodeInfo['SEA_S'] .= "; Significant Wave Height=" . 6.5 . " metres"; break; case 14: $decodeInfo['SEA_S'] .= "; Significant Wave Height=" . 7 . " metres"; break; case 15: $decodeInfo['SEA_S'] .= "; Significant Wave Height=" . 7.5 . " metres"; break; case 16: $decodeInfo['SEA_S'] .= "; Significant Wave Height=" . 8 . " metres"; break; case 17: $decodeInfo['SEA_S'] .= "; Significant Wave Height=" . 8.5 . " metres"; break; case 18: $decodeInfo['SEA_S'] .= "; Significant Wave Height=" . 9 . " metres"; break; case 19: $decodeInfo['SEA_S'] .= "; Significant Wave Height=" . 9.5 . " metres"; break; case 20: $decodeInfo['SEA_S'] .= "; Significant Wave Height=" . 10 . " metres"; break; case 21: $decodeInfo['SEA_S'] .= "; Significant Wave Height=" . 10.5 . " metres"; break; case 22: $decodeInfo['SEA_S'] .= "; Significant Wave Height=" . 11 . " metres"; break; case 23: $decodeInfo['SEA_S'] .= "; Significant Wave Height=" . 11.5 . " metres"; break; case 24: $decodeInfo['SEA_S'] .= "; Significant Wave Height=" . 12 . " metres"; break; case 25: $decodeInfo['SEA_S'] .= "; Significant Wave Height=" . 12.5 . " metres"; break; case 26: $decodeInfo['SEA_S'] .= "; Significant Wave Height=" . 13 . " metres"; break; case 27: $decodeInfo['SEA_S'] .= "; Significant Wave Height > " . 13 . " metres"; break; case '//': $decodeInfo['SEA_S'] .= "; Wave height not measured."; } } $decodeGroupProcessed['SEA_S'] = trim(array_shift($metar_toDecode)); // take out processed segment if($show_diagnostics) echo "Matched against Sea State Group content is " . $decodeGroupProcessed['SEA_S']; // conditional de-bugging section continues $matched[0] = 'SEA_S'; } $returnArray = array(); $returnArray['Info'] = $decodeInfo; $returnArray['GroupProcessed'] = $decodeGroupProcessed; $returnArray['mtr_toDecode'] = $metar_toDecode; $returnArray['GroupCount'] = $decodeGroupCount; $returnArray['Others']= $matched[0]; return $returnArray; } // There is one sub-function per supplementary Group, the relevant sub-functions are listed in the order that they appear in the standard function test_runway_state() { # define error handling for development environment, feel free to comment out (or adjust) next line if you use this script in a production environment # error_reporting(E_ALL); // show all errors is requested as although there should not be any errors in this finalised script, highlighting any that do occur rather than hiding them shows confidence! global $metar_toDecode, $decodeInfo, $decodeGroupProcessed, $decodeGroupCount, $compass, $show_diagnostics, $dM, $dMsf; #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# # Parse METAR for (conditional) Runway State Group # # ( According to FAA in USA up to 4 segments permitted in other countries) # #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# // UK CAA METAR Coding Rules paragraphs 4.132 to 4.144 // The Runway State Group in a METAR is called "SNOWTAM" report by some references (typically those that talk about "NOTAM" for the "Notices to Airmen") // Some sources say this Group comes after the Trend & Changes Groups, but all the official references have it between Sea State Group and Trend Group $decodeGroupCount['RUNWAY'] = 0; $matched[0] = false; // NB other Groups start with 'R', but in this position it is Runway state if(count($metar_toDecode) > 0 and substr($decodeGroupProcessed['BY'], 0, 4) == 'AUTO' and substr($metar_toDecode[0],0,1) == "R"){ // not reported if automatic METAR - paragraph 4.144 while (count($metar_toDecode) > 0 and substr($metar_toDecode[0],0,1) == "R" and $decodeGroupCount['RUNWAY'] < 5) # Nothing in official guidance to say how many allowed, included limit of 5 to prevent looping, in practice likely to only be reporting for 1 or 2 runways { if($show_diagnostics) print_array($metar_toDecode, "
Examining against runway state Group in test_runway_state()");// segments left to be decoded if(!array_key_exists('RUNWAY', $decodeInfo))$decodeInfo['RUNWAY'] = ""; else $decodeInfo['RUNWAY'] .= "; "; $segment = $metar_toDecode[0]; $runway_number = substr($segment,1,2); switch($runway_number){ /// WMO Regulation 15.7.3 case '/S': case 88: $decodeInfo['RUNWAY'] .= "All Runways: "; break; // Additional code figures 88 and 99 are reported in accordance with the European Air Navigation Plan, FASID, Part III-AOP, Attachment A.: case 99: $decodeInfo['RUNWAY'] .= "No change from last report "; break; default: $decodeInfo['RUNWAY'] .= "Runway " . $runway_number . " "; if($runway_number > 50) $runway_number = ($runway_number - 50) . " Right"; // This might be obsolete? http://metaf2xml.sourceforge.net/metaf2xml/parser.html switch(substr($segment,3,1)){ case 'L': $decodeInfo['RUNWAY'] .= "Left end "; break; case 'R': $decodeInfo['RUNWAY'] .= "Right end "; break; case 'C': $decodeInfo['RUNWAY'] .= "Centre "; break; } } if($show_diagnostics) echo "
Matched against Runway State Group " . " with runway=" . $runway_number; // conditional de-bugging section continues # slash "/" added into Group after "RDrDr" by ICAO agreement at Geneva 2008 meeting if(strpos($segment,"/") == 2 or strpos($segment,"/") > 4) decode_remarks(); // NB conditional character at position 3, so slash can be at 1, 3 or 4 (also NB not found and at start both equate to 0 unless === used) $runway = substr($metar_toDecode[0],(1 + strpos($segment,"/"))); if($show_diagnostics) echo "
decoding " . $runway; if($runway == "SNOCLO") $decodeInfo['RUNWAY'] .= "Aerodrome closed due to contaminated runways (e.g. by snow)"; else{ if(substr($runway,0,4) == "CLRD" or $runway == 'D') $decodeInfo['RUNWAY'] .= "Contamination removed: "; else{ // First character - runway deposits $E_designator = substr($runway,0,1); if($show_diagnostics) echo "
decoding element " . $E_designator; switch($E_designator){ case 0: $decodeInfo['RUNWAY'] .= "is/are Clear and dry; "; break; case 1: $decodeInfo['RUNWAY'] .= "is/are Damp; "; break; case 2: $decodeInfo['RUNWAY'] .= "is/are Wet or has/have water patches; "; break; case 3: $decodeInfo['RUNWAY'] .= "is/are Rhime or frost covered; "; break; case 4: $decodeInfo['RUNWAY'] .= "has/have Dry snow; "; break; case 5: $decodeInfo['RUNWAY'] .= "has/have Wet snow; "; break; case 6: $decodeInfo['RUNWAY'] .= "has/have Slush; "; break; case 7: $decodeInfo['RUNWAY'] .= "has/have Ice; "; break; case 8: $decodeInfo['RUNWAY'] .= "has/have Compacted or rolled snow; "; break; case 9: $decodeInfo['RUNWAY'] .= "has/have Frozen ruts or ridges; "; break; case '/': $decodeInfo['RUNWAY'] .= "is/are currently undergoing Clearance; "; } // Second character - extent of contamination $C_designator = substr($runway,1,1); if($show_diagnostics) echo "
decoding coverage " . $C_designator; switch($C_designator){ case 1: $decodeInfo['RUNWAY'] .= "Affecting < 11%; "; break; case 2: $decodeInfo['RUNWAY'] .= "Affecting 11% to 25%; "; break; case 5: $decodeInfo['RUNWAY'] .= "Affecting 26% to 50%; "; break; case 9: $decodeInfo['RUNWAY'] .= "Affecting > 50%; "; break; case '/': $decodeInfo['RUNWAY'] .= "Changing due to Clearance; "; } // Third and Fourth characters - depth of deposit $depth = substr($runway,2,2); if($show_diagnostics) echo "
decoding depth " . $depth; if($depth > 0 and $depth < 91) $decodeInfo['RUNWAY'] .= "Depth of deposit " . $depth . " mm; "; else switch ($depth) { case '00': $decodeInfo['RUNWAY'] .= "Depth of deposit < 1 mm; "; break; case 92: $decodeInfo['RUNWAY'] .= "Depth of deposit 10 cm; "; break; case 93: $decodeInfo['RUNWAY'] .= "Depth of deposit 15 cm; "; break; case 94: $decodeInfo['RUNWAY'] .= "Depth of deposit 20 cm; "; break; case 95: $decodeInfo['RUNWAY'] .= "Depth of deposit 25 cm; "; break; case 96: $decodeInfo['RUNWAY'] .= "Depth of deposit 30 cm; "; break; case 97: $decodeInfo['RUNWAY'] .= "Depth of deposit 35 cm; "; break; case 98: $decodeInfo['RUNWAY'] .= "Depth of deposit >= 40 cm; "; break; case 99: $decodeInfo['RUNWAY'] .= "Runway not operational; "; break; case '//': $decodeInfo['RUNWAY'] .= "Depth of deposit not reported; "; break; } } // Last 2 characters - Friction coefficient / Braking action $breaking_action = substr($runway,-2); switch ($breaking_action){ case '//': $decodeInfo['RUNWAY'] .= "Braking action not reported when runway closed"; break; case 91: $decodeInfo['RUNWAY'] .= "Braking action poor (friction co-efficient < 25%)"; break; case 92: $decodeInfo['RUNWAY'] .= "Braking action medium/poor (25 % < friction co-efficent < 30%)"; break; case 93: $decodeInfo['RUNWAY'] .= "Braking action medium (29 % < friction co-efficent < 36%); "; break; case 94: $decodeInfo['RUNWAY'] .= "Braking action medium/good (35 % < friction co-efficent < 40%)"; break; case 95: $decodeInfo['RUNWAY'] .= "Braking action good (friction co-efficient > 39%)"; break; case 99: $decodeInfo['RUNWAY'] .= "Braking action has not been reliably determined, but runway in use"; break; default: $decodeInfo['RUNWAY'] .= "Friction co-efficient " . $breaking_action . " %"; } } $decodeGroupCount['RUNWAY'] ++; if($decodeGroupCount['RUNWAY'] == 1) $decodeGroupProcessed['RUNWAY'] = ''; else $decodeGroupProcessed['RUNWAY'] .= "_"; $done = trim(array_shift($metar_toDecode)); // take out processed segment $decodeGroupProcessed['RUNWAY'] .= $done; if($show_diagnostics) echo "
Matched Runway State Group with " . $done; // conditional de-bugging section continues $matched[0] = 'RUNWAY'; } } $returnArray = array(); $returnArray['Info'] = $decodeInfo; $returnArray['GroupProcessed'] = $decodeGroupProcessed; $returnArray['mtr_toDecode'] = $metar_toDecode; $returnArray['GroupCount'] = $decodeGroupCount; $returnArray['Others']= $matched[0]; return $returnArray; } // This is a sub-function for a Group, that does not appear in the standard function colour_state() { # define error handling for development environment, feel free to comment out (or adjust) next line if you use this script in a production environment # error_reporting(E_ALL); // show all errors is requested as although there should not be any errors in this finalised script, highlighting any that do occur rather than hiding them shows confidence! global $metar_toDecode, $decodeInfo, $decodeGroupProcessed, $decodeGroupCount, $compass, $pressureHPA, $show_diagnostics, $dM, $dMsf, $colour_array; // Global Variables can be shared with calling script and with other functions or scripts; // the usage description for these global arrays and variables follows: // # The METAR contents are loaded into $metar_toDecode, each space found in the (cleaned) METAR is treated as the separator between elements in this still to be decoded array. # As each element of that array is matched to a standard METAR Group and decoded, it is removed from this still to be decoded array. This script will identify if multiple array elements # belong to the same METAR group, whether more than one array element is required to define a single instance of the METAR Group, or because there are multiple instances of that METAR Group. // # The text created from decoding the METAR is assigned to elements of the array $decodeInfo, each Group in the METAR can assign values to one or more elements in this information array. # Any external script calling this script can either use "if(array_key_exists('element_name', $decodeInfo))" to check for existance of text for that element of the METAR group after the decode, # (more than one element_name can be used for a particular Group to allow the external calling script to have a choice on what it reports), or the external script can check for non-zero # $decodeGroupCount['group_name'] as that indicates whether a particular Group has been decoded or not, and it also counts how many instances were decoded in that Group. // Example from RAF Northolt - airfield currently closed, but observer reporting conditions prior to start of operations: // 2018/02/14 07:19 METAR EGWU 140719Z 13003KT CAVOK M01/M02 Q1006 BLACKBLU NOSIG // Example from RAF Benson - airfield open and weather conditions excellent: // 2018/02/14 06:50 METAR EGUB 140650Z 16004KT CAVOK M01/M03 Q1005 BLU NOSIG #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# # Parse METAR for (conditional) Colour State Group # # Used by some aerodromes as indication of their STATE for flights. # # Represents a quick assimilation of worst visibility/cloud # # Appears as either Cloud/Visibility or Trend Group (or both) METAR # # extension (single segment added to end of either of the two Groups) # #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# /* https://en.wikipedia.org/wiki/Colour_state says the following: Meteorological colour states are determined by the relevant worst condition from the visibility and significant cloud height. WMO say Colour State is used in Belgium (vol II page 15 has figures corresponding to colours, but they mention 'YLO' not the suffixes 1 and 2). In the United Kingdom, Belgium, France and the Netherlands colour state reported relates to the lowest significant cloud layer that is three oktas or more. In the USA and in the other parts of Europe the relevant lowest significant cloud layer is any with five or more oktas. */ //------------------------------ $matched[0] = false; if(count($metar_toDecode) < 1 or $decodeGroupCount['STATE'] > 2) return $matched; if($show_diagnostics) { echo "
Examining " . $metar_toDecode[0] . ", the remaining METAR against Colour State Group (for airfield STATE) on pass " . $decodeGroupCount['STATE']; // conditional de-bugging section continues echo "
Looking for "; print_array($colour_array); } $array_index_C = 'COLOUR' . $decodeGroupCount['STATE']; $array_index_F = 'STATE' . $decodeGroupCount['STATE']; $qualifier = $decodeGroupCount['STATE'] ? "Forecast state: " : "Present state: "; if(substr($metar_toDecode[0],0,5) == 'BLACK') { if($show_diagnostics) echo "
Processing 'BLACK' code in " . $metar_toDecode[0]; // conditional de-bugging section continues $decodeInfo[$array_index_C] = 'Black'; $decodeInfo[$array_index_F] = 'Aerodrome closed (for non-weather reason) - '; $partLocal = substr($metar_toDecode[0],5); }else { if($show_diagnostics) echo "
Processing of " . $metar_toDecode[0] . " on decoding $qualifier entry " . $decodeGroupCount['STATE']; // conditional de-bugging section continues $partLocal = $metar_toDecode[0]; $decodeInfo[$array_index_F] = ""; } if (in_array($partLocal, $colour_array)) { $decodeGroupProcessed[$array_index_C] = trim(array_shift($metar_toDecode)); // take out processed segment if($show_diagnostics) echo "
Matched $qualifier Colour State Group with " . $decodeGroupProcessed[$array_index_C] . "
"; // conditional de-bugging section continues $decodeGroupCount['STATE'] ++; # advance count to indicate colour has been set switch ($partLocal){ case 'BLU+': case 'BLU': # Example METAR from Rostock Laage Airport, Germany: METAR ETNL 011220Z 33005KT 9999 SCT018 BKN100 15/10 Q1020 BLU+ # Assume plus is not an alternative to equals to denote end of METAR, but a variant on best STATE $partLocal == 'BLU' ? $decodeInfo[$array_index_C] = "Blue" : $decodeInfo[$array_index_C] = "Blue plus"; $decodeInfo[$array_index_F] .= 'Best fitness as lowest Cloud base >= 2,500 ft and minimum Visibility >= 8 km'; break; case 'WHT': $decodeInfo[$array_index_C] = "White"; $decodeInfo[$array_index_F] .= 'Very Good fitness as Cloud base is 1,500 to 2,499 ft and minimum Visibility >= 5 km or Cloud base is >= 2,500 ft and minimum Visibility is 5 to 7.999 km'; break; case 'GRN': $decodeInfo[$array_index_C] = "Green"; $decodeInfo[$array_index_F] .= 'Good fitness as Cloud base is 699 to 1,499 ft and minimum Visibility >= 3.7 km or Cloud base is >= 700 feet and minimum visibility 3.7 to 4.999 km'; break; case 'YLO1': $decodeInfo[$array_index_C] = "Yellow"; $decodeInfo[$array_index_F] .= 'Warning ' . $partLocal . ' due to low Cloud 500 to 699 ft or poor Visibility 2.5 to 3.699 km'; break; case 'YLO2': $decodeInfo[$array_index_C] = "Yellow"; $decodeInfo[$array_index_F] .= 'Warning ' . $partLocal . ' due to low Cloud 300 to 499 ft or poor Visibility 1.6 to 2.499 km'; break; case 'YLO': $decodeInfo[$array_index_C] = "Yellow"; $decodeInfo[$array_index_F] .= 'Warning ' . $partLocal . ' as Cloud base 300 to 699 ft and minimum Visibility 1.6 km or Cloud base >= 300 feet and minimum Visibility is 1.6 to 3.699 km'; break; case 'AMB': $decodeInfo[$array_index_C] = "Amber"; $decodeInfo[$array_index_F] .= 'Poor fitness as Cloud base > 200 to 299 ft and minimum Visibility 1.6 km or Cloud base >= 200 feet and minimum Visibility is 800 m to 1.599 km'; break; case 'RED': $decodeInfo[$array_index_C] = "Red"; $decodeInfo[$array_index_F] .= 'Worst fitness due to Cloud base < 200 ft or minimum Visibility < 800 m'; break; } $matched[0]= $array_index_C; if(file_exists('../metar/extra-images') ) //"metar\extra-images\Blue.png" { if(strlen($decodeGroupProcessed[$array_index_C]) > 5 and substr($decodeGroupProcessed[$array_index_C],0,5) == 'BLACK') { $decodeInfo[$array_index_C] = '' . $qualifier . ''; $decodeInfo[$array_index_C] .= 'black flag - airfield closed'; $decodeInfo[$array_index_C] .= "" . $decodeInfo[$array_index_C] . " flag "; } else $decodeInfo[$array_index_C] = "" . $qualifier . "" . $decodeInfo[$array_index_C] . " flag "; } } $returnArray = array(); $returnArray['Info'] = $decodeInfo; $returnArray['GroupProcessed'] = $decodeGroupProcessed; $returnArray['mtr_toDecode'] = $metar_toDecode; $returnArray['GroupCount'] = $decodeGroupCount; $returnArray['Others'] = $matched[0]; return $returnArray; } // There is one sub-function per supplementary Group, the relevant sub-functions are listed in the order that they appear in the standard. // The "Change Group" is an optional supplementary Group that lists the way that features reported in WMO Standard Groups may change after the METAR is issued. // It therefore can indicate forecast changes in surface wind, predictions for prevailing visibility, how present weather might change, and changes in cloud cover. // To include items here there must be high confidence that the accuracy of the forecast is within the specified tolerances in the aviation weather specification. function changeGroup() { # define error handling for development environment, feel free to comment out (or adjust) next line if you use this script in a production environment # error_reporting(E_ALL); // show all errors is requested as although there should not be any errors in this finalised script, highlighting any that do occur rather than hiding them shows confidence! global $metar_toDecode, $decodeInfo, $decodeGroupProcessed, $decodeGroupCount, $compass, $compass_expanded, $pressureHPA, $show_diagnostics, $dM, $dMsf, $colour_array; // Global Variables can be shared with calling script and with other functions or scripts #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# # Parse METAR for (conditional) Change Group # #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# # Report forecast trend weather if present. if(count($metar_toDecode) < 1) goto function_exit; // The governing criteria for issuing trend forecasts are specified in publication WMO - No. 49 €“ Technical Regulations [C.3.1]. // WMO regulation paragraph 15.14.1 specifies that for METAR, trend forecasts are issued in a coded form // UK CAA METAR Coding Rules paragraphs 4.145 to 4.153 - report here if change criteria (published in the UKAIP GEN section) are expected to be crossed; wind, visibility, present weather, and cloud // not used in USA (see international variations in end comments) - http://www.met.tamu.edu/class/metar/metar-pg14-rmk-inter.html if($show_diagnostics) echo "
Examining against Change Group "; // conditional de-bugging section continues if($show_diagnostics) print_array($metar_toDecode);// print each element of array to show segments left to be decoded for de-bugging purposes $decodeGroupCount['TREND'] = 0; $checkedWind = false; $matched[0] = false; # $decodeInfo['TREND_TYPE'] .= ":"; $decodeInfo['CHANGE'] = $decodeGroupProcessed['CHANGE'] = ""; // initialise ready for later concatenates #---------------------------------------------------------------- # UK conditionally uses more segments so this code sequence added by SPAWS, # Remaining segments for 'Change Group' as final 1, 2, or 3 segments can report wind, visibility, # cloud or other predictions using codes declared previously. # (Neither WMO nor WMO define any specific order so this script flexible). # ICAO says Number of change indicators to be kept to a minimum ... # normally not exceeding three Groups, but WMO regulations don't say how many #---------------------------------------------------------------- #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# # Parse METAR for (conditional) Change Group details # # SURFACE WIND FORECAST SEGMENT # #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# // UK CAA METAR Coding Rules paragraph 4.149 - suggests forecasts to changes in surface wind included first, so test for that first # Check for forecast wind segment (conditional, maximum occurrence once) if($show_diagnostics) echo "
" . $metar_toDecode[0] . " applied to decode in sub-function 'novel_decode_wind' = 'forecast changes to surface wind'" ; if(!$checkedWind) $input = novel_decode_wind($metar_toDecode[0], 'change'); // if condition ensures that only check once if($input['matched']) { $decodeInfo['CHANGE'] = "Wind=" . $input['knots'] . " knots (" . $input['mph']. " mph) from " . $input['direction'] . " (" . $input['bearing'] . "); "; $decodeGroupCount['TREND'] ++; $checkedWind = true; // so don't check it again! $decodeGroupProcessed['CHANGE'] = trim(array_shift($metar_toDecode)); // take out processed segment if($show_diagnostics) echo " and found match in " . $decodeGroupProcessed['CHANGE']; unset ($input[0]); if(count($metar_toDecode) < 1) goto function_exit; }else { // As wind always first in sequence, assume that not finding it here confirms it does not need to be looked for again! $checkedWind = true; if($show_diagnostics) echo " but no match"; } #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# # Parse METAR for (conditional) Change Group details # # PREVAILING VISIBILITY FORECAST SEGMENT # #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# // UK CAA METAR Coding Rules paragraph 4.150 - suggests prevailing visibility included second so test it here # Check (conditional, maximum occurrence once) for forecast prevailing visibility after wind if($metar_toDecode[0] == 'CAVOK') { // UK CAA METAR Coding Rules paragraph 4.150 allows use of 'CAVOK', WMO volume II page 17 says Netherlands uses 'CAVOK' here if($show_diagnostics) echo "
" . $metar_toDecode[0] . " applied to decode in main function of 'forecast changes in visibility'" ; $decodeGroupCount['TREND'] ++; $done = trim(array_shift($metar_toDecode)); // take out processed segment if($show_diagnostics) echo " and found match in " . $done; if($decodeGroupProcessed['CHANGE'] == "") $decodeGroupProcessed['CHANGE'] = $done; else $decodeGroupProcessed['CHANGE'] .= " + " . $done; $decodeInfo['CHANGE'] .= 'Cloud and Visibilty are forecast to be OK, with no significant weather phenomena on the aerodrome; '; }else{ if($show_diagnostics) echo "
" . $metar_toDecode[0] . " in Supplementary Change Group examined for 'forecast visibility'" ; $visibility_matched = novel_allVisibility($metar_toDecode[0], "", "", "WMO Change Group"); if($visibility_matched['segments']) { $decodeInfo['CHANGE'] .= "Visibility=" . $visibility_matched['prevailVisibility'] . "; "; $decodeGroupCount['TREND'] ++; $done = trim(array_shift($metar_toDecode)); // take out processed segment if($show_diagnostics) echo " and found match in " . $done . "
"; if($decodeGroupProcessed['CHANGE'] == "") $decodeGroupProcessed['CHANGE'] = $done; else $decodeGroupProcessed['CHANGE'] .= " + " . $done; }else{ if($show_diagnostics) echo " but no match"; } if(count($metar_toDecode) < 1) goto function_exit; } # initialise boolean variables for 'for' loop $cloud_matched = false; $conditions_matched = false; // UK CAA METAR Coding Rules paragraph 4.151 - suggests forecast of changes to present weather (using present weather codes, except for 'nearby') follows before cloud cover changes // This was not found to be always followed in practice during testing of this script, so this script has introduced following loop to cope with either order for($recheck=0; $recheck < 2; $recheck++) // definitely sometimes find cloud before conditions and sometimes other way about, so visit each, and loop twice to cope with both orders { // UK CAA METAR Coding Rules paragraph 4.152 - suggests forecast of cloud cover can include anything allowed for current cloud cover # check if cloud report next (conditional, can be returned multiple times, so concatenate) if($show_diagnostics) echo "
" . $metar_toDecode[0] . " Examining this and successive segments against forecast changes to weather and cloud cover in 'Trend Group'
"; // conditional de-bugging section continues #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# # Parse METAR for (conditional) Change Group details # # CLOUD COVER FORECAST SEGMENTS # #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# # initialise boolean for 'while' loop $tested_cloud = $cloud_matched; // false means continue to test for cloud forecasts while(!$tested_cloud and 0 < count($metar_toDecode)) { if($show_diagnostics) echo "
" . $metar_toDecode[0] . "' applied to decode in sub-function 'novel_decode_cloud'" ; $input = novel_decode_cloud($metar_toDecode[0]); if(array_key_exists(0, $input)) { $decodeInfo['CHANGE'] .= $input[0] . " "; $input[0] = " "; } if(array_key_exists(1, $input) or array_key_exists(2, $input)){ $decodeInfo['CHANGE'] .= $input[1] . " " . $input[2] . "; "; $cloud_matched = true; $matched[0] = 'CHANGE'; unset ($input[1],$input[2]); $decodeGroupCount['TREND'] ++; if($show_diagnostics) echo " and found match"; if($decodeGroupProcessed['CHANGE'] == "") $decodeGroupProcessed['CHANGE'] = $input[4]; else $decodeGroupProcessed['CHANGE'] .= " + " . $input[4]; if(count($metar_toDecode) < 1) goto function_exit; }else{ $tested_cloud = true; // No cloud forecast found, don't test again in this while $matched[0] = false; } } #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# # Parse METAR for (conditional) Change Group details # # PRESENT WEATHER PHENOMENA FORECAST SEGMENT # #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# // UK CAA METAR Coding Rules paragraph 4.151 - suggests forecast of changes to present weather can use anything allowed in present weather codes, except for 'in vicinity' # check conditions after cloud (conditional, can be returned multiple times, so concatenate) # initialise boolean for while loop $tested_conditions = $matched[0]; while(!$tested_conditions and 0 < count($metar_toDecode)) { if(array_key_exists('STATE0', $decodeGroupCount) and in_array($metar_toDecode[0], $colour_array)) goto function_exit; if($show_diagnostics) echo "
" . $metar_toDecode[0] . " applied to decode sub-function 'novel_decode_phenomena'"; $input = novel_decode_phenomena($metar_toDecode[0],'change'); if(array_key_exists(0, $input) and array_key_exists(1, $input)) { if(!$conditions_matched)$decodeInfo['CHANGE'] .= "Phenomena="; $decodeInfo['CHANGE'] .= $input[0] . "; "; $decodeGroupCount['TREND'] ++; $conditions_matched = true; if($decodeGroupProcessed['CHANGE'] == "") $decodeGroupProcessed['CHANGE'] = $input[1]; else $decodeGroupProcessed['CHANGE'] .= " + " . $input[1]; if($show_diagnostics) echo " and found match
"; $matched[0] = 'CHANGE'; }else{ $tested_conditions = true; if($show_diagnostics) echo " but no match
"; $matched[0] = false; } unset ($input[0]); } #------------------------------ if(count($metar_toDecode) < 1) break; #------------------------------ } // end of repeating loop # N.B. If cloud or conditions found on first pass, then only other will be looked for on second loop, if both found first pass, neither examined on second pass function_exit: $returnArray = array(); $returnArray['Info'] = $decodeInfo; $returnArray['GroupProcessed'] = $decodeGroupProcessed; $returnArray['mtr_toDecode'] = $metar_toDecode; $returnArray['GroupCount'] = $decodeGroupCount; if(isset($matched)) $returnArray['Others'] = $matched[0]; return $returnArray; } // There is one sub-function per supplementary Group, the relevant sub-functions are listed in the order that they appear in the standard function decode_remarks() { global $metar_toDecode, $decodeInfo, $decodeGroupProcessed, $decodeGroupCount, $compass, $compass_expanded, $pressureHPA, $show_diagnostics, $show_diagnosticsR, $dM, $dMsf, $dM_Rmk; // Global Variables can be shared with calling script and with other functions or scripts; #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# # Parse METAR for (conditional) 'Aviation Weather Remarks Group' # # (up to any number of segments permitted, no WMO rules on content) # #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# // WMO regulation paragraph 15.15 says the national information in this Group shall not be disseminated internationally, in practice it is seldom stripped off during sharing. // It can contain any free text, but some countries publish documentation describing either some rules about format and content or giving examples of typical contents. // Frequently this will be country or station specific additional information: pressure, worst cloud cover, runway winds, relative humidity. // There are few on-line decoders that can handle any content here e.g. http://metaf2xml.sourceforge.net/metaf2xml/parser.html if(count($metar_toDecode) > 0 and $metar_toDecode[0] == "RMK") { # Split the Remark decoding into different scripts, main Remarks script loaded now we know that Remarks are included // Stored in same directory as this script $pos = strlen(basename(__FILE__)); $path = substr(__FILE__,0, -1 * $pos); if(file_exists($path . "decodeMETAR_Rmk.php")) { include_once($path . "decodeMETAR_Rmk.php"); clearstatcache(); if($show_diagnosticsR) echo "
Loading script to process the segments within Remarks Group"; $matched = new_get_remarks(); // The decoder for this group is separate because it is very complex! } } $returnArray = array(); $returnArray['Info'] = $decodeInfo; $returnArray['GroupProcessed'] = $decodeGroupProcessed; $returnArray['mtr_toDecode'] = $metar_toDecode; $returnArray['GroupCount'] = $decodeGroupCount; if(isset($matched)) $returnArray['Others'] = $matched[0]; return $returnArray; } //---------------------------------------------------------------- // The sub-functions that follow are called from the script for Remark Group // in 'decodeMETAR_Rmk.php' directly (itself called by sub-function above). // All sub-functions below are novel, and written specifically by SPAWS. //---------------------------------------------------------------- ################################################################################################################################### # Novel sub-function written by SPAWS to process a single output from decoding one or more segments in the Remarks Group. # # The inputs to this sub-function are an incrementing counter for the serial number of where Remark Group entry will appear, # # a count for how many segments from the raw METAR still need to be added to list of those processed to create this single # # output, the next parameter contains the text of the output so that it can be added for the serial number of the Remark # # output array, the next parameter contains text describing the specification that has been matched so it can be output # # as diagnostic, the final parameter is optional, included only if there were other segments processed for this output before # # this function was called (and so excluded from the segment count passed to this function). Whether that final parameter is # # included or not this function will output a list of those segments that have been processed in a revised format suitable # # for displaying when mouse hovers over the single output text processed by this function, and update the relevant elements # # of the $remarks_toDecode array to show what specification was matched to them. # ################################################################################################################################### function do_output($remarkIncrement, $segmentCount, $decoderOutput, $spec, $proc = null) { global $metar_toDecode, $decodeInfo, $decodeGroupCount, $decodeGroupProcessed, $remarks_toDecode, $show_diagnostics, $show_diagnosticsR, $dM_Rmk, $file; // shared with other scripts $decodeInfo['REMARK'][$remarkIncrement] = $decoderOutput; // add next item of output from decoder to output array $remove = array_splice($metar_toDecode, 0, $segmentCount); // remove from "still to be decoded" array those segments that have been processed if(is_null($proc)) { if(!array_key_exists($remarkIncrement, $decodeGroupProcessed['REMARK'])) $decodeGroupProcessed['REMARK'][$remarkIncrement] = "(" . $decodeGroupCount['REMARK'] . ") → RMK"; for ($i=0; $i < count($remove); $i++) { $decodeGroupProcessed['REMARK'][$remarkIncrement] .= "_" . $remove[$i]; $remarks_toDecode[$remove[$i]] = $spec; } }else $decodeGroupProcessed['REMARK'][$remarkIncrement] = $proc; // set up processed items into array so available for mouse-over on output web page if($show_diagnosticsR or $show_diagnostics) { echo "

Decoding count = " . $remarkIncrement . " → matched " . $decodeGroupProcessed['REMARK'][$remarkIncrement] . ' against ' . $spec . '

'; file_put_contents($file, "Decoding count = " . $remarkIncrement . " → matched " . $decodeGroupProcessed['REMARK'][$remarkIncrement] . " against " . $spec . "\r\n", FILE_APPEND | LOCK_EX); } $decodeGroupCount['REMARK'] = 1 + $remarkIncrement; } // end sub-function do_output() //--------------------------------------------------------------------------------------------------------- function commonTests(&$value, $key, &$testArray) { global $metar_toDecode, $decodeInfo, $decodeGroupCount, $decodeGroupProcessed, $remarks_toDecode, $show_diagnostics, $show_diagnosticsR, $dM_Rmk; // shared with other scripts global $cloud_type_array, $cloudAmountBandCode, $precipitationTypes; // declared in main script 'decodeMETAR.php' if(!count($metar_toDecode)) return; if($value != "Un-matched") return; if($show_diagnosticsR) echo '
For "' . $key . '" will examine: '; // conditional de-bugging section continues ////////////////////////////////////////////////////////////////// // RMK Group: Most widely used codes to try - Precipitation // ////////////////////////////////////////////////////////////////// if(!$testArray['Precipitation'] and substr($key, 0, 2) == 'PP' and is_numeric(substr($key, 2))) { if($show_diagnosticsR) echo 'Precipitation amount'; $output = "Total precipitation in last hour was " . 0.1 * substr($metar_toDecode[0], 2) . " mm"; walk_output($decodeGroupCount['REMARK'], $key, $output, "Walk Common sub-function = new_get_remarks -> Precipitation amount"); $testArray['Precipitation'] = true; goto exitCall; }else $testArray['Precipitation'] = true; ///////////////////////////////////// // RMK Group: Sea Level Pressure // ///////////////////////////////////// if(substr($key, 0, 3) == 'SLP') { if($show_diagnosticsR) echo 'Sea Level Pressure'; $return = walk_SLP($key, "common SLP type"); goto exitCall; }else ////////////////////////////////////////// // RMK Group: Field Level Pressure // ////////////////////////////////////////// if(substr($key, 0, 3) == 'QFE' and is_numeric(substr($key, 3, 3))) { if($show_diagnosticsR) echo 'Field Level Pressure'; walk_QFE($key, "common QFE type"); goto exitCall; }else ///////////////////////////////////// // RMK Group: Altimeter Pressure // ///////////////////////////////////// if(substr($key, 0, 1) == 'A' and is_numeric(substr($key, 1, 3))) { if($show_diagnosticsR) echo 'Altimeter Pressure'; walk_Altimeter($key, "common Altimeter type"); goto exitCall; } ////////////////////////////////////////////// // RMK Group: Another code to try - Hazy // ////////////////////////////////////////////// if(!$testArray['Hazy'] and $key == 'HZY') { // Lic. Benito Juárez International Airport|Mexico City, Distrito Federal, Mexico===== // 2017/10/26 15:44 MMMX 261544Z 00000KT 6SM SKC 14/04 A3035 NOSIG RMK HZY // HZ is normal code for haze, so this is non standard if($show_diagnosticsR) echo 'Hazy'; $output = "Current phenomena - Hazy"; walk_output($decodeGroupCount['REMARK'], $key, $output, "Walk Common sub-function = new_get_remarks -> Hazy"); $testHazy = true; goto exitCall; }else $testArray['Hazy']= true; ////////////////////////////////////////////// // RMK Group: Another code to try - Dew // ////////////////////////////////////////////// if(!$testArray['Dew'] and ($key == 'GRASS' or $key == 'DEW')) { // METAR example from Tonga Meteorological Services, Fiji # Fua'amotu Aerodrome METAR NFTF 011200Z 00000KT 9999 FEW019 19/18 Q1019 RMK GRASS DEW= // Faleolo International Airport|Apia, Samoa===== 2017/12/27 13:00 METAR NSFA 271300Z 18003KT 9999 FEW028 23/22 Q1010 NOSIG RMK DEW if($show_diagnosticsR) echo 'Dew and Grass Dew'; if($key == 'GRASS') { $output = "Current phenomena - Grass Dew"; walk_output($decodeGroupCount['REMARK'], $key, $output, "Walk Common sub-function = new_get_remarks -> Grass"); }else $output = "Current phenomena - Dew"; walk_output($decodeGroupCount['REMARK'], 'DEW', $output, "Walk Common sub-function = new_get_remarks -> Dew"); $testArray['Dew'] = true; goto exitCall; }else $testArray['Dew'] = true; ////////////////////////////////////////////////////////////// // RMK Group: Most widely used codes to try - Cloud Base // ////////////////////////////////////////////////////////////// if(!$testArray['Base'] and substr($key, 0, 3) == 'QBB' and is_numeric(substr($key, 3))) { if($show_diagnosticsR) echo 'Cloud Base'; $output = "Base of cloud " . 1 * substr($metar_toDecode[0], 3) . " m"; walk_output($decodeGroupCount['REMARK'], $key, $output, 'Walk Common sub-function = new_get_remarks -> Cloud base'); $testArray['Base'] = true; goto exitCall; }else $testArray['Base'] = true; /////////////////////////////////////////////////////////////////// // RMK Group: Most widely used codes to try - Cloud Amount // /////////////////////////////////////////////////////////////////// if(!$testArray['Cloud'] and array_key_exists($key, $cloudAmountBandCode)) { // 2017.12.29 1455 UTC METAR LIBA 291455Z 29021KT 9999 SCT015 FEW025CB SCT050 08/06 Q1008 RMK BKN VIS MIN 9999 WIND THR11 29017G27KT WIND THR29 /////KT BLU if($show_diagnosticsR) echo 'Cloud amount'; $output = "Amount of cloud = " . $cloudAmountBandCode[$key]; walk_output($decodeGroupCount['REMARK'], $key, $output, "Walk Common sub-function = new_get_remarks -> Cloud amount"); // don't set $testCloud to true, so this test can be repeated goto exitCall; }else $testArray['Cloud'] = true; ////////////////////////////////////////////////////////////////////////////////// // RMK Group: Most widely used codes to try - Cloud type and Amount Layers // ////////////////////////////////////////////////////////////////////////////////// if(is_numeric(substr($key, 0, 1)) and !$testArray['Layer'] and (in_array(substr($key, 1, 2), $cloud_type_array) || in_array(substr($key, 1, 3), $cloud_type_array)) and is_numeric(substr($key, -3))) { // Narita International Airport (new Tokyo)|Narita, Chiba, Japan===== // 2017/10/26 16:00 RJAA 261600Z 33005KT 9999 FEW020 10/09 Q1021 NOSIG RMK 1CU020 A3015 // don't recognise 1CU020, assume oktas, cloud type, height if($show_diagnosticsR) echo 'Japan style Oktas, Cloud Type, and Height'; $cloud_layer_type = in_array(substr($key, 1, 2), $cloud_type_array) ? array_search(substr($key, 1, 2), $cloud_type_array) : array_search(substr($key, 1, 3), $cloud_type_array); $altitudeFT = 100 * substr($key, -3); $output = "Cloud of type " . $cloud_layer_type . " filling sky to " . substr($key, 0, 1) . " oktas at height of " . $altitudeFT . ' feet (' . 10 * round(($altitudeFT + 50)/32.8084) . ' metres)'; $skyIterator = array_search(substr($key, -3), $decodeInfo['SKY-DETAILS']); $decodeInfo['SKY-DETAILS'][$skyIterator][0] = $cloud_layer_type; $decodeInfo['SKY-DETAILS'][$skyIterator][1] = "" . substr($key, 0, 1) . " oktas"; $decodeGroupProcessed['SKY'][1 + $skyIterator] .= " + RMK_" . $key; $remarks_toDecode[$key] = "Walk Common sub-function = new_get_remarks -> Japan style Oktas, Cloud Type, and Height"; $array_element_id = array_search($key, $metar_toDecode); // find which item is to be removed from still to be decoded array $done = array_splice($metar_toDecode, $array_element_id, 1); // remove that item }else{ $testArray['Layer'] = true; if($show_diagnosticsR) echo 'multiple precipitation and Cloud types in same segment'; $output = ""; $processed = ""; $iCount = 0; $partLocal = $key; $testMultiple = false; repeatThis: if($testArray['Multiple']) goto stopRepeat; # Check for 3 alternatives if(in_array(substr($partLocal, 0, 2), $precipitationTypes) and is_numeric(substr($partLocal, 2, 1))) { // Precipitation type followed by cloud details any number of times // example 2017/12/14 15:26 METAR CYXU 141526Z 23007KT 2 1/2SM -SHSN OVC024 M11/M14 A2993 RMK SN2SC6 CVCTV CLD EMBD SLP155 $itemName = array_search(substr($partLocal,0,2), $precipitationTypes); $itemCode = substr($partLocal,0,2); $output .= "Precipitation type " . $itemName . " causing opacity of "; $rest = substr($partLocal, 2); $type = "CONDITIONS"; $processed .= $itemCode; }elseif(in_array(substr($partLocal, 0, 2), $cloud_type_array) and is_numeric(substr($partLocal, 2, 1))) { // above condition modified to ensure only 2 letters! (e.g. CIG is not matched to Cirrus) $itemName = array_search(substr($partLocal, 0, 2), $cloud_type_array); $itemCode = substr($partLocal,0,2); $output .= "Cloud of type " . $itemName . " filling sky to "; $rest = substr($partLocal, 2); $type = "SKY-DETAILS"; $processed .= $itemCode; $decodeInfo[$type][$iCount][0] = $itemName; }elseif(in_array(substr($partLocal, 0, 3), $cloud_type_array) and is_numeric(substr($partLocal, 3, 1))) { $itemName = array_search(substr($partLocal, 0, 3), $cloud_type_array); $itemCode = substr($partLocal,0,3); $output .= "Cloud of type " . $itemName; $rest = substr($partLocal, 3); $type = "SKY-DETAILS"; $processed .= $itemCode; $decodeInfo[$type][$iCount][0] = $itemName; }else $testArray['Multiple'] = true; # Common snippet for end of check, see if any produced output if(strlen($output) > 0) { $itemValue = substr($rest, 0, 1); $output .= $type == "SKY-DETAILS" ? " fills " : " obscures "; $output .= $itemValue . " oktas of sky.  "; $processed .= $itemValue; if($type == "SKY-DETAILS" and array_key_exists("SKY-DETAILS", $decodeInfo)) $decodeInfo[$type][$iCount][1] = substr($rest, 0, 1) . " oktas"; elseif($type == "CONDITIONS" and array_key_exists("CONDITIONS", $decodeInfo)) $decodeInfo[$type] .= "; " . $itemName . " obscures " . $itemValue . " oktas of sky"; $partLocal = substr($rest, 1); // remove number of oktas, ready for next increment of loop if($show_diagnostics or $show_diagnosticsR) { echo "

Within segment incremental count = " . $iCount . " is coded='" . $itemCode . $itemValue . "' and sub-function = new_get_remarks "; echo "added " . "Recognised as " . $itemName . ' with obscuration value of ' . $itemValue . ' oktas

'; } if(strlen($partLocal) > 2) { // Cloud type followed by number of oktas pattern, repeated any number of times CU3~SC2 // example 2017/11/10 09:00 METAR CYXU 100900Z 31011G20KT 6SM -SHSN DRSN SCT030 BKN060 M06/M11 A3028 RMK CU3SC2 VIS VRB 3-9 SLP266 if($type == "SKY-DETAILS") $iCount++; $processed .= "~"; goto repeatThis; } $testArray['Multiple'] = true; # end of repeat, with output $remarks_toDecode[$key] = "add type to obscuring layers"; $array_element_id = array_search($key, $metar_toDecode); // find which item is to be removed from still to be decoded array $doneArray = array_splice($metar_toDecode, $array_element_id, 1); // remove that item $decodeGroupProcessed['REMARK'][$decodeGroupCount['REMARK']] = "(" . $decodeGroupCount['REMARK'] . ") → RMK_" . $key; // add that removed item to the mouse-over output if($show_diagnostics or $show_diagnosticsR) { echo "

Decoded = " . $doneArray[0] . ' → matched against Walk Common sub-function with content of ' . $output . '

'; } goto exitCall; } stopRepeat: # re-entry point if no match to conditions to enter repeat code snippet } exitCall: } // end anonymous function - commonTests //--------------------------------------------------------------------------------------------------------- function walk_output($remarkIncrement, $key, $decoderOutput, $spec) { global $metar_toDecode, $decodeInfo, $decodeGroupCount, $decodeGroupProcessed, $remarks_toDecode, $show_diagnostics, $show_diagnosticsR, $dM_Rmk, $file; // shared with other scripts $decodeInfo['REMARK'][$remarkIncrement] = $decoderOutput; // add next item of output from decoder to output array $remarks_toDecode[$key] = $spec; // Change value for segment that has now been decoded $array_element_id = array_search($key, $metar_toDecode); // find which item is to be removed from still to be decoded array $done = array_splice($metar_toDecode, $array_element_id, 1); // remove that item $decodeGroupProcessed['REMARK'][$remarkIncrement] = '(' . $decodeGroupCount['REMARK'] . ') → RMK_' . $key; // add that removed item to the mouse-over output if($show_diagnosticsR or $show_diagnostics) { echo "

Decoding count = " . $remarkIncrement . ' → matched ' . $key . " =" . $decodeGroupProcessed['REMARK'][$remarkIncrement] . ' against ' . $spec . '

'; file_put_contents($file, "Decoding count = " . $remarkIncrement . " → matched " . $key . " =" . $decodeGroupProcessed['REMARK'][$remarkIncrement] . " against " . $spec . "\r\n", FILE_APPEND | LOCK_EX); } $decodeGroupCount['REMARK'] = 1 + $remarkIncrement; } // end function walk_output ################################################################################################################################### # Novel sub-function written by SPAWS to produce a single output from decoding one or more segments in the Remarks Group. # # This is called from various specifications within the USA collection, that report location as part of that specification. # ################################################################################################################################### function searchLocation($locationArray, $spec="Remarks") { global $metar_toDecode, $decodeInfo, $decodeGroupCount, $decodeGroupProcessed, $show_diagnostics, $show_diagnosticsR, $dM_Rmk; // shared with main function global $precipitationTypes, $cloudAmountBandCode, $cloud_type_array, $compass, $compass_expanded, $hoursOffset; // shared with other sub-functions /* Federal Meteorological Handbook No. 1 paragraph 8.5 Present Weather Reporting Standards " ... the location of weather phenomena shall be reported as: ALP - occurring at the station" when within 5 statute miles of the point(s) of observation. VC - in the vicinity of the station" when between 5 and 10 statute miles of the point(s) of observation. DSNT - distant from the station" when beyond 10 statute miles of the point(s) of observation. Other locations mentioned in handbook MT - "mountains" OHD - "overhead" RWY - "runway" followed without a space by runway identifier - see function test_runway_state() in 'decodeMETAR_sub_funct.php' SFC - "surface" TWR - "tower" Canada MANOBS 10.2.19.8.2 - OVR - over, RDG - ridge, In - in, LYRS - layers, FM - from, Canada MANAB defines lots of abbreviations for particular places, BDRY for boundary, DCTLY for directly, DNSL for down slope, DVLGP for developing, EST for estimate, EXC for except, FLDS for fields, FLUCD for fluctuating, HRZN for horizon, IMPRG for improving, INTMT for intermittent, LKLY for likely, MNLY for mainly, MRGG for merging, PVLG for prevailing, RPDLY for rapidly, VISBL for visible */ $output = ""; // Initialise $iCount = 0; $location = ""; if($locationArray == 'ALP') { $location = "within 5 statute miles of observation point "; $iCount = 1; }elseif($locationArray == 'VC') { $location = "within 8 km (5 miles) but not within the perimeter of the aerodrome "; // "In the vicinity". $iCount = 1; /* }elseif($locationArray == '??') // Don't know know what code used, it may be that this is default if no code used? // Canada defines 'ADJ' for adjacent, BYD for beyond, 'NRG' for nearing { $location = "between 5 and 10 statute miles from observation point "; // USA use this for in vicinity $iCount = 1; */ }elseif($locationArray == "DSNT") { $location = "beyond 10 statute miles from observation point "; $iCount = 1; }elseif(substr($locationArray[0], 0, 2) == 'MT') { $location = "on mountains seen from observation point "; $iCount = 1; }elseif($locationArray == 'OHD') { $location = "overhead as seen from observation point "; // Canada also uses 'ABV' for above $iCount = 1; }elseif(substr($locationArray[0], 0, 3) == 'RWY') { $location = "runway " . substr($locationArray[0], 3); $iCount = 1; }elseif(is_numeric(substr($locationArray[0], 0, 1))) { $location = "runway " . substr($locationArray[0], 0, 1); if(substr($locationArray[0], 1, 1) == 'L') $location .= ' left'; if(substr($locationArray[0], 1, 1) == 'R') $location .= ' right'; $iCount = 1; }elseif($locationArray[0] == 'SFC') { $location = "at aerodrome surface level "; $iCount = 1; }elseif($locationArray == 'TWR') { $location = "seen from control tower "; $iCount = 1; } elseif($locationArray[0] == "GRID") { $location = "in Grid direction of "; $iCount = 2; if(strpos($locationArray[1],'-') !== false) { $partLocalL = substr($locationArray[1], 0, strpos($locationArray[1],'-') -1); $partLocalR = substr($locationArray[1], 1 + strpos($locationArray[1],'-')); if(array_key_exists($partLocalL,$compass)) { $output .= " moving " . $compass_expanded[$partLocalL]; $iCount++; } if(!is_null($partLocalR) and array_key_exists($partLocalR,$compass)) { $output .= " to " . $compass_expanded[$partLocalR]; } } } // Buffalo/Cheektow, New York, USA (station id=KBUF) // 2018/01/18 20:54 KBUF 182054Z 24022G28KT 6SM SN BLSN BKN016 OVC022 M06/M09 A3001 // RMK AO2 PK WND 24030/2008 SLP175 VIS LWR WNNE VIS HYR SW P0000 60000 T10611089 58008 $processed = "Location"; for($jCount=0; $jCount < $iCount; $jCount++) { $processed .= "_" . $locationArray[$jCount]; } if(count($locationArray) > 3 and ($locationArray[2] = "MOV" or $locationArray[2] = "MOVG") and array_key_exists($locationArray[3],$compass)) // Canada 10.2.19.8.3 uses MOVG for moving { $processed .= "_" . $locationArray[2] . "_" . $locationArray[3]; $iCount = $iCount + 2; $output .= " moving " . $compass_expanded[$locationArray[3]]; }elseif(array_key_exists(2, $locationArray) and array_key_exists($locationArray[2],$compass)) { $processed .= "_" . $locationArray[2]; $iCount++; $output .= " looking " . $compass_expanded[$locationArray[2]]; } $output .= " " . $location; if($show_diagnosticsR or $show_diagnostics) { echo "

Decoding count = " . $decodeGroupCount['REMARK'] . " → matched " . $processed . ' against ' . $spec . '

'; } $functionReturn = array ($output, $iCount); return $functionReturn; } // end sub-function searchLocation() ################################################################################################################################### # Novel sub-function written by SPAWS to produce a single output from decoding one or more segments in the Remarks Group. # # This is called from Canadian and USA sub-functions to process Obscurations (surface-based or aloft). # # Any phenomenon in the atmosphere, other than precipitation, that reduces the horizontal visibility in the atmosphere. # ################################################################################################################################### function doObscuring($spec, $foot) { global $metar_toDecode, $remarks_toDecode, $decodeInfo, $decodeGroupCount, $decodeGroupProcessed, $show_diagnostics, $show_diagnosticsR, $dM_Rmk, $cloud_type_array; // shared with main function static $obscuring_array = array( 'Blowing' => 'BL', 'Sand' => 'SA', 'Dust' => 'DU', 'Dust Storm' => 'DS', 'Drizzle (including Freezing Drizzle)' => 'DZ', 'Fog (any form including Mist)' => 'FG', 'Smoke' => 'FU', 'Hail' => 'GR', 'Haze' => 'HZ', 'Ice Crystals' => 'IC', 'Ice Pellets (including Ice Pellet Showers)' => 'PL', ' Rain (any form including showers and freezing)' => 'RA', ' Snow (including Showers, Pellets, and Grains)' => 'SN', 'Sandstorm' => 'SS', 'Volcanic Ash' => 'VA' ); $pattern_count = preg_match_all('~([B-V][A-Z][DS]?[ACNU]?)([0-9])~',$metar_toDecode[0],$pattern_array, PREG_SET_ORDER); // returns number of matches and array containing individual matches $returned['count'] = $pattern_count; if($show_diagnostics or $show_diagnosticsR) { echo "
Specification=" . $spec . '     Pattern count ' . $pattern_count . '
'; print_array($pattern_array, 'Matched patterns'); } if($pattern_count > 0) { for($iCount=0; $iCount < $pattern_count; $iCount++) { if(array_search($pattern_array[$iCount][1], $cloud_type_array) != false) { $obscuring_layer_type = array_search($pattern_array[$iCount][1], $cloud_type_array, true); $returned[$iCount] = array($obscuring_layer_type, $pattern_array[$iCount][2] . " oktas of sky"); }else{ $obscuring_layer_type = array_search($pattern_array[$iCount][1], $obscuring_array, true); $returned[$iCount] = 'CONDITIONS'; } $output = $foot . " Obscuration of type " . $obscuring_layer_type . " fills " . $pattern_array[$iCount][2] . " oktas of sky"; if($iCount == 0) $processed = 'RMK_'; else $processed = '~'; do_output($decodeGroupCount['REMARK'], 0, $output, $spec, $processed . $pattern_array[$iCount][0]); } $done = trim(array_shift($metar_toDecode)); // take out processed segment $remarks_toDecode[$done] = $spec; } return $returned; } // end sub-function - doObscuring() ######################################################################################################################### # Novel sub-function written by SPAWS to produce a single output from decoding SLP segment in the Remarks Group. # # This is called from Canadian and USA sub-functions to process Sea Level pressure at time of METAR issue. # # This function works on $metar_toDecode array and will only find SLP if it is in element zero. # ######################################################################################################################### function doSLP($spec) { global $metar_toDecode, $decodeInfo, $decodeGroupCount, $decodeGroupProcessed, $remarks_toDecode, $show_diagnostics, $show_diagnosticsR, $dM_Rmk; // shared with other scripts $matched[0] = false; if(count($metar_toDecode) < 1 or substr($metar_toDecode[0],0,3) != "SLP") return $matched; ////////////////////////////////////////////////////////////// // RMK Group: Most widely used codes to try - pressure // ////////////////////////////////////////////////////////////// if(substr($metar_toDecode[0],0,3) == "SLP" and substr($metar_toDecode[0],-2) == "NO") { $decodeGroupProcessed['SLP'] = "RMK_" . $metar_toDecode[0]; $decodeInfo['SLP'] = "Sea level pressure is not available"; $matched[0] = true; $done = array_shift($metar_toDecode); // take out processed segment $remarks_toDecode[$done] = $spec; if($show_diagnostics or $show_diagnosticsR) { echo "

Decoding Remarks Group → matched against " . $spec . ' → ' . $done . '

'; } }elseif(count($metar_toDecode) > 0 and substr($metar_toDecode[0],0,3) == "SLP" and is_numeric(substr($metar_toDecode[0], 3))) { // Fairbanks International Airport|Fairbanks, Alaska=====2017/10/26 15:53 PAFA 261553Z 00000KT 10SM SCT070 BKN120 00/M02 A2954 RMK AO2 SLP010 T00001017 $ // Hilo International Airport|Hilo, Hawaii=====2017/10/26 15:53 PHTO 261553Z 34007KT 10SM -RA FEW048 FEW080 23/21 A2987 RMK AO2 SLP113 P0000 T02330211 // Chuuk International Airport|Weno, Chuuk, Caroline Islands=====2017/10/26 15:52 PTKK 261552Z 02008KT 15SM FEW014 SCT120 BKN280 29/25 A2974 RMK SLP074 T02860254 $pressureHPA = ((1 * substr($metar_toDecode[0],3,1)) > 5) ? (900 + (0.1 * substr($metar_toDecode[0],3))) : (1000 + (0.1 * substr($metar_toDecode[0],3))); // units are tenths of hectoPascals $pressureKPA = ((1 * substr($metar_toDecode[0],3,1)) > 5) ? (90 + (0.01 * substr($metar_toDecode[0],3))) : (100 + (0.01 * substr($metar_toDecode[0],3))); // units are hundredths of kiloPascals $pressureIN = number_format(0.02953 * $pressureHPA, 2); // convert to hundredths of inches Hg $pressureHPA = number_format($pressureHPA, 1); $pressureKPA = number_format($pressureKPA, 2); if(substr($decodeInfo['STATION'], 0, 1) == 'C') $decodeInfo['SLP'] = 'QNH is ' . $pressureKPA . ' kPa (' . $pressureIN . ' inHg)'; // Canada else $decodeInfo['SLP'] = 'QNH is ' . $pressureHPA . ' hPa (' . $pressureIN . ' inHg)'; $decodeGroupProcessed['SLP'] = "RMK_" . $metar_toDecode[0]; $matched[0] = true; $done = array_shift($metar_toDecode); // take out processed segment $remarks_toDecode[$done] = $spec; if($show_diagnostics or $show_diagnosticsR) { echo "

Decoding Remarks Group → matched in sequence against " . $spec . " → " . $done . '

'; } } return $matched; } // end sub-function - doSLP() ######################################################################################################################### # Novel sub-function written by SPAWS to produce a single output from decoding SLP segment in the Remarks Group. # # This is called from Canadian and Common sub-functions to process Sea Level pressure at time of METAR issue. # # This function works on $remarks_toDecode array and will match any SLP element during an array walk. # ######################################################################################################################### function walk_SLP($segment, $spec) { global $metar_toDecode, $decodeInfo, $decodeGroupCount, $decodeGroupProcessed, $remarks_toDecode, $show_diagnostics, $show_diagnosticsR, $dM_Rmk; // shared with other scripts ////////////////////////////////////////////////////////////// // RMK Group: Most widely used codes to try - pressure // ////////////////////////////////////////////////////////////// if(substr($segment, 3, 2) == "NO") { $decodeGroupProcessed['SLP'] = "RMK_" . $segment; $decodeInfo['SLP'] ="Sea level pressure is not available"; // Change value for segment that has now been decoded $remarks_toDecode[$segment] = $spec; $array_element_id = array_search($segment, $metar_toDecode); // find which item is to be removed from still to be decoded array $done = array_splice($metar_toDecode, $array_element_id, 1); // remove that item if($show_diagnostics or $show_diagnosticsR) { echo "

Decoding count = " . $decodeGroupCount['REMARK'] . ' → matched against ' . $spec . ' → ' . $segment . '

'; } }elseif(is_numeric(substr($segment, 3))) { // Fairbanks International Airport|Fairbanks, Alaska=====2017/10/26 15:53 PAFA 261553Z 00000KT 10SM SCT070 BKN120 00/M02 A2954 RMK AO2 SLP010 T00001017 $ // Hilo International Airport|Hilo, Hawaii=====2017/10/26 15:53 PHTO 261553Z 34007KT 10SM -RA FEW048 FEW080 23/21 A2987 RMK AO2 SLP113 P0000 T02330211 // Chuuk International Airport|Weno, Chuuk, Caroline Islands=====2017/10/26 15:52 PTKK 261552Z 02008KT 15SM FEW014 SCT120 BKN280 29/25 A2974 RMK SLP074 T02860254 $pressureHPA = ((1 * substr($segment,3,1)) > 5) ? (900 + (0.1 * substr($segment,3))) : (1000 + (0.1 * substr($segment,3))); // units are tenths of hectoPascals $pressureKPA = ((1 * substr($segment,3,1)) > 5) ? (90 + (0.01 * substr($segment,3))) : (100 + (0.01 * substr($segment,3))); // units are hundredths of kiloPascals $pressureIN = number_format(0.02953 * $pressureHPA, 2); // convert to hundredths of inches Hg $pressureHPA = number_format($pressureHPA, 1); $pressureKPA = number_format($pressureKPA, 2); if(substr($decodeInfo['STATION'], 0, 1) == 'C') $decodeInfo['SLP'] = 'QNH is ' . $pressureKPA . ' kPa (' . $pressureIN . ' inHg)'; // Canada else $decodeInfo['SLP'] = 'QNH is ' . $pressureHPA . ' hPa (' . $pressureIN . ' inHg)'; $array_element_id = array_search($segment, $metar_toDecode); // find which item is to be removed from still to be decoded array $done = array_splice($metar_toDecode, $array_element_id, 1); // remove that item $decodeGroupProcessed['SLP'] = "RMK_" . $segment; $remarks_toDecode[$segment] = $spec; if($show_diagnostics or $show_diagnosticsR) // conditional de-bugging section continues { echo "

Decoding count = " . $decodeGroupCount['REMARK'] . ' → matched in array walk against ' . $spec . " → " . $segment . '

'; } } } // end sub-function - walk_SLP() function walk_QFE($segment, $spec) { global $metar_toDecode, $decodeInfo, $decodeGroupCount, $decodeGroupProcessed, $remarks_toDecode, $show_diagnostics, $show_diagnosticsR, $dM_Rmk; // shared with other scripts optionalDiagnosticOutput("Remarks Group: new_get_remarks() - Common: Q Pressure Field Elevation"); // Note that "Altimeter pressure" is a computed value on the basis of assumed averages of atmospheric pressure and temperature known as the International Civil Aviation Organization (ICAO) // Standard Atmosphere, traditionally calculated for the height of the instruments in an aircraft parked on the runway (about 10 feet off the ground level), appearing in main part of METAR. // // QFE is a more accurate measurement taking into account the actual temperature experienced in the preceding 15 minutes, and calculated for the ground level elevation of the aerodrome. // N.B. QFE is reported in Remarks Group as combinations of alternative units sometimes, with two figures separated by a slash as in examples below. // METAR examples from Russia: QFE is referred to as "Atmospheric pressure at aerodrome elevation (or at runway threshold)" if(substr($segment,6, 1) == '/') { // Tolmachevo Airport, Novosibirsk: METAR UNNT 011230Z 23003MPS 9999 -SHRA SCT010 OVC023CB 05/04 Q1024 R25/290160 R34/290160 NOSIG RMK QFE758/1011 // Koltsovo Airport, Yekaterinburg: METAR USSS 011230Z 01003MPS 330V030 9999 OVC022 M02/M07 Q1026 R08L/220050 TEMPO 36009MPS RMK QFE750/1000 // Irkutsk International Airport|Irkutsk, Russia=====2017/10/26 16:00 UIII 261600Z 32006MPS 9999 -SHSN OVC040CB M03/M05 Q1021 R30/990330 NOSIG RMK QFE721/0961 // Qurghonteppa International Airport|Kurgan Tyube, Tajikistan=====2017/10/26 12:00 UTDT 261200Z 00000MPS 9999 FEW100 23/02 Q1019 35CLRD70 RMK QFE725/0967 if(is_numeric(substr($metar_toDecode[0],3, 3))) { $pressureMM = substr($segment,3, 3); // units are mmHg $pressureIN = round($pressureMM/25.4,2); // convert to inches Hg $pressureHPA = substr($segment, 7); // units are hectoPascals } }else{ // Chelyabinsk Airport: METAR USCC 011230Z 36005MPS 9999 -SHSN BKN018CB M02/M04 Q1025 R09/290050 NOSIG RMK QFE748 $pressureMM = substr($segment,3); // units are mmHg $pressureIN = round($pressureMM/25.4, 2); // convert to inches Hg $pressureHPA = round($pressureMM / 0.750062, 0); // convert to hectoPascals } $decodeGroupCount['BAROMETER'] = "QFE (pressure at airField Elevation)"; $decodeInfo['BAROMETER'] = "QFE is " . $pressureHPA . " hPa (" . $pressureIN . " inHg)"; $array_element_id = array_search($segment, $metar_toDecode); // find which item is to be removed from still to be decoded array $done = array_splice($metar_toDecode, $array_element_id, 1); // remove that item $decodeGroupProcessed['BAROMETER'] = "RMK_" . $segment; $remarks_toDecode[$segment] = "Common - QFE - parked aircraft level"; if($show_diagnostics or $show_diagnosticsR) { echo "

Specification = Parked Aircraft Level Altimeter Pressure: " . $done . "

"; } } // end sub-function - walk_QFE() function walk_Altimeter($segment, $spec) { global $metar_toDecode, $decodeInfo, $decodeGroupCount, $decodeGroupProcessed, $remarks_toDecode, $show_diagnostics, $show_diagnosticsR, $dM_Rmk; // shared with other scripts optionalDiagnosticOutput("Remarks Group: new_get_remarks() - Common: Pressure for aircraft level Altimeter"); // The altimeter setting calculations adjust the pressure by 0.3 hPa to account for the fact that the altimeter in the plane is approximately 10 ft off the ground. // Canada - An altimeter entry of 992 indicates an altimeter setting of 29.92 inches // Taiwan Taoyuan International Airport|Taoyuan City, Republic of China=====2017/10/26 16:00 RCTP 261600Z 06010KT 9999 FEW020 SCT050 22/16 Q1014 NOSIG RMK A2997 // Ninoy Aquino International Airport |Metro Manila, Philippines=====2017/10/26 16:00 RPLL 261600Z 15003KT 070V200 9999 FEW023 SCT100 27/27 Q1009 NOSIG RMK A2980 // Eloy Alfaro International Airport|Manta, Ecuador=====2017/10/26 16:00 SEMT 261600Z 30008KT 9999 BKN020 26/21 Q1012 RMK A2990 $pressureIN = (1 * substr($segment,1,2)) + (0.01 * substr($segment,3)); // units are hundredths of inches of mercury $pressureHPA = round($pressureIN / 0.02953); // convert to hectoPascals $decodeGroupCount['BAROMETER'] = "Altimeter Pressure (at about 10 feet above elevation of airfield)"; $decodeInfo['BAROMETER'] = "Altimeter reads " . $pressureHPA . " hPa (" . $pressureIN . " inHg)"; $array_element_id = array_search($segment, $metar_toDecode); // find which item is to be removed from still to be decoded array $done = array_splice($metar_toDecode, $array_element_id, 1); // remove that item $decodeGroupProcessed['BAROMETER'] = "RMK_" . $segment; $remarks_toDecode[$segment] = $spec; if($show_diagnostics or $show_diagnosticsR) { echo "

Common Specification = Parked Aircraft Level Altimeter Pressure: " . $segment . "

"; } } // end sub-function - walk_Altimeter() /* Version history =============== 0.0.1 17 July 2017 First developmental version, some novel decoding script, but also taking some code snippets from sources above, to quickly test the complexity of the concept of decoding all UK METAR by running script against live METAR reports from several nearby aerodromes for several days. 0.0.2 27 July 2017 Second developmental version, finalised all UK standard Groups now that RVR (new sub-string approach) decoding is working, now tested against any METAR so confident of concept, improved script with good commenting and neat layout, also now covers all supplementary Groups. Since previous single script had become difficult to read as it was so long, this separated out script now has most sub-functions called from elsewhere in the decoding suite. 0.0.3 31 July 2017 ow written/expanded the various conditions that decode all the predicted content of Runway State Group, Sea State Group, and Remarks Group based on Wikipedia article https://en.wikipedia.org/wiki/METAR for North America. Ready for extensive testing. 0.0.4 18 Aug 2017 Minor tweaking of diagnostic output, adding Cascading Style Sheet class references in some places, to make it easier to read and use for understanding the decoding of longer METAR. Corrected some bugs where my coding was not doing what I intended it to. Better script comments now, so can remember why I did certain bits of code a particular way with aim that future code re-writes for METAR decoding problems don't introduce any more similar bugs. 0.0.5 31 Aug 2017 Expanded the script for decoding the Prevailing Visibility Group, Sea State Group, and Remarks Group Cloud Types specification. If temperature and dew-point specified in Remarks Group with one decimal place, this replaces integer figures from main Temperature / Dew point Group. If sea level pressure specified in Remarks Group, this is placed where output would be for Standard Mandatory Sea Level Pressure Group. For outputs from Ceiling, Visibility and RVR, added 'number_format' to make large figures more readable. Added minimal Wind Shear Group processing just (although not used in UK), to ensure that subsequent groups processed correctly if a METAR that does include wind shear is decoded. 0.0.6 5 Sep 2017 Amended code now to decode real USA METAR following official specifications in Remarks. This version decodes 26 specifications out of the 40 applying in USA standard FCM-H1-2005. An ongoing problem is automated reports from aerodromes that are not following the standard for the order in which they include specifications within Remarks Group, so changed coding so it loops through the tests against specifications until reaches end of the METAR report string. Danger of this is an endless loop, so added incrementing counter to ensure discarding segments, and meeting end condition. Similar problem found within Trend Group, what sequence to test possible content in, again trying a 're-start' approach, despite risk of endless loop, until further investigations have identified what sequences are used, and code can be revised to try just those sequences. 0.0.7 16 Sep 2017 Another problem with Cranfield - observer included a '/' between bearing and wind speed, optional piece added to regular expression for wind to cope with that. 0.0.8 27 Sep 2017 Getting script into a final state by re-examining all the sources I can find about METAR content and re-examining the output from other decoders (as listed in 'decodeMETAR.php') to try to ensure my script does cope with as many possible METAR as possible. Finding that individual national guidance about what is acceptable content for METAR in those nations has not been summarised correctly in other decoders or in places like Wikipedia. Therefore I am now revising my code in line with all the individual nation by nation guidance I can find, ignoring METAR article in Wikipedia. As I am revising my code, I am looking to improve it as I work through, e.g. now use PHP checkdate() function instead of regular expression for checking of date to reduce resource consumption. Rewrote the processing of Aerodrome Identity Group assigning ICAO identifier and looked up place to separate elements of $decodeInfo array (and reducing number of arrays required with simpler search and assign code snippet). Addition of 'AMD' and other correction codes mentioned by other decoders. Minor correction in Runway State Group. Now fully implemented Wind Shear Group decoding (as it is very simple). 0.0.9 30 Sep 2017 Continuing dealing with malformed METAR. Amended code now to ignore exactly repeating segments that follow one another. I have seen METAR with repeats of segments that have something else in-between, currently can't recognise that in my code. 0.1.0 30 Sep 2017 This is version shared with Ken True, although he got a single script combining the three current scripts and labelled up as version 3.0 of 30 Sept 2017. Having mentioned this script in Cumulus Support Forum, I dug out all my past test data (including many malformed but actual METAR reports) and reran around 600 old METAR reports against v.0.0.9 to allow bug correction by this release. N.B. Using Opera for the bulk testing as Firefox tends to crash with memory errors attempting to process all METAR in a file before any output, while Opera outputs each METAR in turn while PHP is still processing decode before moving to next METAR in file. Basically, the bulk testing identified that several of the recent changes to code resulted in more decoding failures with 'Surface Wind Group', 'Prevailing Visibility Group', 'Precipitation type decoding', 'Runway Visual Range Group', 'Trend Group', and some old specifications in 'Remark Group', mainly due to my attempts to reduce overall coding size by simplifying existing code when I added new coding. Didn't understand syntax error for wind speed case 'KMH' at line 320, but turned out to be unprintable characters needed removal. So in the major code re-write today, swapped back to different approach again for Runway Visual Range, regressed precipitation decoding snippet, started afresh on the interface between the wind decoding sub-function (where I had previously tried to keep close to Ken True's outputs for compatibility with his scripts) and its calls from the sub-function decodeMETAR_standard() for the Surface Wind Group and the sub-function changeGroup() for the Trends Group. Rewrite of Prevailing and Minimum Directional Visibility Group coding, combining their decoding into single sub-function which also copes with 'Variable Visibility'. Minor changes to how identifies whether it has matched or not in various places (e.g. RVR and Trends Visibility). A significant amount of code has therefore been changed in consequence for this version so potentially further bugs may be present. Re-testing with the same 600 old METAR reports confirms that this version 0.2.0 successfully copes with as many combinations as possible. 0.2.0 5 Oct 2017 From this version, the code for handling the decoding of the different specifications for Remark Group has been moved to a separate script that is only loaded if a Remark Group is found by this script. Some changes for malformed METAR e.g. Colour State. Another re-work of Prevailing and Minimum Visibility Groups sub-function, now better at coping with use of statute miles by North America, and also able to be called from Remark decoding script for the Canadian specification of 'Variable Visibility'. The opportunity has been taken to make a few other standardisation changes to the layout and commenting of the sub-functions in this script, this has added to the readability and also allowed a number of improvements to be made by using more, but simpler, sub-functions. Now believe sub-functions remaining in this script are in their final state as METAR examples from many countries now mentioned in script and it successfully decodes all of them as they conform to standard international format that applies outside the RMK Group. 0.2.1 7 Oct 2017 From this version, '$decodeGroupCount' array has been changed, so that it can be either count, string, or boolean value. The array '$decodeGroupProcessed' has been introduced, with one element per Group, and into it are placed all the METAR segments successfully decoded for that Group with separators "_" (placed between multiple segments that are each part of Group), or " + " (placed between segments where each of them is used to produce a single item for the Group), or "~" (divides parts of a single segment treated as separate items), where necessary. 0.2.2 8 Oct 2017 This script has PHP error reporting included, so not suitable without modification for production environment. As well as full standard PHP errors appearing on the web page calling this script (and being stored on the server), the script also writes the METAR being processed when the script bug was found and the full error output to the same log file as used for malformed METAR. After correction of a few bugs introduced (e.g. in Prevailing/Directional/Variable Visibility) intended to be final developmental version. Has been checked against all standard formats in volume 1 and the regional deviations in volume 2 of the WMO regulations. 0.2.3 13 Oct 2017 Have moved functions from "decodeMETAR_sub_funct.php" to "decodeMETAR_Rmk.php" and in reverse direction; so latter does initialisation. Corrected 'Prevailing Visibility Group', it was consuming one too many segments, so preventing one segment being decoded! Introduction of coloured flag image for colour state output. 1.0.0 23 Oct 2017 Release version - Changes to several sub-functions, making arrays defined within them global, now can use them elsewhere within this decoder and also in 'selectMETAR.php'. Also now tolerates pressure with fewer than 4 digits, extended "corrected" message, extended $colour_states, and reworked sections dealing with cloud ceiling. Change to $decodeInfo['CLOUDS'] to now correctly report cloud situation when no significant clouds to make Ken True's icon work. 1.0.1 16 Nov 2017 Correction of more bugs in Prevailing and Variable Visibility. Used strangely by Canada, the former units are miles, but latter units are actually hundreds of feet. Function renamed to function novel_allVisibility as there has been multiple bug correction and consequently a lot of function was rewritten to be simpler. 1.0.2 27 Nov 2017 Sub-functions used by re-written "decodeMETAR_Rmk.php" script added to end of this script. Extra diagnostic output added to sub-functions in this script that can be called from either "decodeMETAR.php" or "decodeMETAR_Rmk.php". Ready for production environment, no longer declares PHP error reporting requirements. 1.0.3 11 Dec 2017 Rewrite (again) of begin/end period sub-function in this script that is used for USA specification (12.7.1 k. Precipitation timings) [see version 0.1.0 of 13 Aug 2017 in Remark script]. Minor adjustments to output text related to Colour State, and completion of "friction co-efficient" for Runway State Group. Significant further rewrite of visibility function and minor adjustments to other functions to resolve a multitude of reported decoding issues, many related to this being the first encounter for my script suite of a variety of winter phenomena and mistakes by me in understanding the variety of code combinations that are possible. Correction to the function 'novel_decode_cloud' in the Regular Expression pattern relating to cloud type and to some assignments within its decoding of vertical visibility. 1.0.4 30 Dec 2017 Rewrite (this is becoming too frequent!) of function novel_allVisibility as it was not always recognising all segments relating to visibility, and (if there were no segments relating to visibility as in one Canadian case) assumed temperature and dew-point segment was visibility! Most ironically one failure was for UK METAR where first value was in metres and second value in kilometres, 2 other (Canadian) failures were due to sequence as took whole number as metres and ignored fraction of statute mile in next segment. Rectified by changes to sequence - changing order in which the various conditions are tested - (including looking for SM then km then m) and inclusion of up to 3 segments in call to function now, not just two as previously. Previously some visibility was handled in separate 'specific_visibility sub-function depending on type of visibility, now that snippet of code has become part (for easier maintenance) of the main visibility sub-function; i.e. all visibility related code uses same function novel_allVisibility. Also for easier maintenance, removed concatenating array SKY-DETAILS into string CLOUD-DETAILS, from this script into decodeMETAR.php script, so eliminating previous in-efficient logic of doing it both in this script and also in Remarks script. Functions like "do_output()" and "do_SLP", that are called from functions in "decodeMETAR_Rmk.php", have been amended to update the new array '$remarks_toDecode' that has been introduced to track the matching in the 'Remarks Group', plus variants of those sub-functions "walk_output()", "walk_SLP()", and the new call-back function "commonTests()" have all been added for direct processing of that new '$remarks_toDecode' array and these also make the related deletions from the original array '$metar_toDecode'. The new call-back function "commonTests()" has been given a number of boolean variables to prevent it repeating tests. Minor adjustment of 'novel_decode_cloud' when sky clear or no significant cloud. Change to decoding of "NSW" in Changes Group - now specifically quotes again what already in the Present Weather Group. 1.0.5 12 January 2018 Cosmetic changes, to sub-functions dealing with visibility and wind speed, so output does not show leading zeros. 1.1.0 29 January 2018 Revised four functions dealing with atmospheric pressure (called by processing for Remarks Group), since Sea Level Pressure now separated from altimeter pressure, and corrected pressure so consistently (outside Canada) shows hPa to one decimal place. Further correction of function novel_allVisibility as there were unwanted lines overwriting the correct output, also changed all prevailing visibility output to be in both metres and miles for consistency. Made sub-function doObscuring() actually work. Added 'clearstatcache();' where testing to see if object exists, to reduce memory overheads. In Prevailing Visibility Group (International metric) snippet of code was getting a fatal error, didn't understand why round() not recognised there, but found unprintable characters in the function name, so rectified, at same time removing hundreds of unprintable special characters (can be viewed in FilesCompare tool), found in many other places in this script. These unwanted characters were causing many condition matches to fail. Cosmetic minor changes elsewhere, simplifying script including deleting a dozen or so lines that were not needed (e.g. some instructions were duplicated). 1.1.1 16 Feb 2018 Somehow some odd characters have come back, correcting these again. Also removing leading zeros on output. Repeating correction ensuring SLP reported to correct precision consistently, and consistent with Standard Pressure Group, any Pressure reading in Remarks Group uses units of kiloPascals if Canada, hectoPascals elsewhere. Further corrections to visibility a) change to diagnostics for (Minimum Directional Visibility Group (standard) - Case when comes before Prevailing Visibility); b) addition of code for sixteenths within (Prevailing Visibility Group (USA & Canada quoting figure and 'SM')) and improvement to output text (showing fractions); c) change to diagnostics and now showing in miles and km for Prevailing Visibility Group (International metric - km); d) Changed 'specific visibility' into separate snippet for USA as well as (other snippet as previously) Canada, and e) Using conversion factors approved by ICAO (not UK Nautical miles). Revising phenomena sub-function, so it can now cope with qualifiers like 'past hour', 'intermittent', and 'No Significant Weather'. Correction of begin-end sub-function to meet changes to USA specification 12.7.1 k. Precipitation timings (wwB[hh]mmE[hh]mm...) applying from November 2017 (no longer just rain). Improvement of output for that same began/ended qualification (now if hour appears for began time it also appears for ended time). Removal of comments into portable document format paper. Improvement of output for Colour State reporting in supplementary Groups. Revision of wind average and gust speed outputs, latter was previously giving non-numerical error, but now outputs correct numbers and also clarifies UK or international Knots unit, although probably wrongly for USAF aerodromes in UK where it currently says UK knots and it is probably international knots, need to investigate later. 1.1.2 23 Feb 2018 Improvements to output format for visibility when original in metres (removal of miles with decimal places, replacement with integer miles, and integer yards). Correction of condition for dew-point (previously negative dew-point temperatures were being reported as "measurement not made") but now correctly identifies when measurement not made. Improved "function common_tests()", an array_walk for Remarks Group) diagnostics improved (visibility output matches that used elsewhere, better handling (previously wrong order of instructions as segment removed before being examined and causing error as it did not exist!) now it determines which segment has been matched and fully processes it before it rearranges array of segments remaining to be decoded. Renamed "Pressure Change" into "Pressure Tendency" in line with terminology used officially by USA. Corrected condition for handling of Colour State Code when "BLACK" included, now checks segment length is correct for two colours to be included. Variable Visibility snippet modified to cope with another malformed format with extra space before and after '-'. A few comments within script clarified to make future maintenance easier. 1.1.3 10 Mar 2018 Unused "function doRiseFall()" removed. Once more rewrote the whole of function novel_allVisibility() because of occasional errors in how it processed visibilities, this time greatly simplifying it partly by changing sequence, partly by replacing multiple complex if statements by simpler switch with case statements, and added more comments to it. Corrected tiny syntax typing error in function doSLP(). Now does not convert UK knots to International knots if ICAO code is UK one, but Remarks Group is present, as that is used to indicate it is