attack-barometer 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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')