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.
- data/README.rdoc +51 -9
- data/VERSION.yml +2 -2
- data/bin/barometer +57 -7
- data/lib/barometer.rb +11 -0
- data/lib/barometer/base.rb +3 -0
- data/lib/barometer/data.rb +11 -6
- data/lib/barometer/data/sun.rb +10 -0
- data/lib/barometer/data/zone.rb +79 -188
- data/lib/barometer/formats/coordinates.rb +4 -1
- data/lib/barometer/formats/geocode.rb +9 -7
- data/lib/barometer/formats/icao.rb +2 -2
- data/lib/barometer/formats/weather_id.rb +2 -2
- data/lib/barometer/measurements/common.rb +113 -0
- data/lib/barometer/{data → measurements}/current.rb +17 -42
- data/lib/barometer/measurements/forecast.rb +62 -0
- data/lib/barometer/measurements/forecast_array.rb +72 -0
- data/lib/barometer/{data → measurements}/measurement.rb +57 -45
- data/lib/barometer/measurements/night.rb +27 -0
- data/lib/barometer/query.rb +55 -5
- data/lib/barometer/services.rb +3 -1
- data/lib/barometer/translations/icao_country_codes.yml +274 -1
- data/lib/barometer/translations/weather_country_codes.yml +189 -6
- data/lib/barometer/translations/zone_codes.yml +360 -0
- data/lib/barometer/weather.rb +5 -4
- data/lib/barometer/weather_services/google.rb +19 -35
- data/lib/barometer/weather_services/service.rb +113 -255
- data/lib/barometer/weather_services/weather_bug.rb +291 -2
- data/lib/barometer/weather_services/weather_dot_com.rb +45 -54
- data/lib/barometer/weather_services/wunderground.rb +83 -89
- data/lib/barometer/weather_services/yahoo.rb +44 -91
- data/lib/barometer/web_services/geocode.rb +1 -0
- data/lib/barometer/web_services/timezone.rb +40 -0
- data/lib/barometer/web_services/weather_id.rb +17 -2
- data/lib/demometer/demometer.rb +28 -0
- data/lib/demometer/public/css/master.css +259 -1
- data/lib/demometer/public/css/print.css +94 -0
- data/lib/demometer/public/css/syntax.css +64 -0
- data/lib/demometer/public/images/link-out.gif +0 -0
- data/lib/demometer/views/about.erb +10 -0
- data/lib/demometer/views/index.erb +2 -0
- data/lib/demometer/views/layout.erb +3 -2
- data/lib/demometer/views/measurement.erb +4 -1
- data/lib/demometer/views/readme.erb +116 -88
- data/spec/data/sun_spec.rb +53 -0
- data/spec/data/zone_spec.rb +330 -100
- data/spec/fixtures/formats/weather_id/ksfo.xml +1 -0
- data/spec/fixtures/services/weather_bug/90210_current.xml +1 -0
- data/spec/fixtures/services/weather_bug/90210_forecast.xml +1 -0
- data/spec/formats/weather_id_spec.rb +10 -5
- data/spec/measurements/common_spec.rb +352 -0
- data/spec/{data → measurements}/current_spec.rb +40 -103
- data/spec/measurements/forecast_array_spec.rb +165 -0
- data/spec/measurements/forecast_spec.rb +135 -0
- data/spec/{data → measurements}/measurement_spec.rb +86 -107
- data/spec/measurements/night_measurement_spec.rb +49 -0
- data/spec/query_spec.rb +12 -2
- data/spec/spec_helper.rb +28 -1
- data/spec/weather_services/google_spec.rb +27 -117
- data/spec/weather_services/services_spec.rb +49 -1024
- data/spec/weather_services/weather_bug_spec.rb +274 -0
- data/spec/weather_services/weather_dot_com_spec.rb +45 -125
- data/spec/weather_services/wunderground_spec.rb +42 -136
- data/spec/weather_services/yahoo_spec.rb +26 -116
- data/spec/weather_spec.rb +45 -45
- metadata +27 -11
- data/lib/barometer/data/forecast.rb +0 -84
- data/lib/barometer/data/night.rb +0 -69
- data/lib/barometer/extensions/graticule.rb +0 -51
- data/spec/data/forecast_spec.rb +0 -192
- 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
|
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 =
|
38
|
-
|
39
|
-
|
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
|
-
|
50
|
-
|
51
|
-
|
53
|
+
#########################################################################
|
54
|
+
# PRIVATE
|
55
|
+
# If class methods could be private, the remaining methods would be.
|
56
|
+
#
|
52
57
|
|
53
58
|
#
|
54
|
-
#
|
59
|
+
# REQUIRED
|
60
|
+
# re-defining these methods will be required
|
55
61
|
#
|
56
62
|
|
57
|
-
|
58
|
-
def self.
|
59
|
-
|
60
|
-
|
61
|
-
def self.
|
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
|
-
#
|
64
|
-
|
65
|
-
|
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
|
-
#
|
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.
|
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.
|
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.
|
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.
|
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
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
def self.
|
94
|
-
|
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
|
-
#
|
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
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
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
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
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
|
-
#
|
296
|
-
|
297
|
-
|
298
|
-
|
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.
|
317
|
-
|
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
|