nationalweather 0.1.0

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