attack-barometer 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. data/README.rdoc +51 -9
  2. data/VERSION.yml +1 -1
  3. data/bin/barometer +57 -7
  4. data/lib/barometer/base.rb +3 -0
  5. data/lib/barometer/data/sun.rb +10 -0
  6. data/lib/barometer/data/zone.rb +79 -188
  7. data/lib/barometer/data.rb +11 -6
  8. data/lib/barometer/formats/coordinates.rb +4 -1
  9. data/lib/barometer/formats/geocode.rb +9 -7
  10. data/lib/barometer/formats/icao.rb +2 -2
  11. data/lib/barometer/formats/weather_id.rb +2 -2
  12. data/lib/barometer/measurements/common.rb +113 -0
  13. data/lib/barometer/{data → measurements}/current.rb +17 -42
  14. data/lib/barometer/measurements/forecast.rb +62 -0
  15. data/lib/barometer/measurements/forecast_array.rb +72 -0
  16. data/lib/barometer/{data → measurements}/measurement.rb +57 -45
  17. data/lib/barometer/measurements/night.rb +27 -0
  18. data/lib/barometer/query.rb +55 -5
  19. data/lib/barometer/services.rb +3 -1
  20. data/lib/barometer/translations/zone_codes.yml +360 -0
  21. data/lib/barometer/weather.rb +5 -4
  22. data/lib/barometer/weather_services/google.rb +19 -35
  23. data/lib/barometer/weather_services/service.rb +113 -255
  24. data/lib/barometer/weather_services/weather_bug.rb +291 -2
  25. data/lib/barometer/weather_services/weather_dot_com.rb +45 -54
  26. data/lib/barometer/weather_services/wunderground.rb +83 -89
  27. data/lib/barometer/weather_services/yahoo.rb +44 -91
  28. data/lib/barometer/web_services/geocode.rb +1 -0
  29. data/lib/barometer/web_services/timezone.rb +40 -0
  30. data/lib/barometer/web_services/weather_id.rb +17 -2
  31. data/lib/barometer.rb +11 -0
  32. data/lib/demometer/demometer.rb +28 -0
  33. data/lib/demometer/public/css/master.css +259 -1
  34. data/lib/demometer/views/index.erb +2 -0
  35. data/lib/demometer/views/layout.erb +3 -2
  36. data/lib/demometer/views/measurement.erb +4 -1
  37. data/lib/demometer/views/readme.erb +116 -88
  38. data/spec/data/sun_spec.rb +53 -0
  39. data/spec/data/zone_spec.rb +330 -100
  40. data/spec/fixtures/formats/weather_id/ksfo.xml +1 -0
  41. data/spec/fixtures/services/weather_bug/90210_current.xml +1 -0
  42. data/spec/fixtures/services/weather_bug/90210_forecast.xml +1 -0
  43. data/spec/formats/weather_id_spec.rb +10 -5
  44. data/spec/measurements/common_spec.rb +352 -0
  45. data/spec/{data → measurements}/current_spec.rb +40 -103
  46. data/spec/measurements/forecast_array_spec.rb +165 -0
  47. data/spec/measurements/forecast_spec.rb +135 -0
  48. data/spec/{data → measurements}/measurement_spec.rb +86 -107
  49. data/spec/measurements/night_measurement_spec.rb +49 -0
  50. data/spec/query_spec.rb +12 -2
  51. data/spec/spec_helper.rb +28 -1
  52. data/spec/weather_services/google_spec.rb +27 -117
  53. data/spec/weather_services/services_spec.rb +49 -1024
  54. data/spec/weather_services/weather_bug_spec.rb +274 -0
  55. data/spec/weather_services/weather_dot_com_spec.rb +45 -125
  56. data/spec/weather_services/wunderground_spec.rb +42 -136
  57. data/spec/weather_services/yahoo_spec.rb +26 -116
  58. data/spec/weather_spec.rb +45 -45
  59. metadata +23 -11
  60. data/lib/barometer/data/forecast.rb +0 -84
  61. data/lib/barometer/data/night.rb +0 -69
  62. data/lib/barometer/extensions/graticule.rb +0 -51
  63. data/spec/data/forecast_spec.rb +0 -192
  64. data/spec/data/night_measurement_spec.rb +0 -136
