forecast 0.0.2

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.
@@ -0,0 +1,204 @@
1
+ require 'net/http'
2
+ require 'rexml/document'
3
+ require 'open-uri'
4
+ require 'json'
5
+
6
+ class Forecast
7
+ module Adapter
8
+
9
+ @options = nil
10
+
11
+ module ClassMethods
12
+ def slug
13
+ self.name.split('::').last.gsub(/Adapter$/, '').gsub(/(.)([A-Z])/,'\1_\2').downcase
14
+ end
15
+ end
16
+
17
+ def self.included(base)
18
+ base.extend(ClassMethods)
19
+ end
20
+
21
+ def self.instance
22
+ provider = Forecast.config.provider
23
+ if provider.is_a?(Hash)
24
+ adapter_name = provider['adapter'].to_s
25
+ options = provider.clone
26
+ options.delete('adapter')
27
+ else
28
+ adapter_name = provider
29
+ options = {}
30
+ end
31
+ if adapter_name
32
+ adapter_classname = (adapter_name.to_s << "_adapter").split('_').collect!{ |w| w.capitalize }.join
33
+ adapter_class = Object.const_get('Forecast').const_get("Adapters").const_get(adapter_classname)
34
+ adapter_class.new(options)
35
+ else
36
+ puts 'no adapter provided'
37
+ end
38
+ end
39
+
40
+ def initialize(options = {})
41
+ @options = options
42
+ end
43
+
44
+ def options
45
+ @options
46
+ end
47
+
48
+ def config
49
+ Forecast.config.adapters[self.class.slug] || {}
50
+ end
51
+
52
+ protected
53
+
54
+ def get_json(url, params = {})
55
+ if params.keys.count > 0
56
+ query_string = URI.encode_www_form(params)
57
+ url = url + "?" + query_string
58
+ end
59
+ resp = Net::HTTP.get_response(URI.parse(url))
60
+ data = resp.body
61
+ result = JSON.parse(data)
62
+ if result && result['cod'] != "404"
63
+ return result
64
+ end
65
+ return nil
66
+ end
67
+
68
+ def get_doc(url, params = {})
69
+ if params.keys.count > 0
70
+ query_string = URI.encode_www_form(params)
71
+ url = url + "?" + query_string
72
+ end
73
+ xml_data = Net::HTTP.get_response(URI.parse(url)).body
74
+ doc = REXML::Document.new(xml_data)
75
+ return doc
76
+ end
77
+
78
+ def metric(fahrenheit)
79
+ ((fahrenheit - 32) / 1.800).round()
80
+ end
81
+
82
+ def get_temp(fahrenheit)
83
+ fahrenheit = fahrenheit.to_i.round()
84
+ Forecast.config.temp_scale.to_sym == :metric ? metric(fahrenheit) : fahrenheit
85
+ end
86
+
87
+ def similar_words(first, second)
88
+ similar_words = 0.0
89
+ first_words = first.downcase.split(/\s+/)
90
+ second_words = second.downcase.split(/\s+/)
91
+ first_words.each do |first_word|
92
+ second_words.each do |second_word|
93
+ similar = 0.0
94
+ if first_word == second_word
95
+ similar = 1.0
96
+ else
97
+ l1 = levenshtein(first_word, second_word)
98
+ if l1 > 0 && l1 < 3
99
+ similar = 1.0
100
+ end
101
+ end
102
+ similar_words+= similar
103
+ end
104
+ end
105
+ count = first_words.concat(second_words).uniq.length
106
+ similarity = similar_words / count
107
+ return similarity
108
+ end
109
+
110
+ def levenshtein(first, second)
111
+ matrix = [(0..first.length).to_a]
112
+ (1..second.length).each do |j|
113
+ matrix << [j] + [0] * (first.length)
114
+ end
115
+ (1..second.length).each do |i|
116
+ (1..first.length).each do |j|
117
+ if first[j-1] == second[i-1]
118
+ matrix[i][j] = matrix[i-1][j-1]
119
+ else
120
+ matrix[i][j] = [
121
+ matrix[i-1][j],
122
+ matrix[i][j-1],
123
+ matrix[i-1][j-1],
124
+ ].min + 1
125
+ end
126
+ end
127
+ end
128
+ return matrix.last.last
129
+ end
130
+
131
+ def get_condition_by_similarity(name)
132
+ conditions = Forecast.config.conditions
133
+ c = conditions.values.sort { |a, b| similar_words(name, a) <=> similar_words(name, b) }.reverse
134
+ if c.first && similar_words(name, c.first) > 0
135
+ return c.first
136
+ end
137
+ end
138
+
139
+ def get_condition_name(match)
140
+ if match == nil
141
+ return nil
142
+ end
143
+ conditions = Forecast.config.conditions
144
+ condition = "Unknown"
145
+ if conditions.keys.include?(match)
146
+ condition = conditions[match]
147
+ elsif conditions.values.include?(match)
148
+ condition = match
149
+ end
150
+ return condition
151
+ end
152
+
153
+ def match_adapter_condition(api_condition)
154
+ match = nil
155
+ conditions = config['conditions']
156
+ if conditions != nil
157
+ conditions.each do |key, value|
158
+ # puts "match key #{api_condition} -> #{key.to_s}, #{value.to_s}"
159
+ if is_numeric?(api_condition) && key.is_a?(String)
160
+ if key.include? ".."
161
+ range = key.split(/\.{2}/)
162
+ if api_condition.to_i >= range[0].to_i && api_condition.to_i <= range[1].to_i
163
+ match = value
164
+ end
165
+ end
166
+ elsif key.to_s == api_condition.to_s
167
+ match = value;
168
+ end
169
+ end
170
+ end
171
+ return match
172
+ end
173
+
174
+ def get_condition(api_conditions)
175
+ if !api_conditions.is_a?(Array)
176
+ api_conditions = [api_conditions]
177
+ end
178
+ condition = nil
179
+ api_conditions.each do |api_condition|
180
+ match = match_adapter_condition(api_condition)
181
+ if condition == nil
182
+ condition = get_condition_by_similarity(api_condition)
183
+ end
184
+ if condition
185
+ break
186
+ end
187
+ end
188
+ get_condition_name(condition)
189
+ end
190
+
191
+ private
192
+
193
+ def is_numeric?(s)
194
+ begin
195
+ Float(s)
196
+ rescue
197
+ false # not numeric
198
+ else
199
+ true # numeric
200
+ end
201
+ end
202
+
203
+ end
204
+ end
@@ -0,0 +1,95 @@
1
+ class Forecast
2
+ module Adapters
3
+ class OpenWeatherMapAdapter
4
+
5
+ include Forecast::Adapter
6
+
7
+ def current(latitude, longitude)
8
+ forecast = nil
9
+ result = get_json(api_url('weather', latitude, longitude))
10
+ if result
11
+ forecast = Forecast.new(latitude: latitude, longitude: longitude)
12
+ forecast.date = Time.at(result['dt']).to_datetime
13
+ forecast.temp = get_temp(kelvin_to_fahrenheit(result['main']['temp']))
14
+ result['weather'].each do |obj|
15
+ condition = get_condition(obj['description'])
16
+ if condition != nil
17
+ forecast.condition = condition
18
+ break
19
+ end
20
+ end
21
+ end
22
+ return forecast
23
+ end
24
+
25
+ def hourly(latitude, longitude)
26
+ forecasts = Forecast::Collection.new
27
+ result = get_json(api_url('forecast', latitude, longitude))
28
+ if result
29
+ result['list'].each do |item|
30
+ forecast = Forecast.new(latitude: latitude, longitude:longitude)
31
+ forecast.date = Time.at(item['dt']).to_datetime
32
+ forecast.temp = get_temp(kelvin_to_fahrenheit(item['main']['temp']))
33
+ item['weather'].each do |obj|
34
+ condition = get_condition([obj['description'], obj['id']])
35
+ if condition != nil
36
+ forecast.condition = condition
37
+ break
38
+ end
39
+ end
40
+ # forecast.temp_min = item['main']['temp_min']
41
+ # forecast.temp_max = item['main']['temp_max']
42
+ forecasts << forecast
43
+ end
44
+ end
45
+ return forecasts
46
+ end
47
+
48
+ def daily(latitude, longitude)
49
+ forecasts = Forecast::Collection.new
50
+ result = get_json(api_url('forecast/daily', latitude, longitude))
51
+ result['list'].each do |item|
52
+ forecast = Forecast.new(latitude: latitude, longitude:longitude)
53
+ forecast.date = Time.at(item['dt'])
54
+ forecast.temp_min = get_temp(kelvin_to_fahrenheit(item['temp']['min']))
55
+ forecast.temp_max = get_temp(kelvin_to_fahrenheit(item['temp']['max']))
56
+ forecast.temp = (forecast.temp_min + forecast.temp_max) / 2
57
+ item['weather'].each do |obj|
58
+ condition = get_condition(obj['description'])
59
+ if condition != nil
60
+ forecast.condition = condition
61
+ break
62
+ end
63
+ end
64
+ forecasts << forecast
65
+ end
66
+ return forecasts
67
+ end
68
+
69
+ private
70
+
71
+ def api_url(action, latitude, longitude)
72
+ url = "http://api.openweathermap.org/data/2.5/#{action}"
73
+ params = {
74
+ lat: latitude,
75
+ lon: longitude
76
+ }
77
+ if options[:api_key]
78
+ params['APPID'] = options[:api_key]
79
+ end
80
+ query_string = URI.encode_www_form(params)
81
+ return url + "?" + query_string
82
+ end
83
+
84
+ def kelvin_to_fahrenheit(kelvin)
85
+ return ((kelvin - 273.15) * 1.8000 + 32).round
86
+ end
87
+
88
+ end
89
+ end
90
+ end
91
+
92
+
93
+
94
+
95
+
@@ -0,0 +1,18 @@
1
+ forecast:
2
+ adapters:
3
+ open_weather_map:
4
+ conditions:
5
+ 200..210: Windy
6
+ 211: Storm
7
+ 212..299: 'Heavy Storm'
8
+ 300..399: 'Light Rain'
9
+ 500: 'Light Rain'
10
+ 501: 'Rain'
11
+ 502..599: 'Heavy Rain'
12
+ 600: 'Light Snow'
13
+ 601: 'Snow'
14
+ 602..699: 'Heavy Snow'
15
+ 800: 'Clear'
16
+ 801: 'Partly Cloudy'
17
+ 802..803: 210
18
+ 804..899: 'Mostly Cloudy'
@@ -0,0 +1,70 @@
1
+ class Forecast
2
+ module Adapters
3
+ class WundergroundAdapter
4
+
5
+ include Forecast::Adapter
6
+
7
+ def current(latitude, longitude)
8
+ forecast = nil
9
+ result = get_json(api_url('conditions', latitude, longitude))
10
+ if result
11
+ item = result['current_observation']
12
+ forecast = Forecast.new(latitude: latitude, longitude: longitude)
13
+ forecast.date = Time.rfc822(item['observation_time_rfc822'])
14
+ forecast.temp = get_temp(item['temp_f'])
15
+ forecast.condition = get_condition([item['weather']])
16
+ forecast.orig_condition = item['weather']
17
+ end
18
+ return forecast
19
+ end
20
+
21
+ def hourly(latitude, longitude)
22
+ forecasts = Forecast::Collection.new
23
+ result = get_json(api_url('hourly', latitude, longitude))
24
+ if result
25
+ items = result['hourly_forecast']
26
+ items.each do |item|
27
+ forecast = Forecast.new(latitude: latitude, longitude:longitude)
28
+ forecast.date = Time.at(item['FCTTIME']['epoch'].to_i).to_datetime
29
+ forecast.temp = get_temp(item['temp']['english'])
30
+ forecast.condition = get_condition([item['condition']])
31
+ forecast.orig_condition = item['condition']
32
+ forecasts << forecast
33
+ end
34
+ end
35
+ return forecasts
36
+ end
37
+
38
+ def daily(latitude, longitude)
39
+ forecasts = Forecast::Collection.new
40
+ result = get_json(api_url('forecast', latitude, longitude))
41
+ if result
42
+ items = result['forecast']['simpleforecast']['forecastday']
43
+ items.each do |item|
44
+ forecast = Forecast.new(latitude: latitude, longitude:longitude)
45
+ forecast.date = Time.at(item['date']['epoch'].to_i).to_datetime
46
+ forecast.temp_min = get_temp(item['low']['fahrenheit'])
47
+ forecast.temp_max = get_temp(item['high']['fahrenheit'])
48
+ forecast.temp = (forecast.temp_min + forecast.temp_max) / 2
49
+ forecast.condition = get_condition([item['conditions']])
50
+ forecast.orig_condition = item['conditions']
51
+ forecasts << forecast
52
+ end
53
+ end
54
+ return forecasts
55
+ end
56
+
57
+ private
58
+
59
+ def api_url(action, latitude, longitude)
60
+ url = "http://api.wunderground.com/api/#{options['api_key']}/#{action}/q/#{latitude},#{longitude}.json"
61
+ end
62
+
63
+ end
64
+ end
65
+ end
66
+
67
+
68
+
69
+
70
+
@@ -0,0 +1,90 @@
1
+ class Forecast
2
+ module Adapters
3
+ class YahooAdapter
4
+
5
+ include Forecast::Adapter
6
+
7
+ URL_YQL = 'http://query.yahooapis.com/v1/public/yql'
8
+ URL_RSS = 'http://weather.yahooapis.com/forecastrss'
9
+
10
+ def current(latitude, longitude)
11
+ forecast = nil
12
+ doc = get_rss(latitude, longitude)
13
+ if doc
14
+ forecast = Forecast.new
15
+ doc.elements.each('rss/channel/item/yweather:condition') do |elem|
16
+ elem.attributes.each() do |attr|
17
+ name = attr[0]
18
+ value = attr[1]
19
+ case name
20
+ when 'date'
21
+ forecast.date = DateTime.parse(value)
22
+ when 'temp'
23
+ forecast.temp = value.to_i
24
+ when 'text'
25
+ forecast.condition = get_condition(value)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ return forecast
31
+ end
32
+
33
+ def hourly(latitude, longitude)
34
+ # not supported
35
+ return []
36
+ end
37
+
38
+ def daily(latitude, longitude)
39
+ doc = get_rss(latitude, longitude)
40
+ forecasts = Forecast::Collection.new
41
+ if doc
42
+ doc.elements.each('rss/channel/item/yweather:forecast') do |elem|
43
+ forecast = Forecast.new
44
+ elem.attributes.each() do |attr|
45
+ puts 'attr' + attr.to_s
46
+ name = attr[0]
47
+ value = attr[1]
48
+ case name
49
+ when 'date'
50
+ forecast.date = DateTime.parse(value)
51
+ when 'low'
52
+ forecast.temp_min = get_temp(value)
53
+ when 'high'
54
+ forecast.temp_max = get_temp(value)
55
+ when 'text'
56
+ forecast.condition = get_condition(value)
57
+ end
58
+ end
59
+ forecast.temp = (forecast.temp_min + forecast.temp_max) / 2
60
+ forecasts << forecast
61
+ end
62
+ end
63
+ return forecasts
64
+ end
65
+
66
+ private
67
+
68
+ def get_woeid(latitude, longitude)
69
+ woeid = nil
70
+ query = "SELECT * FROM geo.placefinder WHERE text='#{latitude}, #{longitude}' and gflags='R'"
71
+ url = URL_YQL + "?q=" + URI::encode(query)
72
+ doc = get_doc(url)
73
+ doc.elements.each('query/results/Result/woeid') do |elem|
74
+ woeid = elem.text
75
+ end
76
+ return woeid
77
+ end
78
+
79
+ def get_rss(latitude, longitude)
80
+ woeid = get_woeid(latitude, longitude)
81
+ if woeid
82
+ doc = get_doc(URL_RSS, {w: woeid})
83
+ return doc
84
+ end
85
+ return nil
86
+ end
87
+
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,7 @@
1
+ class Forecast
2
+ class Cache
3
+ def initialize
4
+
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,28 @@
1
+ class Forecast
2
+ class Collection < Array
3
+ def select_date(date)
4
+ result = nil
5
+ date_forecasts = self.select do |obj|
6
+ obj.date.to_date == date.to_date
7
+ end
8
+ if date_forecasts.length == 0
9
+ return nil
10
+ else
11
+ hour_forecasts = date_forecasts.select do |obj|
12
+ obj.date.hour == obj.date.hour
13
+ end
14
+ if hour_forecasts.length > 0
15
+ return hour_forecasts.first
16
+ end
17
+ return date_forecasts.first
18
+ end
19
+ return nil
20
+
21
+ end
22
+
23
+ private
24
+ def seconds_between(date1, date2)
25
+ ((Time.parse(date1.to_s) - Time.parse(date2.to_s)) / 3600).abs
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,12 @@
1
+ forecast:
2
+ conditions:
3
+ 100: 'Clear'
4
+ 200: 'Partly Cloudy'
5
+ 210: 'Cloudy'
6
+ 220: 'Mostly Cloudy'
7
+ 300: 'Light Rain'
8
+ 310: 'Rain'
9
+ 320: 'Heavy Rain'
10
+ 400: 'Light Snow'
11
+ 410: 'Snow'
12
+ 500: 'Thunderstorm'
@@ -0,0 +1,74 @@
1
+ class Forecast
2
+ class Config
3
+
4
+ attr_accessor :adapters, :provider, :temp_scale, :conditions, :cache, :themes, :theme
5
+
6
+ def initialize
7
+
8
+ @config_file = File.dirname(File.dirname(File.dirname(__FILE__))) + "/config/forecast.yml"
9
+
10
+ self.load(File.dirname(__FILE__) + '/**/*.yml')
11
+ self.load(@config_file)
12
+
13
+ def theme
14
+ if @theme != nil
15
+ if @theme.is_a?(Hash)
16
+ return @theme
17
+ end
18
+ if themes[@theme] != nil
19
+ return themes[@theme]
20
+ end
21
+ end
22
+ return @theme
23
+ end
24
+
25
+ end
26
+
27
+ def load(pattern)
28
+ Dir.glob(pattern).sort{ |a, b| a.split(/\//).length <=> b.split(/\//).length}.reverse.each do |f|
29
+ obj = YAML.load_file(f)
30
+ if obj['forecast'] != nil
31
+ obj['forecast'].each do |k, v|
32
+ if respond_to?("#{k}")
33
+ o = send("#{k}")
34
+ if o.is_a?(Hash)
35
+ v = deep_merge(o, v)
36
+ end
37
+ end
38
+ send("#{k}=", v) if respond_to?("#{k}=")
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def deep_merge(hash, other_hash, &block)
47
+ other_hash.each_pair do |k,v|
48
+ tv = hash[k]
49
+ if tv.is_a?(Hash) && v.is_a?(Hash)
50
+ hash[k] = deep_merge(tv, v, &block)
51
+ else
52
+ hash[k] = block && tv ? block.call(k, tv, v) : v
53
+ end
54
+ end
55
+ hash
56
+ end
57
+
58
+ end
59
+
60
+ def self.config
61
+ @@config ||= Config.new
62
+ end
63
+
64
+ def self.configure
65
+ yield self.config
66
+ # puts 'configured'
67
+ # if self.config.config_file != nil
68
+ # puts 'load config from file'
69
+ # self.config.load(@config_file)
70
+ # end
71
+ end
72
+
73
+
74
+ end
@@ -0,0 +1,6 @@
1
+ forecast:
2
+ provider:
3
+ adapter: open_weather_map
4
+ theme: weather_icons
5
+ temp_scale: fahrenheit
6
+ cache: false
@@ -0,0 +1,71 @@
1
+ class Forecast
2
+ module Model
3
+
4
+ def initialize(object_attribute_hash = {})
5
+ object_attribute_hash.map do |(k, v)|
6
+ send("#{k}=", v) if respond_to?("#{k}=")
7
+ end
8
+ end
9
+
10
+ def icon
11
+ if Forecast.config.theme.is_a? Hash
12
+ icon = Forecast.config.theme[self.condition]
13
+ return icon unless icon == nil
14
+ end
15
+ return slugify(self.condition)
16
+ end
17
+
18
+ def self.included(base)
19
+ base.extend(ClassMethods)
20
+ end
21
+
22
+ def as_json options = {}
23
+ serialized = Hash.new
24
+ if self.class.attributes != nil
25
+ self.class.attributes.each do |attribute|
26
+ serialized[attribute] = self.public_send attribute
27
+ end
28
+ end
29
+ serialized
30
+ end
31
+
32
+ def to_json *a
33
+ as_json.to_json a
34
+ end
35
+
36
+ def from_json(json)
37
+ self.class.attributes.each do |attribute|
38
+ writer_m = "#{attribute}="
39
+ value = json[attribute.to_s]
40
+ if attribute == :date
41
+ value = DateTime.parse(value)
42
+ end
43
+ send(writer_m, value) if respond_to?(writer_m)
44
+ end
45
+ end
46
+
47
+ # end
48
+
49
+ private
50
+
51
+ def slugify(string)
52
+ string.downcase.strip.gsub(' ', '-').gsub(/[^\w-]/, '')
53
+ end
54
+
55
+ module ClassMethods
56
+
57
+ @attributes = []
58
+
59
+ def attributes
60
+ @attributes
61
+ end
62
+
63
+ def attr_accessor *attrs
64
+ @attributes = Array attrs
65
+ super
66
+ end
67
+
68
+ end
69
+
70
+ end
71
+ end
@@ -0,0 +1,10 @@
1
+ forecast:
2
+ themes:
3
+ weather_icons:
4
+ Clear: "wi wi-day-sunny"
5
+ Partly Cloudy: "wi wi-day-sunny-overcast"
6
+ Cloudy: "wi wi-day-cloudy"
7
+ Mostly Cloudy: "wi wi-cloudy"
8
+ Light Rain: "wi wi-showers"
9
+ Rain: "wi wi-rain"
10
+ Heavy Rain: "wi wi-rain"