noaa 0.2.4
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +7 -0
- data/README +77 -0
- data/bin/noaa-update-stations +12 -0
- data/data/stations.yml +12091 -0
- data/lib/noaa.rb +63 -0
- data/lib/noaa/current_conditions.rb +185 -0
- data/lib/noaa/forecast.rb +108 -0
- data/lib/noaa/forecast_day.rb +38 -0
- data/lib/noaa/http_service.rb +19 -0
- data/lib/noaa/station.rb +111 -0
- data/lib/noaa/station_writer.rb +26 -0
- data/lib/noaa/version.rb +3 -0
- data/test/data/4-day.xml +116 -0
- data/test/data/KVAY.xml +52 -0
- data/test/data/stations-abridged.xml +44 -0
- data/test/data/stations.xml +22177 -0
- data/test/data/stations.yml +12091 -0
- data/test/test_current_conditions.rb +141 -0
- data/test/test_forecast.rb +69 -0
- data/test/test_helper.rb +15 -0
- data/test/test_http_service.rb +57 -0
- data/test/test_station.rb +65 -0
- data/test/test_station_writer.rb +49 -0
- metadata +168 -0
data/lib/noaa.rb
ADDED
@@ -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
|
data/lib/noaa/station.rb
ADDED
@@ -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
|