script= 'decodeMETAR.php', © author= SPAWS, version= 1.2.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 =============================================================================================================================================== This script is the top-level navigation for translating/decoding the METAR supplied as an input parameter. It is the first of five scripts that make up the SPAWS decoding suite. All scripts are to be placed in same directory on your web server. The second script, "decodeMETAR_sub_funct.php", is mandatory and is loaded by the require_once command that appears at line 65. That contains those essential sub-functions called from this script that are not included in this script, to keep this script small enough to maintain easily. The decoder itself is completed by three more scripts, these are optional, being only used if there is a Remarks Group in the METAR and its contents are to be translated (generally called in this suite 'decoded') to British English. There are three other sub-functions which are not called if the sub-functions do not exist, two of these 'add_source()' and 'display_source()' appearing on lines 45 and 54, are standard functions for enabling the displaying of PHP sources used by all SPAWS scripts. Both those functions are included in optional 'sourceViewC.php' (see line 34), that is not essential, but will give additional functionality. A third sub-function exception 'calculateBeaufort()', is called from "decodeMETAR_sub_funct.php", and that function because it is also used by other SPAWS scripts is also in 'sourceViewC.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 this script 'decodeMETAR.php' and "decodeMETAR_sub_funct.php" are the three scripts that are minimum recommended to run the decoder. ------------------------------------------------------------------------------------------------------------------------------------------------------------------ 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. ----------------------------------------------------------------------------------------------------------------- */ # 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 # #--------------------------------------------------------------------------# global $home; $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 # #-----------------------------------------------------------# #^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^# # Command to load script containing essential sub-functions # #^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^# // Stored in same directory as this script $pos = strlen(basename(__FILE__)); $path = substr(__FILE__,0, -1 * $pos); if(file_exists($path . 'decodeMETAR_sub_funct.php')) require_once($path . 'decodeMETAR_sub_funct.php'); clearstatcache(); #^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^# # End of load script containing essential sub-functions # #^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^# /* --------------------------------------------------------------------------------- PURPOSE OF THIS SCRIPT AND HOW IT IS PART OF A SUITE The purpose of this script in the suite of three decoding scripts is to take as input a string containing a METAR weather report and process the abbreviations, contractions, numbers, plain language, and symbols into and output an associative array containing plain British English as a series of elements. The script was first written to decode all the possible content for METAR reports issued in UK, but now will process content for METAR reports issued anywhere in the world. There is more about the regional variations in METAR later in this script and further comments in the other scripts in this suite. This particular script contains the main sub-functions that deal with the highest level of the processing requirement for the decoding of a METAR. The other scripts in the suite deal with the more complicated detailed level of processing and the Remarks Group that does not have a single international specification, but varies between regions and nations. Having all the decoding in one script as first attempted was somewhat unwieldy at over three thousand, four hundred and fifty lines (and it now would be close to 12 thousand lines as the ability to decode more specifications particularly within Remark Group was added), so the script has been split between this one containing the main script as called from an external script, and the main sub-functions that the main script calls directly; separated from this main script is another script containing all the remaining essential sub-functions called from these main sub-functions (and can also be called from each other and from the sub-functions in the Remarks Group script) and a third script just for decoding the Remark Group (and that as predicted has grown to about six thousand lines on its own). This new separation of the different parts of the suite has added to the readability and allowed a number of improvements to be made by using more, but simpler, sub-functions and calling more of them from this script as boolean variables making it easy to spot whether either the whole METAR can be successfully decoded by matching to WMO standard or the segment in the METAR being processed is either to an obsolete format or malformed; in some of these cases full decoding is impossible. --------------------------------------------------------------------------------- Copyright 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 for use by others on such amateur sites, provided due acknowledgement is made to the work put in by SPAWS, just as SPAWS pays acknowledgement in this suite to snippets written by other programmers that have inspired coding here or even have small parts of their work incorporated in this script while I use the word 'novel' to label those snippets that are entirely my design. A lot of work has gone into development and (even more work involving decoding well over a thousand METAR reports for) testing this suite, consequently the intellectual ownership of this script 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 from use of this code or indirectly from my solutions for matching raw METAR segments into the appropriate WMO Groups and coping with the variety of inputs. --------------------------------------------------------------------------------- */ ##################################################################################################### # Function to decode METAR - this is main entry point from an external calling script. # ##################################################################################################### function SPAWS_identify_Groups ($raw_metar, $remarksOnly = '', $onScreen = true, $everything = '') { // parameter name emphasises that the input string might need some cleaning, see function directly below this one global $home, $show_diagnostics, $show_diagnosticsR, $dM, $dMsf, $dM_Rmk; // Global Variables can be shared with calling script and with other functions or scripts; # The global variable $dm is used to share author and version information for this script, that might be useful for a footer credit for example, # it also reminds you that copyright residing with original author is important! Similar variables are defined for scripts chained from this script. //===================================================// // Clean up and validate the complete METAR // //===================================================// $cleaned_metar = decodeMETAR_clean ($raw_metar); // call separate pre-process //================================================// // DIAGNOSTIC FUNCTIONALITY for testing // //================================================// if(!isset($show_diagnostics)) $show_diagnostics = false; if(!isset($show_diagnosticsR)) $show_diagnosticsR = false; if(!is_null($everything)) $show_diagnosticsR = $show_diagnostics = $everything; if(!is_null($remarksOnly)) $show_diagnosticsR = $remarksOnly; # if(strpos($home, 'UniServerZ')) $show_diagnostics = true; // On my server, always show the diagnostics # if(strpos($home, 'UniServerZ')) $show_diagnosticsR = true; // On my server, always show the diagnostics # conditional de-bugging section heading output to HTML # The extensive optional diagnostic text will look best if the following classes are defined in an external CASCADING STYLE SHEET: # A class called 'titre' is used to format the whole diagnostic text, typically this will define a background colour and font-size that makes it easy to read, but it could be used to define # the text as 'hidden' until clicked on or similar clever formatting. # A class called 'grey' is used to format the less important progress reports, so they are less dominating and in consequence it is easier to spot the more significant messages. # A class called "cday" (its name has a significance external to this suite) is used to format messages that mark the successful completion of a matching process. # A class called "blue" is used to format key phrases in messages so they are easier to spot, typically this will define a font colour (such as blue) different to the default font in class 'titre'. # A class called 'red' is used to highlight specific words in the diagnostic text, typically this will define a font colour (such as red) different to the default font in class 'titre'. # A class called 'sienna', and another called 'black', are used to make the contents of selected diagnostic messages easier to read, again usually by defining the related font colour. # You don't need this CSS to see the optional diagnostic text. Depending on your browser choice some output may appear before all the web page is prepared, or the web page with all # the output will stay in a buffer until the end of processing. if($show_diagnostics or $show_diagnosticsR) { if($onScreen) echo '
'; else echo ''; } #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# # Finished with decoding by SPAWS-METAR-Decoder. # #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# return $metarOutput; } ######################################################################### # Function to decode METAR - end of the main function called externally # ######################################################################### //----------------------------------------------------------------- // Function to pre-process METAR (some parts copied from Ken True's Saratoga script, some original) //----------------------------------------------------------------- function decodeMETAR_clean ($metar) { // Clean up the METAR string ... some are not properly formatted, human error, most likely // Patterns to be looked for are delimited by '~' in all expressions // The individual decoding sub-functions include script for coping with many many more malformed possibilities than the two below as copied from Ken True's Saratoga script $metar = preg_replace("~['@\.]~is",'',$metar); // remove any strange ', @, or . in METAR (none mentioned in any standard as being an allowed character anywhere) $metar = preg_replace('~(\d{5}) KT~i','${1}KT',$metar); // fix any space in surface wind value (or its equivalent in REMARKS) $metar = trim($metar); // trim off spaces at start or end (so can count from start or end if need to) $metar = preg_replace('~[\r\n]+~is',' ',$metar); // replace internal (Windows CR LF) newline found in source with space $metar = preg_replace('~ \s+~is',' ',$metar); // remove multiple spaces (tidying) $metar = strtoupper($metar); // (novel) ensure all alphabetic characters are upper-case $metar = preg_replace('~[=]~','',$metar); // remove terminating equals sign, the only end of message symbol I have seen used (METAR rules allow free choice of terminating character) return $metar; } ############################################################################################################################################################################################# # Most of the remaining sub-functions share a set of global variables (not used in main function at start), 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 and added to the $decodeGroupProcessed array that # corresponds with the Group being processed. If different parts of one segment have been separated for decoding, then a tilde '~' character is added before transfer indicating where separated. # This script will also 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, # (the symbol '_' joins those elements on output) or because there are multiple instances of that METAR Group or a related element is found in Remarks (the symbol "+" links those on output). // # The global variables $dM, $dMsf, $dM_Rmk are used to share author and version information for each script in the suite, that might be useful for a footer credit for example, # but also their content reminds you of the important message that interlectual property ownership and copyright is still residing with original author! ############################################################################################################################################################################################# #--------------------------------------------------------------------# # The novel sub-function that follows is the start of actually # # parsing the input METAR and trying to match it to WMO Groups. # # It also defines most of the validation arrays used elsewhere. # # Intellectual Property of SPAWS - July 2017 # #--------------------------------------------------------------------# function decodeMETAR_parseGroups ($the_metar) { ##################################################################################################### # (Note, this script decodes what found in a METAR, it is written to even decode some misformed # # content, and to cope with content that is particular to certain nations or organisations. # # However, it does not validate against the rules of either WMO, ICAO, or particular nations). # ##################################################################################################### // The following global variables are shared with scripts outside this suite global $show_diagnostics, $show_diagnosticsR, $dM, $dMsf, $dM_Rmk; // The following Global Variables can be shared with all functions in this or other scripts making up this decoder; global $metar_toDecode, $decodeInfo, $decodeGroupProcessed, $decodeGroupCount, $pressureHPA, $lastSuccess, $stillEncoded_file; global $monthArray, $bearingsArray, $compass, $compass_expanded, $descriptorArray, $cloud_type_array, $cloudAmountBandCode, $obscurationTypes, $precipitationTypes, $miscellaneousTypes, $colour_array; ############################################################################################################################################################################################# # The first 4 globals are initialised directly below and are effectively the workings of the decoder suite. # The global boolean $show_diagnostics can be set to true in the calling script, but it is optional, set to true for full diagnostics, if set to false or not defined, then no diagnostic output appears; # The global boolean $show_diagnosticsR can also be set to true in the calling script, and it is optional, but it controls more restricted diagnostics, mainly related to Remarks Group; ############################################################################################################################################################################################# //===================================================// // Define arrays to be used within this suite // //===================================================// // The first array contains the METAR separated into individual segments using space character, this is a dynamic array; // Throughout the decoding suite, it will reduce in size so that it only contains those segments that have not yet been decoded $metar_toDecode = explode(' ', $the_metar); // As each part of the METAR is decoded, the segments removed from the above array are added to the next array, but with additional symbols as explained later $decodeGroupProcessed = array(); // Decoding the parts of the METAR involves matching them with the Groups defined by WMO and this next array keeps a tally of what happens for each group // An individual value for an element in this array is frequently a count of how many items were matched to the group, but it can be a textual decription instead $decodeGroupCount = array(); // The next array is used to hold the British English output, one match may produce more than one array element here, usually giving either alternative units or derived additional statistics $decodeInfo = array(); // Used for decoding dates into friendlier format $monthArray = array ("Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"); $cloud_type_array = array( 'Altocumulus' => 'AC', 'Altocumulus Castellanus' => 'ACC', 'Altostratus' => 'AS', 'Cirrocumulus' => 'CC', 'Cirrostratus' => 'CS', 'Cirrus' => 'CI', 'Cumulonimbus' => 'CB', 'Cumulus' => 'CU', 'Cumulus Fractus' => 'CF', 'Nimbostratus' => 'NS', 'Noctilucent (or Night) Clouds' => 'NLC', # 2017/12/27 11:30 METAR GLRB 271130Z 00000KT 4000 HZ NLC 30/21 Q1011 - Roberts International Airport - Monrovia, Liberia 'Stratocumulus' => 'SC', 'Stratus' => 'ST', 'Stratus Fractus' => 'SF', 'Towering Cumulus (Congestus)' => 'TCU' # The cloud type described in the International Cloud Atlas as “Cumulus Congestus” is “Towering Cumulus.(TCU)” ); $cloudAmountBandCode = array( // Based on WMO definitions, implemented according to UK CAA definitions, but also used for USA FAA (and others) decoding in Remarks Group 'blank' => '', 'VV' => 'Limited vertical visibility', // UK CAA see 4.108, slightly confusing but means VV used but without specifying height, example page 64 suggests 'VV///' 'SKC' => 'Clear (WMO definition)', // Not used in UK, but included in other decoders, so added here 'CLR' => 'Clear (N. American definition)', // Applicable to North America only; below 12,000 ft in USA, below 25,000 ft in Canada 'NSC' => "No significant clouds below 5,000 feet, 1½ km;
or below the Minimum Sector Altitude whichever is greater", // Available for use by Observers. 'NCD' => "No cloud discernible below 5,000 feet, 1½ km;
or below the Minimum Sector Altitude whichever is greater", // Only available for use if automatic reporting 'FEW' => 'Cover up to ' . "¼" . ' of sky (2 oktas)', // UK usage 'FE' => 'Few Clouds', // USA usage 'SCT' => '3-4 oktas', // WMO standard 'BKN' => '5-7 oktas', // UK usage 'BK' => 'Mostly Cloudy', // USA usage 'OVC' => 'Overcast 8 oktas', // WMO standard 'TCU' => 'Towering Cumulus', // WMO standard phrase is 'cumulus with strong vertical development', // ICAO terminology - uses TCU (Towering Cumulus) for internation cloud definition of 'Cumulus congests of great vertical extent' # ADDITIONAL DESCRIPTIVE CODES BELOW are strictly for use with CUMOLONIMBUS (CB) only, but in fact used more widely (see below) 'CB' => 'Cumulonimbus', // WMO standard 'EMBD' => 'Embedded', 'LYR' => 'Layers', 'FRQ' => 'Frequent', // Lightning, but can be used as description for TCU (see examples below) 'ISOL' => 'Isolated', // Lightning, but can be used as description for TS 'OCNL' => 'Occasional', // Lightning ); /* EXAMPLES OF CODE COMBINATIONS: ('_' denotes space between segments) BKN_CLD, FRQ_CB, FRQ_TCU, ISOL_CB, ISOL_TS, ISOL_TSGR (Thunderstorm with hail), ISOL_TCU, MOD_ICE (moderate icing), MOD_MTW (moderate mountain wave), MOD_TURB (moderate turbulance), MT_OBSC (Mountain obscuration), OCNL_CB, OCNL_TS, OCNL_TSGR, OVC_CLD, SFC_VIS (surface visibility), SFC_WIND (widespread mean surface wind) */ $descriptorArray = array( 'Shallow ' => 'MI', 'Partial ' => 'PR', // also used to describe "fog banks" when preceding qualifier to "FG" 'Patches of ' => 'BC', // always followed by type of precipitation so include " of ". 'Low Drifting ' => 'DR', 'Blowing ' => 'BL', // NB crucial not to be confused with 'Airfield STATE' colours black or blue that have same two letters at start // The descriptors above are only used with 'present weather', the descriptors below can apply to 'present weather', 'recent weather' or 'trends' 'Shower' => 'SH', // Standard suggests always precedes a type of precipitation, but in practice many METAR just use SH, so we will decide later whether to add the " of " to the text. 'Thunderstorm' => 'TS', // NB Thunderstorm can be declared both in this Group and in 'Cloud' Group, this script can code storm here from entry in either of these places // UK CAA paragraph 4.109 - this script does not cross-check cloud specification when thunderstorm in present weather 'Freezing ' => 'FZ' ); $precipitationTypes = array( 'Drizzle (drop diameter less than 0.5 mm)' => 'DZ', 'Rain' => 'RA', 'Snow' => 'SN', // snow depth measured in Canada to nearest 2 mm 'Snow Grains (very small white and opaque)' => 'SG', 'Ice Crystals (Diamond Dust)' => 'IC', // WMO use 'Diamond Dust' for what most English speakers call 'Ice Crystals', // this glittering is a phenomena for very low temperatures // To meet Canadian standards, when ice crystals (IC) are observed, // it shall be reported in the METAR/SPECI with any visibility. 'Ice Pellets' => 'PL', // Current recommended code, but still not widely used; see PE, the original code for ice pellets 'Ice Pellets (transparent or translucent)' => 'PE', // An attempt has been made to stop 'PE' arguing that "RA" could be legitimately followed by "PE" 'Hail stones' => 'GR', // Some countries have a broad definition for hail, others see hail only as objects above a minimum size and therefore use a narrower definition // New Zealand CAA, UK CAA still includes small hail or translucent ice particles in "GS". 'Snow Pellets' => 'GS', // Up until October 2017 USA also included small hail in "GS". // Canada 3.4.3.5.2 says "Small hail with a diameter of the largest hailstones less than 5 mm shall be abbreviated as SHGS" 'Unidentified precipitation' => 'UP' // Regional code, WMO authorises it for use, or not, depending on WMO region ); /* The following notes apply exclusively to the way that present weather is encoded in METAR and SPECI reports from WHICH COUNTRY. 3. The weather group(s) are coded by combining appropriate abbreviations from each column working from left to right e.g. a heavy shower of rain is encoded as: +SHRA. 4. If there is more than one weather phenomenon, up to 3 separate groups are encoded in the same order as the columns in the table e.g. light drizzle and fog is encoded as: DZ FG. 5. An exception to the above rule is that the groups for more than one form of precipitation are joined together with the dominant type first e.g. SNRA indicates moderate snow and rain (sleet), with snow the dominant precipitation. 6. GS signifies that the largest hailstones are less than 5 mm in diameter, otherwise GR is used. 7. VC (in the vicinity) denotes between 8 km and 16 km from the aerodrome reference point, and is used to indicate only the following significant weather phenomena observed in the vicinity of the aerodrome: TS, DS, SS, FG, FC, SH, PO, BLDU, BLSA, BLSN and VA. The abbreviation VCFG is used to report any type of fog observed in the vicinity of the aerodrome. 8. In the absence of any precipitation: (a) FG (fog) is used when visibility is less than 1000 m. (b) BR (mist) is used when visibility is between 1000 m and 5000 m. (c) HZ (haze) is used when visibility is less than 5000 m, and the reduction is caused by something other than water droplets or ice crystals. */ $obscurationTypes = array( // Visibility reduction codes according to WMO consist of 19 possibilities // These are DZ, DU, PO, DS,FG, FC, GR, HZ, PL, BR, RA, SA, SS, GS, FU, SN, SG, SQ, VA // Some of these are in this array and some in other arrays. // NB Canada has alternative array see at line 647 in "decodeMETAR_Rmk.php" version 0.2.6 'Mist' => 'BR', 'Fog' => 'FG', 'Smoke' => 'FU', 'Volcanic Ash' => 'VA', 'Dust' => 'DU', 'Sand' => 'SA', 'Haze' => 'HZ', 'Spray' => 'PY' ); $miscellaneousTypes = array( 'Dust devil' => 'PO', 'Squalls' => 'SQ', 'Funnel Cloud' => 'FC', // Tornado and Waterspout denoted by + in intensity 'Dust Storm' => 'DS', 'Sand Storm' => 'SS' ); // Initialisations for Colour State codes are next, as that snippet of script can be entered more than once # Example from Meierwik, Glücksburg, Germany: METAR ETGG 011220Z 18011kt 9999 FEW030 17/10 Q1019 BLU+ # Version 0.2.0 of this script did not decode above METAR due to '+' after colour code, so in version 1.0.0 have added just that one, # assuming only best can be modified! $colour_array = ['', 'BLU+', 'BLU', 'WHT', 'GRN', 'YLO1', 'YLO2', 'AMB', 'RED']; $bearingsArray =array( 'VRB' => 'Variable', "000" => "Calm", '010' => '10 degrees', '020' => '20 degrees', '030' => '30 degrees', '040' => '40 degrees', '050' => '50 degrees', '060' => '60 degrees', '070' => '70 degrees', '080' => '80 degrees', '090' => '90 degrees', '100' => '100 degrees', '110' => '110 degrees', '120' => '120 degrees', '130' => '130 degrees', '140' => '140 degrees', '150' => '150 degrees', '160' => '160 degrees', '170' => '170 degrees', '180' => '180 degrees', '190' => '190 degrees', '200' => '200 degrees', '210' => '210 degrees', '220' => '220 degrees', '230' => '230 degrees', '240' => '240 degrees', '250' => '250 degrees', '260' => '260 degrees', '270' => '270 degrees', '280' => '280 degrees', '290' => '290 degrees', '300' => '300 degrees', '310' => '310 degrees', '320' => '320 degrees', '330' => '330 degrees', '340' => '340 degrees', '350' => '350 degrees', '360' => '360 degrees' ); $compass = array('', 'N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'); $compass_expanded = array( '-' => '', 'calm' => 'calm', 'VRB' => 'Variable directions', 'N' => 'True North', 'NNE' => 'North-North-East', 'NE' => 'North-East', 'ENE' => 'East-North-East', 'E' => 'East', 'ESE' => 'East-South-East', 'SE' => 'South-East', 'SSE' => 'South-South-East', 'S' => 'South', 'SSW' => 'South-South-West', 'SW' => 'South-West', 'WSW' => 'West-South-West', 'W' => 'West', 'WNW' => 'West-North-West', 'NW' => 'North-West', 'NNW' => 'North-North-West' ); //======================================================// // End of define arrays to be used within this suite // //=======================================================// if($the_metar == '') return; // this function called simply to set up variables, return without continuing //===================================================// // Is METAR of minimum length or valid content? // //===================================================// # Example of minimum length METAR ('Type of Report Group', 'Aerodrome Identity Group', 'Time Group' and 'Identification of an automated or missing report Group'): # Cranfield Airport issue at 01/07/2017 06:50-> # METAR EGTC 010650Z NIL // The 'NIL' reports are not generated by the individual aerodromes, but by the communication centre that collects METAR from different aerodromes together and then sends // them as a bulletin of multiple METAR up to satillite or whatever. If a METAR is missing from the expected collection, a short one with 'NIL' as above is created. if(strlen($the_metar) < 22) // the 22 character definition of shortest valid METAR is a string that starts with "METAR" and ends with "NIL" as in example above { $decodeInfo['FEEDBACK'] = 'Input string rejected, no decoding'; // FEEDBACK is global element that contains one message, not for actual decoded output goto function_exit; // 'goto' is allowed in PHP 5.3.0 and higher, and recommended to avoid multiple layers of conditionals and improve readability in PHP 7 } //===================================================================================================================// // Start processing the parts, the approach in this script is to use an array to contain those parts not yet decoded // // Each time, a segment is matched to a Group and decoded, that element is removed from the array, so it is dynamic // //===================================================================================================================// ########################################################################################################### ## Looking for Groups, not part of the METAR standard, included as prefix in those from other sources ## ########################################################################################################### switch(substr($metar_toDecode[0],0,2)) { case 'ME': break; // Starts with 'METAR', no prefix to decode case '20': // Example of string read from NOAA source after it has been through cleaning sub-function 'decodeMETAR_clean()': // "2017/07/26 12:50 EGWU 261250Z 21010KT 9999 -RA SCT013 BKN029 BKN080 20/17 Q1006 GRN TEMPO 7000 -RA GRN" if(substr($metar_toDecode[0],4,1) == '/') { $return = decodeMETAR_NOAA(); if($return) { $decodeInfo['FEEDBACK'] = 'Confirmed in format read from NOAA source file site after cleaned'; // FEEDBACK is global element that contains one message, not for actual decoded output; $decodeGroupProcessed['PREFIX'] = array ($metar_toDecode[0], $metar_toDecode[1]); $metar_toDecode = array_slice($metar_toDecode, 2); // take out 2 processed segments; if omit length parameter in slice, keeps rest to end; // add standard 'METAR' segment, that does not appear on NOAA source (because all the files in that folder are METAR) if(strpos($the_metar, 'METAR') === false) { $done = array_unshift($metar_toDecode, 'METAR'); $the_metar = $decodeGroupProcessed['PREFIX'][0] . ' ' . $decodeGroupProcessed['PREFIX'][1]; for($iCount=0; $iCount < $done; $iCount++) { $the_metar .= ' ' . $metar_toDecode[$iCount]; } } } else die ('Unrecognised NOAA format'); break; }else{ $return = decodeMETAR_OGIMET(1); // fall-through to next case } case 'SA': /* For "Operational Meteorological Information" (OPMET) distributed via the "Satellite Distribution System for Information relating to Air Navigation" (SADIS), the WMO Header codes (prefix) are different, "SA" is used to indicate a METAR that will use the TAC format, the header continues with two alphabetical characters and then one or two digits. A prefix of '0' is added by UK MetO Gateway to any single digit to convert it to two digits for uniformity. (See https://www.icao.int/safety/meteorology/sadisopsg/SADIS User Guide/SADIS Gateway Operations Handbook - Fourth Edition 2009.doc). The two alphabetic characters are a geographical designator, composed of two letters according to WMO No. 386, Manual on the Global Telecommunication System, Part II – Operational Procedures for the GTS, Attachment II-5, Table C1. The one or two digits is a sequential number identifying the bulletin, incremented each time a new bulletin for the same geographical designator is issued. Presumably, it resets to initial number (might not be zero, as number may also be in a range that indicates purpose) each hour. This is followed by a space and then a four-character code (the ICAO aerodrome identification code, although the gateway does not validate it), and then the day_of_month and time Group that ends in 'Z'. The WMO Header code ends with a 'BBB' group, this is optional (in the handbook just referenced, an example of it being 'RRA' is given) and indicates amended, corrected or delayed bulletin. 'RRx' means delayed routine meteorological messages/bulletins; 'CCx' means corrections to previously relayed messages/bulletins; and 'Pxx' is used when there are a large number of METAR to transmit and they have been separated into several bulletins each with their part identity. Each 'x' can be any letter from 'A' to 'X'. After that WMO Header comes all the actual METARs in turn, each ending with a "=" as the terminating character (this is used to confirm that the METAR has not been truncated). In each METAR, the 'SA' gets replaced by either "METAR" or "METAR COR" and is then followed by a valid ICAO aerodrome identifier, then the day_of_month and time Group that ends in 'Z', and after that the remaining METAR Groups. (See https://www.icao.int/WACAF/Documents/APIRG/SG/2013/apirg-met-sg11/WP04_appD_AMBEX_HandBook_7th_Edit_%20Amendmt2-eng.pdf). */ // This format with a SA prefix is still used by OGIMET database if it is asked to output available METAR in HTML format: // "SA 01/07/2017 09:20-> METAR EGTC 010920Z 33005KT 290V010 9999 FEW030 SCT041 17/13 Q1016=" // Example of OGIMET text format, this is different despite starting with year as in NOAA // 201801012350 METAR EGVP 012350Z AUTO 27013KT 9999 // NCD 06/03 Q1010 if(substr($metar_toDecode[0],0,2) == 'SA') $return = decodeMETAR_OGIMET(3); if($return) { $decodeInfo['FEEDBACK'] = 'Confirmed in format read from OGIMET source database after cleaned'; // FEEDBACK is global element that contains one message, not for actual decoded output; $decodeGroupProcessed['PREFIX'] = array ($metar_toDecode[0], $metar_toDecode[1], $metar_toDecode[2]); $metar_toDecode = array_slice($metar_toDecode, $return); // take out 1 (type 1) or 3 (type 3) processed segments; if omit length parameter in slice, keeps rest to end; } else die ('Unrecognised OGIMET format'); break; default: die ('Unrecognised prefix format'); } $lastSuccess = ''; //===================================================// // Store complete cleaned METAR ready for output // //===================================================// $decodeInfo['METAR'] = $the_metar; // add whole METAR to output array, so external script can display what has been decoded! ############################################## # Looking for WMO standard Groups in METAR # ############################################## // Canada and USA leave out a mandatory standard Group if the required sensor is not working // Other regions obey iCAO and WMO rules and use solidi '/' to replace each character of a code that cannot be quantified optionalDiagnosticOutput('After processing any prefix, start looking for content in Standard Groups'); $matched = decodeMETAR_standard(); // Decode WMO Standard Groups using sub-function if(is_null($matched['lastSuccess'])) { print_array($matched, 'After processing standard Groups'); $decodeGroupCount['STANDARD'] = false; // Failed to decode standard Groups, exit without looking at conditional Groups goto function_exit; // 'goto' is allowed in PHP 5.3.0 and higher, and recommended to avoid multiple layers of conditionals and improve readability in PHP 7 }else $decodeGroupCount['STANDARD'] = true; ################################################ # Finished with WMO standard Groups in METAR # ################################################ #------------------------------ # Are there any more Groups? if(!$matched['toDo'] or count($metar_toDecode) < 1) goto WMO_finish; #------------------------------ optionalDiagnosticOutput('Attempting to identify whether to match to a WMO supplementary Group or Colour State Group or Remarks Group'); if($metar_toDecode[0] == 'RMK') goto WMO_finish; // No supplementary segments to decode, jump to Remarks Group ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #--------------------------------------------------------- # Call sub-function that looks at supplementary Groups #--------------------------------------------------------- ########################################################################################################## # Looking for the supplementary Groups in METAR as defined by WMO regulations paragraph 15.13 # # These are defined as 'Recent Weather' (conditional - used only when operational significance), # # 'Wind Shear in lower levels' (conditional - used only when information available), # # 'Sea surface temperature and state of the sea' (regional - a nation can agree usage with WMO), # # and 'State of the Runway' (regional - WMO define some codes but a nation can agree usage). # ########################################################################################################## // None of the supplementary Groups are mandatory. Some are conditional (WMO set the format and define which certain conditions need to exist for the Group to be included), // and some are regional (WMO define a set of codes, but each nation can agree with WMO whether that nation uses the Group and what format it uses around the defined codes). # Additionally, for military purposes, there is an overall quick impact, summary code: // The Colour State Group is not in the WMO standard, however this script will treat it for decoding purposes with the WMO supplementary Groups // as it is associated with them. $matched= decodeMETAR_supplementary(); if(is_null($matched['lastSuccess'])) { $decodeGroupCount['SUPPLEMENTARY'] = false; // Failed to decode standard Groups, exit without looking at conditional Groups goto function_exit; // 'goto' is allowed in PHP 5.3.0 and higher, and recommended to avoid multiple layers of conditionals and improve readability in PHP 7 }else $decodeGroupCount['SUPPLEMENTARY'] = true; ##################################################### # Finished with WMO supplementary Groups in METAR # ##################################################### WMO_finish: if($show_diagnostics) echo "
End of examining by $dMsf\n"; #------------------------------ # Are there any more Groups? if(count($metar_toDecode) < 1) goto function_exit; #------------------------------ jumpRemarks: #--------------------------------------------------------------------- # Call sub-function that looks at Remark Group (Regional specific) #--------------------------------------------------------------------- ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // WMO regulations, in paragraph 15.15, defines a Remarks Group as totally separate. // The format is "RMK . . . . . . . . . . " // The indicator RMK denotes the beginning of a section containing information included by national decision which (WMO say) shall not be disseminated internationally. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// optionalDiagnosticOutput('WMO Regional specific: Remarks Group'); $lastSuccess = 'Remark'; $matched = decode_remarks(); function_exit: #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# # Finished with parsing METAR in my Decoder. # # Create array to transfer outputs back one function # #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# unset($returnArray); $returnArray['Info'] = array_slice($decodeInfo, 0); $returnArray['GroupCount'] = array_slice($decodeGroupCount, 0); $returnArray['GroupProcessed'] = array_slice($decodeGroupProcessed,0); return $returnArray; } ######################################################################## # sub-function to decode METAR - end of decodeMETAR_parseGroups() # ######################################################################## //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++// // The sub-functions that follow are called from the sub-function decodeMETAR_parseGroups () // // found just above here, called by another one of the sub-functions found below here, or // // called by one of the sub-functions in 'decodeMETAR_sub-funct.php' or 'decodeMETAR_Rmk.php'. // // For each sub-function, there are notes explaining origin of that code sequence. // //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++// #--------------------------------------------------------------------# # The novel sub-function that follows is a more friendly way # # of displaying the contents of an array for easy diagnosis. # # Intellectual Property of SPAWS - October 2017 # # The first input parameter is the actual array to be output. # # The second input parameter is a string to describe the array. # # (It can be the actual array name enclosed in single quotes). # #--------------------------------------------------------------------# function print_array($array_input,$array_name = 'Array') { if(!count($array_input)) echo '
'. $array_name . '( )    is Empty'; else{ echo '
'. $array_name . '(     '; foreach ($array_input as $key_input => $value_input) { if (is_array($value_input)) { echo ' [' . $key_input . '] → '; print_array($value_input,'Sub-array'); } else { echo " [$key_input]".' => '; echo ' ' . $array_input[$key_input] . ',        '; } } echo ');    '; } echo '
'; } #-------------------------------------------------------------------------------------------------------------------------------------# # The novel sub-function that follows is used frequently by the other sub-functions and ensures common style for easy diagnosis. # # Basically, when diagnostics are switched on, it outputs (in a grey font) a description of the specification that is currently # # being processed, and a maximum of three undecoded segments that are being examined against that specification. Line breaks # # surround the message so it looks neat. Intellectual Property of SPAWS - October 2017 - novel code with novel appearance. # #-------------------------------------------------------------------------------------------------------------------------------------# function optionalDiagnosticOutput($myParameter, $file = '') { // Global Variables can be shared with calling script and with other functions or scripts; global $decodeGroupProcessed, $metar_toDecode, $show_diagnostics, $show_diagnosticsR; if($show_diagnostics or (isset($show_diagnosticsR) and $show_diagnosticsR and isset($decodeGroupProcessed['REMARK'][0]))) { echo '
For "' . $myParameter . '" will examine:
'; // conditional de-bugging section continues if(count($metar_toDecode) > 4) { list($a, $b, $c, $d) = $metar_toDecode; echo '$metar_toDecode[0]=' . $a . ', $metar_toDecode[1]=' . $b . ', $metar_toDecode[2]=' . $c . ', $metar_toDecode[3]=' . $d . ''; }else print_array($metar_toDecode, '$metar_toDecode ');// print each element of array to show segments left to be decoded for de-bugging purposes echo "
\n"; // Optionally also output diagnostic information to specified log file if($file != '') { file_put_contents($file, "For '" . $myParameter . "' will examine the following Remarks Group segments:\r\n", FILE_APPEND | LOCK_EX); foreach($metar_toDecode as $key => $value) { file_put_contents($file, (' raw=' . $key . ' >>> ' . $value . ' '), FILE_APPEND | LOCK_EX); } file_put_contents($file, "\r\n", FILE_APPEND | LOCK_EX); } } } #-------------------------------------------------------------------------------------------------------------------------------------# # The novel sub-function that follows is used by the other sub-functions anywhere a METAR quotes a time in UTC . # # Intellectual Property of SPAWS - October 2017 - novel code snippet with ability to cope with a variety of input time formats. # # Basically, it takes a time input in format hh:mm where 'hh' can be '99' if only minutes are specified in METAR. # # It takes the multiple hour (or half-hour) difference between the time-zone declared in the PHP script calling this decoding suite # # and Co-ordinated Universal Time so it can apply the appropriate adjustment to the input in UTC and output in local time. # #-------------------------------------------------------------------------------------------------------------------------------------# function adjustTime($UTCtime, $UTCday = '') { global $decodeGroupCount, $decodeGroupProcessed, $metar_toDecode, $show_diagnostics, $show_diagnosticsR, $hoursOffset; if(!is_numeric(substr($UTCtime,0,2))) echo '
Unrecognised format for hour ' . substr($UTCtime,0,2) . '
'; $hour = 1 * substr($UTCtime,0,2); if(!is_numeric(substr($UTCtime,-2))) echo '
Unrecognised format for minute ' . substr($UTCtime,-2) . '
'; $minute = substr($UTCtime,-2); if($show_diagnostics or (isset($show_diagnosticsR) and $show_diagnosticsR and isset($decodeGroupProcessed['REMARK'][0]))) echo '

