noaa_weather_client 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +1 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/.yardopts +2 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +107 -0
- data/Rakefile +8 -0
- data/bin/noaa_weather_client +43 -0
- data/data/xml/current_observation.xsd +79 -0
- data/data/xml/dwml.xsd +97 -0
- data/data/xml/location.xsd +142 -0
- data/data/xml/meta_data.xsd +100 -0
- data/data/xml/moreWeatherInformation.xsd +23 -0
- data/data/xml/ndfd_data.xsd +43 -0
- data/data/xml/parameters.xsd +1173 -0
- data/data/xml/summarizationType.xsd +29 -0
- data/data/xml/time_layout.xsd +51 -0
- data/lib/noaa_weather_client.rb +9 -0
- data/lib/noaa_weather_client/cli.rb +53 -0
- data/lib/noaa_weather_client/cli/templates.rb +53 -0
- data/lib/noaa_weather_client/client.rb +61 -0
- data/lib/noaa_weather_client/errors.rb +7 -0
- data/lib/noaa_weather_client/responses/current_observation.rb +93 -0
- data/lib/noaa_weather_client/responses/forecast.rb +84 -0
- data/lib/noaa_weather_client/responses/generic_response.rb +9 -0
- data/lib/noaa_weather_client/responses/lat_lon_list.rb +25 -0
- data/lib/noaa_weather_client/responses/reactive_xml_response.rb +29 -0
- data/lib/noaa_weather_client/responses/station.rb +28 -0
- data/lib/noaa_weather_client/responses/stations.rb +41 -0
- data/lib/noaa_weather_client/responses/validatable_xml_response.rb +22 -0
- data/lib/noaa_weather_client/rest_client_factory.rb +12 -0
- data/lib/noaa_weather_client/services/calculate_distance_between_lat_lon.rb +20 -0
- data/lib/noaa_weather_client/services/current_observations.rb +32 -0
- data/lib/noaa_weather_client/services/find_nearest_station.rb +16 -0
- data/lib/noaa_weather_client/services/forecast_by_day.rb +52 -0
- data/lib/noaa_weather_client/services/postal_code_to_coordinate.rb +36 -0
- data/lib/noaa_weather_client/services/rest_service.rb +28 -0
- data/lib/noaa_weather_client/services/soap_service.rb +16 -0
- data/lib/noaa_weather_client/services/weather_stations.rb +32 -0
- data/lib/noaa_weather_client/soap_client_factory.rb +17 -0
- data/lib/noaa_weather_client/station_filters.rb +8 -0
- data/lib/noaa_weather_client/version.rb +3 -0
- data/lib/noaa_weather_client/xml_parser_factory.rb +9 -0
- data/noaa_weather_client.gemspec +27 -0
- data/spec/fixtures/vcr_cassettes/current_observations.yml +25890 -0
- data/spec/fixtures/vcr_cassettes/forecast_by_day_3.yml +772 -0
- data/spec/fixtures/vcr_cassettes/forecast_by_day_7.yml +829 -0
- data/spec/fixtures/vcr_cassettes/nearest_weather_station.yml +25842 -0
- data/spec/fixtures/vcr_cassettes/postal_code_to_coordinate.yml +75 -0
- data/spec/fixtures/vcr_cassettes/weather_stations.yml +25842 -0
- data/spec/fixtures/xml/forecast.xml +144 -0
- data/spec/lib/noaa_client/client_spec.rb +93 -0
- data/spec/lib/noaa_client/responses/current_observation_spec.rb +122 -0
- data/spec/lib/noaa_client/responses/forecast_spec.rb +66 -0
- data/spec/lib/noaa_client/responses/lat_lon_list_spec.rb +30 -0
- data/spec/lib/noaa_client/responses/station_spec.rb +53 -0
- data/spec/lib/noaa_client/responses/stations_spec.rb +86 -0
- data/spec/lib/noaa_client/rest_client_factory_spec.rb +15 -0
- data/spec/lib/noaa_client/services/calculate_distance_between_lat_lon_spec.rb +16 -0
- data/spec/lib/noaa_client/services/current_observations_spec.rb +47 -0
- data/spec/lib/noaa_client/services/find_nearest_station_spec.rb +36 -0
- data/spec/lib/noaa_client/services/forecast_by_day_spec.rb +62 -0
- data/spec/lib/noaa_client/services/postal_code_to_coordinate_spec.rb +41 -0
- data/spec/lib/noaa_client/services/rest_service_spec.rb +45 -0
- data/spec/lib/noaa_client/services/soap_service_spec.rb +56 -0
- data/spec/lib/noaa_client/services/weather_stations_spec.rb +40 -0
- data/spec/lib/noaa_client/soap_client_factory_spec.rb +13 -0
- data/spec/lib/noaa_client/xml_parser_factory_spec.rb +14 -0
- data/spec/spec_helper.rb +31 -0
- metadata +228 -0
@@ -0,0 +1,25 @@
|
|
1
|
+
require_relative '../xml_parser_factory'
|
2
|
+
require_relative 'reactive_xml_response'
|
3
|
+
require_relative 'validatable_xml_response'
|
4
|
+
|
5
|
+
module NoaaWeatherClient
|
6
|
+
module Responses
|
7
|
+
class LatLonList
|
8
|
+
include ReactiveXmlResponse
|
9
|
+
include ValidatableXmlResponse
|
10
|
+
|
11
|
+
def initialize(response)
|
12
|
+
@source = XmlParserFactory.build_parser.parse(response)
|
13
|
+
validate! @source, :dwml
|
14
|
+
end
|
15
|
+
|
16
|
+
def latitude
|
17
|
+
@latitude ||= latLonList.split(",").first.to_f
|
18
|
+
end
|
19
|
+
|
20
|
+
def longitude
|
21
|
+
@longitude ||= latLonList.split(",").last.to_f
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module NoaaWeatherClient
|
2
|
+
module Responses
|
3
|
+
module ReactiveXmlResponse
|
4
|
+
def method_missing(method_name, *arguments, &block)
|
5
|
+
if tag = source.css(method_name.to_s)
|
6
|
+
if block
|
7
|
+
block.call tag.text
|
8
|
+
else
|
9
|
+
tag.text
|
10
|
+
end
|
11
|
+
else
|
12
|
+
super
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def respond_to_missing?(method_name, include_private = false)
|
17
|
+
source.css(method_name.to_s) || super
|
18
|
+
end
|
19
|
+
|
20
|
+
def source
|
21
|
+
@source || NullResponse.new
|
22
|
+
end
|
23
|
+
|
24
|
+
class NullResponse
|
25
|
+
def css(*args); end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require_relative 'reactive_xml_response'
|
2
|
+
|
3
|
+
module NoaaWeatherClient
|
4
|
+
module Responses
|
5
|
+
class Station
|
6
|
+
include ReactiveXmlResponse
|
7
|
+
|
8
|
+
def initialize(station)
|
9
|
+
@source = station
|
10
|
+
end
|
11
|
+
|
12
|
+
def latitude
|
13
|
+
source.css('latitude').text.to_f
|
14
|
+
end
|
15
|
+
|
16
|
+
def longitude
|
17
|
+
source.css('longitude').text.to_f
|
18
|
+
end
|
19
|
+
|
20
|
+
def xml_url
|
21
|
+
@xml_url ||= begin
|
22
|
+
path = URI(source.css('xml_url').text).path
|
23
|
+
"http://w1.weather.gov#{path}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require_relative '../xml_parser_factory'
|
2
|
+
require_relative 'station'
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module NoaaWeatherClient
|
6
|
+
module Responses
|
7
|
+
class Stations
|
8
|
+
include Enumerable
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
def_delegators :stations, :'[]', :fetch, :sort_by!, :take, :size
|
12
|
+
|
13
|
+
def initialize(response, options = {})
|
14
|
+
@body = XmlParserFactory.build_parser.parse response
|
15
|
+
@options = options
|
16
|
+
end
|
17
|
+
|
18
|
+
def each
|
19
|
+
stations.each { |s| yield s }
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_xml
|
23
|
+
body.to_xml
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
attr_reader :body, :options
|
29
|
+
|
30
|
+
def stations
|
31
|
+
@stations ||= body.css('station').map do |station|
|
32
|
+
station_class.new station
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def station_class
|
37
|
+
options.fetch(:station_class, Station)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require_relative '../errors'
|
2
|
+
|
3
|
+
module NoaaWeatherClient
|
4
|
+
module Responses
|
5
|
+
module ValidatableXmlResponse
|
6
|
+
SCHEMA_PATH = File.expand_path("../../../../data/xml", __FILE__)
|
7
|
+
|
8
|
+
def validate!(doc, schema_name)
|
9
|
+
# chdir to help Nokogiri load included schemas
|
10
|
+
Dir.chdir(SCHEMA_PATH) do
|
11
|
+
schema_file = File.join(SCHEMA_PATH, "#{schema_name}.xsd")
|
12
|
+
schema = Nokogiri::XML::Schema(File.read(schema_file))
|
13
|
+
errors = schema.validate(doc)
|
14
|
+
unless errors.empty?
|
15
|
+
raise Errors::InvalidXmlError, "Invalid xml: #{errors.map(&:message).join("\n")}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
module NoaaWeatherClient
|
5
|
+
module RestClientFactory
|
6
|
+
def self.build_client(options = {})
|
7
|
+
provider = options.fetch(:provider, Net::HTTP)
|
8
|
+
uri = URI(options.fetch(:url))
|
9
|
+
provider.new uri.host, uri.port
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module NoaaWeatherClient
|
2
|
+
module Services
|
3
|
+
module CalculateDistanceBetweenLatLon
|
4
|
+
extend Math
|
5
|
+
|
6
|
+
def self.get_distance(lat1, lon1, lat2, lon2)
|
7
|
+
lat1, lon1, lat2, lon2 = convert_to_radian(lat1, lon1, lat2, lon2)
|
8
|
+
|
9
|
+
radius = 6371 #km
|
10
|
+
s = sin(lat1) * sin(lat2)
|
11
|
+
c = cos(lat1) * cos(lat2) * cos(lon2-lon1)
|
12
|
+
acos(s + c) * radius;
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.convert_to_radian(*args)
|
16
|
+
args.map { |arg| arg / 180 * Math::PI }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require_relative 'rest_service'
|
2
|
+
require_relative '../responses/current_observation'
|
3
|
+
|
4
|
+
module NoaaWeatherClient
|
5
|
+
module Services
|
6
|
+
class CurrentObservations
|
7
|
+
include RestService
|
8
|
+
|
9
|
+
def initialize(options = {})
|
10
|
+
@options = options
|
11
|
+
end
|
12
|
+
|
13
|
+
def fetch(station, options = {})
|
14
|
+
rest_service.object_from_response(:get,
|
15
|
+
station.xml_url,
|
16
|
+
response_class: response_class)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
attr_reader :options
|
22
|
+
|
23
|
+
def rest_service
|
24
|
+
options.fetch(:rest_service, self)
|
25
|
+
end
|
26
|
+
|
27
|
+
def response_class
|
28
|
+
options.fetch(:response_class, Responses::CurrentObservation)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require_relative 'calculate_distance_between_lat_lon'
|
2
|
+
|
3
|
+
module NoaaWeatherClient
|
4
|
+
module Services
|
5
|
+
module FindNearestStation
|
6
|
+
def self.find(lat, lon, stations, options = {})
|
7
|
+
calc = options.fetch(:calculator, CalculateDistanceBetweenLatLon)
|
8
|
+
filter = options.fetch(:filter, nil)
|
9
|
+
count = options.fetch(:count, 1)
|
10
|
+
stations.select!(&filter) if filter
|
11
|
+
stations.sort_by! { |s| calc.get_distance(lat, lon, s.latitude, s.longitude) }
|
12
|
+
count == 1 ? stations.first : stations.take(count)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require_relative 'soap_service'
|
2
|
+
require_relative '../responses/forecast'
|
3
|
+
|
4
|
+
module NoaaWeatherClient
|
5
|
+
module Services
|
6
|
+
class ForecastByDay
|
7
|
+
include SoapService
|
8
|
+
|
9
|
+
def initialize(options = {})
|
10
|
+
@options = options
|
11
|
+
end
|
12
|
+
|
13
|
+
def fetch(lat, lon, options = {})
|
14
|
+
fopts = build_formated_options options.merge({ latitude: lat.to_s, longitude: lon.to_s })
|
15
|
+
soap_service.object_from_response(:ndf_dgen_by_day, fopts, response_class: response_class)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
attr_reader :options
|
21
|
+
|
22
|
+
def soap_service
|
23
|
+
options.fetch(:soap_service, self)
|
24
|
+
end
|
25
|
+
|
26
|
+
def response_class
|
27
|
+
options.fetch(:response_class, Responses::Forecast)
|
28
|
+
end
|
29
|
+
|
30
|
+
def build_formated_options(options)
|
31
|
+
opts = options.dup
|
32
|
+
{
|
33
|
+
latitude: opts.delete(:latitude),
|
34
|
+
longitude: opts.delete(:longitude),
|
35
|
+
startDate: opts.delete(:start_date) { Date.today.to_s },
|
36
|
+
numDays: opts.delete(:days) { 7 },
|
37
|
+
unit: unit!(opts),
|
38
|
+
format: opts.delete(:format) { '24 hourly' }
|
39
|
+
}.merge!(opts)
|
40
|
+
end
|
41
|
+
|
42
|
+
def unit!(options)
|
43
|
+
u = options.delete(:unit) { :standard }
|
44
|
+
if u == :standard
|
45
|
+
'e'
|
46
|
+
elsif u == :metric
|
47
|
+
'm'
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require_relative 'rest_service'
|
2
|
+
require_relative '../responses/lat_lon_list'
|
3
|
+
|
4
|
+
module NoaaWeatherClient
|
5
|
+
module Services
|
6
|
+
class PostalCodeToCoordinate
|
7
|
+
include RestService
|
8
|
+
|
9
|
+
URL = "http://graphical.weather.gov/xml/sample_products/browser_interface/ndfdXMLclient.php?listZipCodeList="
|
10
|
+
|
11
|
+
def initialize(options = {})
|
12
|
+
@options = options
|
13
|
+
end
|
14
|
+
|
15
|
+
def resolve(zip, options = {})
|
16
|
+
rest_service.object_from_response(:get, build_url(zip), response_class: response_class)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
attr_reader :options
|
22
|
+
|
23
|
+
def rest_service
|
24
|
+
options.fetch(:rest_service, self)
|
25
|
+
end
|
26
|
+
|
27
|
+
def response_class
|
28
|
+
options.fetch(:response_class, Responses::LatLonList)
|
29
|
+
end
|
30
|
+
|
31
|
+
def build_url(zip)
|
32
|
+
"#{URL}#{zip}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require_relative '../rest_client_factory'
|
3
|
+
require_relative '../responses/generic_response'
|
4
|
+
require_relative '../errors'
|
5
|
+
|
6
|
+
module NoaaWeatherClient
|
7
|
+
module RestService
|
8
|
+
def object_from_response(action, url, options = {})
|
9
|
+
response_class = options.fetch(:response_class, Responses::GenericResponse)
|
10
|
+
client = options.fetch(:client) { RestClientFactory.build_client(url: url) }
|
11
|
+
request = build_request_for_action action, url, options
|
12
|
+
response_class.new client.request(request).body
|
13
|
+
rescue Net::HTTPError,
|
14
|
+
Net::HTTPRetriableError,
|
15
|
+
Net::HTTPServerException,
|
16
|
+
Net::HTTPFatalError => e
|
17
|
+
raise Errors::CommunicationError, e.message, e.backtrace
|
18
|
+
end
|
19
|
+
|
20
|
+
# @note Much more to be done here when needed.
|
21
|
+
def build_request_for_action(action, url, options = {})
|
22
|
+
r_class = case action
|
23
|
+
when :get then Net::HTTP::Get
|
24
|
+
end
|
25
|
+
r_class.new url
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'savon'
|
2
|
+
require_relative '../soap_client_factory'
|
3
|
+
require_relative '../responses/generic_response'
|
4
|
+
require_relative '../errors'
|
5
|
+
|
6
|
+
module NoaaWeatherClient
|
7
|
+
module SoapService
|
8
|
+
def object_from_response(soap_action, message, options = {})
|
9
|
+
client = options.fetch(:client, SoapClientFactory.build_client)
|
10
|
+
response_class = options.fetch(:response_class, Responses::GenericResponse)
|
11
|
+
response_class.new client.call(soap_action, message: message).body
|
12
|
+
rescue Savon::Error => e
|
13
|
+
raise Errors::CommunicationError, e.message, e.backtrace
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require_relative 'rest_service'
|
2
|
+
require_relative '../responses/stations'
|
3
|
+
|
4
|
+
module NoaaWeatherClient
|
5
|
+
module Services
|
6
|
+
class WeatherStations
|
7
|
+
include RestService
|
8
|
+
|
9
|
+
URL = 'http://w1.weather.gov/xml/current_obs/index.xml'
|
10
|
+
|
11
|
+
def initialize(options = {})
|
12
|
+
@options = options
|
13
|
+
end
|
14
|
+
|
15
|
+
def fetch(options = {})
|
16
|
+
rest_service.object_from_response(:get, URL, response_class: response_class)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
attr_reader :options
|
22
|
+
|
23
|
+
def rest_service
|
24
|
+
options.fetch(:rest_service, self)
|
25
|
+
end
|
26
|
+
|
27
|
+
def response_class
|
28
|
+
options.fetch(:response_class, Responses::Stations)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'savon'
|
2
|
+
|
3
|
+
module NoaaWeatherClient
|
4
|
+
module SoapClientFactory
|
5
|
+
WSDL = 'http://graphical.weather.gov/xml/DWMLgen/wsdl/ndfdXML.wsdl'
|
6
|
+
|
7
|
+
def self.build_client(options = {})
|
8
|
+
provider = options.fetch(:provider, Savon)
|
9
|
+
wsdl = options.fetch(:wsdl, WSDL)
|
10
|
+
provider.client(wsdl: wsdl,
|
11
|
+
log: false,
|
12
|
+
open_timeout: 10,
|
13
|
+
read_timeout: 30,
|
14
|
+
ssl_verify_mode: :none)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|