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

Documentation generated on Mon, 11 Mar 2019 14:37:52 -0400 by phpDocumentor 1.4.4. PEAR Logo Copyright © PHP Group 2004.