ON ENTRY into adjustTime(): input hour=' . $hour . ', input minutes=' . $minute . '; hour adjustment to make=' . $hoursOffset . '

'; if (!is_numeric($hoursOffset)) { if($show_diagnostics or (isset($show_diagnosticsR) and $show_diagnosticsR)) echo '
Time adjustment not numeric, no adjustment made'; $return['day'] = $UTCday; if($hour < 10) $return['hour'] = '0' . $hour; else $return['hour'] = $hour; if($minute < 10) $return['minute'] = '0' . $minute; else $return['minute'] = $minute; return $return; } if (!is_numeric($hour)) { if($show_diagnostics or (isset($show_diagnosticsR) and $show_diagnosticsR)) echo ',!--
Input hour not numeric, returning only minutes'; $return['day'] = ''; $return['hour'] = ''; if($minute < 10) $return['minute'] = '0' . $minute; else $return['minute'] = $minute; return $return; } if($hoursOffset == 0) { if($show_diagnostics or (isset($show_diagnosticsR) and $show_diagnosticsR)) echo '
Local time is GMT, no adjustment made'; $return['day'] = $UTCday; if($hour < 10) $return['hour'] = '0' . $hour; else $return['hour'] = $hour; if($minute < 10) $return['minute'] = '0' . $minute; else $return['minute'] = $minute; return $return; } if($hour == 99) { if($show_diagnostics or (isset($show_diagnosticsR) and $show_diagnosticsR)) echo '
Input hour not supplied, returning only minutes'; $return['day'] = ''; $return['hour'] = ''; $revised = $minute + ($hoursOffset * 60); // minutes past (may be greater than 59) }else{ $return['day'] = ''; $revised = (60 * $hour) + $minute + ($hoursOffset *60); // minutes past (includes hour discrepancy so may be greater than 59) $return['hour'] = substr(number_format($revised/60, 1), 0, -2); if($return['hour'] < 0) $return['hour'] = 24 + $return['hour']; if($return['hour'] >23) $return['hour'] = -24 + $return['hour']; if($return['hour'] < 10) $return['hour'] = '0' . $return['hour']; if($show_diagnostics or (isset($show_diagnosticsR) and $show_diagnosticsR and isset($decodeGroupProcessed['REMARK'][0]))) echo '

