PageRenderTime 42ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 0ms

/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

Large files files are truncated, but you can click here to view the full file

  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",

Large files files are truncated, but you can click here to view the full file