nationalweather 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,34 @@
1
+
2
+ module NationalWeather
3
+
4
+ class Conditions
5
+
6
+ attr_reader :summary, :values;
7
+
8
+ def initialize(summary, values)
9
+ @summary = summary
10
+ @values = values
11
+ end
12
+
13
+ def to_s
14
+ s = @summary
15
+ vals = Array.new
16
+ if @values != nil
17
+ @values.each do |v|
18
+ # TODO: handle "none" for intensity, ex: "patchy none fog"
19
+ # TODO: handle "qualifier"
20
+ if v.has_key?('additive')
21
+ vals.push("#{v['additive']} #{v['coverage']} #{v['intensity']} #{v['weather-type']}")
22
+ else
23
+ vals.push("#{v['coverage']} #{v['intensity']} #{v['weather-type']}")
24
+ end
25
+ end
26
+ s +' (' + vals.join(' ') + ')'
27
+ else
28
+ s
29
+ end
30
+ end
31
+
32
+ end
33
+
34
+ end
@@ -0,0 +1,105 @@
1
+
2
+ module NationalWeather
3
+
4
+ class Current
5
+
6
+ # if the xml is invalid, subsequent calls to retrieve values will return nil
7
+ def initialize(xml_string)
8
+ @xml = REXML::Document.new xml_string
9
+ end
10
+
11
+ def temperature_f
12
+ value("/current_observation/temp_f").text.to_f
13
+ end
14
+
15
+ def temperature_c
16
+ value("/current_observation/temp_c").text.to_f
17
+ end
18
+
19
+ def dewpoint_f
20
+ value("/current_observation/dewpoint_f").text.to_f
21
+ end
22
+
23
+ def dewpoint_c
24
+ value("/current_observation/dewpoint_c").text.to_f
25
+ end
26
+
27
+ def pressure_mb
28
+ value("/current_observation/pressure_mb").text.to_f
29
+ end
30
+
31
+ def pressure_inhg
32
+ value("/current_observation/pressure_in").text.to_f
33
+ end
34
+
35
+ def relative_humidity
36
+ value("/current_observation/relative_humidity").text.to_i
37
+ end
38
+
39
+ def visibility_miles
40
+ value("/current_observation/visibility_mi").text.to_f
41
+ end
42
+
43
+ def wind_speed_mph
44
+ value("/current_observation/wind_mph").text.to_f
45
+ end
46
+
47
+ def wind_speed_knots
48
+ value("/current_observation/wind_kt").text.to_f
49
+ end
50
+
51
+ def wind_degrees
52
+ value("/current_observation/wind_degrees").text.to_i
53
+ end
54
+
55
+ def wind_direction
56
+ value("/current_observation/wind_dir").text
57
+ end
58
+
59
+ def wind_string
60
+ value("/current_observation/wind_string").text
61
+ end
62
+
63
+ def weather
64
+ value("/current_observation/weather").text
65
+ end
66
+
67
+ def icon
68
+ base = value("/current_observation/icon_url_base").text
69
+ name = value("/current_observation/icon_url_name").text
70
+ base + name
71
+ end
72
+
73
+ def icon_name
74
+ value("/current_observation/icon_url_name").text
75
+ end
76
+
77
+ def station_id
78
+ value("/current_observation/station_id").text
79
+ end
80
+
81
+ def location
82
+ value("/current_observation/location").text
83
+ end
84
+
85
+ def latitude
86
+ value("/current_observation/latitude").text.to_f
87
+ end
88
+
89
+ def longitude
90
+ value("/current_observation/longitude").text.to_f
91
+ end
92
+
93
+ def time
94
+ value("/current_observation/observation_time_rfc822").text
95
+ end
96
+
97
+ private
98
+
99
+ def value(xpath_string)
100
+ REXML::XPath.first(@xml, xpath_string)
101
+ end
102
+
103
+ end
104
+
105
+ end
@@ -0,0 +1,10 @@
1
+
2
+ module NationalWeather
3
+
4
+ class Day
5
+
6
+ attr_accessor :high, :low, :start_time, :end_time, :conditions, :icon, :precipitation_probability_day, :precipitation_probability_night;
7
+
8
+ end
9
+
10
+ end
@@ -0,0 +1,161 @@
1
+
2
+ require 'time'
3
+
4
+ module NationalWeather
5
+
6
+ class Forecast
7
+
8
+ def initialize(xml_string)
9
+ @xml = REXML::Document.new xml_string
10
+ @values = Hash.new
11
+ end
12
+
13
+ def day(index)
14
+ days[index]
15
+ end
16
+
17
+ def days
18
+ if !@values.has_key?('days')
19
+ days = Array.new
20
+ length.times do |i|
21
+ d = NationalWeather::Day.new
22
+ d.high = high_temperatures[i]
23
+ d.low = low_temperatures[i]
24
+ d.start_time = start_times[i]
25
+ d.end_time = end_times[i]
26
+ d.conditions = conditions[i]
27
+ d.icon = icons[i]
28
+ d.precipitation_probability_day = precipitation_probabilities[i*2]
29
+ d.precipitation_probability_night = precipitation_probabilities[i*2+1]
30
+ days.push(d)
31
+ end
32
+ @values['days'] = days
33
+ end
34
+ @values['days']
35
+ end
36
+
37
+ def length
38
+ start_times.length
39
+ end
40
+
41
+ def high_temperatures
42
+ values('/dwml/data[1]/parameters[1]/temperature[@type="maximum"][1]/value', 'high_temperatures') {|node| node.to_i }
43
+ end
44
+
45
+ def low_temperatures
46
+ values('/dwml/data[1]/parameters[1]/temperature[@type="minimum"][1]/value', 'low_temperatures') {|node| node.to_i }
47
+ end
48
+
49
+ def precipitation_probabilities
50
+ values('/dwml/data[1]/parameters[1]/probability-of-precipitation[1]/value', 'precipitation_probabilities') {|node| node.to_i }
51
+ end
52
+
53
+ def start_times
54
+ values('/dwml/data[1]/time-layout[@summarization="24hourly"][1]/start-valid-time', 'start_times') {|node| Time.parse(node) }
55
+ end
56
+
57
+ def end_times
58
+ values('/dwml/data[1]/time-layout[@summarization="24hourly"][1]/end-valid-time', 'end_times') {|node| Time.parse(node) }
59
+ end
60
+
61
+ def icons
62
+ values('/dwml/data[1]/parameters[1]/conditions-icon[1]/icon-link', 'icons')
63
+ end
64
+
65
+ # Returns any Hazards (Watches, Warnings, and Advisories) for the forecast time period.
66
+ #
67
+ # SINGLE:
68
+ # <hazard hazardCode="LW.Y" phenomena="Lake Wind" significance="Advisory" hazardType="long duration">
69
+ # <hazardTextURL>http://forecast.weather.gov/wwamap/wwatxtget.php?cwa=usa&amp;wwa=Lake%20Wind%20Advisory</hazardTextURL>
70
+ # </hazard>
71
+ #
72
+ # EMPTY:
73
+ # <hazard-conditions xsi:nil="true"/>
74
+ def hazards
75
+ if !@values.has_key?('hazards')
76
+ @values['hazards'] = REXML::XPath.match(@xml, '/dwml/data[1]/parameters[1]/hazards[1]/hazard-conditions[1]/hazard').map {|node|
77
+ # handle empty nodes like <hazards-conditions xsi:nil="true" />
78
+ if node.has_elements?
79
+ code = node.attributes["hazardCode"]
80
+ phenomena = node.attributes["phenomena"]
81
+ significance = node.attributes["significance"]
82
+ type = node.attributes["hazardType"]
83
+ url = node.get_elements("hazardTextURL")[0].text
84
+ NationalWeather::Hazard.new(code, phenomena, significance, type, url)
85
+ else
86
+ nil
87
+ end
88
+ }
89
+ end
90
+ @values['hazards']
91
+ end
92
+
93
+ # Returns all Conditions objects for this forecast
94
+ #
95
+ # MULTIPLE:
96
+ # <weather-conditions weather-summary="Chance Rain Showers">
97
+ # <value coverage="chance" intensity="light" weather-type="rain showers" qualifier="none"/>
98
+ # <value coverage="patchy" intensity="none" additive="and" weather-type="fog" qualifier="none"/>
99
+ # </weather-conditions>
100
+ #
101
+ # EMPTY:
102
+ # <weather-conditions weather-summary="Partly Sunny"/>
103
+ #
104
+ # SINGLE:
105
+ # <weather-conditions weather-summary="Chance Rain Showers">
106
+ # <value coverage="chance" intensity="light" weather-type="rain showers" qualifier="none"/>
107
+ # </weather-conditions>
108
+ #
109
+ def conditions
110
+ if !@values.has_key?('conditions')
111
+ allConditions = Array.new
112
+ @values['conditions'] = REXML::XPath.match(@xml, '/dwml/data[1]/parameters[1]/weather[1]/weather-conditions').map {|node|
113
+ # handle weather-conditions with child values
114
+ if node.has_elements?
115
+ # Array to hold <value> attributes
116
+ cValues = Array.new
117
+ # gather the attributes of each <value> into a Hash
118
+ node.get_elements("value").each do |v|
119
+ atts = Hash.new
120
+ v.attributes.each do |k, v|
121
+ atts[k] = v.to_s
122
+ end
123
+ cValues.push(atts)
124
+ end
125
+ end
126
+ allConditions.push(NationalWeather::Conditions.new(node.attributes["weather-summary"], cValues))
127
+ }
128
+ @values['conditions'] = allConditions
129
+ end
130
+ @values['conditions']
131
+ end
132
+
133
+ private
134
+
135
+ # Returns an Array of values for the given XPath query.
136
+ # A block to format each value String can be included with the call to this method.
137
+ # If no block is given the values in the Array will be Strings.
138
+ # This method cached the result so it doesn't need to query the XML and format the nodes each time it's called
139
+ def values(xpath_string, key)
140
+ if !@values.has_key?(key)
141
+ @values[key] = REXML::XPath.match(@xml, xpath_string).map {|node|
142
+ # handle empty nodes like <value xsi:nil="true" />
143
+ if node.has_text?
144
+ if block_given?
145
+ # format the String with the supplied block
146
+ yield(node.text)
147
+ else
148
+ # default: return a String
149
+ node.text
150
+ end
151
+ else
152
+ nil
153
+ end
154
+ }
155
+ end
156
+ @values[key]
157
+ end
158
+
159
+ end
160
+
161
+ end
@@ -0,0 +1,22 @@
1
+
2
+ module NationalWeather
3
+
4
+ class Hazard
5
+
6
+ attr_reader :code, :phenomena, :significance, :type, :url;
7
+
8
+ def initialize(code, phenomena, significance, type, url)
9
+ @code = code
10
+ @phenomena = phenomena
11
+ @significance = significance
12
+ @type = type
13
+ @url = url
14
+ end
15
+
16
+ def to_s
17
+ "#{@type} #{@phenomena} #{@significance}"
18
+ end
19
+
20
+ end
21
+
22
+ end
@@ -0,0 +1,60 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ require 'net/http'
4
+ require 'rexml/document'
5
+ require 'nationalweather/current'
6
+ require 'nationalweather/forecast'
7
+ require 'nationalweather/hazard'
8
+ require 'nationalweather/conditions'
9
+ require 'nationalweather/day'
10
+
11
+ module NationalWeather
12
+
13
+ VERSION = '0.1.0'
14
+
15
+ # Returns the current weather conditions at the station id specified, or nil if there was an error.
16
+ # For the station ID see: http://www.weather.gov/xml/current_obs/
17
+ # XML list of stations: http://www.weather.gov/xml/current_obs/index.xml
18
+ def NationalWeather::current(station_id)
19
+ xml = fetch("http://www.weather.gov/xml/current_obs/#{station_id}.xml")
20
+ NationalWeather::Current.new(xml)
21
+ end
22
+
23
+ # Returns the forecast for the given location, or nil if there was an error.
24
+ # start_date expected in YYYY-MM-DD format
25
+ def NationalWeather::forecast(lat, lng, start_date, days)
26
+ xml = fetch("http://graphical.weather.gov/xml/sample_products/browser_interface/ndfdBrowserClientByDay.php?lat=#{lat.to_s}&lon=#{lng.to_s}&format=24+hourly&numDays=#{days.to_s}&startDate=#{start_date}")
27
+ NationalWeather::Forecast.new(xml)
28
+ end
29
+
30
+ # raised if there are too many redirects while fetching the weather response
31
+ class TooManyRedirectsError < StandardError
32
+ end
33
+
34
+ # raised if the API server does not return an HTTP success code
35
+ class BadHTTPResponseError < StandardError
36
+ end
37
+
38
+ private
39
+
40
+ # Returns the XML response string for the given URL, following redirects along the way
41
+ def NationalWeather::fetch(uri_str, limit = 10)
42
+
43
+ # TODO: optional file cache
44
+
45
+ raise NationalWeather::TooManyRedirectsError, 'Too many HTTP redirects.' if limit == 0
46
+
47
+ response = Net::HTTP.get_response(URI(uri_str))
48
+
49
+ case response
50
+ when Net::HTTPSuccess then
51
+ response.body.to_s
52
+ when Net::HTTPRedirection then
53
+ location = response['location']
54
+ fetch(location, limit - 1)
55
+ else
56
+ raise NationalWeather::BadHTTPResponseError, "Bad return value: #{response.value}"
57
+ end
58
+ end
59
+
60
+ end
@@ -0,0 +1,44 @@
1
+ require 'test/unit'
2
+ require File.dirname(__FILE__) + '/../lib/nationalweather'
3
+
4
+ class TestCurrent < Test::Unit::TestCase
5
+
6
+ def test_values
7
+ cw = NationalWeather::Current.new(File.new(File.dirname(__FILE__) + "/xml/current_KBAF.xml"))
8
+
9
+ assert_equal(62.0, cw.temperature_f)
10
+ assert_equal(16.7, cw.temperature_c)
11
+
12
+ assert_equal(50.0, cw.dewpoint_f)
13
+ assert_equal(10.0, cw.dewpoint_c)
14
+
15
+ assert_equal(1007.2, cw.pressure_mb)
16
+ assert_equal(29.74, cw.pressure_inhg)
17
+
18
+ assert_equal(65, cw.relative_humidity)
19
+
20
+ assert_equal(10.0, cw.visibility_miles);
21
+
22
+ assert_equal(0.0, cw.wind_speed_mph)
23
+ assert_equal(0.0, cw.wind_speed_knots)
24
+ assert_equal(0.0, cw.wind_degrees)
25
+ assert_equal("North", cw.wind_direction)
26
+ assert_equal("Calm", cw.wind_string)
27
+
28
+ assert_equal("Overcast", cw.weather)
29
+
30
+ assert_equal("http://w1.weather.gov/images/fcicons/ovc.jpg", cw.icon)
31
+ assert_equal("ovc.jpg", cw.icon_name)
32
+
33
+ assert_equal("KBAF", cw.station_id)
34
+
35
+ assert_equal("Westfield, Barnes Municipal Airport, MA", cw.location)
36
+
37
+ assert_equal(42.16, cw.latitude)
38
+
39
+ assert_equal(-72.72, cw.longitude)
40
+
41
+ assert_equal("Sun, 30 Sep 2012 14:53:00 -0400", cw.time)
42
+ end
43
+
44
+ end
@@ -0,0 +1,152 @@
1
+ require 'test/unit'
2
+ require File.dirname(__FILE__) + '/../lib/nationalweather'
3
+
4
+ class TestForecast < Test::Unit::TestCase
5
+
6
+ def test_7day
7
+ f = NationalWeather::Forecast.new(File.new(File.dirname(__FILE__) + "/xml/forecast_7day.xml"))
8
+
9
+ # length of forecast
10
+ assert_equal(7, f.length)
11
+
12
+ # high temps
13
+ expectedHighs = [38, 29, 33, 20, 37, 18, 14]
14
+ assert_equal(expectedHighs, f.high_temperatures)
15
+
16
+ # low temps
17
+ expectedLows = [17, 19, 20, 14, 14, 2, nil]
18
+ assert_equal(expectedLows, f.low_temperatures)
19
+
20
+ # precipitation
21
+ expectedPrecipitation = [27, 19, 56, 33, 30, 27, 20, 7, 10, 16, 26, 12, 11, nil]
22
+ assert_equal(expectedPrecipitation, f.precipitation_probabilities)
23
+
24
+ # icons
25
+ expectedIcons = [
26
+ 'http://www.nws.noaa.gov/weather/images/fcicons/sn30.jpg',
27
+ 'http://www.nws.noaa.gov/weather/images/fcicons/sn60.jpg',
28
+ 'http://www.nws.noaa.gov/weather/images/fcicons/sn30.jpg',
29
+ 'http://www.nws.noaa.gov/weather/images/fcicons/sn20.jpg',
30
+ 'http://www.nws.noaa.gov/weather/images/fcicons/bkn.jpg',
31
+ 'http://www.nws.noaa.gov/weather/images/fcicons/sn30.jpg',
32
+ 'http://www.nws.noaa.gov/weather/images/fcicons/bkn.jpg'
33
+ ]
34
+ assert_equal(expectedIcons, f.icons)
35
+
36
+ # start times
37
+ expectedStartTimes = [
38
+ '2008-12-05T06:00:00-07:00',
39
+ '2008-12-06T06:00:00-07:00',
40
+ '2008-12-07T06:00:00-07:00',
41
+ '2008-12-08T06:00:00-07:00',
42
+ '2008-12-09T06:00:00-07:00',
43
+ '2008-12-10T06:00:00-07:00',
44
+ '2008-12-11T06:00:00-07:00'
45
+ ].map {|t| Time.parse(t) }
46
+ assert_equal(expectedStartTimes, f.start_times)
47
+
48
+ # end times
49
+ expectedEndTimes = [
50
+ '2008-12-06T06:00:00-07:00',
51
+ '2008-12-07T06:00:00-07:00',
52
+ '2008-12-08T06:00:00-07:00',
53
+ '2008-12-09T06:00:00-07:00',
54
+ '2008-12-10T06:00:00-07:00',
55
+ '2008-12-11T06:00:00-07:00',
56
+ '2008-12-12T06:00:00-07:00'
57
+ ].map {|t| Time.parse(t) }
58
+ assert_equal(expectedEndTimes, f.end_times)
59
+
60
+ # hazards
61
+ expectedHazard = NationalWeather::Hazard.new(
62
+ "LW.Y",
63
+ "Lake Wind",
64
+ "Advisory",
65
+ "long duration",
66
+ "http://forecast.weather.gov/wwamap/wwatxtget.php?cwa=usa&wwa=Lake%20Wind%20Advisory"
67
+ )
68
+ # returns an array, but there's only one in this test
69
+ hazard = f.hazards[0]
70
+ assert_equal(expectedHazard.code, hazard.code)
71
+ assert_equal(expectedHazard.phenomena, hazard.phenomena)
72
+ assert_equal(expectedHazard.significance, hazard.significance)
73
+ assert_equal(expectedHazard.type, hazard.type)
74
+ assert_equal(expectedHazard.url, hazard.url)
75
+ assert_equal("long duration Lake Wind Advisory", hazard.to_s)
76
+
77
+ # conditions
78
+ expectedConditionsSummaries = [
79
+ 'Snow Likely',
80
+ 'Snow Likely',
81
+ 'Chance Snow',
82
+ 'Slight Chance Snow',
83
+ 'Mostly Cloudy',
84
+ 'Chance Snow',
85
+ 'Mostly Cloudy'
86
+ ]
87
+ expectedConditionsStrings = [
88
+ 'Snow Likely (likely light snow)',
89
+ 'Snow Likely (likely light snow)',
90
+ 'Chance Snow (chance light snow)',
91
+ 'Slight Chance Snow (slight chance light snow)',
92
+ 'Mostly Cloudy',
93
+ 'Chance Snow (chance light snow)',
94
+ 'Mostly Cloudy'
95
+ ]
96
+ i = 0
97
+ f.conditions.each do |c|
98
+ assert_equal(expectedConditionsSummaries[i], c.summary)
99
+ assert_equal(expectedConditionsStrings[i], c.to_s)
100
+ i += 1
101
+ end
102
+
103
+ # days
104
+ 7.times do |i|
105
+ day = f.day(i)
106
+ assert_not_nil(day)
107
+ assert_equal(expectedHighs[i], day.high)
108
+ assert_equal(expectedLows[i], day.low)
109
+ assert_equal(expectedIcons[i], day.icon)
110
+ assert_equal(expectedStartTimes[i], day.start_time)
111
+ assert_equal(expectedEndTimes[i], day.end_time)
112
+ assert_equal(expectedConditionsSummaries[i], day.conditions.summary)
113
+ assert_equal(expectedConditionsStrings[i], day.conditions.to_s)
114
+ assert_equal(expectedPrecipitation[i*2], day.precipitation_probability_day)
115
+ assert_equal(expectedPrecipitation[i*2+1], day.precipitation_probability_night)
116
+ end
117
+
118
+ end
119
+
120
+ def test_conditions
121
+
122
+ f = NationalWeather::Forecast.new(File.new(File.dirname(__FILE__) + "/xml/forecast_conditions.xml"))
123
+
124
+ # conditions
125
+ expectedConditionsSummaries = [
126
+ 'Chance Rain Showers',
127
+ 'Partly Sunny',
128
+ 'Chance Rain Showers',
129
+ 'Chance Rain Showers',
130
+ 'Slight Chance Rain Showers',
131
+ 'Slight Chance Rain Showers',
132
+ 'Slight Chance Rain Showers'
133
+ ]
134
+ expectedConditionsStrings = [
135
+ 'Chance Rain Showers (chance light rain showers and patchy none fog)',
136
+ 'Partly Sunny',
137
+ 'Chance Rain Showers (chance light rain showers)',
138
+ 'Chance Rain Showers (chance light rain showers)',
139
+ 'Slight Chance Rain Showers (slight chance light rain showers)',
140
+ 'Slight Chance Rain Showers (slight chance light rain showers)',
141
+ 'Slight Chance Rain Showers (slight chance light rain showers)'
142
+ ]
143
+ i = 0
144
+ f.conditions.each do |c|
145
+ assert_equal(expectedConditionsSummaries[i], c.summary)
146
+ assert_equal(expectedConditionsStrings[i], c.to_s)
147
+ i += 1
148
+ end
149
+
150
+ end
151
+
152
+ end
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: nationalweather
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Andrew M. Whalen
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-01-04 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: NationalWeather is a Ruby client library for the National Oceanic and
15
+ Atmospheric Administration's (NOAA) National Weather Service (NWS) forecast and
16
+ current weather REST web services.
17
+ email: nationalweather-ruby@amwhalen.com
18
+ executables: []
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - lib/nationalweather.rb
23
+ - lib/nationalweather/conditions.rb
24
+ - lib/nationalweather/current.rb
25
+ - lib/nationalweather/day.rb
26
+ - lib/nationalweather/forecast.rb
27
+ - lib/nationalweather/hazard.rb
28
+ - test/test_current.rb
29
+ - test/test_forecast.rb
30
+ homepage: http://rubygems.org/gems/nationalweather
31
+ licenses:
32
+ - MIT
33
+ post_install_message:
34
+ rdoc_options: []
35
+ require_paths:
36
+ - lib
37
+ required_ruby_version: !ruby/object:Gem::Requirement
38
+ none: false
39
+ requirements:
40
+ - - ! '>='
41
+ - !ruby/object:Gem::Version
42
+ version: 1.8.6
43
+ required_rubygems_version: !ruby/object:Gem::Requirement
44
+ none: false
45
+ requirements:
46
+ - - ! '>='
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ requirements: []
50
+ rubyforge_project:
51
+ rubygems_version: 1.8.24
52
+ signing_key:
53
+ specification_version: 3
54
+ summary: Client library for NOAA's forecast and current weather services.
55
+ test_files:
56
+ - test/test_current.rb
57
+ - test/test_forecast.rb