output hour=' . $return['hour'] . ', minute adjustment to make=' . $revised . '; output minutes=' . $revised % 60 . '

'; if($UTCday != '') { $return['day'] = $revised > 0 ? $UTCday : $UTCday - 1; if($hoursOffset > 0 and $return['hour'] > $hoursOffset) $return['day']++; if($show_diagnostics or (isset($show_diagnosticsR) and $show_diagnosticsR and isset($decodeGroupProcessed['REMARK'][0]))) echo '

input day=' . $UTCday . '; output day=' . $return['day'] . '

'; } } $return['minute'] = $revised % 60; // Now less than 60, but may be negative if($return['minute'] < 0) $return['minute'] += 60; // Now between 0 and 59, and may be one or two digits as no leading zero if($minute < 10) $return['minute'] = '0' . 1 * $minute; // Now two digits with leading zero else $return['minute'] = $minute; if($show_diagnostics or (isset($show_diagnosticsR) and $show_diagnosticsR and isset($decodeGroupProcessed['REMARK'][0]))) echo '

ON EXIT from adjustTime(): output day=' . $return['day'] . ', output hour=' . $return['hour'] . ', output minutes=' . $return['minute'] . '

'; return $return; } #-------------------------------------------------------------------------------------------------------------------------------------# # The novel sub-function that follows is used by the other sub-functions anywhere a METAR segment is not recognised. # # Basically, this function is called when there are at least two segments still to be decoded, the first of those two has not # # been recognised, but the second is recognised as being part of the Group expected at a particular position. # # It moves the segment that cannot be recognised into a special unknown Group and records the whole raw METAR in a log file, # # before resuming the decoding with the expected Group, so ensuring the script does not get out of step, nor into an endless loop. # # Intellectual Property of SPAWS - December 2017 - novel code snippet with ability to cope with any unexpected content. # #-------------------------------------------------------------------------------------------------------------------------------------# function unRecognised($spec) { // Global Variables can be shared with calling script and with other functions or scripts; global $metar_toDecode, $decodeInfo, $decodeGroupCount, $decodeGroupProcessed, $show_diagnostics, $show_diagnosticsR, $stillEncoded_file; $decodeInfo['UNKNOWN']= $metar_toDecode[0]; // processing to deal with anything in main body (not Remark Group) of METAR that cannot be decoded $decodeGroupCount['UNKNOWN'] ++; $done = array_shift($metar_toDecode); // take out processed segment unable to match if($show_diagnostics or $show_diagnosticsR) { echo '
Unable to match segment "' . $done . '" into any preceding Group'; if(count($metar_toDecode) > 0 ) echo ', but next segment= ' . $metar_toDecode[0] . ', has been matched to $spec
' ; // conditional de-bugging section continues } //Log anything this script can't decode so keep a record of that METAR (for later consideration of whether error by METAR generator or error in this script) file_put_contents($stillEncoded_file, ($decodeInfo['METAR'] . "\r\n Unknown=" . $done . "\r\n" ), FILE_APPEND | LOCK_EX); } #################################################################################################################### # Function for prefix Groups, not part of the METAR standard, included in those downloaded from NOAA source. # # The prefix includes an ISO date on input, prior to entry, any line feed in original prefix has been removed. # #################################################################################################################### function decodeMETAR_NOAA() { // Global Variables can be shared with calling script and with other functions or scripts; global $metar_toDecode, $decodeInfo, $decodeGroupCount, $decodeGroupProcessed, $show_diagnostics, $monthArray; $iso_date = $metar_toDecode[0]; $metar_time = $metar_toDecode[1]; #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# # Parse incoming first segment of string for date in format 'yyyy/mm/dd' # # Parse incoming second segment of string for time in format 'hh:mm' # #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# // Example of string read from NOAA source after it has been through cleaning sub-function 'rev_clean_metar ()': // "2017/07/26 12:50 METAR EGWU 261250Z 21010KT 9999 -RA SCT013 BKN029 BKN080 20/17 Q1006 GRN TEMPO 7000 -RA GRN" if(strlen($iso_date) == 10 and checkdate( 1 * substr($iso_date,5,2), 1 * substr($iso_date,8,2), 1 * substr($iso_date,0,4))) { $decodeInfo['ISO_DATE'] = $iso_date; if($show_diagnostics) echo '
Matched into ISO date as found the content of ' . $iso_date; // conditional de-bugging section continues $partLocal = preg_match('/^[012][0-9][:][0-5][0-9]$/',$metar_time); if ($partLocal === 1) { if($show_diagnostics) echo '
Matched into UTC (=GMT) as found the content of ' . $metar_time; // conditional de-bugging section continues $minutes = substr($metar_time,3); $hour = substr($metar_time,0,2); $decodeInfo['ISSUED'] = trim(''); unset($partLocal, $minutes, $hour); return true; } } return false; } ################################################################################################################# # Function for prefix Groups, not part of the METAR standard, included in those downloaded from OGIMET source # # This references the HTML style METAR output from the OGIMET web site # ################################################################################################################# function decodeMETAR_OGIMET($type) { // Global Variables can be shared with calling script and with other functions or scripts; global $metar_toDecode, $decodeInfo, $decodeGroupCount, $decodeGroupProcessed, $show_diagnostics, $monthArray; if($type == 1) { // Example of OGIMET text format, this is different despite starting with year as in NOAA // 201801012350 METAR EGVP 012350Z AUTO 27013KT 9999 // NCD 06/03 Q1010 $iso_date = substr($metar_toDecode[0], 0, 8); if($show_diagnostics) echo '
Matched into iso date the content of ' . $iso_date; // conditional de-bugging section continues $decodeInfo['ISO_DATE'] = $iso_date; $metar_time = substr($metar_toDecode[0], 8, 2) . ':' . substr($metar_toDecode[0], 10, 2); if($show_diagnostics) echo '
Matched into UTC (=GMT) the content of ' . $metar_time; // conditional de-bugging section continues $minutes = substr($metar_time,3); $hour = substr($metar_time,0,2); $decodeInfo['ISSUED'] = trim(''); unset($partLocal, $minutes, $hour); return $type; }else{ // The obsolete SA prefix is still used by OGIMET database if asked to output in HTML format: // "SA 01/07/2017 09:20-> METAR EGTC 010920Z 33005KT 290V010 9999 FEW030 SCT041 17/13 Q1016=" $prefix = $metar_toDecode[0]; $cal_date = $metar_toDecode[1]; // Example of string read from the storage database at http://www.ogimet.com/metars.phtml.en after it has been through cleaning sub-function 'decodeMETAR_clean()': // "SA 01/07/2017 09:20-> METAR EGTC 010920Z 33005KT 290V010 9999 FEW030 SCT041 17/13 Q1016=" # Note that '->' normally appears with the time even after cleaning METAR, but don't want to include that in the 'time' that I try to decode... $metar_time = substr($metar_toDecode[2], 0, 5); # added substr here in final version 1.0.0 } #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# # Parse incoming first segment for "Surface Aviation Observation" ('SA') # # Parse incoming second segment of string for date in format 'dd/mm/yyy' # # Parse incoming third segment of string for time in format 'hh:mm' # #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# if($prefix == 'SA' and strlen($cal_date) == 10 and checkdate( 1 * substr($cal_date,3,2), 1 * substr($cal_date,0,2), 1 * substr($cal_date,6,4))) { $iso_date = substr($cal_date,6,4) . '-' . substr($cal_date,3,2) . '-' . substr($cal_date,0,2); $decodeInfo['ISO_DATE'] = $iso_date; if($show_diagnostics) echo '
Matched into calendar date the content of ' . $cal_date; // conditional de-bugging section continues $partLocal = preg_match('/^[012][0-9][:][0-5][0-9]$/',$metar_time); if ($partLocal === 1) { if($show_diagnostics) echo '
Matched into UTC (=GMT) the content of ' . $metar_time; // conditional de-bugging section continues $minutes = substr($metar_time,3); $hour = substr($metar_time,0,2); $decodeInfo['ISSUED'] = trim(''); unset($partLocal, $minutes, $hour); return $type; } } return false; } // ===================================================================================================================================================================================== ##################################################### # Function to decode WMO standard Groups in METAR # ##################################################### function decodeMETAR_standard() { // Global Variables can be shared with calling script and with other functions or scripts; global $metar_toDecode, $decodeInfo, $decodeGroupProcessed, $decodeGroupCount, $compass, $pressureHPA, $show_diagnostics, $lf, $dM, $dMsf, $monthArray, $cloud_type_array, $cloudAmountBandCode, $lastSuccess, $stillEncoded_file, $hoursOffset; $returnArray = array(); $returnArray['Standard'] = false; $lastSuccess = NULL; // ICAO standard says any automatic station must report all mandatory Groups, in every METAR that is generated unless it is using the 'NIL' qualification mentioned below. // WMO regulation 15.4 says all mandatory Groups must always appear, but the appropriate number of solidi can be reported in any mandatory Group if the automatic // sensor is available but not fully functional, and most countries like Netherlands do indeed use solidi for missing information. // USA instructions say that if the relevant sensors are faulty, the whole Group should be omitted. Thus this script has to cope if a mandatory Group is missing. #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# # Parse METAR for 'Type of Report Group' (mandatory single segment) # #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# // ICAO Annex 3 - Meteorological Services for International Air Navigation, Amendment 73 (WMO FM 15-XII Ext. METAR) applicable from 25 November 2004 // introduced requirement to include the type of Report Group, WMO had included it in previous WMO No. 306 - Manual on Codes - regulations paragraph 15.1.1 // and then removed it from that edition awaiting ICAO agreement WMO volume II page 15 says 'METAR' still not included for CZECH REPUBLIC. // UK CAA Requirements for meteorological observations at aerodromes - METAR Coding Rules table on page 26 // USA FAA standard - Federal Meteorological Handbook No.1, Surface Weather Observations and Reports - paragraph 12.4 a. (1) and 12.6.1 // Canada MANOBS Manual of Surface Weather Observations: 16.3.1 Type of report - The code name METAR ... shall be included at the beginning of an individual report. // Australia Bureau of Meteorology - Aviation Weather Services - Specification of weather product METAR ... optionalDiagnosticOutput('WMO Standard Mandatory Group: Message Type'); if($metar_toDecode[0] == 'METAR') { // Officially report starts with 'METAR', some sources omit this segment as they only display METAR $done = array_shift($metar_toDecode); // take out 1 processed segment $decodeGroupCount['TYPE'] = true; $decodeGroupProcessed['TYPE'] = 'METAR'; $returnArray['Standard'] = true; if($show_diagnostics) echo '

Matched into "Message Type Group" the content of ' . $done . '

'; // conditional de-bugging section continues } #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# # Parse METAR for conditional 'COR' Group (single segment) - in UK standard # # (and WMO but not older ICAO) COR appears before Aerodrome Identity Group # # [http://metaf2xml.sourceforge.net/metaf2xml/parser.html includes 'AMD'] # #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# /* ICAO Amendment 73 (WMO FM 15-XII Ext. METAR) applicable from 25 November 2004 introduced requirement to report Corrections WMO regulations paragraph 15.1 UK CAA METAR Coding Rules final paragraph on page 27 USA FAA standard paragraphs 12.4 a. (4) and 12.6.4 and http://www.met.tamu.edu/class/metar/metar-pg5.html Another specification is in New Zealand Aeronautical Information Publication (AIP) Part 1 - General (GEN), available at http://www.aip.net.nz/pdf/GEN_3.5.pdf (effective 23 June 2015); 3.8.1 If an error is detected in an issued METAR/SPECI, the METAR/SPECI will be re-sent immediately with the error corrected, and with the letters COR entered after the METAR/SPECI (i.e. METAR COR or SPECI COR). */ // optionalDiagnosticOutput('WMO Standard Conditional Group: COR, to see if it exists'); if($metar_toDecode[0] == 'COR' or $metar_toDecode[0] == 'AMD') // CORrected or AMendeD METAR { $decodeInfo['COR'] = 'This Report includes at least one Manually Corrected Observation'; $decodeGroupProcessed['COR'] = trim(array_shift($metar_toDecode)); // take out processed segment $decodeGroupCount['COR'] = true; $returnArray['Standard'] = true; } #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# # Parse METAR for 'Aerodrome Identity Group' (mandatory single segment) # #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# // WMO regulations paragraph 15.2 // UK CAA does not mention this Group specifically, so it follows WMO standard // USA FAA standard paragraph 12.6.2 optionalDiagnosticOutput('WMO Standard Mandatory Group: Aerodrome Identity'); $partLocal = preg_match('|^([A-Z]{4})$|',$metar_toDecode[0]); if($partLocal === 1) { if($show_diagnostics) { echo '
Software version for regular expressions =' . PCRE_VERSION; echo '

Matched into "aerodrome identity" the content of ' . $metar_toDecode[0] . '

