Never return -0 as temperature
[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 © 2011 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         temperature = temperature:gsub('^-0+$', '0')
468         dewpoint = dewpoint:gsub('^M', '-')
469         dewpoint = dewpoint:gsub('^-0+$', '0')
470         return { temperature = tonumber(temperature), dewpoint = tonumber(dewpoint) }
471 end
472
473 local function parse_metar_pressure(unit, pressure)
474         if unit == 'A' then
475                 pressure = weatherlib.convert.pressure[weatherlib.PRESSURE_UNITS.INHG][weatherlib.PRESSURE_UNITS.HPA](tonumber(pressure) / 100)
476         else
477                 pressure = tonumber(pressure)
478         end
479         return { pressure = pressure }
480 end
481
482 -- Define patterns for each snippet of METAR data to parse
483 local metar_token_handlers = {
484         {
485                 pattern = '^(%d%d)(%d%d)(%d%d)Z$',
486                 handler = parse_metar_date,
487         },
488         {
489                 pattern = '^(%w%w%w)(%d+)G(%d+)([A-Z]+)$',
490                 handler = parse_metar_wind,
491         },
492         {
493                 pattern = '^(%w%w%w)(%d+)([A-Z]+)$',
494                 handler = parse_metar_wind,
495         },
496         {
497                 pattern = '^(%d%d%d%d)([A-Z]*)$',
498                 handler = parse_metar_visibility,
499         },
500         {
501                 pattern = '^(CAVOK)$',
502                 handler = parse_metar_visibility,
503         },
504         {
505                 pattern = '^R(%d+)/(%d+)$',
506                 handler = parse_metar_runway_visual_range,
507         },
508         {
509                 pattern = '^([A-Z][A-Z][A-Z])(%d%d%d)([A-Z]*)$',
510                 handler = parse_metar_clouds,
511         },
512         {
513                 pattern = '^VV([%d/][%d/][%d/])$',
514                 handler = parse_metar_vertical_visibility,
515         },
516         {
517                 pattern = '^([A-Z]+)$',
518                 handler = parse_metar_sky,
519         },
520         {
521                 pattern = '^([+-]*)([A-Z][A-Z])$',
522                 handler = parse_metar_weather,
523         },
524         {
525                 pattern = '^([+-]*)([A-Z][A-Z])([A-Z][A-Z])$',
526                 handler = parse_metar_weather,
527         },
528         {
529                 pattern = '^(VC)([A-Z][A-Z])*$',
530                 handler = parse_metar_weather,
531         },
532         {
533                 pattern = '^(VC)([A-Z][A-Z])([A-Z][A-Z])*$',
534                 handler = parse_metar_weather,
535         },
536         {
537                 pattern = '^(M*%d+)/(M*%d+)$',
538                 handler = parse_metar_temperature,
539         },
540         {
541                 pattern = '^([QA])(%d+)$',
542                 handler = parse_metar_pressure,
543         },
544 }
545
546 local function _get_previous_month(date)
547         if date.month == 1 then
548                 return 12
549         end
550         return date.month - 1
551 end
552
553 local function _get_last_day_of_month(date)
554         date.day  = 1
555         date.hour = 0
556         date.min  = 0
557         date.sec  = 0
558         date.wday = 0
559         date.yday = 0
560         local day_seconds = 60 * 60 * 24
561         local timestamp = os.time(date)
562         local test_date = date
563         -- count days forward until month changes
564         while(date.month == test_date.month) do
565                 timestamp = os.time(test_date) + day_seconds
566                 test_date = os.date('*t', timestamp)
567         end
568         -- rewind one day to get the last day of the month
569         timestamp = timestamp - day_seconds
570         return os.date('*t', timestamp).day
571 end
572
573 local metatable = { __index = {} }
574
575 -------------------------------------------------------------------------------
576 -- Create a new METAR object
577 -- @param args String that is either the METAR data string (one line) to parse
578 -- or the four-letter, upper-case <a href="http://en.wikipedia.org/wiki/International_Civil_Aviation_Organization_airport_code">ICAO code</a>
579 -- for the weather station. If weather station code is given, the current
580 -- METAR data for the station is downloaded from <a href="http://weather.noaa.gov">IWS</a>.
581 -- @return A table which is the metar object for METAR data given or
582 -- downloaded from IWS for the given weather station code
583 function new(args)
584         local attributes = {}
585         if args and type(args) == 'string' then
586                 if args:find('^[A-Z][A-Z][A-Z][A-Z]$') then
587                         attributes.station_id = args
588                 else
589                         attributes.metar_string = args
590                 end
591         end
592         return setmetatable(attributes, metatable)
593 end
594
595 -------------------------------------------------------------------------------
596 -- Return parsed METAR data as a table
597 -- @return Table containing the data parsed from the METAR data. 
598 -- If an error occurs, returns nil as the first return value.
599 -- The table may contain following entries
600 -- <ul>
601 -- <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>
602 -- <li><code>wind</code> A table representing the wind phenomena with the following keys. Optional, but usually included.</li>
603 --     <ul>
604 --     <li><code>direction</code> Wind direction as a value of the <a href="#WIND_DIRECTION">WIND_DIRECTION</a> table.</li>
605 --     <li><code>speed</code> Wind speed in knots.</li>
606 --     <li><code>gust</code> Gust speed in knots, optional.</li>
607 --     </ul>
608 -- <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>
609 --     <ul>
610 --     <li><code>direction</code> Direction as a value of the <a href="#WIND_DIRECTION">WIND_DIRECTION</a> table. Optional.</li>
611 --     <li><code>distance</code> Visibility distance in meters</li>
612 --     </ul>
613 -- <li><code>vertical_visibility</code> Vertical visibility in meters. Optional.</li>
614 -- <li><code>runway_visual_range</code> A table representing runway visual range with the following keys. Optional.</li>
615 --     <ul>
616 --     <li><code>runway</code> Runway code</li>
617 --     <li><code>visibility</code> Visibility in meters</li>
618 --     </ul>
619 -- <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>
620 --     <ul>
621 --     <li><code>coverage</code> Cloud coverate as a value of the <a href="#CLOUD_COVERAGE">CLOUD_COVERAGE</a> table.</li>
622 --     <li><code>altitude</code> Altitude of the clouds in feet.</li>
623 --     <li><code>type</code> Cloud type as a value of the <a href="#CLOUD_TYPE">CLOUD_TYPE</a> table.</li>
624 --     </ul>
625 -- <li><code>weather</code> A table representing weather conditions with the following keys. Optional, but usually included.</li>
626 --     <ul>
627 --     <li><code>intensity</code> Weather intensity as a value of the <a href="#WEATHER_INTENSITY">WEATHER_INTENSITY</a> table. Optional.</li></li>
628 --     <li><code>descriptor</code> Weather descriptor as a value of the <a href="#WEATHER_DESCRIPTOR">WEATHER_DESCRIPTOR</a> table. Optional.</li>
629 --     <li><code>phenomena</code> Weather phenomena as a value of the <a href="#WEATHER_PHENOMENA">WEATHER_PHENOMENA</a> table. Always included.</li>
630 --     </ul>
631 -- <li><code>sky</code> Sky status as a value of the <a href="#SKY_STATUS">SKY_STATUS</a> table. Always included.</li>
632 -- <li><code>temperature</code> Temperature in Celcius. Always  included.</li>
633 -- <li><code>dewpoint</code> Dewpoint temperature in Celcius. Always included.</li>
634 -- <li><code>pressure</code> Pressure in hectopascals. Optional, but usually included.</li>
635 --</ul>
636 -- @return Error string in case an error occurred and nil METAR table is returned
637 -- @usage var m = metar.new('EFHF')          -- Weather station Helsinki/Malmi
638 -- @usage var md = m:get_metar_data()        -- metardata.temperature contains the temperature etc.
639 -- @usage if md.temperature >= 30 then print("It's hot!") end
640 -- @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
641 function metatable.__index:get_metar_data()
642         if self.station_id then
643                 local metar_string, error_string = self:_fetch_metar_string(self.station_id)
644                 if not metar_string then
645                         return nil, error_string
646                 end
647                 self.metar_string = metar_string
648         end
649         if not self.metar_string then
650                 return nil, 'No METAR string or station id'
651         end
652         local metar_data = self:_parse_metar_string(self.metar_string)
653         if self.station_id then
654                 metar_data['station'] = self.station_id
655         end
656         metar_data['metar_string'] = self.metar_string
657         return metar_data
658 end
659
660 function metatable.__index:_parse_metar_string(metar_string)
661         local multi_entry = { visibility = 1, runway_visual_range = 1, clouds = 1 }
662         local metar_data  = {}
663         local token_data
664         -- tokenize METAR data
665         metar_string:gsub('%S+', function(token)
666                         local data = {}
667                         -- test each parser against the token and
668                         -- if it matches, pass to the handler
669                         for _, token_handler in pairs(metar_token_handlers) do
670                                 token:gsub(token_handler.pattern,
671                                         function(...)
672                                                 local value = token_handler.handler(...)
673                                                 table.insert(data, value)
674                                         end)
675                         end
676                         -- save results
677                         for _, data_entry in pairs(data) do
678                                 if data_entry then
679                                         for key, value in pairs(data_entry) do
680                                                 if multi_entry[key] then
681                                                         if not metar_data[key] then
682                                                                 metar_data[key] = {}
683                                                         end
684                                                         table.insert(metar_data[key], value)
685                                                 else
686                                                 metar_data[key] = value
687                                                 end
688                                         end
689                                 end
690                         end
691         end)
692         -- METAR data parsed, do some post processing
693         if metar_data.clouds then
694                 metar_data.sky = SKY_STATUS.CLOUDS
695         elseif metar_data.vertical_visibility then
696                 metar_data.sky = SKY_STATUS.OBSCURE
697         end
698         if not metar_data.sky then
699                 metar_data.sky = SKY_STATUS.UNKNOWN
700         end
701         return metar_data
702 end
703
704 -- Download and validate METAR string for a weather station
705 function metatable.__index:_fetch_metar_string(station_id)
706         local body, error_string = self:_download_metar_file(station_id)
707         if not body then
708                 return nil, error_string
709         end
710         local metar_string
711         local metar_pattern = string.format('^%s%%s+%%d%%d%%d%%d%%d%%dZ%%s+', self.station_id)
712         body:gsub('(.-)\n', function (line)
713                         if line:find(metar_pattern) then
714                                 metar_string = line
715                                 return
716                         end
717         end)
718         if metar_string then
719                 return metar_string
720         else
721                 return nil, 'Failed to find METAR string from input'
722         end
723 end
724
725 -- Download the METAR data for a weather station from IWS
726 function metatable.__index:_download_metar_file(station_id)
727         local metar_url = string.format('http://weather.noaa.gov/pub/data/observations/metar/stations/%s.TXT', station_id)
728         local body, status, _, status_string = socket_http.request(metar_url)
729         local error_details
730         if not status or (type(status) ~= 'number' and type(status) ~= 'string') then
731                 error_details = 'Unknown error'
732         elseif type(status) == 'string' then
733                 error_details = status
734         elseif type(status) == 'number' and status ~= 200 then
735                 error_details = status_string
736         end
737         if error_details then
738                 local error_string = string.format('Failed to fetch METAR data for station %s: %s', self.station_id, error_details)
739                 self:log_error(error_string)
740                 return nil, error_string
741         end
742         return body
743 end
744
745 function metatable.__index:log_error(error_string)
746         print(string.format('[metar] %s', error_string))
747 end