PageRenderTime 34ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 1ms

/application/libraries/PEAR/Services/Weather/Metar.php

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