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
@@ -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