noaa 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,63 @@
1
+ begin
2
+ require 'time'
3
+ require 'nokogiri'
4
+ require 'geokit'
5
+ rescue LoadError => e
6
+ if require 'rubygems' then retry
7
+ else raise(e)
8
+ end
9
+ end
10
+
11
+ %w(current_conditions forecast forecast_day http_service station station_writer).each { |file| require File.join(File.dirname(__FILE__), 'noaa', file) }
12
+
13
+ #
14
+ # The NOAA singleton provides methods to conveniently access information from the NOAA weather feed.
15
+ # For the most part, NOAA.current_conditions and NOAA.forecast will be the only entry point into the
16
+ # NOAA API you will need; one exception is discussed below.
17
+ #
18
+ module NOAA
19
+ autoload :VERSION, File.join(File.dirname(__FILE__), 'noaa', 'version')
20
+
21
+ class <<self
22
+ #
23
+ # Retrieve the current weather conditions for a given latitude and longitude. Returns an
24
+ # instance of NOAA::CurrentConditions.
25
+ #
26
+ # NOAA.current_conditions(37.989, -77.507) #=> NOAA::CurrentConditions encapsulating current conditions at this point
27
+ #
28
+ # <b>Note:</b> This method parses the stored list of all weather stations in the US and then finds the closest one to
29
+ # the given coordinates, as the NOAA XML API only takes a weather station ID as input. Clearly, this is an expensive
30
+ # operation; if your application needs to repeatedly request conditions for the same point, you will be much better off
31
+ # determining the current station once using NOAA::Station.closest_to, storing the station ID, and then passing it into
32
+ # NOAA.current_conditions_at_station when you need to get the latest conditions.
33
+ #
34
+ def current_conditions(lat, lng)
35
+ current_conditions_at_station(Station.closest_to(lat, lng).id)
36
+ end
37
+
38
+ #
39
+ # Retrieve the current weather conditions for a given weather station ID. Returns an
40
+ # instance of NOAA::CurrentConditions.
41
+ #
42
+ # NOAA.current_conditions_at_station('KNYC') #=> NOAA::CurrentConditions encapsulating current conditions in Central Park
43
+ #
44
+ # See discussion above regarding why this method is often preferable to simply calling #current_conditions.
45
+ #
46
+ def current_conditions_at_station(station_id)
47
+ CurrentConditions.from_xml(HttpService.new.get_current_conditions(station_id))
48
+ end
49
+
50
+ #
51
+ # Retrieve daily forecast information for a given latitude and longitude. Returns
52
+ # an instance of NOAA::Forecast.
53
+ #
54
+ # NOAA.forecast(4, 37.989, -77.507) #=> NOAA::Forecast containing next four days of forecast data for given coordinates
55
+ #
56
+ # <b>Note:</b> The NOAA updates this data no more than once an hour, and asks that users of the API not request the forecast
57
+ # for a given location more frequently than that. For more information, please see http://www.nws.noaa.gov/xml/#frequency
58
+ #
59
+ def forecast(num_days, lat, lng)
60
+ Forecast.from_xml(HttpService.new.get_forecast(num_days, lat, lng))
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,185 @@
1
+ module NOAA
2
+ #
3
+ # Representation of the current conditions for a given observation point.
4
+ #
5
+ class CurrentConditions
6
+
7
+ class <<self
8
+ private :new
9
+
10
+ def from_xml(doc) #:nodoc:
11
+ new(doc)
12
+ end
13
+ end
14
+
15
+ def initialize(doc) #:notnew:
16
+ @doc = doc
17
+ end
18
+
19
+ #
20
+ # Time object containing the time at which these conditions were observed at the NOAA station
21
+ #
22
+ def observed_at
23
+ @observed_at ||= Time.parse(text_from_node('observation_time_rfc822'))
24
+ end
25
+
26
+ #
27
+ # Text description of the current weather conditions, e.g. "Fair"
28
+ #
29
+ def weather_description
30
+ @weather_description ||= text_from_node('weather')
31
+ end
32
+ alias_method :weather_summary, :weather_description
33
+
34
+ #
35
+ # NWS code representing weather type. This distills the #weather_description
36
+ # into one of a much more manageable set of possibilities. Possible values are:
37
+ #
38
+ # - <code>:skc</code> - Clear
39
+ # - <code>:wind</code> - Windy
40
+ # - <code>:few</code> - A Few Clouds
41
+ # - <code>:sct</code> - Partly Cloudy
42
+ # - <code>:bkn</code> - Mostly Cloudy
43
+ # - <code>:ovc</code> - Overcast
44
+ # - <code>:ra1</code> - Light Rain
45
+ # - <code>:ra</code> - Rain
46
+ # - <code>:shra</code> - Rain Showers
47
+ # - <code>:tsra</code> - Thunderstorms
48
+ # - <code>:ip</code> - Hail
49
+ # - <code>:fzra</code> - Freezing Rain
50
+ # - <code>:mix</code> - Wintry Mix
51
+ # - <code>:sn</code> - Snow
52
+ # - <code>:fg</code> - Fog
53
+ # - <code>:smoke</code> - Smoke
54
+ # - <code>:dust</code> - Dust/Sand
55
+ # - <code>:mist</code> - Haze
56
+ # - <code>:svrtsra</code> - Tornado
57
+ # - <code>:fzrara</code> - Freezing Rain/Rain
58
+ # - <code>:raip</code> - Rain/Hail
59
+ # - <code>:rasn</code> - Rain/Snow
60
+ # - <code>:hi_shwrs</code> - Showers in Vicinity
61
+ # - <code>:hi_tsra</code> - Thunderstorm in Vicinity
62
+ #
63
+ # See http://www.weather.gov/xml/current_obs/weather.php for the NWS's list of possible
64
+ # descriptions and their type codes.
65
+ #
66
+ def weather_type_code
67
+ @weather_type_code ||= text_from_node('icon_url_name').gsub(/^n|\.jpg$/, '').to_sym
68
+ end
69
+
70
+ #
71
+ # Return the NWS image URL for the current weather as string
72
+ #
73
+ def image_url
74
+ @image_url ||= "#{text_from_node('icon_url_base')}#{text_from_node('icon_url_name')}"
75
+ end
76
+
77
+ #
78
+ # The current temperature in the requested units.
79
+ #
80
+ # conditions.temperature #=> temperature in fahrenheit
81
+ # conditions.temperature(:c) #=> temperature in celsius
82
+ # conditions.temperature(:kelvin) #=> anything else raises an exception
83
+ #
84
+ def temperature(unit = :f)
85
+ text_from_node_with_unit('temp', unit, :f, :c).to_i
86
+ end
87
+
88
+ #
89
+ # The current relative humidity percentage (0-100)
90
+ #
91
+ def relative_humidity
92
+ text_from_node('relative_humidity').to_i
93
+ end
94
+
95
+ #
96
+ # The current cardinal or ordinal direction that the wind is coming from (e.g., "Northwest")
97
+ #
98
+ def wind_direction
99
+ text_from_node('wind_dir')
100
+ end
101
+
102
+ #
103
+ # The current direction that the wind is coming from degrees (e.g. 330)
104
+ #
105
+ def wind_degrees
106
+ text_from_node('wind_degrees').to_i
107
+ end
108
+
109
+ #
110
+ # The current wind speed in miles per hour as a float (e.g., 3.45)
111
+ #
112
+ def wind_speed
113
+ text_from_node('wind_mph').to_f
114
+ end
115
+
116
+ #
117
+ # The current wind gust in miles per hour as a float, or nil if none
118
+ #
119
+ def wind_gust
120
+ text_from_node('wind_gust_mph').to_f
121
+ end
122
+
123
+ #
124
+ # The current barometric pressure
125
+ #
126
+ # conditions.pressure #=> pressure in inches
127
+ # conditions.pressure(:mb) #=> pressure in millibars
128
+ # conditions.pressure(:psi) #=> anything else raises an exception
129
+ #
130
+ def pressure(unit = :in)
131
+ text_from_node_with_unit('pressure', unit, :in, :mb).to_f
132
+ end
133
+
134
+ #
135
+ # The current dew point.
136
+ #
137
+ # conditions.dew_point #=> dew point in fahrenheit
138
+ # conditions.dew_point(:c) #=> dew point in celsius
139
+ # conditions.dew_point(:kelvin) #=> anything else raises an exception
140
+ #
141
+ def dew_point(unit = :f)
142
+ text_from_node_with_unit('dewpoint', unit, :f, :c).to_i
143
+ end
144
+
145
+ #
146
+ # The current heat index
147
+ #
148
+ # conditions.heat_index #=> heat index in fahrenheit
149
+ # conditions.heat_index(:c) #=> heat index in celsius
150
+ # conditions.heat_index(:kelvin) #=> anything else raises an exception
151
+ #
152
+ def heat_index(unit = :f)
153
+ text_from_node_with_unit('heat_index', unit, :f, :c).to_i
154
+ end
155
+
156
+ #
157
+ # The current wind chill
158
+ #
159
+ # conditions.wind_chill #=> wind chill in fahrenheit
160
+ # conditions.wind_chill(:c) #=> wind chill in celsius
161
+ # conditions.wind_chill(:kelvin) #=> anything else raises an exception
162
+ #
163
+ def wind_chill(unit = :f)
164
+ text_from_node_with_unit('windchill', unit, :f, :c).to_i
165
+ end
166
+
167
+ #
168
+ # The current visibility in miles
169
+ #
170
+ def visibility
171
+ text_from_node('visibility_mi').to_f
172
+ end
173
+
174
+ private
175
+
176
+ def text_from_node(element_name)
177
+ @doc.xpath("/current_observation/#{element_name}[1]/child::text()").first.to_s
178
+ end
179
+
180
+ def text_from_node_with_unit(element_name, unit, *allowed_units)
181
+ raise ArgumentError, "Unknown unit #{unit.inspect} - allowed units are #{allowed_units.inspect}" unless allowed_units.include?(unit)
182
+ text_from_node("#{element_name}_#{unit}")
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,108 @@
1
+ module NOAA
2
+ #
3
+ # A Forecast object represents a multi-day forecast for a particular place. The forecast for a given day can
4
+ # be accessed using the [] method; e.g. (assuming +forecast+ is a forecast for 12/20/2008 - 12/24/2008):
5
+ #
6
+ # forecast[1] #=> ForecastDay for 12/21/2008
7
+ # forecast.length #=> 4
8
+ #
9
+ class Forecast
10
+
11
+ class <<self
12
+ private :new
13
+
14
+ def from_xml(doc) #:nodoc:
15
+ new(doc)
16
+ end
17
+ end
18
+
19
+ def initialize(doc) #:noinit:
20
+ @doc = doc
21
+ end
22
+
23
+ #
24
+ # The number of days provided by the forecast
25
+ #
26
+ def length
27
+ @length ||= @doc.find(%q{/dwml/data/time-layout[@summarization='24hourly'][1]/start-valid-time}).length
28
+ end
29
+
30
+ #
31
+ # Get the ForecastDay for day i
32
+ #
33
+ # forecast[1] #=> returns the ForecastDay for the second day
34
+ #
35
+ def [](i)
36
+ forecast_days[i]
37
+ end
38
+
39
+ private
40
+
41
+ def forecast_days
42
+ @forecast_days ||= begin
43
+ days = []
44
+ length.times do |i|
45
+ days << day = NOAA::ForecastDay.new
46
+ day.starts_at = starts[i]
47
+ day.ends_at = ends[i]
48
+ day.high = maxima[i]
49
+ day.low = minima[i]
50
+ day.weather_summary = weather_summaries[i]
51
+ day.weather_type_code = weather_type_codes[i]
52
+ day.image_url = image_urls[i]
53
+ day.daytime_precipitation_probability = precipitation_probabilities[i*2]
54
+ day.evening_precipitation_probability = precipitation_probabilities[i*2+1]
55
+ end
56
+ days
57
+ end
58
+ end
59
+
60
+ def starts
61
+ @starts ||= @doc.find(%q{/dwml/data/time-layout[@summarization='24hourly'][1]/start-valid-time/text()}).map do |node|
62
+ Time.parse(node.to_s)
63
+ end
64
+ end
65
+
66
+ def ends
67
+ @ends ||= @doc.find(%q{/dwml/data/time-layout[@summarization='24hourly'][1]/end-valid-time/text()}).map do |node|
68
+ Time.parse(node.to_s)
69
+ end
70
+ end
71
+
72
+ def maxima
73
+ @maxima ||= @doc.find(%q{/dwml/data/parameters[1]/temperature[@type='maximum'][@units='Fahrenheit'][1]/value/text()}).map do |node|
74
+ node.to_s.to_i
75
+ end
76
+ end
77
+
78
+ def minima
79
+ @minima ||= @doc.find(%q{/dwml/data/parameters[1]/temperature[@type='minimum'][@units='Fahrenheit'][1]/value/text()}).map do |node|
80
+ node.to_s.to_i
81
+ end
82
+ end
83
+
84
+ def weather_summaries
85
+ @weather_summaries ||= @doc.find(%q{/dwml/data/parameters[1]/weather[1]/weather-conditions}).map do |node|
86
+ node['weather-summary'].to_s
87
+ end
88
+ end
89
+
90
+ def image_urls
91
+ @image_urls ||= @doc.find(%q{/dwml/data/parameters[1]/conditions-icon/icon-link/text()}).map do |node|
92
+ node.to_s
93
+ end
94
+ end
95
+
96
+ def weather_type_codes
97
+ @weather_type_codes ||= image_urls.map do |url|
98
+ url.match(/n?([a-z_]+)\d*\.jpg$/)[1].to_sym
99
+ end
100
+ end
101
+
102
+ def precipitation_probabilities
103
+ @precipitation_probabilities ||= @doc.find(%q{/dwml/data/parameters[1]/probability-of-precipitation[1]/value/text()}).map do |node|
104
+ node.to_s.to_i
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,38 @@
1
+ module NOAA
2
+ #
3
+ # A ForecastDay contains forecast data for a single day. Each day should start at 6am and
4
+ # end at 6am the following day (assuming that is invariant on the part of the NOAA's data
5
+ # feed). ForecastDay objects are accessed using NOAA::Forecast#[]
6
+ class ForecastDay
7
+ # Time when this forecast's valid time span begins
8
+ attr_reader :starts_at
9
+
10
+ # Time when this forecast's valid time span ends
11
+ attr_reader :ends_at
12
+
13
+ # High temperature for the day in Fahrenheit
14
+ attr_reader :high
15
+
16
+ # Low temperature for the day in Fahrenheit
17
+ attr_reader :low
18
+
19
+ # String summary of weather (e.g., 'Fair')
20
+ attr_reader :weather_summary
21
+ alias_method :weather_description, :weather_summary
22
+
23
+ # Symbol representing NOAA weather type. See NOAA::CurrentConditions#weather_type_code
24
+ attr_reader :weather_type_code
25
+
26
+ # URL string for NOAA weather image
27
+ attr_reader :image_url
28
+
29
+ # Percentage probability of precipitation during the day, between 6am and 6pm, as an integer (0-100)
30
+ attr_reader :daytime_precipitation_probability
31
+
32
+ # Percentage probability of precipitation during the evening/night, between 6pm and 6am, as an integer (0-100)
33
+ attr_reader :evening_precipitation_probability
34
+
35
+ attr_writer :starts_at, :ends_at, :high, :low, :weather_summary, :weather_type_code, :image_url, #:nodoc:
36
+ :daytime_precipitation_probability, :evening_precipitation_probability #:nodoc:
37
+ end
38
+ end
@@ -0,0 +1,19 @@
1
+ module NOAA
2
+ class HttpService #:nodoc:
3
+ def initialize(http = Net::HTTP)
4
+ @HTTP = http
5
+ end
6
+
7
+ def get_current_conditions(station_id)
8
+ LibXML::XML::Document.string(@HTTP.get(URI.parse("http://www.weather.gov/xml/current_obs/#{station_id}.xml")))
9
+ end
10
+
11
+ def get_forecast(num_days, lat, lng)
12
+ LibXML::XML::Document.string(@HTTP.get(URI.parse("http://www.weather.gov/forecasts/xml/sample_products/browser_interface/ndfdBrowserClientByDay.php?lat=#{lat}&lon=#{lng}&format=24+hourly&numDays=#{num_days}")))
13
+ end
14
+
15
+ def get_station_list
16
+ LibXML::XML::Document.string(@HTTP.get(URI.parse("http://www.weather.gov/xml/current_obs/index.xml")))
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,111 @@
1
+ module NOAA
2
+ #
3
+ # Data about an NOAA observation station. When accessing current conditions, the NOAA XML API
4
+ # takes a station ID as input; thus, to find the current conditions for an arbitrary location, one
5
+ # must first determine the closest weather station to that location. The NOAA.current_conditions
6
+ # method performs this task implicitly; however, for applications that need to find conditions
7
+ # for the same location(s) repeatedly, it is far more efficient to find the closest weather station
8
+ # once, store that information, and then query directly against the weather station when updated
9
+ # conditions are needed.
10
+ #
11
+ # Station data is stored in a YAML file that is created using the <tt>noaa-update-stations</tt> executable.
12
+ # Be sure to run this command at least once when you first install the NOAA library, and any time
13
+ # thereafter that you would like to get the latest list of stations. I don't imagine the list
14
+ # changes very often but I don't really have any idea.
15
+ #
16
+ class Station
17
+ class <<self
18
+ attr_writer :stations_file #:nodoc:
19
+
20
+ #
21
+ # Retrieve information about a station given a station ID
22
+ #
23
+ # NOAA::Station.find('KNYC') #=> NOAA::Station object for the Central Park station
24
+ def find(id)
25
+ stations.find { |station| station.id == id }
26
+ end
27
+
28
+ #
29
+ # Find the station closest to a given location. Can accept arguments in any of the following
30
+ # three forms (all are equivalent):
31
+ #
32
+ # NOAA::Station.closest_to(37.989, -77.507)
33
+ # NOAA::Station.closest_to([37.989, -77.507])
34
+ # NOAA::Station.closest_to(GeoKit::LatLng.new(37.989, -77.507))
35
+ def closest_to(*args)
36
+ if args.length == 1
37
+ if args.first.respond_to?(:distance_to)
38
+ closest_to_coordinates(args.first)
39
+ elsif %w(first last).all? { |m| args.first.respond_to?(m) }
40
+ closest_to_lat_lng(args.first)
41
+ else
42
+ raise ArgumentError, "expected two-element Array or GeoKit::LatLng"
43
+ end
44
+ elsif args.length == 2
45
+ closest_to_lat_lng(args)
46
+ else
47
+ raise ArgumentError, "closest_to() will accept one Array argument, one GeoKit::LatLng argument, or two FixNum arguments"
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def closest_to_lat_lng(pair)
54
+ closest_to_coordinates(GeoKit::LatLng.new(pair.first, pair.last))
55
+ end
56
+
57
+ def closest_to_coordinates(coordinates)
58
+ stations.map do |station|
59
+ [coordinates.distance_to(station.coordinates), station]
60
+ end.min do |p1, p2|
61
+ p1.first <=> p2.first # compare distance
62
+ end[1]
63
+ end
64
+
65
+ def stations
66
+ File.open(stations_file) do |file|
67
+ yaml = YAML.load(file) || raise("Can't parse #{file.path} - be sure to run noaa-update-stations")
68
+ yaml.map { |station_hash| new(station_hash) }
69
+ end
70
+ end
71
+
72
+ def stations_file
73
+ @stations_file ||= File.join(File.dirname(__FILE__), '..', '..', 'data', 'stations.yml')
74
+ end
75
+ end
76
+
77
+ # GeoKit::LatLng containing the station's coordinates
78
+ attr_reader :coordinates
79
+
80
+ # Station ID (e.g., "KNYC")
81
+ attr_reader :id
82
+
83
+ # Station name (e.g., "New York City, Central Park")
84
+ attr_reader :name
85
+
86
+ # Two-digit abbreviation for state in which station resides (e.g., "NY")
87
+ attr_reader :state
88
+
89
+ attr_reader :xml_url #:nodoc:
90
+
91
+ def initialize(properties)
92
+ @id, @name, @state, @xml_url = %w(id name state xml_url).map do |p|
93
+ properties[p]
94
+ end
95
+ @coordinates = GeoKit::LatLng.new(properties['latitude'], properties['longitude'])
96
+ end
97
+
98
+ # Latitude of station
99
+ def latitude
100
+ @coordinates.lat
101
+ end
102
+ alias_method :lat, :latitude
103
+
104
+ # Longitude of station
105
+ def longitude
106
+ @coordinates.lng
107
+ end
108
+ alias_method :lng, :longitude
109
+ alias_method :lon, :longitude
110
+ end
111
+ end