barometer 0.5.0 → 0.6.1

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 (70) hide show
  1. data/README.rdoc +51 -9
  2. data/VERSION.yml +2 -2
  3. data/bin/barometer +57 -7
  4. data/lib/barometer.rb +11 -0
  5. data/lib/barometer/base.rb +3 -0
  6. data/lib/barometer/data.rb +11 -6
  7. data/lib/barometer/data/sun.rb +10 -0
  8. data/lib/barometer/data/zone.rb +79 -188
  9. data/lib/barometer/formats/coordinates.rb +4 -1
  10. data/lib/barometer/formats/geocode.rb +9 -7
  11. data/lib/barometer/formats/icao.rb +2 -2
  12. data/lib/barometer/formats/weather_id.rb +2 -2
  13. data/lib/barometer/measurements/common.rb +113 -0
  14. data/lib/barometer/{data → measurements}/current.rb +17 -42
  15. data/lib/barometer/measurements/forecast.rb +62 -0
  16. data/lib/barometer/measurements/forecast_array.rb +72 -0
  17. data/lib/barometer/{data → measurements}/measurement.rb +57 -45
  18. data/lib/barometer/measurements/night.rb +27 -0
  19. data/lib/barometer/query.rb +55 -5
  20. data/lib/barometer/services.rb +3 -1
  21. data/lib/barometer/translations/icao_country_codes.yml +274 -1
  22. data/lib/barometer/translations/weather_country_codes.yml +189 -6
  23. data/lib/barometer/translations/zone_codes.yml +360 -0
  24. data/lib/barometer/weather.rb +5 -4
  25. data/lib/barometer/weather_services/google.rb +19 -35
  26. data/lib/barometer/weather_services/service.rb +113 -255
  27. data/lib/barometer/weather_services/weather_bug.rb +291 -2
  28. data/lib/barometer/weather_services/weather_dot_com.rb +45 -54
  29. data/lib/barometer/weather_services/wunderground.rb +83 -89
  30. data/lib/barometer/weather_services/yahoo.rb +44 -91
  31. data/lib/barometer/web_services/geocode.rb +1 -0
  32. data/lib/barometer/web_services/timezone.rb +40 -0
  33. data/lib/barometer/web_services/weather_id.rb +17 -2
  34. data/lib/demometer/demometer.rb +28 -0
  35. data/lib/demometer/public/css/master.css +259 -1
  36. data/lib/demometer/public/css/print.css +94 -0
  37. data/lib/demometer/public/css/syntax.css +64 -0
  38. data/lib/demometer/public/images/link-out.gif +0 -0
  39. data/lib/demometer/views/about.erb +10 -0
  40. data/lib/demometer/views/index.erb +2 -0
  41. data/lib/demometer/views/layout.erb +3 -2
  42. data/lib/demometer/views/measurement.erb +4 -1
  43. data/lib/demometer/views/readme.erb +116 -88
  44. data/spec/data/sun_spec.rb +53 -0
  45. data/spec/data/zone_spec.rb +330 -100
  46. data/spec/fixtures/formats/weather_id/ksfo.xml +1 -0
  47. data/spec/fixtures/services/weather_bug/90210_current.xml +1 -0
  48. data/spec/fixtures/services/weather_bug/90210_forecast.xml +1 -0
  49. data/spec/formats/weather_id_spec.rb +10 -5
  50. data/spec/measurements/common_spec.rb +352 -0
  51. data/spec/{data → measurements}/current_spec.rb +40 -103
  52. data/spec/measurements/forecast_array_spec.rb +165 -0
  53. data/spec/measurements/forecast_spec.rb +135 -0
  54. data/spec/{data → measurements}/measurement_spec.rb +86 -107
  55. data/spec/measurements/night_measurement_spec.rb +49 -0
  56. data/spec/query_spec.rb +12 -2
  57. data/spec/spec_helper.rb +28 -1
  58. data/spec/weather_services/google_spec.rb +27 -117
  59. data/spec/weather_services/services_spec.rb +49 -1024
  60. data/spec/weather_services/weather_bug_spec.rb +274 -0
  61. data/spec/weather_services/weather_dot_com_spec.rb +45 -125
  62. data/spec/weather_services/wunderground_spec.rb +42 -136
  63. data/spec/weather_services/yahoo_spec.rb +26 -116
  64. data/spec/weather_spec.rb +45 -45
  65. metadata +27 -11
  66. data/lib/barometer/data/forecast.rb +0 -84
  67. data/lib/barometer/data/night.rb +0 -69
  68. data/lib/barometer/extensions/graticule.rb +0 -51
  69. data/spec/data/forecast_spec.rb +0 -192
  70. data/spec/data/night_measurement_spec.rb +0 -136