'; // conditional de-bugging section continues } $decodeInfo['STATION'] = $decodeGroupProcessed['AIG'] = trim(array_shift($metar_toDecode)); // take out processed segment $decodeGroupCount['AIG'] = true; $returnArray['Standard'] = true; if(function_exists ('find_aerodrome')) { $decodeInfo['PLACE'] = find_aerodrome($decodeInfo['STATION'])['name']; } }else{ $decodeInfo['FEEDBACK'] = 'Input string rejected, aerodrome identity not found'; // FEEDBACK is global element that contains one message, not for actual decoded output $decodeGroupCount['FEEDBACK'] = false; goto function_exit; // 'goto' is allowed in PHP 5.3.0 and higher, and recommended to avoid multiple layers of conditionals and improve readability in PHP 7 } #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# # Parse METAR for 'Day+time Group' - date/time in ddhhmmZ or hhmmZ format # # dd=day of month, hh=hour (24 hour clock), mm=minutes, Z=Zulu identifier # # (mandatory single segment) # #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# // WMO regulations paragraph 15.3, confirms WMO latest standard consists of partial date (just day of month) then time with 'Z' at end (Zulu time is UTC) // WMO vol II page 15 says 'YYGGggZ' not included for CZECH REPUBLIC, page 19 says not used for LITHUANIA or SWEDEN. // Third parties suggest ICAO apparently (I can't find ICAO specification that confirms this) only has time mandatory // UK CAA does not mention this Group specifically, so it follows WMO standard // USA FAA standard paragraph 12.6.3 USA - http://www.met.tamu.edu/class/metar/metar-pg2.html and http://www.met.tamu.edu/class/metar/metar-pg5.html // Environment and Climate in Canada ManObs paragraph 16.3.3 optionalDiagnosticOutput('WMO standard Group: "Day+time Group"'); # Nested round brackets DOES NOT WORK $partLocal=preg_match('~^([0-3][0-9])?(("2"[0-4])|([01][0-9]))([0-5][0-9][Z]?)$~', $metar_toDecode[0],$pieces); $partLocal=preg_match('~^([0-3][0-9])?([012][0-9])([0-5][0-9])([Z]?)$~', $metar_toDecode[0],$pieces); if($partLocal === 1) { $decodeGroupProcessed['DAY+TIME'] = array_shift($metar_toDecode); // take out processed segment $currrentZtime = new DateTime("now", timezone_open("UTC")); $hoursOffset = date('O', time()) / 100; // Pass hour, minute, and day, parts (with any leading zeroes) of day-time segment into function to adjust to local time $day_of_month = "$pieces[1]"; $hourUTC = "$pieces[2]"; $minutesUTC = "$pieces[3]"; $return = adjustTime($hourUTC . ':' . $minutesUTC, $day_of_month); $localHour = $return['hour']; $localMinutes = $return['minute']; $localDay = $return['day']; if(isset($decodeInfo['ISO_DATE'])) // already know month and year from prefix { // Processing for when know the month and year if($show_diagnostics) echo ' Taking month and year information from the prefix
'; //------------------------------------------// // Warning if METAR is over 16 hours old // //------------------------------------------// $report_dateTime = new DateTime($decodeInfo['ISO_DATE'] . ' ' . $hourUTC . ':' . $minutesUTC . "+00:00"); $difference = $report_dateTime->diff($currrentZtime); $diffHours = $difference->format("%h"); // reports up to 23; excludes hours contained within number of days # echo '
TESTING ' . $diffHours; $diffDays = $difference->format("%a"); // valid number of days even if over 1 month; ("%d" would only output days not included in number of months) if($diffDays > 0) { // Processing for more than 23 hours ago $decodeInfo['LOCAL'] = 'local time ' . $localHour . ':' . $localMinutes . ' on ' . $localDay; if($diffDays == 1) $decodeInfo['LOCAL'] .= " (more than 1 day and $diffHours hours ago)"; else $decodeInfo['LOCAL'] .= " (more than $diffDays days and $diffHours hours ago)"; $report_dateTime = false; goto endDayTime; } if($diffHours > 16) { // Processing for between 16 and 24 hours ago $decodeInfo['LOCAL'] = 'more than 16 hours ago'; $report_dateTime = false; goto endDayTime; } // Processing for less than 17 hours ago //--------------------------------------------------------// // Now calculate equivalent to Issued in Local Time // // Time-zone needs to be defined in calling web-page // // e.g. date_default_timezone_set('America/Toronto'); // //--------------------------------------------------------// if($localDay == substr($decodeInfo['ISO_DATE'], 8, 2)) { // Day of month in day-time Group matches day of month in UTC time if($show_diagnostics) echo $decodeGroupProcessed['DAY+TIME'] . ' == issued on current day == '; $decodeInfo['LOCAL'] = 'local time ' . $localHour . ':' . $localMinutes . ' on ' . $localDay . ' ' . $monthArray[substr($decodeInfo['ISO_DATE'], 5, 2) - 1] . ' ' . substr($decodeInfo['ISO_DATE'], 0, 4); if($diffHours > 1) { $decodeInfo['LOCAL'] .= ' (' . $diffHours . ' hours ago)'; } goto endDayTime; }else{ // Day of month in day-time Group different to day of month in UTC time (as less than 17 hours ago, must be one day out) if($show_diagnostics) echo $decodeGroupProcessed['DAY+TIME'] . ' == issued on previous day == ' . $diffHours . ' hours ago'; $decodeInfo['LOCAL'] = 'local time ' . $localHour . ':' . $localMinutes . ' on ' . $localDay . ' ' . $monthArray[substr($decodeInfo['ISO_DATE'], 5, 2) - 1] . ' ' . substr($decodeInfo['ISO_DATE'], 0, 4); if($diffHours > 1) { $decodeInfo['LOCAL'] .= ' (yesterday, now ' . $diffHours . ' hours ago)'; } goto endDayTime; } } // Processing for when do not know the month and year (as they don't appear in day-time Group) because not supplied in a prefix if($show_diagnostics) echo "Don't know any month and year information because no prefix
"; // Make a guess that it is current month if day of month in day-time Group matches current UTC day of month (although it could be any multiple of months/years old) $iso_date = date("Y-m-d"); // current date for local time if(1 * $day_of_month == 1 * substr($iso_date,8,2)) // same day { # echo " SAME DAY "; $issuedDateTime = $iso_date . ' ' . $hourUTC . ':' . $minutesUTC . "+00:00"; }else // When do not know month and year because no prefix, can make a guess that it is current month if UTC day of month is just one day out from current UTC day (but it could be any multiple of months old) if(abs($pieces[1] - substr($iso_date,-2)) == 1) // same month { # echo " SAME MONTH "; $issuedDateTime = substr($iso_date, 0, 8) . "$day_of_month" . ' ' . $hourUTC . ':' . $minutesUTC . "+00:00"; }else // When do not know month and year because no prefix, can make a guess that it is previous month if UTC day of month is different to current UTC day (but it could be any multiple of months old) { # echo " DIFFERENT MONTH "; $iso_part_date = date("Y-m-", strtotime('last month')); $issuedDateTime = $iso_part_date . "$day_of_month" . ' ' . $hourUTC . ':' . $minutesUTC . "+00:00"; } $report_dateTime = new DateTime($issuedDateTime, timezone_open("UTC")); //------------------------------------------// // Warning if METAR is over 16 hours old // //------------------------------------------// $difference = $report_dateTime->diff($currrentZtime); $diffHours = $difference->format("%h"); // valid up to 23; excludes hours contained within number of days $diffDays = $difference->format("%a"); // valid number of days even if over 1 month; ("%d" would only output days not included in number of months) if($diffHours > 16) { $decodeInfo['LOCAL'] = 'more than 16 hours ago,'; $report_dateTime = false; } if($diffDays > 0) { $decodeInfo['LOCAL'] = "more than $diffDays day(s) and $diffHours hours ago,"; $report_dateTime = false; } //-----------------------------------------------------// // Report Issued Time if not already Calculated // //-----------------------------------------------------// if(!isset($decodeInfo['ISSUED'])) { $decodeInfo['ISSUED'] = trim(''); } //--------------------------------------------------------// // Now calculate equivalent to Issued in Local Time // // Time-zone needs to be defined in calling web-page // // e.g. date_default_timezone_set('America/Toronto'); // //--------------------------------------------------------// if($report_dateTime !== false) { if($hoursOffset != 0) date_modify($report_dateTime, $hoursOffset . ' hour'); $decodeInfo['LOCAL'] = 'local time ' . date_format($report_dateTime, 'G\:i \o\n l j F Y'); } endDayTime: $decodeGroupCount['DAY+TIME'] = true; if($show_diagnostics) echo '

Matched into Time (date/time) Group the content of ' . $metar_toDecode[0] . '

'; // conditional de-bugging section continues } #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# # Parse METAR for conditional 'Identification of an automated or missing METAR # # report Group' (single segment) - Note WMO and ICAO disagree re 'COR' position. # # In ICAO standard all appear here after standard METAR issue day_time-stamp Group # #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# // ICAO Amendment 73 (WMO FM 15-XII Ext. METAR) applicable from 25 November 2004 introduced requirement to report NIL after standard METAR issue day_time-stamp Group // WMO regulations treat 'NIL' as one Group, then 'AUTO' as next Group, while 'COR' has been an earlier Group - this script will test for all here // USA FAA Federal Meteorological Handbook No.1 CHAPTER 12 CODING: paragraph 12.6.4 Report Modifier (AUTO or COR). The report modifier, AUTO, identifies the METAR/SPECI as // a fully automated report with no human intervention or oversight. In the event of a corrected METAR or SPECI, the report modifier, COR, shall be substituted in place of AUTO. // AFMAN15-111 27 FEBRUARY 2013 Chapter 13 REPORTING AND ENCODING OF WEATHER OBSERVATIONS: 13.4.4. Report Modifier (AUTO or COR). // Canada ManObs paragraph 16.3.3 AUTO: Automatic Station Indicator. Indicates the report is from an auto station. BBB: Correction indicator. // Formed by the letters CC preceding an incremented letter to indicate the corrected observation. Use CCA for first correction, CCB for second correction, and so on. /* Since November 2007 ICAO Annex 3 Chapter 4, paragraph 4.7.1 (Amendment 74) has allowed for METAR and SPECI to be provided from automatic observing systems during both non-operational and operational hours of an airport as determined by the Meteorological Authority in consultation with users based on the availability and efficient use of personnel. */ optionalDiagnosticOutput('WMO standard Group: "Report Modifier Group"'); $decodeGroupCount['BY'] = 0; switch($metar_toDecode[0]) { case 'NI': // 2017/12/08 11:00 METAR SVBC 081100Z NI case 'NIL': // ICAO code for missing METAR issue - if this code appears, that is end of METAR message and no further groups can be included # example: (SA 01/07/2017 12:20->) "METAR EGTC 011220Z NIL=" $decodeInfo['FEEDBACK'] = 'NIL METAR report generated at communications centre because expected METAR is missing from batch received'; if($show_diagnostics) echo '
Matched into "AUTO etc. Group" the content of ' . $metar_toDecode[0]; // conditional de-bugging section continues goto function_exit; case 'RTD': // http://metaf2xml.sourceforge.net/metaf2xml/parser.html $decodeInfo['FEEDBACK'] = 'METAR report delayed'; // If a communications centre receives a METAR after the allowed period of minutes by which it is allowed to be late, then delayed METAR can be sent in next bulletin batch // Not clear if communications centre, identifies those METAR that were delayed in any way, but if delay is marked, could be by using this, in which case rest of METAR follows // However, as not found explanation for 'Retarded', nor whether it is used by anyone, this script assumes it means delayed, so nothing more can follow if($show_diagnostics) echo '
Matched into "AUTO etc. Group" the content of ' . $metar_toDecode[0]; // conditional de-bugging section continues goto function_exit; case 'CCA': case 'CCB': case 'CCC': // Canada uses this sequence for manual corrections, apparently expecting multiple corrections, theoretically up to CCZ! // I've coded to look for "CCA", "CCB", or "CCC" as those are all that are mentioned in MANOBS Chapter 16 at https://ec.gc.ca/manobs/default.asp?lang=En&n=3a380ded-1 // Anyway this script treats 'BBB' as those possible values are known collectively, just like 'COR', and simply modifies output text. case 'COR': $decodeInfo['BY'] = 'Observer'; $decodeGroupProcessed['BY'] = $metar_toDecode[0]; $decodeInfo['COR'] = substr($metar_toDecode[0],1,1) == 'O' ? 'Corrected observation' : 'Correction ' . substr($metar_toDecode[0],2); if($show_diagnostics) echo '
Matched into "AUTO etc. Group" the content of ' . $metar_toDecode[0]; // conditional de-bugging section continues $decodeGroupProcessed['AUTO'] = trim(array_shift($metar_toDecode)); // take out processed segment break; case 'AUTO': // ICAO Annex 3 paragraph 4.7.2 specifies use of 'AUTO' // UK CAA METAR Coding Rules paragraph 4.158 // USA FAA standard paragraphs 12.4 a. (4) and 12.6.4 and http://www.met.tamu.edu/class/metar/metar-pg5.html $decodeInfo['BY'] = 'Automated observation'; if($show_diagnostics) echo '
Matched into "AUTO etc. Group" the content of ' . $metar_toDecode[0]; // conditional de-bugging section continues $decodeGroupProcessed['BY'] = trim(array_shift($metar_toDecode)); // take out processed segment $decodeGroupCount['BY'] = 1; break; default: $decodeInfo['BY'] = "Observer"; // no segment, so don't move $decodeGroupProcessed['BY'] = ''; } // Initialise number where unable to achieve decoding successfully $decodeGroupCount['UNKNOWN'] = 0; if(!is_dir("Logs")) mkdir('Logs'); $stillEncoded_file = "Logs/stillEncoded_metar.txt"; clearstatcache(); // Start parsing for reported observations if($show_diagnostics) echo 'Start of decoding by ' . $dMsf ."
\n"; re_entryWind: // positioned here because an unknown Group once appeared in this position // (now believe it was a misformed surface wind group with required 'K' missing - 2017/07/19 06:20 METAR EGTC 190620Z 08010T 7000 FEW005 18/17 Q1006 // Unknown=08010T) #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# # Parse METAR for mandatory Surface Wind Group (bearing, speed, gust) (single segment) # #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# // ICAO_Annex 3 (16th edition July 2007) paragraph 4.6.1 - report in degrees and km per hour (or knots) // WMO regulations paragraph 15.5 and sub-paragraphs - report in metres per second (see sub-function comments for how standard has been revised re use of other units) // UK CAA METAR Coding Rules paragraphs for "Surface Wind" 4.5 to 4.21 say use knots // USA FAA standard chapter 5 plus paragraphs 12.4 a. (5) and 12.6.5 and http://www.met.tamu.edu/class/metar/metar-pg6.html says use knots // Australian BOM also specify using knots. WMO vol II page 12 says Mexico use knots for wind speeds. // Environment and Climate in Canada ManObs paragraph 16.3.4 - In this Group, Canada will use solidii '/' for missing readings, other Groups are omitted if sensor failure optionalDiagnosticOutput('WMO standard Group: "main Surface Wind Group"'); if(substr($metar_toDecode[0],0,2) == '//') { // automatic station can return 'sensor failure' $decodeInfo['DIRECTION'] = 'VRB'; // Treat unknown direction as variable direction $decodeInfo['SPEED'] = 'Sensor failure'; // put text in for wind speed as output by SPAWS scripts if($show_diagnostics) echo ' and found match including message of ' . $decodeInfo['SPEED'] . '
'; $decodeGroupCount['WIND'] = true; $decodeGroupProcessed['WIND'] = trim(array_shift($metar_toDecode)); // take out processed segment $lastSuccess = 'Wind'; if($show_diagnostics) echo '

Matched into "main Surface Wind Group" the content of ' . $metar_toDecode[0] . '

'; // conditional de-bugging section continues }else{ $input = novel_decode_wind($metar_toDecode[0], 'swg'); # --------------------------- # ['wind'], ['gust'] - complex string 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'] - main reporting unit # ['mph'] - for external scripts # ['mps'] - alternative units (metres per second) # ['house'] - beaufort force image # ['kph'] - alternative units (km per hour) # --------------------------- if($input['matched']) { $decodeGroupCount['WIND'] = 1; $decodeInfo['SPEED'] = $input['speed']; if($input['speed'] == 'calm') { $decodeInfo['KNOTS'] = 0; $decodeInfo['WIND'] = 'calm'; // Text recognised by Ken True's scripts $decodeInfo['DIRECTION'] = 'calm'; // Text recognised by Ken True's scripts $decodeInfo['WINDMPS'] = 0; $decodeInfo['WINDMPH'] = 0; $decodeInfo['WINDKPH'] = 0; }else{ $decodeInfo['DIRECTION'] = $input['direction']; if(isset($input['bearing'])){ $decodeInfo['BEARING'] = $input['bearing']; } $decodeInfo['KNOTS'] = $input['knots']; if(isset($input['mps'])){ $decodeInfo['WINDMPS'] = $input['mps']; $decodeInfo['WINDMPH'] = $input['mph']; $decodeInfo['WINDKPH'] = $input['kph']; } if($input['gust'] != '') { // gust is reported if its speed exceeds average wind speed by 10 knots or more $decodeInfo['GUST'] = $input['knotsGust'] . ' knots, ' . $input['mphGust'] . ' mph, ' . $input['mpsGust'] . ' m s-1'; } } if(isset($input['house'])) $decodeInfo['BEAUFORT'] = $input['house']; // Used by SPAWS scripts $decodeGroupCount['WIND'] = true; $decodeGroupProcessed['WIND'] = trim(array_shift($metar_toDecode)); // take out processed segment if($show_diagnostics) echo ' and found match including wind speed of ' . $decodeInfo['SPEED'] . '
'; $lastSuccess = 'Wind'; if($show_diagnostics) echo '

Matched into "main Surface Wind Group" the content of ' . $decodeGroupProcessed['WIND'] . '

'; // conditional de-bugging section continues } } #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# # Parse METAR for conditional Wind bearing extremes Group (single segment) # #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# // WMO has this as separate Group to previous as it is an optional extension, see regulations paragraph 15.5.3. // Note generally this variability is specified only if wind speed is 3 knots or more // UK CAA METAR Coding Rules combines this Group with previous one (see paragraphs previously referenced) and makes no mention of any space in the format description. # However, all METAR encountered have included a space before the codes defined by the regular expression below, so this script treats it (like WMO) as a separate Group, # and the CAA standard does include the space in its examples numbered 5 and 6. // USA FAA METAR standard combines this Group with previous one but shows the space (using the underline symbol to indicate space). // For USA minimum wind speed must be more than 6 knots. // Australian BOM report this group if variation in direction exceeds 60 degrees. // Environment and Climate in Canada ManObs paragraph 16.3.5 treats it as separate group. $partLocal = preg_match('/([0-9]{3})V([0-9]{3})/',$metar_toDecode[0],$pieces); if ($partLocal === 1) { optionalDiagnosticOutput('WMO standard Group: Wind Group (variable bearing part)'); $decodeInfo['VARIABLE'] = 'Wind direction reported as variable: bearings ' . $pieces[1] . '° to ' . $pieces[2] . '°'; $decodeInfo['VARIABLE_FROM'] = $pieces[1]; $decodeInfo['VARIABLE_TO'] = $pieces[2]; $decodeInfo['FROM'] = $compass[1 + round($pieces[1] / 22.5) % 16]; $decodeInfo['TO'] = $compass[1 + round($pieces[2] / 22.5) % 16]; $decodeGroupCount['VARIABLE'] = true; $decodeGroupProcessed['VARIABLE'] = trim(array_shift($metar_toDecode)); // take out processed segment if($show_diagnostics) echo '

Matched into "Wind Group (variable bearing part)" the content of ' . $decodeInfo['VARIABLE'] . '

