Services_Weather
[ class tree: Services_Weather ] [ index: Services_Weather ] [ all elements ]

Source for file Metar.php

Documentation is available at Metar.php

  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-2009, 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-2009 Alexander Wirtz
  42.  * @license     http://www.opensource.org/licenses/bsd-license.php  BSD License
  43.  * @version     CVS: $Id: Metar.php,v 1.107 2009/03/12 23:16:41 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-2009 Alexander Wirtz
  86.  * @license     http://www.opensource.org/licenses/bsd-license.php  BSD License
  87.  * @version     Release: 1.4.4
  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. {
  95.     // {{{ properties
  96.     /**
  97.      * Information to access the location DB
  98.      *
  99.      * @var     object  DB                  $_db 
  100.      * @access  private
  101.      */
  102.     var $_db;
  103.  
  104.     /**
  105.      * The source METAR uses
  106.      *
  107.      * @var     string                      $_sourceMetar 
  108.      * @access  private
  109.      */
  110.     var $_sourceMetar;
  111.  
  112.     /**
  113.      * The source TAF uses
  114.      *
  115.      * @var     string                      $_sourceTaf 
  116.      * @access  private
  117.      */
  118.     var $_sourceTaf;
  119.  
  120.     /**
  121.      * This path is used to find the METAR data
  122.      *
  123.      * @var     string                      $_sourcePathMetar 
  124.      * @access  private
  125.      */
  126.     var $_sourcePathMetar;
  127.  
  128.     /**
  129.      * This path is used to find the TAF data
  130.      *
  131.      * @var     string                      $_sourcePathTaf 
  132.      * @access  private
  133.      */
  134.     var $_sourcePathTaf;
  135.     // }}}
  136.  
  137.     // {{{ constructor
  138.     /**
  139.      * Constructor
  140.      *
  141.      * @param   array                       $options 
  142.      * @param   mixed                       $error 
  143.      * @throws  PEAR_Error
  144.      * @access  private
  145.      */
  146.     function Services_Weather_Metar($options&$error)
  147.     {
  148.         $perror = null;
  149.         $this->Services_Weather_Common($options$perror);
  150.         if (Services_Weather::isError($perror)) {
  151.             $error $perror;
  152.             return;
  153.         }
  154.  
  155.         // Set options accordingly
  156.         $status = null;
  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($sourceMetararray("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($sourceTafarray("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"]$tempfiletrueFTP_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.         // Eliminate trailing information
  565.         if (strpos($data"="!== false{
  566.             $data substr($data0strpos($data"="));
  567.         }
  568.         $weatherData["dataRaw"]   implode(" "$data);
  569.         $weatherData["update"]    strtotime(trim($data[0])." GMT");
  570.         $weatherData["updateRaw"trim($data[0]);
  571.         // and prepare the rest for stepping through
  572.         array_shift($data);
  573.         $metar explode(" "preg_replace("/\s{2,}/"" "implode(" "$data)));
  574.  
  575.         // Add a few local variables for data processing
  576.         $trendCount = 0;             // If we have trends, we need this
  577.         $pointer    =$weatherData// Pointer to the array we add the data to
  578.         for ($i = 0; $i sizeof($metar)$i++{
  579.             // Check for whitespace and step loop, if nothing's there
  580.             $metar[$itrim($metar[$i]);
  581.             if (!strlen($metar[$i])) {
  582.                 continue;
  583.             }
  584.  
  585.             if (SERVICES_WEATHER_DEBUG{
  586.                 $tab str_repeat("\t"3 - floor((strlen($metar[$i]+ 2/ 8));
  587.                 echo "\"".$metar[$i]."\"".$tab."-> ";
  588.             }
  589.  
  590.             // Initialize some arrays
  591.             $result   = array();
  592.             $resultVF = array();
  593.             $lresult  = array();
  594.  
  595.             $found = false;
  596.             foreach ($metarCode as $key => $regexp{
  597.                 // Check if current code matches current metar snippet
  598.                 if (($found preg_match("/^".$regexp."$/i"$metar[$i]$result)) == true{
  599.                     switch ($key{
  600.                         case "station":
  601.                             $pointer["station"$result[0];
  602.                             unset($metarCode["station"]);
  603.                             break;
  604.                         case "wind":
  605.                             // Parse wind data, first the speed, convert from kt to chosen unit
  606.                             if ($result[5== "KTS"{
  607.                                 $result[5"KT";
  608.                             }
  609.                             $pointer["wind"$this->convertSpeed($result[2]$result[5]"mph");
  610.                             if ($result[1== "VAR" || $result[1== "VRB"{
  611.                                 // Variable winds
  612.                                 $pointer["windDegrees"]   "Variable";
  613.                                 $pointer["windDirection""Variable";
  614.                             else {
  615.                                 // Save wind degree and calc direction
  616.                                 $pointer["windDegrees"]   intval($result[1]);
  617.                                 $pointer["windDirection"$compass[round($result[1/ 22.5% 16];
  618.                             }
  619.                             if (is_numeric($result[4])) {
  620.                                 // Wind with gusts...
  621.                                 $pointer["windGust"$this->convertSpeed($result[4]$result[5]"mph");
  622.                             }
  623.                             break;
  624.                         case "windVar":
  625.                             // Once more wind, now variability around the current wind-direction
  626.                             $pointer["windVariability"= array("from" => intval($result[1])"to" => intval($result[2]));
  627.                             break;
  628.                         case "visFrac":
  629.                             // Possible fractional visibility here. Check if it matches with the next METAR piece for visibility
  630.                             if (!isset($metar[$i + 1]|| !preg_match("/^".$metarCode["visibility"]."$/i"$result[1]." ".$metar[$i + 1]$resultVF)) {
  631.                                 // No next METAR piece available or not matching. Match against next METAR code
  632.                                 $found = false;
  633.                                 break;
  634.                             else {
  635.                                 // Match. Hand over result and advance METAR
  636.                                 if (SERVICES_WEATHER_DEBUG{
  637.                                     echo $key."\n";
  638.                                     echo "\"".$result[1]." ".$metar[$i + 1]."\"".str_repeat("\t"2 - floor((strlen($result[1]." ".$metar[$i + 1]+ 2/ 8))."-> ";
  639.                                 }
  640.                                 $key "visibility";
  641.                                 $result $resultVF;
  642.                                 $i++;
  643.                             }
  644.                         case "visibility":
  645.                             $pointer["visQualifier""AT";
  646.                             if (is_numeric($result[1]&& ($result[1== 9999)) {
  647.                                 // Upper limit of visibility range
  648.                                 $visibility $this->convertDistance(10"km""sm");
  649.                                 $pointer["visQualifier""BEYOND";
  650.                             elseif (is_numeric($result[1])) {
  651.                                 // 4-digit visibility in m
  652.                                 $visibility $this->convertDistance(($result[1]/1000)"km""sm");
  653.                             elseif (!isset($result[11]|| $result[11!= "CAVOK"{
  654.                                 if ($result[3== "M"{
  655.                                     $pointer["visQualifier""BELOW";
  656.                                 elseif ($result[3== "P"{
  657.                                     $pointer["visQualifier""BEYOND";
  658.                                 }
  659.                                 if (is_numeric($result[5])) {
  660.                                     // visibility as one/two-digit number
  661.                                     $visibility $this->convertDistance($result[5]$result[10]"sm");
  662.                                 else {
  663.                                     // the y/z part, add if we had a x part (see visibility1)
  664.                                     if (is_numeric($result[7])) {
  665.                                         $visibility $this->convertDistance($result[7$result[8$result[9]$result[10]"sm");
  666.                                     else {
  667.                                         $visibility $this->convertDistance($result[8$result[9]$result[10]"sm");
  668.                                     }
  669.                                 }
  670.                             else {
  671.                                 $pointer["visQualifier""BEYOND";
  672.                                 $visibility $this->convertDistance(10"km""sm");
  673.                                 $pointer["clouds"= array(array("amount" => "Clear below""height" => 5000));
  674.                                 $pointer["condition""no significant weather";
  675.                             }
  676.                             $pointer["visibility"$visibility;
  677.                             break;
  678.                         case "condition":
  679.                             // First some basic setups
  680.                             if (!isset($pointer["condition"])) {
  681.                                 $pointer["condition""";
  682.                             elseif (strlen($pointer["condition"]> 0{
  683.                                 $pointer["condition".= ",";
  684.                             }
  685.  
  686.                             if (in_array(strtolower($result[0])$conditions)) {
  687.                                 // First try matching the complete string
  688.                                 $pointer["condition".= " ".$conditions[strtolower($result[0])];
  689.                             else {
  690.                                 // No luck, match part by part
  691.                                 array_shift($result);
  692.                                 $result array_unique($result);
  693.                                 foreach ($result as $condition{
  694.                                     if (strlen($condition> 0{
  695.                                         $pointer["condition".= " ".$conditions[strtolower($condition)];
  696.                                     }
  697.                                 }
  698.                             }
  699.                             $pointer["condition"trim($pointer["condition"]);
  700.                             break;
  701.                         case "clouds":
  702.                             if (!isset($pointer["clouds"])) {
  703.                                 $pointer["clouds"= array();
  704.                             }
  705.  
  706.                             if (sizeof($result== 5{
  707.                                 // Only amount and height
  708.                                 $cloud = array("amount" => $clouds[strtolower($result[3])]);
  709.                                 if ($result[4== "///"{
  710.                                     $cloud["height""station level or below";
  711.                                 else {
  712.                                     $cloud["height"$result[4* 100;
  713.                                 }
  714.                             elseif (sizeof($result== 6{
  715.                                 // Amount, height and type
  716.                                 $cloud = array("amount" => $clouds[strtolower($result[3])]"type" => $clouds[strtolower($result[5])]);
  717.                                 if ($result[4== "///"{
  718.                                     $cloud["height""station level or below";
  719.                                 else {
  720.                                     $cloud["height"$result[4* 100;
  721.                                 }
  722.                             else {
  723.                                 // SKC or CLR or NSC
  724.                                 $cloud = array("amount" => $clouds[strtolower($result[0])]);
  725.                             }
  726.                             $pointer["clouds"][$cloud;
  727.                             break;
  728.                         case "temperature":
  729.                             // normal temperature in first part
  730.                             // negative value
  731.                             if ($result[1== "M"{
  732.                                 $result[2*= -1;
  733.                             }
  734.                             $pointer["temperature"$this->convertTemperature($result[2]"c""f");
  735.                             if (sizeof($result> 4{
  736.                                 // same for dewpoint
  737.                                 if ($result[4== "M"{
  738.                                     $result[5*= -1;
  739.                                 }
  740.                                 $pointer["dewPoint"$this->convertTemperature($result[5]"c""f");
  741.                                 $pointer["humidity"$this->calculateHumidity($result[2]$result[5]);
  742.                             }
  743.                             if (isset($pointer["wind"])) {
  744.                                 // Now calculate windchill from temperature and windspeed
  745.                                 $pointer["feltTemperature"$this->calculateWindChill($pointer["temperature"]$pointer["wind"]);
  746.                             }
  747.                             break;
  748.                         case "pressure":
  749.                             if ($result[1== "A"{
  750.                                 // Pressure provided in inches
  751.                                 $pointer["pressure"$result[2/ 100;
  752.                             elseif ($result[3== "Q"{
  753.                                 // ... in hectopascal
  754.                                 $pointer["pressure"$this->convertPressure($result[4]"hpa""in");
  755.                             }
  756.                             break;
  757.                         case "trend":
  758.                             // We may have a trend here... extract type and set pointer on
  759.                             // created new array
  760.                             if (!isset($weatherData["trend"])) {
  761.                                 $weatherData["trend"= array();
  762.                                 $weatherData["trend"][$trendCount= array();
  763.                             }
  764.                             $pointer =$weatherData["trend"][$trendCount];
  765.                             $trendCount++;
  766.                             $pointer["type"$result[0];
  767.                             while (isset($metar[$i + 1]&& preg_match("/^(FM|TL|AT)(\d{2})(\d{2})$/i"$metar[$i + 1]$lresult)) {
  768.                                 if ($lresult[1== "FM"{
  769.                                     $pointer["from"$lresult[2].":".$lresult[3];
  770.                                 elseif ($lresult[1== "TL"{
  771.                                     $pointer["to"$lresult[2].":".$lresult[3];
  772.                                 else {
  773.                                     $pointer["at"$lresult[2].":".$lresult[3];
  774.                                 }
  775.                                 // As we have just extracted the time for this trend
  776.                                 // from our METAR, increase field-counter
  777.                                 $i++;
  778.                             }
  779.                             break;
  780.                         case "remark":
  781.                             // Remark part begins
  782.                             $metarCode $remarks;
  783.                             $weatherData["remark"= array();
  784.                             break;
  785.                         case "autostation":
  786.                             // Which autostation do we have here?
  787.                             if ($result[1== 0{
  788.                                 $weatherData["remark"]["autostation""Automatic weatherstation w/o precipitation discriminator";
  789.                             else {
  790.                                 $weatherData["remark"]["autostation""Automatic weatherstation w/ precipitation discriminator";
  791.                             }
  792.                             unset($metarCode["autostation"]);
  793.                             break;
  794.                         case "presschg":
  795.                             // Decoding for rapid pressure changes
  796.                             if (strtolower($result[1]== "r"{
  797.                                 $weatherData["remark"]["presschg""Pressure rising rapidly";
  798.                             else {
  799.                                 $weatherData["remark"]["presschg""Pressure falling rapidly";
  800.                             }
  801.                             unset($metarCode["presschg"]);
  802.                             break;
  803.                         case "seapressure":
  804.                             // Pressure at sea level (delivered in hpa)
  805.                             // Decoding is a bit obscure as 982 gets 998.2
  806.                             // whereas 113 becomes 1113 -> no real rule here
  807.                             if (strtolower($result[1]!= "no"{
  808.                                 if ($result[1> 500{
  809.                                     $press = 900 + round($result[1/ 1001);
  810.                                 else {
  811.                                     $press = 1000 + $result[1];
  812.                                 }
  813.                                 $weatherData["remark"]["seapressure"$this->convertPressure($press"hpa""in");
  814.                             }
  815.                             unset($metarCode["seapressure"]);
  816.                             break;
  817.                         case "precip":
  818.                             // Precipitation in inches
  819.                             static $hours;
  820.                             if (!isset($weatherData["precipitation"])) {
  821.                                 $weatherData["precipitation"= array();
  822.                                 $hours = array("P" => "1""6" => "3/6""7" => "24");
  823.                             }
  824.                             if (!is_numeric($result[2])) {
  825.                                 $precip "indeterminable";
  826.                             elseif ($result[2== "0000"{
  827.                                 $precip "traceable";
  828.                             else {
  829.                                 $precip $result[2/ 100;
  830.                             }
  831.                             $weatherData["precipitation"][= array(
  832.                                 "amount" => $precip,
  833.                                 "hours"  => $hours[$result[1]]
  834.                             );
  835.                             break;
  836.                         case "snowdepth":
  837.                             // Snow depth in inches
  838.                             $weatherData["remark"]["snowdepth"$result[1];
  839.                             unset($metarCode["snowdepth"]);
  840.                             break;
  841.                         case "snowequiv":
  842.                             // Same for equivalent in Water... (inches)
  843.                             $weatherData["remark"]["snowequiv"$result[1/ 10;
  844.                             unset($metarCode["snowequiv"]);
  845.                             break;
  846.                         case "cloudtypes":
  847.                             // Cloud types
  848.                             $weatherData["remark"]["cloudtypes"= array(
  849.                                 "low"    => $cloudtypes["low"][$result[1]],
  850.                                 "middle" => $cloudtypes["middle"][$result[2]],
  851.                                 "high"   => $cloudtypes["high"][$result[3]]
  852.                             );
  853.                             unset($metarCode["cloudtypes"]);
  854.                             break;
  855.                         case "sunduration":
  856.                             // Duration of sunshine (in minutes)
  857.                             $weatherData["remark"]["sunduration""Total minutes of sunshine: ".$result[1];
  858.                             unset($metarCode["sunduration"]);
  859.                             break;
  860.                         case "1htempdew":
  861.                             // Temperatures in the last hour in C
  862.                             if ($result[1== "1"{
  863.                                 $result[2*= -1;
  864.                             }
  865.                             $weatherData["remark"]["1htemp"$this->convertTemperature($result[2/ 10"c""f");
  866.  
  867.                             if (sizeof($result> 3{
  868.                                 // same for dewpoint
  869.                                 if ($result[4== "1"{
  870.                                     $result[5*= -1;
  871.                                 }
  872.                                 $weatherData["remark"]["1hdew"$this->convertTemperature($result[5/ 10"c""f");
  873.                             }
  874.                             unset($metarCode["1htempdew"]);
  875.                             break;
  876.                         case "6hmaxtemp":
  877.                             // Max temperature in the last 6 hours in C
  878.                             if ($result[1== "1"{
  879.                                 $result[2*= -1;
  880.                             }
  881.                             $weatherData["remark"]["6hmaxtemp"$this->convertTemperature($result[2/ 10"c""f");
  882.                             unset($metarCode["6hmaxtemp"]);
  883.                             break;
  884.                         case "6hmintemp":
  885.                             // Min temperature in the last 6 hours in C
  886.                             if ($result[1== "1"{
  887.                                 $result[2*= -1;
  888.                             }
  889.                             $weatherData["remark"]["6hmintemp"$this->convertTemperature($result[2/ 10"c""f");
  890.                             unset($metarCode["6hmintemp"]);
  891.                             break;
  892.                         case "24htemp":
  893.                             // Max/Min temperatures in the last 24 hours in C
  894.                             if ($result[1== "1"{
  895.                                 $result[2*= -1;
  896.                             }
  897.                             $weatherData["remark"]["24hmaxtemp"$this->convertTemperature($result[2/ 10"c""f");
  898.  
  899.                             if ($result[3== "1"{
  900.                                 $result[4*= -1;
  901.                             }
  902.                             $weatherData["remark"]["24hmintemp"$this->convertTemperature($result[4/ 10"c""f");
  903.                             unset($metarCode["24htemp"]);
  904.                             break;
  905.                         case "3hpresstend":
  906.                             // Pressure tendency of the last 3 hours
  907.                             // no special processing, just passing the data
  908.                             $weatherData["remark"]["3hpresstend"= array(
  909.                                 "presscode" => $result[1],
  910.                                 "presschng" => $this->convertPressure($result[2/ 10"hpa""in")
  911.                             );
  912.                             unset($metarCode["3hpresstend"]);
  913.                             break;
  914.                         case "nospeci":
  915.                             // No change during the last hour
  916.                             $weatherData["remark"]["nospeci""No changes in weather conditions";
  917.                             unset($metarCode["nospeci"]);
  918.                             break;
  919.                         case "sensors":
  920.                             // We may have multiple broken sensors, so do not unset
  921.                             if (!isset($weatherData["remark"]["sensors"])) {
  922.                                 $weatherData["remark"]["sensors"= array();
  923.                             }
  924.                             $weatherData["remark"]["sensors"][strtolower($result[0])$sensors[strtolower($result[0])];
  925.                             break;
  926.                         case "maintain":
  927.                             $weatherData["remark"]["maintain""Maintainance needed";
  928.                             unset($metarCode["maintain"]);
  929.                             break;
  930.                         default:
  931.                             // Do nothing, just prevent further matching
  932.                             unset($metarCode[$key]);
  933.                             break;
  934.                     }
  935.                     if ($found && !SERVICES_WEATHER_DEBUG{
  936.                         break;
  937.                     elseif ($found && SERVICES_WEATHER_DEBUG{
  938.                         echo $key."\n";
  939.                         break;
  940.                     }
  941.                 }
  942.             }
  943.             if (!$found{
  944.                 if (SERVICES_WEATHER_DEBUG{
  945.                     echo "n/a\n";
  946.                 }
  947.                 if (!isset($weatherData["noparse"])) {
  948.                     $weatherData["noparse"= array();
  949.                 }
  950.                 $weatherData["noparse"][$metar[$i];
  951.             }
  952.         }
  953.  
  954.         if (isset($weatherData["noparse"])) {
  955.             $weatherData["noparse"implode(" ",  $weatherData["noparse"]);
  956.         }
  957.  
  958.         return $weatherData;
  959.     }
  960.     // }}}
  961.  
  962.     // {{{ _parseForecastData()
  963.     /**
  964.      * Parses the data and caches it
  965.      *
  966.      * TAF KLGA 271734Z 271818 11007KT P6SM -RA SCT020 BKN200
  967.      *     FM2300 14007KT P6SM SCT030 BKN150
  968.      *     FM0400 VRB03KT P6SM SCT035 OVC080 PROB30 0509 P6SM -RA BKN035
  969.      *     FM0900 VRB03KT 6SM -RA BR SCT015 OVC035
  970.      *         TEMPO 1215 5SM -RA BR SCT009 BKN015
  971.      *         BECMG 1517 16007KT P6SM NSW SCT015 BKN070
  972.      *
  973.      * @param   array                       $data 
  974.      * @return  PEAR_Error|array
  975.      * @throws  PEAR_Error::SERVICES_WEATHER_ERROR_WRONG_SERVER_DATA
  976.      * @throws  PEAR_Error::SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION
  977.      * @access  private
  978.      */
  979.     function _parseForecastData($data)
  980.     {
  981.         static $compass;
  982.         static $clouds;
  983.         static $conditions;
  984.         static $sensors;
  985.         if (!isset($compass)) {
  986.             $compass = array(
  987.                 "N""NNE""NE""ENE",
  988.                 "E""ESE""SE""SSE",
  989.                 "S""SSW""SW""WSW",
  990.                 "W""WNW""NW""NNW"
  991.             );
  992.             $clouds    = array(
  993.                 "skc"         => "sky clear",
  994.                 "nsc"         => "no significant cloud",
  995.                 "few"         => "few",
  996.                 "sct"         => "scattered",
  997.                 "bkn"         => "broken",
  998.                 "ovc"         => "overcast",
  999.                 "vv"          => "vertical visibility",
  1000.                 "tcu"         => "Towering Cumulus",
  1001.                 "cb"          => "Cumulonimbus",
  1002.                 "clr"         => "clear below 12,000 ft"
  1003.             );
  1004.             $conditions = array(
  1005.                 "+"           => "heavy",                   "-"           => "light",
  1006.  
  1007.                 "vc"          => "vicinity",                "re"          => "recent",
  1008.                 "nsw"         => "no significant weather",
  1009.  
  1010.                 "mi"          => "shallow",                 "bc"          => "patches",
  1011.                 "pr"          => "partial",                 "ts"          => "thunderstorm",
  1012.                 "bl"          => "blowing",                 "sh"          => "showers",
  1013.                 "dr"          => "low drifting",            "fz"          => "freezing",
  1014.  
  1015.                 "dz"          => "drizzle",                 "ra"          => "rain",
  1016.                 "sn"          => "snow",                    "sg"          => "snow grains",
  1017.                 "ic"          => "ice crystals",            "pe"          => "ice pellets",
  1018.                 "pl"          => "ice pellets",             "gr"          => "hail",
  1019.                 "gs"          => "small hail/snow pellets""up"          => "unknown precipitation",
  1020.  
  1021.                 "br"          => "mist",                    "fg"          => "fog",
  1022.                 "fu"          => "smoke",                   "va"          => "volcanic ash",
  1023.                 "sa"          => "sand",                    "hz"          => "haze",
  1024.                 "py"          => "spray",                   "du"          => "widespread dust",
  1025.  
  1026.                 "sq"          => "squall",                  "ss"          => "sandstorm",
  1027.                 "ds"          => "duststorm",               "po"          => "well developed dust/sand whirls",
  1028.                 "fc"          => "funnel cloud",
  1029.  
  1030.                 "+fc"         => "tornado/waterspout"
  1031.             );
  1032.         }
  1033.  
  1034.         $tafCode = array(
  1035.             "report"      => "TAF|AMD",
  1036.             "station"     => "\w{4}",
  1037.             "update"      => "(\d{2})?(\d{4})Z",
  1038.             "valid"       => "(\d{2})(\d{2})\/(\d{2})(\d{2})",
  1039.             "wind"        => "(\d{3}|VAR|VRB)(\d{2,3})(G(\d{2,3}))?(FPS|KPH|KT|KTS|MPH|MPS)",
  1040.             "visFrac"     => "(\d{1})",
  1041.             "visibility"  => "(\d{4})|((M|P)?((\d{1,2}|((\d) )?(\d)\/(\d))(SM|KM)))|(CAVOK)",
  1042.             "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)?",
  1043.             "clouds"      => "(SKC|CLR|NSC|((FEW|SCT|BKN|OVC|VV)(\d{3}|\/{3})(TCU|CB)?))",
  1044.             "windshear"   => "WS(\d{3})\/(\d{3})(\d{2,3})(FPS|KPH|KT|KTS|MPH|MPS)",
  1045.             "tempmax"     => "TX(\d{2})\/(\d{2})(\w)",
  1046.             "tempmin"     => "TN(\d{2})\/(\d{2})(\w)",
  1047.             "tempmaxmin"  => "TX(\d{2})\/(\d{2})(\w)TN(\d{2})\/(\d{2})(\w)",
  1048.             "from"        => "FM(\d{2})(\d{2})(\d{2})?Z?",
  1049.             "fmc"         => "(PROB|BECMG|TEMPO)(\d{2})?"
  1050.         );
  1051.  
  1052.         if (SERVICES_WEATHER_DEBUG{
  1053.             for ($i = 0; $i sizeof($data)$i++{
  1054.                 echo $data[$i]."\n";
  1055.             }
  1056.         }
  1057.         // Ok, we have correct data, start with parsing the first line for the last update
  1058.         $forecastData = array();
  1059.         $forecastData["station"]   "";
  1060.         // Eliminate trailing information
  1061.         if (strpos($data"="!== false{
  1062.             $data substr($data0strpos($data"="));
  1063.         }
  1064.         $forecastData["dataRaw"]   implode(" "$data);
  1065.         $forecastData["update"]    strtotime(trim($data[0])." GMT");
  1066.         $forecastData["updateRaw"trim($data[0]);
  1067.         // and prepare the rest for stepping through
  1068.         array_shift($data);
  1069.         $taf explode(" "preg_replace("/\s{2,}/"" "implode(" "$data)));
  1070.  
  1071.         // Add a few local variables for data processing
  1072.         $fromTime =  "";            // The timeperiod the data gets added to
  1073.         $fmcCount =  0;             // If we have FMCs (Forecast Meteorological Conditions), we need this
  1074.         $pointer  =$forecastData// Pointer to the array we add the data to
  1075.         for ($i = 0; $i sizeof($taf)$i++{
  1076.             // Check for whitespace and step loop, if nothing's there
  1077.             $taf[$itrim($taf[$i]);
  1078.             if (!strlen($taf[$i])) {
  1079.                 continue;
  1080.             }
  1081.  
  1082.             if (SERVICES_WEATHER_DEBUG{
  1083.                 $tab str_repeat("\t"3 - floor((strlen($taf[$i]+ 2/ 8));
  1084.                 echo "\"".$taf[$i]."\"".$tab."-> ";
  1085.             }
  1086.  
  1087.             // Initialize some arrays
  1088.             $result   = array();
  1089.             $resultVF = array();
  1090.             $lresult  = array();
  1091.  
  1092.             $found = false;
  1093.             foreach ($tafCode as $key => $regexp{
  1094.                 // Check if current code matches current taf snippet
  1095.                 if (($found preg_match("/^".$regexp."$/i"$taf[$i]$result)) == true{
  1096.                     $insert = array();
  1097.                     switch ($key{
  1098.                         case "station":
  1099.                             $pointer["station"$result[0];
  1100.                             unset($tafCode["station"]);
  1101.                             break;
  1102.                         case "valid":
  1103.                             $pointer["validRaw"$result[0];
  1104.                             // Generates the timeperiod the report is valid for
  1105.                             list($year$month$dayexplode("-"gmdate("Y-m-d"$forecastData["update"]));
  1106.                             // Date is in next month
  1107.                             if ($result[1$day{
  1108.                                 $month++;
  1109.                             }
  1110.                             $pointer["validFrom"gmmktime($result[2]00$month$result[1]$year);
  1111.                             $pointer["validTo"]   gmmktime($result[4]00$month$result[3]$year);
  1112.                             unset($tafCode["valid"]);
  1113.                             // Now the groups will start, so initialize the time groups
  1114.                             $pointer["time"= array();
  1115.                             $fromTime $result[2].":00";
  1116.                             $pointer["time"][$fromTime= array();
  1117.                             // Set pointer to the first timeperiod
  1118.                             $pointer =$pointer["time"][$fromTime];
  1119.                             break;
  1120.                         case "wind":
  1121.                             // Parse wind data, first the speed, convert from kt to chosen unit
  1122.                             if ($result[5== "KTS"{
  1123.                                 $result[5"KT";
  1124.                             }
  1125.                             $pointer["wind"$this->convertSpeed($result[2]$result[5]"mph");
  1126.                             if ($result[1== "VAR" || $result[1== "VRB"{
  1127.                                 // Variable winds
  1128.                                 $pointer["windDegrees"]   "Variable";
  1129.                                 $pointer["windDirection""Variable";
  1130.                             else {
  1131.                                 // Save wind degree and calc direction
  1132.                                 $pointer["windDegrees"]   $result[1];
  1133.                                 $pointer["windDirection"$compass[round($result[1/ 22.5% 16];
  1134.                             }
  1135.                             if (is_numeric($result[4])) {
  1136.                                 // Wind with gusts...
  1137.                                 $pointer["windGust"$this->convertSpeed($result[4]$result[5]"mph");
  1138.                             }
  1139.                             if (isset($probability)) {
  1140.                                 $pointer["windProb"$probability;
  1141.                                 unset($probability);
  1142.                             }
  1143.                             break;
  1144.                         case "visFrac":
  1145.                             // Possible fractional visibility here. Check if it matches with the next TAF piece for visibility
  1146.                             if (!isset($taf[$i + 1]|| !preg_match("/^".$tafCode["visibility"]."$/i"$result[1]." ".$taf[$i + 1]$resultVF)) {
  1147.                                 // No next TAF piece available or not matching. Match against next TAF code
  1148.                                 $found = false;
  1149.                                 break;
  1150.                             else {
  1151.                                 // Match. Hand over result and advance TAF
  1152.                                 if (SERVICES_WEATHER_DEBUG{
  1153.                                     echo $key."\n";
  1154.                                     echo "\"".$result[1]." ".$taf[$i + 1]."\"".str_repeat("\t"2 - floor((strlen($result[1]." ".$taf[$i + 1]+ 2/ 8))."-> ";
  1155.                                 }
  1156.                                 $key "visibility";
  1157.                                 $result $resultVF;
  1158.                                 $i++;
  1159.                             }
  1160.                         case "visibility":
  1161.                             $pointer["visQualifier""AT";
  1162.                             if (is_numeric($result[1]&& ($result[1== 9999)) {
  1163.                                 // Upper limit of visibility range
  1164.                                 $visibility $this->convertDistance(10"km""sm");
  1165.                                 $pointer["visQualifier""BEYOND";
  1166.                             elseif (is_numeric($result[1])) {
  1167.                                 // 4-digit visibility in m
  1168.                                 $visibility $this->convertDistance(($result[1]/1000)"km""sm");
  1169.                             elseif (!isset($result[11]|| $result[11!= "CAVOK"{
  1170.                                 if ($result[3== "M"{
  1171.                                     $pointer["visQualifier""BELOW";
  1172.                                 elseif ($result[3== "P"{
  1173.                                     $pointer["visQualifier""BEYOND";
  1174.                                 }
  1175.                                 if (is_numeric($result[5])) {
  1176.                                     // visibility as one/two-digit number
  1177.                                     $visibility $this->convertDistance($result[5]$result[10]"sm");
  1178.                                 else {
  1179.                                     // the y/z part, add if we had a x part (see visibility1)
  1180.                                     if (is_numeric($result[7])) {
  1181.                                         $visibility $this->convertDistance($result[7$result[8$result[9]$result[10]"sm");
  1182.                                     else {
  1183.                                         $visibility $this->convertDistance($result[8$result[9]$result[10]"sm");
  1184.                                     }
  1185.                                 }
  1186.                             else {
  1187.                                 $pointer["visQualifier""BEYOND";
  1188.                                 $visibility $this->convertDistance(10"km""sm");
  1189.                                 $pointer["clouds"= array(array("amount" => "Clear below""height" => 5000));
  1190.                                 $pointer["condition""no significant weather";
  1191.                             }
  1192.                             if (isset($probability)) {
  1193.                                 $pointer["visProb"$probability;
  1194.                                 unset($probability);
  1195.                             }
  1196.                             $pointer["visibility"$visibility;
  1197.                             break;
  1198.                         case "condition":
  1199.                             // First some basic setups
  1200.                             if (!isset($pointer["condition"])) {
  1201.                                 $pointer["condition""";
  1202.                             elseif (strlen($pointer["condition"]> 0{
  1203.                                 $pointer["condition".= ",";
  1204.                             }
  1205.  
  1206.                             if (in_array(strtolower($result[0])$conditions)) {
  1207.                                 // First try matching the complete string
  1208.                                 $pointer["condition".= " ".$conditions[strtolower($result[0])];
  1209.                             else {
  1210.                                 // No luck, match part by part
  1211.                                 array_shift($result);
  1212.                                 $result array_unique($result);
  1213.                                 foreach ($result as $condition{
  1214.                                     if (strlen($condition> 0{
  1215.                                         $pointer["condition".= " ".$conditions[strtolower($condition)];
  1216.                                     }
  1217.                                 }
  1218.                             }
  1219.                             $pointer["condition"trim($pointer["condition"]);
  1220.                             if (isset($probability)) {
  1221.                                 $pointer["condition".= " (".$probability."% prob.)";
  1222.                                 unset($probability);
  1223.                             }
  1224.                             break;
  1225.                         case "clouds":
  1226.                             if (!isset($pointer["clouds"])) {
  1227.                                 $pointer["clouds"= array();
  1228.                             }
  1229.  
  1230.                             if (sizeof($result== 5{
  1231.                                 // Only amount and height
  1232.                                 $cloud = array("amount" => $clouds[strtolower($result[3])]);
  1233.                                 if ($result[4== "///"{
  1234.                                     $cloud["height""station level or below";
  1235.                                 else {
  1236.                                     $cloud["height"$result[4* 100;
  1237.                                 }
  1238.                             elseif (sizeof($result== 6{
  1239.                                 // Amount, height and type
  1240.                                 $cloud = array("amount" => $clouds[strtolower($result[3])]"type" => $clouds[strtolower($result[5])]);
  1241.                                 if ($result[4== "///"{
  1242.                                     $cloud["height""station level or below";
  1243.                                 else {
  1244.                                     $cloud["height"$result[4* 100;
  1245.                                 }
  1246.                             else {
  1247.                                 // SKC or CLR or NSC
  1248.                                 $cloud = array("amount" => $clouds[strtolower($result[0])]);
  1249.                             }
  1250.                             if (isset($probability)) {
  1251.                                 $cloud["prob"$probability;
  1252.                                 unset($probability);
  1253.                             }
  1254.                             $pointer["clouds"][$cloud;
  1255.                             break;
  1256.                         case "windshear":
  1257.                             // Parse windshear, if available
  1258.                             if ($result[4== "KTS"{
  1259.                                 $result[4"KT";
  1260.                             }
  1261.                             $pointer["windshear"]          $this->convertSpeed($result[3]$result[4]"mph");
  1262.                             $pointer["windshearHeight"]    $result[1* 100;
  1263.                             $pointer["windshearDegrees"]   $result[2];
  1264.                             $pointer["windshearDirection"$compass[round($result[2/ 22.5% 16];
  1265.                             break;
  1266.                         case "tempmax":
  1267.                             $forecastData["temperatureHigh"$this->convertTemperature($result[1]"c""f");
  1268.                             break;
  1269.                         case "tempmin":
  1270.                             // Parse max/min temperature
  1271.                             $forecastData["temperatureLow"]  $this->convertTemperature($result[1]"c""f");
  1272.                             break;
  1273.                         case "tempmaxmin":
  1274.                             $forecastData["temperatureHigh"$this->convertTemperature($result[1]"c""f");
  1275.                             $forecastData["temperatureLow"]  $this->convertTemperature($result[4]"c""f");
  1276.                             break;
  1277.                         case "from":
  1278.                             // Next timeperiod is coming up, prepare array and
  1279.                             // set pointer accordingly
  1280.                             if (sizeof($result> 2{
  1281.                                 // The ICAO way
  1282.                                 $fromTime $result[2].":".$result[3];
  1283.                             else {
  1284.                                 // The Australian way (Hey mates!)
  1285.                                 $fromTime $result[1].":00";
  1286.                             }
  1287.                             $forecastData["time"][$fromTime= array();
  1288.                             $fmcCount = 0;
  1289.                             $pointer =$forecastData["time"][$fromTime];
  1290.                             break;
  1291.                         case "fmc";
  1292.                             // Test, if this is a probability for the next FMC
  1293.                             if (isset($result[2]&& preg_match("/^BECMG|TEMPO$/i"$taf[$i + 1]$lresult)) {
  1294.                                 // Set type to BECMG or TEMPO
  1295.                                 $type $lresult[0];
  1296.                                 // Set probability
  1297.                                 $probability $result[2];
  1298.                                 // Now extract time for this group
  1299.                                 if (preg_match("/^(\d{2})(\d{2})$/i"$taf[$i + 2]$lresult)) {
  1300.                                     $from $lresult[1].":00";
  1301.                                     $to   $lresult[2].":00";
  1302.                                     $to   ($to == "24:00""00:00" $to;
  1303.                                     // As we now have type, probability and time for this FMC
  1304.                                     // from our TAF, increase field-counter
  1305.                                     $i += 2;
  1306.                                 else {
  1307.                                     // No timegroup present, so just increase field-counter by one
  1308.                                     $i += 1;
  1309.                                 }
  1310.                             elseif (preg_match("/^(\d{2})(\d{2})\/(\d{2})(\d{2})$/i"$taf[$i + 1]$lresult)) {
  1311.                                 // Normal group, set type and use extracted time
  1312.                                 $type $result[1];
  1313.                                 // Check for PROBdd
  1314.                                 if (isset($result[2])) {
  1315.                                     $probability $result[2];
  1316.                                 }
  1317.                                 $from $lresult[2].":00";
  1318.                                 $to   $lresult[4].":00";
  1319.                                 $to   ($to == "24:00""00:00" $to;
  1320.                                 // Same as above, we have a time for this FMC from our TAF,
  1321.                                 // increase field-counter
  1322.                                 $i += 1;
  1323.                             elseif (isset($result[2])) {
  1324.                                 // This is either a PROBdd or a malformed TAF with missing timegroup
  1325.                                 $probability $result[2];
  1326.                             }
  1327.  
  1328.                             // Handle the FMC, generate neccessary array if it's the first...
  1329.                             if (isset($type)) {
  1330.                                 if (!isset($forecastData["time"][$fromTime]["fmc"])) {
  1331.                                     $forecastData["time"][$fromTime]["fmc"= array();
  1332.                                 }
  1333.                                 $forecastData["time"][$fromTime]["fmc"][$fmcCount= array();
  1334.                                 // ...and set pointer.
  1335.                                 $pointer =$forecastData["time"][$fromTime]["fmc"][$fmcCount];
  1336.                                 $fmcCount++;
  1337.                                 // Insert data
  1338.                                 $pointer["type"$type;
  1339.                                 unset($type);
  1340.                                 if (isset($from)) {
  1341.                                     $pointer["from"$from;
  1342.                                     $pointer["to"]   $to;
  1343.                                     unset($from$to);
  1344.                                 }
  1345.                                 if (isset($probability)) {
  1346.                                     $pointer["probability"$probability;
  1347.                                     unset($probability);
  1348.                                 }
  1349.                             }
  1350.                             break;
  1351.                         default:
  1352.                             // Do nothing
  1353.                             break;
  1354.                     }
  1355.                     if ($found && !SERVICES_WEATHER_DEBUG{
  1356.                         break;
  1357.                     elseif ($found && SERVICES_WEATHER_DEBUG{
  1358.                         echo $key."\n";
  1359.                         break;
  1360.                     }
  1361.                 }
  1362.             }
  1363.             if (!$found{
  1364.                 if (SERVICES_WEATHER_DEBUG{
  1365.                     echo "n/a\n";
  1366.                 }
  1367.                 if (!isset($forecastData["noparse"])) {
  1368.                     $forecastData["noparse"= array();
  1369.                 }
  1370.                 $forecastData["noparse"][$taf[$i];
  1371.             }
  1372.         }
  1373.  
  1374.         if (isset($forecastData["noparse"])) {
  1375.             $forecastData["noparse"implode(" ",  $forecastData["noparse"]);
  1376.         }
  1377.  
  1378.         return $forecastData;
  1379.     }
  1380.     // }}}
  1381.  
  1382.     // {{{ _convertReturn()
  1383.     /**
  1384.      * Converts the data in the return array to the desired units and/or
  1385.      * output format.
  1386.      *
  1387.      * @param   array                       $target 
  1388.      * @param   string                      $units 
  1389.      * @param   string                      $location 
  1390.      * @access  private
  1391.      */
  1392.     function _convertReturn(&$target$units$location)
  1393.     {
  1394.         if (is_array($target)) {
  1395.             foreach ($target as $key => $val{
  1396.                 if (is_array($val)) {
  1397.                     // Another array detected, so recurse into it to convert the units
  1398.                     $this->_convertReturn($target[$key]$units$location);
  1399.                 else {
  1400.                     switch ($key{
  1401.                         case "station":
  1402.                             $newVal $location["name"];
  1403.                             break;
  1404.                         case "update":
  1405.                         case "validFrom":
  1406.                         case "validTo":
  1407.                             $newVal gmdate(trim($this->_dateFormat." ".$this->_timeFormat)$val);
  1408.                             break;
  1409.                         case "wind":
  1410.                         case "windGust":
  1411.                         case "windshear":
  1412.                             $newVal $this->convertSpeed($val"mph"$units["wind"]);
  1413.                             break;
  1414.                         case "visibility":
  1415.                             $newVal $this->convertDistance($val"sm"$units["vis"]);
  1416.                             break;
  1417.                         case "height":
  1418.                         case "windshearHeight":
  1419.                             if (is_numeric($val)) {
  1420.                                 $newVal $this->convertDistance($val"ft"$units["height"]);
  1421.                             else {
  1422.                                 $newVal $val;
  1423.                             }
  1424.                             break;
  1425.                         case "temperature":
  1426.                         case "temperatureHigh":
  1427.                         case "temperatureLow":
  1428.                         case "dewPoint":
  1429.                         case "feltTemperature":
  1430.                             $newVal $this->convertTemperature($val"f"$units["temp"]);
  1431.                             break;
  1432.                         case "pressure":
  1433.                         case "seapressure":
  1434.                         case "presschng":
  1435.                             $newVal $this->convertPressure($val"in"$units["pres"]);
  1436.                             break;
  1437.                         case "amount":
  1438.                         case "snowdepth":
  1439.                         case "snowequiv":
  1440.                             if (is_numeric($val)) {
  1441.                                 $newVal $this->convertPressure($val"in"$units["rain"]);
  1442.                             else {
  1443.                                 $newVal $val;
  1444.                             }
  1445.                             break;
  1446.                         case "1htemp":
  1447.                         case "1hdew":
  1448.                         case "6hmaxtemp":
  1449.                         case "6hmintemp":
  1450.                         case "24hmaxtemp":
  1451.                         case "24hmintemp":
  1452.                             $newVal $this->convertTemperature($val"f"$units["temp"]);
  1453.                             break;
  1454.                         default:
  1455.                             continue 2;
  1456.                     }
  1457.                     $target[$key$newVal;
  1458.                 }
  1459.             }
  1460.         }
  1461.     }
  1462.     // }}}
  1463.  
  1464.     // {{{ searchLocation()
  1465.     /**
  1466.      * Searches IDs for given location, returns array of possible locations
  1467.      * or single ID
  1468.      *
  1469.      * @param   string|array               $location 
  1470.      * @param   bool                        $useFirst       If set, first ID of result-array is returned
  1471.      * @return  PEAR_Error|array|string
  1472.      * @throws  PEAR_Error::SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION
  1473.      * @throws  PEAR_Error::SERVICES_WEATHER_ERROR_DB_NOT_CONNECTED
  1474.      * @throws  PEAR_Error::SERVICES_WEATHER_ERROR_INVALID_LOCATION
  1475.      * @access  public
  1476.      */
  1477.     function searchLocation($location$useFirst = false)
  1478.     {
  1479.         if (!isset($this->_db|| !DB::isConnection($this->_db)) {
  1480.             return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_DB_NOT_CONNECTED__FILE____LINE__);
  1481.         }
  1482.  
  1483.         if (is_string($location)) {
  1484.             // Try to part search string in name, state and country part
  1485.             // and build where clause from it for the select
  1486.             $location explode(","$location);
  1487.  
  1488.             // Trim, caps-low and quote the strings
  1489.             for ($i = 0; $i sizeof($location)$i++{
  1490.                 $location[$i$this->_db->quote("%".strtolower(trim($location[$i]))."%");
  1491.             }
  1492.  
  1493.             if (sizeof($location== 1{
  1494.                 $where  "LOWER(name) LIKE ".$location[0];
  1495.             elseif (sizeof($location== 2{
  1496.                 $where  "LOWER(name) LIKE ".$location[0];
  1497.                 $where .= " AND LOWER(country) LIKE ".$location[1];
  1498.             elseif (sizeof($location== 3{
  1499.                 $where  "LOWER(name) LIKE ".$location[0];
  1500.                 $where .= " AND LOWER(state) LIKE ".$location[1];
  1501.                 $where .= " AND LOWER(country) LIKE ".$location[2];
  1502.             elseif (sizeof($location== 4{
  1503.                 $where  "LOWER(name) LIKE ".substr($location[0]0-2).", ".substr($location[1]2);
  1504.                 $where .= " AND LOWER(state) LIKE ".$location[2];
  1505.                 $where .= " AND LOWER(country) LIKE ".$location[3];
  1506.             }
  1507.  
  1508.             // Create select, locations with ICAO first
  1509.             $select "SELECT icao, name, state, country, latitude, longitude ".
  1510.                       "FROM metarLocations ".
  1511.                       "WHERE ".$where." ".
  1512.                       "ORDER BY icao DESC";
  1513.             $result $this->_db->query($select);
  1514.             // Check result for validity
  1515.             if (DB::isError($result)) {
  1516.                 return $result;
  1517.             elseif (strtolower(get_class($result)) != "db_result" || $result->numRows(== 0{
  1518.                 return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION__FILE____LINE__);
  1519.             }
  1520.  
  1521.             // Result is valid, start preparing the return
  1522.             $icao = array();
  1523.             while (($row $result->fetchRow(DB_FETCHMODE_ASSOC)) != null{
  1524.                 $locicao $row["icao"];
  1525.                 // First the name of the location
  1526.                 if (!strlen($row["state"])) {
  1527.                     $locname $row["name"].", ".$row["country"];
  1528.                 else {
  1529.                     $locname $row["name"].", ".$row["state"].", ".$row["country"];
  1530.                 }
  1531.                 if ($locicao != "----"{
  1532.                     // We have a location with ICAO
  1533.                     $icao[$locicao$locname;
  1534.                 else {
  1535.                     // No ICAO, try finding the nearest airport
  1536.                     $locicao $this->searchAirport($row["latitude"]$row["longitude"]);
  1537.                     if (!isset($icao[$locicao])) {
  1538.                         $icao[$locicao$locname;
  1539.                     }
  1540.                 }
  1541.             }
  1542.             // Only one result? Return as string
  1543.             if (sizeof($icao== 1 || $useFirst{
  1544.                 $icao key($icao);
  1545.             }
  1546.         elseif (is_array($location)) {
  1547.             // Location was provided as coordinates, search nearest airport
  1548.             $icao $this->searchAirport($location[0]$location[1]);
  1549.         else {
  1550.             return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_INVALID_LOCATION__FILE____LINE__);
  1551.         }
  1552.  
  1553.         return $icao;
  1554.     }
  1555.     // }}}
  1556.  
  1557.     // {{{ searchLocationByCountry()
  1558.     /**
  1559.      * Returns IDs with location-name for a given country or all available
  1560.      * countries, if no value was given
  1561.      *
  1562.      * @param   string                      $country 
  1563.      * @return  PEAR_Error|array
  1564.      * @throws  PEAR_Error::SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION
  1565.      * @throws  PEAR_Error::SERVICES_WEATHER_ERROR_DB_NOT_CONNECTED
  1566.      * @throws  PEAR_Error::SERVICES_WEATHER_ERROR_WRONG_SERVER_DATA
  1567.      * @access  public
  1568.      */
  1569.     function searchLocationByCountry($country "")
  1570.     {
  1571.         if (!isset($this->_db|| !DB::isConnection($this->_db)) {
  1572.             return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_DB_NOT_CONNECTED__FILE____LINE__);
  1573.         }
  1574.  
  1575.         // Return the available countries as no country was given
  1576.         if (!strlen($country)) {
  1577.             $select "SELECT DISTINCT(country) ".
  1578.                       "FROM metarAirports ".
  1579.                       "ORDER BY country ASC";
  1580.             $countries $this->_db->getCol($select);
  1581.  
  1582.             // As $countries is either an error or the true result,
  1583.             // we can just return it
  1584.             return $countries;
  1585.         }
  1586.  
  1587.         // Now for the real search
  1588.         $select "SELECT icao, name, state, country ".
  1589.                   "FROM metarAirports ".
  1590.                   "WHERE LOWER(country) LIKE '%".strtolower(trim($country))."%' ".
  1591.                   "ORDER BY name ASC";
  1592.         $result $this->_db->query($select);
  1593.         // Check result for validity
  1594.         if (DB::isError($result)) {
  1595.             return $result;
  1596.         elseif (strtolower(get_class($result)) != "db_result" || $result->numRows(== 0{
  1597.             return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION__FILE____LINE__);
  1598.         }
  1599.  
  1600.         // Construct the result
  1601.         $locations = array();
  1602.         while (($row $result->fetchRow(DB_FETCHMODE_ASSOC)) != null{
  1603.             $locicao $row["icao"];
  1604.             if ($locicao != "----"{
  1605.                 // First the name of the location
  1606.                 if (!strlen($row["state"])) {
  1607.                     $locname $row["name"].", ".$row["country"];
  1608.                 else {
  1609.                     $locname $row["name"].", ".$row["state"].", ".$row["country"];
  1610.                 }
  1611.                 $locations[$locicao$locname;
  1612.             }
  1613.         }
  1614.  
  1615.         return $locations;
  1616.     }
  1617.     // }}}
  1618.  
  1619.     // {{{ searchAirport()
  1620.     /**
  1621.      * Searches the nearest airport(s) for given coordinates, returns array
  1622.      * of IDs or single ID
  1623.      *
  1624.      * @param   float                       $latitude 
  1625.      * @param   float                       $longitude 
  1626.      * @param   int                         $numResults 
  1627.      * @return  PEAR_Error|array|string
  1628.      * @throws  PEAR_Error::SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION
  1629.      * @throws  PEAR_Error::SERVICES_WEATHER_ERROR_DB_NOT_CONNECTED
  1630.      * @throws  PEAR_Error::SERVICES_WEATHER_ERROR_INVALID_LOCATION
  1631.      * @access  public
  1632.      */
  1633.     function searchAirport($latitude$longitude$numResults = 1)
  1634.     {
  1635.         if (!isset($this->_db|| !DB::isConnection($this->_db)) {
  1636.             return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_DB_NOT_CONNECTED__FILE____LINE__);
  1637.         }
  1638.         if (!is_numeric($latitude|| !is_numeric($longitude)) {
  1639.             return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_INVALID_LOCATION__FILE____LINE__);
  1640.         }
  1641.  
  1642.         // Get all airports
  1643.         $select "SELECT icao, x, y, z FROM metarAirports";
  1644.         $result $this->_db->query($select);
  1645.         if (DB::isError($result)) {
  1646.             return $result;
  1647.         elseif (strtolower(get_class($result)) != "db_result" || $result->numRows(== 0{
  1648.             return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION__FILE____LINE__);
  1649.         }
  1650.  
  1651.         // Result is valid, start search
  1652.         // Initialize values
  1653.         $min_dist = null;
  1654.         $query    $this->polar2cartesian($latitude$longitude);
  1655.         $search   = array("dist" => array()"icao" => array());
  1656.         while (($row $result->fetchRow(DB_FETCHMODE_AS