@@ -10,10 +10,11 @@ module Barometer
10
10
  # Service Class
11
11
  #
12
12
  # This is a base class for creating alternate weather api-consuming
13
- # drivers. Each driver inherits from this class.
13
+ # drivers. Each driver inherits from this class. This class creates
14
+ # some default behaviours, but they can easily be over-ridden.
14
15
  #
15
16
  # Basically, all a service is required to do is take a query
16
- # (ie "Paris") and return a complete Data::Measurement instance.
17
+ # (ie "Paris") and return a complete Barometer::Measurement instance.
17
18
  #
18
19
  class WeatherService
19
20
  # all service drivers will use the HTTParty gem
@@ -34,300 +35,157 @@ module Barometer
34
35
  def self.measure(query, metric=true)
35
36
  raise ArgumentError unless query.is_a?(Barometer::Query)
36
37
 
37
- measurement = Data::Measurement.new(self.source_name, metric)
38
- if self.meets_requirements?(query)
39
- converted_query = query.convert!(self.accepted_formats)
38
+ measurement = Barometer::Measurement.new(self._source_name, metric)
39
+ measurement.start_at = Time.now.utc
40
+ if self._meets_requirements?(query)
41
+ converted_query = query.convert!(self._accepted_formats)
40
42
  if converted_query
43
+ measurement.source = self._source_name
41
44
  measurement.query = converted_query.q
42
45
  measurement.format = converted_query.format
43
46
  measurement = self._measure(measurement, converted_query, metric)
44
47
  end
45
48
  end
49
+ measurement.end_at = Time.now.utc
46
50
  measurement
47
51
  end
48
52
 
49
- def self.meets_requirements?(query=nil)
50
- self.supports_country?(query) && (!self.requires_keys? || self.has_keys?)
51
- end
53
+ #########################################################################
54
+ # PRIVATE
55
+ # If class methods could be private, the remaining methods would be.
56
+ #
52
57
 
53
58
  #
54
- # NOTE: The following methods MUST be re-defined by each driver.
59
+ # REQUIRED
60
+ # re-defining these methods will be required
55
61
  #
56
62
 
57
- # STUB: define this method to actually retireve the source_name
58
- def self.source_name; raise NotImplementedError; end
59
-
60
- # STUB: define this method to indicate what query formats are accepted
61
- def self.accepted_formats; raise NotImplementedError; end
63
+ def self._source_name; raise NotImplementedError; end
64
+ def self._accepted_formats; raise NotImplementedError; end
65
+ def self._fetch(query=nil, metric=true); nil; end
66
+ def self._build_current(result=nil, metric=true); nil; end
67
+ def self._build_forecast(result=nil, metric=true); nil; end
62
68
 
63
- # STUB: define this method to measure the current & future weather
64
- def self._measure(measurement=nil, query=nil, metric=true)
65
- raise NotImplementedError
69
+ #
70
+ # PROBABLE
71
+ # re-defining these methods is probable though not a must
72
+ #
73
+
74
+ # data processing stubs
75
+ #
76
+ def self._build_location(result=nil, geo=nil); nil; end
77
+ def self._build_station(result=nil); Data::Location.new; end
78
+ def self._build_links(result=nil); {}; end
79
+ def self._build_sun(result=nil); Data::Sun.new; end
80
+ def self._build_timezone(result=nil); nil; end
81
+ def self._build_extra(measurement=nil, result=nil, metric=true); measurement; end
82
+ def self._build_local_time(measurement)
83
+ (measurement && measurement.timezone) ? Data::LocalTime.parse(measurement.timezone.now) : nil
66
84
  end
67
-
85
+
86
+ # given the result set, return the full_timezone or local time ...
87
+ # if not available return nil
88
+ def self._parse_full_timezone(result=nil); nil; end
89
+ def self._parse_local_time(result=nil); nil; end
90
+
91
+ # this returns an array of codes that indicate "wet"
92
+ def self._wet_icon_codes; nil; end
93
+ # this returns an array of codes that indicate "sunny"
94
+ def self._sunny_icon_codes; nil; end
95
+
68
96
  #