'; // conditional de-bugging section continues } if($lastSuccess != 'Wind' and count($metar_toDecode) > 1) { $input = novel_decode_wind($metar_toDecode[1], 'swg'); if($input['matched']) { unRecognised('Surface Wind Group'); // handle unrecognised segment goto re_entryWind; // process Surface Wind Group } } #------------------------------ # Are there any more Groups? if(count($metar_toDecode) < 1) goto function_exit; #------------------------------ optionalDiagnosticOutput('WMO standard Group: Visibility, Weather, and Cloud Cover details'); re_entryVisib: ############################################################# # Visibility, Weather, and Cloud Cover details appear # # either in (conditional) 'CAVOK' Group # # or divided between the following Groups: # # - Prevailing Visibility Group # # - (conditional) Minimum directional visibility Group # # - (conditional) Runway Visual Range Group # # - (conditional) Present weather phenomena Group # # - Cloud cover/sky condition Group # ############################################################# if($metar_toDecode[0] == 'CAVOK' or count($metar_toDecode) > 4 && $metar_toDecode[0] == 'C' && $metar_toDecode[1] == 'A' && $metar_toDecode[2] == 'V' && $metar_toDecode[3] == 'O' && $metar_toDecode[4] == 'K') { #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# # Parse METAR for (conditional) 'CAVOK' Group (single segment) # #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# optionalDiagnosticOutput('WMO standard Group: CAVOK'); // WMO regulations paragraph 15.10, states that if 'CAVOK' Group appears, the Groups specified by regulations 15.6 (Groups for prevailing and for directional visibility), // 15.8 (Group for Present Weather), and 15.9 (Group for Cloud amount and cloud height) are replaced by this 'CAVOK'. // CAVOK is mandated by the ICAO standard, but it is not used in USA, Canada, or Mexico. // UK CAA METAR Coding Rules paragraphs 4.22 to 4.24, describe CAVOK. If it is included, it says there that it replaces the Visibility, RVR, present weather, and cloud Groups. // Paragraph 4.31 in CAA document is confusing, it defines case for including CAVOK in terms of prevailing or minimum wind, although I can see that wind may affect ability for fog to form, // the text still confuses me (maybe it means visibility), and I can't understand what might be included as well as CAVOK. $decodeInfo['CAVOK'] = 'Cloud and Visibility OK, with no weather phenomena significant to aviators on the aerodrome or in its vicinity (within 8 km)'; if($metar_toDecode[0] == 'CAVOK') $decodeGroupProcessed['CAVOK'] = array_shift($metar_toDecode); // take out processed segment else { $splice = array_splice($metar_toDecode, 0, 5); // return processed segments, removing from array $decodeGroupProcessed['CAVOK'] = $splice[0] . '_' . $splice[1] . '_' . $splice[2] . '_' .$splice[3] . '_' . $splice[4]; } if($show_diagnostics) echo '

Matched into "Visibility", "RVR", "Present weather", and "Cloud" Groups the content of ' . $decodeGroupProcessed['CAVOK'] . '

'; // conditional de-bugging section continues // This script gives the above short diagnostic report and also populates the full individual output reports it replaces as defined below $decodeInfo['VISIBILITY'] = 'Good visibility, at least 10 km'; $decodeGroupProcessed['VISIBILITY'] = 'CAVOK'; $decodeGroupCount['VISIBILITY'] = -1; // Setting count to non-zero figure to show the related array element $decodeInfo['VISIBILITY'] has contents set $decodeInfo['CONDITIONS'] = 'No significant weather phenomena on the aerodrome or in its vicinity'; $decodeGroupProcessed['CONDITIONS'] = 'CAVOK'; $decodeGroupCount['CONDITIONS'] = -1; // Setting count to non-zero figure to show the related array element $decodeInfo['CONDITIONS'] has contents set $decodeInfo['CLOUDS'] = 'No significant clouds'; // required to be defined starting with 'No' for summary icon to work /* WMO Note: Highest minimum sector altitude is defined in The International Civil Aviation Organization's Document 8168 "Procedures for Air Navigation Services – aircraft OPerationS" ICAO PANS-OPS volume 1 'Flight Procedures' (5th edition 2006). In PART I. FLIGHT PROCEDURES — GENERAL; Section 1. Definitions, abbreviations and acronyms; Chapter 1. Definitions: "Minimum sector altitude. The lowest altitude which may be used which will provide a minimum clearance of 300 m (1 000 ft) above all objects located in an area contained within a sector of a circle of 46 km (25 NM) radius centred on a radio aid to navigation." */ $decodeInfo['SKY'] = 'No cloud below 5,000 feet, or below the MSA (minimum sector altitude), (whichever is higher), and no evidence of thunderstorms'; $decodeGroupProcessed['SKY'][0] = 'CAVOK'; // NB - must be in a sub-array within 'SKY' $decodeGroupCount['SKY'] = -1; // Setting count to non-zero figure to show the related array element $decodeInfo['SKY'] has contents set $lastSuccess = 'Visib'; goto re_entryTD; // since got match to 'CAVOK', skip past other cloud and visibility Groups, next comes temperature and dew-point Group } optionalDiagnosticOutput('WMO standard Group: Visibility details'); #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# # Parse METAR for Prevailing Visibility Group (WMO says just one segment and that only allowed format is "VVVV"). # # Prevailing Visibility is defined as that applicable to at least half of the whole horizon. # # Conditional - WMO rules state that this segment does not appear when CAVOK Group used. # # WMO regulations paragraph 15.4 and 15.6: mandatory units are metres using exactly 4 digits including leading zeros.# # ICAO_Annex3 chapter 1 and paragraph 4.6.2.1 states prevailing visibility is reported in metres or km. # # WMO says to be used to report the lowest directional visibility if the prevailing visibility cannot be determined. # #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# # As of June 2011 WMO standard changed, WMO have withdrawn the previous 'VVVNDV' format that meant 'visibility # # sensors were being used that could not give directional variation'. In practice, this obsolete format is still # # being used by New Zealand, Switzerland etc., and that is accepted in the sub-function "novel_allVisibility called" # # from here. WMO do still have another Group for Minimum directional visibility, also processed by same call. # #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# # WMO permit their Region IV, that comprises Canada, the United States and the Central American countries to use # # statute or nautical miles for visibility, this requires two segments as there can be whole and fractional parts # # figure (e.g. '1 3/4'), and the final Group segment includes the characters "SM". However, where this Region has # # visibilities in the Remarks Group, they conform to the same whole and fractional rules, but don't include 'SM'. # # WMO volume II page 13 USA Military bases may use metres instead of SM, and page 10 says Canada does not use 'NDV'. # #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# # Non-standard Groups still start with a numerical digit (hence condition below), but although there is nothing in # # the WMO rules, some METAR omit leading zeroes or end with "FT" or "KM" to indicate using feet or kilometres. # #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# if($metar_toDecode[0] == '////' or is_numeric(substr($metar_toDecode[0], 0, 1))) { optionalDiagnosticOutput('WMO standard Group: "Prevailing Visibility Group" (includes "Minimum Directional Visibility Group" checking)'); if(count($metar_toDecode) > 2 ) $input = novel_allVisibility($metar_toDecode[0], $metar_toDecode[1], $metar_toDecode[2], 'WMO standard Group'); else $input = novel_allVisibility($metar_toDecode[0], $metar_toDecode[1], '', 'WMO standard Group'); if(isset($input['pV_match']) and $input['pV_match']) { // 2018/02/15 22:00 METAR CYZR 152200Z AUTO 19010KT 1/8SM R33/1200V1800FT/ -RA FG BKN002 BKN095 OVC140 06/06 A2961 RMK SLP032 $decodeGroupCount['VISIBILITY'] = 1; // Prevailing visibility means furthest distance seen that applies for at least half of the 360 degree horizon (not necessarily a continuous half) $decodeInfo['VISIBILITY'] = $input['prevailVisibility']; if($input['segments'] > 0)$decodeGroupProcessed['VISIBILITY'] = trim(array_shift($metar_toDecode)); // take out processed segment if($input['segments'] == 2 and !isset($input['dV_match']) and !isset($input['vV_match'])) $decodeGroupProcessed['VISIBILITY'] .= '_' . trim(array_shift($metar_toDecode)); // take out 2nd processed segment $lastSuccess = 'Visib'; if($show_diagnostics) echo '

Matched into "Prevailing Visibility Group" the content of ' . $decodeGroupProcessed['VISIBILITY'] . '

'; // conditional de-bugging section continues } #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# # Parse METAR for (conditional) Minimum directional visibility Group (single segment). # # Already handled as part of decoding in sub-function called by previous Group. # #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# /* WMO regulations paragraph 15.6.2 - additional 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 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 */ if(isset($input['dV_match']) and $input['dV_match']) { $decodeInfo['MINIMUM'] = $input['mdv']; $decodeGroupProcessed['MINIMUM'] = trim(array_shift($metar_toDecode)); // take out processed segment if($show_diagnostics) echo '

Matched into "Minimum Directional Visibility Group" the content of ' . $metar_toDecode[0] . '

'; // conditional de-bugging section continues } unset($input); // clear memory of array returned by sub-function } #------------------------------ # Are there any more Groups? if(count($metar_toDecode) < 1) goto function_exit; #------------------------------ optionalDiagnosticOutput('WMO standard Group: Visibility - any more details?'); #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# # Parse METAR for (conditional) Runway Visual Range Group (up to 4 segments) # #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# /* REGIONAL STANDARDS: WMO vol II page 16 says The Netherlands use 'R////////' if they cannot report RVR, or they use 'Rnn//////' if for a specific runway they cannot report RVR. At the start of chapter 4 the UK CAA guidance says RVR is not reported in UK, but it then in paragraphs 4.38 to 4.50 describes its use as if it should be used in UK! Furthermore in paragraph 4.155 the UK CAA rules confirm that automatic METARs should include RVR on operational aerodromes. Despite this confusion, this script has decoded this Group right from an early version as it was used in some of the UK METAR seen during the script development period. Runway visual range coding requirements for UK are described in paragraph 4.38 General format: RDRDR/VRVRVRVR, where R is the group identifier. DRDR is the runway designator of the threshold nearest to which the RVR is measured, followed (if necessary) by L, C or R to distinguish left, centre and right parallel runways respectively. VRVRVRVR is the 4 digit RVR value reported in metres. In other countries, the format is far more complicated, and this script can handle all the possible formats. For example, New Zealand adds ability to identify where along the length of the runway the meaurement applies. Other countries express a variable range, using a "V" between the two figures (before is maximum, after is minimum) */ // ICAO_Annex 3 chapter 1 and paragraph 4.6.3.1 say RVR reported where approach and landing is operated according to Category II or III, // or for precision approach using category 1 // ICAO Annex 5 specifies unit for reporting to be metres, they apparently say that any visibility less than 1,500 metres must be reported in metres, // that presumably means RVR is optional for higher visibilities, if so that is irrelevant to this decoder // (but it could be alternatively read to mean that higher visibilities are given to nearest kilometre - see Prevailing Visibility notes re WMO regulations paragraph 15.4 and 15.6) /* FAA Advisory Circular 3-8 14 Nov 2016 Aviation Weather Services (AC 00-45H) paragraph 3.1.5.7 Runway Visual Range (RVR) Group: (this is where I found reference to maximum 1 in USA, maximum 4 elsewhere): For U.S. airports only, the touchdown zones (TDZ) RVR is reported. For U.S. airports with multiple runways, the operating runway with the lowest 3-8 11/14/16 Aviation Weather Services AC 00-45H touchdown RVR is reported. RVR may be reported for up to four designated runways in other countries. When the RVR varies by more than one reportable value, the lowest and highest values will be shown with V between them, indicating variable conditions. For example, the 10-minute RVR for Runway 01L varying between 600 ft and 1,000 ft would be coded R01L/0600V1000FT. If RVR is less than its lowest reportable value, the visual range group is preceded by M. For example, an RVR for Runway 01L of less than 600 ft is coded R01L/M0600FT. If RVR is greater than its highest reportable value, the visual range group is preceded by a P. For example, an RVR for Runway 27 of greater than 6,000 ft will be coded R27/P6000FT. */ // USA FAA standard chapter 7 plus paragraphs 12.4 a. (7) and 12.6.7 and http://www.met.tamu.edu/class/metar/metar-pg8-RVR.html // Environment and Climate in Canada ManObs paragraph 16.3.7 // WMO in volume II page 10-11 says that both Canada and USA add 'FT' after figure as they report in feet // (confusing as Canada use hundreds of feet for 'Variable Visibility') if(substr($metar_toDecode[0],0,1) != 'R' or !is_numeric(substr($metar_toDecode[0],1,1))) goto re_entryPresent; else{ // NB other Groups start with 'R', but in this position R means RVR Group optionalDiagnosticOutput('WMO standard Group: Runway Visual Range Group'); $decodeGroupCount['RVR'] = 0; while (substr($metar_toDecode[0],0,1) == 'R' and $decodeGroupCount['RVR'] < 5) // MANOBS for Canada states maximum number of RVR reports is 4. { if(!isset($decodeInfo['RVR']))$decodeInfo['RVR'] = ''; else $decodeInfo['RVR'] .= '
'; $partLocal = $metar_toDecode[0]; // 2017/12/13 23:00 METAR CYXU 132300Z 08010KT 1SM R15/P6000FT/D -SHSN OVC020 M09/M11 A2952 RMK SN4SC4 VIS VRB 3/4-11/2 CVCTV CLD EMBD SLP015 // 2017/12/12 21:00 METAR CYXU 122100Z 28017G24KT 3/4SM R15/4500VP6000FT/N -SHSN BLSN BKN012TCU M10/M13 A2971 RMK SN3TCU4 SLP077 $found = preg_match('~^[R]([0-9][0-9])([LR]?[LRC])?([T][D][Z])?([M][I][D])?([E][N][D])?[\/]([PM])?([0-9]{4})([V][PM])?([0-9\/]{4})?(FT)?([\/][UDN])?~', $partLocal, $pieces); // New Zealand special: RVR is reported for the touchdown zone(TDZ) of the runway(s)in use. if($show_diagnostics) { echo '
Splitting ' . $partLocal . '
'; print_array($pieces, 'WMO standard Group: Runway Visual Range Group - pieces'); /* 0 = whole; 1 = runway code; 2 = Left, Right, Centre; 3 = touchdown zone; 4 = middle; 5 = end; 6 = more than or less than; 7 = visibility range; 8 = Variable indicator plus more than or less than; 9 = minimum range; 10 = units; 11 = trend. */ } if($found === 1) { // The runway designator is reported followed by the RVR, e.g. R05/1400. switch($pieces[1]) { case 88: $decodeInfo['RVR'] .= 'All Runways: '; break; case 99: $decodeInfo['RVR'] .= 'No change from last report '; break; default: $decodeInfo['RVR'] .= 'Runway threshold number ' . $pieces[1] . ': '; if(array_key_exists(2, $pieces)) { switch($pieces[2]){ # For parallel runways, 5 codes available to indicate runway that visibility applies to case 'LL': $decodeInfo['RVR'] .= ' Extreme Left Runway:
'; break; case 'L': $decodeInfo['RVR'] .= ' Left Runway:
'; break; case 'C': $decodeInfo['RVR'] .= ' Centre Runway:
'; break; case 'R': $decodeInfo['RVR'] .= ' Right Runway:
'; break; case 'RR': $decodeInfo['RVR'] .= ' Extreme Right Runway:
'; break; default: $decodeInfo['RVR'] .= ':
'; break; } } } /* One specification is in New Zealand Aeronautical Information Publication (AIP) Part 1 - General (GEN), available at http://www.aip.net.nz/pdf/GEN_3.5.pdf (effective 23 June 2015); 3.12.1 Instrumented Runway Visual Range (IRVR) equipment has been installed at Auckland International airport on RWY 05R/23L and Christchurch International Airport on RWY 02/20. The equipment consists of three transmissometers located adjacent to the Touchdown zone (TDZ), the runway Mid-point (MID) and the runway Stop-end (END). This enables ATC to issue runway visual range (RVR) visibility values based on the use of this equipment. Another specification is in Canada's M A N O B S – Manual of Surface Weather Observations 10—165 Seventh Edition, Amendment 19, April 2015: 10.2.19.13 Runway Visual Range (RVR) Where RVR data is displayed, it shall be included in hourly and SPECI observations. RVR shall be reported for the active or most-aligned into-the-wind runway(s) when the prevailing visibility is 1 SM or less and/or the RVR value for the designated runway(s) is 6,000 feet or less. Stations with the capability to display values for multiple RVRs may record and transmit a maximum of four RVR values and may include RVR data for runway(s) other than the active or most-aligned into-the-wind. All RVR values transmitted shall be representative of the touchdown zone of the active landing runway(s). 10.2.19.13.1 RVR is recorded and transmitted using the following format: RD D /V V V V i R R R R R R or RD D /V V V V VV V V V i R R R R R R R R R R 10.2.19.13.2 Group RD D /V V V V i R R R R R R R: Indicator. D D :The designator of each runway for which runway visual range is reported. Parallel R R runways should be distinguished by appending to D D letters L, C or R indicating the left, R R central or right parallel runway, respectively. A suitable combination of these letters is used for up to, and including, five parallel runways (i.e. LL, L, C, R, RR). The letter(s) shall be appended to D D as necessary in accordance with the standard practice for runway R R designation. V V V V : Mean value of runway visual range over the 10-minute period immediately R R R R preceding the observation. However, when the 10-minute period includes a marked discontinuity in the RVR (for example, sudden advection of fog, rapid onset or cessation of an obscuring snow shower), only the data after the discontinuity shall be used for obtaining mean RVR values and variations thereof, hence the time interval in these circumstances shall be correspondingly reduced. FT shall be appended to the measurement to indicate that the measurement is in feet. i: If the runway visual range values during the 10-minute period preceding the observation shows a distinct upward or downward tendency such that the mean during the first five minutes varies by 300 feet or more from the mean during the second five minutes of the period, this shall be indicated by i = U for upward and i = D for downward tendency of runway visual range values. When no distinct change in runway visual range is observed, i = N shall be used. When it is not possible to determine the tendency, i shall be omitted. 10.2.19.13.3 RD D /V V V V VV V V V i – significant variation of runway range R R R R R R R R R R When the RVR at a runway varies significantly and when during the 10-minute period preceding the nominal observation time, the one-minute mean extreme values assessed vary from the mean value by more than 150 feet or more than 20% of the mean value, whichever is greater, the one-minute mean minimum and the one-minute mean maximum values shall be given in that order in the form RD D /V V V V VV V V V i instead of R R R R R R R R R R the 10-minute mean. The tendency shall also be included. 10.2.19.13.4 When actual RVR values are outside the measuring range of the observing system in use, the following procedure shall apply: 1) When the RVR is greater than the maximum value which can be assessed with the system in use, P shall be appended to the group V V V V : e.g. P6000. 2) When the RVR is below the minimum value which can assessed with the system in use, M shall be appended to the group V V V V : e.g. M0600. 10.2.19.13.5 Sites that are using RVR data based on a 10-minute mean shall enter the RVR data in the specified field of the input screen and record in Column 41. 10.2.19.13.6 Sites that are using the Remarks section to transmit RVR shall use only one value of RVR tendency and variations by this method shall not be reported in Remarks. */ if($pieces[3] == 'TDZ') $decodeInfo['RVR'] .= ' touchdown zone: '; if($pieces[4] == 'MID') $decodeInfo['RVR'] .= ' mid-point: '; if($pieces[5] == 'END') $decodeInfo['RVR'] .= ' stop end: '; $decodeInfo['RVR'] .= '   '; if(array_key_exists(8, $pieces) and strlen($pieces[8]) > 0) $decodeInfo['RVR'] .= '

