Initial import
[metar.git] / src / metar.lua
1 -------------------------------------------------------------------------------
2 -- Lua class to parse METAR coded weather reports and fetch current METAR
3 -- reports from <a href="http://www.noaa.gov">NOAA</a> <a href="http://weather.noaa.gov">Internet Weather Service</a>.
4 -- The parser is pretty simple and by no means claims to support every feature
5 -- one might find in METAR coded weather reports. For example, weather forecasts
6 -- and automatic weather reports are not detected. Unsupported features in
7 -- the weather reports are silently dropped.
8 -- @author Tuomas Jormola
9 -- @copyright © 2010 Tuomas Jormola <a href="mailto:tj@solitudo.net">tj@solitudo.net</a> <a href="http://solitudo.net">http://solitudo.net</a>
10 --  Licensed under the terms of
11 -- the <a href="http://www.gnu.org/licenses/gpl-2.0.html">GNU General Public License Version 2.0</a>.
12 --
13 -------------------------------------------------------------------------------
14
15 local socket_http  = require('socket.http')
16
17 local weatherlib   = require('weatherlib')
18
19 local os           = { date = os.date, time = os.time }
20 local pairs        = pairs
21 local print        = print
22 local setmetatable = setmetatable
23 local string       = { format = string.format }
24 local table        = { insert = table.insert, maxn = table.maxn}
25 local tonumber     = tonumber
26 local type         = type
27
28 module('metar')
29
30 local token_data = {
31         wind = {
32                 directions = {
33                         { key = 'N',   min = 349, max = 11                              },
34                         { key = 'NNE', min = 12,  max = 33                              },
35                         { key = 'NE',  min = 34,  max = 56                              },
36                         { key = 'ENE', min = 57,  max = 78                              },
37                         { key = 'E',   min = 79,  max = 101                             },
38                         { key = 'ESE', min = 102, max = 123                             },
39                         { key = 'SE',  min = 124, max = 146                             },
40                         { key = 'SSE', min = 147, max = 168                             },
41                         { key = 'S',   min = 169, max = 191                             },
42                         { key = 'SSW', min = 192, max = 213                             },
43                         { key = 'SW',  min = 214, max = 236                             },
44                         { key = 'WSW', min = 237, max = 258                             },
45                         { key = 'W',   min = 259, max = 281                             },
46                         { key = 'WNW', min = 282, max = 303                             },
47                         { key = 'NW',  min = 304, max = 326                             },
48                         { key = 'NNW', min = 327, max = 348                             },
49                 },
50                 speeds = {
51                         KT  = weatherlib.SPEED_UNITS.KNOT,
52                         MPS = weatherlib.SPEED_UNITS.MS,
53                         KMH = weatherlib.SPEED_UNITS.KMH,
54                 },
55                 offset = 1,
56         },
57         clouds = {
58                 coverages = {
59                         { token = 'SKC', alias = 'CLR',   key = 'CLEAR'                 },
60                         { token = 'FEW',                  key = 'FEW'                   },
61                         { token = 'SCT',                  key = 'SCATTERED'             },
62                         { token = 'BKN',                  key = 'BROKEN_SKY'            },
63                         { token = 'OVC',                  key = 'OVERCAST'              },
64                 },
65                 types = {
66                         { token = 'CB',                   key = 'CUMULONIMBUS'          },
67                         { token = 'TCU',                  key = 'TOWERING_CUMULUS'      },
68                 },
69         },
70         sky = {
71                 types = {
72                         { token = 'CAVOK', alias = 'SKC', key = 'CLEAR'                 },
73                         { token = 'NSC',                  key = 'NO_SIGNIFICANT_CLOUDS' },
74                         { token = 'NCD',                  key = 'NO_CLOUDS_DETECTED'    },
75                 },
76                 offset = 3,
77         },
78         weather_intensity = {
79                 types = {
80                         { token = '-',                    key = 'LIGHT'                 },
81                         { token = '+',                    key = 'HEAVY'                 },
82                         { token = 'VC',                   key = 'VICINITY'              },
83                 },
84                 offset = 1,
85         },
86         weather_descriptor = {
87                 types = {
88                         { token = 'MI',                   key = 'SHALLOW'               },
89                         { token = 'PR',                   key = 'PARTIAL'               },
90                         { token = 'BC',                   key = 'PATCHES'               },
91                         { token = 'DR',                   key = 'DRIFTING'              },
92                         { token = 'BL',                   key = 'BLOWING'               },
93                         { token = 'SH',                   key = 'SHOWERS'               },
94                         { token = 'TS',                   key = 'THUNDERSTORM'          },
95                         { token = 'FZ',                   key = 'FREEZING'              },
96                 },
97         },
98         weather_phenomena = {
99                 types = {
100                         { token = 'DZ',                   key = 'DRIZZLE'               },
101                         { token = 'RA',                   key = 'RAIN'                  },
102                         { token = 'SN',                   key = 'SNOW'                  },
103                         { token = 'SG',                   key = 'SNOW_GRAINS'           },
104                         { token = 'IC',                   key = 'ICE_CRYSTALS'          },
105                         { token = 'PL', alias = 'PE',     key = 'ICE_PELLETS'           },
106                         { token = 'GR',                   key = 'HAIL'                  },
107                         { token = 'GS',                   key = 'SMALL_HAIL'            },
108                         { token = 'UP',                   key = 'UNKNOWN'               },
109                         { token = 'BR',                   key = 'MIST'                  },
110                         { token = 'FG',                   key = 'FOG'                   },
111                         { token = 'FU',                   key = 'SMOKE'                 },
112                         { token = 'VA',                   key = 'VOLCANIC_ASH'          },
113                         { token = 'DU',                   key = 'WIDESPREAD_DUST'       },
114                         { token = 'SA',                   key = 'SAND'                  },
115                         { token = 'HZ',                   key = 'HAZE'                  },
116                         { token = 'PY',                   key = 'SPRAY'                 },
117                         { token = 'PO',                   key = 'DUST_WHIRLS'           },
118                         { token = 'SQ',                   key = 'SQUALLS'               },
119                         { token = 'FC',                   key = 'FUNNEL_CLOUD'          },
120                         { token = 'SS',                   key = 'SAND_STORM'            },
121                         { token = 'DS',                   key = 'DUST_STORM'            },
122                 },
123         }
124 }
125
126 -------------------------------------------------------------------------------
127 -- Wind direction table.
128 -- Values from this table are used in the result table with key <code>wind.direction</code>
129 -- returned by <a href="#metatable.__index:get_metar_data"><code>get_metar_data()</code></a>.
130 -- @class table
131 -- @name WIND_DIRECTION
132 -- @field VRB Index value for variable speed direction
133 -- @field N Index value for North
134 -- @field NNE Index value for North - North East
135 -- @field NE Index value for Nort - East
136 -- @field ENE Index value for East - North East
137 -- @field E Index value for East
138 -- @field ESE Index value for East - South East
139 -- @field SE Index value for South East
140 -- @field SSE Index value for South - South Est
141 -- @field S Index value for South
142 -- @field SSW Index value for South - South West
143 -- @field SW Index value for South West
144 -- @field WSW Index value for West - South West
145 -- @field W  Index value for West
146 -- @field WNW Index value for West - North West
147 -- @field NW Index value for North West
148 -- @field NNW Index value for North - North West
149 WIND_DIRECTION      = { VRB = 1 }
150
151 -------------------------------------------------------------------------------
152 -- Could coverage table.
153 -- Values from this table are used in the result table with key <code>clouds[n].coverage</code> returned by
154 -- <a href="#metatable.__index:get_metar_data"><code>get_metar_data()</code></a>.
155 -- @class table
156 -- @name CLOUD_COVERAGE
157 -- @field CLEAR Clear
158 -- @field FEW Few clouds
159 -- @field SCATTERED Scattered clouds
160 -- @field BROKEN_SKY Broken sky
161 -- @field OVERCAST Overcast
162 CLOUD_COVERAGE      = {}
163
164 -------------------------------------------------------------------------------
165 -- Could type table.
166 -- Values from this table are used in the result table with key <code>clouds[n].type</code> returned by
167 -- <a href="#metatable.__index:get_metar_data"><code>get_metar_data()</code></a>.
168 -- @class table
169 -- @name CLOUD_TYPE
170 -- @field CUMULONIMBUS Cumulonimbus clouds
171 -- @field TOWERING_CUMULUS Towering Cumulus clouds
172 CLOUD_TYPE          = {}
173
174 -------------------------------------------------------------------------------
175 -- Could type table.
176 -- Values from this table are used in the result table with key <code>sky</code> returned by
177 -- <a href="#metatable.__index:get_metar_data"><code>get_metar_data()</code></a>.
178 -- @class table
179 -- @name SKY_STATUS
180 -- @field UNKNOWN Sky type is unknown
181 -- @field OBSCURE Obscured sky
182 -- @field CLOUDS Clouds in the sky
183 -- @field CLEAR Clear sky
184 -- @field NO_SIGNIFICANT_CLOUDS No significant clouds detected
185 -- @field NO_CLOUDS_DETECTED No clouds detected
186 SKY_STATUS          = { UNKNOWN = 1, OBSCURE = 2, CLOUDS = 3}
187
188 -------------------------------------------------------------------------------
189 -- Weather intensity table.
190 -- Values from this table are used in the result table with key <code>weather.intensity</code> returned by
191 -- <a href="#metatable.__index:get_metar_data"><code>get_metar_data()</code></a>.
192 -- @class table
193 -- @name WEATHER_INTENSITY
194 -- @field MODERATE Moderate phenomena
195 -- @field LIGHT Light phenomena
196 -- @field HEAVY Heavy phenomena
197 -- @field VICINITY In the vicinity of the weather observation point
198 WEATHER_INTENSITY   = { MODERATE = 1 }
199
200 -------------------------------------------------------------------------------
201 -- Weather descriptor table.
202 -- Values from this table are used in the result table with key <code>weather.descriptor</code> returned by
203 -- <a href="#metatable.__index:get_metar_data"><code>get_metar_data()</code></a>.
204 -- @class table
205 -- @name WEATHER_DESCRIPTOR
206 -- @field SHALLOW Shallow phenomena
207 -- @field PARTIAL Partial phenomena
208 -- @field PATCHES Patches phenomena
209 -- @field DRIFTING Drifring phenomena
210 -- @field BLOWING Blowing phenomena
211 -- @field SHOWERS Showers phenomena
212 -- @field THUNDERSTORM Thunderstorm phenomena
213 -- @field FREEZING Freezing phenomena
214 WEATHER_DESCRIPTOR   = {}
215
216 -------------------------------------------------------------------------------
217 -- Weather phenomena table.
218 -- Values from this table are used in the result table with key <code>weather.phenomena</code> returned by
219 -- <a href="#metatable.__index:get_metar_data"><code>get_metar_data()</code></a>.
220 -- @class table
221 -- @name WEATHER_PHENOMENA
222 -- @field DRIZZLE Drizzle
223 -- @field RAIN Rain
224 -- @field SNOW Snow
225 -- @field SNOW_GRAINS Snow grains
226 -- @field ICE_CRYSTALS Ice crystals
227 -- @field ICE_PELLETS Ice pellets
228 -- @field HAIL Hail
229 -- @field SMALL_HAIL Small hail
230 -- @field UNKNOWN Unknown phenomena
231 -- @field MIST Mist
232 -- @field FOG Fog
233 -- @field SMOKE Smoke
234 -- @field VOLCANIC_ASH Volcanic ash
235 -- @field WIDESPREAD_DUST Widespread dust
236 -- @field SAND Sand
237 -- @field HAZE Haze
238 -- @field SPRAY Spray
239 -- @field DUST_WHIRLS Dust whirls
240 -- @field SQUALLS Squalls
241 -- @field FUNNEL_CLOUD Funnel cloud
242 -- @field SAND_STORM Sand storm
243 -- @field DUST_STORM Dust storm
244 WEATHER_PHENOMENA   = {}
245
246 -- Fill the constant tables
247 for i, key in pairs(token_data.wind.directions) do
248         WIND_DIRECTION[key.key] = i + token_data.wind.offset
249 end
250 for i, key in pairs(token_data.clouds.coverages) do
251         CLOUD_COVERAGE[key.key] = i
252 end
253 for i, key in pairs(token_data.clouds.types) do
254         CLOUD_TYPE[key.key] = i
255 end
256 for i, key in pairs(token_data.sky.types) do
257         SKY_STATUS[key.key] = i + token_data.sky.offset
258 end
259 for i, key in pairs(token_data.weather_intensity.types) do
260         WEATHER_INTENSITY[key.key] = i + token_data.weather_intensity.offset
261 end
262 for i, key in pairs(token_data.weather_descriptor.types) do
263         WEATHER_DESCRIPTOR[key.key] = i
264 end
265 for i, key in pairs(token_data.weather_phenomena.types) do
266         WEATHER_PHENOMENA[key.key] = i
267 end
268
269 local function parse_metar_date(day, hour, min)
270         if not day or not hour or not min then
271                 return
272         end
273         day  = tonumber(day)
274         hour = tonumber(hour)
275         min  = tonumber(min)
276         local now = os.date('!*t')
277         if day > now.day and now.day == 1 then
278                 now.month = _get_previous_month(now)
279                 now.day = _get_last_day_of_month(now)
280                 if now.month == 12 then
281                         now.year = now.year - 1
282                 end
283         else
284                 now.day = day
285         end
286         now.hour = hour
287         now.min  = min
288         now.sec  = 0
289         return { timestamp = os.time(now) }
290 end
291
292 -- The parse_* routines parse snippets of METAR data
293
294 local function parse_metar_wind(dir, speed, gust, unit)
295         local direction
296         if gust and token_data.wind.speeds[gust] then
297                         unit = gust
298                         gust = nil
299         end
300         if not dir or not speed or not unit or not token_data.wind.speeds[unit] then
301                 return
302         end
303         if dir:match('^%d%d%d$') then
304                 dir = tonumber(dir)
305                 for _, test_direction in pairs(token_data.wind.directions) do
306                         if (
307                                                         (test_direction.min > test_direction.max)
308                                                 and
309                                                         (test_direction.min <= dir or dir <= test_direction.max)
310                                         or
311                                                         (test_direction.max > test_direction.min)
312                                                 and
313                                                         (test_direction.min <= dir and dir <= test_direction.max)
314                                         ) then
315                                 direction = WIND_DIRECTION[test_direction.key]
316                                 break
317                         end
318                 end
319         else
320                 direction = WIND_DIRECTION[dir]
321         end
322         if not direction then
323                 return
324         end
325         local data = { direction = direction, speed = weatherlib.convert_speed(token_data.wind.speeds[unit], weatherlib.SPEED_UNITS.KNOT, tonumber(speed)) }
326         if gust ~= nil then
327                 data['gust'] = weatherlib.convert_speed(token_data.wind.speeds[unit], weatherlib.SPEED_UNITS.KNOT, tonumber(gust))
328         end
329         return { wind = data }
330 end
331
332 local function parse_metar_visibility(visibility, direction)
333         if visibility == 'CAVOK' then
334                 visibility = 9999
335         end
336         local data = { distance = tonumber(visibility) }
337         if direction and direction:len() > 0 then
338                 if WIND_DIRECTION[direction] then
339                         data['direction'] = WIND_DIRECTION[direction]
340                 else
341                         return
342                 end
343         end
344         return { visibility = data }
345 end
346
347 local function parse_metar_runway_visual_range(runway, visibility)
348         return { runway_visual_range = { runway = tonumber(runway), visibility = tonumber(visibility) } } 
349 end
350
351 local function parse_metar_clouds(coverage, altitude, type)
352         local coverage_key
353         for _, coverage_data in pairs(token_data.clouds.coverages) do
354                 if coverage == coverage_data.token or coverage == coverage_data.alias then
355                         coverage_key = coverage_data.key
356                         break
357                 end
358         end
359         if not coverage_key then
360                 return
361         end
362         if not CLOUD_COVERAGE[coverage_key] then
363                 return
364         end
365         local data = { coverage = CLOUD_COVERAGE[coverage_key], altitude = tonumber(altitude) }
366         if type and type:len() > 0 then
367                 local type_key
368                 for _, type_data in pairs(token_data.clouds.types) do
369                         if type == type_data.token then
370                                 type_key = type_data.key
371                                 break
372                         end
373                 end
374                 if not type_key then
375                         return
376                 end
377                 if not CLOUD_TYPE[type_key] then
378                         return
379                 end
380                 data['type'] = CLOUD_TYPE[type_key]
381         end
382         return { clouds = data }
383 end
384
385 local function parse_metar_vertical_visibility(visibility)
386         if visibility and visibility:find('^%d%d%d$') then
387                 visibility = tonumber(visibility)
388         elseif visibility == '///' then
389                 visibility = 0
390         else
391                 return
392         end
393         return { vertical_visibility = visibility }
394 end
395
396 local function parse_metar_sky(status)
397         local sky_key
398         for _, sky_data in pairs(token_data.sky.types) do
399                 if status == sky_data.token or status == sky_data.alias then
400                         sky_key = sky_data.key
401                         break
402                 end
403         end
404         if not sky_key then
405                 return
406         end
407         if not SKY_STATUS[sky_key] then
408                 return
409         end
410         return { sky = SKY_STATUS[sky_key] }
411 end
412
413 local function parse_metar_weather(intensity, descriptor, phenomena)
414         if not phenomena or phenomena:len() == 0 then
415                 phenomena = descriptor
416                 descriptor = nil
417         end
418         local intensity_key, descriptor_key, phenomena_key
419         if not intensity or intensity:len() == 0 then
420                 intensity = WEATHER_INTENSITY.MODERATE
421         else
422                 for _, intensity_data in pairs(token_data.weather_intensity.types) do
423                         if intensity == intensity_data.token then
424                                 intensity_key = intensity_data.key
425                                 break
426                         end
427                 end
428                 if not intensity_key or not WEATHER_INTENSITY[intensity_key] then
429                         return
430                 end
431                 intensity = WEATHER_INTENSITY[intensity_key]
432         end
433         if descriptor and descriptor:len() > 0 then
434                 for _, descriptor_data in pairs(token_data.weather_descriptor.types) do
435                         if descriptor == descriptor_data.token then
436                                 descriptor_key = descriptor_data.key
437                                 break
438                         end
439                 end
440                 if not descriptor_key or not WEATHER_DESCRIPTOR[descriptor_key] then
441                         return
442                 end
443                 descriptor = WEATHER_DESCRIPTOR[descriptor_key]
444         end
445         if not phenomena or phenomena:len() == 0 then
446                 return
447         end
448         for _, phenomena_data in pairs(token_data.weather_phenomena.types) do
449                 if phenomena == phenomena_data.token or phenomena == phenomena_data.alias then
450                         phenomena_key = phenomena_data.key
451                         break
452                 end
453         end
454         if not phenomena_key or not WEATHER_PHENOMENA[phenomena_key] then
455                 return
456         end
457         phenomena = WEATHER_PHENOMENA[phenomena_key]
458         local weather_data = { intensity = intensity, phenomena = phenomena }
459         if descriptor then
460                 weather_data['descriptor'] = descriptor
461         end
462         return { weather = weather_data }
463 end
464
465 local function parse_metar_temperature(temperature, dewpoint)
466         temperature = temperature:gsub('^M', '-')
467         dewpoint = dewpoint:gsub('^M', '-')
468         return { temperature = tonumber(temperature), dewpoint = tonumber(dewpoint) }
469 end
470
471 local function parse_metar_pressure(unit, pressure)
472         if unit == 'A' then
473                 pressure = weatherlib.convert.pressure[weatherlib.PRESSURE_UNITS.INHG][weatherlib.PRESSURE_UNITS.HPA](tonumber(pressure) / 100)
474         else
475                 pressure = tonumber(pressure)
476         end
477         return { pressure = pressure }
478 end
479
480 -- Define patterns for each snippet of METAR data to parse
481 local metar_token_handlers = {
482         {
483                 pattern = '^(%d%d)(%d%d)(%d%d)Z$',
484                 handler = parse_metar_date,
485         },
486         {
487                 pattern = '^(%w%w%w)(%d+)G(%d+)([A-Z]+)$',
488                 handler = parse_metar_wind,
489         },
490         {
491                 pattern = '^(%w%w%w)(%d+)([A-Z]+)$',
492                 handler = parse_metar_wind,
493         },
494         {
495                 pattern = '^(%d%d%d%d)([A-Z]*)$',
496                 handler = parse_metar_visibility,
497         },
498         {
499                 pattern = '^(CAVOK)$',
500                 handler = parse_metar_visibility,
501         },
502         {
503                 pattern = '^R(%d+)/(%d+)$',
504                 handler = parse_metar_runway_visual_range,
505         },
506         {
507                 pattern = '^([A-Z][A-Z][A-Z])(%d%d%d)([A-Z]*)$',
508                 handler = parse_metar_clouds,
509         },
510         {
511                 pattern = '^VV([%d/][%d/][%d/])$',
512                 handler = parse_metar_vertical_visibility,
513         },
514         {
515                 pattern = '^([A-Z]+)$',
516                 handler = parse_metar_sky,
517         },
518         {
519                 pattern = '^([+-]*)([A-Z][A-Z])$',
520                 handler = parse_metar_weather,
521         },
522         {
523                 pattern = '^([+-]*)([A-Z][A-Z])([A-Z][A-Z])$',
524                 handler = parse_metar_weather,
525         },
526         {
527                 pattern = '^(VC)([A-Z][A-Z])*$',
528                 handler = parse_metar_weather,
529         },
530         {
531                 pattern = '^(VC)([A-Z][A-Z])([A-Z][A-Z])*$',
532                 handler = parse_metar_weather,
533         },
534         {
535                 pattern = '^(M*%d+)/(M*%d+)$',
536                 handler = parse_metar_temperature,
537         },
538         {
539                 pattern = '^([QA])(%d+)$',
540                 handler = parse_metar_pressure,
541         },
542 }
543
544 local function _get_previous_month(date)
545         if date.month == 1 then
546                 return 12
547         end
548         return date.month - 1
549 end
550
551 local function _get_last_day_of_month(date)
552         date.day  = 1
553         date.hour = 0
554         date.min  = 0
555         date.sec  = 0
556         date.wday = 0
557         date.yday = 0
558         local day_seconds = 60 * 60 * 24
559         local timestamp = os.time(date)
560         local test_date = date
561         -- count days forward until month changes
562         while(date.month == test_date.month) do
563                 timestamp = os.time(test_date) + day_seconds
564                 test_date = os.date('*t', timestamp)
565         end
566         -- rewind one day to get the last day of the month
567         timestamp = timestamp - day_seconds
568         return os.date('*t', timestamp).day
569 end
570
571 local metatable = { __index = {} }
572
573 -------------------------------------------------------------------------------
574 -- Create a new METAR object
575 -- @param args String that is either the METAR data string (one line) to parse
576 -- or the four-letter, upper-case <a href="http://en.wikipedia.org/wiki/International_Civil_Aviation_Organization_airport_code">ICAO code</a>
577 -- for the weather station. If weather station code is given, the current
578 -- METAR data for the station is downloaded from <a href="http://weather.noaa.gov">IWS</a>.
579 -- @return A table which is the metar object for METAR data given or
580 -- downloaded from IWS for the given weather station code
581 function new(args)
582         local attributes = {}
583         if args and type(args) == 'string' then
584                 if args:find('^[A-Z][A-Z][A-Z][A-Z]$') then
585                         attributes.station_id = args
586                 else
587                         attributes.metar_string = args
588                 end
589         end
590         return setmetatable(attributes, metatable)
591 end
592
593 -------------------------------------------------------------------------------
594 -- Return parsed METAR data as a table
595 -- @return Table containing the data parsed from the METAR data. 
596 -- If an error occurs, returns nil as the first return value.
597 -- The table may contain following entries
598 -- <ul>
599 -- <li><code>timestamp</code> <code>os.time</code> table which represents the timestamp when the METAR data was generated. Time is in UTC. Always included.</li>
600 -- <li><code>wind</code> A table representing the wind phenomena with the following keys. Optional, but usually included.</li>
601 --     <ul>
602 --     <li><code>direction</code> Wind direction as a value of the <a href="#WIND_DIRECTION">WIND_DIRECTION</a> table.</li>
603 --     <li><code>speed</code> Wind speed in knots.</li>
604 --     <li><code>gust</code> Gust speed in knots, optional.</li>
605 --     </ul>
606 -- <li><code>visibility</code> A list of tables that represent the visibility towards different directions. Tables contain the following keys. Optional, but if defined, at least one visibility entry exists in the list. Usually included.</li>
607 --     <ul>
608 --     <li><code>direction</code> Direction as a value of the <a href="#WIND_DIRECTION">WIND_DIRECTION</a> table. Optional.</li>
609 --     <li><code>distance</code> Visibility distance in meters</li>
610 --     </ul>
611 -- <li><code>vertical_visibility</code> Vertical visibility in meters. Optional.</li>
612 -- <li><code>runway_visual_range</code> A table representing runway visual range with the following keys. Optional.</li>
613 --     <ul>
614 --     <li><code>runway</code> Runway code</li>
615 --     <li><code>visibility</code> Visibility in meters</li>
616 --     </ul>
617 -- <li><code>clouds</code> A list of tables that represent clouds at different altitudes. Tables contain the following keys. Optional, but if defined, at least one cloud entry exists in the list. Usually included.</li>
618 --     <ul>
619 --     <li><code>coverage</code> Cloud coverate as a value of the <a href="#CLOUD_COVERAGE">CLOUD_COVERAGE</a> table.</li>
620 --     <li><code>altitude</code> Altitude of the clouds in feet.</li>
621 --     <li><code>type</code> Cloud type as a value of the <a href="#CLOUD_TYPE">CLOUD_TYPE</a> table.</li>
622 --     </ul>
623 -- <li><code>weather</code> A table representing weather conditions with the following keys. Optional, but usually included.</li>
624 --     <ul>
625 --     <li><code>intensity</code> Weather intensity as a value of the <a href="#WEATHER_INTENSITY">WEATHER_INTENSITY</a> table. Optional.</li></li>
626 --     <li><code>descriptor</code> Weather descriptor as a value of the <a href="#WEATHER_DESCRIPTOR">WEATHER_DESCRIPTOR</a> table. Optional.</li>
627 --     <li><code>phenomena</code> Weather phenomena as a value of the <a href="#WEATHER_PHENOMENA">WEATHER_PHENOMENA</a> table. Always included.</li>
628 --     </ul>
629 -- <li><code>sky</code> Sky status as a value of the <a href="#SKY_STATUS">SKY_STATUS</a> table. Always included.</li>
630 -- <li><code>temperature</code> Temperature in Celcius. Always  included.</li>
631 -- <li><code>dewpoint</code> Dewpoint temperature in Celcius. Always included.</li>
632 -- <li><code>pressure</code> Pressure in hectopascals. Optional, but usually included.</li>
633 --</ul>
634 -- @return Error string in case an error occurred and nil METAR table is returned
635 -- @usage var m = metar.new('EFHF')          -- Weather station Helsinki/Malmi
636 -- @usage var md = m:get_metar_data()        -- metardata.temperature contains the temperature etc.
637 -- @usage if md.temperature >= 30 then print("It's hot!") end
638 -- @usage if md.weather.intensity and md.weather.intensity == m.WEATHER_INTENSITY.HEAVY and md.weather.phenomena and md.weather.phenomena == m.WEATHER_PHENOMENA.RAIN then print("It's raining a lot!") end
639 function metatable.__index:get_metar_data()
640         if self.station_id then
641                 local metar_string, error_string = self:_fetch_metar_string(self.station_id)
642                 if not metar_string then
643                         return nil, error_string
644                 end
645                 self.metar_string = metar_string
646         end
647         if not self.metar_string then
648                 return nil, 'No METAR string or station id'
649         end
650         local metar_data = self:_parse_metar_string(self.metar_string)
651         if self.station_id then
652                 metar_data['station'] = self.station_id
653         end
654         metar_data['metar_string'] = self.metar_string
655         return metar_data
656 end
657
658 function metatable.__index:_parse_metar_string(metar_string)
659         local multi_entry = { visibility = 1, runway_visual_range = 1, clouds = 1 }
660         local metar_data  = {}
661         local token_data
662         -- tokenize METAR data
663         metar_string:gsub('%S+', function(token)
664                         local data = {}
665                         -- test each parser against the token and
666                         -- if it matches, pass to the handler
667                         for _, token_handler in pairs(metar_token_handlers) do
668                                 token:gsub(token_handler.pattern,
669                                         function(...)
670                                                 local value = token_handler.handler(...)
671                                                 table.insert(data, value)
672                                         end)
673                         end
674                         -- save results
675                         for _, data_entry in pairs(data) do
676                                 if data_entry then
677                                         for key, value in pairs(data_entry) do
678                                                 if multi_entry[key] then
679                                                         if not metar_data[key] then
680                                                                 metar_data[key] = {}
681                                                         end
682                                                         table.insert(metar_data[key], value)
683                                                 else
684                                                 metar_data[key] = value
685                                                 end
686                                         end
687                                 end
688                         end
689         end)
690         -- METAR data parsed, do some post processing
691         if metar_data.clouds then
692                 metar_data.sky = SKY_STATUS.CLOUDS
693         elseif metar_data.vertical_visibility then
694                 metar_data.sky = SKY_STATUS.OBSCURE
695         end
696         if not metar_data.sky then
697                 metar_data.sky = SKY_STATUS.UNKNOWN
698         end
699         return metar_data
700 end
701
702 -- Download and validate METAR string for a weather station
703 function metatable.__index:_fetch_metar_string(station_id)
704         local body, error_string = self:_download_metar_file(station_id)
705         if not body then
706                 return nil, error_string
707         end
708         local metar_string
709         local metar_pattern = string.format('^%s%%s+%%d%%d%%d%%d%%d%%dZ%%s+', self.station_id)
710         body:gsub('(.-)\n', function (line)
711                         if line:find(metar_pattern) then
712                                 metar_string = line
713                                 return
714                         end
715         end)
716         if metar_string then
717                 return metar_string
718         else
719                 return nil, 'Failed to find METAR string from input'
720         end
721 end
722
723 -- Download the METAR data for a weather station from IWS
724 function metatable.__index:_download_metar_file(station_id)
725         local metar_url = string.format('http://weather.noaa.gov/pub/data/observations/metar/stations/%s.TXT', station_id)
726         local body, status, _, status_string = socket_http.request(metar_url)
727         local error_details
728         if not status or (type(status) ~= 'number' and type(status) ~= 'string') then
729                 error_details = 'Unknown error'
730         elseif type(status) == 'string' then
731                 error_details = status
732         elseif type(status) == 'number' and status ~= 200 then
733                 error_details = status_string
734         end
735         if error_details then
736                 local error_string = string.format('Failed to fetch METAR data for station %s: %s', self.station_id, error_details)
737                 self:log_error(error_string)
738                 return nil, error_string
739         end
740         return body
741 end
742
743 function metatable.__index:log_error(error_string)
744         print(string.format('[metar] %s', error_string))
745 end