69
- # NOTE: The following methods can be re-defined by each driver. [OPTIONAL]
97
+ # OPTIONAL
98
+ # re-defining these methods will be optional
70
99
  #
71
100
 
72
101
  # STUB: define this method to check for the existance of API keys,
73
102
  # this method is NOT needed if requires_keys? returns false
74
- def self.has_keys?; raise NotImplementedError; end
103
+ def self._has_keys?; raise NotImplementedError; end
75
104
 
76
105
  # STUB: define this method to check for the existance of API keys,
77
106
  # this method is NOT needed if requires_keys? returns false
78
- def self.keys=(keys=nil); nil; end
107
+ def self._keys=(keys=nil); nil; end
79
108
 
80
109
  # DEFAULT: override this if you need to determine if the country is specified
81
- def self.supports_country?(query=nil); true; end
110
+ def self._supports_country?(query=nil); true; end
82
111
 
83
112
  # DEFAULT: override this if you need to determine if API keys are required
84
- def self.requires_keys?; false; end
113
+ def self._requires_keys?; false; end
85
114
 
115
+ # data accessors
116
+ # (see the wunderground driver for an example of overriding these)
86
117
  #
87
- # answer simple questions
88
- #
89
-
90
- #
91
- # WINDY?
92
- #
93
- def self.windy?(measurement, threshold=10, time_string=nil)
94
- local_time = Data::LocalDateTime.parse(time_string) if time_string
95
- raise ArgumentError unless measurement.is_a?(Data::Measurement)
96
- raise ArgumentError unless (threshold.is_a?(Fixnum) || threshold.is_a?(Float))
97
- raise ArgumentError unless (local_time.is_a?(Data::LocalDateTime) || local_time.nil?)
98
-
99
- measurement.current?(local_time) ?
100
- self.currently_windy?(measurement, threshold) :
101
- self.forecasted_windy?(measurement, threshold, local_time)
102
- end
103
-
104
- # cookie cutter answer, a driver can override this if they answer it differently
105
- # if a service doesn't support obtaining the wind value, it will be ignored
106
- def self.currently_windy?(measurement, threshold=10)
107
- raise ArgumentError unless measurement.is_a?(Data::Measurement)
108
- raise ArgumentError unless (threshold.is_a?(Fixnum) || threshold.is_a?(Float))
109
- return nil if (!measurement.current || !measurement.current.wind?)
110
- measurement.metric? ?
111
- measurement.current.wind.kph.to_f >= threshold.to_f :
112
- measurement.current.wind.mph.to_f >= threshold.to_f
113
- end
118
+ def self._current_result(data=nil); data; end
119
+ def self._forecast_result(data=nil); data; end
120
+ def self._location_result(data=nil); data; end
121
+ def self._station_result(data=nil); data; end
122
+ def self._links_result(data=nil); data; end
123
+ def self._sun_result(data=nil); data; end
124
+ def self._timezone_result(data=nil); data; end
125
+ def self._time_result(data=nil); data; end
114
126
 
115
- # no driver can currently answer this question, so it doesn't have any code
116
- def self.forecasted_windy?(measurement, threshold, time_string); nil; end
117
-
118
127
  #
119
- # WET?
128
+ # COMPLETE
129
+ # re-defining these methods should not be needed, as the behavior
130
+ # can be adjusted using methods above
120
131
  #