'; } // end preg_match RVR_continue: $decodeGroupCount['RVR'] ++; if($decodeGroupCount['RVR'] > 1) $decodeGroupProcessed['RVR'] .= ' + '; if(!isset($decodeGroupProcessed['RVR'])) $decodeGroupProcessed['RVR'] = ''; $removed = trim(array_shift($metar_toDecode)); // take out processed segment if($show_diagnostics) echo '

Matched into "Runway Visibility Range Group" the content of ' . $removed . ' giving output of ' . $decodeInfo['RVR'] . '

'; // conditional de-bugging section continues $decodeGroupProcessed['RVR'] .= $removed; } // end while unset($partLocal,$pieces); #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# } # End parse METAR for Runway Visual Range Group (up to 4 segments) # #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# #------------------------------ # Are there any more Groups? if(count($metar_toDecode) < 1) goto function_exit; #------------------------------ optionalDiagnosticOutput('WMO standard Group: Weather details'); #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# # Parse METAR for (conditional) Present weather phenomena Group # # (may appear in up to 3 segments) # #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# re_entryPresent: // ICAO annex 3 paragraph 4.6.4.1 specifies minimum required; 4.6.4.2-3 add recommendations re on aerodrome and in vicinity respectively // WMO regulations paragraph 15.4, 15.8 and quite complex as described in sub-paragraphs 15.8.1 to 15.8.20 - the last instructs it is not used if 'CAVOK' used. // Described in UK CAA METAR Coding Rules in paragraphs 4.51 to 4.98, so quite complex. // This script snippet calls a sub-function 'novel_decode_phenomena' that is also used for recent weather and trends // For automatic reporting also see UK CAA METAR Coding Rules in paragraphs 4.165 to 4.168 (some of this also applies to recent weather) // USA FAA standard chapter 8 plus paragraphs 12.4 a. (8) and 12.6.8 and http://www.met.tamu.edu/class/metar/metar-pg9-ww.html // Environment and Climate in Canada ManObs paragraph 16.3.8 # METAR example: mist and fog but forecast changing to just fog: NZCH 011200Z AUTO 02003KT 9999 MIFG NCD 05/05 Q0992 BECMG 0400 FG optionalDiagnosticOutput('WMO standard Group: Present Weather Phenomena Group'); if($metar_toDecode[0] == '//'){ // automatic station can return 'sensor failure' // WMO paragraph 15.4 $decodeInfo['CONDITIONS'] = 'Failure of Present Weather Sensor(s)'; // see UK paragraph 4.167 $decodeGroupCount['CONDITIONS'] = 1; $decodeGroupProcessed['CONDITIONS'] = trim(array_shift($metar_toDecode)); // take out processed segment if($show_diagnostics) echo 'Matched into "Present Weather Phenomena Group" with content of ' . $decodeGroupProcessed['CONDITIONS'] . ' giving output of ' . $decodeInfo['CONDITIONS'] . '
'; }else{ $decodeGroupCount['CONDITIONS'] = 0; unset($decodeInfo['CONDITIONS']); while (count($metar_toDecode) > 0 and $decodeGroupCount['CONDITIONS'] < 4) { $present = novel_decode_phenomena($metar_toDecode[0],'present'); if(isset($present[0]) and strlen($present[0]) > 3) { if(!isset($decodeInfo['CONDITIONS'])) $decodeInfo['CONDITIONS'] = ''; $decodeInfo['CONDITIONS'] .= $present[0]; $decodeGroupCount['CONDITIONS'] ++; if(!isset($decodeGroupProcessed['CONDITIONS'])) $decodeGroupProcessed['CONDITIONS'] = ''; if($decodeGroupCount['CONDITIONS'] > 1) $decodeGroupProcessed['CONDITIONS'] .= ' + '; $decodeGroupProcessed['CONDITIONS'] .= $present[1]; unset($present); }else break; } // end While if(isset($decodeInfo['CONDITIONS']) and substr(trim($decodeInfo['CONDITIONS']),-1) ==',') { $decodeInfo['CONDITIONS'] = substr(trim($decodeInfo['CONDITIONS']), 0, -1); #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# } $lastSuccess = 'Present'; } # End parse METAR for (conditional) Present weather phenomena Group # #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# #------------------------------ # Are there any more Groups? if(count($metar_toDecode) < 1) goto function_exit; #------------------------------ optionalDiagnosticOutput('WMO standard Group: Cloud Cover details'); re_entryCloud: #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# # Parse METAR for Cloud Cover (UK) or Layer Aloft (Canada) or Sky Condition (USA) Group # #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# // ICAO Annex 3 paragraph 4.6.5.1 says observe cloud amount, cloud type, and cloud height (in metres but also allows feet as alternative) if of operational significance // WMO regulations paragraph 15.4, 15.9 and sub-paragraphs of 15.9 // UK CAA METAR Coding Cloud Cover Rules paragraphs 4.99 to 4.109 (I've corrected typo in 4.100), and 4.159 to 4.163 - UK standard allows up to 3 segments but can have extra one if thunderstorms // USA FAA standard chapter 9 plus paragraphs 12.4 a. (9) and 15.6.9 Sky Condition and http://www.met.tamu.edu/class/metar/metar-pg10-sky.html // Environment and Climate in Canada ManObs paragraph 16.3.9.1 Layers aloft (NSNSNShShShS) talks about Layer Aloft Group and paragraph 16.3.13.1 Layer type and amount (oktas) talks about correlation with content of Remarks Group if(array_key_exists('SKY', $decodeInfo)) unset($decodeInfo['SKY']); if(array_key_exists('SKY-DETAILS', $decodeInfo)) unset($decodeInfo['SKY-DETAILS']); if($metar_toDecode[0] == '///') { // WMO paragraph 15.4 optionalDiagnosticOutput("WMO standard Group: Cloud Cover Group - failure of sensor"); $lastSuccess = 'Cloud'; $decodeGroupCount['SKY'] = 1; // matched is true $decodeInfo['CLOUD-DETAILS'] = "Automatic Station - Sensor failure for cloud detection"; $decodeGroupProcessed['SKY'] = trim(array_shift($metar_toDecode)); // take out processed segment if($show_diagnostics) echo '
Matched into "Cloud Cover Group" with content of ' .$decodeGroupProcessed['SKY'] . '
'; // conditional de-bugging section continues }elseif(in_array($metar_toDecode[0], $cloud_type_array)) { optionalDiagnosticOutput('WMO standard Group: Cloud Cover Group - found cloud type code'); $lastSuccess = 'Cloud'; $decodeGroupCount['SKY'] = 1; // matched is true $decodeInfo['SKY-DETAILS'][0][0] = array_search($metar_toDecode[0], $cloud_type_array); $decodeGroupProcessed['SKY'] = trim(array_shift($metar_toDecode)); // take out processed segment if($show_diagnostics) echo '
Matched into "Cloud Cover Group" with content of ' .$decodeGroupProcessed['SKY']; // conditional de-bugging section continues }elseif(substr($metar_toDecode[0], 0, 2) == 'VV' or array_key_exists(substr($metar_toDecode[0], 0, 3), $cloudAmountBandCode)) { optionalDiagnosticOutput('WMO standard Group: Cloud Group - found cloud amount band code'); $cloud[4] = true; // this array element used purely to control while $decodeGroupProcessed['SKY'] = array(); $decodeGroupCount['SKY'] = 0; // used to count number of cloud layers reported // USA standard has maximum 3 segments for auto, maximum of 6 segments for observer while ($cloud[4] and $decodeGroupCount['SKY'] < 7 and count($metar_toDecode) > 0) { // condition allows for 6 segments in Group as per USA standard, WMO standard followed by UK is maximum of 3 plus exception for storm clouds // - see paragraph CAA document paragraph 4.100 reproduced (with its typo corrected) below // Canada and New Zealand frequently report 4 normal cloud layer reports // Canada may quantify cloud layer to exact number of oktas in Remarks Group // In this standard Group, Cloud Amount is only quantified roughly by either none, few, broken, or overcast /* Normally up to three cloud layers may be reported (although in certain circumstances more can be reported - see (4) below for WMO and table below for USA): 1) the lowest layer, whatever the amount; 2) the next layer above of amount 3 oktas or more (SCT, BKN or OVC); 3) the next layer above of amount 5 oktas or more (BKN or OVC); 4) insert any towering cumulus or cumulonimbus cloud omitted by the above rules, whilst retaining base height order from lowest to highest. For USA (N N N h h h or VVh h h or SKC/CLR): Table 9-3. Priority for Reporting Layers Priority Layer Description -------- ----------------- 1 lowest few layer. 2 lowest broken layer. 3 overcast layer. 4 lowest scattered layer. 5 second lowest scattered layer. 6 second lowest broken layer. 7 highest broken layer. 8 highest scattered layer. */ // this script does not check METAR obeys above rules yet note that these rules allow // some METAR to correctly report multiple layers with same high (5 oktas or over) cloud amount! optionalDiagnosticOutput('WMO standard Group: Cloud Group'); $cloud = novel_decode_cloud ($metar_toDecode[0]); // call sub-function to see if Cloud Group is missing or present //---------------------------------------------------------------- // $cloud array returned from 'novel_decode_cloud' function by SPAWS: // [0] = Sky summary - Clear, Cloudy, Overcast, Obscured // [1] = Cloud Amount, [2] = Cloud Height, [3] = Cloud Type Code // [4] = Boolean indicating whether METAR segment recognised as 'Cloud Cover' Group if(!$cloud[4]) break; // called sub-function has set $cloud[4] to false as segment being processed does not match Cloud Group format // [5] = METAR segment that has been decoded // [6] = Cloud Ceiling // [7] = Ken True condition - 'Clear', 'No significant clouds', 'Cloud', 'Cloudy' //---------------------------------------------------------------- $lastSuccess = 'Cloud'; if($show_diagnostics) print_array($cloud, '
WMO standard Group: Cloud decoder returned'); ##################################################################################################################################### # Process single segment with cloud type (optional), cloud coverage in oktas (mandatory), and cloud height (mandatory) report # ##################################################################################################################################### $skyIterator = $decodeGroupCount['SKY']; $decodeGroupProcessed['SKY'] [$skyIterator + 1] = $cloud[5]; if(array_key_exists(3, $cloud)) $decodeInfo['SKY-DETAILS'][$skyIterator][0] = $cloud[3]; // WMO permit cloud type to be specified here elseif($cloud[0] != 'Obscured' and substr($cloud[7],0, 5) == 'Cloud') $decodeInfo['SKY-DETAILS'][$skyIterator][0] = 'Unspecified cloud: '; // In Canada, the cloud type that will be inserted here is included in Remarks Group if(array_key_exists(1, $cloud) and array_key_exists(2, $cloud)) // oktas and height // WMO paragraph 15.9.1.5 says heights reported in multiples of 30 metres, UK and USA report heights in hundreds of feet { $decodeInfo['SKY-DETAILS'][$skyIterator][1] = '' . $cloud[1] . ''; // WMO standard oktas code, in Canada a more accurate oktas figure is included in Remarks Group $decodeInfo['SKY-DETAILS'][$skyIterator][2] = $cloud[2]; // add cloud layer height details } if(array_key_exists(0, $cloud) and !array_key_exists('SKY', $decodeInfo)) $decodeInfo['SKY'] = $cloud[0]; # N.B. The array element 'SKY' is not used in Saratoga scripts ################################################################################# # From last valid segment produce a summary of overall sky conditions # ################################################################################# if(array_key_exists(7, $cloud)) // clear, cloudy, storm cloud, or overcast { $decodeInfo['CLOUDS'] = $cloud[7]; # This array element is included for selecting a conditions weather icon in Saratoga scripts # and I wanted my script to work with anyone wanting to emulate his look. } ######################################################################################################################################## # From the appropriate single segment of all the cloud reports, derive the ceiling, it is the first layer that covers half the sky # ######################################################################################################################################## if(array_key_exists(6, $cloud)) { /* The ceiling has been returned from the called sub-function, it 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. */ if(!array_key_exists('CEILING', $decodeInfo)) // Ensures records lowest as first reported is reported, and if higher appear afterwards, it is ignored { $decodeInfo['CEILING'] = $cloud[6]; $decodeGroupProcessed['CEILING'] = $cloud[5]; } } // end ceiling returned if($show_diagnostics) echo '

Matched into "Cloud Cover Group" the content of ' . $cloud[5] . '

'; // conditional de-bugging section continues $decodeGroupCount['SKY'] ++; } // end while #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# } # End parse METAR for Cloud cover/sky condition Group # #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# // end of visibility, weather and cloud cover #------------------------------ # Are there any more Groups? if(count($metar_toDecode) < 1) goto function_exit; #------------------------------ if($lastSuccess != 'Cloud' and array_key_exists(1, $metar_toDecode) and substr($metar_toDecode[1], 0, 2) == 'VV' or array_key_exists(substr($metar_toDecode[1], 0, 3), $cloudAmountBandCode)) { $input = novel_decode_cloud($metar_toDecode[1]); if($input[4]) { unRecognised('Cloud cover/sky condition Group'); // handle unrecognised segment goto re_entryCloud; // process Cloud cover/sky condition Group } } re_entryTD: #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# # Parse METAR for Temperature / Dew point Group (single mandatory segment) # #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# // ICAO annex 3 paragraph 4.6.6.1 says report temperature and dew-point in Celsius; // ICAO paragraph 4.6.6.2 recommends that the reported values are representative of whole runway complex // WMO regulations paragraph 15.11 // UK CAA standard paragraphs 4.110 to 4.116 // USA FAA standard chapter 10 plus paragraphs 12.4 a. (10) and 12.6.10 and http://www.met.tamu.edu/class/metar/metar-pg11-t-td.html // Environment and Climate in Canada ManObs paragraph 16.3.10 optionalDiagnosticOutput('WMO standard Group: Air Temperature and Dew-point Group'); $partLocal= preg_match('/^(M?[0-9]{2}|[\/]{2})([\/])(M?[0-9]{2}|[\/]{2})?/',$metar_toDecode[0],$components); // UK Para 4.116 allows dew point to be replaced by '//'. USA FAA allow dew-point to be omitted (as does New Zealand) // 2017/12/10 23:30 METAR UTDT 102330Z 00000MPS 1600 BR SCT086 01/00 Q1022 35////// RMK QFE726/0969 if ($partLocal === 1) { // if added below as testing showed that observers UK stations like Barkston Heath report "/////" instead of leaving out temperature/dew-point Group when measurements not available if($components[1] == '//') { $decodeGroupCount['TEMP'] = 1; $decodeInfo['TEMP'] = 'Temperature sensor defective'; }elseif(array_key_exists(3, $components)) { if($show_diagnostics) echo '
temperature=' . $components[1] . ', dew-point=' . $components[3]; $decodeGroupCount['TEMP'] = 0; rev_get_temperature($components[1], $components[3]); // called sub-function also adds extra output like apparent temperature, absolute humidity }else { // 2017/11/09 17:50 METAR NZSP 091750Z 33017G23KT 0800 BLSN VV008 M28/ A2852 RMK PK WND 33029/1658 CLN AIR 33016G25KT ALL WNDS GRID SDP/HDN $decodeGroupCount['TEMP'] = 0; rev_get_temperature($components[1], '//'); /// temperature only, no dew-point } # example METAR from Ha'il Regional Airport, Saudi Arabia: METAR OEHL 011300Z 15004KT CAVOK 36/00 Q1017 NOSIG # note dew point much lower than temperature # example METAR from King Khalid International Airport, Riyadh, Saudi Arabia: METAR OEHL OERK 011400Z 03009KT CAVOK 37/M02 Q1014 NOSIG # note dew point negative temperature $decodeGroupProcessed['TEMP'] = trim(array_shift($metar_toDecode)); // take out processed segment if($show_diagnostics) echo '

Matched into "Temperature / Dew-point Group" the content of ' . $decodeGroupProcessed['TEMP'] . '