@@ -1,6 +1,295 @@
1
1
  module Barometer
2
-
2
+ #
3
+ # = WeatherBug
4
+ # www.weatherbug.com
5
+ #
6
+ # - key required: YES (api_code)
7
+ # - registration required: YES
8
+ # - supported countries: US (by zipcode), International (by coordinates)
9
+ #
10
+ # === performs geo coding
11
+ # - city: YES
12
+ # - coordinates: PARTIAL (just for weather station)
13
+ #
14
+ # === time info
15
+ # - sun rise/set: YES
16
+ # - provides timezone: NO, but provides a timezone short code and utc offset
17
+ # - requires TZInfo: NO
18
+ #
19
+ # == resources
20
+ # - API: http://weather.weatherbug.com/corporate/products/API/help.aspx
21
+ #
22
+ # === Possible queries:
23
+ # - http://[API_Code].api.wxbug.net:80/getLiveWeatherRSS.aspx?ACode=[API_Code]&OutputType=1&UnitType=1&zipCode=90210
24
+ #
25
+ # where query can be:
26
+ # - zipcode (US) [5 digits only]
27
+ # - coordinates (International)
28
+ #
29
+ # = WeatherBug.com terms of use
30
+ # ???
31
+ #
32
+ # == notes
33
+ # - WeatherBug also supports queries using "citycode" and "stationID", but these
34
+ # are specific to WeatherBug and un-supported by Barometer
35
+ #
3
36
  class WeatherService::WeatherBug < WeatherService
