lib/barometer/weather_services/noaa.rb in barometer-0.7.3 vs lib/barometer/weather_services/noaa.rb in barometer-0.8.0
- old
+ new
@@ -1,6 +1,317 @@
module Barometer
-
+ #
+ # = NOAA Weather
+ # http://www.weather.gov/
+ #
+ # - key required: NO
+ # - registration required: NO
+ # - supported countries: US only
+ #
+ # === performs geo coding
+ # - city: NO
+ # - coordinates: YES
+ #
+ # === time info
+ # - sun rise/set: NO
+ # - provides timezone: ?
+ # - requires TZInfo: ?
+ #
+ # == resources
+ # - API: http://www.weather.gov/forecasts/xml/rest.php
+ #
+ # === Possible queries:
+ # - http://www.weather.gov/forecasts/xml/sample_products/browser_interface/ndfdBrowserClientByDay.php? \
+ # format=24%20hourly&numDays=7&zipCodeList=90210
+ # - http://www.weather.gov/xml/current_obs/KSMO.xml
+ #
+ # what query can be:
+ # - zipcode
+ # - coordinates
+ #
+ # = NOAA terms of use
+ # see API url provided above
+ #
class WeatherService::Noaa < WeatherService
+
+ #########################################################################
+ # PRIVATE
+ # If class methods could be private, the remaining methods would be.
+ #
+
+ def self._source_name; :noaa; end
+ def self._accepted_formats; [:zipcode, :coordinates]; end
+
+ # we can accept US, or we can try if the country is unknown
+ #
+ def self._supports_country?(query=nil)
+ ["US", nil, ""].include?(query.country_code)
+ end
+
+ def self._build_current(data, metric=true)
+ raise ArgumentError unless data.is_a?(Hash)
+
+ current = Measurement::Result.new
+ return current if data.empty?
+
+ if data && data['observation_time_rfc822'] && (time_match = data['observation_time_rfc822'].match(/(.* \d\d:\d\d:\d\d)/))
+ current.updated_at = Data::LocalDateTime.parse(time_match[1])
+ end
+
+ current.temperature = Data::Temperature.new(metric)
+ current.temperature << [data['temp_c'], data['temp_f']]
+
+ current.wind = Data::Speed.new(metric)
+ current.wind.mph = data['wind_mph'].to_f
+ current.wind.direction = data['wind_dir']
+ current.wind.degrees = data['wind_degrees'].to_i
+
+ current.humidity = data['relative_humidity'].to_i
+
+ current.pressure = Data::Pressure.new(metric)
+ current.pressure << [data['pressure_mb'], data['pressure_in']]
+
+ current.dew_point = Data::Temperature.new(metric)
+ current.dew_point << [data['dewpoint_c'], data['dewpoint_f']]
+
+ if data['windchill_c'] || data['windchill_f']
+ current.wind_chill = Data::Temperature.new(metric)
+ current.wind_chill << [data['windchill_c'], data['windchill_f']]
+ end
+
+ current.visibility = Data::Distance.new(metric)
+ current.visibility.m = data['visibility_mi'].to_f
+
+ current.condition = data['weather']
+ if data['icon_url_name']
+ icon_match = data['icon_url_name'].match(/(.*).(jpg|png)/)
+ current.icon = icon_match[1] if icon_match
+ end
+
+ current
+ end
+
+ def self._build_forecast(data, metric=true)
+ raise ArgumentError unless data.is_a?(Hash)
+
+ forecasts = Measurement::ResultArray.new
+ return forecasts unless data && data['time_layout']
+
+ twelve_hour_starts = []
+ twelve_hour_ends = []
+ data['time_layout'].each do |time_layout|
+ if time_layout["summarization"] == "24hourly"
+ twelve_hour_starts = time_layout["start_valid_time"]
+ twelve_hour_ends = time_layout["end_valid_time"]
+ break
+ end
+ end
+
+ daily_highs = []
+ daily_lows = []
+ data['parameters']['temperature'].each do |temps|
+ case temps["type"]
+ when "maximum"
+ daily_highs = temps['value']
+ when "minimum"
+ daily_lows = temps['value']
+ end
+ end
+
+ # NOAA returns 2 pop values for each day ... for each day, use the max pop value
+ #
+ daily_pops = []
+ if data['parameters']['probability_of_precipitation'] &&
+ data['parameters']['probability_of_precipitation']['value']
+ daily_pops = data['parameters']['probability_of_precipitation']['value'].collect{|i|i.respond_to?(:to_i) ? i.to_i : 0}.each_slice(2).to_a.collect{|x|x.max}
+ end
+
+ daily_conditions = []
+ if data['parameters']['weather'] &&
+ data['parameters']['weather']['weather_conditions']
+ daily_conditions = data['parameters']['weather']['weather_conditions'].collect{|c|c["weather_summary"]}
+ end
+
+ daily_icons = []
+ if data['parameters']['conditions_icon'] &&
+ data['parameters']['conditions_icon']['icon_link']
+ daily_icons = data['parameters']['conditions_icon']['icon_link'].collect{|c|c.match(/.*\/(.*)\.jpg/)[1]}
+ end
+
+ d = 0
+ # go through each forecast start date and create an instance
+ twelve_hour_starts.each do |start_date|
+ forecast_measurement = Measurement::Result.new(metric)
+
+ # day = 6am - 6am (next day)
+ date_s = Date.parse(start_date)
+ date_e = Date.parse(start_date) + 1
+ forecast_measurement.valid_start_date = Data::LocalDateTime.new(date_s.year,date_s.month,date_s.day,6,0,0)
+ forecast_measurement.valid_end_date = Data::LocalDateTime.new(date_e.year,date_e.month,date_e.day,5,59,59)
+
+ forecast_measurement.high = Data::Temperature.new(metric)
+ forecast_measurement.high.f = (daily_highs[d].respond_to?(:to_f) ? daily_highs[d].to_f : nil)
+ forecast_measurement.low = Data::Temperature.new(metric)
+ forecast_measurement.low.f = (daily_lows[d].respond_to?(:to_f) ? daily_lows[d].to_f : nil)
+
+ forecast_measurement.pop = daily_pops[d]
+ forecast_measurement.condition = daily_conditions[d]
+ forecast_measurement.icon = daily_icons[d]
+
+ forecasts << forecast_measurement
+ d += 1
+ end
+
+ forecasts
+ end
+
+ def self._build_location(data, geo=nil)
+ raise ArgumentError unless data.is_a?(Hash)
+ raise ArgumentError unless (geo.nil? || geo.is_a?(Data::Geo))
+ location = Data::Location.new
+ # use the geocoded data if available, otherwise get data from result
+ if geo
+ location.city = geo.locality
+ location.state_code = geo.region
+ location.country = geo.country
+ location.country_code = geo.country_code
+ location.latitude = geo.latitude
+ location.longitude = geo.longitude
+ else
+ if data && data['location']
+ location.city = data['location'].split(',')[0].strip
+ location.state_code = data['location'].split(',')[-1].strip
+ location.country_code = 'US'
+ end
+ end
+ location
+ end
+
+ def self._build_station(data)
+ raise ArgumentError unless data.is_a?(Hash)
+ station = Data::Location.new
+ station.id = data['station_id']
+ if data['location']
+ station.name = data['location']
+ station.city = data['location'].split(',')[0].strip
+ station.state_code = data['location'].split(',')[-1].strip
+ station.country_code = 'US'
+ station.latitude = data['latitude']
+ station.longitude = data['longitude']
+ end
+ station
+ end
+
+ def self._build_timezone(data)
+ if data && data['observation_time']
+ zone_match = data['observation_time'].match(/ ([A-Z]*)$/)
+ Data::Zone.new(zone_match[1]) if zone_match
+ end
+ end
+
+ # override default _fetch behavior
+ # this service requires TWO seperate http requests (one for current
+ # and one for forecasted weather) ... combine the results
+ #
+ def self._fetch(query, metric=true)
+ result = []
+ result << _fetch_forecast(query,metric)
+
+ # only proceed if we are getting results
+ #
+ # binding.pry
+ if result[0] && !result[0].empty?
+ # we need to use the lst/long from the forecast data (result[0])
+ # to get the closest "station_id", to get the current conditions
+ #
+ station_id = Barometer::WebService::NoaaStation.fetch(
+ result[0]["location"]["point"]["latitude"],
+ result[0]["location"]["point"]["longitude"]
+ )
+
+ result << _fetch_current(station_id,metric)
+ else
+ puts "NOAA cannot proceed to fetching current weather, lat/lon unknown" if Barometer::debug?
+ result << {}
+ end
+
+ result
+ end
+
+ # use HTTParty to get the current weather
+ #
+ def self._fetch_current(station_id, metric=true)
+ return {} unless station_id
+ puts "fetching NOAA current weather: #{station_id}" if Barometer::debug?
+
+ self.get(
+ "http://w1.weather.gov/xml/current_obs/#{station_id}.xml",
+ :query => {},
+ :format => :xml,
+ :timeout => Barometer.timeout
+ )["current_observation"]
+ end
+
+ # use HTTParty to get the forecasted weather
+ #
+ def self._fetch_forecast(query, metric=true)
+ puts "fetching NOAA forecast: #{query.q}" if Barometer::debug?
+
+ q = case query.format.to_sym
+ when :short_zipcode
+ { :zipCodeList => query.q }
+ when :zipcode
+ { :zipCodeList => query.q }
+ when :coordinates
+ { :lat => query.q.split(',')[0], :lon => query.q.split(',')[1] }
+ else
+ {}
+ end
+
+ result = self.get(
+ "http://graphical.weather.gov/xml/sample_products/browser_interface/ndfdBrowserClientByDay.php",
+ :query => {
+ :format => "24 hourly",
+ :numDays => "7"
+ }.merge(q),
+ :format => :xml,
+ :timeout => Barometer.timeout
+ )
+
+
+ # binding.pry
+
+ if result && result["dwml"] && result["dwml"]["data"]
+ result = result["dwml"]["data"]
+ else
+ return {}
+ end
+
+ # check that we have data ... we have to dig deep to find out since
+ # NOAA will return a good looking result, even when there isn't any data to return
+ #
+ if result && result['parameters'] &&
+ result['parameters']['temperature'] &&
+ result['parameters']['temperature'].first &&
+ result['parameters']['temperature'].first['value'] &&
+ !result['parameters']['temperature'].first['value'].collect{|t| t.respond_to?(:to_i) ? t.to_i : nil}.compact.empty?
+ else
+ return {}
+ end
+
+ result
+ end
+
+ # since we have two sets of data, override these calls to choose the
+ # right set of data
+ #
+ def self._current_result(data); data[1]; end
+ def self._forecast_result(data=nil); data[0]; end
+ def self._location_result(data=nil); data[1]; end
+ def self._station_result(data=nil); data[1]; end
+ def self._sun_result(data=nil); nil; end
+ def self._timezone_result(data=nil); data[1]; end
+ def self._time_result(data=nil); data[1]; end
+
end
-
-end
\ No newline at end of file
+
+end