* Copyright (c) 2005-2011, Alexander Wirtz * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 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. * * * @category Web Services * @package Services_Weather * @author Alexander Wirtz * @copyright 2005-2011 Alexander Wirtz * @license http://www.opensource.org/licenses/bsd-license.php BSD License * @version CVS: $Id$ * @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 * @filesource */ require_once "Services/Weather/Common.php"; require_once "DB.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 * * * @category Web Services * @package Services_Weather * @author Alexander Wirtz * @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 */ class Services_Weather_Metar extends Services_Weather_Common { // {{{ properties /** * Information to access the location DB * * @var object DB $_db * @access private */ var $_db; /** * The source METAR uses * * @var string $_sourceMetar * @access private */ var $_sourceMetar; /** * The source TAF uses * * @var string $_sourceTaf * @access private */ var $_sourceTaf; /** * This path is used to find the METAR data * * @var string $_sourcePathMetar * @access private */ var $_sourcePathMetar; /** * This path is used to find the TAF data * * @var string $_sourcePathTaf * @access private */ var $_sourcePathTaf; // }}} // {{{ constructor /** * Constructor * * @param array $options * @param mixed $error * @throws PEAR_Error * @access private */ function Services_Weather_Metar($options, &$error) { $perror = null; $this->Services_Weather_Common($options, $perror); if (Services_Weather::isError($perror)) { $error = $perror; return; } // Set options accordingly $status = null; if (isset($options["dsn"])) { if (isset($options["dbOptions"])) { $status = $this->setMetarDB($options["dsn"], $options["dbOptions"]); } else { $status = $this->setMetarDB($options["dsn"]); } } if (Services_Weather::isError($status)) { $error = $status; return; } // 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"]; } else { $sourcePathMetar = ""; } } else { $sourceMetar = "http"; $sourcePathMetar = ""; } if (isset($options["sourceTaf"])) { $sourceTaf = $options["sourceTaf"]; if (isset($option["sourcePathTaf"])) { $sourcePathTaf = $options["sourcePathTaf"]; } else { $soucePathTaf = ""; } } else { $sourceTaf = "http"; $sourcePathTaf = ""; } $status = $this->setMetarSource($sourceMetar, $sourcePathMetar, $sourceTaf, $sourcePathTaf); if (Services_Weather::isError($status)) { $error = $status; return; } } // }}} // {{{ setMetarDB() /** * 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 string $dsn * @param array $dbOptions * @return DB_Error|bool * @throws DB_Error * @see DB::parseDSN * @access public */ function setMetarDB($dsn, $dbOptions = array()) { $dsninfo = DB::parseDSN($dsn); if (is_array($dsninfo) && !isset($dsninfo["mode"])) { $dsninfo["mode"]= 0644; } // Initialize connection to DB and store in object if successful $db = DB::connect($dsninfo, $dbOptions); if (DB::isError($db)) { return $db; } $this->_db = $db; return true; } // }}} // {{{ setMetarSource() /** * 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 * @access public */ function setMetarSource($sourceMetar, $sourcePathMetar = "", $sourceTaf = "", $sourcePathTaf = "") { if (in_array($sourceMetar, array("http", "ftp", "file"))) { $this->_sourceMetar = $sourceMetar; } else { return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_METAR_SOURCE_INVALID, __FILE__, __LINE__); } // Check for a proper METAR source if parameter is set, if not set use defaults clearstatcache(); if (strlen($sourcePathMetar)) { if (($this->_sourceMetar == "file" && is_dir($sourcePathMetar)) || ($this->_sourceMetar != "file" && parse_url($sourcePathMetar))) { $this->_sourcePathMetar = $sourcePathMetar; } else { return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_METAR_SOURCE_INVALID, __FILE__, __LINE__); } } else { switch ($sourceMetar) { case "http": $this->_sourcePathMetar = "http://weather.noaa.gov/pub/data/observations/metar/stations"; break; case "ftp": $this->_sourcePathMetar = "ftp://weather.noaa.gov/data/observations/metar/stations"; break; case "file": $this->_sourcePathMetar = "."; break; } } if (in_array($sourceTaf, array("http", "ftp", "file"))) { $this->_sourceTaf = $sourceTaf; } elseif ($sourceTaf != "") { return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_METAR_SOURCE_INVALID, __FILE__, __LINE__); } // Check for a proper TAF source if parameter is set, if not set use defaults clearstatcache(); if (strlen($sourcePathTaf)) { if (($this->_sourceTaf == "file" && is_dir($sourcePathTaf)) || ($this->_sourceTaf != "file" && parse_url($sourcePathTaf))) { $this->_sourcePathTaf = $sourcePathTaf; } else { return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_METAR_SOURCE_INVALID, __FILE__, __LINE__); } } else { switch ($sourceTaf) { case "http": $this->_sourcePathTaf = "http://weather.noaa.gov/pub/data/forecasts/taf/stations"; break; case "ftp": $this->_sourcePathTaf = "ftp://weather.noaa.gov/data/forecasts/taf/stations"; break; case "file": $this->_sourcePathTaf = "."; break; } } return true; } // }}} // {{{ _checkLocationID() /** * Checks the id for valid values and thus prevents silly requests to * METAR server * * @param string $id * @return PEAR_Error|bool * @throws PEAR_Error::SERVICES_WEATHER_ERROR_NO_LOCATION * @throws PEAR_Error::SERVICES_WEATHER_ERROR_INVALID_LOCATION * @access private */ function _checkLocationID($id) { if (is_array($id) || is_object($id) || !strlen($id)) { return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_NO_LOCATION, __FILE__, __LINE__); } elseif (!ctype_alnum($id) || (strlen($id) > 4)) { return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_INVALID_LOCATION, __FILE__, __LINE__); } return true; } // }}} /** * Downloads the weather- or forecast-data for an id from the server dependant on the datatype and returns it * * @param string $id * @param string $dataType * @return PEAR_Error|array * @throws PEAR_Error::SERVICES_WEATHER_ERROR_WRONG_SERVER_DATA * @access private */ // {{{ _retrieveServerData() function _retrieveServerData($id, $dataType) { switch($this->{"_source".ucfirst($dataType)}) { case "file": // File source is used, get file and read as-is into a string $source = realpath($this->{"_sourcePath".ucfirst($dataType)}."/".$id.".TXT"); $data = @file_get_contents($source); if ($data === false) { return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_WRONG_SERVER_DATA, __FILE__, __LINE__); } break; case "http": // 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(); if (Services_Weather::isError($status) || (int) $request->getResponseCode() <> 200) { return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_WRONG_SERVER_DATA, __FILE__, __LINE__); } $data = $request->getResponseBody(); break; case "ftp": // FTP as source, acquire neccessary object first include_once "Net/FTP.php"; // Parse source to get the server data $server = parse_url($this->{"_sourcePath".ucfirst($dataType)}."/".$id.".TXT"); // If neccessary options are not set, use defaults if (!isset($server["port"]) || $server["port"] == "" || $server["port"] == 0) { $server["port"] = 21; } if (!isset($server["user"]) || $server["user"] == "") { $server["user"] = "ftp"; } 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(); if (Services_Weather::isError($status)) { return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_WRONG_SERVER_DATA, __FILE__, __LINE__); } // Login to server... $status = $ftp->login($server["user"], $server["pass"]); if (Services_Weather::isError($status)) { return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_WRONG_SERVER_DATA, __FILE__, __LINE__); } // ...and retrieve the data into a temporary file $tempfile = tempnam("./", "Services_Weather_Metar"); $status = $ftp->get($server["path"], $tempfile, true, FTP_ASCII); if (Services_Weather::isError($status)) { unlink($tempfile); return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_WRONG_SERVER_DATA, __FILE__, __LINE__); } // Disconnect FTP server, and read data from temporary file $ftp->disconnect(); $data = @file_get_contents($tempfile); unlink($tempfile); break; } // Split data into an array and return return preg_split("/\n|\r\n|\n\r/", $data); } // }}} // {{{ _parseWeatherData() /** * Parses the data and caches it * * METAR KPIT 091955Z COR 22015G25KT 3/4SM R28L/2600FT TSRA OVC010CB * 18/16 A2992 RMK SLP045 T01820159 * * @param array $data * @return PEAR_Error|array * @throws PEAR_Error::SERVICES_WEATHER_ERROR_WRONG_SERVER_DATA * @throws PEAR_Error::SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION * @access private */ function _parseWeatherData($data) { static $compass; static $clouds; static $cloudtypes; static $conditions; static $sensors; if (!isset($compass)) { $compass = array( "N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW" ); $clouds = array( "skc" => "sky clear", "nsc" => "no significant cloud", "few" => "few", "sct" => "scattered", "bkn" => "broken", "ovc" => "overcast", "vv" => "vertical visibility", "tcu" => "Towering Cumulus", "cb" => "Cumulonimbus", "clr" => "clear below 12,000 ft" ); $cloudtypes = array( "low" => array( "/" => "Overcast", "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)" ), "middle" => array( "/" => "Overcast", "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)" ), "high" => array( "/" => "Overcast", "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" ) ); $conditions = array( "+" => "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" => "funnel cloud", "+fc" => "tornado/waterspout" ); $sensors = array( "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" ); } $metarCode = array( "report" => "METAR|SPECI", "station" => "\w{4}", "update" => "(\d{2})?(\d{4})Z", "type" => "AUTO|COR", "wind" => "(\d{3}|VAR|VRB)(\d{2,3})(G(\d{2,3}))?(FPS|KPH|KT|KTS|MPH|MPS)", "windVar" => "(\d{3})V(\d{3})", "visFrac" => "(\d{1})", "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", "remark" => "RMK" ); $remarks = array( "nospeci" => "NOSPECI", "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", "maintain" => "[\$]" ); if (SERVICES_WEATHER_DEBUG) { for ($i = 0; $i < sizeof($data); $i++) { echo $data[$i]."\n"; } } // Eliminate trailing information for ($i = 0; $i < sizeof($data); $i++) { if (strpos($data[$i], "=") !== false) { $data[$i] = substr($data[$i], 0, strpos($data[$i], "=")); $data = array_slice($data, 0, $i + 1); break; } } // Start with parsing the first line for the last update $weatherData = array(); $weatherData["station"] = ""; $weatherData["dataRaw"] = implode(" ", $data); $weatherData["update"] = strtotime(trim($data[0])." GMT"); $weatherData["updateRaw"] = trim($data[0]); // and prepare the rest for stepping through array_shift($data); $metar = explode(" ", preg_replace("/\s{2,}/", " ", implode(" ", $data))); // 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 (!strlen($metar[$i])) { continue; } if (SERVICES_WEATHER_DEBUG) { $tab = str_repeat("\t", 3 - floor((strlen($metar[$i]) + 2) / 8)); echo "\"".$metar[$i]."\"".$tab."-> "; } // Initialize some arrays $result = array(); $resultVF = array(); $lresult = array(); $found = false; foreach ($metarCode as $key => $regexp) { // Check if current code matches current metar snippet if (($found = preg_match("/^".$regexp."$/i", $metar[$i], $result)) == true) { switch ($key) { case "station": $pointer["station"] = $result[0]; unset($metarCode["station"]); break; case "wind": // Parse wind data, first the speed, convert from kt to chosen unit if ($result[5] == "KTS") { $result[5] = "KT"; } $pointer["wind"] = $this->convertSpeed($result[2], $result[5], "mph"); if ($result[1] == "VAR" || $result[1] == "VRB") { // Variable winds $pointer["windDegrees"] = "Variable"; $pointer["windDirection"] = "Variable"; } else { // Save wind degree and calc direction $pointer["windDegrees"] = intval($result[1]); $pointer["windDirection"] = $compass[round($result[1] / 22.5) % 16]; } if (is_numeric($result[4])) { // Wind with gusts... $pointer["windGust"] = $this->convertSpeed($result[4], $result[5], "mph"); } break; case "windVar": // Once more wind, now variability around the current wind-direction $pointer["windVariability"] = array("from" => intval($result[1]), "to" => intval($result[2])); break; case "visFrac": // 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 $found = false; break; } else { // Match. Hand over result and advance METAR if (SERVICES_WEATHER_DEBUG) { echo $key."\n"; echo "\"".$result[1]." ".$metar[$i + 1]."\"".str_repeat("\t", 2 - floor((strlen($result[1]." ".$metar[$i + 1]) + 2) / 8))."-> "; } $key = "visibility"; $result = $resultVF; $i++; } case "visibility": $pointer["visQualifier"] = "AT"; if (is_numeric($result[1]) && ($result[1] == 9999)) { // Upper limit of visibility range $visibility = $this->convertDistance(10, "km", "sm"); $pointer["visQualifier"] = "BEYOND"; } elseif (is_numeric($result[1])) { // 4-digit visibility in m $visibility = $this->convertDistance(($result[1]/1000), "km", "sm"); } elseif (!isset($result[11]) || $result[11] != "CAVOK") { if ($result[3] == "M") { $pointer["visQualifier"] = "BELOW"; } elseif ($result[3] == "P") { $pointer["visQualifier"] = "BEYOND"; } if (is_numeric($result[5])) { // visibility as one/two-digit number $visibility = $this->convertDistance($result[5], $result[10], "sm"); } else { // the y/z part, add if we had a x part (see visibility1) if (is_numeric($result[7])) { $visibility = $this->convertDistance($result[7] + $result[8] / $result[9], $result[10], "sm"); } else { $visibility = $this->convertDistance($result[8] / $result[9], $result[10], "sm"); } } } else { $pointer["visQualifier"] = "BEYOND"; $visibility = $this->convertDistance(10, "km", "sm"); $pointer["clouds"] = array(array("amount" => "Clear below", "height" => 5000)); $pointer["condition"] = "no significant weather"; } $pointer["visibility"] = $visibility; break; case "condition": // First some basic setups if (!isset($pointer["condition"])) { $pointer["condition"] = ""; } elseif (strlen($pointer["condition"]) > 0) { $pointer["condition"] .= ","; } if (in_array(strtolower($result[0]), $conditions)) { // First try matching the complete string $pointer["condition"] .= " ".$conditions[strtolower($result[0])]; } else { // No luck, match part by part array_shift($result); $result = array_unique($result); foreach ($result as $condition) { if (strlen($condition) > 0) { $pointer["condition"] .= " ".$conditions[strtolower($condition)]; } } } $pointer["condition"] = trim($pointer["condition"]); break; case "clouds": if (!isset($pointer["clouds"])) { $pointer["clouds"] = array(); } if (sizeof($result) == 5) { // Only amount and height $cloud = array("amount" => $clouds[strtolower($result[3])]); if ($result[4] == "///") { $cloud["height"] = "station level or below"; } else { $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"; } else { $cloud["height"] = $result[4] * 100; } } else { // SKC or CLR or NSC $cloud = array("amount" => $clouds[strtolower($result[0])]); } $pointer["clouds"][] = $cloud; break; case "temperature": // normal temperature in first part // negative value if ($result[1] == "M") { $result[2] *= -1; } $pointer["temperature"] = $this->convertTemperature($result[2], "c", "f"); if (sizeof($result) > 4) { // same for dewpoint if ($result[4] == "M") { $result[5] *= -1; } $pointer["dewPoint"] = $this->convertTemperature($result[5], "c", "f"); $pointer["humidity"] = $this->calculateHumidity($result[2], $result[5]); } if (isset($pointer["wind"])) { // Now calculate windchill from temperature and windspeed $pointer["feltTemperature"] = $this->calculateWindChill($pointer["temperature"], $pointer["wind"]); } break; case "pressure": if ($result[1] == "A") { // Pressure provided in inches $pointer["pressure"] = $result[2] / 100; } elseif ($result[3] == "Q") { // ... in hectopascal $pointer["pressure"] = $this->convertPressure($result[4], "hpa", "in"); } break; case "trend": // We may have a trend here... extract type and set pointer on // created new array if (!isset($weatherData["trend"])) { $weatherData["trend"] = array(); $weatherData["trend"][$trendCount] = array(); } $pointer =& $weatherData["trend"][$trendCount]; $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]; } else { $pointer["at"] = $lresult[2].":".$lresult[3]; } // As we have just extracted the time for this trend // from our METAR, increase field-counter $i++; } break; case "remark": // Remark part begins $metarCode = $remarks; $weatherData["remark"] = array(); break; case "autostation": // Which autostation do we have here? if ($result[1] == 0) { $weatherData["remark"]["autostation"] = "Automatic weatherstation w/o precipitation discriminator"; } else { $weatherData["remark"]["autostation"] = "Automatic weatherstation w/ precipitation discriminator"; } unset($metarCode["autostation"]); break; case "presschg": // Decoding for rapid pressure changes if (strtolower($result[1]) == "r") { $weatherData["remark"]["presschg"] = "Pressure rising rapidly"; } else { $weatherData["remark"]["presschg"] = "Pressure falling rapidly"; } unset($metarCode["presschg"]); break; case "seapressure": // 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 if (strtolower($result[1]) != "no") { if ($result[1] > 500) { $press = 900 + round($result[1] / 100, 1); } else { $press = 1000 + $result[1]; } $weatherData["remark"]["seapressure"] = $this->convertPressure($press, "hpa", "in"); } unset($metarCode["seapressure"]); break; case "precip": // Precipitation in inches static $hours; if (!isset($weatherData["precipitation"])) { $weatherData["precipitation"] = array(); $hours = array("P" => "1", "6" => "3/6", "7" => "24"); } if (!is_numeric($result[2])) { $precip = "indeterminable"; } elseif ($result[2] == "0000") { $precip = "traceable"; } else { $precip = $result[2] / 100; } $weatherData["precipitation"][] = array( "amount" => $precip, "hours" => $hours[$result[1]] ); break; case "snowdepth": // Snow depth in inches $weatherData["remark"]["snowdepth"] = $result[1]; unset($metarCode["snowdepth"]); break; case "snowequiv": // Same for equivalent in Water... (inches) $weatherData["remark"]["snowequiv"] = $result[1] / 10; unset($metarCode["snowequiv"]); break; case "cloudtypes": // Cloud types $weatherData["remark"]["cloudtypes"] = array( "low" => $cloudtypes["low"][$result[1]], "middle" => $cloudtypes["middle"][$result[2]], "high" => $cloudtypes["high"][$result[3]] ); unset($metarCode["cloudtypes"]); break; case "sunduration": // Duration of sunshine (in minutes) $weatherData["remark"]["sunduration"] = "Total minutes of sunshine: ".$result[1]; unset($metarCode["sunduration"]); break; case "1htempdew": // Temperatures in the last hour in C if ($result[1] == "1") { $result[2] *= -1; } $weatherData["remark"]["1htemp"] = $this->convertTemperature($result[2] / 10, "c", "f"); if (sizeof($result) > 3) { // same for dewpoint if ($result[4] == "1") { $result[5] *= -1; } $weatherData["remark"]["1hdew"] = $this->convertTemperature($result[5] / 10, "c", "f"); } unset($metarCode["1htempdew"]); break; case "6hmaxtemp": // Max temperature in the last 6 hours in C if ($result[1] == "1") { $result[2] *= -1; } $weatherData["remark"]["6hmaxtemp"] = $this->convertTemperature($result[2] / 10, "c", "f"); unset($metarCode["6hmaxtemp"]); break; case "6hmintemp": // Min temperature in the last 6 hours in C if ($result[1] == "1") { $result[2] *= -1; } $weatherData["remark"]["6hmintemp"] = $this->convertTemperature($result[2] / 10, "c", "f"); unset($metarCode["6hmintemp"]); break; case "24htemp": // Max/Min temperatures in the last 24 hours in C if ($result[1] == "1") { $result[2] *= -1; } $weatherData["remark"]["24hmaxtemp"] = $this->convertTemperature($result[2] / 10, "c", "f"); if ($result[3] == "1") { $result[4] *= -1; } $weatherData["remark"]["24hmintemp"] = $this->convertTemperature($result[4] / 10, "c", "f"); unset($metarCode["24htemp"]); break; case "3hpresstend": // Pressure tendency of the last 3 hours // no special processing, just passing the data $weatherData["remark"]["3hpresstend"] = array( "presscode" => $result[1], "presschng" => $this->convertPressure($result[2] / 10, "hpa", "in") ); unset($metarCode["3hpresstend"]); break; case "nospeci": // No change during the last hour $weatherData["remark"]["nospeci"] = "No changes in weather conditions"; unset($metarCode["nospeci"]); break; case "sensors": // We may have multiple broken sensors, so do not unset if (!isset($weatherData["remark"]["sensors"])) { $weatherData["remark"]["sensors"] = array(); } $weatherData["remark"]["sensors"][strtolower($result[0])] = $sensors[strtolower($result[0])]; break; case "maintain": $weatherData["remark"]["maintain"] = "Maintainance needed"; unset($metarCode["maintain"]); break; default: // Do nothing, just prevent further matching unset($metarCode[$key]); break; } if ($found && !SERVICES_WEATHER_DEBUG) { break; } elseif ($found && SERVICES_WEATHER_DEBUG) { echo $key."\n"; break; } } } if (!$found) { if (SERVICES_WEATHER_DEBUG) { echo "n/a\n"; } if (!isset($weatherData["noparse"])) { $weatherData["noparse"] = array(); } $weatherData["noparse"][] = $metar[$i]; } } if (isset($weatherData["noparse"])) { $weatherData["noparse"] = implode(" ", $weatherData["noparse"]); } return $weatherData; } // }}} // {{{ _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 * * @param array $data * @return PEAR_Error|array * @throws PEAR_Error::SERVICES_WEATHER_ERROR_WRONG_SERVER_DATA * @throws PEAR_Error::SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION * @access private */ function _parseForecastData($data) { static $compass; static $clouds; static $conditions; static $sensors; if (!isset($compass)) { $compass = array( "N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW" ); $clouds = array( "skc" => "sky clear", "nsc" => "no significant cloud", "few" => "few", "sct" => "scattered", "bkn" => "broken", "ovc" => "overcast", "vv" => "vertical visibility", "tcu" => "Towering Cumulus", "cb" => "Cumulonimbus", "clr" => "clear below 12,000 ft" ); $conditions = array( "+" => "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" => "funnel cloud", "+fc" => "tornado/waterspout" ); } $tafCode = array( "report" => "TAF|AMD", "station" => "\w{4}", "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)", "visFrac" => "(\d{1})", "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++) { echo $data[$i]."\n"; } } // Eliminate trailing information for ($i = 0; $i < sizeof($data); $i++) { if (strpos($data[$i], "=") !== false) { $data[$i] = substr($data[$i], 0, strpos($data[$i], "=")); $data = array_slice($data, 0, $i + 1); break; } } // Ok, we have correct data, start with parsing the first line for the last update $forecastData = array(); $forecastData["station"] = ""; $forecastData["dataRaw"] = implode(" ", $data); $forecastData["update"] = strtotime(trim($data[0])." GMT"); $forecastData["updateRaw"] = trim($data[0]); // and prepare the rest for stepping through array_shift($data); $taf = explode(" ", preg_replace("/\s{2,}/", " ", implode(" ", $data))); // 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 (!strlen($taf[$i])) { continue; } if (SERVICES_WEATHER_DEBUG) { $tab = str_repeat("\t", 3 - floor((strlen($taf[$i]) + 2) / 8)); echo "\"".$taf[$i]."\"".$tab."-> "; } // Initialize some arrays $result = array(); $resultVF = array(); $lresult = array(); $found = false; foreach ($tafCode as $key => $regexp) { // Check if current code matches current taf snippet if (($found = preg_match("/^".$regexp."$/i", $taf[$i], $result)) == true) { $insert = array(); switch ($key) { case "station": $pointer["station"] = $result[0]; unset($tafCode["station"]); break; case "valid": $pointer["validRaw"] = $result[0]; // Generates the timeperiod the report is valid for list($year, $month, $day) = explode("-", gmdate("Y-m-d", $forecastData["update"])); // Date is in next month if ($result[1] < $day) { $month++; } $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]; break; case "wind": // Parse wind data, first the speed, convert from kt to chosen unit if ($result[5] == "KTS") { $result[5] = "KT"; } $pointer["wind"] = $this->convertSpeed($result[2], $result[5], "mph"); if ($result[1] == "VAR" || $result[1] == "VRB") { // Variable winds $pointer["windDegrees"] = "Variable"; $pointer["windDirection"] = "Variable"; } else { // Save wind degree and calc direction $pointer["windDegrees"] = $result[1]; $pointer["windDirection"] = $compass[round($result[1] / 22.5) % 16]; } if (is_numeric($result[4])) { // Wind with gusts... $pointer["windGust"] = $this->convertSpeed($result[4], $result[5], "mph"); } if (isset($probability)) { $pointer["windProb"] = $probability; unset($probability); } break; case "visFrac": // 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 $found = false; break; } else { // Match. Hand over result and advance TAF if (SERVICES_WEATHER_DEBUG) { echo $key."\n"; echo "\"".$result[1]." ".$taf[$i + 1]."\"".str_repeat("\t", 2 - floor((strlen($result[1]." ".$taf[$i + 1]) + 2) / 8))."-> "; } $key = "visibility"; $result = $resultVF; $i++; } case "visibility": $pointer["visQualifier"] = "AT"; if (is_numeric($result[1]) && ($result[1] == 9999)) { // Upper limit of visibility range $visibility = $this->convertDistance(10, "km", "sm"); $pointer["visQualifier"] = "BEYOND"; } elseif (is_numeric($result[1])) { // 4-digit visibility in m $visibility = $this->convertDistance(($result[1]/1000), "km", "sm"); } elseif (!isset($result[11]) || $result[11] != "CAVOK") { if ($result[3] == "M") { $pointer["visQualifier"] = "BELOW"; } elseif ($result[3] == "P") { $pointer["visQualifier"] = "BEYOND"; } if (is_numeric($result[5])) { // visibility as one/two-digit number $visibility = $this->convertDistance($result[5], $result[10], "sm"); } else { // the y/z part, add if we had a x part (see visibility1) if (is_numeric($result[7])) { $visibility = $this->convertDistance($result[7] + $result[8] / $result[9], $result[10], "sm"); } else { $visibility = $this->convertDistance($result[8] / $result[9], $result[10], "sm"); } } } else { $pointer["visQualifier"] = "BEYOND"; $visibility = $this->convertDistance(10, "km", "sm"); $pointer["clouds"] = array(array("amount" => "Clear below", "height" => 5000)); $pointer["condition"] = "no significant weather"; } if (isset($probability)) { $pointer["visProb"] = $probability; unset($probability); } $pointer["visibility"] = $visibility; break; case "condition": // First some basic setups if (!isset($pointer["condition"])) { $pointer["condition"] = ""; } elseif (strlen($pointer["condition"]) > 0) { $pointer["condition"] .= ","; } if (in_array(strtolower($result[0]), $conditions)) { // First try matching the complete string $pointer["condition"] .= " ".$conditions[strtolower($result[0])]; } else { // No luck, match part by part array_shift($result); $result = array_unique($result); foreach ($result as $condition) { if (strlen($condition) > 0) { $pointer["condition"] .= " ".$conditions[strtolower($condition)]; } } } $pointer["condition"] = trim($pointer["condition"]); if (isset($probability)) { $pointer["condition"] .= " (".$probability."% prob.)"; unset($probability); } break; case "clouds": if (!isset($pointer["clouds"])) { $pointer["clouds"] = array(); } if (sizeof($result) == 5) { // Only amount and height $cloud = array("amount" => $clouds[strtolower($result[3])]); if ($result[4] == "///") { $cloud["height"] = "station level or below"; } else { $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"; } else { $cloud["height"] = $result[4] * 100; } } else { // SKC or CLR or NSC $cloud = array("amount" => $clouds[strtolower($result[0])]); } if (isset($probability)) { $cloud["prob"] = $probability; unset($probability); } $pointer["clouds"][] = $cloud; break; case "windshear": // Parse windshear, if available if ($result[4] == "KTS") { $result[4] = "KT"; } $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]; break; case "tempmax": $forecastData["temperatureHigh"] = $this->convertTemperature($result[1], "c", "f"); break; case "tempmin": // Parse max/min temperature $forecastData["temperatureLow"] = $this->convertTemperature($result[1], "c", "f"); break; case "tempmaxmin": $forecastData["temperatureHigh"] = $this->convertTemperature($result[1], "c", "f"); $forecastData["temperatureLow"] = $this->convertTemperature($result[4], "c", "f"); break; case "from": // Next timeperiod is coming up, prepare array and // set pointer accordingly if (sizeof($result) > 2) { // The ICAO way $fromTime = $result[2].":".$result[3]; } else { // The Australian way (Hey mates!) $fromTime = $result[1].":00"; } $forecastData["time"][$fromTime] = array(); $fmcCount = 0; $pointer =& $forecastData["time"][$fromTime]; break; case "fmc"; // 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 $type = $lresult[0]; // Set probability $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 = $lresult[2].":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 $i += 2; } else { // No timegroup present, so just increase field-counter by one $i += 1; } } elseif (preg_match("/^(\d{2})(\d{2})\/(\d{2})(\d{2})$/i", $taf[$i + 1], $lresult)) { // Normal group, set type and use extracted time $type = $result[1]; // Check for PROBdd if (isset($result[2])) { $probability = $result[2]; } $from = $lresult[2].":00"; $to = $lresult[4].":00"; $to = ($to == "24:00") ? "00:00" : $to; // Same as above, we have a time for this FMC from our TAF, // increase field-counter $i += 1; } 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($type)) { if (!isset($forecastData["time"][$fromTime]["fmc"])) { $forecastData["time"][$fromTime]["fmc"] = array(); } $forecastData["time"][$fromTime]["fmc"][$fmcCount] = array(); // ...and set pointer. $pointer =& $forecastData["time"][$fromTime]["fmc"][$fmcCount]; $fmcCount++; // Insert data $pointer["type"] = $type; unset($type); if (isset($from)) { $pointer["from"] = $from; $pointer["to"] = $to; unset($from, $to); } if (isset($probability)) { $pointer["probability"] = $probability; unset($probability); } } break; default: // Do nothing break; } if ($found && !SERVICES_WEATHER_DEBUG) { break; } elseif ($found && SERVICES_WEATHER_DEBUG) { echo $key."\n"; break; } } } if (!$found) { if (SERVICES_WEATHER_DEBUG) { echo "n/a\n"; } if (!isset($forecastData["noparse"])) { $forecastData["noparse"] = array(); } $forecastData["noparse"][] = $taf[$i]; } } if (isset($forecastData["noparse"])) { $forecastData["noparse"] = implode(" ", $forecastData["noparse"]); } return $forecastData; } // }}} // {{{ _convertReturn() /** * Converts the data in the return array to the desired units and/or * output format. * * @param array $target * @param string $units * @param string $location * @access private */ function _convertReturn(&$target, $units, $location) { if (is_array($target)) { foreach ($target as $key => $val) { if (is_array($val)) { // Another array detected, so recurse into it to convert the units $this->_convertReturn($target[$key], $units, $location); } else { switch ($key) { case "station": $newVal = $location["name"]; break; case "update": case "validFrom": case "validTo": $newVal = gmdate(trim($this->_dateFormat." ".$this->_timeFormat), $val); break; case "wind": case "windGust": case "windshear": $newVal = round($this->convertSpeed($val, "mph", $units["wind"]), 2); break; case "visibility": $newVal = round($this->convertDistance($val, "sm", $units["vis"], 2)); break; case "height": case "windshearHeight": if (is_numeric($val)) { $newVal = round($this->convertDistance($val, "ft", $units["height"]), 2); } else { $newVal = $val; } break; case "temperature": case "temperatureHigh": case "temperatureLow": case "dewPoint": case "feltTemperature": $newVal = round($this->convertTemperature($val, "f", $units["temp"]), 2); break; case "pressure": case "seapressure": case "presschng": $newVal = round($this->convertPressure($val, "in", $units["pres"]), 2); break; case "amount": case "snowdepth": case "snowequiv": if (is_numeric($val)) { $newVal = round($this->convertPressure($val, "in", $units["rain"]), 2); } else { $newVal = $val; } break; case "1htemp": case "1hdew": case "6hmaxtemp": case "6hmintemp": case "24hmaxtemp": case "24hmintemp": $newVal = round($this->convertTemperature($val, "f", $units["temp"]), 2); break; case "humidity": $newVal = round($val, 1); break; default: continue 2; } $target[$key] = $newVal; } } } } // }}} // {{{ searchLocation() /** * Searches IDs for given location, returns array of possible locations * or single ID * * @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 * @access public */ function searchLocation($location, $useFirst = false) { if (!isset($this->_db) || !DB::isConnection($this->_db)) { return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_DB_NOT_CONNECTED, __FILE__, __LINE__); } if (is_string($location)) { // 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]))."%"); } if (sizeof($location) == 1) { $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 ". "FROM metarLocations ". "WHERE ".$where." ". "ORDER BY icao DESC"; $result = $this->_db->query($select); // Check result for validity if (DB::isError($result)) { return $result; } elseif (strtolower(get_class($result)) != "db_result" || $result->numRows() == 0) { return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION, __FILE__, __LINE__); } // Result is valid, start preparing the return $icao = array(); while (($row = $result->fetchRow(DB_FETCHMODE_ASSOC)) != null) { $locicao = $row["icao"]; // First the name of the location if (!strlen($row["state"])) { $locname = $row["name"].", ".$row["country"]; } else { $locname = $row["name"].", ".$row["state"].", ".$row["country"]; } if ($locicao != "----") { // We have a location with ICAO $icao[$locicao] = $locname; } else { // 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) { $icao = key($icao); } } elseif (is_array($location)) { // Location was provided as coordinates, search nearest airport $icao = $this->searchAirport($location[0], $location[1]); } else { return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_INVALID_LOCATION, __FILE__, __LINE__); } return $icao; } // }}} // {{{ searchLocationByCountry() /** * Returns IDs with location-name for a given country or all available * countries, if no value was given * * @param string $country * @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 * @access public */ function searchLocationByCountry($country = "") { if (!isset($this->_db) || !DB::isConnection($this->_db)) { return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_DB_NOT_CONNECTED, __FILE__, __LINE__); } // Return the available countries as no country was given if (!strlen($country)) { $select = "SELECT DISTINCT(country) ". "FROM metarAirports ". "ORDER BY country ASC"; $countries = $this->_db->getCol($select); // As $countries is either an error or the true result, // we can just return it return $countries; } // Now for the real search $select = "SELECT icao, name, state, country ". "FROM metarAirports ". "WHERE LOWER(country) LIKE '%".strtolower(trim($country))."%' ". "ORDER BY name ASC"; $result = $this->_db->query($select); // Check result for validity if (DB::isError($result)) { return $result; } elseif (strtolower(get_class($result)) != "db_result" || $result->numRows() == 0) { return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION, __FILE__, __LINE__); } // Construct the result $locations = array(); while (($row = $result->fetchRow(DB_FETCHMODE_ASSOC)) != null) { $locicao = $row["icao"]; if ($locicao != "----") { // First the name of the location if (!strlen($row["state"])) { $locname = $row["name"].", ".$row["country"]; } else { $locname = $row["name"].", ".$row["state"].", ".$row["country"]; } $locations[$locicao] = $locname; } } return $locations; } // }}} // {{{ searchAirport() /** * Searches the nearest airport(s) for given coordinates, returns array * of IDs or single ID * * @param float $latitude * @param float $longitude * @param int $numResults * @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 * @access public */ function searchAirport($latitude, $longitude, $numResults = 1) { if (!isset($this->_db) || !DB::isConnection($this->_db)) { return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_DB_NOT_CONNECTED, __FILE__, __LINE__); } if (!is_numeric($latitude) || !is_numeric($longitude)) { return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_INVALID_LOCATION, __FILE__, __LINE__); } // Get all airports $select = "SELECT icao, x, y, z FROM metarAirports"; $result = $this->_db->query($select); if (DB::isError($result)) { return $result; } elseif (strtolower(get_class($result)) != "db_result" || $result->numRows() == 0) { return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION, __FILE__, __LINE__); } // Result is valid, start search // Initialize values $min_dist = null; $query = $this->polar2cartesian($latitude, $longitude); $search = array("dist" => array(), "icao" => array()); while (($row = $result->fetchRow(DB_FETCHMODE_ASSOC)) != null) { $icao = $row["icao"]; $air = array($row["x"], $row["y"], $row["z"]); $dist = 0; $d = 0; // 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]; $dist += pow($t, 2); if ($min_dist != null && $dist > $min_dist) { break; } } if ($d >= sizeof($air)) { // Ok, current airport is one of the nearer locations // add to result-array $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) { array_pop($search["dist"]); array_pop($search["icao"]); } $min_dist = max($search["dist"]); } } if ($numResults == 1) { // Only one result wanted, return as string return $search["icao"][0]; } elseif ($numResults > 1) { // Return found locations return $search["icao"]; } else { return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION, __FILE__, __LINE__); } } // }}} // {{{ getLocation() /** * Returns the data for the location belonging to the ID * * @param string $id * @return PEAR_Error|array * @throws PEAR_Error * @access public */ function getLocation($id = "") { $status = $this->_checkLocationID($id); if (Services_Weather::isError($status)) { return $status; } $locationReturn = array(); if ($this->_cacheEnabled && ($location = $this->_getCache("METAR-".$id, "location"))) { // Grab stuff from cache $this->_location = $location; $locationReturn["cache"] = "HIT"; } elseif (isset($this->_db) && DB::isConnection($this->_db)) { // Get data from DB $select = "SELECT icao, name, state, country, latitude, longitude, elevation ". "FROM metarAirports WHERE icao='".$id."'"; $result = $this->_db->query($select); if (DB::isError($result)) { return $result; } elseif (strtolower(get_class($result)) != "db_result" || $result->numRows() == 0) { return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION, __FILE__, __LINE__); } // Result is ok, put things into object $this->_location = $result->fetchRow(DB_FETCHMODE_ASSOC); if ($this->_cacheEnabled) { // ...and cache it $this->_saveCache("METAR-".$id, $this->_location, "", "location"); } $locationReturn["cache"] = "MISS"; } else { $this->_location = array( "name" => $id, "state" => "", "country" => "", "latitude" => "", "longitude" => "", "elevation" => "" ); } // 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"]; } else { $locname = $this->_location["name"]; } $locationReturn["name"] = $locname; $locationReturn["latitude"] = $this->_location["latitude"]; $locationReturn["longitude"] = $this->_location["longitude"]; $locationReturn["sunrise"] = gmdate($this->_timeFormat, $this->calculateSunRiseSet(gmmktime(), SUNFUNCS_RET_TIMESTAMP, $this->_location["latitude"], $this->_location["longitude"], SERVICES_WEATHER_SUNFUNCS_SUNRISE_ZENITH, 0, true)); $locationReturn["sunset"] = gmdate($this->_timeFormat, $this->calculateSunRiseSet(gmmktime(), SUNFUNCS_RET_TIMESTAMP, $this->_location["latitude"], $this->_location["longitude"], SERVICES_WEATHER_SUNFUNCS_SUNSET_ZENITH, 0, false)); $locationReturn["elevation"] = $this->_location["elevation"]; return $locationReturn; } // }}} // {{{ getWeather() /** * Returns the weather-data for the supplied location * * @param string $id * @param string $unitsFormat * @return PHP_Error|array * @throws PHP_Error * @access public */ function getWeather($id = "", $unitsFormat = "") { $id = strtoupper($id); $status = $this->_checkLocationID($id); if (Services_Weather::isError($status)) { return $status; } // Get other data $units = $this->getUnitsFormat($unitsFormat); $location = $this->getLocation($id); if (Services_Weather::isError($location)) { return $location; } 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"; } else { // Download weather $weatherData = $this->_retrieveServerData($id, "metar"); if (Services_Weather::isError($weatherData)) { return $weatherData; } elseif (!is_array($weatherData) || sizeof($weatherData) < 2) { return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_WRONG_SERVER_DATA, __FILE__, __LINE__); } // Parse weather $weatherReturn = $this->_parseWeatherData($weatherData); if (Services_Weather::isError($weatherReturn)) { return $weatherReturn; } // 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; // Get the icon $weatherReturn["conditionIcon"] = $this->getWeatherIcon($condition, $clouds, $wind, $temperature, $latitude, $longitude, strtotime($weatherReturn["updateRaw"]." GMT")); // Calculate the moon phase and age $moon = $this->calculateMoonPhase(strtotime($weatherReturn["updateRaw"]." GMT")); if (Services_Weather::isError($moon)) { return $moon; } $weatherReturn["moon"] = $moon["phase"]; $weatherReturn["moonIcon"] = $moon["icon"]; if ($this->_cacheEnabled) { // Cache weather $this->_saveCache("METAR-".$id, $weatherReturn, $unitsFormat, "weather"); } $this->_weather = $weatherReturn; $weatherReturn["cache"] = "MISS"; } $this->_convertReturn($weatherReturn, $units, $location); return $weatherReturn; } // }}} // {{{ getForecast() /** * METAR provides no forecast per se, we use the TAF reports to generate * a forecast for the announced timeperiod * * @param string $id * @param int $days Ignored, not applicable * @param string $unitsFormat * @return PEAR_Error|array * @throws PEAR_Error * @access public */ function getForecast($id = "", $days = null, $unitsFormat = "") { $id = strtoupper($id); $status = $this->_checkLocationID($id); if (Services_Weather::isError($status)) { return $status; } // Get other data $units = $this->getUnitsFormat($unitsFormat); $location = $this->getLocation($id); if (Services_Weather::isError($location)) { return $location; } 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"; } else { // Download forecast $forecastData = $this->_retrieveServerData($id, "taf"); if (Services_Weather::isError($forecastData)) { return $forecastData; } elseif (!is_array($forecastData) || sizeof($forecastData) < 2) { return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_WRONG_SERVER_DATA, __FILE__, __LINE__); } // Parse forecast $forecastReturn = $this->_parseForecastData($forecastData); if (Services_Weather::isError($forecastReturn)) { return $forecastReturn; } if ($this->_cacheEnabled) { // Cache weather $this->_saveCache("METAR-".$id, $forecastReturn, $unitsFormat, "forecast"); } $this->_forecast = $forecastReturn; $forecastReturn["cache"] = "MISS"; } $this->_convertReturn($forecastReturn, $units, $location); return $forecastReturn; } // }}} } // }}} ?>