]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - data/rbot/plugins/weather.rb
Fri Jul 29 13:07:56 BST 2005 Tom Gilbert <tom@linuxbrit.co.uk>
[user/henk/code/ruby/rbot.git] / data / rbot / plugins / weather.rb
1 # This is nasty-ass. I hate writing parsers.
2 class Metar
3   attr_reader :decoded
4   attr_reader :input
5   attr_reader :date
6   attr_reader :nodata
7   def initialize(string)
8     str = nil
9     @nodata = false
10     string.each_line {|l|
11       if str == nil
12         # grab first line (date)
13         @date = l.chomp.strip
14         str = ""
15       else
16         if(str == "")
17           str = l.chomp.strip
18         else
19           str += " " + l.chomp.strip
20         end
21       end
22     }
23     if @date && @date =~ /^(\d+)\/(\d+)\/(\d+) (\d+):(\d+)$/
24       # 2002/02/26 05:00
25       @date = Time.gm($1, $2, $3, $4, $5, 0)
26     else
27       @date = Time.now
28     end
29     @input = str.chomp
30     @cloud_layers = 0
31     @cloud_coverage = {
32       'SKC' => '0',
33       'CLR' => '0',
34       'VV'  => '8/8',
35       'FEW' => '1/8 - 2/8',
36       'SCT' => '3/8 - 4/8',
37       'BKN' => '5/8 - 7/8',
38       'OVC' => '8/8'
39     }
40     @wind_dir_texts = [
41       'North',
42       'North/Northeast',
43       'Northeast',
44       'East/Northeast',
45       'East',
46       'East/Southeast',
47       'Southeast',
48       'South/Southeast',
49       'South',
50       'South/Southwest',
51       'Southwest',
52       'West/Southwest',
53       'West',
54       'West/Northwest',
55       'Northwest',
56       'North/Northwest',
57       'North'
58     ]
59     @wind_dir_texts_short = [
60       'N',
61       'N/NE',
62       'NE',
63       'E/NE',
64       'E',
65       'E/SE',
66       'SE',
67       'S/SE',
68       'S',
69       'S/SW',
70       'SW',
71       'W/SW',
72       'W',
73       'W/NW',
74       'NW',
75       'N/NW',
76       'N'
77     ]
78     @weather_array = {
79       'MI' => 'Mild ',
80       'PR' => 'Partial ',
81       'BC' => 'Patches ',
82       'DR' => 'Low Drifting ',
83       'BL' => 'Blowing ',
84       'SH' => 'Shower(s) ',
85       'TS' => 'Thunderstorm ',
86       'FZ' => 'Freezing',
87       'DZ' => 'Drizzle ',
88       'RA' => 'Rain ',
89       'SN' => 'Snow ',
90       'SG' => 'Snow Grains ',
91       'IC' => 'Ice Crystals ',
92       'PE' => 'Ice Pellets ',
93       'GR' => 'Hail ',
94       'GS' => 'Small Hail and/or Snow Pellets ',
95       'UP' => 'Unknown ',
96       'BR' => 'Mist ',
97       'FG' => 'Fog ',
98       'FU' => 'Smoke ',
99       'VA' => 'Volcanic Ash ',
100       'DU' => 'Widespread Dust ',
101       'SA' => 'Sand ',
102       'HZ' => 'Haze ',
103       'PY' => 'Spray',
104       'PO' => 'Well-Developed Dust/Sand Whirls ',
105       'SQ' => 'Squalls ',
106       'FC' => 'Funnel Cloud Tornado Waterspout ',
107       'SS' => 'Sandstorm/Duststorm '
108     }
109     @cloud_condition_array = {
110       'SKC' => 'clear',
111       'CLR' => 'clear',
112       'VV'  => 'vertical visibility',
113       'FEW' => 'a few',
114       'SCT' => 'scattered',
115       'BKN' => 'broken',
116       'OVC' => 'overcast'
117     }
118     @strings = {
119       'mm_inches'             => '%s mm (%s inches)',
120       'precip_a_trace'        => 'a trace',
121       'precip_there_was'      => 'There was %s of precipitation ',
122       'sky_str_format1'       => 'There were %s at a height of %s meters (%s feet)',
123       'sky_str_clear'         => 'The sky was clear',
124       'sky_str_format2'       => ', %s at a height of %s meter (%s feet) and %s at a height of %s meters (%s feet)',
125       'sky_str_format3'       => ' and %s at a height of %s meters (%s feet)',
126       'clouds'                => ' clouds',
127       'clouds_cb'             => ' cumulonimbus clouds',
128       'clouds_tcu'            => ' towering cumulus clouds',
129       'visibility_format'     => 'The visibility was %s kilometers (%s miles).',
130       'wind_str_format1'      => 'blowing at a speed of %s meters per second (%s miles per hour)',
131       'wind_str_format2'      => ', with gusts to %s meters per second (%s miles per hour),',
132       'wind_str_format3'      => ' from the %s',
133       'wind_str_calm'         => 'calm',
134       'precip_last_hour'      => 'in the last hour. ',
135       'precip_last_6_hours'   => 'in the last 3 to 6 hours. ',
136       'precip_last_24_hours'  => 'in the last 24 hours. ',
137       'precip_snow'           => 'There is %s mm (%s inches) of snow on the ground. ',
138       'temp_min_max_6_hours'  => 'The maximum and minimum temperatures over the last 6 hours were %s and %s degrees Celsius (%s and %s degrees Fahrenheit).',
139       'temp_max_6_hours'      => 'The maximum temperature over the last 6 hours was %s degrees Celsius (%s degrees Fahrenheit). ',
140       'temp_min_6_hours'      => 'The minimum temperature over the last 6 hours was %s degrees Celsius (%s degrees Fahrenheit). ',
141       'temp_min_max_24_hours' => 'The maximum and minimum temperatures over the last 24 hours were %s and %s degrees Celsius (%s and %s degrees Fahrenheit). ',
142       'light'                 => 'Light ',
143       'moderate'              => 'Moderate ',
144       'heavy'                 => 'Heavy ',
145       'mild'                  => 'Mild ',
146       'nearby'                => 'Nearby ',
147       'current_weather'       => 'Current weather is %s. ',
148       'pretty_print_metar'    => '%s on %s, the wind was %s at %s. The temperature was %s degrees Celsius (%s degrees Fahrenheit), and the pressure was %s hPa (%s inHg). The relative humidity was %s%%. %s %s %s %s %s'
149     }
150
151     parse
152   end
153
154   def store_speed(value, windunit, meterspersec, knots, milesperhour)
155     # Helper function to convert and store speed based on unit.
156     # &$meterspersec, &$knots and &$milesperhour are passed on
157     # reference
158     if (windunit == 'KT')
159       # The windspeed measured in knots:
160       @decoded[knots] = sprintf("%.2f", value)
161       # The windspeed measured in meters per second, rounded to one decimal place:
162       @decoded[meterspersec] = sprintf("%.2f", value.to_f * 0.51444)
163       # The windspeed measured in miles per hour, rounded to one decimal place: */
164       @decoded[milesperhour] = sprintf("%.2f", value.to_f * 1.1507695060844667)
165     elsif (windunit == 'MPS')
166       # The windspeed measured in meters per second:
167       @decoded[meterspersec] = sprintf("%.2f", value)
168       # The windspeed measured in knots, rounded to one decimal place:
169       @decoded[knots] = sprintf("%.2f", value.to_f / 0.51444)
170       #The windspeed measured in miles per hour, rounded to one decimal place:
171       @decoded[milesperhour] = sprintf("%.1f", value.to_f / 0.51444 * 1.1507695060844667)
172     elsif (windunit == 'KMH')
173       # The windspeed measured in kilometers per hour:
174       @decoded[meterspersec] = sprintf("%.1f", value.to_f * 1000 / 3600)
175       @decoded[knots] = sprintf("%.1f", value.to_f * 1000 / 3600 / 0.51444)
176       # The windspeed measured in miles per hour, rounded to one decimal place:
177       @decoded[milesperhour] = sprintf("%.1f", knots.to_f * 1.1507695060844667)
178     end
179   end
180   
181   def parse
182     @decoded = Hash.new
183     puts @input
184     @input.split(" ").each {|part|
185       if (part == 'METAR')
186         # Type of Report: METAR
187         @decoded['type'] = 'METAR'
188       elsif (part == 'SPECI')
189         # Type of Report: SPECI
190         @decoded['type'] = 'SPECI'
191       elsif (part == 'AUTO')
192         # Report Modifier: AUTO
193         @decoded['report_mod'] = 'AUTO'
194       elsif (part == 'NIL')
195         @nodata = true
196       elsif (part =~ /^\S{4}$/ && ! (@decoded.has_key?('station')))
197         # Station Identifier
198         @decoded['station'] = part
199       elsif (part =~ /([0-9]{2})([0-9]{2})([0-9]{2})Z/)
200         # ignore this bit, it's useless without month/year. some of these
201         # things are hideously out of date.
202         # now = Time.new
203         # time = Time.gm(now.year, now.month, $1, $2, $3, 0)
204         # Date and Time of Report
205         # @decoded['time'] = time
206       elsif (part == 'COR')
207         # Report Modifier: COR
208         @decoded['report_mod'] = 'COR'
209       elsif (part =~ /([0-9]{3}|VRB)([0-9]{2,3}).*(KT|MPS|KMH)/)
210         # Wind Group
211         windunit = $3
212         # now do ereg to get the actual values
213         part =~ /([0-9]{3}|VRB)([0-9]{2,3})((G[0-9]{2,3})?#{windunit})/
214         if ($1 == 'VRB')
215           @decoded['wind_deg'] = 'variable directions'
216           @decoded['wind_dir_text'] = 'variable directions'
217           @decoded['wind_dir_text_short'] = 'VAR'
218         else
219           @decoded['wind_deg'] = $1
220           @decoded['wind_dir_text'] = @wind_dir_texts[($1.to_i/22.5).round]
221           @decoded['wind_dir_text_short'] = @wind_dir_texts_short[($1.to_i/22.5).round]
222         end
223         store_speed($2, windunit,
224                     'wind_meters_per_second',
225                     'wind_knots',
226                     'wind_miles_per_hour')
227
228         if ($4 != nil)
229           # We have a report with information about the gust.
230           # First we have the gust measured in knots
231     if ($4 =~ /G([0-9]{2,3})/)
232           store_speed($1,windunit,
233                       'wind_gust_meters_per_second',
234                       'wind_gust_knots',
235                       'wind_gust_miles_per_hour')
236     end
237         end
238       elsif (part =~ /([0-9]{3})V([0-9]{3})/)
239         #  Variable wind-direction
240         @decoded['wind_var_beg'] = $1
241         @decoded['wind_var_end'] = $2
242       elsif (part == "9999")
243         # A strange value. When you look at other pages you see it
244         # interpreted like this (where I use > to signify 'Greater
245         # than'):
246         @decoded['visibility_miles'] = '>7';
247         @decoded['visibility_km']    = '>11.3';
248       elsif (part =~ /^([0-9]{4})$/)
249         # Visibility in meters (4 digits only)
250         # The visibility measured in kilometers, rounded to one decimal place.
251         @decoded['visibility_km'] = sprintf("%.1f", $1.to_i / 1000)
252         # The visibility measured in miles, rounded to one decimal place.
253         @decoded['visibility_miles'] = sprintf("%.1f", $1.to_i / 1000 / 1.609344)
254       elsif (part =~ /^[0-9]$/)
255         # Temp Visibility Group, single digit followed by space
256         @decoded['temp_visibility_miles'] = part
257       elsif (@decoded['temp_visibility_miles'] && (@decoded['temp_visibility_miles']+' '+part) =~ /^M?(([0-9]?)[ ]?([0-9])(\/?)([0-9]*))SM$/)
258         # Visibility Group
259         if ($4 == '/')
260           vis_miles = $2.to_i + $3.to_i/$5.to_i
261         else
262           vis_miles = $1.to_i;
263         end
264         if (@decoded['temp_visibility_miles'][0] == 'M')
265           # The visibility measured in miles, prefixed with < to indicate 'Less than'
266           @decoded['visibility_miles'] = '<' + sprintf("%.1f", vis_miles)
267           # The visibility measured in kilometers. The value is rounded
268           # to one decimal place, prefixed with < to indicate 'Less than' */
269           @decoded['visibility_km']    = '<' . sprintf("%.1f", vis_miles * 1.609344)
270         else
271           # The visibility measured in mile.s */
272           @decoded['visibility_miles'] = sprintf("%.1f", vis_miles)
273           # The visibility measured in kilometers, rounded to one decimal place.
274           @decoded['visibility_km']    = sprintf("%.1f", vis_miles * 1.609344)
275         end
276       elsif (part =~ /^(-|\+|VC|MI)?(TS|SH|FZ|BL|DR|BC|PR|RA|DZ|SN|SG|GR|GS|PE|IC|UP|BR|FG|FU|VA|DU|SA|HZ|PY|PO|SQ|FC|SS|DS)+$/)
277         # Current weather-group
278         @decoded['weather'] = '' unless @decoded.has_key?('weather')
279         if (part[0].chr == '-')
280           # A light phenomenon
281           @decoded['weather'] += @strings['light']
282           part = part[1,part.length]
283         elsif (part[0].chr == '+')
284           # A heavy phenomenon
285           @decoded['weather'] += @strings['heavy']
286           part = part[1,part.length]
287         elsif (part[0,2] == 'VC')
288           # Proximity Qualifier
289           @decoded['weather'] += @strings['nearby']
290           part = part[2,part.length]
291         elsif (part[0,2] == 'MI')
292           @decoded['weather'] += @strings['mild']
293           part = part[2,part.length]
294         else
295           # no intensity code => moderate phenomenon
296           @decoded['weather'] += @strings['moderate']
297         end
298         
299         while (part && bite = part[0,2]) do
300           # Now we take the first two letters and determine what they
301           # mean. We append this to the variable so that we gradually
302           # build up a phrase.
303
304           @decoded['weather'] += @weather_array[bite]
305           # Here we chop off the two first letters, so that we can take
306           # a new bite at top of the while-loop.
307           part = part[2,-1]
308         end
309       elsif (part =~ /(SKC|CLR)/)
310         # Cloud-layer-group.
311         # There can be up to three of these groups, so we store them as
312         # cloud_layer1, cloud_layer2 and cloud_layer3.
313         
314         @cloud_layers += 1;
315         # Again we have to translate the code-characters to a
316         # meaningful string.
317         @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_condition']  = @cloud_condition_array[$1]
318         @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_coverage'] = @cloud_coverage[$1]
319       elsif (part =~ /^(VV|FEW|SCT|BKN|OVC)([0-9]{3})(CB|TCU)?$/)
320         # We have found (another) a cloud-layer-group. There can be up
321         # to three of these groups, so we store them as cloud_layer1,
322         # cloud_layer2 and cloud_layer3.
323         @cloud_layers += 1;
324         # Again we have to translate the code-characters to a meaningful string.
325         if ($3 == 'CB')
326           # cumulonimbus (CB) clouds were observed. */
327           @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_condition'] =
328                       @cloud_condition_array[$1] + @strings['clouds_cb']
329         elsif ($3 == 'TCU')
330           # towering cumulus (TCU) clouds were observed.
331           @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_condition'] =
332                       @cloud_condition_array[$1] + @strings['clouds_tcu']
333         else
334           @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_condition'] =
335                       @cloud_condition_array[$1] + @strings['clouds']
336         end
337         @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_coverage'] = @cloud_coverage[$1]
338         @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_altitude_ft'] = $2.to_i * 100
339         @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_altitude_m']  = ($2.to_f * 30.48).round
340       elsif (part =~ /^T([0-9]{4})$/)
341         store_temp($1,'temp_c','temp_f')
342       elsif (part =~ /^T?(M?[0-9]{2})\/(M?[0-9\/]{1,2})?$/)
343         # Temperature/Dew Point Group
344         # The temperature and dew-point measured in Celsius.
345         @decoded['temp_c'] = sprintf("%d", $1.tr('M', '-'))
346         if $2 == "//" || !$2
347           @decoded['dew_c'] = 0
348         else
349           @decoded['dew_c'] = sprintf("%.1f", $2.tr('M', '-'))
350         end
351         # The temperature and dew-point measured in Fahrenheit, rounded to
352         # the nearest degree.
353         @decoded['temp_f'] = ((@decoded['temp_c'].to_f * 9 / 5) + 32).round
354         @decoded['dew_f']  = ((@decoded['dew_c'].to_f * 9 / 5) + 32).round
355       elsif(part =~ /A([0-9]{4})/)
356         # Altimeter
357         # The pressure measured in inHg
358         @decoded['altimeter_inhg'] = sprintf("%.2f", $1.to_i/100)
359         # The pressure measured in mmHg, hPa and atm
360         @decoded['altimeter_mmhg'] = sprintf("%.1f", $1.to_f * 0.254)
361         @decoded['altimeter_hpa']  = sprintf("%d", ($1.to_f * 0.33863881578947).to_i)
362         @decoded['altimeter_atm']  = sprintf("%.3f", $1.to_f * 3.3421052631579e-4)
363       elsif(part =~ /Q([0-9]{4})/)
364         # Altimeter
365         # This is strange, the specification doesnt say anything about
366         # the Qxxxx-form, but it's in the METARs.
367         # The pressure measured in hPa
368         @decoded['altimeter_hpa']  = sprintf("%d", $1.to_i)
369         # The pressure measured in mmHg, inHg and atm
370         @decoded['altimeter_mmhg'] = sprintf("%.1f", $1.to_f * 0.7500616827)
371         @decoded['altimeter_inhg'] = sprintf("%.2f", $1.to_f * 0.0295299875)
372         @decoded['altimeter_atm']  = sprintf("%.3f", $1.to_f * 9.869232667e-4)
373       elsif (part =~ /^T([0-9]{4})([0-9]{4})/)
374         # Temperature/Dew Point Group, coded to tenth of degree.
375         # The temperature and dew-point measured in Celsius.
376         store_temp($1,'temp_c','temp_f')
377         store_temp($2,'dew_c','dew_f')
378       elsif (part =~ /^1([0-9]{4}$)/)
379         # 6 hour maximum temperature Celsius, coded to tenth of degree
380         store_temp($1,'temp_max6h_c','temp_max6h_f')
381       elsif (part =~ /^2([0-9]{4}$)/)
382         # 6 hour minimum temperature Celsius, coded to tenth of degree
383         store_temp($1,'temp_min6h_c','temp_min6h_f')
384       elsif (part =~ /^4([0-9]{4})([0-9]{4})$/)
385         # 24 hour maximum and minimum temperature Celsius, coded to
386         # tenth of degree
387         store_temp($1,'temp_max24h_c','temp_max24h_f')
388         store_temp($2,'temp_min24h_c','temp_min24h_f')
389       elsif (part =~ /^P([0-9]{4})/)
390         # Precipitation during last hour in hundredths of an inch
391         # (store as inches)
392         @decoded['precip_in'] = sprintf("%.2f", $1.to_f/100)
393         @decoded['precip_mm'] = sprintf("%.2f", $1.to_f * 0.254)
394       elsif (part =~ /^6([0-9]{4})/)
395         # Precipitation during last 3 or 6 hours in hundredths of an
396         # inch  (store as inches)
397         @decoded['precip_6h_in'] = sprintf("%.2f", $1.to_f/100)
398         @decoded['precip_6h_mm'] = sprintf("%.2f", $1.to_f * 0.254)
399       elsif (part =~ /^7([0-9]{4})/)
400         # Precipitation during last 24 hours in hundredths of an inch
401         # (store as inches)
402         @decoded['precip_24h_in'] = sprintf("%.2f", $1.to_f/100)
403         @decoded['precip_24h_mm'] = sprintf("%.2f", $1.to_f * 0.254)
404       elsif(part =~ /^4\/([0-9]{3})/)
405         # Snow depth in inches
406         @decoded['snow_in'] = sprintf("%.2f", $1);
407         @decoded['snow_mm'] = sprintf("%.2f", $1.to_f * 25.4)
408       else
409         # If we couldn't match the group, we assume that it was a
410         # remark.
411         @decoded['remarks'] = '' unless @decoded.has_key?("remarks")
412         @decoded['remarks'] += ' ' + part;
413       end
414     }
415     
416     # Relative humidity
417     # p @decoded['dew_c'] # 11.0
418     # p @decoded['temp_c'] # 21.0
419     # => 56.1
420     @decoded['rel_humidity'] = sprintf("%.1f",100 * 
421       (6.11 * (10.0**(7.5 * @decoded['dew_c'].to_f / (237.7 + @decoded['dew_c'].to_f)))) / (6.11 * (10.0 ** (7.5 * @decoded['temp_c'].to_f / (237.7 + @decoded['temp_c'].to_f))))) if @decoded.has_key?('dew_c')
422   end
423
424   def store_temp(temp,temp_cname,temp_fname)
425     # Given a numerical temperature temp in Celsius, coded to tenth of
426     # degree, store in @decoded[temp_cname], convert to Fahrenheit
427     # and store in @decoded[temp_fname]
428     # Note: temp is converted to negative if temp > 100.0 (See
429     # Federal Meteorological Handbook for groups T, 1, 2 and 4)
430
431     # Temperature measured in Celsius, coded to tenth of degree
432     temp = temp.to_f/10
433     if (temp >100.0) 
434         # first digit = 1 means minus temperature
435         temp = -(temp - 100.0)
436     end
437     @decoded[temp_cname] = sprintf("%.1f", temp)
438     # The temperature in Fahrenheit.
439     @decoded[temp_fname] = sprintf("%.1f", (temp * 9 / 5) + 32)        
440   end
441
442     def pretty_print_precip(precip_mm, precip_in)
443       # Returns amount if $precip_mm > 0, otherwise "trace" (see Federal
444       # Meteorological Handbook No. 1 for code groups P, 6 and 7) used in
445       # several places, so standardized in one function.
446       if (precip_mm.to_i > 0)
447         amount = sprintf(@strings['mm_inches'], precip_mm, precip_in)
448       else
449         amount = @strings['a_trace']
450       end
451       return sprintf(@strings['precip_there_was'], amount)
452   end
453
454   def pretty_print
455     if @nodata
456       return "The weather stored for #{@decoded['station']} consists of the string 'NIL' :("
457     end
458
459     ["temp_c", "altimeter_hpa"].each {|key|
460       if !@decoded.has_key?(key)
461         return "The weather stored for #{@decoded['station']} could not be parsed (#{@input})"
462       end
463     }
464     
465     mins_old = ((Time.now - @date.to_i).to_f/60).round
466     if (mins_old <= 60)
467       weather_age = mins_old.to_s + " minutes ago,"
468     elsif (mins_old <= 60 * 25)
469       weather_age = (mins_old / 60).to_s + " hours, "
470       weather_age += (mins_old % 60).to_s + " minutes ago,"
471     else
472       # return "The weather stored for #{@decoded['station']} is hideously out of date :( (Last update #{@date})"
473       weather_age = "The weather stored for #{@decoded['station']} is hideously out of date :( here it is anyway:"
474     end
475     
476     if(@decoded.has_key?("cloud_layer1_altitude_ft"))
477       sky_str = sprintf(@strings['sky_str_format1'],
478                         @decoded["cloud_layer1_condition"],
479                         @decoded["cloud_layer1_altitude_m"],
480                         @decoded["cloud_layer1_altitude_ft"])
481     else
482       sky_str = @strings['sky_str_clear']
483     end
484
485     if(@decoded.has_key?("cloud_layer2_altitude_ft"))
486       if(@decoded.has_key?("cloud_layer3_altitude_ft"))
487         sky_str += sprintf(@strings['sky_str_format2'],
488                           @decoded["cloud_layer2_condition"],
489                           @decoded["cloud_layer2_altitude_m"],
490                           @decoded["cloud_layer2_altitude_ft"],
491                           @decoded["cloud_layer3_condition"],
492                           @decoded["cloud_layer3_altitude_m"],
493                           @decoded["cloud_layer3_altitude_ft"])
494       else
495         sky_str += sprintf(@strings['sky_str_format3'],
496                           @decoded["cloud_layer2_condition"],
497                           @decoded["cloud_layer2_altitude_m"],
498                           @decoded["cloud_layer2_altitude_ft"])
499       end
500     end
501     sky_str += "."
502
503     if(@decoded.has_key?("visibility_miles"))
504       visibility = sprintf(@strings['visibility_format'],
505                           @decoded["visibility_km"],
506                           @decoded["visibility_miles"])
507     else
508       visibility = ""
509     end
510
511     if (@decoded.has_key?("wind_meters_per_second") && @decoded["wind_meters_per_second"].to_i > 0)
512       wind_str = sprintf(@strings['wind_str_format1'],
513                         @decoded["wind_meters_per_second"],
514                         @decoded["wind_miles_per_hour"])
515       if (@decoded.has_key?("wind_gust_meters_per_second") && @decoded["wind_gust_meters_per_second"].to_i > 0)
516         wind_str += sprintf(@strings['wind_str_format2'],
517                             @decoded["wind_gust_meters_per_second"],
518                             @decoded["wind_gust_miles_per_hour"])
519       end
520       wind_str += sprintf(@strings['wind_str_format3'],
521                           @decoded["wind_dir_text"])
522     else
523       wind_str = @strings['wind_str_calm']
524     end
525
526     prec_str = ""
527     if (@decoded.has_key?("precip_in"))
528       prec_str += pretty_print_precip(@decoded["precip_mm"], @decoded["precip_in"]) + @strings['precip_last_hour']
529     end
530     if (@decoded.has_key?("precip_6h_in"))
531       prec_str += pretty_print_precip(@decoded["precip_6h_mm"], @decoded["precip_6h_in"]) + @strings['precip_last_6_hours']
532     end
533     if (@decoded.has_key?("precip_24h_in"))
534       prec_str += pretty_print_precip(@decoded["precip_24h_mm"], @decoded["precip_24h_in"]) + @strings['precip_last_24_hours']
535     end
536     if (@decoded.has_key?("snow_in"))
537       prec_str += sprintf(@strings['precip_snow'], @decoded["snow_mm"], @decoded["snow_in"])
538     end
539
540     temp_str = ""
541     if (@decoded.has_key?("temp_max6h_c") && @decoded.has_key?("temp_min6h_c"))
542       temp_str += sprintf(@strings['temp_min_max_6_hours'],
543                           @decoded["temp_max6h_c"],
544                           @decoded["temp_min6h_c"],
545                           @decoded["temp_max6h_f"],
546                           @decoded["temp_min6h_f"])
547     else
548       if (@decoded.has_key?("temp_max6h_c"))
549         temp_str += sprintf(@strings['temp_max_6_hours'],
550                             @decoded["temp_max6h_c"],
551                             @decoded["temp_max6h_f"])
552       end
553       if (@decoded.has_key?("temp_min6h_c"))
554         temp_str += sprintf(@strings['temp_max_6_hours'],
555                             @decoded["temp_min6h_c"],
556                             @decoded["temp_min6h_f"])
557       end
558     end
559     if (@decoded.has_key?("temp_max24h_c"))
560       temp_str += sprintf(@strings['temp_min_max_24_hours'],
561                           @decoded["temp_max24h_c"],
562                           @decoded["temp_min24h_c"],
563                           @decoded["temp_max24h_f"],
564                           @decoded["temp_min24h_f"])
565     end
566
567     if (@decoded.has_key?("weather"))
568       weather_str = sprintf(@strings['current_weather'], @decoded["weather"])
569     else
570       weather_str = ''
571     end
572
573     return sprintf(@strings['pretty_print_metar'],
574                   weather_age,
575                   @date,
576                   wind_str, @decoded["station"], @decoded["temp_c"],
577                   @decoded["temp_f"], @decoded["altimeter_hpa"],
578                   @decoded["altimeter_inhg"],
579                   @decoded["rel_humidity"], sky_str,
580                   visibility, weather_str, prec_str, temp_str).strip
581   end
582
583   def to_s
584     @input
585   end
586 end
587
588
589 class WeatherPlugin < Plugin
590   
591   def help(plugin, topic="")
592     "weather <ICAO> => display the current weather at the location specified by the ICAO code [Lookup your ICAO code at http://www.nws.noaa.gov/tg/siteloc.shtml - this will also store the ICAO against your nick, so you can later just say \"weather\", weather => display the current weather at the location you last asked for"
593   end
594
595   def get_metar(station)
596     station.upcase!
597     
598     result = @bot.httputil.get(URI.parse("http://weather.noaa.gov/pub/data/observations/metar/stations/#{station}.TXT"))
599     return nil unless result
600     return Metar.new(result)
601   end
602
603   
604   def initialize
605     super
606     # this plugin only wants to store strings
607     class << @registry
608       def store(val)
609         val
610       end
611       def restore(val)
612         val
613       end
614     end
615     @metar_cache = Hash.new
616   end
617   
618   def describe(m, where)
619     if @metar_cache.has_key?(where) &&
620        Time.now - @metar_cache[where].date < 3600
621       met = @metar_cache[where]
622     else
623       met = get_metar(where)
624     end
625     
626     if met
627       m.reply met.pretty_print
628       @metar_cache[where] = met
629     else
630       m.reply "couldn't find weather data for #{where}"
631     end
632   end
633
634   def weather(m, params)
635     if params[:where]
636       @registry[m.sourcenick] = params[:where]
637       describe(m,params[:where])
638     else
639       if @registry.has_key?(m.sourcenick)
640         where = @registry[m.sourcenick]
641         describe(m,where)
642       else
643         m.reply "I don't know where you are yet! Lookup your code at http://www.nws.noaa.gov/tg/siteloc.shtml and tell me 'weather <code>', then I'll know."
644       end
645     end
646   end
647 end
648 plugin = WeatherPlugin.new
649 plugin.map 'weather :where', :defaults => {:where => false}