121
- def self.wet?(measurement, threshold=50, time_string=nil)
122
- local_time = Data::LocalDateTime.parse(time_string) if time_string
123
- raise ArgumentError unless measurement.is_a?(Data::Measurement)
124
- raise ArgumentError unless (threshold.is_a?(Fixnum) || threshold.is_a?(Float))
125
- raise ArgumentError unless (local_time.is_a?(Data::LocalDateTime) || local_time.nil?)
126
- measurement.current?(local_time) ?
127
- self.currently_wet?(measurement, threshold) :
128
- self.forecasted_wet?(measurement, threshold, local_time)
129
- end
130
-
131
- # cookie cutter answer
132
- def self.currently_wet?(measurement, threshold=50)
133
- raise ArgumentError unless measurement.is_a?(Data::Measurement)
134
- raise ArgumentError unless (threshold.is_a?(Fixnum) || threshold.is_a?(Float))
135
- return nil unless measurement.current
136
- self.currently_wet_by_icon?(measurement.current) ||
137
- self.currently_wet_by_dewpoint?(measurement) ||
138
- self.currently_wet_by_humidity?(measurement.current) ||
139
- self.currently_wet_by_pop?(measurement, threshold)
140
- end
141
-
142
- # cookie cutter answer
143
- def self.currently_wet_by_dewpoint?(measurement)
144
- raise ArgumentError unless measurement.is_a?(Data::Measurement)
145
- return nil if (!measurement.current || !measurement.current.temperature? ||
146
- !measurement.current.dew_point?)
147
- measurement.metric? ?
148
- measurement.current.temperature.c.to_f <= measurement.current.dew_point.c.to_f :
149
- measurement.current.temperature.f.to_f <= measurement.current.dew_point.f.to_f
150
- end
151
-
152
- # cookie cutter answer
153
- def self.currently_wet_by_humidity?(current_measurement)
154
- raise ArgumentError unless current_measurement.is_a?(Data::CurrentMeasurement)
155
- return nil unless current_measurement.humidity?
156
- current_measurement.humidity.to_i >= 99
157
- end
158
-
159
- # cookie cutter answer
160
- def self.currently_wet_by_pop?(measurement, threshold=50)
161
- raise ArgumentError unless measurement.is_a?(Data::Measurement)
162
- raise ArgumentError unless (threshold.is_a?(Fixnum) || threshold.is_a?(Float))
163
- return nil unless measurement.forecast
164
- # get todays forecast
165
- forecast_measurement = measurement.for
166
- return nil unless forecast_measurement
167
- forecast_measurement.pop.to_f >= threshold.to_f
168
- end
169
-
170
- # cookie cutter answer
171
- def self.forecasted_wet?(measurement, threshold=50, time_string=nil)
172
- local_time = Data::LocalDateTime.parse(time_string) if time_string
173
- raise ArgumentError unless measurement.is_a?(Data::Measurement)
174
- raise ArgumentError unless (threshold.is_a?(Fixnum) || threshold.is_a?(Float))
175
- raise ArgumentError unless (local_time.is_a?(Data::LocalDateTime) || local_time.nil?)
176
- return nil unless measurement.forecast
177
- forecast_measurement = measurement.for(local_time)
178
- return nil unless forecast_measurement
179
- self.forecasted_wet_by_icon?(forecast_measurement) ||
180
- self.forecasted_wet_by_pop?(forecast_measurement, threshold)
181
- end
182
-
183
- # cookie cutter answer
184
- def self.forecasted_wet_by_pop?(forecast_measurement, threshold=50)
185
- raise ArgumentError unless forecast_measurement.is_a?(Data::ForecastMeasurement)
186
- raise ArgumentError unless (threshold.is_a?(Fixnum) || threshold.is_a?(Float))
187
- return nil unless forecast_measurement.pop?
188
- forecast_measurement.pop.to_f >= threshold.to_f
189
- end
190
-
191
- def self.currently_wet_by_icon?(current_measurement)
192
- raise ArgumentError unless current_measurement.is_a?(Data::CurrentMeasurement)
193
- return nil unless self.wet_icon_codes
194
- return nil unless current_measurement.icon?
195
- current_measurement.icon.is_a?(String) ?
196
- self.wet_icon_codes.include?(current_measurement.icon.to_s.downcase) :
197
- self.wet_icon_codes.include?(current_measurement.icon)
198
- end
199
-
200
- def self.forecasted_wet_by_icon?(forecast_measurement)
201
- raise ArgumentError unless forecast_measurement.is_a?(Data::ForecastMeasurement)
202
- return nil unless self.wet_icon_codes
203
- return nil unless forecast_measurement.icon?
204
- forecast_measurement.icon.is_a?(String) ?
205
- self.wet_icon_codes.include?(forecast_measurement.icon.to_s.downcase) :
206
- self.wet_icon_codes.include?(forecast_measurement.icon)
207
- end
208
-
209
- # this returns an array of codes that indicate "wet"
210
- def self.wet_icon_codes; nil; end
211
132
 
