PageRenderTime 61ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/metar/Metar.py

http://github.com/timetric/python-metar
Python | 1186 lines | 1153 code | 4 blank | 29 comment | 3 complexity | 582503c3faa116f937356fd0a788024f MD5 | raw file
  1. #!/usr/bin/env python
  2. #
  3. # A python package for interpreting METAR and SPECI weather reports.
  4. #
  5. # US conventions for METAR/SPECI reports are described in chapter 12 of
  6. # the Federal Meteorological Handbook No.1. (FMH-1 1995), issued by NOAA.
  7. # See <http://metar.noaa.gov/>
  8. #
  9. # International conventions for the METAR and SPECI codes are specified in
  10. # the WMO Manual on Codes, vol I.1, Part A (WMO-306 I.i.A).
  11. #
  12. # This module handles a reports that follow the US conventions, as well
  13. # the more general encodings in the WMO spec. Other regional conventions
  14. # are not supported at present.
  15. #
  16. # The current METAR report for a given station is available at the URL
  17. # http://weather.noaa.gov/pub/data/observations/metar/stations/<station>.TXT
  18. # where <station> is the four-letter ICAO station code.
  19. #
  20. # The METAR reports for all reporting stations for any "cycle" (i.e., hour)
  21. # in the last 24 hours is available in a single file at the URL
  22. # http://weather.noaa.gov/pub/data/observations/metar/cycles/<cycle>Z.TXT
  23. # where <cycle> is a 2-digit cycle number (e.g., "00", "05" or "23").
  24. #
  25. # Copyright 2004 Tom Pollard
  26. #
  27. """
  28. This module defines the Metar class. A Metar object represents the weather report encoded by a single METAR code.
  29. """
  30. __author__ = "Tom Pollard"
  31. __email__ = "pollard@alum.mit.edu"
  32. __version__ = "1.2"
  33. __LICENSE__ = """
  34. Copyright (c) 2004, %s
  35. All rights reserved.
  36. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
  37. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
  38. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  39. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  40. """ % __author__
  41. import re
  42. import datetime
  43. import string
  44. from Datatypes import *
  45. ## Exceptions
  46. class ParserError(Exception):
  47. """Exception raised when an unparseable group is found in body of the report."""
  48. pass
  49. ## regular expressions to decode various groups of the METAR code
  50. MISSING_RE = re.compile(r"^[M/]+$")
  51. TYPE_RE = re.compile(r"^(?P<type>METAR|SPECI)\s+")
  52. STATION_RE = re.compile(r"^(?P<station>[A-Z][A-Z0-9]{3})\s+")
  53. TIME_RE = re.compile(r"""^(?P<day>\d\d)
  54. (?P<hour>\d\d)
  55. (?P<min>\d\d)Z?\s+""",
  56. re.VERBOSE)
  57. MODIFIER_RE = re.compile(r"^(?P<mod>AUTO|FINO|NIL|TEST|CORR?|RTD|CC[A-G])\s+")
  58. WIND_RE = re.compile(r"""^(?P<dir>[\dO]{3}|[0O]|///|MMM|VRB)
  59. (?P<speed>P?[\dO]{2,3}|[0O]+|[/M]{2,3})
  60. (G(?P<gust>P?(\d{1,3}|[/M]{1,3})))?
  61. (?P<units>KTS?|LT|K|T|KMH|MPS)?
  62. (\s+(?P<varfrom>\d\d\d)V
  63. (?P<varto>\d\d\d))?\s+""",
  64. re.VERBOSE)
  65. # VISIBILITY_RE = re.compile(r"""^(?P<vis>(?P<dist>M?(\d\s+)?\d/\d\d?|M?\d+)
  66. # ( \s*(?P<units>SM|KM|M|U) | NDV |
  67. # (?P<dir>[NSEW][EW]?) )? |
  68. # CAVOK )\s+""",
  69. # re.VERBOSE)
  70. # start patch
  71. VISIBILITY_RE = re.compile(r"""^(?P<vis>(?P<dist>(M|P)?\d\d\d\d|////)
  72. (?P<dir>[NSEW][EW]? | NDV)? |
  73. (?P<distu>(M|P)?(\d+|\d\d?/\d\d?|\d+\s+\d/\d))
  74. (?P<units>SM|KM|M|U) |
  75. CAVOK )\s+""",
  76. re.VERBOSE)
  77. # end patch
  78. RUNWAY_RE = re.compile(r"""^(RVRNO |
  79. R(?P<name>\d\d(RR?|LL?|C)?)/
  80. (?P<low>(M|P)?\d\d\d\d)
  81. (V(?P<high>(M|P)?\d\d\d\d))?
  82. (?P<unit>FT)?[/NDU]*)\s+""",
  83. re.VERBOSE)
  84. WEATHER_RE = re.compile(r"""^(?P<int>(-|\+|VC)*)
  85. (?P<desc>(MI|PR|BC|DR|BL|SH|TS|FZ)+)?
  86. (?P<prec>(DZ|RA|SN|SG|IC|PL|GR|GS|UP|/)*)
  87. (?P<obsc>BR|FG|FU|VA|DU|SA|HZ|PY)?
  88. (?P<other>PO|SQ|FC|SS|DS|NSW|/+)?
  89. (?P<int2>[-+])?\s+""",
  90. re.VERBOSE)
  91. SKY_RE= re.compile(r"""^(?P<cover>VV|CLR|SKC|SCK|NSC|NCD|BKN|SCT|FEW|[O0]VC|///)
  92. (?P<height>[\dO]{2,4}|///)?
  93. (?P<cloud>([A-Z][A-Z]+|///))?\s+""",
  94. re.VERBOSE)
  95. TEMP_RE = re.compile(r"""^(?P<temp>(M|-)?\d+|//|XX|MM)/
  96. (?P<dewpt>(M|-)?\d+|//|XX|MM)?\s+""",
  97. re.VERBOSE)
  98. PRESS_RE = re.compile(r"""^(?P<unit>A|Q|QNH|SLP)?
  99. (?P<press>[\dO]{3,4}|////)
  100. (?P<unit2>INS)?\s+""",
  101. re.VERBOSE)
  102. RECENT_RE = re.compile(r"""^RE(?P<desc>MI|PR|BC|DR|BL|SH|TS|FZ)?
  103. (?P<prec>(DZ|RA|SN|SG|IC|PL|GR|GS|UP)*)?
  104. (?P<obsc>BR|FG|FU|VA|DU|SA|HZ|PY)?
  105. (?P<other>PO|SQ|FC|SS|DS)?\s+""",
  106. re.VERBOSE)
  107. WINDSHEAR_RE = re.compile(r"^(WS\s+)?(ALL\s+RWY|RWY(?P<name>\d\d(RR?|L?|C)?))\s+")
  108. COLOR_RE = re.compile(r"""^(BLACK)?(BLU|GRN|WHT|RED)\+?
  109. (/?(BLACK)?(BLU|GRN|WHT|RED)\+?)*\s*""",
  110. re.VERBOSE)
  111. RUNWAYSTATE_RE = re.compile(r"""((?P<name>\d\d) | R(?P<namenew>\d\d)(RR?|LL?|C)?/?)
  112. ((?P<special> SNOCLO|CLRD(\d\d|//)) |
  113. (?P<deposit>(\d|/))
  114. (?P<extent>(\d|/))
  115. (?P<depth>(\d\d|//))
  116. (?P<friction>(\d\d|//)))\s+""",
  117. re.VERBOSE)
  118. TREND_RE = re.compile(r"^(?P<trend>TEMPO|BECMG|FCST|NOSIG)\s+")
  119. TRENDTIME_RE = re.compile(r"(?P<when>(FM|TL|AT))(?P<hour>\d\d)(?P<min>\d\d)\s+")
  120. REMARK_RE = re.compile(r"^(RMKS?|NOSPECI|NOSIG)\s+")
  121. ## regular expressions for remark groups
  122. AUTO_RE = re.compile(r"^AO(?P<type>\d)\s+")
  123. SEALVL_PRESS_RE = re.compile(r"^SLP(?P<press>\d\d\d)\s+")
  124. PEAK_WIND_RE = re.compile(r"""^P[A-Z]\s+WND\s+
  125. (?P<dir>\d\d\d)
  126. (?P<speed>P?\d\d\d?)/
  127. (?P<hour>\d\d)?
  128. (?P<min>\d\d)\s+""",
  129. re.VERBOSE)
  130. WIND_SHIFT_RE = re.compile(r"""^WSHFT\s+
  131. (?P<hour>\d\d)?
  132. (?P<min>\d\d)
  133. (\s+(?P<front>FROPA))?\s+""",
  134. re.VERBOSE)
  135. PRECIP_1HR_RE = re.compile(r"^P(?P<precip>\d\d\d\d)\s+")
  136. PRECIP_24HR_RE = re.compile(r"""^(?P<type>6|7)
  137. (?P<precip>\d\d\d\d)\s+""",
  138. re.VERBOSE)
  139. PRESS_3HR_RE = re.compile(r"""^5(?P<tend>[0-8])
  140. (?P<press>\d\d\d)\s+""",
  141. re.VERBOSE)
  142. TEMP_1HR_RE = re.compile(r"""^T(?P<tsign>0|1)
  143. (?P<temp>\d\d\d)
  144. ((?P<dsign>0|1)
  145. (?P<dewpt>\d\d\d))?\s+""",
  146. re.VERBOSE)
  147. TEMP_6HR_RE = re.compile(r"""^(?P<type>1|2)
  148. (?P<sign>0|1)
  149. (?P<temp>\d\d\d)\s+""",
  150. re.VERBOSE)
  151. TEMP_24HR_RE = re.compile(r"""^4(?P<smaxt>0|1)
  152. (?P<maxt>\d\d\d)
  153. (?P<smint>0|1)
  154. (?P<mint>\d\d\d)\s+""",
  155. re.VERBOSE)
  156. UNPARSED_RE = re.compile(r"(?P<group>\S+)\s+")
  157. LIGHTNING_RE = re.compile(r"""^((?P<freq>OCNL|FRQ|CONS)\s+)?
  158. LTG(?P<type>(IC|CC|CG|CA)*)
  159. ( \s+(?P<loc>( OHD | VC | DSNT\s+ | \s+AND\s+ |
  160. [NSEW][EW]? (-[NSEW][EW]?)* )+) )?\s+""",
  161. re.VERBOSE)
  162. TS_LOC_RE = re.compile(r"""TS(\s+(?P<loc>( OHD | VC | DSNT\s+ | \s+AND\s+ |
  163. [NSEW][EW]? (-[NSEW][EW]?)* )+))?
  164. ( \s+MOV\s+(?P<dir>[NSEW][EW]?) )?\s+""",
  165. re.VERBOSE)
  166. ## translation of weather location codes
  167. loc_terms = [ ("OHD", "overhead"),
  168. ("DSNT", "distant"),
  169. ("AND", "and"),
  170. ("VC", "nearby") ]
  171. def xlate_loc( loc ):
  172. """Substitute English terms for the location codes in the given string."""
  173. for code, english in loc_terms:
  174. loc = loc.replace(code,english)
  175. return loc
  176. ## translation of the sky-condition codes into english
  177. SKY_COVER = { "SKC":"clear",
  178. "CLR":"clear",
  179. "NSC":"clear",
  180. "NCD":"clear",
  181. "FEW":"a few ",
  182. "SCT":"scattered ",
  183. "BKN":"broken ",
  184. "OVC":"overcast",
  185. "///":"",
  186. "VV":"indefinite ceiling" }
  187. CLOUD_TYPE = { "TCU":"towering cumulus",
  188. "CU":"cumulus",
  189. "CB":"cumulonimbus",
  190. "SC":"stratocumulus",
  191. "CBMAM":"cumulonimbus mammatus",
  192. "ACC":"altocumulus castellanus",
  193. "SCSL":"standing lenticular stratocumulus",
  194. "CCSL":"standing lenticular cirrocumulus",
  195. "ACSL":"standing lenticular altocumulus" }
  196. ## translation of the present-weather codes into english
  197. WEATHER_INT = { "-":"light",
  198. "+":"heavy",
  199. "-VC":"nearby light",
  200. "+VC":"nearby heavy",
  201. "VC":"nearby" }
  202. WEATHER_DESC = { "MI":"shallow",
  203. "PR":"partial",
  204. "BC":"patches of",
  205. "DR":"low drifting",
  206. "BL":"blowing",
  207. "SH":"showers",
  208. "TS":"thunderstorm",
  209. "FZ":"freezing" }
  210. WEATHER_PREC = { "DZ":"drizzle",
  211. "RA":"rain",
  212. "SN":"snow",
  213. "SG":"snow grains",
  214. "IC":"ice crystals",
  215. "PL":"ice pellets",
  216. "GR":"hail",
  217. "GS":"snow pellets",
  218. "UP":"unknown precipitation",
  219. "//":"" }
  220. WEATHER_OBSC = { "BR":"mist",
  221. "FG":"fog",
  222. "FU":"smoke",
  223. "VA":"volcanic ash",
  224. "DU":"dust",
  225. "SA":"sand",
  226. "HZ":"haze",
  227. "PY":"spray" }
  228. WEATHER_OTHER = { "PO":"sand whirls",
  229. "SQ":"squalls",
  230. "FC":"funnel cloud",
  231. "SS":"sandstorm",
  232. "DS":"dust storm" }
  233. WEATHER_SPECIAL = { "+FC":"tornado" }
  234. COLOR = { "BLU":"blue",
  235. "GRN":"green",
  236. "WHT":"white" }
  237. ## translation of various remark codes into English
  238. PRESSURE_TENDENCY = { "0":"increasing, then decreasing",
  239. "1":"increasing more slowly",
  240. "2":"increasing",
  241. "3":"increasing more quickly",
  242. "4":"steady",
  243. "5":"decreasing, then increasing",
  244. "6":"decreasing more slowly",
  245. "7":"decreasing",
  246. "8":"decreasing more quickly" }
  247. LIGHTNING_FREQUENCY = { "OCNL":"occasional",
  248. "FRQ":"frequent",
  249. "CONS":"constant" }
  250. LIGHTNING_TYPE = { "IC":"intracloud",
  251. "CC":"cloud-to-cloud",
  252. "CG":"cloud-to-ground",
  253. "CA":"cloud-to-air" }
  254. REPORT_TYPE = { "METAR":"routine report",
  255. "SPECI":"special report",
  256. "AUTO":"automatic report",
  257. "COR":"manually corrected report" }
  258. ## Helper functions
  259. def _report_match(handler,match):
  260. """Report success or failure of the given handler function. (DEBUG)"""
  261. if match:
  262. print handler.__name__," matched '"+match+"'"
  263. else:
  264. print handler.__name__," didn't match..."
  265. def _unparsedGroup( self, d ):
  266. """
  267. Handle otherwise unparseable main-body groups.
  268. """
  269. self._unparsed_groups.append(d['group'])
  270. ## METAR report objects
  271. debug = False
  272. class Metar(object):
  273. """METAR (aviation meteorology report)"""
  274. def __init__( self, metarcode, month=None, year=None, utcdelta=None):
  275. """Parse raw METAR code."""
  276. self.code = metarcode # original METAR code
  277. self.type = 'METAR' # METAR (routine) or SPECI (special)
  278. self.mod = "AUTO" # AUTO (automatic) or COR (corrected)
  279. self.station_id = None # 4-character ICAO station code
  280. self.time = None # observation time [datetime]
  281. self.cycle = None # observation cycle (0-23) [int]
  282. self.wind_dir = None # wind direction [direction]
  283. self.wind_speed = None # wind speed [speed]
  284. self.wind_gust = None # wind gust speed [speed]
  285. self.wind_dir_from = None # beginning of range for win dir [direction]
  286. self.wind_dir_to = None # end of range for wind dir [direction]
  287. self.vis = None # visibility [distance]
  288. self.vis_dir = None # visibility direction [direction]
  289. self.max_vis = None # visibility [distance]
  290. self.max_vis_dir = None # visibility direction [direction]
  291. self.temp = None # temperature (C) [temperature]
  292. self.dewpt = None # dew point (C) [temperature]
  293. self.press = None # barometric pressure [pressure]
  294. self.runway = [] # runway visibility (list of tuples)
  295. self.weather = [] # present weather (list of tuples)
  296. self.recent = [] # recent weather (list of tuples)
  297. self.sky = [] # sky conditions (list of tuples)
  298. self.windshear = [] # runways w/ wind shear (list of strings)
  299. self.wind_speed_peak = None # peak wind speed in last hour
  300. self.wind_dir_peak = None # direction of peak wind speed in last hour
  301. self.peak_wind_time = None # time of peak wind observation [datetime]
  302. self.wind_shift_time = None # time of wind shift [datetime]
  303. self.max_temp_6hr = None # max temp in last 6 hours
  304. self.min_temp_6hr = None # min temp in last 6 hours
  305. self.max_temp_24hr = None # max temp in last 24 hours
  306. self.min_temp_24hr = None # min temp in last 24 hours
  307. self.press_sea_level = None # sea-level pressure
  308. self.precip_1hr = None # precipitation over the last hour
  309. self.precip_3hr = None # precipitation over the last 3 hours
  310. self.precip_6hr = None # precipitation over the last 6 hours
  311. self.precip_24hr = None # precipitation over the last 24 hours
  312. self._trend = False # trend groups present (bool)
  313. self._trend_groups = [] # trend forecast groups
  314. self._remarks = [] # remarks (list of strings)
  315. self._unparsed_groups = []
  316. self._unparsed_remarks = []
  317. self._now = datetime.datetime.utcnow()
  318. if utcdelta:
  319. self._utcdelta = utcdelta
  320. else:
  321. self._utcdelta = datetime.datetime.now() - self._now
  322. self._month = month
  323. self._year = year
  324. code = self.code+" " # (the regexps all expect trailing spaces...)
  325. try:
  326. ngroup = len(Metar.handlers)
  327. igroup = 0
  328. ifailed = -1
  329. while igroup < ngroup and code:
  330. pattern, handler, repeatable = Metar.handlers[igroup]
  331. if debug: print handler.__name__,":",code
  332. m = pattern.match(code)
  333. while m:
  334. ifailed = -1
  335. if debug: _report_match(handler,m.group())
  336. handler(self,m.groupdict())
  337. code = code[m.end():]
  338. if self._trend:
  339. code = self._do_trend_handlers(code)
  340. if not repeatable: break
  341. if debug: print handler.__name__,":",code
  342. m = pattern.match(code)
  343. if not m and ifailed < 0:
  344. ifailed = igroup
  345. igroup += 1
  346. if igroup == ngroup and not m:
  347. # print "** it's not a main-body group **"
  348. pattern, handler = (UNPARSED_RE, _unparsedGroup)
  349. if debug: print handler.__name__,":",code
  350. m = pattern.match(code)
  351. if debug: _report_match(handler,m.group())
  352. handler(self,m.groupdict())
  353. code = code[m.end():]
  354. igroup = ifailed
  355. ifailed = -2 # if it's still -2 when we run out of main-body
  356. # groups, we'll try parsing this group as a remark
  357. if pattern == REMARK_RE or self.press:
  358. while code:
  359. for pattern, handler in Metar.remark_handlers:
  360. if debug: print handler.__name__,":",code
  361. m = pattern.match(code)
  362. if m:
  363. if debug: _report_match(handler,m.group())
  364. handler(self,m.groupdict())
  365. code = pattern.sub("",code,1)
  366. break
  367. except Exception, err:
  368. raise ParserError(handler.__name__+" failed while processing '"+code+"'\n"+string.join(err.args))
  369. raise err
  370. if self._unparsed_groups:
  371. code = ' '.join(self._unparsed_groups)
  372. raise ParserError("Unparsed groups in body: "+code)
  373. def _do_trend_handlers(self, code):
  374. for pattern, handler, repeatable in Metar.trend_handlers:
  375. if debug: print handler.__name__,":",code
  376. m = pattern.match(code)
  377. while m:
  378. if debug: _report_match(handler, m.group())
  379. self._trend_groups.append(string.strip(m.group()))
  380. handler(self,m.groupdict())
  381. code = code[m.end():]
  382. if not repeatable: break
  383. m = pattern.match(code)
  384. return code
  385. def __str__(self):
  386. return self.string()
  387. def _handleType( self, d ):
  388. """
  389. Parse the code-type group.
  390. The following attributes are set:
  391. type [string]
  392. """
  393. self.type = d['type']
  394. def _handleStation( self, d ):
  395. """
  396. Parse the station id group.
  397. The following attributes are set:
  398. station_id [string]
  399. """
  400. self.station_id = d['station']
  401. def _handleModifier( self, d ):
  402. """
  403. Parse the report-modifier group.
  404. The following attributes are set:
  405. mod [string]
  406. """
  407. mod = d['mod']
  408. if mod == 'CORR': mod = 'COR'
  409. if mod == 'NIL' or mod == 'FINO': mod = 'NO DATA'
  410. self.mod = mod
  411. def _handleTime( self, d ):
  412. """
  413. Parse the observation-time group.
  414. The following attributes are set:
  415. time [datetime]
  416. cycle [int]
  417. _day [int]
  418. _hour [int]
  419. _min [int]
  420. """
  421. self._day = int(d['day'])
  422. if not self._month:
  423. self._month = self._now.month
  424. if self._day > self._now.day:
  425. if self._month == 1:
  426. self._month = 12
  427. else:
  428. self._month = self._month - 1
  429. if not self._year:
  430. self._year = self._now.year
  431. if self._month > self._now.month:
  432. self._year = self._year - 1
  433. elif self._month == self._now.month and self._day > self._now.day:
  434. self._year = self._year - 1
  435. self._hour = int(d['hour'])
  436. self._min = int(d['min'])
  437. self.time = datetime.datetime(self._year, self._month, self._day,
  438. self._hour, self._min)
  439. if self._min < 45:
  440. self.cycle = self._hour
  441. else:
  442. self.cycle = self._hour+1
  443. def _handleWind( self, d ):
  444. """
  445. Parse the wind and variable-wind groups.
  446. The following attributes are set:
  447. wind_dir [direction]
  448. wind_speed [speed]
  449. wind_gust [speed]
  450. wind_dir_from [int]
  451. wind_dir_to [int]
  452. """
  453. wind_dir = d['dir'].replace('O','0')
  454. if wind_dir != "VRB" and wind_dir != "///" and wind_dir != "MMM":
  455. self.wind_dir = direction(wind_dir)
  456. wind_speed = d['speed'].replace('O','0')
  457. units = d['units']
  458. if units == 'KTS' or units == 'K' or units == 'T' or units == 'LT':
  459. units = 'KT'
  460. if wind_speed.startswith("P"):
  461. self.wind_speed = speed(wind_speed[1:], units, ">")
  462. elif not MISSING_RE.match(wind_speed):
  463. self.wind_speed = speed(wind_speed, units)
  464. if d['gust']:
  465. wind_gust = d['gust']
  466. if wind_gust.startswith("P"):
  467. self.wind_gust = speed(wind_gust[1:], units, ">")
  468. elif not MISSING_RE.match(wind_gust):
  469. self.wind_gust = speed(wind_gust, units)
  470. if d['varfrom']:
  471. self.wind_dir_from = direction(d['varfrom'])
  472. self.wind_dir_to = direction(d['varto'])
  473. def _handleVisibility( self, d ):
  474. """
  475. Parse the minimum and maximum visibility groups.
  476. The following attributes are set:
  477. vis [distance]
  478. vis_dir [direction]
  479. max_vis [distance]
  480. max_vis_dir [direction]
  481. """
  482. vis = d['vis']
  483. vis_less = None
  484. vis_dir = None
  485. vis_units = "M"
  486. vis_dist = "10000"
  487. if d['dist'] and d['dist'] != '////':
  488. vis_dist = d['dist']
  489. if d['dir'] and d['dir'] != 'NDV':
  490. vis_dir = d['dir']
  491. elif d['distu']:
  492. vis_dist = d['distu']
  493. if d['units'] and d['units'] != "U":
  494. vis_units = d['units']
  495. if vis_dist == "9999":
  496. vis_dist = "10000"
  497. vis_less = ">"
  498. if self.vis:
  499. if vis_dir:
  500. self.max_vis_dir = direction(vis_dir)
  501. self.max_vis = distance(vis_dist, vis_units, vis_less)
  502. else:
  503. if vis_dir:
  504. self.vis_dir = direction(vis_dir)
  505. self.vis = distance(vis_dist, vis_units, vis_less)
  506. def _handleRunway( self, d ):
  507. """
  508. Parse a runway visual range group.
  509. The following attributes are set:
  510. range [list of tuples]
  511. . name [string]
  512. . low [distance]
  513. . high [distance]
  514. """
  515. if d['name']:
  516. name = d['name']
  517. low = distance(d['low'])
  518. if d['high']:
  519. high = distance(d['high'])
  520. else:
  521. high = low
  522. self.runway.append((name,low,high))
  523. def _handleWeather( self, d ):
  524. """
  525. Parse a present-weather group.
  526. The following attributes are set:
  527. weather [list of tuples]
  528. . intensity [string]
  529. . description [string]
  530. . precipitation [string]
  531. . obscuration [string]
  532. . other [string]
  533. """
  534. inteni = d['int']
  535. if not inteni and d['int2']:
  536. inteni = d['int2']
  537. desci = d['desc']
  538. preci = d['prec']
  539. obsci = d['obsc']
  540. otheri = d['other']
  541. self.weather.append((inteni,desci,preci,obsci,otheri))
  542. def _handleSky( self, d ):
  543. """
  544. Parse a sky-conditions group.
  545. The following attributes are set:
  546. sky [list of tuples]
  547. . cover [string]
  548. . height [distance]
  549. . cloud [string]
  550. """
  551. height = d['height']
  552. if not height or height == "///":
  553. height = None
  554. else:
  555. height = height.replace('O','0')
  556. height = distance(int(height)*100,"FT")
  557. cover = d['cover']
  558. if cover == 'SCK' or cover == 'SKC' or cover == 'CL': cover = 'CLR'
  559. if cover == '0VC': cover = 'OVC'
  560. cloud = d['cloud']
  561. if cloud == '///': cloud = ""
  562. self.sky.append((cover,height,cloud))
  563. def _handleTemp( self, d ):
  564. """
  565. Parse a temperature-dewpoint group.
  566. The following attributes are set:
  567. temp temperature (Celsius) [float]
  568. dewpt dew point (Celsius) [float]
  569. """
  570. temp = d['temp']
  571. dewpt = d['dewpt']
  572. if temp and temp != "//" and temp != "XX" and temp != "MM" :
  573. self.temp = temperature(temp)
  574. if dewpt and dewpt != "//" and dewpt != "XX" and dewpt != "MM" :
  575. self.dewpt = temperature(dewpt)
  576. def _handlePressure( self, d ):
  577. """
  578. Parse an altimeter-pressure group.
  579. The following attributes are set:
  580. press [int]
  581. """
  582. press = d['press']
  583. if press != '////':
  584. press = float(press.replace('O','0'))
  585. if d['unit']:
  586. if d['unit'] == 'A' or (d['unit2'] and d['unit2'] == 'INS'):
  587. self.press = pressure(press/100,'IN')
  588. elif d['unit'] == 'SLP':
  589. if press < 500:
  590. press = press/10 + 1000
  591. else:
  592. press = press/10 + 900
  593. self.press = pressure(press,'MB')
  594. self._remarks.append("sea-level pressure %.1fhPa" % press)
  595. else:
  596. self.press = pressure(press,'MB')
  597. elif press > 2500:
  598. self.press = pressure(press/100,'IN')
  599. else:
  600. self.press = pressure(press,'MB')
  601. def _handleRecent( self, d ):
  602. """
  603. Parse a recent-weather group.
  604. The following attributes are set:
  605. weather [list of tuples]
  606. . intensity [string]
  607. . description [string]
  608. . precipitation [string]
  609. . obscuration [string]
  610. . other [string]
  611. """
  612. desci = d['desc']
  613. preci = d['prec']
  614. obsci = d['obsc']
  615. otheri = d['other']
  616. self.recent.append(("",desci,preci,obsci,otheri))
  617. def _handleWindShear( self, d ):
  618. """
  619. Parse wind-shear groups.
  620. The following attributes are set:
  621. windshear [list of strings]
  622. """
  623. if d['name']:
  624. self.windshear.append(d['name'])
  625. else:
  626. self.windshear.append("ALL")
  627. def _handleColor( self, d ):
  628. """
  629. Parse (and ignore) the color groups.
  630. The following attributes are set:
  631. trend [list of strings]
  632. """
  633. pass
  634. def _handleRunwayState( self, d ):
  635. """
  636. Parse (and ignore) the runway state.
  637. The following attributes are set:
  638. """
  639. pass
  640. def _handleTrend( self, d ):
  641. """
  642. Parse (and ignore) the trend groups.
  643. """
  644. if d.has_key('trend'):
  645. self._trend_groups.append(d['trend'])
  646. self._trend = True
  647. def _startRemarks( self, d ):
  648. """
  649. Found the start of the remarks section.
  650. """
  651. self._remarks = []
  652. def _handleSealvlPressRemark( self, d ):
  653. """
  654. Parse the sea-level pressure remark group.
  655. """
  656. value = float(d['press'])/10.0
  657. if value < 50:
  658. value += 1000
  659. else:
  660. value += 900
  661. if not self.press:
  662. self.press = pressure(value,"MB")
  663. self.press_sea_level = pressure(value,"MB")
  664. def _handlePrecip24hrRemark( self, d ):
  665. """
  666. Parse a 3-, 6- or 24-hour cumulative preciptation remark group.
  667. """
  668. value = float(d['precip'])/100.0
  669. if d['type'] == "6":
  670. if self.cycle == 3 or self.cycle == 9 or self.cycle == 15 or self.cycle == 21:
  671. self.precip_3hr = precipitation(value,"IN")
  672. else:
  673. self.precip_6hr = precipitation(value,"IN")
  674. else:
  675. self.precip_24hr = precipitation(value,"IN")
  676. def _handlePrecip1hrRemark( self, d ):
  677. """Parse an hourly precipitation remark group."""
  678. value = float(d['precip'])/100.0
  679. self.precip_1hr = precipitation(value,"IN")
  680. def _handleTemp1hrRemark( self, d ):
  681. """
  682. Parse a temperature & dewpoint remark group.
  683. These values replace the temp and dewpt from the body of the report.
  684. """
  685. value = float(d['temp'])/10.0
  686. if d['tsign'] == "1": value = -value
  687. self.temp = temperature(value)
  688. if d['dewpt']:
  689. value2 = float(d['dewpt'])/10.0
  690. if d['dsign'] == "1": value2 = -value2
  691. self.dewpt = temperature(value2)
  692. def _handleTemp6hrRemark( self, d ):
  693. """
  694. Parse a 6-hour maximum or minimum temperature remark group.
  695. """
  696. value = float(d['temp'])/10.0
  697. if d['sign'] == "1": value = -value
  698. if d['type'] == "1":
  699. self.max_temp_6hr = temperature(value,"C")
  700. else:
  701. self.min_temp_6hr = temperature(value,"C")
  702. def _handleTemp24hrRemark( self, d ):
  703. """
  704. Parse a 24-hour maximum/minimum temperature remark group.
  705. """
  706. value = float(d['maxt'])/10.0
  707. if d['smaxt'] == "1": value = -value
  708. value2 = float(d['mint'])/10.0
  709. if d['smint'] == "1": value2 = -value2
  710. self.max_temp_24hr = temperature(value,"C")
  711. self.min_temp_24hr = temperature(value2,"C")
  712. def _handlePress3hrRemark( self, d ):
  713. """
  714. Parse a pressure-tendency remark group.
  715. """
  716. value = float(d['press'])/10.0
  717. descrip = PRESSURE_TENDENCY[d['tend']]
  718. self._remarks.append("3-hr pressure change %.1fhPa, %s" % (value,descrip))
  719. def _handlePeakWindRemark( self, d ):
  720. """
  721. Parse a peak wind remark group.
  722. """
  723. peak_dir = int(d['dir'])
  724. peak_speed = int(d['speed'])
  725. self.wind_speed_peak = speed(peak_speed, "KT")
  726. self.wind_dir_peak = direction(peak_dir)
  727. peak_min = int(d['min'])
  728. if d['hour']:
  729. peak_hour = int(d['hour'])
  730. else:
  731. peak_hour = self._hour
  732. self.peak_wind_time = datetime.datetime(self._year, self._month, self._day,
  733. peak_hour, peak_min)
  734. if self.peak_wind_time > self.time:
  735. if peak_hour > self._hour:
  736. self.peak_wind_time -= datetime.timedelta(hours=24)
  737. else:
  738. self.peak_wind_time -= datetime.timedelta(hours=1)
  739. self._remarks.append("peak wind %dkt from %d degrees at %d:%02d" % \
  740. (peak_speed, peak_dir, peak_hour, peak_min))
  741. def _handleWindShiftRemark( self, d ):
  742. """
  743. Parse a wind shift remark group.
  744. """
  745. if d['hour']:
  746. wshft_hour = int(d['hour'])
  747. else:
  748. wshft_hour = self._hour
  749. wshft_min = int(d['min'])
  750. self.wind_shift_time = datetime.datetime(self._year, self._month, self._day,
  751. wshft_hour, wshft_min)
  752. if self.wind_shift_time > self.time:
  753. if wshft_hour > self._hour:
  754. self.wind_shift_time -= datetime.timedelta(hours=24)
  755. else:
  756. self.wind_shift_time -= datetime.timedelta(hours=1)
  757. text = "wind shift at %d:%02d" % (wshft_hour, wshft_min)
  758. if d['front']:
  759. text += " (front)"
  760. self._remarks.append(text)
  761. def _handleLightningRemark( self, d ):
  762. """
  763. Parse a lightning observation remark group.
  764. """
  765. parts = []
  766. if d['freq']:
  767. parts.append(LIGHTNING_FREQUENCY[d['freq']])
  768. parts.append("lightning")
  769. if d['type']:
  770. ltg_types = []
  771. group = d['type']
  772. while group:
  773. ltg_types.append(LIGHTNING_TYPE[group[:2]])
  774. group = group[2:]
  775. parts.append("("+string.join(ltg_types,",")+")")
  776. if d['loc']:
  777. parts.append(xlate_loc(d['loc']))
  778. self._remarks.append(string.join(parts," "))
  779. def _handleTSLocRemark( self, d ):
  780. """
  781. Parse a thunderstorm location remark group.
  782. """
  783. text = "thunderstorm"
  784. if d['loc']:
  785. text += " "+xlate_loc(d['loc'])
  786. if d['dir']:
  787. text += " moving %s" % d['dir']
  788. self._remarks.append(text)
  789. def _handleAutoRemark( self, d ):
  790. """
  791. Parse an automatic station remark group.
  792. """
  793. if d['type'] == "1":
  794. self._remarks.append("Automated station")
  795. elif d['type'] == "2":
  796. self._remarks.append("Automated station (type 2)")
  797. def _unparsedRemark( self, d ):
  798. """
  799. Handle otherwise unparseable remark groups.
  800. """
  801. self._unparsed_remarks.append(d['group'])
  802. ## the list of handler functions to use (in order) to process a METAR report
  803. handlers = [ (TYPE_RE, _handleType, False),
  804. (STATION_RE, _handleStation, False),
  805. (TIME_RE, _handleTime, False),
  806. (MODIFIER_RE, _handleModifier, False),
  807. (WIND_RE, _handleWind, False),
  808. (VISIBILITY_RE, _handleVisibility, True),
  809. (RUNWAY_RE, _handleRunway, True),
  810. (WEATHER_RE, _handleWeather, True),
  811. (SKY_RE, _handleSky, True),
  812. (TEMP_RE, _handleTemp, False),
  813. (PRESS_RE, _handlePressure, True),
  814. (RECENT_RE,_handleRecent, True),
  815. (WINDSHEAR_RE, _handleWindShear, True),
  816. (COLOR_RE, _handleColor, True),
  817. (RUNWAYSTATE_RE, _handleRunwayState, True),
  818. (TREND_RE, _handleTrend, False),
  819. (REMARK_RE, _startRemarks, False) ]
  820. trend_handlers = [ (TRENDTIME_RE, _handleTrend, True),
  821. (WIND_RE, _handleTrend, True),
  822. (VISIBILITY_RE, _handleTrend, True),
  823. (WEATHER_RE, _handleTrend, True),
  824. (SKY_RE, _handleTrend, True),
  825. (COLOR_RE, _handleTrend, True)]
  826. ## the list of patterns for the various remark groups,
  827. ## paired with the handler functions to use to record the decoded remark.
  828. remark_handlers = [ (AUTO_RE, _handleAutoRemark),
  829. (SEALVL_PRESS_RE, _handleSealvlPressRemark),
  830. (PEAK_WIND_RE, _handlePeakWindRemark),
  831. (WIND_SHIFT_RE, _handleWindShiftRemark),
  832. (LIGHTNING_RE, _handleLightningRemark),
  833. (TS_LOC_RE, _handleTSLocRemark),
  834. (TEMP_1HR_RE, _handleTemp1hrRemark),
  835. (PRECIP_1HR_RE, _handlePrecip1hrRemark),
  836. (PRECIP_24HR_RE, _handlePrecip24hrRemark),
  837. (PRESS_3HR_RE, _handlePress3hrRemark),
  838. (TEMP_6HR_RE, _handleTemp6hrRemark),
  839. (TEMP_24HR_RE, _handleTemp24hrRemark),
  840. (UNPARSED_RE, _unparsedRemark) ]
  841. ## functions that return text representations of conditions for output
  842. def string( self ):
  843. """
  844. Return a human-readable version of the decoded report.
  845. """
  846. lines = []
  847. lines.append("station: %s" % self.station_id)
  848. if self.type:
  849. lines.append("type: %s" % self.report_type())
  850. if self.time:
  851. lines.append("time: %s" % self.time.ctime())
  852. if self.temp:
  853. lines.append("temperature: %s" % self.temp.string("C"))
  854. if self.dewpt:
  855. lines.append("dew point: %s" % self.dewpt.string("C"))
  856. if self.wind_speed:
  857. lines.append("wind: %s" % self.wind())
  858. if self.wind_speed_peak:
  859. lines.append("peak wind: %s" % self.peak_wind())
  860. if self.wind_shift_time:
  861. lines.append("wind shift: %s" % self.wind_shift())
  862. if self.vis:
  863. lines.append("visibility: %s" % self.visibility())
  864. if self.runway:
  865. lines.append("visual range: %s" % self.runway_visual_range())
  866. if self.press:
  867. lines.append("pressure: %s" % self.press.string("mb"))
  868. if self.weather:
  869. lines.append("weather: %s" % self.present_weather())
  870. if self.sky:
  871. lines.append("sky: %s" % self.sky_conditions("\n "))
  872. if self.press_sea_level:
  873. lines.append("sea-level pressure: %s" % self.press_sea_level.string("mb"))
  874. if self.max_temp_6hr:
  875. lines.append("6-hour max temp: %s" % str(self.max_temp_6hr))
  876. if self.max_temp_6hr:
  877. lines.append("6-hour min temp: %s" % str(self.min_temp_6hr))
  878. if self.max_temp_24hr:
  879. lines.append("24-hour max temp: %s" % str(self.max_temp_24hr))
  880. if self.max_temp_24hr:
  881. lines.append("24-hour min temp: %s" % str(self.min_temp_24hr))
  882. if self.precip_1hr:
  883. lines.append("1-hour precipitation: %s" % str(self.precip_1hr))
  884. if self.precip_3hr:
  885. lines.append("3-hour precipitation: %s" % str(self.precip_3hr))
  886. if self.precip_6hr:
  887. lines.append("6-hour precipitation: %s" % str(self.precip_6hr))
  888. if self.precip_24hr:
  889. lines.append("24-hour precipitation: %s" % str(self.precip_24hr))
  890. if self._remarks:
  891. lines.append("remarks:")
  892. lines.append("- "+self.remarks("\n- "))
  893. if self._unparsed_remarks:
  894. lines.append("- "+' '.join(self._unparsed_remarks))
  895. lines.append("METAR: "+self.code)
  896. return string.join(lines,"\n")
  897. def report_type( self ):
  898. """
  899. Return a textual description of the report type.
  900. """
  901. if self.type == None:
  902. text = "unknown report type"
  903. elif REPORT_TYPE.has_key(self.type):
  904. text = REPORT_TYPE[self.type]
  905. else:
  906. text = self.type+" report"
  907. if self.cycle:
  908. text += ", cycle %d" % self.cycle
  909. if self.mod:
  910. if REPORT_TYPE.has_key(self.mod):
  911. text += " (%s)" % REPORT_TYPE[self.mod]
  912. else:
  913. text += " (%s)" % self.mod
  914. return text
  915. def wind( self, units="KT" ):
  916. """
  917. Return a textual description of the wind conditions.
  918. Units may be specified as "MPS", "KT", "KMH", or "MPH".
  919. """
  920. if self.wind_speed == None:
  921. return "missing"
  922. elif self.wind_speed.value() == 0.0:
  923. text = "calm"
  924. else:
  925. wind_speed = self.wind_speed.string(units)
  926. if not self.wind_dir:
  927. text = "variable at %s" % wind_speed
  928. elif self.wind_dir_from:
  929. text = "%s to %s at %s" % \
  930. (self.wind_dir_from.compass(), self.wind_dir_to.compass(), wind_speed)
  931. else:
  932. text = "%s at %s" % (self.wind_dir.compass(), wind_speed)
  933. if self.wind_gust:
  934. text += ", gusting to %s" % self.wind_gust.string(units)
  935. return text
  936. def peak_wind( self, units="KT" ):
  937. """
  938. Return a textual description of the peak wind conditions.
  939. Units may be specified as "MPS", "KT", "KMH", or "MPH".
  940. """
  941. if self.wind_speed_peak == None:
  942. return "missing"
  943. elif self.wind_speed_peak.value() == 0.0:
  944. text = "calm"
  945. else:
  946. wind_speed = self.wind_speed_peak.string(units)
  947. if not self.wind_dir_peak:
  948. text = wind_speed
  949. else:
  950. text = "%s at %s" % (self.wind_dir_peak.compass(), wind_speed)
  951. if not self.peak_wind_time == None:
  952. text += " at %s" % self.peak_wind_time.strftime('%H:%M')
  953. return text
  954. def wind_shift( self, units="KT" ):
  955. """
  956. Return a textual description of the wind shift time
  957. Units may be specified as "MPS", "KT", "KMH", or "MPH".
  958. """
  959. if self.wind_shift_time == None:
  960. return "missing"
  961. else:
  962. return self.wind_shift_time.strftime('%H:%M')
  963. def visibility( self, units=None ):
  964. """
  965. Return a textual description of the visibility.
  966. Units may be statute miles ("SM") or meters ("M").
  967. """
  968. if self.vis == None:
  969. return "missing"
  970. if self.vis_dir:
  971. text = "%s to %s" % (self.vis.string(units), self.vis_dir.compass())
  972. else:
  973. text = self.vis.string(units)
  974. if self.max_vis:
  975. if self.max_vis_dir:
  976. text += "; %s to %s" % (self.max_vis.string(units), self.max_vis_dir.compass())
  977. else:
  978. text += "; %s" % self.max_vis.string(units)
  979. return text
  980. def runway_visual_range( self, units=None ):
  981. """
  982. Return a textual description of the runway visual range.
  983. """
  984. lines = []
  985. for name,low,high in self.runway:
  986. if low != high:
  987. lines.append("on runway %s, from %d to %s" % (name, low.value(units), high.string(units)))
  988. else:
  989. lines.append("on runway %s, %s" % (name, low.string(units)))
  990. return string.join(lines,"; ")
  991. def present_weather( self ):
  992. """
  993. Return a textual description of the present weather.
  994. """
  995. text_list = []
  996. for weatheri in self.weather:
  997. (inteni,desci,preci,obsci,otheri) = weatheri
  998. text_parts = []
  999. code_parts = []
  1000. if inteni:
  1001. code_parts.append(inteni)
  1002. text_parts.append(WEATHER_INT[inteni])
  1003. if desci:
  1004. code_parts.append(desci)
  1005. if desci != "SH" or not preci:
  1006. text_parts.append(WEATHER_DESC[desci[0:2]])
  1007. if len(desci) == 4:
  1008. text_parts.append(WEATHER_DESC[desci[2:]])
  1009. if preci:
  1010. code_parts.append(preci)
  1011. if len(preci) == 2:
  1012. precip_text = WEATHER_PREC[preci]
  1013. elif len(preci) == 4:
  1014. precip_text = WEATHER_PREC[preci[:2]]+" and "
  1015. precip_text += WEATHER_PREC[preci[2:]]
  1016. elif len(preci) == 6:
  1017. precip_text = WEATHER_PREC[preci[:2]]+", "
  1018. precip_text += WEATHER_PREC[preci[2:4]]+" and "
  1019. precip_text += WEATHER_PREC[preci[4:]]
  1020. if desci == "TS":
  1021. text_parts.append("with")
  1022. text_parts.append(precip_text)
  1023. if desci == "SH":
  1024. text_parts.append(WEATHER_DESC[desci])
  1025. if obsci:
  1026. code_parts.append(obsci)
  1027. text_parts.append(WEATHER_OBSC[obsci])
  1028. if otheri:
  1029. code_parts.append(otheri)
  1030. text_parts.append(WEATHER_OTHER[otheri])
  1031. code = string.join(code_parts)
  1032. if WEATHER_SPECIAL.has_key(code):
  1033. text_list.append(WEATHER_SPECIAL[code])
  1034. else:
  1035. text_list.append(string.join(text_parts," "))
  1036. return string.join(text_list,"; ")
  1037. def sky_conditions( self, sep="; " ):
  1038. """
  1039. Return a textual description of the sky conditions.
  1040. """
  1041. text_list = []
  1042. for skyi in self.sky:
  1043. (cover,height,cloud) = skyi
  1044. if cover == "SKC" or cover == "CLR":
  1045. text_list.append(SKY_COVER[cover])
  1046. else:
  1047. if cloud:
  1048. what = CLOUD_TYPE[cloud]
  1049. elif cover != "OVC":
  1050. what = "clouds"
  1051. else:
  1052. what = ""
  1053. if cover == "VV":
  1054. text_list.append("%s%s, visibility to %s" %
  1055. (SKY_COVER[cover],what,str(height)))
  1056. else:
  1057. text_list.append("%s%s at %s" %
  1058. (SKY_COVER[cover],what,str(height)))
  1059. return string.join(text_list,sep)
  1060. def trend( self ):
  1061. """
  1062. Return the trend forecast groups
  1063. """
  1064. return " ".join(self._trend_groups)
  1065. def remarks( self, sep="; "):
  1066. """
  1067. Return the decoded remarks.
  1068. """
  1069. return string.join(self._remarks,sep)