37
+
38
+ @@api_code = nil
39
+
40
+ def self.keys=(keys)
41
+ raise ArgumentError unless keys.is_a?(Hash)
42
+ keys.each do |key, value|
43
+ @@api_code = value.to_s if key.to_s.downcase == "code"
44
+ end
45
+ end
46
+
47
+ #########################################################################
48
+ # PRIVATE
49
+ # If class methods could be private, the remaining methods would be.
50
+ #
51
+
52
+ def self._source_name; :weather_bug; end
53
+ def self._accepted_formats; [:short_zipcode, :coordinates]; end
54
+
55
+ def self._has_keys?; !@@api_code.nil?; end
56
+ def self._requires_keys?; true; end
57
+
58
+ def self._wet_icon_codes
59
+ codes = [5,6,8,9,11,12,14,15] + (18..22).to_a + [25] + (27..30).to_a +
60
+ [32,36] + (38..49).to_a + (52..63).to_a + (80..157).to_a +
61
+ (161..176).to_a
62
+ codes.collect {|c| c.to_s}
63
+ end
64
+ def self._sunny_icon_codes
65
+ codes = [0,2,3,4,7,26,31,64,65,75]
66
+ codes.collect {|c| c.to_s}
67
+ end
68
+
69
+ def self._build_extra(measurement, result, metric=true)
70
+ #raise ArgumentError unless measurement.is_a?(Data::Measurement)
71
+ #raise ArgumentError unless query.is_a?(Barometer::Query)
72
+
73
+ # use todays sun data for all future days
74
+ if measurement.forecast && measurement.current.sun
75
+ measurement.forecast.each do |forecast|
76
+ forecast.sun = measurement.current.sun
77
+ end
78
+ end
79
+
80
+ measurement
81
+ end
82
+
83
+ def self._parse_local_time(data)
84
+ Data::LocalTime.new(
85
+ data["aws:ob_date"]["aws:hour"]["hour_24"].to_i,
86
+ data["aws:ob_date"]["aws:minute"]["number"].to_i,
87
+ data["aws:ob_date"]["aws:second"]["number"].to_i
88
+ ) if data && data["aws:ob_date"]
89
+ end
90
+
91
+ def self._build_timezone(data)
92
+ if data && data["aws:ob_date"] && data["aws:ob_date"]["aws:time_zone"]
93
+ Data::Zone.new(data["aws:ob_date"]["aws:time_zone"]["abbrv"])
94
+ end
95
+ end
96
+
97
+ def self._build_current(data, metric=true)
98
+ raise ArgumentError unless data.is_a?(Hash)
99
+
100
+ current = Measurement::Current.new
101
+ # current.updated_at = Data::LocalDateTime.parse(data['observation_time']) if data['observation_time']
102
+ current.humidity = data['aws:humidity'].to_i
103
+ current.condition = data['aws:current_condition'] if data['aws:current_condition']
104
+ current.icon = data['aws:icon'].to_i.to_s if data['aws:icon']
105
+
106
+ current.temperature = Data::Temperature.new(metric)
107
+ current.temperature << data['aws:temp']
108
+
109
+ current.wind = Data::Speed.new(metric)
110
+ current.wind << data['aws:wind_speed'].to_f
111
+ current.wind.direction = data['aws:wind_direction']
112
+
113
+ current.pressure = Data::Pressure.new(metric)
114
+ current.pressure << data['aws:pressure']
115
+
116
+ current.dew_point = Data::Temperature.new(metric)
117
+ current.dew_point << data['aws:dew_point']
118
+
119
+ current.wind_chill = Data::Temperature.new(metric)
120
+ current.wind_chill << data['aws:feels_like']
121
+
122
+ current
123
+ end
124
+
125
+ def self._build_forecast(data, metric=true)
126
+ raise ArgumentError unless data.is_a?(Hash)
127
+ forecasts = Measurement::ForecastArray.new
128
+ # go through each forecast and create an instance
129
+ if data && data["aws:forecast"]
130
+ start_date = Date.parse(data['date'])
131
+ i = 0
132
+ data["aws:forecast"].each do |forecast|
133
+ forecast_measurement = Measurement::Forecast.new
134
+ icon_match = forecast['aws:image'].match(/cond(\d*)\.gif$/)
135
+ forecast_measurement.icon = icon_match[1].to_i.to_s if icon_match
136
+ forecast_measurement.date = start_date + i
137
+ forecast_measurement.condition = forecast['aws:short_prediction']
138
+
139
+ forecast_measurement.high = Data::Temperature.new(metric)
140
+ forecast_measurement.high << forecast['aws:high']
141
+
142
+ forecast_measurement.low = Data::Temperature.new(metric)
143
+ forecast_measurement.low << forecast['aws:low']
144
+
145
+ forecasts << forecast_measurement
146
+ i += 1
147
+ end
148
+ end
149
+ forecasts
150
+ end
151
+
152
+ def self._build_location(data, geo=nil)
153
+ raise ArgumentError unless data.is_a?(Hash)
154
+ raise ArgumentError unless (geo.nil? || geo.is_a?(Data::Geo))
155
+ location = Data::Location.new
156
+ # use the geocoded data if available, otherwise get data from result
157
+ if geo
158
+ location.city = geo.locality
159
+ location.state_code = geo.region
160
+ location.country = geo.country
161
+ location.country_code = geo.country_code
162
+ location.latitude = geo.latitude
163
+ location.longitude = geo.longitude
164
+ else
165
+ if data && data['aws:location']
166
+ location.city = data['aws:location']['aws:city']
167
+ location.state_code = data['aws:location']['aws:state']
168
+ location.zip_code = data['aws:location']['aws:zip']
169
+ end
170
+ end
171
+ location
172
+ end
173
+
174
+ def self._build_station(data)
175
+ raise ArgumentError unless data.is_a?(Hash)
176
+ station = Data::Location.new
177
+ station.id = data['aws:station_id']
178
+ station.name = data['aws:station']
179
+ station.city = data['aws:city_state'].split(',')[0].strip
180
+ station.state_code = data['aws:city_state'].split(',')[1].strip
181
+ station.country = data['aws:country']
182
+ station.zip_code = data['aws:station_zipcode']
183
+ station.latitude = data['aws:latitude']
184
+ station.longitude = data['aws:longitude']
185
+ station
186
+ end
187
+
188
+ def self._build_sun(data)
189
+ raise ArgumentError unless data.is_a?(Hash)
190
+ sun = nil
191
+ if data
192
+ if data['aws:sunrise']
193
+ rise = Data::LocalTime.new(
194
+ data['aws:sunrise']['aws:hour']['hour_24'].to_i,
195
+ data['aws:sunrise']['aws:minute']['number'].to_i,
196
+ data['aws:sunrise']['aws:second']['number'].to_i
197
+ )
198
+ end
199
+ if data['aws:sunset']
200
+ set = Data::LocalTime.new(
201
+ data['aws:sunset']['aws:hour']['hour_24'].to_i,
202
+ data['aws:sunset']['aws:minute']['number'].to_i,
203
+ data['aws:sunset']['aws:second']['number'].to_i
204
+ )
205
+ sun = Data::Sun.new(rise,set)
206
+ end
207
+ end
208
+ sun || Data::Sun.new
209
+ end
210
+
211
+ # override default _fetch behavior
212
+ # this service requires TWO seperate http requests (one for current
213
+ # and one for forecasted weather) ... combine the results
214
+ #
215
+ def self._fetch(query, metric=true)
216
+ result = []
217
+ result << _fetch_current(query,metric)
218
+ result << _fetch_forecast(query,metric)
219
+ result
220
+ end
221
+
222
+ # use HTTParty to get the current weather
223
+ #
224
+ def self._fetch_current(query, metric=true)
225
+ puts "fetch weatherbug current: #{query.q}" if Barometer::debug?
226
+
227
+ q = ( query.format.to_sym == :short_zipcode ?
228
+ { :zipCode => query.q } :
229
+ { :lat => query.q.split(',')[0], :long => query.q.split(',')[1] })
230
+
231
+ # httparty and the xml builder it uses miss some information
232
+ # 1st - get the raw response
233
+ # 2nd - manually get the missing information
234
+ # 3rd - let httparty build xml as normal
235
+ #
236
+ response = self.get(
237
+ "http://#{@@api_code}.api.wxbug.net/getLiveWeatherRSS.aspx",
238
+ :query => { :ACode => @@api_code,
239
+ :OutputType => "1", :UnitType => (metric ? '1' : '0')
240
+ }.merge(q),
241
+ :format => :plain,
242
+ :timeout => Barometer.timeout
243
+ )
244
+
245
+ # get icon
246
+ icon_match = response.match(/cond(\d*)\.gif/)
247
+ icon = icon_match[1] if icon_match
248
+
249
+ # get station zipcode
250
+ zip_match = response.match(/zipcode=\"(\d*)\"/)
251
+ zipcode = zip_match[1] if zip_match
252
+
253
+ # build xml
254
+ output = Crack::XML.parse(response)
255
+ output = output["aws:weather"]["aws:ob"]
256
+
257
+ # add missing data
258
+ output["aws:icon"] = icon
259
+ output["aws:station_zipcode"] = zipcode
260
+
261
+ output
262
+ end
263
+
264
+ # use HTTParty to get the current weather
265
+ #
266
+ def self._fetch_forecast(query, metric=true)
267
+ puts "fetch weatherbug forecast: #{query.q}" if Barometer::debug?
268
+
269
+ q = ( query.format.to_sym == :short_zipcode ?
270
+ { :zipCode => query.q } :
271
+ { :lat => query.q.split(',')[0], :long => query.q.split(',')[1] })
272
+
273
+ self.get(
274
+ "http://#{@@api_code}.api.wxbug.net/getForecastRSS.aspx",
275
+ :query => { :ACode => @@api_code,
276
+ :OutputType => "1", :UnitType => (metric ? '1' : '0')
277
+ }.merge(q),
278
+ :format => :xml,
279
+ :timeout => Barometer.timeout
280
+ )["aws:weather"]["aws:forecasts"]
281
+ end
282
+
283
+ # since we have two sets of data, override these calls to choose the
284
+ # right set of data
285
+ #
286
+ def self._current_result(data); data[0]; end
287
+ def self._forecast_result(data=nil); data[1]; end
288
+ def self._location_result(data=nil); data[1]; end
289
+ def self._station_result(data=nil); data[0]; end
290
+ def self._sun_result(data=nil); data[0]; end
291
+ def self._timezone_result(data=nil); data[0]; end
292
+ def self._time_result(data=nil); data[0]; end
293
+
4
294
  end
5
-
6
295
  end
@@ -43,9 +43,6 @@ module Barometer
43
43
  @@partner_key = nil
44
44
  @@license_key = nil
45
45
 
46
- def self.source_name; :weather_dot_com; end
47
- def self.accepted_formats; [:short_zipcode, :weather_id]; end
48
-
49
46
  def self.keys=(keys)
50
47
  raise ArgumentError unless keys.is_a?(Hash)
51
48
  keys.each do |key, value|
@@ -54,64 +51,56 @@ module Barometer
54
51
  end
55
52
  end
56
53
 
57
- def self.has_keys?; !@@partner_key.nil? && !@@license_key.nil?; end
58
- def self.requires_keys?; true; end
54
+ #########################################################################
55
+ # PRIVATE
56
+ # If class methods could be private, the remaining methods would be.
57
+ #
58
+
59
+ def self._source_name; :weather_dot_com; end
60
+ def self._accepted_formats; [:short_zipcode, :weather_id]; end
59
61
 
60
- def self.wet_icon_codes
62
+ def self._has_keys?; !@@partner_key.nil? && !@@license_key.nil?; end
63
+ def self._requires_keys?; true; end
64
+
65
+ def self._wet_icon_codes
61
66
  codes = (0..18).to_a + [35] + (37..43).to_a + (45..47).to_a
62
67
  codes.collect {|c| c.to_s}
63
68
  end
64
- def self.sunny_icon_codes
69
+ def self._sunny_icon_codes
65
70
  codes = [19, 22, 28, 30, 32, 34, 36]
66
71
  codes.collect {|c| c.to_s}
67
72
  end
68
73
 
69
- def self._measure(measurement, query, metric=true)
70
- raise ArgumentError unless measurement.is_a?(Data::Measurement)
71
- raise ArgumentError unless query.is_a?(Barometer::Query)
72
- measurement.source = self.source_name
73
-
74
- begin
75
- result = self.fetch(query.q, metric)
76
- rescue Timeout::Error => e
77
- return measurement
78
- end
79
-
80
- measurement.current = self.build_current(result, metric)
81
- measurement.forecast = self.build_forecast(result, metric)
82
- measurement.location = self.build_location(result, query.geo)
83
- measurement.current.sun = self.build_sun(result)
84
-
85
- # add links
86
- if result && result['lnks'] && result['lnks']['link']
87
- result['lnks']['link'].each do |link_hash|
88
- measurement.links[link_hash['t']] = link_hash['l']
74
+ # first try to match the zone code, otherwise use the zone offset
75
+ #
76
+ def self._build_timezone(data)
77
+ if data
78
+ if data['cc'] && data['cc']['lsup'] &&
79
+ (zone_match = data['cc']['lsup'].match(/ ([A-Z]{1,4})$/))
80
+ Data::Zone.new(zone_match[1])
81
+ elsif data['loc'] && data['loc']['zone']
82
+ Data::Zone.new(data['loc']['zone'].to_f)
89
83
  end
90
84
  end
91
-
92
- # set local time of measurement
93
- local_time = self.build_local_time(result)
94
- measurement.measured_at = local_time
95
- measurement.current.current_at = local_time
96
-
97
- measurement
98
85
  end
99
86
 
100
- # WARNING
101
- # this is a best guess method. the data provided for time conversions
102
- # leaves a lot to be desired. some time zones, offsets, local times are
103
- # out to lunch. eg. Tahiti (all times seem to be 30 min off), but there
104
- # is no way to determine this
105
- #
106
- # regardless of the above, this method will trust the data given to it
107
- #
108
- def self.build_local_time(data)
87
+ def self._parse_local_time(data)
109
88
  (data && data['loc']) ? Data::LocalTime.parse(data['loc']['tm']) : nil
110
89
  end
111
-
112
- def self.build_current(data, metric=true)
90
+
91
+ def self._build_links(data)
92
+ links = {}
93
+ if data && data['lnks'] && data['lnks']['link']
94
+ data['lnks']['link'].each do |link_hash|
95
+ links[link_hash['t']] = link_hash['l']
96
+ end
97
+ end
98
+ links
99
+ end
100
+
101
+ def self._build_current(data, metric=true)
113
102
  raise ArgumentError unless data.is_a?(Hash)
114
- current = Data::CurrentMeasurement.new
103
+ current = Measurement::Current.new
115
104
  if data
116
105
  if data['cc']
117
106
  current.updated_at = Data::LocalDateTime.parse(data['cc']['lsup'])
@@ -141,14 +130,14 @@ module Barometer
141
130
  current
142
131
  end
143
132
 
144
- def self.build_forecast(data, metric=true)
133
+ def self._build_forecast(data, metric=true)
145
134
  raise ArgumentError unless data.is_a?(Hash)
146
- forecasts = []
135
+ forecasts = Measurement::ForecastArray.new
147
136
 
148
137
  if data && data['dayf'] && data['dayf']['day']
149
138
  local_date = data['dayf']['lsup']
150
139
  data['dayf']['day'].each do |forecast|
151
- forecast_measurement = Data::ForecastMeasurement.new
140
+ forecast_measurement = Measurement::Forecast.new
152
141
  forecast_measurement.date = Date.parse(forecast['dt'])
153
142
 
154
143
  forecast_measurement.high = Data::Temperature.new(metric)
@@ -180,7 +169,7 @@ module Barometer
180
169
 
181
170
  elsif part['p'] == 'n'
182
171
  # add this to the NightMeasurement
183
- forecast_measurement.night = Data::NightMeasurement.new
172
+ forecast_measurement.night = Measurement::ForecastNight.new
184
173
  forecast_measurement.night.condition = part['t']
185
174
  forecast_measurement.night.icon = part['icon']
186
175
  forecast_measurement.night.pop = part['ppcp'].to_i
@@ -202,7 +191,7 @@ module Barometer
202
191
  forecasts
203
192
  end
204
193
 
205
- def self.build_location(data, geo=nil)
194
+ def self._build_location(data, geo=nil)
206
195
  raise ArgumentError unless data.is_a?(Hash)
207
196
  raise ArgumentError unless (geo.nil? || geo.is_a?(Data::Geo))
208
197
  location = Data::Location.new
@@ -224,7 +213,7 @@ module Barometer
224
213
  location
225
214
  end
226
215
 
227
- def self.build_sun(data)
216
+ def self._build_sun(data)
228
217
  raise ArgumentError unless data.is_a?(Hash)
229
218
  sun = nil
230
219
  if data
@@ -239,9 +228,11 @@ module Barometer
239
228
 
240
229
  # use HTTParty to get the current weather
241
230
  #
242
- def self.fetch(query, metric=true)
231
+ def self._fetch(query, metric=true)
232
+ return unless query
233
+ puts "fetch weather.com: #{query.q}" if Barometer::debug?
243
234
  self.get(
244
- "http://xoap.weather.com/weather/local/#{query}",
235
+ "http://xoap.weather.com/weather/local/#{query.q}",
245
236
  :query => { :par => @@partner_key, :key => @@license_key,
246
237
  :prod => "xoap", :link => "xoap", :cc => "*",
247
238
  :dayf => "5", :unit => (metric ? 'm' : 's')