'; // conditional de-bugging section continues $lastSuccess = 'TD'; }else{ if($show_diagnostics) echo '
No match against temperature Group for ' . $metar_toDecode[0]; } #------------------------------ # Are there any more Groups? if(count($metar_toDecode) < 1) goto function_exit; #------------------------------ if($lastSuccess != 'TD' and count($metar_toDecode) > 1 and preg_match('/^(M?[0-9]{2}|[\/]{2})([\/])(M?[0-9]{2}|[\/]{2})?/',$metar_toDecode[1],$components) === 1) { unRecognised('Temperature / Dew point Group'); // handle unrecognised segment goto re_entryTD; // process Temperature / Dew point Group } #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# # Parse METAR for Sea/airfield Level Atmospheric Pressure Group (mandatory single segment) # # Novel code snippet in this script as allows for second pressure segment here too # #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# ######################################################################################################################### # There are three possible pressures that can be reported, either here in the Atmospheric Pressure Group of the # # mandatory part of the WMO Standard Group, or in the region specific optional Remarks Group (processed later). # # QNH) Sea Level Pressure - the WMO standard pressure reported is in integer hectoPascals, aka millibars # # ALT) Altimeter Pressure - officially the pressure reading at cockpit level of an aircraft parked on the runway # # QFE) Airfield Level Pressure - the pressure reading as measured at the official airfield altitude in inches Hg # ######################################################################################################################### // ICAO Annex 3 paragraph 4.6.7 says compute QNH and QFE values and report them in hectoPascals // ICAO annex 5 prescribes use of hectoPascals for all pressure reporting // QNH is Sea Level Pressure (also referred to as Atmospheric pressure at Nautical Height) // QFE is referred to as "Atmospheric pressure at aerodrome Field Elevation (or at runway threshold)" // WMO regulations paragraph 15.12 says report in whole hectoPascals rounding down, many decoders therefore explain the reported figure X as in the range X.0 to X.9 // UK CAA METAR Coding Rules calls this Group 'Atmospheric Pressure' and defines it in paragraphs 4.117 to 4.124 // USA FAA standard chapter 11 plus paragraphs 12.4 a. (11) and 12.6.11 calls this Group 'Altimeter' as explained below. See also http://www.met.tamu.edu/class/metar/metar-pg12-pres.html // Belgium rules say they report in tenths of hectoPascals, but all the Belgian METAR I have seen show their METAR just reports pressure same as others internationally in Q format // Pakistan does actually report to tenths of hectoPascals, as its METAR contain one more digit and that represents the extra precision // Environment and Climate in Canada ManObs paragraph 16.3.11 optionalDiagnosticOutput('WMO Standard Mandatory Group: Atmospheric Pressure Group'); // 2017/12/08 21:00 METAR MZBZ 082100Z 11004KT 9999 VCSH BKN022 28/25 A2980 Q1009 NOSIG RMK VCSH-N/NW ----> Example with both A and Q segments // 2017/12/09 22:00 METAR MGGT 092200Z 36022KT 9999 FEW018 19/10 Q1026 A3030 ----> Example with both Q and A segments (other order) // 2017/12/10 19:30 METAR OPRN 101930Z 00000KT 4000 HZ FEW140 SCT180 12/07 Q10181/3006 ----> Example with both hPa and InHg units in same segment // Democratic Republic of the Congo is an example of somewhere that does not obey rule of using prefix Q e.g. Bangoka International Airport: // 2017/12/25 10:00 METAR FZIC 251000Z 16006KT 9999 SCT020 29/25 1012 NOSIG ----> Example with both visibility and pressure using same segment pattern $decodeGroupCount['SLP'] = false; $decodeGroupCount['BAROMETER'] = false; re_entryPress: #------------------------------ # Are there any more Groups? if(count($metar_toDecode) < 1) goto function_exit; #------------------------------ $partLocal = preg_match('~^(A|Q)?([0-9]+|[\/]{4})(\/)?([0-9]{4})?~',$metar_toDecode[0],$pieces); # $pieces[1] - The prefix 'A', 'Q', or none # $pieces[2] - Either a number (normally 4 digits) or '////' # $pieces[3] - Optional '/' indicating both units specified # $pieces[4] - The pressure repeated in non-standard (in Hg) units // Regular expression as used by Mark Woodward (see comment section below) has been improved upon. // ('Woody' assumed as in USA, the whole segment is left out if pressure not measured) // Above version caters for the alternative of slashes allowed by WMO when automatic sensor cannot measure pressure // Also works with those METAR that report to tenths of hPA and hundredths of inch // Also caters for fewer digits as during testing I encountered a rogue "Q10" // Finally allows for pressures reported without the prefix letter /* MARK WOODWARD ORIGINAL CODING function get_altimeter($part, &$metarPtr, &$group, &$wxInfo) { // Decodes altimeter or barometer information. // Format is Annnn where nnnn represents a real number as nn.nn in inches of Hg, // or Qpppp where pppp = hectoPascals. // Some other common conversion factors: // 1 millibar = 1 hPa // 1 in Hg = 0.02953 hPa // 1 mm Hg = 25.4 in Hg = 0.750062 hPa // 1 lb/sq in = 0.491154 in Hg = 0.014504 hPa // 1 atm = 0.33421 in Hg = 0.0009869 hPa if (ereg('^(A|Q)([0-9]{4})',$part,$pieces)) { if ($pieces[1] == 'A') { $pressureIN = substr($pieces[2],0,2) . '.' . substr($pieces[2],2); // units are inches Hg $pressureHPA = round($pressureIN / 0.02953); // convert to hectoPascals }else { $pressureHPA = (integer) $pieces[2]; // units are hectoPascals $pressureIN = round(0.02953 * $pressureHPA,2); // convert to inches Hg } $wxInfo['BAROMETER'] = "$pressureHPA hPa ($pressureIN in Hg)"; $metarPtr++; $group++; }else { $group++; } } */ if($show_diagnostics) print_array($pieces, 'Looking at Standard pressure segment in pieces'); if($partLocal === 1) // matched { if ($pieces[1] == 'Q' or $pieces[1] == '') // International Standard prefix 'Q' when pressure reported is in integer hectoPascals, also known as millibars { ######################################################################################################################### # IMPORTANT NOTE: Sea Level Pressure # # The international standard says the figure reported here should be the barometric pressure corrected to sea-level. # # The American Region standard says the figure reported here should be the Altimeter pressure [the pressure value # # to which an aircraft altimeter scale is set so that it will indicate the altitude above mean sea level of an # # aircraft on the ground at the airfield (location where the value was determined)]. # # In that Variant of METAR Standard, the sea level pressure is conditionally reported in the Remarks section. # ######################################################################################################################### # Pilots talk about 'Nautical Height' pressure to remember QNH - ICAO 'Q codes' are 3 letter codes (not abbreviations) introduced for radio communication # (QFE is different, that is pressure at airfield level, pilots talk about 'Field Elevation' pressure to remember that code) if(is_numeric($pieces[2])) // units are hectoPascals { // NB - rules say always 4 digits, includes leading zeroes if below 1000 hPa, but may be 5 digits (greater precision) or fewer digits (malformed) if($pieces[2] < 50) $pieces[2] = 1000 + $pieces[2]; // Allow for malformed report that fails to include all 4 digits if(strlen($pieces[2]) == 5) { $pressureHPA = 0.1 * $pieces[2]; // Pakistan is an example of a country that reports the actual measured pressure (to tenths of a hPa) $pressureIN = round(0.002953 * $pieces[2], 2); // convert to inches Hg, remembering actual reading in hPa has been reported }else{ $pressureHPA = 1 * $pieces[2]; $pressureIN = round(0.02953 * ($pieces[2] + 0.4), 2); // convert to inches Hg, assuming that some rounding down of hPa value has taken place } if(array_key_exists(4, $pieces)) { $pressureIN = $pieces[4] /100; $output = 'QNH measured as ' . $pressureIN . ' inHg and '; }else{ $output = 'QNH calculated as approximately ' . $pressureIN . " inHg but "; } if($pressureHPA * 10 == ($pressureHPA * 10) % 1) $output .= " measured as " . $pressureHPA . " hPa"; // UK CAA para 4.121, 4.122 say measure to tenths of hPa and round down for reporting here, i.e. ignore decimal part, same applies to many other countries // This decoder follows convention of some of the other METAR decoders by quoting the full range of measurements that can be represented by any integer reported else{ $output .= 'measured between ' . $pressureHPA . ' and '; $output .= 0.9 + $pressureHPA; $output .= ' hPa'; } $decodeInfo['SLP'] = $output; // Report both units in single output. }else{ $pressureHPA = $pressureIN = null; $decodeInfo['SLP'] = 'Pressure sensor failure'; } $decodeGroupProcessed['SLP'] = array_shift($metar_toDecode); // take out processed segment if($show_diagnostics) echo '

Matched into "Pressure Group" the content of ' . $decodeGroupProcessed['SLP'] . '

'; // conditional de-bugging section continues goto re_entryPress; }elseif ($pieces[1] == 'A') // International Standard prefix 'A' when pressure reported in hundredths of inches of mercury (UK CAA paragraph 4.124) as used by some military airfields in UK instead of 'Q' { if(!array_key_exists('BAROMETER', $decodeGroupCount)) $decodeGroupCount['BAROMETER'] =''; elseif(strlen($decodeGroupCount['BAROMETER']) > 5) $decodeGroupCount['BAROMETER'] .= ' and '; // Assume all METAR using prefix 'A', and therefore inches of Mercury, in this mandatory Pressure Group are reporting pressure at airfield level $decodeGroupCount['BAROMETER'] .= 'Altimeter Pressure (at elevation of airfield)'; // USA specify different units for slp and altimeter, but nothing in WMO or ICAO guidance to confirm this. $pressureIN = substr($pieces[2],0,2) . '.' . substr($pieces[2],2); // units are hundredths of inches Hg $pressureKPA = round($pressureIN / 0.2953,2); // converts to kiloPascals $pressureHPA = round($pressureIN / 0.02953,1); // converts to hectoPascals if(!array_key_exists('BAROMETER', $decodeInfo)) $decodeInfo['BAROMETER'] =""; if(substr($decodeInfo['STATION'], 0, 1) == 'C') $decodeInfo['BAROMETER'] .= " QFE reported as $pressureIN inHg ($pressureKPA kPa)"; // Report both units in single output. else $decodeInfo['BAROMETER'] .= " QFE reported as $pressureIN inHg ($pressureHPA hPa)"; // Report both units in single output. if(!array_key_exists('BAROMETER', $decodeGroupProcessed)) $decodeGroupProcessed['BAROMETER'] =""; else $decodeGroupProcessed['BAROMETER'] .='_'; $decodeGroupProcessed['BAROMETER'] .= array_shift($metar_toDecode); // take out processed segment if($show_diagnostics) echo '

Matched into "Pressure Group" the content of ' . $decodeGroupProcessed['BAROMETER'] . '

'; // conditional de-bugging section continues goto re_entryPress; } } function_exit: #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# # Finished with decoding standard Groups # #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# $returnArray['lastSuccess'] = $lastSuccess; if($show_diagnostics) echo '
###############
Ending decoding of WMO standard Groups

