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

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