barometer 0.5.0 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
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