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

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