133
+ # this is the generic measuring and data processing for each weather service
134
+ # driver. this method should be re-defined if the driver in question
135
+ # doesn't fit into "generic" (ie wunderground)
212
136
  #
213
- # DAY?
214
- #
215
- def self.day?(measurement, time_string=nil)
216
- local_datetime = Data::LocalDateTime.parse(time_string) if time_string
217
- raise ArgumentError unless measurement.is_a?(Data::Measurement)
218
- raise ArgumentError unless (local_datetime.is_a?(Data::LocalDateTime) || local_datetime.nil?)
219
-
220
- measurement.current?(local_datetime) ?
221
- self.currently_day?(measurement) :
222
- self.forecasted_day?(measurement, local_datetime)
223
- end
224
-
225
- def self.currently_day?(measurement)
226
- raise ArgumentError unless measurement.is_a?(Data::Measurement)
227
- return nil unless measurement.current && measurement.current.sun
228
- self.currently_after_sunrise?(measurement.current) &&
229
- self.currently_before_sunset?(measurement.current)
230
- end
231
-
232
- def self.currently_after_sunrise?(current_measurement)
233
- raise ArgumentError unless current_measurement.is_a?(Data::CurrentMeasurement)
234
- return nil unless current_measurement.current_at &&
235
- current_measurement.sun && current_measurement.sun.rise
236
- #Time.now.utc >= current_measurement.sun.rise
237
- current_measurement.current_at >= current_measurement.sun.rise
238
- end
239
-
240
- def self.currently_before_sunset?(current_measurement)
241
- raise ArgumentError unless current_measurement.is_a?(Data::CurrentMeasurement)
242
- return nil unless current_measurement.current_at &&
243
- current_measurement.sun && current_measurement.sun.set
244
- #Time.now.utc <= current_measurement.sun.set
245
- current_measurement.current_at <= current_measurement.sun.set
246
- end
247
-
248
- def self.forecasted_day?(measurement, time_string=nil)
249
- local_datetime = Data::LocalDateTime.parse(time_string) if time_string
250
- raise ArgumentError unless measurement.is_a?(Data::Measurement)
251
- raise ArgumentError unless (local_datetime.is_a?(Data::LocalDateTime) || local_datetime.nil?)
252
- return nil unless measurement.forecast
253
- forecast_measurement = measurement.for(local_datetime)
254
- return nil unless forecast_measurement
255
- self.forecasted_after_sunrise?(forecast_measurement, local_datetime) &&
256
- self.forecasted_before_sunset?(forecast_measurement, local_datetime)
257
- end
258
-
259
- def self.forecasted_after_sunrise?(forecast_measurement, time_string)
260
- local_datetime = Data::LocalDateTime.parse(time_string) if time_string
261
- raise ArgumentError unless forecast_measurement.is_a?(Data::ForecastMeasurement)
262
- raise ArgumentError unless (local_datetime.is_a?(Data::LocalDateTime) || local_datetime.nil?)
263
- return nil unless forecast_measurement.sun && forecast_measurement.sun.rise
264
- local_datetime >= forecast_measurement.sun.rise
265
- end
266
-
267
- def self.forecasted_before_sunset?(forecast_measurement, time_string)
268
- local_datetime = Data::LocalDateTime.parse(time_string) if time_string
269
- raise ArgumentError unless forecast_measurement.is_a?(Data::ForecastMeasurement)
270
- raise ArgumentError unless (local_datetime.is_a?(Data::LocalDateTime) || local_datetime.nil?)
271
- return nil unless forecast_measurement.sun && forecast_measurement.sun.set
272
- local_datetime <= forecast_measurement.sun.set
137
+ def self._measure(measurement, query, metric=true)
138
+ raise ArgumentError unless measurement.is_a?(Barometer::Measurement)
139
+ raise ArgumentError unless query.is_a?(Barometer::Query)
140
+
141
+ begin
142
+ result = _fetch(query, metric)
143
+ rescue Timeout::Error => e
144
+ return measurement
145
+ end
146
+
147
+ if result
148
+ measurement.current = _build_current(_current_result(result), metric)
149
+ measurement.forecast = _build_forecast(_forecast_result(result), metric)
150
+ measurement.location = _build_location(_location_result(result), query.geo)
151
+ measurement.station = _build_station(_station_result(result))
152
+ measurement.links = _build_links(_links_result(result))
153
+ measurement.current.sun = _build_sun(_sun_result(result)) if measurement.current
154
+ measurement.timezone = _timezone(_timezone_result(result), query, measurement.location)
155
+ if local_time = _local_time(_time_result(result), measurement)
156
+ measurement.measured_at = local_time
157
+ measurement.current.current_at = local_time
158
+ end
159
+ measurement = _build_extra(measurement, result, metric)
160
+ end
161
+
162
+ measurement
273
163
  end
