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

Documentation generated on Mon, 11 Mar 2019 15:50:59 -0400 by phpDocumentor 1.4.4. PEAR Logo Copyright © PHP Group 2004.