Source for file Metar.php
Documentation is available at Metar.php
/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4 foldmethod=marker: */
* PEAR::Services_Weather_Metar
* Copyright (c) 2005-2011, Alexander Wirtz
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* o Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* o Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* o Neither the name of the software nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
* @package Services_Weather
* @author Alexander Wirtz <alex@pc4p.net>
* @copyright 2005-2011 Alexander Wirtz
* @license http://www.opensource.org/licenses/bsd-license.php BSD License
* @link http://pear.php.net/package/Services_Weather
* @link http://weather.noaa.gov/weather/metar.shtml
* @link http://weather.noaa.gov/weather/taf.shtml
* @example examples/metar-basic.php metar-basic.php
* @example examples/metar-extensive.php metar-extensive.php
require_once "Services/Weather/Common.php";
// {{{ class Services_Weather_Metar
* This class acts as an interface to the METAR/TAF service of
* weather.noaa.gov. It searches for locations given in ICAO notation and
* retrieves the current weather data.
* Of course the parsing of the METAR-data has its limitations, as it
* follows the Federal Meteorological Handbook No.1 with modifications to
* accomodate for non-US reports, so if the report deviates from these
* standards, you won't get it parsed correctly.
* Anything that is not parsed, is saved in the "noparse" array-entry,
* returned by getWeather(), so you can do your own parsing afterwards. This
* limitation is specifically given for remarks, as the class is not
* processing everything mentioned there, but you will get the most common
* fields like precipitation and temperature-changes. Again, everything not
* parsed, goes into "noparse".
* If you think, some important field is missing or not correctly parsed,
* please file a feature-request/bugreport at http://pear.php.net/ and be
* sure to provide the METAR (or TAF) report with a _detailed_ explanation!
* For working examples, please take a look at
* docs/Services_Weather/examples/metar-basic.php
* docs/Services_Weather/examples/metar-extensive.php
* @package Services_Weather
* @author Alexander Wirtz <alex@pc4p.net>
* @copyright 2005-2011 Alexander Wirtz
* @license http://www.opensource.org/licenses/bsd-license.php BSD License
* @version Release: 1.4.7
* @link http://pear.php.net/package/Services_Weather
* @link http://weather.noaa.gov/weather/metar.shtml
* @link http://weather.noaa.gov/weather/taf.shtml
* @example examples/metar-basic.php metar-basic.php
* @example examples/metar-extensive.php metar-extensive.php
* Information to access the location DB
* @var string $_sourceMetar
* @var string $_sourceTaf
* This path is used to find the METAR data
* @var string $_sourcePathMetar
* This path is used to find the TAF data
* @var string $_sourcePathTaf
function Services_Weather_Metar ($options, &$error)
$this->Services_Weather_Common ($options, $perror);
// Set options accordingly
if (isset ($options["dsn"])) {
if (isset ($options["dbOptions"])) {
$status = $this->setMetarDB($options["dsn"], $options["dbOptions"]);
// Setting the data sources for METAR and TAF - have to watch out for older API usage
if (($source = isset ($options["source"])) || isset ($options["sourceMetar"])) {
$sourceMetar = $source ? $options["source"] : $options["sourceMetar"];
if (($sourcePath = isset ($options["sourcePath"])) || isset ($options["sourcePathMetar"])) {
$sourcePathMetar = $sourcePath ? $options["sourcePath"] : $options["sourcePathMetar"];
if (isset ($options["sourceTaf"])) {
$sourceTaf = $options["sourceTaf"];
if (isset ($option["sourcePathTaf"])) {
$sourcePathTaf = $options["sourcePathTaf"];
$status = $this->setMetarSource($sourceMetar, $sourcePathMetar, $sourceTaf, $sourcePathTaf);
* Sets the parameters needed for connecting to the DB, where the
* location-search is fetching its data from. You need to build a DB
* with the external tool buildMetarDB first, it fetches the locations
* and airports from a NOAA-website.
* @param array $dbOptions
$dsninfo = DB ::parseDSN ($dsn);
if (is_array($dsninfo) && !isset ($dsninfo["mode"])) {
// Initialize connection to DB and store in object if successful
$db = DB ::connect ($dsninfo, $dbOptions);
* Sets the source, where the class tries to locate the METAR/TAF data
* Source can be http, ftp or file.
* Alternate sourcepaths can be provided.
* @param string $sourceMetar
* @param string $sourcePathMetar
* @param string $sourceTaf
* @param string $sourcePathTaf
* @return PEAR_ERROR|bool
* @throws PEAR_Error::SERVICES_WEATHER_ERROR_METAR_SOURCE_INVALID
function setMetarSource($sourceMetar, $sourcePathMetar = "", $sourceTaf = "", $sourcePathTaf = "")
if (in_array($sourceMetar, array ("http", "ftp", "file"))) {
$this->_sourceMetar = $sourceMetar;
// Check for a proper METAR source if parameter is set, if not set use defaults
if (strlen($sourcePathMetar)) {
if (($this->_sourceMetar == "file" && is_dir($sourcePathMetar)) || ($this->_sourceMetar != "file" && parse_url($sourcePathMetar))) {
$this->_sourcePathMetar = $sourcePathMetar;
$this->_sourcePathMetar = "http://weather.noaa.gov/pub/data/observations/metar/stations";
$this->_sourcePathMetar = "ftp://weather.noaa.gov/data/observations/metar/stations";
$this->_sourcePathMetar = ".";
if (in_array($sourceTaf, array ("http", "ftp", "file"))) {
$this->_sourceTaf = $sourceTaf;
} elseif ($sourceTaf != "") {
// Check for a proper TAF source if parameter is set, if not set use defaults
if (($this->_sourceTaf == "file" && is_dir($sourcePathTaf)) || ($this->_sourceTaf != "file" && parse_url($sourcePathTaf))) {
$this->_sourcePathTaf = $sourcePathTaf;
$this->_sourcePathTaf = "http://weather.noaa.gov/pub/data/forecasts/taf/stations";
$this->_sourcePathTaf = "ftp://weather.noaa.gov/data/forecasts/taf/stations";
$this->_sourcePathTaf = ".";
// {{{ _checkLocationID()
* Checks the id for valid values and thus prevents silly requests to
* @return PEAR_Error|bool
* @throws PEAR_Error::SERVICES_WEATHER_ERROR_NO_LOCATION
* @throws PEAR_Error::SERVICES_WEATHER_ERROR_INVALID_LOCATION
function _checkLocationID ($id)
* Downloads the weather- or forecast-data for an id from the server dependant on the datatype and returns it
* @param string $dataType
* @return PEAR_Error|array
* @throws PEAR_Error::SERVICES_WEATHER_ERROR_WRONG_SERVER_DATA
// {{{ _retrieveServerData()
function _retrieveServerData ($id, $dataType) {
switch($this->{"_source". ucfirst($dataType)}) {
// File source is used, get file and read as-is into a string
$source = realpath ($this->{"_sourcePath".ucfirst ($dataType)}. "/". $id. ".TXT");
// HTTP used, acquire request object and fetch data from webserver. Return body of reply
include_once "HTTP/Request.php";
$request = &new HTTP_Request ($this->{"_sourcePath". ucfirst($dataType)}. "/". $id. ".TXT", $this->_httpOptions );
$status = $request->sendRequest ();
$data = $request->getResponseBody ();
// FTP as source, acquire neccessary object first
include_once "Net/FTP.php";
// Parse source to get the server data
// If neccessary options are not set, use defaults
if (!isset ($server["port"]) || $server["port"] == "" || $server["port"] == 0 ) {
if (!isset ($server["user"]) || $server["user"] == "") {
if (!isset ($server["pass"]) || $server["pass"] == "") {
$server["pass"] = "ftp@";
// Instantiate object and connect to server
$ftp = &new Net_FTP ($server["host"], $server["port"], $this->_httpOptions ["timeout"]);
$status = $ftp->connect ();
$status = $ftp->login ($server["user"], $server["pass"]);
// ...and retrieve the data into a temporary file
$tempfile = tempnam("./", "Services_Weather_Metar");
$status = $ftp->get ($server["path"], $tempfile, true , FTP_ASCII );
// Disconnect FTP server, and read data from temporary file
// Split data into an array and return
// {{{ _parseWeatherData()
* Parses the data and caches it
* METAR KPIT 091955Z COR 22015G25KT 3/4SM R28L/2600FT TSRA OVC010CB
* 18/16 A2992 RMK SLP045 T01820159
* @return PEAR_Error|array
* @throws PEAR_Error::SERVICES_WEATHER_ERROR_WRONG_SERVER_DATA
* @throws PEAR_Error::SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION
function _parseWeatherData ($data)
"nsc" => "no significant cloud",
"vv" => "vertical visibility",
"tcu" => "Towering Cumulus",
"clr" => "clear below 12,000 ft"
"0" => "None", "1" => "Cumulus (fair weather)",
"2" => "Cumulus (towering)", "3" => "Cumulonimbus (no anvil)",
"4" => "Stratocumulus (from Cumulus)", "5" => "Stratocumulus (not Cumulus)",
"6" => "Stratus or Fractostratus (fair)", "7" => "Fractocumulus/Fractostratus (bad weather)",
"8" => "Cumulus and Stratocumulus", "9" => "Cumulonimbus (thunderstorm)"
"0" => "None", "1" => "Altostratus (thin)",
"2" => "Altostratus (thick)", "3" => "Altocumulus (thin)",
"4" => "Altocumulus (patchy)", "5" => "Altocumulus (thickening)",
"6" => "Altocumulus (from Cumulus)", "7" => "Altocumulus (w/ Altocumulus, Altostratus, Nimbostratus)",
"8" => "Altocumulus (w/ turrets)", "9" => "Altocumulus (chaotic)"
"0" => "None", "1" => "Cirrus (filaments)",
"2" => "Cirrus (dense)", "3" => "Cirrus (often w/ Cumulonimbus)",
"4" => "Cirrus (thickening)", "5" => "Cirrus/Cirrostratus (low in sky)",
"6" => "Cirrus/Cirrostratus (high in sky)", "7" => "Cirrostratus (entire sky)",
"8" => "Cirrostratus (partial)", "9" => "Cirrocumulus or Cirrocumulus/Cirrus/Cirrostratus"
"+" => "heavy", "-" => "light",
"vc" => "vicinity", "re" => "recent",
"nsw" => "no significant weather",
"mi" => "shallow", "bc" => "patches",
"pr" => "partial", "ts" => "thunderstorm",
"bl" => "blowing", "sh" => "showers",
"dr" => "low drifting", "fz" => "freezing",
"dz" => "drizzle", "ra" => "rain",
"sn" => "snow", "sg" => "snow grains",
"ic" => "ice crystals", "pe" => "ice pellets",
"pl" => "ice pellets", "gr" => "hail",
"gs" => "small hail/snow pellets", "up" => "unknown precipitation",
"br" => "mist", "fg" => "fog",
"fu" => "smoke", "va" => "volcanic ash",
"sa" => "sand", "hz" => "haze",
"py" => "spray", "du" => "widespread dust",
"sq" => "squall", "ss" => "sandstorm",
"ds" => "duststorm", "po" => "well developed dust/sand whirls",
"+fc" => "tornado/waterspout"
"rvrno" => "Runway Visual Range Detector offline",
"pwino" => "Present Weather Identifier offline",
"pno" => "Tipping Bucket Rain Gauge offline",
"fzrano" => "Freezing Rain Sensor offline",
"tsno" => "Lightning Detection System offline",
"visno" => "2nd Visibility Sensor offline",
"chino" => "2nd Ceiling Height Indicator offline"
"report" => "METAR|SPECI",
"update" => "(\d{2})?(\d{4})Z",
"wind" => "(\d{3}|VAR|VRB)(\d{2,3})(G(\d{2,3}))?(FPS|KPH|KT|KTS|MPH|MPS)",
"windVar" => "(\d{3})V(\d{3})",
"visibility" => "(\d{4})|((M|P)?((\d{1,2}|((\d) )?(\d)\/(\d))(SM|KM)))|(CAVOK)",
"runway" => "R(\d{2})(\w)?\/(P|M)?(\d{4})(FT)?(V(P|M)?(\d{4})(FT)?)?(\w)?",
"condition" => "(-|\+|VC|RE|NSW)?(MI|BC|PR|TS|BL|SH|DR|FZ)?((DZ)|(RA)|(SN)|(SG)|(IC)|(PE)|(PL)|(GR)|(GS)|(UP))*(BR|FG|FU|VA|DU|SA|HZ|PY)?(PO|SQ|FC|SS|DS)?",
"clouds" => "(SKC|CLR|NSC|((FEW|SCT|BKN|OVC|VV)(\d{3}|\/{3})(TCU|CB)?))",
"temperature" => "(M)?(\d{2})\/((M)?(\d{2})|XX|\/\/)?",
"pressure" => "(A)(\d{4})|(Q)(\d{4})",
"trend" => "NOSIG|TEMPO|BECMG",
"autostation" => "AO(1|2)",
"presschg" => "PRES(R|F)R",
"seapressure" => "SLP(\d{3}|NO)",
"precip" => "(P|6|7)(\d{4}|\/{4})",
"snowdepth" => "4\/(\d{3})",
"snowequiv" => "933(\d{3})",
"cloudtypes" => "8\/(\d|\/)(\d|\/)(\d|\/)",
"sunduration" => "98(\d{3})",
"1htempdew" => "T(0|1)(\d{3})((0|1)(\d{3}))?",
"6hmaxtemp" => "1(0|1)(\d{3})",
"6hmintemp" => "2(0|1)(\d{3})",
"24htemp" => "4(0|1)(\d{3})(0|1)(\d{3})",
"3hpresstend" => "5([0-8])(\d{3})",
"sensors" => "RVRNO|PWINO|PNO|FZRANO|TSNO|VISNO|CHINO",
if (SERVICES_WEATHER_DEBUG ) {
for ($i = 0; $i < sizeof($data); $i++ ) {
// Eliminate trailing information
for ($i = 0; $i < sizeof($data); $i++ ) {
if (strpos($data[$i], "=") !== false ) {
// Start with parsing the first line for the last update
$weatherData["station"] = "";
$weatherData["dataRaw"] = implode(" ", $data);
$weatherData["updateRaw"] = trim($data[0 ]);
// and prepare the rest for stepping through
// Add a few local variables for data processing
$trendCount = 0; // If we have trends, we need this
$pointer = & $weatherData; // Pointer to the array we add the data to
for ($i = 0; $i < sizeof($metar); $i++ ) {
// Check for whitespace and step loop, if nothing's there
$metar[$i] = trim($metar[$i]);
if (SERVICES_WEATHER_DEBUG ) {
echo "\"". $metar[$i]. "\"". $tab. "-> ";
// Initialize some arrays
foreach ($metarCode as $key => $regexp) {
// Check if current code matches current metar snippet
if (($found = preg_match("/^". $regexp. "$/i", $metar[$i], $result)) == true ) {
$pointer["station"] = $result[0 ];
unset ($metarCode["station"]);
// Parse wind data, first the speed, convert from kt to chosen unit
if ($result[5 ] == "KTS") {
$pointer["wind"] = $this->convertSpeed($result[2 ], $result[5 ], "mph");
if ($result[1 ] == "VAR" || $result[1 ] == "VRB") {
$pointer["windDegrees"] = "Variable";
$pointer["windDirection"] = "Variable";
// Save wind degree and calc direction
$pointer["windDegrees"] = intval($result[1 ]);
$pointer["windDirection"] = $compass[round($result[1 ] / 22.5 ) % 16 ];
$pointer["windGust"] = $this->convertSpeed($result[4 ], $result[5 ], "mph");
// Once more wind, now variability around the current wind-direction
$pointer["windVariability"] = array ("from" => intval($result[1 ]), "to" => intval($result[2 ]));
// Possible fractional visibility here. Check if it matches with the next METAR piece for visibility
if (!isset ($metar[$i + 1 ]) || !preg_match("/^". $metarCode["visibility"]. "$/i", $result[1 ]. " ". $metar[$i + 1 ], $resultVF)) {
// No next METAR piece available or not matching. Match against next METAR code
// Match. Hand over result and advance METAR
if (SERVICES_WEATHER_DEBUG ) {
echo "\"". $result[1 ]. " ". $metar[$i + 1 ]. "\"". str_repeat("\t", 2 - floor((strlen($result[1 ]. " ". $metar[$i + 1 ]) + 2 ) / 8 )). "-> ";
$pointer["visQualifier"] = "AT";
if (is_numeric($result[1 ]) && ($result[1 ] == 9999 )) {
// Upper limit of visibility range
$pointer["visQualifier"] = "BEYOND";
// 4-digit visibility in m
} elseif (!isset ($result[11 ]) || $result[11 ] != "CAVOK") {
$pointer["visQualifier"] = "BELOW";
} elseif ($result[3 ] == "P") {
$pointer["visQualifier"] = "BEYOND";
// visibility as one/two-digit number
// the y/z part, add if we had a x part (see visibility1)
$visibility = $this->convertDistance($result[7 ] + $result[8 ] / $result[9 ], $result[10 ], "sm");
$visibility = $this->convertDistance($result[8 ] / $result[9 ], $result[10 ], "sm");
$pointer["visQualifier"] = "BEYOND";
$pointer["clouds"] = array (array ("amount" => "Clear below", "height" => 5000 ));
$pointer["condition"] = "no significant weather";
$pointer["visibility"] = $visibility;
// First some basic setups
if (!isset ($pointer["condition"])) {
$pointer["condition"] = "";
} elseif (strlen($pointer["condition"]) > 0 ) {
$pointer["condition"] .= ",";
// First try matching the complete string
$pointer["condition"] .= " ". $conditions[strtolower($result[0 ])];
// No luck, match part by part
foreach ($result as $condition) {
$pointer["condition"] .= " ". $conditions[strtolower($condition)];
$pointer["condition"] = trim($pointer["condition"]);
if (!isset ($pointer["clouds"])) {
$pointer["clouds"] = array ();
// Only amount and height
$cloud = array ("amount" => $clouds[strtolower($result[3 ])]);
if ($result[4 ] == "///") {
$cloud["height"] = "station level or below";
$cloud["height"] = $result[4 ] * 100;
} elseif (sizeof($result) == 6 ) {
// Amount, height and type
$cloud = array ("amount" => $clouds[strtolower($result[3 ])], "type" => $clouds[strtolower($result[5 ])]);
if ($result[4 ] == "///") {
$cloud["height"] = "station level or below";
$cloud["height"] = $result[4 ] * 100;
$cloud = array ("amount" => $clouds[strtolower($result[0 ])]);
$pointer["clouds"][] = $cloud;
// normal temperature in first part
if (isset ($pointer["wind"])) {
// Now calculate windchill from temperature and windspeed
$pointer["feltTemperature"] = $this->calculateWindChill($pointer["temperature"], $pointer["wind"]);
// Pressure provided in inches
$pointer["pressure"] = $result[2 ] / 100;
} elseif ($result[3 ] == "Q") {
// We may have a trend here... extract type and set pointer on
if (!isset ($weatherData["trend"])) {
$weatherData["trend"] = array ();
$weatherData["trend"][$trendCount] = array ();
$pointer = & $weatherData["trend"][$trendCount];
$pointer["type"] = $result[0 ];
while (isset ($metar[$i + 1 ]) && preg_match("/^(FM|TL|AT)(\d{2})(\d{2})$/i", $metar[$i + 1 ], $lresult)) {
if ($lresult[1 ] == "FM") {
$pointer["from"] = $lresult[2 ]. ":". $lresult[3 ];
} elseif ($lresult[1 ] == "TL") {
$pointer["to"] = $lresult[2 ]. ":". $lresult[3 ];
$pointer["at"] = $lresult[2 ]. ":". $lresult[3 ];
// As we have just extracted the time for this trend
// from our METAR, increase field-counter
$weatherData["remark"] = array ();
// Which autostation do we have here?
$weatherData["remark"]["autostation"] = "Automatic weatherstation w/o precipitation discriminator";
$weatherData["remark"]["autostation"] = "Automatic weatherstation w/ precipitation discriminator";
unset ($metarCode["autostation"]);
// Decoding for rapid pressure changes
$weatherData["remark"]["presschg"] = "Pressure rising rapidly";
$weatherData["remark"]["presschg"] = "Pressure falling rapidly";
unset ($metarCode["presschg"]);
// Pressure at sea level (delivered in hpa)
// Decoding is a bit obscure as 982 gets 998.2
// whereas 113 becomes 1113 -> no real rule here
$press = 900 + round($result[1 ] / 100 , 1 );
$press = 1000 + $result[1 ];
$weatherData["remark"]["seapressure"] = $this->convertPressure($press, "hpa", "in");
unset ($metarCode["seapressure"]);
// Precipitation in inches
if (!isset ($weatherData["precipitation"])) {
$weatherData["precipitation"] = array ();
$hours = array ("P" => "1", "6" => "3/6", "7" => "24");
$precip = "indeterminable";
} elseif ($result[2 ] == "0000") {
$precip = $result[2 ] / 100;
$weatherData["precipitation"][] = array (
"hours" => $hours[$result[1 ]]
$weatherData["remark"]["snowdepth"] = $result[1 ];
unset ($metarCode["snowdepth"]);
// Same for equivalent in Water... (inches)
$weatherData["remark"]["snowequiv"] = $result[1 ] / 10;
unset ($metarCode["snowequiv"]);
$weatherData["remark"]["cloudtypes"] = array (
"low" => $cloudtypes["low"][$result[1 ]],
"middle" => $cloudtypes["middle"][$result[2 ]],
"high" => $cloudtypes["high"][$result[3 ]]
unset ($metarCode["cloudtypes"]);
// Duration of sunshine (in minutes)
$weatherData["remark"]["sunduration"] = "Total minutes of sunshine: ". $result[1 ];
unset ($metarCode["sunduration"]);
// Temperatures in the last hour in C
unset ($metarCode["1htempdew"]);
// Max temperature in the last 6 hours in C
$weatherData["remark"]["6hmaxtemp"] = $this->convertTemperature($result[2 ] / 10 , "c", "f");
unset ($metarCode["6hmaxtemp"]);
// Min temperature in the last 6 hours in C
$weatherData["remark"]["6hmintemp"] = $this->convertTemperature($result[2 ] / 10 , "c", "f");
unset ($metarCode["6hmintemp"]);
// Max/Min temperatures in the last 24 hours in C
$weatherData["remark"]["24hmaxtemp"] = $this->convertTemperature($result[2 ] / 10 , "c", "f");
$weatherData["remark"]["24hmintemp"] = $this->convertTemperature($result[4 ] / 10 , "c", "f");
unset ($metarCode["24htemp"]);
// Pressure tendency of the last 3 hours
// no special processing, just passing the data
$weatherData["remark"]["3hpresstend"] = array (
"presscode" => $result[1 ],
unset ($metarCode["3hpresstend"]);
// No change during the last hour
$weatherData["remark"]["nospeci"] = "No changes in weather conditions";
unset ($metarCode["nospeci"]);
// We may have multiple broken sensors, so do not unset
if (!isset ($weatherData["remark"]["sensors"])) {
$weatherData["remark"]["sensors"] = array ();
$weatherData["remark"]["maintain"] = "Maintainance needed";
unset ($metarCode["maintain"]);
// Do nothing, just prevent further matching
if ($found && !SERVICES_WEATHER_DEBUG ) {
} elseif ($found && SERVICES_WEATHER_DEBUG ) {
if (SERVICES_WEATHER_DEBUG ) {
if (!isset ($weatherData["noparse"])) {
$weatherData["noparse"] = array ();
$weatherData["noparse"][] = $metar[$i];
if (isset ($weatherData["noparse"])) {
$weatherData["noparse"] = implode(" ", $weatherData["noparse"]);
// {{{ _parseForecastData()
* Parses the data and caches it
* TAF KLGA 271734Z 271818 11007KT P6SM -RA SCT020 BKN200
* FM2300 14007KT P6SM SCT030 BKN150
* FM0400 VRB03KT P6SM SCT035 OVC080 PROB30 0509 P6SM -RA BKN035
* FM0900 VRB03KT 6SM -RA BR SCT015 OVC035
* TEMPO 1215 5SM -RA BR SCT009 BKN015
* BECMG 1517 16007KT P6SM NSW SCT015 BKN070
* @return PEAR_Error|array
* @throws PEAR_Error::SERVICES_WEATHER_ERROR_WRONG_SERVER_DATA
* @throws PEAR_Error::SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION
function _parseForecastData ($data)
"nsc" => "no significant cloud",
"vv" => "vertical visibility",
"tcu" => "Towering Cumulus",
"clr" => "clear below 12,000 ft"
"+" => "heavy", "-" => "light",
"vc" => "vicinity", "re" => "recent",
"nsw" => "no significant weather",
"mi" => "shallow", "bc" => "patches",
"pr" => "partial", "ts" => "thunderstorm",
"bl" => "blowing", "sh" => "showers",
"dr" => "low drifting", "fz" => "freezing",
"dz" => "drizzle", "ra" => "rain",
"sn" => "snow", "sg" => "snow grains",
"ic" => "ice crystals", "pe" => "ice pellets",
"pl" => "ice pellets", "gr" => "hail",
"gs" => "small hail/snow pellets", "up" => "unknown precipitation",
"br" => "mist", "fg" => "fog",
"fu" => "smoke", "va" => "volcanic ash",
"sa" => "sand", "hz" => "haze",
"py" => "spray", "du" => "widespread dust",
"sq" => "squall", "ss" => "sandstorm",
"ds" => "duststorm", "po" => "well developed dust/sand whirls",
"+fc" => "tornado/waterspout"
"update" => "(\d{2})?(\d{4})Z",
"valid" => "(\d{2})(\d{2})\/(\d{2})(\d{2})",
"wind" => "(\d{3}|VAR|VRB)(\d{2,3})(G(\d{2,3}))?(FPS|KPH|KT|KTS|MPH|MPS)",
"visibility" => "(\d{4})|((M|P)?((\d{1,2}|((\d) )?(\d)\/(\d))(SM|KM)))|(CAVOK)",
"condition" => "(-|\+|VC|RE|NSW)?(MI|BC|PR|TS|BL|SH|DR|FZ)?((DZ)|(RA)|(SN)|(SG)|(IC)|(PE)|(PL)|(GR)|(GS)|(UP))*(BR|FG|FU|VA|DU|SA|HZ|PY)?(PO|SQ|FC|SS|DS)?",
"clouds" => "(SKC|CLR|NSC|((FEW|SCT|BKN|OVC|VV)(\d{3}|\/{3})(TCU|CB)?))",
"windshear" => "WS(\d{3})\/(\d{3})(\d{2,3})(FPS|KPH|KT|KTS|MPH|MPS)",
"tempmax" => "TX(\d{2})\/(\d{2})(\w)",
"tempmin" => "TN(\d{2})\/(\d{2})(\w)",
"tempmaxmin" => "TX(\d{2})\/(\d{2})(\w)TN(\d{2})\/(\d{2})(\w)",
"from" => "FM(\d{2})(\d{2})(\d{2})?Z?",
"fmc" => "(PROB|BECMG|TEMPO)(\d{2})?"
if (SERVICES_WEATHER_DEBUG ) {
for ($i = 0; $i < sizeof($data); $i++ ) {
// Eliminate trailing information
for ($i = 0; $i < sizeof($data); $i++ ) {
if (strpos($data[$i], "=") !== false ) {
// Ok, we have correct data, start with parsing the first line for the last update
$forecastData["station"] = "";
$forecastData["dataRaw"] = implode(" ", $data);
$forecastData["updateRaw"] = trim($data[0 ]);
// and prepare the rest for stepping through
// Add a few local variables for data processing
$fromTime = ""; // The timeperiod the data gets added to
$fmcCount = 0; // If we have FMCs (Forecast Meteorological Conditions), we need this
$pointer = & $forecastData; // Pointer to the array we add the data to
for ($i = 0; $i < sizeof($taf); $i++ ) {
// Check for whitespace and step loop, if nothing's there
$taf[$i] = trim($taf[$i]);
if (SERVICES_WEATHER_DEBUG ) {
echo "\"". $taf[$i]. "\"". $tab. "-> ";
// Initialize some arrays
foreach ($tafCode as $key => $regexp) {
// Check if current code matches current taf snippet
if (($found = preg_match("/^". $regexp. "$/i", $taf[$i], $result)) == true ) {
$pointer["station"] = $result[0 ];
unset ($tafCode["station"]);
$pointer["validRaw"] = $result[0 ];
// Generates the timeperiod the report is valid for
list ($year, $month, $day) = explode("-", gmdate("Y-m-d", $forecastData["update"]));
$pointer["validFrom"] = gmmktime($result[2 ], 0 , 0 , $month, $result[1 ], $year);
$pointer["validTo"] = gmmktime($result[4 ], 0 , 0 , $month, $result[3 ], $year);
unset ($tafCode["valid"]);
// Now the groups will start, so initialize the time groups
$pointer["time"] = array ();
$fromTime = $result[2 ]. ":00";
$pointer["time"][$fromTime] = array ();
// Set pointer to the first timeperiod
$pointer = & $pointer["time"][$fromTime];
// Parse wind data, first the speed, convert from kt to chosen unit
if ($result[5 ] == "KTS") {
$pointer["wind"] = $this->convertSpeed($result[2 ], $result[5 ], "mph");
if ($result[1 ] == "VAR" || $result[1 ] == "VRB") {
$pointer["windDegrees"] = "Variable";
$pointer["windDirection"] = "Variable";
// Save wind degree and calc direction
$pointer["windDegrees"] = $result[1 ];
$pointer["windDirection"] = $compass[round($result[1 ] / 22.5 ) % 16 ];
$pointer["windGust"] = $this->convertSpeed($result[4 ], $result[5 ], "mph");
if (isset ($probability)) {
$pointer["windProb"] = $probability;
// Possible fractional visibility here. Check if it matches with the next TAF piece for visibility
if (!isset ($taf[$i + 1 ]) || !preg_match("/^". $tafCode["visibility"]. "$/i", $result[1 ]. " ". $taf[$i + 1 ], $resultVF)) {
// No next TAF piece available or not matching. Match against next TAF code
// Match. Hand over result and advance TAF
if (SERVICES_WEATHER_DEBUG ) {
echo "\"". $result[1 ]. " ". $taf[$i + 1 ]. "\"". str_repeat("\t", 2 - floor((strlen($result[1 ]. " ". $taf[$i + 1 ]) + 2 ) / 8 )). "-> ";
$pointer["visQualifier"] = "AT";
if (is_numeric($result[1 ]) && ($result[1 ] == 9999 )) {
// Upper limit of visibility range
$pointer["visQualifier"] = "BEYOND";
// 4-digit visibility in m
} elseif (!isset ($result[11 ]) || $result[11 ] != "CAVOK") {
$pointer["visQualifier"] = "BELOW";
} elseif ($result[3 ] == "P") {
$pointer["visQualifier"] = "BEYOND";
// visibility as one/two-digit number
// the y/z part, add if we had a x part (see visibility1)
$visibility = $this->convertDistance($result[7 ] + $result[8 ] / $result[9 ], $result[10 ], "sm");
$visibility = $this->convertDistance($result[8 ] / $result[9 ], $result[10 ], "sm");
$pointer["visQualifier"] = "BEYOND";
$pointer["clouds"] = array (array ("amount" => "Clear below", "height" => 5000 ));
$pointer["condition"] = "no significant weather";
if (isset ($probability)) {
$pointer["visProb"] = $probability;
$pointer["visibility"] = $visibility;
// First some basic setups
if (!isset ($pointer["condition"])) {
$pointer["condition"] = "";
} elseif (strlen($pointer["condition"]) > 0 ) {
$pointer["condition"] .= ",";
// First try matching the complete string
$pointer["condition"] .= " ". $conditions[strtolower($result[0 ])];
// No luck, match part by part
foreach ($result as $condition) {
$pointer["condition"] .= " ". $conditions[strtolower($condition)];
$pointer["condition"] = trim($pointer["condition"]);
if (isset ($probability)) {
$pointer["condition"] .= " (". $probability. "% prob.)";
if (!isset ($pointer["clouds"])) {
$pointer["clouds"] = array ();
// Only amount and height
$cloud = array ("amount" => $clouds[strtolower($result[3 ])]);
if ($result[4 ] == "///") {
$cloud["height"] = "station level or below";
$cloud["height"] = $result[4 ] * 100;
} elseif (sizeof($result) == 6 ) {
// Amount, height and type
$cloud = array ("amount" => $clouds[strtolower($result[3 ])], "type" => $clouds[strtolower($result[5 ])]);
if ($result[4 ] == "///") {
$cloud["height"] = "station level or below";
$cloud["height"] = $result[4 ] * 100;
$cloud = array ("amount" => $clouds[strtolower($result[0 ])]);
if (isset ($probability)) {
$cloud["prob"] = $probability;
$pointer["clouds"][] = $cloud;
// Parse windshear, if available
if ($result[4 ] == "KTS") {
$pointer["windshear"] = $this->convertSpeed($result[3 ], $result[4 ], "mph");
$pointer["windshearHeight"] = $result[1 ] * 100;
$pointer["windshearDegrees"] = $result[2 ];
$pointer["windshearDirection"] = $compass[round($result[2 ] / 22.5 ) % 16 ];
// Parse max/min temperature
// Next timeperiod is coming up, prepare array and
// set pointer accordingly
$fromTime = $result[2 ]. ":". $result[3 ];
// The Australian way (Hey mates!)
$fromTime = $result[1 ]. ":00";
$forecastData["time"][$fromTime] = array ();
$pointer = & $forecastData["time"][$fromTime];
// Test, if this is a probability for the next FMC
if (isset ($result[2 ]) && preg_match("/^BECMG|TEMPO$/i", $taf[$i + 1 ], $lresult)) {
// Set type to BECMG or TEMPO
$probability = $result[2 ];
// Now extract time for this group
if (preg_match("/^(\d{2})(\d{2})$/i", $taf[$i + 2 ], $lresult)) {
$from = $lresult[1 ]. ":00";
$to = ($to == "24:00") ? "00:00" : $to;
// As we now have type, probability and time for this FMC
// from our TAF, increase field-counter
// No timegroup present, so just increase field-counter by one
} elseif (preg_match("/^(\d{2})(\d{2})\/(\d{2})(\d{2})$/i", $taf[$i + 1 ], $lresult)) {
// Normal group, set type and use extracted time
$probability = $result[2 ];
$from = $lresult[2 ]. ":00";
$to = ($to == "24:00") ? "00:00" : $to;
// Same as above, we have a time for this FMC from our TAF,
// increase field-counter
} elseif (isset ($result[2 ])) {
// This is either a PROBdd or a malformed TAF with missing timegroup
$probability = $result[2 ];
// Handle the FMC, generate neccessary array if it's the first...
if (!isset ($forecastData["time"][$fromTime]["fmc"])) {
$forecastData["time"][$fromTime]["fmc"] = array ();
$forecastData["time"][$fromTime]["fmc"][$fmcCount] = array ();
$pointer = & $forecastData["time"][$fromTime]["fmc"][$fmcCount];
$pointer["type"] = $type;
$pointer["from"] = $from;
if (isset ($probability)) {
$pointer["probability"] = $probability;
if ($found && !SERVICES_WEATHER_DEBUG ) {
} elseif ($found && SERVICES_WEATHER_DEBUG ) {
if (SERVICES_WEATHER_DEBUG ) {
if (!isset ($forecastData["noparse"])) {
$forecastData["noparse"] = array ();
$forecastData["noparse"][] = $taf[$i];
if (isset ($forecastData["noparse"])) {
$forecastData["noparse"] = implode(" ", $forecastData["noparse"]);
* Converts the data in the return array to the desired units and/or
* @param string $location
function _convertReturn (&$target, $units, $location)
foreach ($target as $key => $val) {
// Another array detected, so recurse into it to convert the units
$this->_convertReturn ($target[$key], $units, $location);
$newVal = $location["name"];
$newVal = gmdate(trim($this->_dateFormat. " ". $this->_timeFormat ), $val);
$newVal = round($val, 1 );
* Searches IDs for given location, returns array of possible locations
* @param string|array $location
* @param bool $useFirst If set, first ID of result-array is returned
* @return PEAR_Error|array|string
* @throws PEAR_Error::SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION
* @throws PEAR_Error::SERVICES_WEATHER_ERROR_DB_NOT_CONNECTED
* @throws PEAR_Error::SERVICES_WEATHER_ERROR_INVALID_LOCATION
if (!isset ($this->_db ) || !DB ::isConnection ($this->_db )) {
// Try to part search string in name, state and country part
// and build where clause from it for the select
$location = explode(",", $location);
// Trim, caps-low and quote the strings
for ($i = 0; $i < sizeof($location); $i++ ) {
$location[$i] = $this->_db ->quote ("%". strtolower(trim($location[$i])). "%");
$where = "LOWER(name) LIKE ". $location[0 ];
} elseif (sizeof($location) == 2 ) {
$where = "LOWER(name) LIKE ". $location[0 ];
$where .= " AND LOWER(country) LIKE ". $location[1 ];
} elseif (sizeof($location) == 3 ) {
$where = "LOWER(name) LIKE ". $location[0 ];
$where .= " AND LOWER(state) LIKE ". $location[1 ];
$where .= " AND LOWER(country) LIKE ". $location[2 ];
} elseif (sizeof($location) == 4 ) {
$where = "LOWER(name) LIKE ". substr($location[0 ], 0 , -2 ). ", ". substr($location[1 ], 2 );
$where .= " AND LOWER(state) LIKE ". $location[2 ];
$where .= " AND LOWER(country) LIKE ". $location[3 ];
// Create select, locations with ICAO first
$select = "SELECT icao, name, state, country, latitude, longitude ".
$result = $this->_db ->query ($select);
// Check result for validity
if (DB ::isError ($result)) {
// Result is valid, start preparing the return
while (($row = $result->fetchRow (DB_FETCHMODE_ASSOC )) != null ) {
// First the name of the location
$locname = $row["name"]. ", ". $row["country"];
$locname = $row["name"]. ", ". $row["state"]. ", ". $row["country"];
if ($locicao != "----") {
// We have a location with ICAO
$icao[$locicao] = $locname;
// No ICAO, try finding the nearest airport
$locicao = $this->searchAirport($row["latitude"], $row["longitude"]);
if (!isset ($icao[$locicao])) {
$icao[$locicao] = $locname;
// Only one result? Return as string
if (sizeof($icao) == 1 || $useFirst) {
// Location was provided as coordinates, search nearest airport
// {{{ searchLocationByCountry()
* Returns IDs with location-name for a given country or all available
* countries, if no value was given
* @return PEAR_Error|array
* @throws PEAR_Error::SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION
* @throws PEAR_Error::SERVICES_WEATHER_ERROR_DB_NOT_CONNECTED
* @throws PEAR_Error::SERVICES_WEATHER_ERROR_WRONG_SERVER_DATA
if (!isset ($this->_db ) || !DB ::isConnection ($this->_db )) {
// Return the available countries as no country was given
$select = "SELECT DISTINCT(country) ".
$countries = $this->_db ->getCol ($select);
// As $countries is either an error or the true result,
// Now for the real search
$select = "SELECT icao, name, state, country ".
$result = $this->_db ->query ($select);
// Check result for validity
if (DB ::isError ($result)) {
while (($row = $result->fetchRow (DB_FETCHMODE_ASSOC )) != null ) {
if ($locicao != "----") {
// First the name of the location
$locname = $row["name"]. ", ". $row["country"];
$locname = $row["name"]. ", ". $row["state"]. ", ". $row["country"];
$locations[$locicao] = $locname;
* Searches the nearest airport(s) for given coordinates, returns array
* @param float $longitude
* @return PEAR_Error|array|string
* @throws PEAR_Error::SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION
* @throws PEAR_Error::SERVICES_WEATHER_ERROR_DB_NOT_CONNECTED
* @throws PEAR_Error::SERVICES_WEATHER_ERROR_INVALID_LOCATION
if (!isset ($this->_db ) || !DB ::isConnection ($this->_db )) {
$select = "SELECT icao, x, y, z FROM metarAirports";
$result = $this->_db ->query ($select);
if (DB ::isError ($result)) {
// Result is valid, start search
$search = array ("dist" => array (), "icao" => array ());
while (($row = $result->fetchRow (DB_FETCHMODE_ASSOC )) != null ) {
$air = array ($row["x"], $row["y"], $row["z"]);
// Calculate distance of query and current airport
// break off, if distance is larger than current $min_dist
for($d; $d < sizeof($air); $d++ ) {
$t = $air[$d] - $query[$d];
if ($min_dist != null && $dist > $min_dist) {
// Ok, current airport is one of the nearer locations
$search["dist"][] = $dist;
$search["icao"][] = $icao;
// Sort array for distance
array_multisort($search["dist"], SORT_NUMERIC , SORT_ASC , $search["icao"], SORT_STRING , SORT_ASC );
// If array is larger then desired results, chop off last one
if (sizeof($search["dist"]) > $numResults) {
$min_dist = max($search["dist"]);
// Only one result wanted, return as string
return $search["icao"][0 ];
} elseif ($numResults > 1 ) {
// Return found locations
* Returns the data for the location belonging to the ID
* @return PEAR_Error|array
$status = $this->_checkLocationID ($id);
$locationReturn = array ();
if ($this->_cacheEnabled && ($location = $this->_getCache ("METAR-". $id, "location"))) {
$this->_location = $location;
$locationReturn["cache"] = "HIT";
} elseif (isset ($this->_db ) && DB ::isConnection ($this->_db )) {
$select = "SELECT icao, name, state, country, latitude, longitude, elevation ".
"FROM metarAirports WHERE icao='". $id. "'";
$result = $this->_db ->query ($select);
if (DB ::isError ($result)) {
// Result is ok, put things into object
$this->_location = $result->fetchRow (DB_FETCHMODE_ASSOC );
if ($this->_cacheEnabled ) {
$this->_saveCache ("METAR-". $id, $this->_location , "", "location");
$locationReturn["cache"] = "MISS";
$this->_location = array (
// Stuff name-string together
if (strlen($this->_location ["state"]) && strlen($this->_location ["country"])) {
$locname = $this->_location ["name"]. ", ". $this->_location ["state"]. ", ". $this->_location ["country"];
} elseif (strlen($this->_location ["country"])) {
$locname = $this->_location ["name"]. ", ". $this->_location ["country"];
$locname = $this->_location ["name"];
$locationReturn["name"] = $locname;
$locationReturn["latitude"] = $this->_location ["latitude"];
$locationReturn["longitude"] = $this->_location ["longitude"];
$locationReturn["elevation"] = $this->_location ["elevation"];
* Returns the weather-data for the supplied location
* @param string $unitsFormat
* @return PHP_Error|array
$status = $this->_checkLocationID ($id);
if ($this->_cacheEnabled && ($weather = $this->_getCache ("METAR-". $id, "weather"))) {
// Wee... it was cached, let's have it...
$weatherReturn = $weather;
$this->_weather = $weatherReturn;
$weatherReturn["cache"] = "HIT";
$weatherData = $this->_retrieveServerData ($id, "metar");
$weatherReturn = $this->_parseWeatherData ($weatherData);
// Add an icon for the current conditions
// Determine if certain values are set, if not use defaults
$condition = isset ($weatherReturn["condition"]) ? $weatherReturn["condition"] : "No Significant Weather";
$clouds = isset ($weatherReturn["clouds"]) ? $weatherReturn["clouds"] : array ();
$wind = isset ($weatherReturn["wind"]) ? $weatherReturn["wind"] : 5;
$temperature = isset ($weatherReturn["temperature"]) ? $weatherReturn["temperature"] : 70;
$latitude = isset ($location["latitude"]) ? $location["latitude"] : -360;
$longitude = isset ($location["longitude"]) ? $location["longitude"] : -360;
$weatherReturn["conditionIcon"] = $this->getWeatherIcon($condition, $clouds, $wind, $temperature, $latitude, $longitude, strtotime($weatherReturn["updateRaw"]. " GMT"));
// Calculate the moon phase and age
$weatherReturn["moon"] = $moon["phase"];
$weatherReturn["moonIcon"] = $moon["icon"];
if ($this->_cacheEnabled ) {
$this->_saveCache ("METAR-". $id, $weatherReturn, $unitsFormat, "weather");
$this->_weather = $weatherReturn;
$weatherReturn["cache"] = "MISS";
$this->_convertReturn ($weatherReturn, $units, $location);
* METAR provides no forecast per se, we use the TAF reports to generate
* a forecast for the announced timeperiod
* @param int $days Ignored, not applicable
* @param string $unitsFormat
* @return PEAR_Error|array
function getForecast($id = "", $days = null , $unitsFormat = "")
$status = $this->_checkLocationID ($id);
if ($this->_cacheEnabled && ($forecast = $this->_getCache ("METAR-". $id, "forecast"))) {
// Wee... it was cached, let's have it...
$forecastReturn = $forecast;
$this->_forecast = $forecastReturn;
$forecastReturn["cache"] = "HIT";
$forecastData = $this->_retrieveServerData ($id, "taf");
$forecastReturn = $this->_parseForecastData ($forecastData);
if ($this->_cacheEnabled ) {
$this->_saveCache ("METAR-". $id, $forecastReturn, $unitsFormat, "forecast");
$this->_forecast = $forecastReturn;
$forecastReturn["cache"] = "MISS";
$this->_convertReturn ($forecastReturn, $units, $location);
Documentation generated on Mon, 11 Mar 2019 15:50:59 -0400 by phpDocumentor 1.4.4. PEAR Logo Copyright © PHP Group 2004.
|