274
164
 
165
+ # either get the timezone based on coords, or build it from the data
275
166
  #
276
- # SUNNY?
277
- #
278
- def self.sunny?(measurement, time_string=nil)
279
- local_time = Data::LocalDateTime.parse(time_string) if time_string
280
- raise ArgumentError unless measurement.is_a?(Data::Measurement)
281
- raise ArgumentError unless (local_time.is_a?(Data::LocalDateTime) || local_time.nil?)
282
- measurement.current?(local_time) ?
283
- self.currently_sunny?(measurement) :
284
- self.forecasted_sunny?(measurement, local_time)
285
- end
286
-
287
- # cookie cutter answer
288
- def self.currently_sunny?(measurement)
289
- raise ArgumentError unless measurement.is_a?(Data::Measurement)
290
- return nil unless measurement.current
291
- return false if self.currently_day?(measurement) == false
292
- self.currently_sunny_by_icon?(measurement.current)
167
+ def self._timezone(result=nil, query=nil, location=nil)
168
+ if full_timezone = _parse_full_timezone(result)
169
+ full_timezone
170
+ elsif query && query.timezone
171
+ query.timezone
172
+ elsif Barometer.enhance_timezone && location &&
173
+ location.latitude && location.longitude
174
+ WebService::Timezone.fetch(location.latitude, location.longitude)
175
+ else
176
+ _build_timezone(result)
177
+ end
293
178
  end
294
179
 
295
- # cookie cutter answer
296
- def self.forecasted_sunny?(measurement, time_string=nil)
297
- local_time = Data::LocalDateTime.parse(time_string) if time_string
298
- raise ArgumentError unless measurement.is_a?(Data::Measurement)
299
- raise ArgumentError unless (local_time.is_a?(Data::LocalDateTime) || local_time.nil?)
300
- return nil unless measurement.forecast
301
- return false if self.forecasted_day?(measurement, local_time) == false
302
- forecast_measurement = measurement.for(local_time)
303
- return nil unless forecast_measurement
304
- self.forecasted_sunny_by_icon?(forecast_measurement)
305
- end
306
-
307
- def self.currently_sunny_by_icon?(current_measurement)
308
- raise ArgumentError unless current_measurement.is_a?(Data::CurrentMeasurement)
309
- return nil unless self.sunny_icon_codes
310
- return nil unless current_measurement.icon?
311
- current_measurement.icon.is_a?(String) ?
312
- self.sunny_icon_codes.include?(current_measurement.icon.to_s.downcase) :
313
- self.sunny_icon_codes.include?(current_measurement.icon)
180
+ # return the current local time (as Data::LocalTime)
181
+ #
182
+ def self._local_time(result, measurement=nil)
183
+ _parse_local_time(result) || _build_local_time(measurement)
314
184
  end
315
185
 
316
- def self.forecasted_sunny_by_icon?(forecast_measurement)
317
- raise ArgumentError unless forecast_measurement.is_a?(Data::ForecastMeasurement)
318
- return nil unless self.sunny_icon_codes
319
- return nil unless forecast_measurement.icon?
320
- forecast_measurement.icon.is_a?(String) ?
321
- self.sunny_icon_codes.include?(forecast_measurement.icon.to_s.downcase) :
322
- self.sunny_icon_codes.include?(forecast_measurement.icon)
186
+ def self._meets_requirements?(query=nil)
187
+ self._supports_country?(query) && (!self._requires_keys? || self._has_keys?)
323
188
  end
324
-
325
- # this returns an array of codes that indicate "sunny"
326
- def self.sunny_icon_codes; nil; end
327
189
 
328
190
  end
329
- end
330
-
331
- # def key_name
332
- # # what variables holds the api key?
333
- # end
191
+ end
@@ -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