home *** CD-ROM | disk | FTP | other *** search
/ Cricao de Sites - 650 Layouts Prontos / WebMasters.iso / Servidores / xampp-win32-1.6.7-installer.exe / php / PEAR / Services / Weather / Metar.php < prev    next >
Encoding:
PHP Script  |  2008-07-02  |  89.6 KB  |  1,909 lines

  1. <?php
  2. /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4 foldmethod=marker: */
  3.  
  4. /**
  5.  * PEAR::Services_Weather_Metar
  6.  *
  7.  * PHP versions 4 and 5
  8.  *
  9.  * <LICENSE>
  10.  * Copyright (c) 2005-2007, Alexander Wirtz
  11.  * All rights reserved.
  12.  *
  13.  * Redistribution and use in source and binary forms, with or without
  14.  * modification, are permitted provided that the following conditions
  15.  * are met:
  16.  * o Redistributions of source code must retain the above copyright notice,
  17.  *   this list of conditions and the following disclaimer.
  18.  * o Redistributions in binary form must reproduce the above copyright notice,
  19.  *   this list of conditions and the following disclaimer in the documentation
  20.  *   and/or other materials provided with the distribution.
  21.  * o Neither the name of the software nor the names of its contributors
  22.  *   may be used to endorse or promote products derived from this software
  23.  *   without specific prior written permission.
  24.  *
  25.  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  26.  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  27.  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  28.  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
  29.  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  30.  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  31.  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  32.  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  33.  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  34.  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  35.  * POSSIBILITY OF SUCH DAMAGE.
  36.  * </LICENSE>
  37.  *
  38.  * @category    Web Services
  39.  * @package     Services_Weather
  40.  * @author      Alexander Wirtz <alex@pc4p.net>
  41.  * @copyright   2005-2007 Alexander Wirtz
  42.  * @license     http://www.opensource.org/licenses/bsd-license.php  BSD License
  43.  * @version     CVS: $Id: Metar.php,v 1.100 2007/04/16 10:36:05 eru Exp $
  44.  * @link        http://pear.php.net/package/Services_Weather
  45.  * @link        http://weather.noaa.gov/weather/metar.shtml
  46.  * @link        http://weather.noaa.gov/weather/taf.shtml
  47.  * @example     examples/metar-basic.php            metar-basic.php
  48.  * @example     examples/metar-extensive.php        metar-extensive.php
  49.  * @filesource
  50.  */
  51.  
  52. require_once "Services/Weather/Common.php";
  53.  
  54. require_once "DB.php";
  55.  
  56. // {{{ class Services_Weather_Metar
  57. /**
  58.  * This class acts as an interface to the METAR/TAF service of
  59.  * weather.noaa.gov. It searches for locations given in ICAO notation and
  60.  * retrieves the current weather data.
  61.  *
  62.  * Of course the parsing of the METAR-data has its limitations, as it
  63.  * follows the Federal Meteorological Handbook No.1 with modifications to
  64.  * accomodate for non-US reports, so if the report deviates from these
  65.  * standards, you won't get it parsed correctly.
  66.  * Anything that is not parsed, is saved in the "noparse" array-entry,
  67.  * returned by getWeather(), so you can do your own parsing afterwards. This
  68.  * limitation is specifically given for remarks, as the class is not
  69.  * processing everything mentioned there, but you will get the most common
  70.  * fields like precipitation and temperature-changes. Again, everything not
  71.  * parsed, goes into "noparse".
  72.  *
  73.  * If you think, some important field is missing or not correctly parsed,
  74.  * please file a feature-request/bugreport at http://pear.php.net/ and be
  75.  * sure to provide the METAR (or TAF) report with a _detailed_ explanation!
  76.  *
  77.  * For working examples, please take a look at
  78.  *     docs/Services_Weather/examples/metar-basic.php
  79.  *     docs/Services_Weather/examples/metar-extensive.php
  80.  *
  81.  *
  82.  * @category    Web Services
  83.  * @package     Services_Weather
  84.  * @author      Alexander Wirtz <alex@pc4p.net>
  85.  * @copyright   2005-2007 Alexander Wirtz
  86.  * @license     http://www.opensource.org/licenses/bsd-license.php  BSD License
  87.  * @version     Release: 1.4.3
  88.  * @link        http://pear.php.net/package/Services_Weather
  89.  * @link        http://weather.noaa.gov/weather/metar.shtml
  90.  * @link        http://weather.noaa.gov/weather/taf.shtml
  91.  * @example     examples/metar-basic.php            metar-basic.php
  92.  * @example     examples/metar-extensive.php        metar-extensive.php
  93.  */
  94. class Services_Weather_Metar extends Services_Weather_Common
  95. {
  96.     // {{{ properties
  97.     /**
  98.      * Information to access the location DB
  99.      *
  100.      * @var     object  DB                  $_db
  101.      * @access  private
  102.      */
  103.     var $_db;
  104.  
  105.     /**
  106.      * The source METAR uses
  107.      *
  108.      * @var     string                      $_sourceMetar
  109.      * @access  private
  110.      */
  111.     var $_sourceMetar;
  112.  
  113.     /**
  114.      * The source TAF uses
  115.      *
  116.      * @var     string                      $_sourceTaf
  117.      * @access  private
  118.      */
  119.     var $_sourceTaf;
  120.  
  121.     /**
  122.      * This path is used to find the METAR data
  123.      *
  124.      * @var     string                      $_sourcePathMetar
  125.      * @access  private
  126.      */
  127.     var $_sourcePathMetar;
  128.  
  129.     /**
  130.      * This path is used to find the TAF data
  131.      *
  132.      * @var     string                      $_sourcePathTaf
  133.      * @access  private
  134.      */
  135.     var $_sourcePathTaf;
  136.     // }}}
  137.  
  138.     // {{{ constructor
  139.     /**
  140.      * Constructor
  141.      *
  142.      * @param   array                       $options
  143.      * @param   mixed                       $error
  144.      * @throws  PEAR_Error
  145.      * @access  private
  146.      */
  147.     function Services_Weather_Metar($options, &$error)
  148.     {
  149.         $perror = null;
  150.         $this->Services_Weather_Common($options, $perror);
  151.         if (Services_Weather::isError($perror)) {
  152.             $error = $perror;
  153.             return;
  154.         }
  155.  
  156.         // Set options accordingly
  157.         if (isset($options["dsn"])) {
  158.             if (isset($options["dbOptions"])) {
  159.                 $status = $this->setMetarDB($options["dsn"], $options["dbOptions"]);
  160.             } else {
  161.                 $status = $this->setMetarDB($options["dsn"]);
  162.             }
  163.         }
  164.         if (Services_Weather::isError($status)) {
  165.             $error = $status;
  166.             return;
  167.         }
  168.  
  169.         // Setting the data sources for METAR and TAF - have to watch out for older API usage
  170.         if (($source = isset($options["source"])) || isset($options["sourceMetar"])) {
  171.             $sourceMetar = $source ? $options["source"] : $options["sourceMetar"];
  172.             if (($sourcePath = isset($options["sourcePath"])) || isset($options["sourcePathMetar"])) {
  173.                 $sourcePathMetar = $sourcePath ? $options["sourcePath"] : $options["sourcePathMetar"];
  174.             } else {
  175.                 $sourcePathMetar = "";
  176.             }
  177.         } else {
  178.             $sourceMetar = "http";
  179.             $sourcePathMetar = "";
  180.         }
  181.         if (isset($options["sourceTaf"])) {
  182.             $sourceTaf = $options["sourceTaf"];
  183.             if (isset($option["sourcePathTaf"])) {
  184.                 $sourcePathTaf = $options["sourcePathTaf"];
  185.             } else {
  186.                 $soucePathTaf = "";
  187.             }
  188.         } else {
  189.             $sourceTaf = "http";
  190.             $sourcePathTaf = "";
  191.         }
  192.         $status = $this->setMetarSource($sourceMetar, $sourcePathMetar, $sourceTaf, $sourcePathTaf);
  193.         if (Services_Weather::isError($status)) {
  194.             $error = $status;
  195.             return;
  196.         }
  197.     }
  198.     // }}}
  199.  
  200.     // {{{ setMetarDB()
  201.     /**
  202.      * Sets the parameters needed for connecting to the DB, where the
  203.      * location-search is fetching its data from. You need to build a DB
  204.      * with the external tool buildMetarDB first, it fetches the locations
  205.      * and airports from a NOAA-website.
  206.      *
  207.      * @param   string                      $dsn
  208.      * @param   array                       $dbOptions
  209.      * @return  DB_Error|bool
  210.      * @throws  DB_Error
  211.      * @see     DB::parseDSN
  212.      * @access  public
  213.      */
  214.     function setMetarDB($dsn, $dbOptions = array())
  215.     {
  216.         $dsninfo = DB::parseDSN($dsn);
  217.         if (is_array($dsninfo) && !isset($dsninfo["mode"])) {
  218.             $dsninfo["mode"]= 0644;
  219.         }
  220.  
  221.         // Initialize connection to DB and store in object if successful
  222.         $db =  DB::connect($dsninfo, $dbOptions);
  223.         if (DB::isError($db)) {
  224.             return $db;
  225.         }
  226.         $this->_db = $db;
  227.  
  228.         return true;
  229.     }
  230.     // }}}
  231.  
  232.     // {{{ setMetarSource()
  233.     /**
  234.      * Sets the source, where the class tries to locate the METAR/TAF data
  235.      *
  236.      * Source can be http, ftp or file.
  237.      * Alternate sourcepaths can be provided.
  238.      *
  239.      * @param   string                      $sourceMetar
  240.      * @param   string                      $sourcePathMetar
  241.      * @param   string                      $sourceTaf
  242.      * @param   string                      $sourcePathTaf
  243.      * @return  PEAR_ERROR|bool
  244.      * @throws  PEAR_Error::SERVICES_WEATHER_ERROR_METAR_SOURCE_INVALID
  245.      * @access  public
  246.      */
  247.     function setMetarSource($sourceMetar, $sourcePathMetar = "", $sourceTaf = "", $sourcePathTaf = "")
  248.     {
  249.         if (in_array($sourceMetar, array("http", "ftp", "file"))) {
  250.             $this->_sourceMetar = $sourceMetar;
  251.         } else {
  252.             return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_METAR_SOURCE_INVALID, __FILE__, __LINE__);
  253.         }
  254.  
  255.         // Check for a proper METAR source if parameter is set, if not set use defaults
  256.         clearstatcache();
  257.         if (strlen($sourcePathMetar)) {
  258.             if (($this->_sourceMetar == "file" && is_dir($sourcePathMetar)) || ($this->_sourceMetar != "file" && parse_url($sourcePathMetar))) {
  259.                 $this->_sourcePathMetar = $sourcePathMetar;
  260.             } else {
  261.                 return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_METAR_SOURCE_INVALID, __FILE__, __LINE__);
  262.             }
  263.         } else {
  264.             switch ($sourceMetar) {
  265.                 case "http":
  266.                     $this->_sourcePathMetar = "http://weather.noaa.gov/pub/data/observations/metar/stations";
  267.                     break;
  268.                 case "ftp":
  269.                     $this->_sourcePathMetar = "ftp://weather.noaa.gov/data/observations/metar/stations";
  270.                     break;
  271.                 case "file":
  272.                     $this->_sourcePathMetar = ".";
  273.                     break;
  274.             }
  275.         }
  276.  
  277.         if (in_array($sourceTaf, array("http", "ftp", "file"))) {
  278.             $this->_sourceTaf = $sourceTaf;
  279.         } elseif ($sourceTaf != "") {
  280.             return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_METAR_SOURCE_INVALID, __FILE__, __LINE__);
  281.         }
  282.  
  283.         // Check for a proper TAF source if parameter is set, if not set use defaults
  284.         clearstatcache();
  285.         if (strlen($sourcePathTaf)) {
  286.             if (($this->_sourceTaf == "file" && is_dir($sourcePathTaf)) || ($this->_sourceTaf != "file" && parse_url($sourcePathTaf))) {
  287.                 $this->_sourcePathTaf = $sourcePathTaf;
  288.             } else {
  289.                 return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_METAR_SOURCE_INVALID, __FILE__, __LINE__);
  290.             }
  291.         } else {
  292.             switch ($sourceTaf) {
  293.                 case "http":
  294.                     $this->_sourcePathTaf = "http://weather.noaa.gov/pub/data/forecasts/taf/stations";
  295.                     break;
  296.                 case "ftp":
  297.                     $this->_sourcePathTaf = "ftp://weather.noaa.gov/data/forecasts/taf/stations";
  298.                     break;
  299.                 case "file":
  300.                     $this->_sourcePathTaf = ".";
  301.                     break;
  302.             }
  303.         }
  304.  
  305.         return true;
  306.     }
  307.     // }}}
  308.  
  309.     // {{{ _checkLocationID()
  310.     /**
  311.      * Checks the id for valid values and thus prevents silly requests to
  312.      * METAR server
  313.      *
  314.      * @param   string                      $id
  315.      * @return  PEAR_Error|bool
  316.      * @throws  PEAR_Error::SERVICES_WEATHER_ERROR_NO_LOCATION
  317.      * @throws  PEAR_Error::SERVICES_WEATHER_ERROR_INVALID_LOCATION
  318.      * @access  private
  319.      */
  320.     function _checkLocationID($id)
  321.     {
  322.         if (is_array($id) || is_object($id) || !strlen($id)) {
  323.             return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_NO_LOCATION, __FILE__, __LINE__);
  324.         } elseif (!ctype_alnum($id) || (strlen($id) > 4)) {
  325.             return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_INVALID_LOCATION, __FILE__, __LINE__);
  326.         }
  327.  
  328.         return true;
  329.     }
  330.     // }}}
  331.  
  332.     /**
  333.      * Downloads the weather- or forecast-data for an id from the server dependant on the datatype and returns it
  334.      *
  335.      * @param   string                      $id
  336.      * @param   string                      $dataType
  337.      * @return  PEAR_Error|array
  338.      * @throws  PEAR_Error::SERVICES_WEATHER_ERROR_WRONG_SERVER_DATA
  339.      * @access  private
  340.      */
  341.     // {{{ _retrieveServerData()
  342.     function _retrieveServerData($id, $dataType) {
  343.         switch($this->{"_source".ucfirst($dataType)}) {
  344.             case "file":
  345.                 // File source is used, get file and read as-is into a string
  346.                 $source = realpath($this->{"_sourcePath".ucfirst($dataType)}."/".$id.".TXT");
  347.                 $data = @file_get_contents($source);
  348.                 if ($data === false) {
  349.                     return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_WRONG_SERVER_DATA, __FILE__, __LINE__);
  350.                 }
  351.                 break;
  352.             case "http":
  353.                 // HTTP used, acquire request object and fetch data from webserver. Return body of reply
  354.                 include_once "HTTP/Request.php";
  355.  
  356.                 $request = &new HTTP_Request($this->{"_sourcePath".ucfirst($dataType)}."/".$id.".TXT", $this->_httpOptions);
  357.                 $status = $request->sendRequest();
  358.                 if (Services_Weather::isError($status) || (int) $request->getResponseCode() <> 200) {
  359.                     return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_WRONG_SERVER_DATA, __FILE__, __LINE__);
  360.                 }
  361.  
  362.                 $data = $request->getResponseBody();
  363.                 break;
  364.             case "ftp":
  365.                 // FTP as source, acquire neccessary object first
  366.                 include_once "Net/FTP.php";
  367.  
  368.                 // Parse source to get the server data
  369.                 $server = parse_url($this->{"_sourcePath".ucfirst($dataType)}."/".$id.".TXT");
  370.  
  371.                 // If neccessary options are not set, use defaults
  372.                 if (!isset($server["port"]) || $server["port"] == "" || $server["port"] == 0) {
  373.                     $server["port"] = 21;
  374.                 }
  375.                 if (!isset($server["user"]) || $server["user"] == "") {
  376.                     $server["user"] = "ftp";
  377.                 }
  378.                 if (!isset($server["pass"]) || $server["pass"] == "") {
  379.                     $server["pass"] = "ftp@";
  380.                 }
  381.  
  382.                 // Instantiate object and connect to server
  383.                 $ftp = &new Net_FTP($server["host"], $server["port"], $this->_httpOptions["timeout"]);
  384.                 $status = $ftp->connect();
  385.                 if (Services_Weather::isError($status)) {
  386.                     return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_WRONG_SERVER_DATA, __FILE__, __LINE__);
  387.                 }
  388.  
  389.                 // Login to server...
  390.                 $status = $ftp->login($server["user"], $server["pass"]);
  391.                 if (Services_Weather::isError($status)) {
  392.                     return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_WRONG_SERVER_DATA, __FILE__, __LINE__);
  393.                 }
  394.  
  395.                 // ...and retrieve the data into a temporary file
  396.                 $tempfile = tempnam("./", "Services_Weather_Metar");
  397.                 $status = $ftp->get($server["path"], $tempfile, true, FTP_ASCII);
  398.                 if (Services_Weather::isError($status)) {
  399.                     unlink($tempfile);
  400.                     return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_WRONG_SERVER_DATA, __FILE__, __LINE__);
  401.                 }
  402.  
  403.                 // Disconnect FTP server, and read data from temporary file
  404.                 $ftp->disconnect();
  405.                 $data = @file_get_contents($tempfile);
  406.                 unlink($tempfile);
  407.                 break;
  408.         }
  409.  
  410.         // Split data into an array and return
  411.         return preg_split("/\n|\r\n|\n\r/", $data);
  412.     }
  413.     // }}}
  414.  
  415.     // {{{ _parseWeatherData()
  416.     /**
  417.      * Parses the data and caches it
  418.      *
  419.      * METAR KPIT 091955Z COR 22015G25KT 3/4SM R28L/2600FT TSRA OVC010CB
  420.      * 18/16 A2992 RMK SLP045 T01820159
  421.      *
  422.      * @param   array                       $data
  423.      * @return  PEAR_Error|array
  424.      * @throws  PEAR_Error::SERVICES_WEATHER_ERROR_WRONG_SERVER_DATA
  425.      * @throws  PEAR_Error::SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION
  426.      * @access  private
  427.      */
  428.     function _parseWeatherData($data)
  429.     {
  430.         static $compass;
  431.         static $clouds;
  432.         static $cloudtypes;
  433.         static $conditions;
  434.         static $sensors;
  435.         if (!isset($compass)) {
  436.             $compass = array(
  437.                 "N", "NNE", "NE", "ENE",
  438.                 "E", "ESE", "SE", "SSE",
  439.                 "S", "SSW", "SW", "WSW",
  440.                 "W", "WNW", "NW", "NNW"
  441.             );
  442.             $clouds    = array(
  443.                 "skc"         => "sky clear",
  444.                 "nsc"         => "no significant cloud",
  445.                 "few"         => "few",
  446.                 "sct"         => "scattered",
  447.                 "bkn"         => "broken",
  448.                 "ovc"         => "overcast",
  449.                 "vv"          => "vertical visibility",
  450.                 "tcu"         => "Towering Cumulus",
  451.                 "cb"          => "Cumulonimbus",
  452.                 "clr"         => "clear below 12,000 ft"
  453.             );
  454.             $cloudtypes = array(
  455.                 "low" => array(
  456.                     "/" => "Overcast",
  457.                     "0" => "None",                                "1" => "Cumulus (fair weather)",
  458.                     "2" => "Cumulus (towering)",                  "3" => "Cumulonimbus (no anvil)",
  459.                     "4" => "Stratocumulus (from Cumulus)",        "5" => "Stratocumulus (not Cumulus)",
  460.                     "6" => "Stratus or Fractostratus (fair)",     "7" => "Fractocumulus/Fractostratus (bad weather)",
  461.                     "8" => "Cumulus and Stratocumulus",           "9" => "Cumulonimbus (thunderstorm)"
  462.                 ),
  463.                 "middle" => array(
  464.                     "/" => "Overcast",
  465.                     "0" => "None",                                "1" => "Altostratus (thin)",
  466.                     "2" => "Altostratus (thick)",                 "3" => "Altocumulus (thin)",
  467.                     "4" => "Altocumulus (patchy)",                "5" => "Altocumulus (thickening)",
  468.                     "6" => "Altocumulus (from Cumulus)",          "7" => "Altocumulus (w/ Altocumulus, Altostratus, Nimbostratus)",
  469.                     "8" => "Altocumulus (w/ turrets)",            "9" => "Altocumulus (chaotic)"
  470.                 ),
  471.                 "high" => array(
  472.                     "/" => "Overcast",
  473.                     "0" => "None",                                "1" => "Cirrus (filaments)",
  474.                     "2" => "Cirrus (dense)",                      "3" => "Cirrus (often w/ Cumulonimbus)",
  475.                     "4" => "Cirrus (thickening)",                 "5" => "Cirrus/Cirrostratus (low in sky)",
  476.                     "6" => "Cirrus/Cirrostratus (high in sky)",   "7" => "Cirrostratus (entire sky)",
  477.                     "8" => "Cirrostratus (partial)",              "9" => "Cirrocumulus or Cirrocumulus/Cirrus/Cirrostratus"
  478.                 )
  479.             );
  480.             $conditions = array(
  481.                 "+"           => "heavy",                   "-"           => "light",
  482.  
  483.                 "vc"          => "vicinity",                "re"          => "recent",
  484.                 "nsw"         => "no significant weather",
  485.  
  486.                 "mi"          => "shallow",                 "bc"          => "patches",
  487.                 "pr"          => "partial",                 "ts"          => "thunderstorm",
  488.                 "bl"          => "blowing",                 "sh"          => "showers",
  489.                 "dr"          => "low drifting",            "fz"          => "freezing",
  490.  
  491.                 "dz"          => "drizzle",                 "ra"          => "rain",
  492.                 "sn"          => "snow",                    "sg"          => "snow grains",
  493.                 "ic"          => "ice crystals",            "pe"          => "ice pellets",
  494.                 "pl"          => "ice pellets",             "gr"          => "hail",
  495.                 "gs"          => "small hail/snow pellets", "up"          => "unknown precipitation",
  496.  
  497.                 "br"          => "mist",                    "fg"          => "fog",
  498.                 "fu"          => "smoke",                   "va"          => "volcanic ash",
  499.                 "sa"          => "sand",                    "hz"          => "haze",
  500.                 "py"          => "spray",                   "du"          => "widespread dust",
  501.  
  502.                 "sq"          => "squall",                  "ss"          => "sandstorm",
  503.                 "ds"          => "duststorm",               "po"          => "well developed dust/sand whirls",
  504.                 "fc"          => "funnel cloud",
  505.  
  506.                 "+fc"         => "tornado/waterspout"
  507.             );
  508.             $sensors = array(
  509.                 "rvrno"  => "Runway Visual Range Detector offline",
  510.                 "pwino"  => "Present Weather Identifier offline",
  511.                 "pno"    => "Tipping Bucket Rain Gauge offline",
  512.                 "fzrano" => "Freezing Rain Sensor offline",
  513.                 "tsno"   => "Lightning Detection System offline",
  514.                 "visno"  => "2nd Visibility Sensor offline",
  515.                 "chino"  => "2nd Ceiling Height Indicator offline"
  516.             );
  517.         }
  518.  
  519.         $metarCode = array(
  520.             "report"      => "METAR|SPECI",
  521.             "station"     => "\w{4}",
  522.             "update"      => "(\d{2})?(\d{4})Z",
  523.             "type"        => "AUTO|COR",
  524.             "wind"        => "(\d{3}|VAR|VRB)(\d{2,3})(G(\d{2,3}))?(FPS|KPH|KT|KTS|MPH|MPS)",
  525.             "windVar"     => "(\d{3})V(\d{3})",
  526.             "visFrac"     => "(\d{1})",
  527.             "visibility"  => "(\d{4})|((M|P)?((\d{1,2}|((\d) )?(\d)\/(\d))(SM|KM)))|(CAVOK)",
  528.             "runway"      => "R(\d{2})(\w)?\/(P|M)?(\d{4})(FT)?(V(P|M)?(\d{4})(FT)?)?(\w)?",
  529.             "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)?",
  530.             "clouds"      => "(SKC|CLR|NSC|((FEW|SCT|BKN|OVC|VV)(\d{3}|\/{3})(TCU|CB)?))",
  531.             "temperature" => "(M)?(\d{2})\/((M)?(\d{2})|XX|\/\/)?",
  532.             "pressure"    => "(A)(\d{4})|(Q)(\d{4})",
  533.             "trend"       => "NOSIG|TEMPO|BECMG",
  534.             "remark"      => "RMK"
  535.         );
  536.  
  537.         $remarks = array(
  538.             "nospeci"     => "NOSPECI",
  539.             "autostation" => "AO(1|2)",
  540.             "presschg"    => "PRES(R|F)R",
  541.             "seapressure" => "SLP(\d{3}|NO)",
  542.             "precip"      => "(P|6|7)(\d{4}|\/{4})",
  543.             "snowdepth"   => "4\/(\d{3})",
  544.             "snowequiv"   => "933(\d{3})",
  545.             "cloudtypes"  => "8\/(\d|\/)(\d|\/)(\d|\/)",
  546.             "sunduration" => "98(\d{3})",
  547.             "1htempdew"   => "T(0|1)(\d{3})((0|1)(\d{3}))?",
  548.             "6hmaxtemp"   => "1(0|1)(\d{3})",
  549.             "6hmintemp"   => "2(0|1)(\d{3})",
  550.             "24htemp"     => "4(0|1)(\d{3})(0|1)(\d{3})",
  551.             "3hpresstend" => "5([0-8])(\d{3})",
  552.             "sensors"     => "RVRNO|PWINO|PNO|FZRANO|TSNO|VISNO|CHINO",
  553.             "maintain"    => "[\$]"
  554.         );
  555.  
  556.         if (SERVICES_WEATHER_DEBUG) {
  557.             for ($i = 0; $i < sizeof($data); $i++) {
  558.                 echo $data[$i]."\n";
  559.             }
  560.         }
  561.         // Start with parsing the first line for the last update
  562.         $weatherData = array();
  563.         $weatherData["station"]   = "";
  564.         $weatherData["dataRaw"]   = implode(" ", $data);
  565.         $weatherData["update"]    = strtotime(trim($data[0])." GMT");
  566.         $weatherData["updateRaw"] = trim($data[0]);
  567.         // and prepare the rest for stepping through
  568.         array_shift($data);
  569.         $metar = explode(" ", preg_replace("/\s{2,}/", " ", implode(" ", $data)));
  570.  
  571.         // Add a few local variables for data processing
  572.         $trendCount = 0;             // If we have trends, we need this
  573.         $pointer    =& $weatherData; // Pointer to the array we add the data to
  574.         for ($i = 0; $i < sizeof($metar); $i++) {
  575.             // Check for whitespace and step loop, if nothing's there
  576.             $metar[$i] = trim($metar[$i]);
  577.             if (!strlen($metar[$i])) {
  578.                 continue;
  579.             }
  580.  
  581.             if (SERVICES_WEATHER_DEBUG) {
  582.                 $tab = str_repeat("\t", 3 - floor((strlen($metar[$i]) + 2) / 8));
  583.                 echo "\"".$metar[$i]."\"".$tab."-> ";
  584.             }
  585.  
  586.             // Initialize some arrays
  587.             $result   = array();
  588.             $resultVF = array();
  589.             $lresult  = array();
  590.  
  591.             $found = false;
  592.             foreach ($metarCode as $key => $regexp) {
  593.                 // Check if current code matches current metar snippet
  594.                 if (($found = preg_match("/^".$regexp."$/i", $metar[$i], $result)) == true) {
  595.                     switch ($key) {
  596.                         case "station":
  597.                             $pointer["station"] = $result[0];
  598.                             unset($metarCode["station"]);
  599.                             break;
  600.                         case "wind":
  601.                             // Parse wind data, first the speed, convert from kt to chosen unit
  602.                             if ($result[5] == "KTS") {
  603.                                 $result[5] = "KT";
  604.                             }
  605.                             $pointer["wind"] = $this->convertSpeed($result[2], $result[5], "mph");
  606.                             if ($result[1] == "VAR" || $result[1] == "VRB") {
  607.                                 // Variable winds
  608.                                 $pointer["windDegrees"]   = "Variable";
  609.                                 $pointer["windDirection"] = "Variable";
  610.                             } else {
  611.                                 // Save wind degree and calc direction
  612.                                 $pointer["windDegrees"]   = intval($result[1]);
  613.                                 $pointer["windDirection"] = $compass[round($result[1] / 22.5) % 16];
  614.                             }
  615.                             if (is_numeric($result[4])) {
  616.                                 // Wind with gusts...
  617.                                 $pointer["windGust"] = $this->convertSpeed($result[4], $result[5], "mph");
  618.                             }
  619.                             break;
  620.                         case "windVar":
  621.                             // Once more wind, now variability around the current wind-direction
  622.                             $pointer["windVariability"] = array("from" => intval($result[1]), "to" => intval($result[2]));
  623.                             break;
  624.                         case "visFrac":
  625.                             // Possible fractional visibility here. Check if it matches with the next METAR piece for visibility
  626.                             if (!isset($metar[$i + 1]) || !preg_match("/^".$metarCode["visibility"]."$/i", $result[1]." ".$metar[$i + 1], $resultVF)) {
  627.                                 // No next METAR piece available or not matching. Match against next METAR code
  628.                                 $found = false;
  629.                                 break;
  630.                             } else {
  631.                                 // Match. Hand over result and advance METAR
  632.                                 if (SERVICES_WEATHER_DEBUG) {
  633.                                     echo $key."\n";
  634.                                     echo "\"".$result[1]." ".$metar[$i + 1]."\"".str_repeat("\t", 2 - floor((strlen($result[1]." ".$metar[$i + 1]) + 2) / 8))."-> ";
  635.                                 }
  636.                                 $key = "visibility";
  637.                                 $result = $resultVF;
  638.                                 $i++;
  639.                             }
  640.                         case "visibility":
  641.                             $pointer["visQualifier"] = "AT";
  642.                             if (is_numeric($result[1]) && ($result[1] == 9999)) {
  643.                                 // Upper limit of visibility range
  644.                                 $visibility = $this->convertDistance(10, "km", "sm");
  645.                                 $pointer["visQualifier"] = "BEYOND";
  646.                             } elseif (is_numeric($result[1])) {
  647.                                 // 4-digit visibility in m
  648.                                 $visibility = $this->convertDistance(($result[1]/1000), "km", "sm");
  649.                             } elseif (!isset($result[11]) || $result[11] != "CAVOK") {
  650.                                 if ($result[3] == "M") {
  651.                                     $pointer["visQualifier"] = "BELOW";
  652.                                 } elseif ($result[3] == "P") {
  653.                                     $pointer["visQualifier"] = "BEYOND";
  654.                                 }
  655.                                 if (is_numeric($result[5])) {
  656.                                     // visibility as one/two-digit number
  657.                                     $visibility = $this->convertDistance($result[5], $result[10], "sm");
  658.                                 } else {
  659.                                     // the y/z part, add if we had a x part (see visibility1)
  660.                                     if (is_numeric($result[7])) {
  661.                                         $visibility = $this->convertDistance($result[7] + $result[8] / $result[9], $result[10], "sm");
  662.                                     } else {
  663.                                         $visibility = $this->convertDistance($result[8] / $result[9], $result[10], "sm");
  664.                                     }
  665.                                 }
  666.                             } else {
  667.                                 $pointer["visQualifier"] = "BEYOND";
  668.                                 $visibility = $this->convertDistance(10, "km", "sm");
  669.                                 $pointer["clouds"] = array(array("amount" => "Clear below", "height" => 5000));
  670.                                 $pointer["condition"] = "no significant weather";
  671.                             }
  672.                             $pointer["visibility"] = $visibility;
  673.                             break;
  674.                         case "condition":
  675.                             // First some basic setups
  676.                             if (!isset($pointer["condition"])) {
  677.                                 $pointer["condition"] = "";
  678.                             } elseif (strlen($pointer["condition"]) > 0) {
  679.                                 $pointer["condition"] .= ",";
  680.                             }
  681.  
  682.                             if (in_array(strtolower($result[0]), $conditions)) {
  683.                                 // First try matching the complete string
  684.                                 $pointer["condition"] .= " ".$conditions[strtolower($result[0])];
  685.                             } else {
  686.                                 // No luck, match part by part
  687.                                 array_shift($result);
  688.                                 $result = array_unique($result);
  689.                                 foreach ($result as $condition) {
  690.                                     if (strlen($condition) > 0) {
  691.                                         $pointer["condition"] .= " ".$conditions[strtolower($condition)];
  692.                                     }
  693.                                 }
  694.                             }
  695.                             $pointer["condition"] = trim($pointer["condition"]);
  696.                             break;
  697.                         case "clouds":
  698.                             if (!isset($pointer["clouds"])) {
  699.                                 $pointer["clouds"] = array();
  700.                             }
  701.  
  702.                             if (sizeof($result) == 5) {
  703.                                 // Only amount and height
  704.                                 $cloud = array("amount" => $clouds[strtolower($result[3])]);
  705.                                 if ($result[4] == "///") {
  706.                                     $cloud["height"] = "station level or below";
  707.                                 } else {
  708.                                     $cloud["height"] = $result[4] * 100;
  709.                                 }
  710.                             } elseif (sizeof($result) == 6) {
  711.                                 // Amount, height and type
  712.                                 $cloud = array("amount" => $clouds[strtolower($result[3])], "type" => $clouds[strtolower($result[5])]);
  713.                                 if ($result[4] == "///") {
  714.                                     $cloud["height"] = "station level or below";
  715.                                 } else {
  716.                                     $cloud["height"] = $result[4] * 100;
  717.                                 }
  718.                             } else {
  719.                                 // SKC or CLR or NSC
  720.                                 $cloud = array("amount" => $clouds[strtolower($result[0])]);
  721.                             }
  722.                             $pointer["clouds"][] = $cloud;
  723.                             break;
  724.                         case "temperature":
  725.                             // normal temperature in first part
  726.                             // negative value
  727.                             if ($result[1] == "M") {
  728.                                 $result[2] *= -1;
  729.                             }
  730.                             $pointer["temperature"] = $this->convertTemperature($result[2], "c", "f");
  731.                             if (sizeof($result) > 4) {
  732.                                 // same for dewpoint
  733.                                 if ($result[4] == "M") {
  734.                                     $result[5] *= -1;
  735.                                 }
  736.                                 $pointer["dewPoint"] = $this->convertTemperature($result[5], "c", "f");
  737.                                 $pointer["humidity"] = $this->calculateHumidity($result[2], $result[5]);
  738.                             }
  739.                             if (isset($pointer["wind"])) {
  740.                                 // Now calculate windchill from temperature and windspeed
  741.                                 $pointer["feltTemperature"] = $this->calculateWindChill($pointer["temperature"], $pointer["wind"]);
  742.                             }
  743.                             break;
  744.                         case "pressure":
  745.                             if ($result[1] == "A") {
  746.                                 // Pressure provided in inches
  747.                                 $pointer["pressure"] = $result[2] / 100;
  748.                             } elseif ($result[3] == "Q") {
  749.                                 // ... in hectopascal
  750.                                 $pointer["pressure"] = $this->convertPressure($result[4], "hpa", "in");
  751.                             }
  752.                             break;
  753.                         case "trend":
  754.                             // We may have a trend here... extract type and set pointer on
  755.                             // created new array
  756.                             if (!isset($weatherData["trend"])) {
  757.                                 $weatherData["trend"] = array();
  758.                                 $weatherData["trend"][$trendCount] = array();
  759.                             }
  760.                             $pointer =& $weatherData["trend"][$trendCount];
  761.                             $trendCount++;
  762.                             $pointer["type"] = $result[0];
  763.                             while (isset($metar[$i + 1]) && preg_match("/^(FM|TL|AT)(\d{2})(\d{2})$/i", $metar[$i + 1], $lresult)) {
  764.                                 if ($lresult[1] == "FM") {
  765.                                     $pointer["from"] = $lresult[2].":".$lresult[3];
  766.                                 } elseif ($lresult[1] == "TL") {
  767.                                     $pointer["to"] = $lresult[2].":".$lresult[3];
  768.                                 } else {
  769.                                     $pointer["at"] = $lresult[2].":".$lresult[3];
  770.                                 }
  771.                                 // As we have just extracted the time for this trend
  772.                                 // from our METAR, increase field-counter
  773.                                 $i++;
  774.                             }
  775.                             break;
  776.                         case "remark":
  777.                             // Remark part begins
  778.                             $metarCode = $remarks;
  779.                             $weatherData["remark"] = array();
  780.                             break;
  781.                         case "autostation":
  782.                             // Which autostation do we have here?
  783.                             if ($result[1] == 0) {
  784.                                 $weatherData["remark"]["autostation"] = "Automatic weatherstation w/o precipitation discriminator";
  785.                             } else {
  786.                                 $weatherData["remark"]["autostation"] = "Automatic weatherstation w/ precipitation discriminator";
  787.                             }
  788.                             unset($metarCode["autostation"]);
  789.                             break;
  790.                         case "presschg":
  791.                             // Decoding for rapid pressure changes
  792.                             if (strtolower($result[1]) == "r") {
  793.                                 $weatherData["remark"]["presschg"] = "Pressure rising rapidly";
  794.                             } else {
  795.                                 $weatherData["remark"]["presschg"] = "Pressure falling rapidly";
  796.                             }
  797.                             unset($metarCode["presschg"]);
  798.                             break;
  799.                         case "seapressure":
  800.                             // Pressure at sea level (delivered in hpa)
  801.                             // Decoding is a bit obscure as 982 gets 998.2
  802.                             // whereas 113 becomes 1113 -> no real rule here
  803.                             if (strtolower($result[1]) != "no") {
  804.                                 if ($result[1] > 500) {
  805.                                     $press = 900 + round($result[1] / 100, 1);
  806.                                 } else {
  807.                                     $press = 1000 + $result[1];
  808.                                 }
  809.                                 $weatherData["remark"]["seapressure"] = $this->convertPressure($press, "hpa", "in");
  810.                             }
  811.                             unset($metarCode["seapressure"]);
  812.                             break;
  813.                         case "precip":
  814.                             // Precipitation in inches
  815.                             static $hours;
  816.                             if (!isset($weatherData["precipitation"])) {
  817.                                 $weatherData["precipitation"] = array();
  818.                                 $hours = array("P" => "1", "6" => "3/6", "7" => "24");
  819.                             }
  820.                             if (!is_numeric($result[2])) {
  821.                                 $precip = "indeterminable";
  822.                             } elseif ($result[2] == "0000") {
  823.                                 $precip = "traceable";
  824.                             } else {
  825.                                 $precip = $result[2] / 100;
  826.                             }
  827.                             $weatherData["precipitation"][] = array(
  828.                                 "amount" => $precip,
  829.                                 "hours"  => $hours[$result[1]]
  830.                             );
  831.                             break;
  832.                         case "snowdepth":
  833.                             // Snow depth in inches
  834.                             $weatherData["remark"]["snowdepth"] = $result[1];
  835.                             unset($metarCode["snowdepth"]);
  836.                             break;
  837.                         case "snowequiv":
  838.                             // Same for equivalent in Water... (inches)
  839.                             $weatherData["remark"]["snowequiv"] = $result[1] / 10;
  840.                             unset($metarCode["snowequiv"]);
  841.                             break;
  842.                         case "cloudtypes":
  843.                             // Cloud types
  844.                             $weatherData["remark"]["cloudtypes"] = array(
  845.                                 "low"    => $cloudtypes["low"][$result[1]],
  846.                                 "middle" => $cloudtypes["middle"][$result[2]],
  847.                                 "high"   => $cloudtypes["high"][$result[3]]
  848.                             );
  849.                             unset($metarCode["cloudtypes"]);
  850.                             break;
  851.                         case "sunduration":
  852.                             // Duration of sunshine (in minutes)
  853.                             $weatherData["remark"]["sunduration"] = "Total minutes of sunshine: ".$result[1];
  854.                             unset($metarCode["sunduration"]);
  855.                             break;
  856.                         case "1htempdew":
  857.                             // Temperatures in the last hour in C
  858.                             if ($result[1] == "1") {
  859.                                 $result[2] *= -1;
  860.                             }
  861.                             $weatherData["remark"]["1htemp"] = $this->convertTemperature($result[2] / 10, "c", "f");
  862.  
  863.                             if (sizeof($result) > 3) {
  864.                                 // same for dewpoint
  865.                                 if ($result[4] == "1") {
  866.                                     $result[5] *= -1;
  867.                                 }
  868.                                 $weatherData["remark"]["1hdew"] = $this->convertTemperature($result[5] / 10, "c", "f");
  869.                             }
  870.                             unset($metarCode["1htempdew"]);
  871.                             break;
  872.                         case "6hmaxtemp":
  873.                             // Max temperature in the last 6 hours in C
  874.                             if ($result[1] == "1") {
  875.                                 $result[2] *= -1;
  876.                             }
  877.                             $weatherData["remark"]["6hmaxtemp"] = $this->convertTemperature($result[2] / 10, "c", "f");
  878.                             unset($metarCode["6hmaxtemp"]);
  879.                             break;
  880.                         case "6hmintemp":
  881.                             // Min temperature in the last 6 hours in C
  882.                             if ($result[1] == "1") {
  883.                                 $result[2] *= -1;
  884.                             }
  885.                             $weatherData["remark"]["6hmintemp"] = $this->convertTemperature($result[2] / 10, "c", "f");
  886.                             unset($metarCode["6hmintemp"]);
  887.                             break;
  888.                         case "24htemp":
  889.                             // Max/Min temperatures in the last 24 hours in C
  890.                             if ($result[1] == "1") {
  891.                                 $result[2] *= -1;
  892.                             }
  893.                             $weatherData["remark"]["24hmaxtemp"] = $this->convertTemperature($result[2] / 10, "c", "f");
  894.  
  895.                             if ($result[3] == "1") {
  896.                                 $result[4] *= -1;
  897.                             }
  898.                             $weatherData["remark"]["24hmintemp"] = $this->convertTemperature($result[4] / 10, "c", "f");
  899.                             unset($metarCode["24htemp"]);
  900.                             break;
  901.                         case "3hpresstend":
  902.                             // Pressure tendency of the last 3 hours
  903.                             // no special processing, just passing the data
  904.                             $weatherData["remark"]["3hpresstend"] = array(
  905.                                 "presscode" => $result[1],
  906.                                 "presschng" => $this->convertPressure($result[2] / 10, "hpa", "in")
  907.                             );
  908.                             unset($metarCode["3hpresstend"]);
  909.                             break;
  910.                         case "nospeci":
  911.                             // No change during the last hour
  912.                             $weatherData["remark"]["nospeci"] = "No changes in weather conditions";
  913.                             unset($metarCode["nospeci"]);
  914.                             break;
  915.                         case "sensors":
  916.                             // We may have multiple broken sensors, so do not unset
  917.                             if (!isset($weatherData["remark"]["sensors"])) {
  918.                                 $weatherData["remark"]["sensors"] = array();
  919.                             }
  920.                             $weatherData["remark"]["sensors"][strtolower($result[0])] = $sensors[strtolower($result[0])];
  921.                             break;
  922.                         case "maintain":
  923.                             $weatherData["remark"]["maintain"] = "Maintainance needed";
  924.                             unset($metarCode["maintain"]);
  925.                             break;
  926.                         default:
  927.                             // Do nothing, just prevent further matching
  928.                             unset($metarCode[$key]);
  929.                             break;
  930.                     }
  931.                     if ($found && !SERVICES_WEATHER_DEBUG) {
  932.                         break;
  933.                     } elseif ($found && SERVICES_WEATHER_DEBUG) {
  934.                         echo $key."\n";
  935.                         break;
  936.                     }
  937.                 }
  938.             }
  939.             if (!$found) {
  940.                 if (SERVICES_WEATHER_DEBUG) {
  941.                     echo "n/a\n";
  942.                 }
  943.                 if (!isset($weatherData["noparse"])) {
  944.                     $weatherData["noparse"] = array();
  945.                 }
  946.                 $weatherData["noparse"][] = $metar[$i];
  947.             }
  948.         }
  949.  
  950.         if (isset($weatherData["noparse"])) {
  951.             $weatherData["noparse"] = implode(" ",  $weatherData["noparse"]);
  952.         }
  953.  
  954.         return $weatherData;
  955.     }
  956.     // }}}
  957.  
  958.     // {{{ _parseForecastData()
  959.     /**
  960.      * Parses the data and caches it
  961.      *
  962.      * TAF KLGA 271734Z 271818 11007KT P6SM -RA SCT020 BKN200
  963.      *     FM2300 14007KT P6SM SCT030 BKN150
  964.      *     FM0400 VRB03KT P6SM SCT035 OVC080 PROB30 0509 P6SM -RA BKN035
  965.      *     FM0900 VRB03KT 6SM -RA BR SCT015 OVC035
  966.      *         TEMPO 1215 5SM -RA BR SCT009 BKN015
  967.      *         BECMG 1517 16007KT P6SM NSW SCT015 BKN070
  968.      *
  969.      * @param   array                       $data
  970.      * @return  PEAR_Error|array
  971.      * @throws  PEAR_Error::SERVICES_WEATHER_ERROR_WRONG_SERVER_DATA
  972.      * @throws  PEAR_Error::SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION
  973.      * @access  private
  974.      */
  975.     function _parseForecastData($data)
  976.     {
  977.         static $compass;
  978.         static $clouds;
  979.         static $conditions;
  980.         static $sensors;
  981.         if (!isset($compass)) {
  982.             $compass = array(
  983.                 "N", "NNE", "NE", "ENE",
  984.                 "E", "ESE", "SE", "SSE",
  985.                 "S", "SSW", "SW", "WSW",
  986.                 "W", "WNW", "NW", "NNW"
  987.             );
  988.             $clouds    = array(
  989.                 "skc"         => "sky clear",
  990.                 "nsc"         => "no significant cloud",
  991.                 "few"         => "few",
  992.                 "sct"         => "scattered",
  993.                 "bkn"         => "broken",
  994.                 "ovc"         => "overcast",
  995.                 "vv"          => "vertical visibility",
  996.                 "tcu"         => "Towering Cumulus",
  997.                 "cb"          => "Cumulonimbus",
  998.                 "clr"         => "clear below 12,000 ft"
  999.             );
  1000.             $conditions = array(
  1001.                 "+"           => "heavy",                   "-"           => "light",
  1002.  
  1003.                 "vc"          => "vicinity",                "re"          => "recent",
  1004.                 "nsw"         => "no significant weather",
  1005.  
  1006.                 "mi"          => "shallow",                 "bc"          => "patches",
  1007.                 "pr"          => "partial",                 "ts"          => "thunderstorm",
  1008.                 "bl"          => "blowing",                 "sh"          => "showers",
  1009.                 "dr"          => "low drifting",            "fz"          => "freezing",
  1010.  
  1011.                 "dz"          => "drizzle",                 "ra"          => "rain",
  1012.                 "sn"          => "snow",                    "sg"          => "snow grains",
  1013.                 "ic"          => "ice crystals",            "pe"          => "ice pellets",
  1014.                 "pl"          => "ice pellets",             "gr"          => "hail",
  1015.                 "gs"          => "small hail/snow pellets", "up"          => "unknown precipitation",
  1016.  
  1017.                 "br"          => "mist",                    "fg"          => "fog",
  1018.                 "fu"          => "smoke",                   "va"          => "volcanic ash",
  1019.                 "sa"          => "sand",                    "hz"          => "haze",
  1020.                 "py"          => "spray",                   "du"          => "widespread dust",
  1021.  
  1022.                 "sq"          => "squall",                  "ss"          => "sandstorm",
  1023.                 "ds"          => "duststorm",               "po"          => "well developed dust/sand whirls",
  1024.                 "fc"          => "funnel cloud",
  1025.  
  1026.                 "+fc"         => "tornado/waterspout"
  1027.             );
  1028.         }
  1029.  
  1030.         $tafCode = array(
  1031.             "report"      => "TAF|AMD",
  1032.             "station"     => "\w{4}",
  1033.             "update"      => "(\d{2})?(\d{4})Z",
  1034.             "valid"       => "(\d{2})(\d{2})(\d{2})",
  1035.             "wind"        => "(\d{3}|VAR|VRB)(\d{2,3})(G(\d{2,3}))?(FPS|KPH|KT|KTS|MPH|MPS)",
  1036.             "visFrac"     => "(\d{1})",
  1037.             "visibility"  => "(\d{4})|((M|P)?((\d{1,2}|((\d) )?(\d)\/(\d))(SM|KM)))|(CAVOK)",
  1038.             "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)?",
  1039.             "clouds"      => "(SKC|CLR|NSC|((FEW|SCT|BKN|OVC|VV)(\d{3}|\/{3})(TCU|CB)?))",
  1040.             "windshear"   => "WS(\d{3})\/(\d{3})(\d{2,3})(FPS|KPH|KT|KTS|MPH|MPS)",
  1041.             "tempmax"     => "TX(\d{2})\/(\d{2})(\w)",
  1042.             "tempmin"     => "TN(\d{2})\/(\d{2})(\w)",
  1043.             "tempmaxmin"  => "TX(\d{2})\/(\d{2})(\w)TN(\d{2})\/(\d{2})(\w)",
  1044.             "from"        => "FM(\d{2})(\d{2})?Z?",
  1045.             "fmc"         => "(PROB|BECMG|TEMPO)(\d{2})?"
  1046.         );
  1047.  
  1048.         if (SERVICES_WEATHER_DEBUG) {
  1049.             for ($i = 0; $i < sizeof($data); $i++) {
  1050.                 echo $data[$i]."\n";
  1051.             }
  1052.         }
  1053.         // Ok, we have correct data, start with parsing the first line for the last update
  1054.         $forecastData = array();
  1055.         $forecastData["station"]   = "";
  1056.         $forecastData["dataRaw"]   = implode(" ", $data);
  1057.         $forecastData["update"]    = strtotime(trim($data[0])." GMT");
  1058.         $forecastData["updateRaw"] = trim($data[0]);
  1059.         // and prepare the rest for stepping through
  1060.         array_shift($data);
  1061.         $taf = explode(" ", preg_replace("/\s{2,}/", " ", implode(" ", $data)));
  1062.  
  1063.         // Add a few local variables for data processing
  1064.         $fromTime =  "";            // The timeperiod the data gets added to
  1065.         $fmcCount =  0;             // If we have FMCs (Forecast Meteorological Conditions), we need this
  1066.         $pointer  =& $forecastData; // Pointer to the array we add the data to
  1067.         for ($i = 0; $i < sizeof($taf); $i++) {
  1068.             // Check for whitespace and step loop, if nothing's there
  1069.             $taf[$i] = trim($taf[$i]);
  1070.             if (!strlen($taf[$i])) {
  1071.                 continue;
  1072.             }
  1073.  
  1074.             if (SERVICES_WEATHER_DEBUG) {
  1075.                 $tab = str_repeat("\t", 3 - floor((strlen($taf[$i]) + 2) / 8));
  1076.                 echo "\"".$taf[$i]."\"".$tab."-> ";
  1077.             }
  1078.  
  1079.             // Initialize some arrays
  1080.             $result   = array();
  1081.             $resultVF = array();
  1082.             $lresult  = array();
  1083.  
  1084.             $found = false;
  1085.             foreach ($tafCode as $key => $regexp) {
  1086.                 // Check if current code matches current taf snippet
  1087.                 if (($found = preg_match("/^".$regexp."$/i", $taf[$i], $result)) == true) {
  1088.                     $insert = array();
  1089.                     switch ($key) {
  1090.                         case "station":
  1091.                             $pointer["station"] = $result[0];
  1092.                             unset($tafCode["station"]);
  1093.                             break;
  1094.                         case "valid":
  1095.                             $pointer["validRaw"] = $result[0];
  1096.                             // Generates the timeperiod the report is valid for
  1097.                             list($year, $month, $day) = explode("-", gmdate("Y-m-d", $forecastData["update"]));
  1098.                             // Date is in next month
  1099.                             if ($result[1] < $day) {
  1100.                                 $month++;
  1101.                             }
  1102.                             $pointer["validFrom"] = gmmktime($result[2], 0, 0, $month, $result[1], $year);
  1103.                             // Valid time ends next day
  1104.                             if ($result[2] >= $result[3]) {
  1105.                                 $result[1]++;
  1106.                             }
  1107.                             $pointer["validTo"]   = gmmktime($result[3], 0, 0, $month, $result[1], $year);
  1108.                             unset($tafCode["valid"]);
  1109.                             // Now the groups will start, so initialize the time groups
  1110.                             $pointer["time"] = array();
  1111.                             $fromTime = $result[2].":00";
  1112.                             $pointer["time"][$fromTime] = array();
  1113.                             // Set pointer to the first timeperiod
  1114.                             $pointer =& $pointer["time"][$fromTime];
  1115.                             break;
  1116.                         case "wind":
  1117.                             // Parse wind data, first the speed, convert from kt to chosen unit
  1118.                             if ($result[5] == "KTS") {
  1119.                                 $result[5] = "KT";
  1120.                             }
  1121.                             $pointer["wind"] = $this->convertSpeed($result[2], $result[5], "mph");
  1122.                             if ($result[1] == "VAR" || $result[1] == "VRB") {
  1123.                                 // Variable winds
  1124.                                 $pointer["windDegrees"]   = "Variable";
  1125.                                 $pointer["windDirection"] = "Variable";
  1126.                             } else {
  1127.                                 // Save wind degree and calc direction
  1128.                                 $pointer["windDegrees"]   = $result[1];
  1129.                                 $pointer["windDirection"] = $compass[round($result[1] / 22.5) % 16];
  1130.                             }
  1131.                             if (is_numeric($result[4])) {
  1132.                                 // Wind with gusts...
  1133.                                 $pointer["windGust"] = $this->convertSpeed($result[4], $result[5], "mph");
  1134.                             }
  1135.                             if (isset($probability)) {
  1136.                                 $pointer["windProb"] = $probability;
  1137.                                 unset($probability);
  1138.                             }
  1139.                             break;
  1140.                         case "visFrac":
  1141.                             // Possible fractional visibility here. Check if it matches with the next TAF piece for visibility
  1142.                             if (!isset($taf[$i + 1]) || !preg_match("/^".$tafCode["visibility"]."$/i", $result[1]." ".$taf[$i + 1], $resultVF)) {
  1143.                                 // No next TAF piece available or not matching. Match against next TAF code
  1144.                                 $found = false;
  1145.                                 break;
  1146.                             } else {
  1147.                                 // Match. Hand over result and advance TAF
  1148.                                 if (SERVICES_WEATHER_DEBUG) {
  1149.                                     echo $key."\n";
  1150.                                     echo "\"".$result[1]." ".$taf[$i + 1]."\"".str_repeat("\t", 2 - floor((strlen($result[1]." ".$taf[$i + 1]) + 2) / 8))."-> ";
  1151.                                 }
  1152.                                 $key = "visibility";
  1153.                                 $result = $resultVF;
  1154.                                 $i++;
  1155.                             }
  1156.                         case "visibility":
  1157.                             $pointer["visQualifier"] = "AT";
  1158.                             if (is_numeric($result[1]) && ($result[1] == 9999)) {
  1159.                                 // Upper limit of visibility range
  1160.                                 $visibility = $this->convertDistance(10, "km", "sm");
  1161.                                 $pointer["visQualifier"] = "BEYOND";
  1162.                             } elseif (is_numeric($result[1])) {
  1163.                                 // 4-digit visibility in m
  1164.                                 $visibility = $this->convertDistance(($result[1]/1000), "km", "sm");
  1165.                             } elseif (!isset($result[11]) || $result[11] != "CAVOK") {
  1166.                                 if ($result[3] == "M") {
  1167.                                     $pointer["visQualifier"] = "BELOW";
  1168.                                 } elseif ($result[3] == "P") {
  1169.                                     $pointer["visQualifier"] = "BEYOND";
  1170.                                 }
  1171.                                 if (is_numeric($result[5])) {
  1172.                                     // visibility as one/two-digit number
  1173.                                     $visibility = $this->convertDistance($result[5], $result[10], "sm");
  1174.                                 } else {
  1175.                                     // the y/z part, add if we had a x part (see visibility1)
  1176.                                     if (is_numeric($result[7])) {
  1177.                                         $visibility = $this->convertDistance($result[7] + $result[8] / $result[9], $result[10], "sm");
  1178.                                     } else {
  1179.                                         $visibility = $this->convertDistance($result[8] / $result[9], $result[10], "sm");
  1180.                                     }
  1181.                                 }
  1182.                             } else {
  1183.                                 $pointer["visQualifier"] = "BEYOND";
  1184.                                 $visibility = $this->convertDistance(10, "km", "sm");
  1185.                                 $pointer["clouds"] = array(array("amount" => "Clear below", "height" => 5000));
  1186.                                 $pointer["condition"] = "no significant weather";
  1187.                             }
  1188.                             if (isset($probability)) {
  1189.                                 $pointer["visProb"] = $probability;
  1190.                                 unset($probability);
  1191.                             }
  1192.                             $pointer["visibility"] = $visibility;
  1193.                             break;
  1194.                         case "condition":
  1195.                             // First some basic setups
  1196.                             if (!isset($pointer["condition"])) {
  1197.                                 $pointer["condition"] = "";
  1198.                             } elseif (strlen($pointer["condition"]) > 0) {
  1199.                                 $pointer["condition"] .= ",";
  1200.                             }
  1201.  
  1202.                             if (in_array(strtolower($result[0]), $conditions)) {
  1203.                                 // First try matching the complete string
  1204.                                 $pointer["condition"] .= " ".$conditions[strtolower($result[0])];
  1205.                             } else {
  1206.                                 // No luck, match part by part
  1207.                                 array_shift($result);
  1208.                                 $result = array_unique($result);
  1209.                                 foreach ($result as $condition) {
  1210.                                     if (strlen($condition) > 0) {
  1211.                                         $pointer["condition"] .= " ".$conditions[strtolower($condition)];
  1212.                                     }
  1213.                                 }
  1214.                             }
  1215.                             $pointer["condition"] = trim($pointer["condition"]);
  1216.                             if (isset($probability)) {
  1217.                                 $pointer["condition"] .= " (".$probability."% prob.)";
  1218.                                 unset($probability);
  1219.                             }
  1220.                             break;
  1221.                         case "clouds":
  1222.                             if (!isset($pointer["clouds"])) {
  1223.                                 $pointer["clouds"] = array();
  1224.                             }
  1225.  
  1226.                             if (sizeof($result) == 5) {
  1227.                                 // Only amount and height
  1228.                                 $cloud = array("amount" => $clouds[strtolower($result[3])]);
  1229.                                 if ($result[4] == "///") {
  1230.                                     $cloud["height"] = "station level or below";
  1231.                                 } else {
  1232.                                     $cloud["height"] = $result[4] * 100;
  1233.                                 }
  1234.                             } elseif (sizeof($result) == 6) {
  1235.                                 // Amount, height and type
  1236.                                 $cloud = array("amount" => $clouds[strtolower($result[3])], "type" => $clouds[strtolower($result[5])]);
  1237.                                 if ($result[4] == "///") {
  1238.                                     $cloud["height"] = "station level or below";
  1239.                                 } else {
  1240.                                     $cloud["height"] = $result[4] * 100;
  1241.                                 }
  1242.                             } else {
  1243.                                 // SKC or CLR or NSC
  1244.                                 $cloud = array("amount" => $clouds[strtolower($result[0])]);
  1245.                             }
  1246.                             if (isset($probability)) {
  1247.                                 $cloud["prob"] = $probability;
  1248.                                 unset($probability);
  1249.                             }
  1250.                             $pointer["clouds"][] = $cloud;
  1251.                             break;
  1252.                         case "windshear":
  1253.                             // Parse windshear, if available
  1254.                             if ($result[4] == "KTS") {
  1255.                                 $result[4] = "KT";
  1256.                             }
  1257.                             $pointer["windshear"]          = $this->convertSpeed($result[3], $result[4], "mph");
  1258.                             $pointer["windshearHeight"]    = $result[1] * 100;
  1259.                             $pointer["windshearDegrees"]   = $result[2];
  1260.                             $pointer["windshearDirection"] = $compass[round($result[2] / 22.5) % 16];
  1261.                             break;
  1262.                         case "tempmax":
  1263.                             $forecastData["temperatureHigh"] = $this->convertTemperature($result[1], "c", "f");
  1264.                             break;
  1265.                         case "tempmin":
  1266.                             // Parse max/min temperature
  1267.                             $forecastData["temperatureLow"]  = $this->convertTemperature($result[1], "c", "f");
  1268.                             break;
  1269.                         case "tempmaxmin":
  1270.                             $forecastData["temperatureHigh"] = $this->convertTemperature($result[1], "c", "f");
  1271.                             $forecastData["temperatureLow"]  = $this->convertTemperature($result[4], "c", "f");
  1272.                             break;
  1273.                         case "from":
  1274.                             // Next timeperiod is coming up, prepare array and
  1275.                             // set pointer accordingly
  1276.                             if (sizeof($result) > 2) {
  1277.                                 // The ICAO way
  1278.                                 $fromTime = $result[1].":".$result[2];
  1279.                             } else {
  1280.                                 // The Australian way (Hey mates!)
  1281.                                 $fromTime = $result[1].":00";
  1282.                             }
  1283.                             $forecastData["time"][$fromTime] = array();
  1284.                             $fmcCount = 0;
  1285.                             $pointer =& $forecastData["time"][$fromTime];
  1286.                             break;
  1287.                         case "fmc";
  1288.                             // Test, if this is a probability for the next FMC
  1289.                             if (isset($result[2]) && preg_match("/^BECMG|TEMPO$/i", $taf[$i + 1], $lresult)) {
  1290.                                 // Set type to BECMG or TEMPO
  1291.                                 $type = $lresult[0];
  1292.                                 // Set probability
  1293.                                 $probability = $result[2];
  1294.                                 // Now extract time for this group
  1295.                                 if (preg_match("/^(\d{2})(\d{2})$/i", $taf[$i + 2], $lresult)) {
  1296.                                     $from = $lresult[1].":00";
  1297.                                     $to   = $lresult[2].":00";
  1298.                                     $to   = ($to == "24:00") ? "00:00" : $to;
  1299.                                     // As we now have type, probability and time for this FMC
  1300.                                     // from our TAF, increase field-counter
  1301.                                     $i += 2;
  1302.                                 } else {
  1303.                                     // No timegroup present, so just increase field-counter by one
  1304.                                     $i += 1;
  1305.                                 }
  1306.                             } elseif (preg_match("/^(\d{2})(\d{2})$/i", $taf[$i + 1], $lresult)) {
  1307.                                 // Normal group, set type and use extracted time
  1308.                                 $type = $result[1];
  1309.                                 // Check for PROBdd
  1310.                                 if (isset($result[2])) {
  1311.                                     $probability = $result[2];
  1312.                                 }
  1313.                                 $from = $lresult[1].":00";
  1314.                                 $to   = $lresult[2].":00";
  1315.                                 $to   = ($to == "24:00") ? "00:00" : $to;
  1316.                                 // Same as above, we have a time for this FMC from our TAF,
  1317.                                 // increase field-counter
  1318.                                 $i += 1;
  1319.                             } elseif (isset($result[2])) {
  1320.                                 // This is either a PROBdd or a malformed TAF with missing timegroup
  1321.                                 $probability = $result[2];
  1322.                             }
  1323.  
  1324.                             // Handle the FMC, generate neccessary array if it's the first...
  1325.                             if (isset($type)) {
  1326.                                 if (!isset($forecastData["time"][$fromTime]["fmc"])) {
  1327.                                     $forecastData["time"][$fromTime]["fmc"] = array();
  1328.                                 }
  1329.                                 $forecastData["time"][$fromTime]["fmc"][$fmcCount] = array();
  1330.                                 // ...and set pointer.
  1331.                                 $pointer =& $forecastData["time"][$fromTime]["fmc"][$fmcCount];
  1332.                                 $fmcCount++;
  1333.                                 // Insert data
  1334.                                 $pointer["type"] = $type;
  1335.                                 unset($type);
  1336.                                 if (isset($from)) {
  1337.                                     $pointer["from"] = $from;
  1338.                                     $pointer["to"]   = $to;
  1339.                                     unset($from, $to);
  1340.                                 }
  1341.                                 if (isset($probability)) {
  1342.                                     $pointer["probability"] = $probability;
  1343.                                     unset($probability);
  1344.                                 }
  1345.                             }
  1346.                             break;
  1347.                         default:
  1348.                             // Do nothing
  1349.                             break;
  1350.                     }
  1351.                     if ($found && !SERVICES_WEATHER_DEBUG) {
  1352.                         break;
  1353.                     } elseif ($found && SERVICES_WEATHER_DEBUG) {
  1354.                         echo $key."\n";
  1355.                         break;
  1356.                     }
  1357.                 }
  1358.             }
  1359.             if (!$found) {
  1360.                 if (SERVICES_WEATHER_DEBUG) {
  1361.                     echo "n/a\n";
  1362.                 }
  1363.                 if (!isset($forecastData["noparse"])) {
  1364.                     $forecastData["noparse"] = array();
  1365.                 }
  1366.                 $forecastData["noparse"][] = $taf[$i];
  1367.             }
  1368.         }
  1369.  
  1370.         if (isset($forecastData["noparse"])) {
  1371.             $forecastData["noparse"] = implode(" ",  $forecastData["noparse"]);
  1372.         }
  1373.  
  1374.         return $forecastData;
  1375.     }
  1376.     // }}}
  1377.  
  1378.     // {{{ _convertReturn()
  1379.     /**
  1380.      * Converts the data in the return array to the desired units and/or
  1381.      * output format.
  1382.      *
  1383.      * @param   array                       $target
  1384.      * @param   string                      $units
  1385.      * @param   string                      $location
  1386.      * @access  private
  1387.      */
  1388.     function _convertReturn(&$target, $units, $location)
  1389.     {
  1390.         if (is_array($target)) {
  1391.             foreach ($target as $key => $val) {
  1392.                 if (is_array($val)) {
  1393.                     // Another array detected, so recurse into it to convert the units
  1394.                     $this->_convertReturn($target[$key], $units, $location);
  1395.                 } else {
  1396.                     switch ($key) {
  1397.                         case "station":
  1398.                             $newVal = $location["name"];
  1399.                             break;
  1400.                         case "update":
  1401.                         case "validFrom":
  1402.                         case "validTo":
  1403.                             $newVal = gmdate(trim($this->_dateFormat." ".$this->_timeFormat), $val);
  1404.                             break;
  1405.                         case "wind":
  1406.                         case "windGust":
  1407.                         case "windshear":
  1408.                             $newVal = $this->convertSpeed($val, "mph", $units["wind"]);
  1409.                             break;
  1410.                         case "visibility":
  1411.                             $newVal = $this->convertDistance($val, "sm", $units["vis"]);
  1412.                             break;
  1413.                         case "height":
  1414.                         case "windshearHeight":
  1415.                             if (is_numeric($val)) {
  1416.                                 $newVal = $this->convertDistance($val, "ft", $units["height"]);
  1417.                             } else {
  1418.                                 $newVal = $val;
  1419.                             }
  1420.                             break;
  1421.                         case "temperature":
  1422.                         case "temperatureHigh":
  1423.                         case "temperatureLow":
  1424.                         case "dewPoint":
  1425.                         case "feltTemperature":
  1426.                             $newVal = $this->convertTemperature($val, "f", $units["temp"]);
  1427.                             break;
  1428.                         case "pressure":
  1429.                         case "seapressure":
  1430.                         case "presschng":
  1431.                             $newVal = $this->convertPressure($val, "in", $units["pres"]);
  1432.                             break;
  1433.                         case "amount":
  1434.                         case "snowdepth":
  1435.                         case "snowequiv":
  1436.                             if (is_numeric($val)) {
  1437.                                 $newVal = $this->convertPressure($val, "in", $units["rain"]);
  1438.                             } else {
  1439.                                 $newVal = $val;
  1440.                             }
  1441.                             break;
  1442.                         case "1htemp":
  1443.                         case "1hdew":
  1444.                         case "6hmaxtemp":
  1445.                         case "6hmintemp":
  1446.                         case "24hmaxtemp":
  1447.                         case "24hmintemp":
  1448.                             $newVal = $this->convertTemperature($val, "f", $units["temp"]);
  1449.                             break;
  1450.                         default:
  1451.                             continue 2;
  1452.                     }
  1453.                     $target[$key] = $newVal;
  1454.                 }
  1455.             }
  1456.         }
  1457.     }
  1458.     // }}}
  1459.  
  1460.     // {{{ searchLocation()
  1461.     /**
  1462.      * Searches IDs for given location, returns array of possible locations
  1463.      * or single ID
  1464.      *
  1465.      * @param   string|array                $location
  1466.      * @param   bool                        $useFirst       If set, first ID of result-array is returned
  1467.      * @return  PEAR_Error|array|string
  1468.      * @throws  PEAR_Error::SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION
  1469.      * @throws  PEAR_Error::SERVICES_WEATHER_ERROR_DB_NOT_CONNECTED
  1470.      * @throws  PEAR_Error::SERVICES_WEATHER_ERROR_INVALID_LOCATION
  1471.      * @access  public
  1472.      */
  1473.     function searchLocation($location, $useFirst = false)
  1474.     {
  1475.         if (!isset($this->_db) || !DB::isConnection($this->_db)) {
  1476.             return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_DB_NOT_CONNECTED, __FILE__, __LINE__);
  1477.         }
  1478.  
  1479.         if (is_string($location)) {
  1480.             // Try to part search string in name, state and country part
  1481.             // and build where clause from it for the select
  1482.             $location = explode(",", $location);
  1483.  
  1484.             // Trim, caps-low and quote the strings
  1485.             for ($i = 0; $i < sizeof($location); $i++) {
  1486.                 $location[$i] = $this->_db->quote("%".strtolower(trim($location[$i]))."%");
  1487.             }
  1488.  
  1489.             if (sizeof($location) == 1) {
  1490.                 $where  = "LOWER(name) LIKE ".$location[0];
  1491.             } elseif (sizeof($location) == 2) {
  1492.                 $where  = "LOWER(name) LIKE ".$location[0];
  1493.                 $where .= " AND LOWER(country) LIKE ".$location[1];
  1494.             } elseif (sizeof($location) == 3) {
  1495.                 $where  = "LOWER(name) LIKE ".$location[0];
  1496.                 $where .= " AND LOWER(state) LIKE ".$location[1];
  1497.                 $where .= " AND LOWER(country) LIKE ".$location[2];
  1498.             } elseif (sizeof($location) == 4) {
  1499.                 $where  = "LOWER(name) LIKE ".substr($location[0], 0, -2).", ".substr($location[1], 2);
  1500.                 $where .= " AND LOWER(state) LIKE ".$location[2];
  1501.                 $where .= " AND LOWER(country) LIKE ".$location[3];
  1502.             }
  1503.  
  1504.             // Create select, locations with ICAO first
  1505.             $select = "SELECT icao, name, state, country, latitude, longitude ".
  1506.                       "FROM metarLocations ".
  1507.                       "WHERE ".$where." ".
  1508.                       "ORDER BY icao DESC";
  1509.             $result = $this->_db->query($select);
  1510.             // Check result for validity
  1511.             if (DB::isError($result)) {
  1512.                 return $result;
  1513.             } elseif (strtolower(get_class($result)) != "db_result" || $result->numRows() == 0) {
  1514.                 return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION, __FILE__, __LINE__);
  1515.             }
  1516.  
  1517.             // Result is valid, start preparing the return
  1518.             $icao = array();
  1519.             while (($row = $result->fetchRow(DB_FETCHMODE_ASSOC)) != null) {
  1520.                 $locicao = $row["icao"];
  1521.                 // First the name of the location
  1522.                 if (!strlen($row["state"])) {
  1523.                     $locname = $row["name"].", ".$row["country"];
  1524.                 } else {
  1525.                     $locname = $row["name"].", ".$row["state"].", ".$row["country"];
  1526.                 }
  1527.                 if ($locicao != "----") {
  1528.                     // We have a location with ICAO
  1529.                     $icao[$locicao] = $locname;
  1530.                 } else {
  1531.                     // No ICAO, try finding the nearest airport
  1532.                     $locicao = $this->searchAirport($row["latitude"], $row["longitude"]);
  1533.                     if (!isset($icao[$locicao])) {
  1534.                         $icao[$locicao] = $locname;
  1535.                     }
  1536.                 }
  1537.             }
  1538.             // Only one result? Return as string
  1539.             if (sizeof($icao) == 1 || $useFirst) {
  1540.                 $icao = key($icao);
  1541.             }
  1542.         } elseif (is_array($location)) {
  1543.             // Location was provided as coordinates, search nearest airport
  1544.             $icao = $this->searchAirport($location[0], $location[1]);
  1545.         } else {
  1546.             return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_INVALID_LOCATION, __FILE__, __LINE__);
  1547.         }
  1548.  
  1549.         return $icao;
  1550.     }
  1551.     // }}}
  1552.  
  1553.     // {{{ searchLocationByCountry()
  1554.     /**
  1555.      * Returns IDs with location-name for a given country or all available
  1556.      * countries, if no value was given
  1557.      *
  1558.      * @param   string                      $country
  1559.      * @return  PEAR_Error|array
  1560.      * @throws  PEAR_Error::SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION
  1561.      * @throws  PEAR_Error::SERVICES_WEATHER_ERROR_DB_NOT_CONNECTED
  1562.      * @throws  PEAR_Error::SERVICES_WEATHER_ERROR_WRONG_SERVER_DATA
  1563.      * @access  public
  1564.      */
  1565.     function searchLocationByCountry($country = "")
  1566.     {
  1567.         if (!isset($this->_db) || !DB::isConnection($this->_db)) {
  1568.             return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_DB_NOT_CONNECTED, __FILE__, __LINE__);
  1569.         }
  1570.  
  1571.         // Return the available countries as no country was given
  1572.         if (!strlen($country)) {
  1573.             $select = "SELECT DISTINCT(country) ".
  1574.                       "FROM metarAirports ".
  1575.                       "ORDER BY country ASC";
  1576.             $countries = $this->_db->getCol($select);
  1577.  
  1578.             // As $countries is either an error or the true result,
  1579.             // we can just return it
  1580.             return $countries;
  1581.         }
  1582.  
  1583.         // Now for the real search
  1584.         $select = "SELECT icao, name, state, country ".
  1585.                   "FROM metarAirports ".
  1586.                   "WHERE LOWER(country) LIKE '%".strtolower(trim($country))."%' ".
  1587.                   "ORDER BY name ASC";
  1588.         $result = $this->_db->query($select);
  1589.         // Check result for validity
  1590.         if (DB::isError($result)) {
  1591.             return $result;
  1592.         } elseif (strtolower(get_class($result)) != "db_result" || $result->numRows() == 0) {
  1593.             return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION, __FILE__, __LINE__);
  1594.         }
  1595.  
  1596.         // Construct the result
  1597.         $locations = array();
  1598.         while (($row = $result->fetchRow(DB_FETCHMODE_ASSOC)) != null) {
  1599.             $locicao = $row["icao"];
  1600.             if ($locicao != "----") {
  1601.                 // First the name of the location
  1602.                 if (!strlen($row["state"])) {
  1603.                     $locname = $row["name"].", ".$row["country"];
  1604.                 } else {
  1605.                     $locname = $row["name"].", ".$row["state"].", ".$row["country"];
  1606.                 }
  1607.                 $locations[$locicao] = $locname;
  1608.             }
  1609.         }
  1610.  
  1611.         return $locations;
  1612.     }
  1613.     // }}}
  1614.  
  1615.     // {{{ searchAirport()
  1616.     /**
  1617.      * Searches the nearest airport(s) for given coordinates, returns array
  1618.      * of IDs or single ID
  1619.      *
  1620.      * @param   float                       $latitude
  1621.      * @param   float                       $longitude
  1622.      * @param   int                         $numResults
  1623.      * @return  PEAR_Error|array|string
  1624.      * @throws  PEAR_Error::SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION
  1625.      * @throws  PEAR_Error::SERVICES_WEATHER_ERROR_DB_NOT_CONNECTED
  1626.      * @throws  PEAR_Error::SERVICES_WEATHER_ERROR_INVALID_LOCATION
  1627.      * @access  public
  1628.      */
  1629.     function searchAirport($latitude, $longitude, $numResults = 1)
  1630.     {
  1631.         if (!isset($this->_db) || !DB::isConnection($this->_db)) {
  1632.             return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_DB_NOT_CONNECTED, __FILE__, __LINE__);
  1633.         }
  1634.         if (!is_numeric($latitude) || !is_numeric($longitude)) {
  1635.             return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_INVALID_LOCATION, __FILE__, __LINE__);
  1636.         }
  1637.  
  1638.         // Get all airports
  1639.         $select = "SELECT icao, x, y, z FROM metarAirports";
  1640.         $result = $this->_db->query($select);
  1641.         if (DB::isError($result)) {
  1642.             return $result;
  1643.         } elseif (strtolower(get_class($result)) != "db_result" || $result->numRows() == 0) {
  1644.             return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION, __FILE__, __LINE__);
  1645.         }
  1646.  
  1647.         // Result is valid, start search
  1648.         // Initialize values
  1649.         $min_dist = null;
  1650.         $query    = $this->polar2cartesian($latitude, $longitude);
  1651.         $search   = array("dist" => array(), "icao" => array());
  1652.         while (($row = $result->fetchRow(DB_FETCHMODE_ASSOC)) != null) {
  1653.             $icao = $row["icao"];
  1654.             $air  = array($row["x"], $row["y"], $row["z"]);
  1655.  
  1656.             $dist = 0;
  1657.             $d = 0;
  1658.             // Calculate distance of query and current airport
  1659.             // break off, if distance is larger than current $min_dist
  1660.             for($d; $d < sizeof($air); $d++) {
  1661.                 $t = $air[$d] - $query[$d];
  1662.                 $dist += pow($t, 2);
  1663.                 if ($min_dist != null && $dist > $min_dist) {
  1664.                     break;
  1665.                 }
  1666.             }
  1667.  
  1668.             if ($d >= sizeof($air)) {
  1669.                 // Ok, current airport is one of the nearer locations
  1670.                 // add to result-array
  1671.                 $search["dist"][] = $dist;
  1672.                 $search["icao"][] = $icao;
  1673.                 // Sort array for distance
  1674.                 array_multisort($search["dist"], SORT_NUMERIC, SORT_ASC, $search["icao"], SORT_STRING, SORT_ASC);
  1675.                 // If array is larger then desired results, chop off last one
  1676.                 if (sizeof($search["dist"]) > $numResults) {
  1677.                     array_pop($search["dist"]);
  1678.                     array_pop($search["icao"]);
  1679.                 }
  1680.                 $min_dist = max($search["dist"]);
  1681.             }
  1682.         }
  1683.         if ($numResults == 1) {
  1684.             // Only one result wanted, return as string
  1685.             return $search["icao"][0];
  1686.         } elseif ($numResults > 1) {
  1687.             // Return found locations
  1688.             return $search["icao"];
  1689.         } else {
  1690.             return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION, __FILE__, __LINE__);
  1691.         }
  1692.     }
  1693.     // }}}
  1694.  
  1695.     // {{{ getLocation()
  1696.     /**
  1697.      * Returns the data for the location belonging to the ID
  1698.      *
  1699.      * @param   string                      $id
  1700.      * @return  PEAR_Error|array
  1701.      * @throws  PEAR_Error
  1702.      * @access  public
  1703.      */
  1704.     function getLocation($id = "")
  1705.     {
  1706.         $status = $this->_checkLocationID($id);
  1707.  
  1708.         if (Services_Weather::isError($status)) {
  1709.             return $status;
  1710.         }
  1711.  
  1712.         $locationReturn = array();
  1713.  
  1714.         if ($this->_cacheEnabled && ($location = $this->_cache->get("METAR-".$id, "location"))) {
  1715.             // Grab stuff from cache
  1716.             $this->_location = $location;
  1717.             $locationReturn["cache"] = "HIT";
  1718.         } elseif (isset($this->_db) && DB::isConnection($this->_db)) {
  1719.             // Get data from DB
  1720.             $select = "SELECT icao, name, state, country, latitude, longitude, elevation ".
  1721.                       "FROM metarAirports WHERE icao='".$id."'";
  1722.             $result = $this->_db->query($select);
  1723.  
  1724.             if (DB::isError($result)) {
  1725.                 return $result;
  1726.             } elseif (strtolower(get_class($result)) != "db_result" || $result->numRows() == 0) {
  1727.                 return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION, __FILE__, __LINE__);
  1728.             }
  1729.             // Result is ok, put things into object
  1730.             $this->_location = $result->fetchRow(DB_FETCHMODE_ASSOC);
  1731.  
  1732.             if ($this->_cacheEnabled) {
  1733.                 // ...and cache it
  1734.                 $expire = constant("SERVICES_WEATHER_EXPIRES_LOCATION");
  1735.                 $this->_cache->extSave("METAR-".$id, $this->_location, "", $expire, "location");
  1736.             }
  1737.  
  1738.             $locationReturn["cache"] = "MISS";
  1739.         } else {
  1740.             $this->_location = array(
  1741.                 "name"      => $id,
  1742.                 "state"     => "",
  1743.                 "country"   => "",
  1744.                 "latitude"  => "",
  1745.                 "longitude" => "",
  1746.                 "elevation" => ""
  1747.             );
  1748.         }
  1749.         // Stuff name-string together
  1750.         if (strlen($this->_location["state"]) && strlen($this->_location["country"])) {
  1751.             $locname = $this->_location["name"].", ".$this->_location["state"].", ".$this->_location["country"];
  1752.         } elseif (strlen($this->_location["country"])) {
  1753.             $locname = $this->_location["name"].", ".$this->_location["country"];
  1754.         } else {
  1755.             $locname = $this->_location["name"];
  1756.         }
  1757.         $locationReturn["name"]      = $locname;
  1758.         $locationReturn["latitude"]  = $this->_location["latitude"];
  1759.         $locationReturn["longitude"] = $this->_location["longitude"];
  1760.         $locationReturn["sunrise"]   = gmdate($this->_timeFormat, $this->calculateSunRiseSet(gmmktime(), SUNFUNCS_RET_TIMESTAMP, $this->_location["latitude"], $this->_location["longitude"], SERVICES_WEATHER_SUNFUNCS_SUNRISE_ZENITH, 0, true));
  1761.         $locationReturn["sunset"]    = gmdate($this->_timeFormat, $this->calculateSunRiseSet(gmmktime(), SUNFUNCS_RET_TIMESTAMP, $this->_location["latitude"], $this->_location["longitude"], SERVICES_WEATHER_SUNFUNCS_SUNSET_ZENITH,  0, false));
  1762.         $locationReturn["elevation"] = $this->_location["elevation"];
  1763.  
  1764.         return $locationReturn;
  1765.     }
  1766.     // }}}
  1767.  
  1768.     // {{{ getWeather()
  1769.     /**
  1770.      * Returns the weather-data for the supplied location
  1771.      *
  1772.      * @param   string                      $id
  1773.      * @param   string                      $unitsFormat
  1774.      * @return  PHP_Error|array
  1775.      * @throws  PHP_Error
  1776.      * @access  public
  1777.      */
  1778.     function getWeather($id = "", $unitsFormat = "")
  1779.     {
  1780.         $id     = strtoupper($id);
  1781.         $status = $this->_checkLocationID($id);
  1782.  
  1783.         if (Services_Weather::isError($status)) {
  1784.             return $status;
  1785.         }
  1786.  
  1787.         // Get other data
  1788.         $units    = $this->getUnitsFormat($unitsFormat);
  1789.         $location = $this->getLocation($id);
  1790.  
  1791.         if (Services_Weather::isError($location)) {
  1792.             return $location;
  1793.         }
  1794.  
  1795.         if ($this->_cacheEnabled && ($weather = $this->_cache->get("METAR-".$id, "weather"))) {
  1796.             // Wee... it was cached, let's have it...
  1797.             $weatherReturn  = $weather;
  1798.             $this->_weather = $weatherReturn;
  1799.             $weatherReturn["cache"] = "HIT";
  1800.         } else {
  1801.             // Download weather
  1802.             $weatherData = $this->_retrieveServerData($id, "metar");
  1803.             if (Services_Weather::isError($weatherData)) {
  1804.                 return $weatherData;
  1805.             } elseif (!is_array($weatherData) || sizeof($weatherData) < 2) {
  1806.                 return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_WRONG_SERVER_DATA, __FILE__, __LINE__);
  1807.             }
  1808.  
  1809.             // Parse weather
  1810.             $weatherReturn  = $this->_parseWeatherData($weatherData);
  1811.  
  1812.             if (Services_Weather::isError($weatherReturn)) {
  1813.                 return $weatherReturn;
  1814.             }
  1815.  
  1816.             // Add an icon for the current conditions
  1817.             // Determine if certain values are set, if not use defaults
  1818.             $condition   = isset($weatherReturn["condition"])   ? $weatherReturn["condition"]   : "No Significant Weather";
  1819.             $clouds      = isset($weatherReturn["clouds"])      ? $weatherReturn["clouds"]      :                  array();
  1820.             $wind        = isset($weatherReturn["wind"])        ? $weatherReturn["wind"]        :                        5;
  1821.             $temperature = isset($weatherReturn["temperature"]) ? $weatherReturn["temperature"] :                       70;
  1822.             $latitude    = isset($location["latitude"])         ? $location["latitude"]         :                     -360;
  1823.             $longitude   = isset($location["longitude"])        ? $location["longitude"]        :                     -360;
  1824.  
  1825.             // Get the icon
  1826.             $weatherReturn["conditionIcon"] = $this->getWeatherIcon($condition, $clouds, $wind, $temperature, $latitude, $longitude);
  1827.  
  1828.             if ($this->_cacheEnabled) {
  1829.                 // Cache weather
  1830.                 $expire = constant("SERVICES_WEATHER_EXPIRES_WEATHER");
  1831.                 $this->_cache->extSave("METAR-".$id, $weatherReturn, $unitsFormat, $expire, "weather");
  1832.             }
  1833.             $this->_weather = $weatherReturn;
  1834.             $weatherReturn["cache"] = "MISS";
  1835.         }
  1836.  
  1837.         $this->_convertReturn($weatherReturn, $units, $location);
  1838.  
  1839.         return $weatherReturn;
  1840.     }
  1841.     // }}}
  1842.  
  1843.     // {{{ getForecast()
  1844.     /**
  1845.      * METAR provides no forecast per se, we use the TAF reports to generate
  1846.      * a forecast for the announced timeperiod
  1847.      *
  1848.      * @param   string                      $id
  1849.      * @param   int                         $days           Ignored, not applicable
  1850.      * @param   string                      $unitsFormat
  1851.      * @return  PEAR_Error|array
  1852.      * @throws  PEAR_Error
  1853.      * @access  public
  1854.      */
  1855.     function getForecast($id = "", $days = null, $unitsFormat = "")
  1856.     {
  1857.         $id     = strtoupper($id);
  1858.         $status = $this->_checkLocationID($id);
  1859.  
  1860.         if (Services_Weather::isError($status)) {
  1861.             return $status;
  1862.         }
  1863.  
  1864.         // Get other data
  1865.         $units    = $this->getUnitsFormat($unitsFormat);
  1866.         $location = $this->getLocation($id);
  1867.  
  1868.         if (Services_Weather::isError($location)) {
  1869.             return $location;
  1870.         }
  1871.  
  1872.         if ($this->_cacheEnabled && ($forecast = $this->_cache->get("METAR-".$id, "forecast"))) {
  1873.             // Wee... it was cached, let's have it...
  1874.             $forecastReturn  = $forecast;
  1875.             $this->_forecast = $forecastReturn;
  1876.             $forecastReturn["cache"] = "HIT";
  1877.         } else {
  1878.             // Download forecast
  1879.             $forecastData = $this->_retrieveServerData($id, "taf");
  1880.             if (Services_Weather::isError($forecastData)) {
  1881.                 return $forecastData;
  1882.             } elseif (!is_array($forecastData) || sizeof($forecastData) < 2) {
  1883.                 return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_WRONG_SERVER_DATA, __FILE__, __LINE__);
  1884.             }
  1885.  
  1886.             // Parse forecast
  1887.             $forecastReturn  = $this->_parseForecastData($forecastData);
  1888.  
  1889.             if (Services_Weather::isError($forecastReturn)) {
  1890.                 return $forecastReturn;
  1891.             }
  1892.             if ($this->_cacheEnabled) {
  1893.                 // Cache weather
  1894.                 $expire = constant("SERVICES_WEATHER_EXPIRES_FORECAST");
  1895.                 $this->_cache->extSave("METAR-".$id, $forecastReturn, $unitsFormat, $expire, "forecast");
  1896.             }
  1897.             $this->_forecast = $forecastReturn;
  1898.             $forecastReturn["cache"] = "MISS";
  1899.         }
  1900.  
  1901.         $this->_convertReturn($forecastReturn, $units, $location);
  1902.  
  1903.         return $forecastReturn;
  1904.     }
  1905.     // }}}
  1906. }
  1907. // }}}
  1908. ?>
  1909.