###############
'; $returnArray['toDo'] = count($metar_toDecode); # if($show_diagnostics) print_array($returnArray, '$returnArray'); return $returnArray; } // ===================================================================================================================================================================================== ################################################################## # Functions to decode WMO supplementary Groups in METAR # ################################################################## function decodeMETAR_supplementary() { global $metar_toDecode, $decodeInfo, $decodeGroupProcessed, $decodeGroupCount, $colour_array, $compass, $pressureHPA, $show_diagnostics, $lf, $dM, $dMsf, $lastSuccess, $stillEncoded_file; // Global Variables can be shared with calling script and with other functions or scripts; $lastSuccess = NULL; $matched = false; optionalDiagnosticOutput('WMO supplementary Groups: Ready to start with any optional'); if($show_diagnostics) echo 'Initial Counts: $metar_toDecode=' . count($metar_toDecode) . '   $decodeGroupCount=' . count($decodeGroupCount) . '   $decodeGroupProcessed=' . count($decodeGroupProcessed); #--------------------------------------------------- # Are there any more Supplementary Groups? if(!count($metar_toDecode) or $metar_toDecode[0] == 'RMK') goto function_exit; #--------------------------------------------------- re_entryRecent: ################################# # Is it 'Recent Phenomena' ? # ################################# if($metar_toDecode[0] != 'RED' and substr($metar_toDecode[0], 0, 2) == 'RE') { optionalDiagnosticOutput('WMO supplementary Group: Recent phenomena Group'); /* Canada in their Manual of Surface Weather Observations Seventh edition (April 2015) has removed the mention of conditions for the reporting of Recent Weather (RE) phenomena and METAR/SPECI issued by aerodromes and other stations in Canada must not report that Group from 2015 onwards. This is in compliance with ICAO Annex 3 Eighteenth Edition July 2013, Appendix 3 Section 4.8.1.1 that was first responsible for removing a formal requirement that where METAR/SPECI are issued there was a need to report Recent Weather (RE) phenomena in certain conditions. */ $matched = test_recent(); $lastSuccess = 'Recent'; } #--------------------------------------------------- # Are there any more Supplementary Groups? if(!count($metar_toDecode) or $metar_toDecode[0] == 'RMK') goto function_exit; #--------------------------------------------------- if($lastSuccess != 'Recent' and count($metar_toDecode) > 1 and $metar_toDecode[1] != 'RED' and substr($metar_toDecode[1], 0, 2) == 'RE') { unRecognised('Recent Phenomena Group'); // handle unrecognised segment goto re_entryRecent; // process Recent Phenomena Group } re_entryColour: ################################# # Is it 'Colour State' ? # ################################# // The current colour state is often included as the last segment (when there is no Trend Group) and so may be appended at the end of Remarks Group // The code called below is therefore also repeated within the script dealing with the Remarks Group. if(substr($metar_toDecode[0], 0, 5) == 'BLACK' or (in_array($metar_toDecode[0], $colour_array) and array_search($metar_toDecode[0], $colour_array) != 0)) { // Some METAR, particularly Belgium ones, seem to repeat this colour state twice in this position // e.g. EBFS 050725Z 24006KT 2500 BR FEW004 SCT008 BKN011 03/02 Q1036 YLO YLO TEMPO AMB - This is a report for Florennes - Belgium // EBBE 050725Z 23007KT 4500 BR SCT008 BKN036 04/03 Q1036 GRN GRN TEMPO YLO - This is a report for Beauvechain - Belgium. // EBBL 050725Z 23009KT 4500 BR FEW015 BKN035 05/04 Q1035 GRN GRN TEMPO YLO - This is a report for Kleine Brogel - Belgium. // EBFN 050725Z 28010KT 9999 BKN032 09/03 Q1035 BLU BLU - This is a report for Koksijde - Belgium. // EBDT 050725Z 23005KT 6000 FEW007 BKN035 05/04 Q1035 WHT WHT TEMPO GRN - This is a report for Schaffen - Belgium. if(count($metar_toDecode) > 1 and $metar_toDecode[0] == $metar_toDecode[1]) $done = array_shift($metar_toDecode); // Discard duplicate segment optionalDiagnosticOutput('WMO supplementary Group: Declared Colour State Group'); $decodeGroupCount['STATE'] = 0; $matched = colour_state(); $lastSuccess = 'Colour'; } #--------------------------------------------------- # Are there any more Supplementary Groups? if(!count($metar_toDecode) or $metar_toDecode[0] == 'RMK') goto function_exit; #--------------------------------------------------- re_entryWS: ################################# # Is it 'Wind Shear' ? # ################################# if(substr($metar_toDecode[0], 0, 2) == 'WS') { /* One specification is in New Zealand Aeronautical Information Publication (AIP) Part 1 - General (GEN), available at http://www.aip.net.nz/pdf/GEN_3.5.pdf (effective 23 June 2015); 3.9.1 Information about observed wind shear on the approach or take-off paths is included, when appropriate, in METAR and SPECI reports. */ optionalDiagnosticOutput('WMO supplementary Group: Wind Shear Group'); $matched = test_wind_shear(); $lastSuccess = 'WS'; } #--------------------------------------------------- # Are there any more Supplementary Groups? if(!count($metar_toDecode) or $metar_toDecode[0] == 'RMK') goto function_exit; #--------------------------------------------------- re_entrySS: ################################# # Is it 'Sea State' ? # ################################# if(substr($metar_toDecode[0],0,1) == 'W' and substr($metar_toDecode[0],3,1) == '/') { optionalDiagnosticOutput('WMO supplementary Group: Sea state Group'); $matched = test_sea_state(); $lastSuccess = 'SS'; } #--------------------------------------------------- # Are there any more Supplementary Groups? if(!count($metar_toDecode) or $metar_toDecode[0] == 'RMK') goto function_exit; #--------------------------------------------------- re_entryRS: ################################# # Is it 'Runway State' ? # ################################# // 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 if(substr($metar_toDecode[0],0,1) == 'R' and substr($metar_toDecode[0],1,1) == '/' || is_numeric(substr($metar_toDecode[0],1,1))) { optionalDiagnosticOutput('WMO supplementary Group: Runway state Group'); $matched = test_runway_state(); $lastSuccess = 'RS'; } #--------------------------------------------------- # Are there any more Supplementary Groups? if(!count($metar_toDecode) or $metar_toDecode[0] == 'RMK') goto function_exit; #--------------------------------------------------- re_entryTrend: ################################# # Is it 'Trend forecast' ? # ################################# optionalDiagnosticOutput('WMO supplementary Group: Trend forecast Group'); /* One specification is in New Zealand Aeronautical Information Publication (AIP) Part 1 - General (GEN), available at http://www.aip.net.nz/pdf/GEN_3.5.pdf (effective 23 June 2015); 3.5 Manual Observations (METAR and SPECI) 3.7.1 TREND forecasts will be appended to METAR AUTO reports from NZAA, NZCH, NZWN and to METAR and SPECI reports from the military aerodromes at NZOH and NZWP. New Zealand only: Each AWS-created METAR AUTO is sent first to MetService, where a TREND forecast (derived from the current TAF) is automatically added to meet ICAO requirements for international METARs. */ $timeArray = array (); switch ($metar_toDecode[0]) { case 'NOSIG': // UK specification case 'NTSIG': // Used by some published METAR whose ICAO starts with 'T', while others with same starting letter use 'NOSIG' $decodeInfo['TREND_TYPE'] = 'No significant changes are forecast'; $decodeGroupProcessed['TREND_TYPE'] = trim(array_shift($metar_toDecode)); // take out single processed segment $matched[0] = 'TREND_TYPE'; goto function_skip; break; case "BECMG": // WMO regulation paragraph 15.14.2 and 15.4.4 # METAR example from Christchurch, New Zealand: NZCH 011200Z AUTO 02003KT 9999 MIFG NCD 05/05 Q0992 BECMG 0400 FG $decodeInfo['TREND_TYPE'] = 'The changes shown below are forecast'; $decodeGroupProcessed['TREND_TYPE'] = trim(array_shift($metar_toDecode)); // take out 1st processed segment "BECMG" if($show_diagnostics) { echo '
Matched "BCMG" as in trend Group; next test for any trend timings
'; // WMO regulation paragraph 15.14.3 print_array($metar_toDecode,'Trend Group timings'); } if(substr($metar_toDecode[0], 0, 2) == 'AT') { $timeArray = adjustTime(substr($metar_toDecode[0], 2)); $decodeInfo['TREND_TYPE'] .= ' commencing at ' . $timeArray['hour'] . ':' . $timeArray['minute'] .' (local time)'; $decodeGroupProcessed['TREND_TYPE'] .= '_' . trim(array_shift($metar_toDecode)); // take out 2nd processed segment "AT" and time }elseif(substr($metar_toDecode[0], 0, 2) != 'FM' and substr($metar_toDecode[0], 0, 2) != 'TL') { $decodeInfo['TREND_TYPE'] .= ' during period until next report'; // no 2nd segment to remove }else{ if(substr($metar_toDecode[0], 0, 2) == 'FM'){ $timeArray = adjustTime(substr($metar_toDecode[0], 2)); $decodeInfo['TREND_TYPE'] .= ' after ' . $timeArray['hour'] . ':' . $timeArray['minute'] . ' (local time)'; $decodeGroupProcessed['TREND_TYPE'] .= '_' . trim(array_shift($metar_toDecode)); // take out 2nd processed segment 'FM and time } if(substr($metar_toDecode[0], 0, 2) == 'TL'){ // can be in addition to 'FM' $timeArray = adjustTime(substr($metar_toDecode[0], 2)); $decodeInfo['TREND_TYPE'] .= ' lasting to ' . $timeArray['hour'] . ':' . $timeArray['minute'] . ' (local time)'; $decodeGroupProcessed['TREND_TYPE'] .= '_' . trim(array_shift($metar_toDecode)); // take out 2nd/3rd processed segment "TL" } } break; case "TEMPO": // WMO regulation paragraph 15.14.2 and 15.14.7 # Example from Berlin Tegel Airport, Germany: METAR EDDT 011320Z 28004KT 250V320 9999 FEW008 BKN011 14/12 Q1022 TEMPO 4000 -DZ BKN006 # Example from Berlin Schönefeld Airport, Germany: METAR EDDB 011320Z 24005KT 9999 BKN008 14/12 Q1022 TEMPO 4000 -DZ $decodeInfo['TREND_TYPE'] = 'Temporary fluctuations from present weather are forecast'; $decodeGroupProcessed['TREND_TYPE'] = trim(array_shift($metar_toDecode)); // take out processed segment if($show_diagnostics) echo '
Matched "TEMPO" as in trend Group; next test for any trend timings'; // WMO regulation paragraph 15.14.3 if(substr($metar_toDecode[0], 0, 2) == 'AT') { $timeArray = adjustTime(substr($metar_toDecode[0], 2)); $decodeInfo['TREND_TYPE'] .= ' commencing at ' . $timeArray['hour'] . ':' . $timeArray['minute'] . ' (local time)'; $decodeGroupProcessed['TREND_TYPE'] .= '_' . trim(array_shift($metar_toDecode)); // take out 2nd processed segment "AT" and time }elseif(substr($metar_toDecode[0], 0, 2) != 'FM' and substr($metar_toDecode[0], 0, 2) != 'TL') { $decodeInfo['TREND_TYPE'] .= ' during period until next report'; // no 2nd segment to remove }else{ if(substr($metar_toDecode[0], 0, 2) == 'FM'){ $timeArray = adjustTime(substr($metar_toDecode[0], 2)); $decodeInfo['TREND_TYPE'] .= ' after ' . $timeArray['hour'] . ':' . $timeArray['minute'] . ' (local time)'; $decodeGroupProcessed['TREND_TYPE'] .= '_' . trim(array_shift($metar_toDecode)); // take out 2nd processed segment 'FM and time } if(substr($metar_toDecode[0], 0, 2) == 'TL'){ // can be in addition to 'FM' $timeArray = adjustTime(substr($metar_toDecode[0], 2)); $decodeInfo['TREND_TYPE'] .= ' lasting to ' . $timeArray['hour'] . ':' . $timeArray['minute'] . ' (local time)'; $decodeGroupProcessed['TREND_TYPE'] .= '_' . trim(array_shift($metar_toDecode)); // take out 2nd/3rd processed segment "TL" } } break; default: goto next_function; } re_entryChange: $matched = changeGroup(); $lastSuccess = 'Change'; goto next_function; function_skip: if($show_diagnostics) echo '
Matched into Trend forecast Group with content of ' . $decodeGroupProcessed['TREND_TYPE'] . '
'; // conditional de-bugging section continues $lastSuccess = 'Trend'; next_function: #--------------------------------------------------- # Are there any more Supplementary Groups? if(!count($metar_toDecode) or $metar_toDecode[0] == 'RMK') goto function_exit; #--------------------------------------------------- ################################# # Is it 'Colour State' ? # ################################# if(substr($metar_toDecode[0], 0, 5) == 'BLACK' or in_array($metar_toDecode[0], $colour_array)) { optionalDiagnosticOutput('WMO supplementary Group: Forecast Colour State Group'); $decodeGroupCount['STATE'] = 2; $matched = colour_state(); } #--------------------------------------------------- # Are there any more Supplementary Groups? if(!count($metar_toDecode) or $metar_toDecode[0] == 'RMK') goto function_exit; #--------------------------------------------------- function_exit: ################################# # No more Groups left! # ################################# if($show_diagnostics) echo '
###############
Ending decoding of WMO supplementary Groups
###############
'; $returnArray['lastSuccess'] = $lastSuccess; $returnArray['SUPPLEMENTARY'] = $matched; return $returnArray; } /* Version history =============== 0.0.1 17 July 2017 First developmental version, writing novel code that calls sub-functions for testing the design concepts by running script against live METAR reports from several nearby aerodromes for several days. a) function that will have METAR as its input parameter, first action is to split that parameter into series of encoded segments, b) discarding segments from encoded array as they are matched, putting output into series of array elements to give flexibility over which used, c) aiming to decode every WMO Group, and all possible contents, from any METAR report in UK only. Currently updating Global arrays set in script calling this decoding function, so all scripts calling this decoder must define all the necessary arrays and understand the use of them. Thus on entry into decoding script, it will wipe out existing content, in case populated on any previous call to decoder. 0.0.2 21 July 2017 Second developmental version, almost total rewrite with novel code, working from CAA standard for UK METAR, and drawing on actual METAR decodes of recent days. Novel new code for Surface Wind, Bearing Range, CAVOK, Prevailing Visibility, Present Weather Phenomena, Cloud Cover, and Temperature working, but used comment markings to exclude my novel runway related coding (Runway Visual Range and Runway State Groups) as found my use of regular expressions for those was not working. This version focussed on first getting the decoding and discarding aspect, and the sequencing right for planned volume testing against many METARs. It then focussed on starting to deal with malformed content, presumably from observer input rather than automatic stations, where any segment could not be successfully recognised in terms of permitted format for any Group. This was after finding that a couple of successive METAR from Cranfield that were entered by an observer contained malformed content of a numeric value with a 'T' as suffix (later recognised as surface wind without the 'K' for knots, but originally (in error) assumed to be something to do with visibility without the 'F' for feet). Added code to deal with unknown segments by adding a new 'UNKNOWN' element to output array. Added code to store in a log file the whole METAR where one segment was not decoded, so can use the same METAR to test a subsequent change in the script to deal with malformed METAR of that type. (During the time taken to change the script, it is likely that any live METAR is replaced by a later issue). 0.0.3 27 July 2017 Third developmental version, finalised all UK standard Groups now that RVR (new sub-string approach) decoding is working for suitable real METAR. In this version, all conditions extended to cover international METAR not just UK standard. Also works for North America where differs from international standard. Aim is now for world-wide coverage, able to decode any METAR from anywhere. The whole script has been tested against many METAR so confident of concept, improved script with good commenting and neat layout, all Groups can be matched including REMARKS. From this version onwards, because the script has become difficult to read as it became so long, this particular script now only has 1) main function that deals with the input to the suite and the output from it, 2) the sub-functions to clean and pre-process the raw strings, 3) the functions to process the WMO Standard Groups, the WMO Supplementary Groups (and the additional Colour State Group) in the correct sequence. The other sub-functions, that are called from these main processing elements, have now been moved to a separate script. There they deal with the more complex Groups that can have various alternative formats and in some cases may involve looking at multiple segments. This second script in the suite also contains sub-functions that calculate additional statistics. 0.1.0 3 Aug 2017 Following stress testing with all easy combinations that I can invent that still remain within rules, and a few tweaks to the script to make all those work reliably, have 'released' this working 'production' version. 0.1.1 13 Oct 2017 Very minor changes, I replaced a series of "if" with a "switch" to determine whether prefix included before or instead of 'METAR'. Also slightly edited the decoding of the Day-time Group, so full date determined from just day being quoted using 'last month' feature of PHP. Added "function print_array($array_input)" to replace all uses of "print_r" for arrays by "print_array" and therefore improve readability of diagnostics. 0.1.2 17 Oct 2017 Correction of regular expression for Runway Visual Range Group to allow 'FT' at end as per USA guidance. 1.0.0 22 Oct 2017 Formal Release version - The suite is now stand-alone, it does not impose any constraints on anything external to it, with a simpler input and output. The output from this script is returned via the function call as a single array instead of using multiple Global Arrays that might have different content in calling script. Major advantages are that this script is simplified, both its input (parameters to the function when called) and its output (via that returning array) are only defined for the duration of a single decode. Any script calling this decoding suite is now also simplified and less constrained because what it chooses to use for its own arrays is not determined by need to match global arrays within this suite. However, the interfaces between the 3 scripts that make up this decoder, and with the sub-functions called, do all use the same Global Arrays, including those that relate to diagnostics and script names. Minor improvement to parameter for the function call when raw METAR starts with 'SA' for better consistency. 1.0.1 8 Nov 2017 Corrected one tiny bug relating to wind bearing range (bearing to compass direction). The processed cloud segments are now returned as an array (rather than string with ' + ') to make the separation by layer easier in other scripts, this new approach will also permit correlation with content in Remarks Group. Transform to upper-case added to cleaning sub-function, because it greatly simplifies all the conditional tests throughout the decoding suite. 1.0.2 11 Nov 2017 This suite was originally written in PHP 7.1 taking advantage of new features, included in the major re-write of PHP 5 into PHP 7. (PHP 6 was only ever experimental). I changed my decoder to use "pow(base, value)" instead of "base ** value", and changed all my array declarations to use "x = array (value_list)" instead of "x =[value_list]". This made my suite compatible with PHP version 5.3 upwards, and thus extended its potential appeal. This change was required so my suite could start running live on Paul's web-site in Ontario, as he is not yet using PHP 7. 1.0.3 16 Nov 2017 Added new sub-function to convert any times mentioned in the METAR contents from Coordinated Universal Time (UTC) into Local Standard Time (LST), abbreviated to Local Time where mentioned in outputs, and added calls to this function in Trends Group in the supplementary part (and in Remarks Group - see that script). Again this was for Paul's benefit, Canada time in his area is 5 hours out of step with UTC, and so sometimes the UTC day of the month is also wrong. Updated function call for visibility to reflect name (renamed to function 'novel_allVisibility') and parameter change in 'decodeMETAR_sub_funct.php'. 1.0.4 11 Dec 2017 Further change to interface (see v.1.0.1) with sub-function 'novel_decode_cloud' making it easier for processing in Remarks Group to revise what is output for Cloud Cover Group. Added "This work is licensed under the Creative Commons Attribution-NonCommercial 4.0 International License". Minor script improvements. Added code to highlight METAR that are over 16 hours old, message says how many hours old up to 23, and changes to how many days old for older METAR issues. Changes to processing for visibility and weather phenomena, after having some failures to decode. Rewrite of main Pressure Group, it now permits both items to be reported (i.e. A and Q can both appear and in either order), following previous ignores when testing many METARs from throughout the world. 1.1.0 30 Dec 2017 Significant re-design of logical path, the previous version was consuming so much processing time it sometimes would time out. To be fair, much of that time was spent in functions in the other scripts in this suite, but even within this script the logical flow involved a lot of jumping between sections of the program and a lot of lines of program were followed simply to produce the necessary parameters for a sub-function call and to unravel the returning array on exit from the sub-function back to the main function before the next stage could be followed. In this new version, I have streamlined the entry function called from outside the suite by simplifying function "SPAWS_identify_Groups ()". It then calls a new sub-function "decodeMETAR_parseGroups ($the_metar)" where almost all the working arrays are now defined (and shared as global variables, thereby eliminating need to pass as parameters) and to actually speed-up the path, I have rewritten the code so it is able to do more actions in different sequences. Thus, it represents a simplification for future code maintenance, and will handle deviations from WMO code form standard more reliably. For example, I have removed much of the jumping back associated with the old design where flow had to get to the end to deal with duplicated or unrecognised content. In this, and other, scripts there is more checking that variables or array elements exist to reduce the possibility of execution time errors and the opportunity has been taken to improve the quality of the PHP language at various points, and to revisit many selection conditions to improve the reliability of the matching process. Out of sequence, unrecognised or malformed content is now processed before moving onto next Group, to eliminate any possibility of endless loops, and to stop failure with one segment leading to failure for successive Groups. The Remarks Group is now treated as totally separate to WMO supplementary Groups as that simplifies script flow. Moved concatenating array SKY-DETAILS into string CLOUD-DETAILS to just before this decoder ends, so eliminating previous in-efficient logic of it being in both other scripts. Simplification of previous two regular patterns used for Runway Visual Range into just one, leading to significant decrease in associated revised processing lines, where also revised the wording to describe the figure or figures being output (in season for mist and fog locally so well tested). Revision of array $cloudAmountBandCode to aid its future use in decoding of content in Remarks Group by adding other qualifiers, and correction of Cloud Cover Group processing in this and other scripts, so better able to cope with regional (or nation by nation) differences. Made two small changes in Prevailing Visibility Group for re-written (in other script) function novel_allVisibility, first change in parameters passed in, and second rectifying a logic bug in my coding for handling returned array. Added option to "function optionalDiagnosticOutput()" for its flow related diagnostic information to be output to either the web page or to both web page and a file, the latter option making it easier to diagnose problems when there is a browser crash caused by coding error, time-out, or other reason, causing the web page to be lost before it can be studied. 1.1.1 3 Jan 2018 Added ability to cope with both OGIMET formats (text and HTML). Removed "adding 'METAR'" in place of "CRLF" from cleaning function, as a test METAR had more than one CRLF, and can't decode if that segment is found in multiple places, instead adds "METAR" after processing the date and time prefix segments for NOAA source. Changed logic bug for Present Weather Group, so now if while loop fails to match, don't output matched upon exit from that while loop. Changed Standard Atmospheric Pressure Group so accepts single digit (before required at least 2) after Q after failure to decode for EGDY - Yeovilton (navy) [or it might have been EGVP - Middle Wallop] (I believe it was actually an error (truncation) in reading from NOAA source as OGIMET source does not have a 'Q1' but 'Q1008' for Yeovilton and 'Q1010' for Middle Wallop). 1.2.0 29 Jan 2018 Runway Visual Range: major correction of the output text. Atmospheric Pressure Group: Separation of the QNH (Sea Level pressure) from reporting other pressures. This was recognising that those using web pages based on the Saratoga (ex Mark Woodward) Nearby METAR decoder see something labelled as barometric pressure that might be airfield level or sea level and might be using it to check their own instrument that is corrected for sea level without making allowance. (I made the clarification particularly for Brian Underwood in Turkey as he also provides a widely used web page template showing nearby METAR). Otherwise minor cosmetic adjustments to comments and output messages, either to make debugging easier, to make it slightly more efficient, or to make output clearer. 1.2.1 16 Feb 2018 An update that focussed on improving the look of outputs from this suite, as almost all possible METAR content in all Groups now seems to be recognised correctly. Correction of decimal place error in calculation of SLP for Pakistan and any others quoting to tenths of hPa accuracy. Altimeter pressure changed to prevent that word appearing twice and to recognise Canada uses kiloPascals. Error corrected in condition testing when raw METAR supplied to this suite starts with 'METAR' as it was not recognising subsequent segments. Tiny format correction for Runway Visual Range - minimum and maximum now consistent format. Correction of misbehaving function adjustTime() as it was returning inconsistent times when called in GMT and no adjustment needed. Change so that request for diagnostic output can be passed in via parameters when calling suite. Added new output for when METAR report out of date by 1 to 16 hours (in v. 1.0.4 dealt with over 16 hours out of date), improved output for when more than 24 hours out of date. Changed format of output of Gust speed, so will cope with all units not just knots. 1.2.2 24 Feb 2018 Correction to global variables. Further improvement to outputs. Minor change to function optionalDiagnosticOutput(), now shows the 4 segments to be decoded next, rather than just 3 (so better for multi-segment diagnosis). Change to function unRecognised()now properly coded to be applicable to un-recognised segments in Remarks Group as well as elsewhere. Improvements to output from Runway Visual Range Group, inclusion of "number_format()" for clearer output and of alternative units to match other visi