forecast 0.0.2

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