Metar.php 53 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168
  1. <?php
  2. /* vim: set expandtab tabstop=4 shiftwidth=4: */
  3. // +----------------------------------------------------------------------+
  4. // | PHP version 4 |
  5. // +----------------------------------------------------------------------+
  6. // | Copyright (c) 1997-2004 The PHP Group |
  7. // +----------------------------------------------------------------------+
  8. // | This source file is subject to version 2.0 of the PHP license, |
  9. // | that is bundled with this package in the file LICENSE, and is |
  10. // | available through the world-wide-web at |
  11. // | http://www.php.net/license/2_02.txt. |
  12. // | If you did not receive a copy of the PHP license and are unable to |
  13. // | obtain it through the world-wide-web, please send a note to |
  14. // | license@php.net so we can mail you a copy immediately. |
  15. // +----------------------------------------------------------------------+
  16. // | Authors: Alexander Wirtz <alex@pc4p.net> |
  17. // +----------------------------------------------------------------------+
  18. //
  19. // $Id: Metar.php,v 1.34 2004/01/06 19:36:27 eru Exp $
  20. require_once "Services/Weather/Common.php";
  21. require_once "DB.php";
  22. // {{{ class Services_Weather_Metar
  23. /**
  24. * PEAR::Services_Weather_Metar
  25. *
  26. * This class acts as an interface to the metar service of weather.noaa.gov. It searches for
  27. * locations given in ICAO notation and retrieves the current weather data.
  28. *
  29. * Of course the parsing of the METAR-data has its limitations, as it follows the
  30. * Federal Meteorological Handbook No.1 with modifications to accomodate for non-US reports,
  31. * so if the report deviates from these standards, you won't get it parsed correctly.
  32. * Anything that is not parsed, is saved in the "noparse" array-entry, returned by
  33. * getWeather(), so you can do your own parsing afterwards. This limitation is specifically
  34. * given for remarks, as the class is not processing everything mentioned there, but you will
  35. * get the most common fields like precipitation and temperature-changes. Again, everything
  36. * not parsed, goes into "noparse".
  37. *
  38. * If you think, some important field is missing or not correctly parsed, please file a feature-
  39. * request/bugreport at http://pear.php.net/ and be sure to provide the METAR report with a
  40. * _detailed_ explanation!
  41. *
  42. * For a working example, please take a look at
  43. * docs/Services_Weather/examples/metar-basic.php
  44. *
  45. * @author Alexander Wirtz <alex@pc4p.net>
  46. * @link http://weather.noaa.gov/weather/metar.shtml
  47. * @example docs/Services_Weather/examples/metar-basic.php
  48. * @package Services_Weather
  49. * @license http://www.php.net/license/2_02.txt
  50. * @version 1.2
  51. */
  52. class Services_Weather_Metar extends Services_Weather_Common
  53. {
  54. // {{{ properties
  55. /**
  56. * Information to access the location DB
  57. *
  58. * @var object DB $_db
  59. * @access private
  60. */
  61. var $_db;
  62. /**
  63. * The source METAR uses
  64. *
  65. * @var string $_source
  66. * @access private
  67. */
  68. var $_source;
  69. /**
  70. * This path is used to find the METAR data
  71. *
  72. * @var string $_sourcePath
  73. * @access private
  74. */
  75. var $_sourcePath;
  76. // }}}
  77. // {{{ constructor
  78. /**
  79. * Constructor
  80. *
  81. * @param array $options
  82. * @param mixed $error
  83. * @throws PEAR_Error
  84. * @see Science_Weather::Science_Weather
  85. * @access private
  86. */
  87. function Services_Weather_Metar($options, &$error)
  88. {
  89. $perror = null;
  90. $this->Services_Weather_Common($options, $perror);
  91. if (Services_Weather::isError($perror)) {
  92. $error = $perror;
  93. return;
  94. }
  95. // Set options accordingly
  96. if (isset($options["dsn"])) {
  97. if (isset($options["dbOptions"])) {
  98. $status = $this->setMetarDB($options["dsn"], $options["dbOptions"]);
  99. } else {
  100. $status = $this->setMetarDB($options["dsn"]);
  101. }
  102. }
  103. if (Services_Weather::isError($status)) {
  104. $error = $status;
  105. return;
  106. }
  107. if (isset($options["source"])) {
  108. if (isset($options["sourcePath"])) {
  109. $this->setMetarSource($options["source"], $options["sourcePath"]);
  110. } else {
  111. $this->setMetarSource($options["source"]);
  112. }
  113. } else {
  114. $this->setMetarSource("http");
  115. }
  116. }
  117. // }}}
  118. // {{{ setMetarDB()
  119. /**
  120. * Sets the parameters needed for connecting to the DB, where the location-
  121. * search is fetching its data from. You need to build a DB with the external
  122. * tool buildMetarDB first, it fetches the locations and airports from a
  123. * NOAA-website.
  124. *
  125. * @param string $dsn
  126. * @param array $dbOptions
  127. * @return DB_Error|bool
  128. * @throws DB_Error
  129. * @see DB::parseDSN
  130. * @access public
  131. */
  132. function setMetarDB($dsn, $dbOptions = array())
  133. {
  134. $dsninfo = DB::parseDSN($dsn);
  135. if (is_array($dsninfo) && !isset($dsninfo["mode"])) {
  136. $dsninfo["mode"]= 0644;
  137. }
  138. // Initialize connection to DB and store in object if successful
  139. $db = DB::connect($dsninfo, $dbOptions);
  140. if (DB::isError($db)) {
  141. return $db;
  142. }
  143. $this->_db = $db;
  144. return true;
  145. }
  146. // }}}
  147. // {{{ setMetarSource()
  148. /**
  149. * Sets the source, where the class tries to locate the METAR data
  150. *
  151. * Source can be http, ftp or file.
  152. * An alternate sourcepath can be provided.
  153. *
  154. * @param string $source
  155. * @param string $sourcePath
  156. * @access public
  157. */
  158. function setMetarSource($source, $sourcePath = "")
  159. {
  160. if (in_array($source, array("http", "ftp", "file"))) {
  161. $this->_source = $source;
  162. }
  163. if (strlen($sourcePath)) {
  164. $this->_sourcePath = $sourcePath;
  165. } else {
  166. switch ($source) {
  167. case "http":
  168. $this->_sourcePath = "http://weather.noaa.gov/pub/data/observations/metar/stations/";
  169. break;
  170. case "ftp":
  171. $this->_sourcePath = "ftp://weather.noaa.gov/data/observations/metar/stations/";
  172. break;
  173. case "file":
  174. $this->_sourcePath = "./";
  175. break;
  176. }
  177. }
  178. }
  179. // }}}
  180. // {{{ _checkLocationID()
  181. /**
  182. * Checks the id for valid values and thus prevents silly requests to METAR server
  183. *
  184. * @param string $id
  185. * @return PEAR_Error|bool
  186. * @throws PEAR_Error::SERVICES_WEATHER_ERROR_NO_LOCATION
  187. * @throws PEAR_Error::SERVICES_WEATHER_ERROR_INVALID_LOCATION
  188. * @access private
  189. */
  190. function _checkLocationID($id)
  191. {
  192. if (!strlen($id)) {
  193. return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_NO_LOCATION);
  194. } elseif (!ctype_alpha($id) || (strlen($id) > 4)) {
  195. return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_INVALID_LOCATION);
  196. }
  197. return true;
  198. }
  199. // }}}
  200. // {{{ _parseWeatherData()
  201. /**
  202. * Parses the data returned by the provided source and caches it
  203. *
  204. * METAR KPIT 091955Z COR 22015G25KT 3/4SM R28L/2600FT TSRA OVC010CB 18/16 A2992 RMK SLP045 T01820159
  205. *
  206. * @param string $source
  207. * @return PEAR_Error|array
  208. * @throws PEAR_Error::SERVICES_WEATHER_ERROR_WRONG_SERVER_DATA
  209. * @throws PEAR_Error::SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION
  210. * @access private
  211. */
  212. function _parseWeatherData($source)
  213. {
  214. static $compass;
  215. static $clouds;
  216. static $conditions;
  217. static $sensors;
  218. if (!isset($compass)) {
  219. $compass = array(
  220. "N", "NNE", "NE", "ENE",
  221. "E", "ESE", "SE", "SSE",
  222. "S", "SSW", "SW", "WSW",
  223. "W", "WNW", "NW", "NNW"
  224. );
  225. $clouds = array(
  226. "skc" => "sky clear",
  227. "few" => "few",
  228. "sct" => "scattered",
  229. "bkn" => "broken",
  230. "ovc" => "overcast",
  231. "vv" => "vertical visibility",
  232. "tcu" => "Towering Cumulus",
  233. "cb" => "Cumulonimbus",
  234. "clr" => "clear below 12,000 ft"
  235. );
  236. $conditions = array(
  237. "+" => "heavy", "-" => "light",
  238. "vc" => "vicinity",
  239. "mi" => "shallow", "bc" => "patches",
  240. "pr" => "partial", "ts" => "thunderstorm",
  241. "bl" => "blowing", "sh" => "showers",
  242. "dr" => "low drifting", "fz" => "freezing",
  243. "dz" => "drizzle", "ra" => "rain",
  244. "sn" => "snow", "sg" => "snow grains",
  245. "ic" => "ice crystals", "pe" => "ice pellets",
  246. "gr" => "hail", "gs" => "small hail/snow pellets",
  247. "up" => "unknown precipitation",
  248. "br" => "mist", "fg" => "fog",
  249. "fu" => "smoke", "va" => "volcanic ash",
  250. "sa" => "sand", "hz" => "haze",
  251. "py" => "spray", "du" => "widespread dust",
  252. "sq" => "squall", "ss" => "sandstorm",
  253. "ds" => "duststorm", "po" => "well developed dust/sand whirls",
  254. "fc" => "funnel cloud",
  255. "+fc" => "tornado/waterspout"
  256. );
  257. $sensors = array(
  258. "rvrno" => "Runway Visual Range Detector offline",
  259. "pwino" => "Present Weather Identifier offline",
  260. "pno" => "Tipping Bucket Rain Gauge offline",
  261. "fzrano" => "Freezing Rain Sensor offline",
  262. "tsno" => "Lightning Detection System offline",
  263. "visno_loc" => "2nd Visibility Sensor offline",
  264. "chino_loc" => "2nd Ceiling Height Indicator offline"
  265. );
  266. }
  267. $metarCode = array(
  268. "report" => "METAR|SPECI",
  269. "station" => "\w{4}",
  270. "update" => "(\d{2})?(\d{4})Z",
  271. "type" => "AUTO|COR",
  272. "wind" => "(\d{3}|VAR|VRB)(\d{2,3})(G(\d{2}))?(\w{2,3})",
  273. "windVar" => "(\d{3})V(\d{3})",
  274. "visibility1" => "\d",
  275. "visibility2" => "M?(\d{4})|((\d{1,2}|(\d)\/(\d))(SM|KM))|(CAVOK)",
  276. "runway" => "R(\d{2})(\w)?\/(P|M)?(\d{4})(FT)?(V(P|M)?(\d{4})(FT)?)?(\w)?",
  277. "condition" => "(-|\+|VC)?(MI|BC|PR|TS|BL|SH|DR|FZ)?(DZ|RA|SN|SG|IC|PL|GR|GS|UP)?(BR|FG|FU|VA|DU|SA|HZ|PY)?(PO|SQ|FC|SS|DS)?",
  278. "clouds" => "(SKC|CLR|((FEW|SCT|BKN|OVC|VV)(\d{3})(TCU|CB)?))",
  279. "temperature" => "(M)?(\d{2})\/((M)?(\d{2})|XX|\/\/)?",
  280. "pressure" => "(A)(\d{4})|(Q)(\d{4})",
  281. "nosig" => "NOSIG",
  282. "remark" => "RMK"
  283. );
  284. $remarks = array(
  285. "nospeci" => "NOSPECI",
  286. "autostation" => "AO(1|2)",
  287. "presschg" => "PRESS(R|F)R",
  288. "seapressure" => "SLP(\d{3}|NO)",
  289. "1hprecip" => "P(\d{4})",
  290. "6hprecip" => "6(\d{4}|\/{4})",
  291. "24hprecip" => "7(\d{4}|\/{4})",
  292. "snowdepth" => "4\/(\d{3})",
  293. "snowequiv" => "933(\d{3})",
  294. "cloudtypes" => "8\/(\d|\/)(\d|\/)(\d|\/)",
  295. "sunduration" => "98(\d{3})",
  296. "1htempdew" => "T(0|1)(\d{3})((0|1)(\d{3}))?",
  297. "6hmaxtemp" => "1(0|1)(\d{3})",
  298. "6hmintemp" => "2(0|1)(\d{3})",
  299. "24htemp" => "4(0|1)(\d{3})(0|1)(\d{3})",
  300. "3hpresstend" => "5([0-8])(\d{3})",
  301. "sensors" => "RVRNO|PWINO|PNO|FZRANO|TSNO|VISNO_LOC|CHINO_LOC",
  302. "maintain" => "[\$]"
  303. );
  304. $data = @file($source);
  305. // Check for correct data, 2 lines in size
  306. if (!$data || !is_array($data) || sizeof($data) < 2) {
  307. return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_WRONG_SERVER_DATA);
  308. } elseif (sizeof($data) > 2) {
  309. return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION);
  310. } else {
  311. if (SERVICES_WEATHER_DEBUG) {
  312. echo $data[0].$data[1];
  313. }
  314. // Ok, we have correct data, start with parsing the first line for the last update
  315. $weatherData = array();
  316. $weatherData["station"] = "";
  317. $weatherData["update"] = strtotime(trim($data[0])." GMT");
  318. // and prepare the second line for stepping through
  319. $metar = explode(" ", trim($data[1]));
  320. for ($i = 0; $i < sizeof($metar); $i++) {
  321. // Check for whitespace and step loop, if nothing's there
  322. $metar[$i] = trim($metar[$i]);
  323. if (!strlen($metar[$i])) {
  324. continue;
  325. }
  326. if (SERVICES_WEATHER_DEBUG) {
  327. $tab = str_repeat("\t", 2 - floor((strlen($metar[$i]) + 2) / 8));
  328. echo "\"".$metar[$i]."\"".$tab."-> ";
  329. }
  330. $found = false;
  331. foreach ($metarCode as $key => $regexp) {
  332. // Check if current code matches current metar snippet
  333. if (($found = preg_match("/^".$regexp."$/i", $metar[$i], $result)) == true) {
  334. switch ($key) {
  335. case "station":
  336. $weatherData["station"] = $result[0];
  337. unset($metarCode["station"]);
  338. break;
  339. case "wind":
  340. // Parse wind data, first the speed, convert from kt to chosen unit
  341. $weatherData["wind"] = $this->convertSpeed($result[2], strtolower($result[5]), "mph");
  342. if ($result[1] == "VAR" || $result[1] == "VRB") {
  343. // Variable winds
  344. $weatherData["windDegrees"] = "Variable";
  345. $weatherData["windDirection"] = "Variable";
  346. } else {
  347. // Save wind degree and calc direction
  348. $weatherData["windDegrees"] = $result[1];
  349. $weatherData["windDirection"] = $compass[round($result[1] / 22.5) % 16];
  350. }
  351. if (is_numeric($result[4])) {
  352. // Wind with gusts...
  353. $weatherData["windGust"] = $this->convertSpeed($result[4], strtolower($result[5]), "mph");
  354. }
  355. // We got that, unset
  356. unset($metarCode["wind"]);
  357. break;
  358. case "windVar":
  359. // Once more wind, now variability around the current wind-direction
  360. $weatherData["windVariability"] = array("from" => $result[1], "to" => $result[2]);
  361. unset($metarCode["windVar"]);
  362. break;
  363. case "visibility1":
  364. // Visibility will come as x y/z, first the single digit part
  365. $weatherData["visibility"] = $result[0];
  366. unset($metarCode["visibility1"]);
  367. break;
  368. case "visibility2":
  369. if (is_numeric($result[1]) && ($result[1] == 9999)) {
  370. // Upper limit of visibility range
  371. $visibility = $this->convertDistance(10, "km", "sm");
  372. $weatherData["visQualifier"] = "BEYOND";
  373. } elseif (is_numeric($result[1])) {
  374. // 4-digit visibility in m
  375. $visibility = $this->convertDistance(($result[1]/1000), "km", "sm");
  376. $weatherData["visQualifier"] = "AT";
  377. } elseif (!isset($result[7]) || $result[7] != "CAVOK") {
  378. if (is_numeric($result[3])) {
  379. // visibility as one/two-digit number
  380. $visibility = $this->convertDistance($result[3], $result[6], "sm");
  381. $weatherData["visQualifier"] = "AT";
  382. } else {
  383. // the y/z part, add if we had a x part (see visibility1)
  384. $visibility = $this->convertDistance($result[4] / $result[5], $result[6], "sm");
  385. if (isset($weatherData["visibility"])) {
  386. $visibility += $weatherData["visibility"];
  387. }
  388. if ($result[0]{0} == "M") {
  389. $weatherData["visQualifier"] = "BELOW";
  390. } else {
  391. $weatherData["visQualifier"] = "AT";
  392. }
  393. }
  394. } else {
  395. $weatherData["visQualifier"] = "BEYOND";
  396. $visibility = $this->convertDistance(10, "km", "sm");
  397. $weatherData["clouds"] = array("amount" => "none", "height" => "below 5000ft");
  398. $weatherData["condition"] = "no significant weather";
  399. }
  400. $weatherData["visibility"] = $visibility;
  401. unset($metarCode["visibility2"]);
  402. break;
  403. case "condition":
  404. // First some basic setups
  405. if (!isset($weatherData["condition"])) {
  406. $weatherData["condition"] = "";
  407. } elseif (strlen($weatherData["condition"]) > 0) {
  408. $weatherData["condition"] .= ",";
  409. }
  410. if (in_array(strtolower($result[0]), $conditions)) {
  411. // First try matching the complete string
  412. $weatherData["condition"] .= " ".$conditions[strtolower($result[0])];
  413. } else {
  414. // No luck, match part by part
  415. for ($c = 1; $c < sizeof($result); $c++) {
  416. if (strlen($result[$c]) > 0) {
  417. $weatherData["condition"] .= " ".$conditions[strtolower($result[$c])];
  418. }
  419. }
  420. }
  421. $weatherData["condition"] = trim($weatherData["condition"]);
  422. break;
  423. case "clouds":
  424. if (!isset($weatherData["clouds"])) {
  425. $weatherData["clouds"] = array();
  426. }
  427. if (sizeof($result) == 5) {
  428. // Only amount and height
  429. $cloud = array("amount" => $clouds[strtolower($result[3])], "height" => ($result[4]*100));
  430. }
  431. elseif (sizeof($result) == 6) {
  432. // Amount, height and type
  433. $cloud = array("amount" => $clouds[strtolower($result[3])], "height" => ($result[4]*100), "type" => $clouds[strtolower($result[5])]);
  434. }
  435. else {
  436. // SKC or CLR
  437. $cloud = array("amount" => $clouds[strtolower($result[0])]);
  438. }
  439. $weatherData["clouds"][] = $cloud;
  440. break;
  441. case "temperature":
  442. // normal temperature in first part
  443. // negative value
  444. if ($result[1] == "M") {
  445. $result[2] *= -1;
  446. }
  447. $weatherData["temperature"] = $this->convertTemperature($result[2], "c", "f");
  448. if (sizeof($result) > 4) {
  449. // same for dewpoint
  450. if ($result[4] == "M") {
  451. $result[5] *= -1;
  452. }
  453. $weatherData["dewPoint"] = $this->convertTemperature($result[5], "c", "f");
  454. $weatherData["humidity"] = $this->calculateHumidity($result[2], $result[5]);
  455. }
  456. if (isset($weatherData["wind"])) {
  457. // Now calculate windchill from temperature and windspeed
  458. $weatherData["feltTemperature"] = $this->calculateWindChill($weatherData["temperature"], $weatherData["wind"]);
  459. }
  460. unset($metarCode["temperature"]);
  461. break;
  462. case "pressure":
  463. if ($result[1] == "A") {
  464. // Pressure provided in inches
  465. $weatherData["pressure"] = $result[2] / 100;
  466. } elseif ($result[3] == "Q") {
  467. // ... in hectopascal
  468. $weatherData["pressure"] = $this->convertPressure($result[4], "hpa", "in");
  469. }
  470. unset($metarCode["pressure"]);
  471. break;
  472. case "nosig":
  473. case "nospeci":
  474. // No change during the last hour
  475. if (!isset($weatherData["remark"])) {
  476. $weatherData["remark"] = array();
  477. }
  478. $weatherData["remark"]["nosig"] = "No changes in weather conditions";
  479. unset($metarCode[$key]);
  480. break;
  481. case "remark":
  482. // Remark part begins
  483. $metarCode = $remarks;
  484. if (!isset($weatherData["remark"])) {
  485. $weatherData["remark"] = array();
  486. }
  487. break;
  488. case "autostation":
  489. // Which autostation do we have here?
  490. if ($result[1] == 0) {
  491. $weatherData["remark"]["autostation"] = "Automatic weatherstation w/o precipitation discriminator";
  492. } else {
  493. $weatherData["remark"]["autostation"] = "Automatic weatherstation w/ precipitation discriminator";
  494. }
  495. unset($metarCode["autostation"]);
  496. break;
  497. case "presschg":
  498. // Decoding for rapid pressure changes
  499. if (strtolower($result[1]) == "r") {
  500. $weatherData["remark"]["presschg"] = "Pressure rising rapidly";
  501. } else {
  502. $weatherData["remark"]["presschg"] = "Pressure falling rapidly";
  503. }
  504. unset($metarCode["presschg"]);
  505. break;
  506. case "seapressure":
  507. // Pressure at sea level (delivered in hpa)
  508. // Decoding is a bit obscure as 982 gets 998.2
  509. // whereas 113 becomes 1113 -> no real rule here
  510. if (strtolower($result[1]) != "no") {
  511. if ($result[1] > 500) {
  512. $press = 900 + round($result[1] / 100, 1);
  513. } else {
  514. $press = 1000 + $result[1];
  515. }
  516. $weatherData["remark"]["seapressure"] = $this->convertPressure($press, "hpa", "in");
  517. }
  518. unset($metarCode["seapressure"]);
  519. break;
  520. case "1hprecip":
  521. // Precipitation for the last hour in inches
  522. if (!isset($weatherData["precipitation"])) {
  523. $weatherData["precipitation"] = array();
  524. }
  525. if (!is_numeric($result[1])) {
  526. $precip = "indeterminable";
  527. } elseif ($result[1] == "0000") {
  528. $precip = "traceable";
  529. }else {
  530. $precip = $result[1] / 100;
  531. }
  532. $weatherData["precipitation"][] = array(
  533. "amount" => $precip,
  534. "hours" => "1"
  535. );
  536. unset($metarCode["1hprecip"]);
  537. break;
  538. case "6hprecip":
  539. // Same for last 3 resp. 6 hours... no way to determine
  540. // which report this is, so keeping the text general
  541. if (!isset($weatherData["precipitation"])) {
  542. $weatherData["precipitation"] = array();
  543. }
  544. if (!is_numeric($result[1])) {
  545. $precip = "indeterminable";
  546. } elseif ($result[1] == "0000") {
  547. $precip = "traceable";
  548. }else {
  549. $precip = $result[1] / 100;
  550. }
  551. $weatherData["precipitation"][] = array(
  552. "amount" => $precip,
  553. "hours" => "3/6"
  554. );
  555. unset($metarCode["6hprecip"]);
  556. break;
  557. case "24hprecip":
  558. // And the same for the last 24 hours
  559. if (!isset($weatherData["precipitation"])) {
  560. $weatherData["precipitation"] = array();
  561. }
  562. if (!is_numeric($result[1])) {
  563. $precip = "indeterminable";
  564. } elseif ($result[1] == "0000") {
  565. $precip = "traceable";
  566. }else {
  567. $precip = $result[1] / 100;
  568. }
  569. $weatherData["precipitation"][] = array(
  570. "amount" => $precip,
  571. "hours" => "24"
  572. );
  573. unset($metarCode["24hprecip"]);
  574. break;
  575. case "snowdepth":
  576. // Snow depth in inches
  577. $weatherData["remark"]["snowdepth"] = $result[1];
  578. unset($metarCode["snowdepth"]);
  579. break;
  580. case "snowequiv":
  581. // Same for equivalent in Water... (inches)
  582. $weatherData["remark"]["snowequiv"] = $result[1] / 10;
  583. unset($metarCode["snowequiv"]);
  584. break;
  585. case "cloudtypes":
  586. // Cloud types, haven't found a way for decent decoding (yet)
  587. unset($metarCode["cloudtypes"]);
  588. break;
  589. case "sunduration":
  590. // Duration of sunshine (in minutes)
  591. $weatherData["remark"]["sunduration"] = "Total minutes of sunshine: ".$result[1];
  592. unset($metarCode["sunduration"]);
  593. break;
  594. case "1htempdew":
  595. // Temperatures in the last hour in C
  596. if ($result[1] == "1") {
  597. $result[2] *= -1;
  598. }
  599. $weatherData["remark"]["1htemp"] = $this->convertTemperature($result[2] / 10, "c", "f");
  600. if (sizeof($result) > 3) {
  601. // same for dewpoint
  602. if ($result[4] == "1") {
  603. $result[5] *= -1;
  604. }
  605. $weatherData["remark"]["1hdew"] = $this->convertTemperature($result[5] / 10, "c", "f");
  606. }
  607. unset($metarCode["1htempdew"]);
  608. break;
  609. case "6hmaxtemp":
  610. // Max temperature in the last 6 hours in C
  611. if ($result[1] == "1") {
  612. $result[2] *= -1;
  613. }
  614. $weatherData["remark"]["6hmaxtemp"] = $this->convertTemperature($result[2] / 10, "c", "f");
  615. unset($metarCode["6hmaxtemp"]);
  616. break;
  617. case "6hmintemp":
  618. // Min temperature in the last 6 hours in C
  619. if ($result[1] == "1") {
  620. $result[2] *= -1;
  621. }
  622. $weatherData["remark"]["6hmintemp"] = $this->convertTemperature($result[2] / 10, "c", "f");
  623. unset($metarCode["6hmintemp"]);
  624. break;
  625. case "24htemp":
  626. // Max/Min temperatures in the last 24 hours in C
  627. if ($result[1] == "1") {
  628. $result[2] *= -1;
  629. }
  630. $weatherData["remark"]["24hmaxtemp"] = $this->convertTemperature($result[2] / 10, "c", "f");
  631. if ($result[3] == "1") {
  632. $result[4] *= -1;
  633. }
  634. $weatherData["remark"]["24hmintemp"] = $this->convertTemperature($result[4] / 10, "c", "f");
  635. unset($metarCode["24htemp"]);
  636. break;
  637. case "3hpresstend":
  638. // We don't save the pressure during the day, so no decoding
  639. // possible, sorry
  640. unset($metarCode["3hpresstend"]);
  641. break;
  642. case "sensors":
  643. // We may have multiple broken sensors, so do not unset
  644. if (!isset($weatherData["remark"]["sensors"])) {
  645. $weatherData["remark"]["sensors"] = array();
  646. }
  647. $weatherData["remark"]["sensors"][strtolower($result[0])] = $sensors[strtolower($result[0])];
  648. break;
  649. case "maintain":
  650. $weatherData["remark"]["maintain"] = "Maintainance needed";
  651. unset($metarCode["maintain"]);
  652. break;
  653. default:
  654. // Do nothing, just prevent further matching
  655. unset($metarCode[$key]);
  656. break;
  657. }
  658. if (SERVICES_WEATHER_DEBUG) {
  659. echo $key."\n";
  660. }
  661. break;
  662. }
  663. }
  664. if (!$found) {
  665. if (SERVICES_WEATHER_DEBUG) {
  666. echo "n/a\n";
  667. }
  668. if (!isset($weatherData["noparse"])) {
  669. $weatherData["noparse"] = array();
  670. }
  671. $weatherData["noparse"][] = $metar[$i];
  672. }
  673. }
  674. }
  675. if (isset($weatherData["noparse"])) {
  676. $weatherData["noparse"] = implode(" ", $weatherData["noparse"]);
  677. }
  678. return $weatherData;
  679. }
  680. // }}}
  681. // {{{ searchLocation()
  682. /**
  683. * Searches IDs for given location, returns array of possible locations or single ID
  684. *
  685. * @param string|array $location
  686. * @param bool $useFirst If set, first ID of result-array is returned
  687. * @return PEAR_Error|array|string
  688. * @throws PEAR_Error::SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION
  689. * @throws PEAR_Error::SERVICES_WEATHER_ERROR_DB_NOT_CONNECTED
  690. * @throws PEAR_Error::SERVICES_WEATHER_ERROR_INVALID_LOCATION
  691. * @access public
  692. */
  693. function searchLocation($location, $useFirst = false)
  694. {
  695. if (!isset($this->_db) || !DB::isConnection($this->_db)) {
  696. return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_DB_NOT_CONNECTED);
  697. }
  698. if (is_string($location)) {
  699. // Try to part search string in name, state and country part
  700. // and build where clause from it for the select
  701. $location = explode(",", $location);
  702. if (sizeof($location) >= 1) {
  703. $where = "LOWER(name) LIKE '%".strtolower(trim($location[0]))."%'";
  704. }
  705. if (sizeof($location) == 2) {
  706. $where .= " AND LOWER(country) LIKE '%".strtolower(trim($location[1]))."%'";
  707. } elseif (sizeof($location) == 3) {
  708. $where .= " AND LOWER(state) LIKE '%".strtolower(trim($location[1]))."%'";
  709. $where .= " AND LOWER(country) LIKE '%".strtolower(trim($location[2]))."%'";
  710. }
  711. // Create select, locations with ICAO first
  712. $select = "SELECT icao, name, state, country, latitude, longitude ".
  713. "FROM metarLocations ".
  714. "WHERE ".$where." ".
  715. "ORDER BY icao DESC";
  716. $result = $this->_db->query($select);
  717. // Check result for validity
  718. if (DB::isError($result)) {
  719. return $result;
  720. } elseif (get_class($result) != "db_result" || $result->numRows() == 0) {
  721. return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION);
  722. }
  723. // Result is valid, start preparing the return
  724. $icao = array();
  725. while (($row = $result->fetchRow(DB_FETCHMODE_ASSOC)) != null) {
  726. $locicao = $row["icao"];
  727. // First the name of the location
  728. if (!strlen($row["state"])) {
  729. $locname = $row["name"].", ".$row["country"];
  730. } else {
  731. $locname = $row["name"].", ".$row["state"].", ".$row["country"];
  732. }
  733. if ($locicao != "----") {
  734. // We have a location with ICAO
  735. $icao[$locicao] = $locname;
  736. } else {
  737. // No ICAO, try finding the nearest airport
  738. $locicao = $this->searchAirport($row["latitude"], $row["longitude"]);
  739. if (!isset($icao[$locicao])) {
  740. $icao[$locicao] = $locname;
  741. }
  742. }
  743. }
  744. // Only one result? Return as string
  745. if (sizeof($icao) == 1) {
  746. $icao = key($icao);
  747. }
  748. } elseif (is_array($location)) {
  749. // Location was provided as coordinates, search nearest airport
  750. $icao = $this->searchAirport($location[0], $location[1]);
  751. } else {
  752. return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_INVALID_LOCATION);
  753. }
  754. return $icao;
  755. }
  756. // }}}
  757. // {{{ searchLocationByCountry()
  758. /**
  759. * Returns IDs with location-name for a given country or all available countries, if no value was given
  760. *
  761. * @param string $country
  762. * @return PEAR_Error|array
  763. * @throws PEAR_Error::SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION
  764. * @throws PEAR_Error::SERVICES_WEATHER_ERROR_DB_NOT_CONNECTED
  765. * @throws PEAR_Error::SERVICES_WEATHER_ERROR_WRONG_SERVER_DATA
  766. * @access public
  767. */
  768. function searchLocationByCountry($country = "")
  769. {
  770. if (!isset($this->_db) || !DB::isConnection($this->_db)) {
  771. return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_DB_NOT_CONNECTED);
  772. }
  773. // Return the available countries as no country was given
  774. if (!strlen($country)) {
  775. $select = "SELECT DISTINCT(country) ".
  776. "FROM metarAirports ".
  777. "ORDER BY country ASC";
  778. $countries = $this->_db->getCol($select);
  779. // As $countries is either an error or the true result,
  780. // we can just return it
  781. return $countries;
  782. }
  783. // Now for the real search
  784. $select = "SELECT icao, name, state, country ".
  785. "FROM metarAirports ".
  786. "WHERE LOWER(country) LIKE '%".strtolower(trim($country))."%' ".
  787. "ORDER BY name ASC";
  788. $result = $this->_db->query($select);
  789. // Check result for validity
  790. if (DB::isError($result)) {
  791. return $result;
  792. } elseif (get_class($result) != "db_result" || $result->numRows() == 0) {
  793. return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION);
  794. }
  795. // Construct the result
  796. $locations = array();
  797. while (($row = $result->fetchRow(DB_FETCHMODE_ASSOC)) != null) {
  798. $locicao = $row["icao"];
  799. // First the name of the location
  800. if (!strlen($row["state"])) {
  801. $locname = $row["name"].", ".$row["country"];
  802. } else {
  803. $locname = $row["name"].", ".$row["state"].", ".$row["country"];
  804. }
  805. $locations[$locicao] = $locname;
  806. }
  807. return $locations;
  808. }
  809. // }}}
  810. // {{{ searchAirport()
  811. /**
  812. * Searches the nearest airport(s) for given coordinates, returns array of IDs or single ID
  813. *
  814. * @param float $latitude
  815. * @param float $longitude
  816. * @param int $numResults
  817. * @return PEAR_Error|array|string
  818. * @throws PEAR_Error::SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION
  819. * @throws PEAR_Error::SERVICES_WEATHER_ERROR_DB_NOT_CONNECTED
  820. * @throws PEAR_Error::SERVICES_WEATHER_ERROR_INVALID_LOCATION
  821. * @access public
  822. */
  823. function searchAirport($latitude, $longitude, $numResults = 1)
  824. {
  825. if (!isset($this->_db) || !DB::isConnection($this->_db)) {
  826. return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_DB_NOT_CONNECTED);
  827. }
  828. if (!is_numeric($latitude) || !is_numeric($longitude)) {
  829. return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_INVALID_LOCATION);
  830. }
  831. // Get all airports
  832. $select = "SELECT icao, x, y, z FROM metarAirports";
  833. $result = $this->_db->query($select);
  834. if (DB::isError($result)) {
  835. return $result;
  836. } elseif (get_class($result) != "db_result" || $result->numRows() == 0) {
  837. return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION);
  838. }
  839. // Result is valid, start search
  840. // Initialize values
  841. $min_dist = null;
  842. $query = $this->polar2cartesian($latitude, $longitude);
  843. $search = array("dist" => array(), "icao" => array());
  844. while (($row = $result->fetchRow(DB_FETCHMODE_ASSOC)) != null) {
  845. $icao = $row["icao"];
  846. $air = array($row["x"], $row["y"], $row["z"]);
  847. $dist = 0;
  848. $d = 0;
  849. // Calculate distance of query and current airport
  850. // break off, if distance is larger than current $min_dist
  851. for($d; $d < sizeof($air); $d++) {
  852. $t = $air[$d] - $query[$d];
  853. $dist += pow($t, 2);
  854. if ($min_dist != null && $dist > $min_dist) {
  855. break;
  856. }
  857. }
  858. if ($d >= sizeof($air)) {
  859. // Ok, current airport is one of the nearer locations
  860. // add to result-array
  861. $search["dist"][] = $dist;
  862. $search["icao"][] = $icao;
  863. // Sort array for distance
  864. array_multisort($search["dist"], SORT_NUMERIC, SORT_ASC, $search["icao"], SORT_STRING, SORT_ASC);
  865. // If array is larger then desired results, chop off last one
  866. if (sizeof($search["dist"]) > $numResults) {
  867. array_pop($search["dist"]);
  868. array_pop($search["icao"]);
  869. }
  870. $min_dist = max($search["dist"]);
  871. }
  872. }
  873. if ($numResults == 1) {
  874. // Only one result wanted, return as string
  875. return $search["icao"][0];
  876. } elseif ($numResults > 1) {
  877. // Return found locations
  878. return $search["icao"];
  879. } else {
  880. return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION);
  881. }
  882. }
  883. // }}}
  884. // {{{ getUnits()
  885. /**
  886. * Returns the units for the current query
  887. *
  888. * @param string $id
  889. * @param string $unitsFormat
  890. * @return array
  891. * @deprecated
  892. * @access public
  893. */
  894. function getUnits($id = null, $unitsFormat = "")
  895. {
  896. return $this->getUnitsFormat($unitsFormat);
  897. }
  898. // }}}
  899. // {{{ getLocation()
  900. /**
  901. * Returns the data for the location belonging to the ID
  902. *
  903. * @param string $id
  904. * @return PEAR_Error|array
  905. * @throws PEAR_Error
  906. * @access public
  907. */
  908. function getLocation($id = "")
  909. {
  910. $status = $this->_checkLocationID($id);
  911. if (Services_Weather::isError($status)) {
  912. return $status;
  913. }
  914. $locationReturn = array();
  915. if ($this->_cacheEnabled && ($location = $this->_cache->get("METAR-".$id, "location"))) {
  916. // Grab stuff from cache
  917. $this->_location = $location;
  918. $locationReturn["cache"] = "HIT";
  919. } elseif (isset($this->_db) && DB::isConnection($this->_db)) {
  920. // Get data from DB
  921. $select = "SELECT icao, name, state, country, latitude, longitude, elevation ".
  922. "FROM metarAirports WHERE icao='".$id."'";
  923. $result = $this->_db->query($select);
  924. if (DB::isError($result)) {
  925. return $result;
  926. } elseif (get_class($result) != "db_result" || $result->numRows() == 0) {
  927. return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION);
  928. }
  929. // Result is ok, put things into object
  930. $this->_location = $result->fetchRow(DB_FETCHMODE_ASSOC);
  931. if ($this->_cacheEnabled) {
  932. // ...and cache it
  933. $expire = constant("SERVICES_WEATHER_EXPIRES_LOCATION");
  934. $this->_cache->extSave("METAR-".$id, $this->_location, "", $expire, "location");
  935. }
  936. $locationReturn["cache"] = "MISS";
  937. } else {
  938. $this->_location = array(
  939. "name" => $id,
  940. "state" => "",
  941. "country" => "",
  942. "latitude" => "",
  943. "longitude" => "",
  944. "elevation" => ""
  945. );
  946. }
  947. // Stuff name-string together
  948. if (strlen($this->_location["state"]) && strlen($this->_location["country"])) {
  949. $locname = $this->_location["name"].", ".$this->_location["state"].", ".$this->_location["country"];
  950. } elseif (strlen($this->_location["country"])) {
  951. $locname = $this->_location["name"].", ".$this->_location["country"];
  952. } else {
  953. $locname = $this->_location["name"];
  954. }
  955. $locationReturn["name"] = $locname;
  956. $locationReturn["latitude"] = $this->_location["latitude"];
  957. $locationReturn["longitude"] = $this->_location["longitude"];
  958. $locationReturn["elevation"] = $this->_location["elevation"];
  959. return $locationReturn;
  960. }
  961. // }}}
  962. // {{{ getWeather()
  963. /**
  964. * Returns the weather-data for the supplied location
  965. *
  966. * @param string $id
  967. * @param string $unitsFormat
  968. * @return PHP_Error|array
  969. * @throws PHP_Error
  970. * @access public
  971. */
  972. function getWeather($id = "", $unitsFormat = "")
  973. {
  974. $id = strtoupper($id);
  975. $status = $this->_checkLocationID($id);
  976. if (Services_Weather::isError($status)) {
  977. return $status;
  978. }
  979. // Get other data
  980. $units = $this->getUnitsFormat($unitsFormat);
  981. $location = $this->getLocation($id);
  982. if ($this->_cacheEnabled && ($weather = $this->_cache->get("METAR-".$id, "weather"))) {
  983. // Wee... it was cached, let's have it...
  984. $weatherReturn = $weather;
  985. $this->_weather = $weatherReturn;
  986. $weatherReturn["cache"] = "HIT";
  987. } else {
  988. // Set the source
  989. if ($this->_source == "file") {
  990. $source = realpath($this->_sourcePath.$id.".TXT");
  991. } else {
  992. $source = $this->_sourcePath.$id.".TXT";
  993. }
  994. // Download and parse weather
  995. $weatherReturn = $this->_parseWeatherData($source, $units);
  996. if (Services_Weather::isError($weatherReturn)) {
  997. return $weatherReturn;
  998. }
  999. if ($this->_cacheEnabled) {
  1000. // Cache weather
  1001. $expire = constant("SERVICES_WEATHER_EXPIRES_WEATHER");
  1002. $this->_cache->extSave("METAR-".$id, $weatherReturn, $unitsFormat, $expire, "weather");
  1003. }
  1004. $this->_weather = $weatherReturn;
  1005. $weatherReturn["cache"] = "MISS";
  1006. }
  1007. if (isset($weatherReturn["remark"])) {
  1008. foreach ($weatherReturn["remark"] as $key => $val) {
  1009. switch ($key) {
  1010. case "seapressure":
  1011. $newVal = $this->convertPressure($val, "in", $units["pres"]);
  1012. break;
  1013. case "snowdepth":
  1014. case "snowequiv":
  1015. $newVal = $this->convertPressure($val, "in", $units["rain"]);
  1016. break;
  1017. case "1htemp":
  1018. case "1hdew":
  1019. case "6hmaxtemp":
  1020. case "6hmintemp":
  1021. case "24hmaxtemp":
  1022. case "24hmintemp":
  1023. $newVal = $this->convertTemperature($val, "f", $units["temp"]);
  1024. break;
  1025. default:
  1026. continue 2;
  1027. break;
  1028. }
  1029. $weatherReturn["remark"][$key] = $newVal;
  1030. }
  1031. }
  1032. foreach ($weatherReturn as $key => $val) {
  1033. switch ($key) {
  1034. case "station":
  1035. $newVal = $location["name"];
  1036. break;
  1037. case "update":
  1038. $newVal = gmdate(trim($this->_dateFormat." ".$this->_timeFormat), $val);
  1039. break;
  1040. case "wind":
  1041. case "windGust":
  1042. $newVal = $this->convertSpeed($val, "mph", $units["wind"]);
  1043. break;
  1044. case "visibility":
  1045. $newVal = $this->convertDistance($val, "sm", $units["vis"]);
  1046. break;
  1047. case "temperature":
  1048. case "dewPoint":
  1049. case "feltTemperature":
  1050. $newVal = $this->convertTemperature($val, "f", $units["temp"]);
  1051. break;
  1052. case "pressure":
  1053. $newVal = $this->convertPressure($val, "in", $units["pres"]);
  1054. break;
  1055. case "precipitation":
  1056. $newVal = array();
  1057. for ($p = 0; $p < sizeof($val); $p++) {
  1058. $newVal[$p] = array();
  1059. if (is_numeric($val[$p]["amount"])) {
  1060. $newVal[$p]["amount"] = $this->convertPressure($val[$p]["amount"], "in", $units["rain"]);
  1061. } else {
  1062. $newVal[$p]["amount"] = $val[$p]["amount"];
  1063. }
  1064. $newVal[$p]["hours"] = $val[$p]["hours"];
  1065. }
  1066. break;
  1067. /*
  1068. case "remark":
  1069. $newVal = implode(", ", $val);
  1070. break;
  1071. */
  1072. default:
  1073. continue 2;
  1074. break;
  1075. }
  1076. $weatherReturn[$key] = $newVal;
  1077. }
  1078. return $weatherReturn;
  1079. }
  1080. // }}}
  1081. // {{{ getForecast()
  1082. /**
  1083. * METAR has no forecast per se, so this function is just for
  1084. * compatibility purposes.
  1085. *
  1086. * @param string $int
  1087. * @param int $days
  1088. * @param string $unitsFormat
  1089. * @return bool
  1090. * @access public
  1091. * @deprecated
  1092. */
  1093. function getForecast($id = null, $days = null, $unitsFormat = null)
  1094. {
  1095. return false;
  1096. }
  1097. // }}}
  1098. }
  1099. // }}}
  1100. ?>