noaa_weather_client 0.0.1
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.
- 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,29 @@
|
|
1
|
+
<?xml version="1.0"?>
|
2
|
+
|
3
|
+
<!-- **********************************************************************
|
4
|
+
|
5
|
+
summarizationType.xsd
|
6
|
+
|
7
|
+
John L. Schattel MDL 29 June 2009
|
8
|
+
Red Hat Linux Apache Server
|
9
|
+
|
10
|
+
|
11
|
+
************************************************************************* -->
|
12
|
+
|
13
|
+
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
14
|
+
|
15
|
+
<xsd:simpleType name="summarizationType">
|
16
|
+
<xsd:restriction base="xsd:string">
|
17
|
+
<xsd:enumeration value="none" />
|
18
|
+
<xsd:enumeration value="mean" />
|
19
|
+
<xsd:enumeration value="maximum" />
|
20
|
+
<xsd:enumeration value="minimum" />
|
21
|
+
<xsd:enumeration value="12hourly" />
|
22
|
+
<xsd:enumeration value="24hourly" />
|
23
|
+
<xsd:enumeration value="national" />
|
24
|
+
<xsd:enumeration value="conus" />
|
25
|
+
<xsd:enumeration value="alaska" />
|
26
|
+
</xsd:restriction>
|
27
|
+
</xsd:simpleType>
|
28
|
+
|
29
|
+
</xsd:schema>
|
@@ -0,0 +1,51 @@
|
|
1
|
+
<?xml version="1.0"?>
|
2
|
+
|
3
|
+
<!-- **********************************************************************
|
4
|
+
|
5
|
+
time_layout.xsd
|
6
|
+
|
7
|
+
John L. Schattel MDL 4 August 2004
|
8
|
+
Red Hat Linux Apache Server
|
9
|
+
|
10
|
+
<xsd:include schemaLocation="http://graphical.weather.gov/xml/DWMLgen/schema/summarizationType.xsd" />
|
11
|
+
|
12
|
+
************************************************************************* -->
|
13
|
+
|
14
|
+
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
15
|
+
|
16
|
+
<xsd:include schemaLocation="summarizationType.xsd" />
|
17
|
+
|
18
|
+
<xsd:complexType name="time-layoutElementType">
|
19
|
+
<xsd:sequence>
|
20
|
+
<xsd:element name="layout-key" type="layout-keyType" minOccurs="1" maxOccurs="1" />
|
21
|
+
<xsd:sequence minOccurs="1" maxOccurs="unbounded">
|
22
|
+
<xsd:element name="start-valid-time" type="start-valid-timeType" minOccurs="1" maxOccurs="unbounded" />
|
23
|
+
<xsd:element name="end-valid-time" type="xsd:dateTime" minOccurs="0" maxOccurs="unbounded" />
|
24
|
+
</xsd:sequence>
|
25
|
+
</xsd:sequence>
|
26
|
+
<xsd:attribute name="time-coordinate" type="time-coordinateType" use="required" />
|
27
|
+
<xsd:attribute name="summarization" type="summarizationType" use="optional" />
|
28
|
+
</xsd:complexType>
|
29
|
+
|
30
|
+
<xsd:simpleType name="time-coordinateType">
|
31
|
+
<xsd:restriction base="xsd:string">
|
32
|
+
<xsd:enumeration value="UTC" />
|
33
|
+
<xsd:enumeration value="local" />
|
34
|
+
</xsd:restriction>
|
35
|
+
</xsd:simpleType>
|
36
|
+
|
37
|
+
<xsd:complexType name="start-valid-timeType">
|
38
|
+
<xsd:simpleContent>
|
39
|
+
<xsd:extension base="xsd:dateTime">
|
40
|
+
<xsd:attribute name="period-name" type="xsd:string" />
|
41
|
+
</xsd:extension>
|
42
|
+
</xsd:simpleContent>
|
43
|
+
</xsd:complexType>
|
44
|
+
|
45
|
+
<xsd:simpleType name="layout-keyType">
|
46
|
+
<xsd:restriction base="xsd:string">
|
47
|
+
<xsd:pattern value="k-p\d+[h|d|m|y]-n\d+-\d+" />
|
48
|
+
</xsd:restriction>
|
49
|
+
</xsd:simpleType>
|
50
|
+
|
51
|
+
</xsd:schema>
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'noaa_weather_client/cli/templates'
|
2
|
+
|
3
|
+
module NoaaWeatherClient
|
4
|
+
class CLI
|
5
|
+
def self.postal_code_to_coordinate(postal_code, buffer = STDOUT)
|
6
|
+
client = NoaaWeatherClient.build_client
|
7
|
+
coordinate = client.postal_code_to_coordinate postal_code
|
8
|
+
buffer.puts Templates::PostalCode.new(coordinate).to_s
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_writer :buffer
|
12
|
+
|
13
|
+
def initialize(latitude, longitude)
|
14
|
+
@coordinate = Coordinate.new(latitude, longitude)
|
15
|
+
raise ArgumentError, "Invalid coordinate #{latitude}, #{longitude}" unless coordinate.valid?
|
16
|
+
end
|
17
|
+
|
18
|
+
def render(*features)
|
19
|
+
features.each { |f| render_feature buffer, f }
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
attr_reader :coordinate
|
25
|
+
|
26
|
+
def render_feature(buffer, feature)
|
27
|
+
if feature.to_s == 'observations'
|
28
|
+
observations = client.current_observations(coordinate.latitude, coordinate.longitude)
|
29
|
+
buffer.puts Templates::CurrentObservations.new(observations).to_s
|
30
|
+
elsif feature.to_s == 'forecast'
|
31
|
+
forecast = client.forecast_by_day(coordinate.latitude, coordinate.longitude)
|
32
|
+
buffer.puts Templates::Forecast.new(forecast).to_s
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def buffer
|
37
|
+
@buffer || STDOUT
|
38
|
+
end
|
39
|
+
|
40
|
+
def client
|
41
|
+
@client ||= NoaaWeatherClient.build_client
|
42
|
+
end
|
43
|
+
|
44
|
+
Coordinate = Struct.new(:latitude, :longitude) do
|
45
|
+
def valid?
|
46
|
+
latitude && longitude rescue false
|
47
|
+
end
|
48
|
+
|
49
|
+
def latitude; Float(self[:latitude]) end
|
50
|
+
def longitude; Float(self[:longitude]) end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module NoaaWeatherClient
|
2
|
+
module Templates
|
3
|
+
CurrentObservations = Struct.new(:observation) do
|
4
|
+
def to_s
|
5
|
+
<<-template
|
6
|
+
Current Observations
|
7
|
+
================================================================================
|
8
|
+
StationID: #{observation.station_id}
|
9
|
+
Station Name: #{observation.location}
|
10
|
+
Location: #{observation.latitude}, #{observation.longitude}
|
11
|
+
Observation Time: #{observation.observation_time_string}
|
12
|
+
Temperature: #{observation.temperature_fahrenheit} | #{observation.temperature_celsius}
|
13
|
+
Dewpoint: #{observation.dewpoint_fahrenheit} | #{observation.dewpoint_celsius}
|
14
|
+
Pressure: #{observation.pressure_in} in | #{observation.pressure_mb} mb
|
15
|
+
Humidity: #{observation.relative_humidity}
|
16
|
+
Wind: #{observation.wind_string}
|
17
|
+
Wind Direction: #{observation.wind_dir} | #{observation.wind_degrees} degrees
|
18
|
+
Wind Speed: #{observation.wind_mph} mph | #{observation.wind_kt} kt
|
19
|
+
Visibility: #{observation.visibility_mi} mi
|
20
|
+
|
21
|
+
template
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
PostalCode = Struct.new(:coordinate) do
|
26
|
+
def to_s
|
27
|
+
"#{coordinate.latitude} #{coordinate.longitude}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
ForecastDay = Struct.new(:day) do
|
32
|
+
def to_s
|
33
|
+
<<-template
|
34
|
+
Day: #{day.name}
|
35
|
+
--------------------------------------------------------------------------------
|
36
|
+
High: #{day.maximum_temperature} F
|
37
|
+
Low: #{day.minimum_temperature} F
|
38
|
+
Summary: #{day.weather_summary}
|
39
|
+
|
40
|
+
template
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
Forecast = Struct.new(:forecast) do
|
45
|
+
def to_s
|
46
|
+
template = "\n#{forecast.days.size}-Day Forecast\n"
|
47
|
+
template += "================================================================================\n"
|
48
|
+
forecast.days.each { |d| template += ForecastDay.new(d).to_s }
|
49
|
+
template
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require_relative 'services/forecast_by_day'
|
2
|
+
require_relative 'services/weather_stations'
|
3
|
+
require_relative 'services/current_observations'
|
4
|
+
require_relative 'services/find_nearest_station'
|
5
|
+
require_relative 'services/postal_code_to_coordinate'
|
6
|
+
|
7
|
+
module NoaaWeatherClient
|
8
|
+
# Provides entry point for interacting with the noaa api via multiple services.
|
9
|
+
class Client
|
10
|
+
# Fetches a daily forecast for a location from today. Default is 7 days.
|
11
|
+
# The forecast for the current day ceases to be returned after 6:00pm at the
|
12
|
+
# observation area.
|
13
|
+
# @param lat [Float] latitude
|
14
|
+
# @param lon [Float] longitude
|
15
|
+
# @return [Responses::Forecast] a list of daily forecasts.
|
16
|
+
def forecast_by_day(lat, lon, options = {})
|
17
|
+
Services::ForecastByDay.new.fetch(lat, lon, options)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Retrieves a list of weather stations from noaa.
|
21
|
+
# @return [Responses::Stations] a list of weather stations.
|
22
|
+
def weather_stations(options = {})
|
23
|
+
if options.delete(:reload) { false } || @weather_stations.nil?
|
24
|
+
@weather_stations = Services::WeatherStations.new(options).fetch
|
25
|
+
end
|
26
|
+
@weather_stations
|
27
|
+
end
|
28
|
+
|
29
|
+
# Retrieves the current weather observations for a location.
|
30
|
+
# @note It is important to cache a copy of the available stations for use here, as the xml stations response is quite large noaa does not appreciate repeated calls.
|
31
|
+
# @param lat [Float] latitude
|
32
|
+
# @param lon [Float] longitude
|
33
|
+
# @param [Hash] options
|
34
|
+
# @option options [Responses::Stations] stations A cached stations response to prevent having to query noaa for the list.
|
35
|
+
# @return [Responses::CurrentObservation]
|
36
|
+
def current_observations(lat, lon, options = {})
|
37
|
+
station = options.fetch(:station) { nearest_weather_station(lat, lon, options) }
|
38
|
+
Services::CurrentObservations.new(options).fetch(station)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Retrieves the weather station nearest to the location.
|
42
|
+
# @note It is important to cache a copy of the available stations for use here, as the xml stations response is quite large noaa does not appreciate repeated calls.
|
43
|
+
# @param lat [Float] latitude
|
44
|
+
# @param lon [Float] longitude
|
45
|
+
# @param [Hash] options
|
46
|
+
# @option options [Responses::Stations] stations A cached stations response to prevent having to query noaa for the list.
|
47
|
+
# @return [Responses::Station]
|
48
|
+
def nearest_weather_station(lat, lon, options = {})
|
49
|
+
stations = options.fetch(:stations) { weather_stations }
|
50
|
+
Services::FindNearestStation.find(lat, lon, stations)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Converts a zip code to a latitude and longitude.
|
54
|
+
# @param lat [Float] latitude
|
55
|
+
# @param lon [Float] longitude
|
56
|
+
# @return [Responses::LatLonList]
|
57
|
+
def postal_code_to_coordinate(postal_code, options = {})
|
58
|
+
Services::PostalCodeToCoordinate.new(options).resolve(postal_code)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,93 @@
|
|
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 CurrentObservation
|
8
|
+
include ReactiveXmlResponse
|
9
|
+
include ValidatableXmlResponse
|
10
|
+
|
11
|
+
def initialize(response)
|
12
|
+
@source = XmlParserFactory.build_parser.parse response
|
13
|
+
validate! @source, :current_observation
|
14
|
+
init
|
15
|
+
end
|
16
|
+
|
17
|
+
def temperature_celsius
|
18
|
+
@temp_c.to_f
|
19
|
+
end
|
20
|
+
|
21
|
+
def temperature_fahrenheit
|
22
|
+
@temp_f.to_f
|
23
|
+
end
|
24
|
+
|
25
|
+
def dewpoint_celsius
|
26
|
+
@dewpoint_c.to_f
|
27
|
+
end
|
28
|
+
|
29
|
+
def dewpoint_fahrenheit
|
30
|
+
@dewpoint_f.to_f
|
31
|
+
end
|
32
|
+
|
33
|
+
def latitude
|
34
|
+
@latitude.to_f
|
35
|
+
end
|
36
|
+
|
37
|
+
def longitude
|
38
|
+
@longitude.to_f
|
39
|
+
end
|
40
|
+
|
41
|
+
def observation_time_string
|
42
|
+
@observation_time
|
43
|
+
end
|
44
|
+
|
45
|
+
def observation_time
|
46
|
+
Time.parse(@observation_time_rfc822.to_s) if @observation_time_rfc822
|
47
|
+
end
|
48
|
+
|
49
|
+
def pressure_in
|
50
|
+
@pressure_in.to_f
|
51
|
+
end
|
52
|
+
|
53
|
+
def pressure_mb
|
54
|
+
@pressure_mb.to_f
|
55
|
+
end
|
56
|
+
|
57
|
+
def relative_humidity
|
58
|
+
@relative_humidity.to_f
|
59
|
+
end
|
60
|
+
|
61
|
+
def wind_degrees
|
62
|
+
@wind_degrees.to_f
|
63
|
+
end
|
64
|
+
|
65
|
+
def wind_kt
|
66
|
+
@wind_kt.to_f
|
67
|
+
end
|
68
|
+
|
69
|
+
def wind_mph
|
70
|
+
@wind_mph.to_f
|
71
|
+
end
|
72
|
+
|
73
|
+
def visibility_mi
|
74
|
+
@visibility_mi.to_f
|
75
|
+
end
|
76
|
+
|
77
|
+
def to_hash
|
78
|
+
arr = instance_variables.map { |v|
|
79
|
+
[ v.to_s[1..-1], instance_variable_get(v) ] unless v =~ /source/
|
80
|
+
}.compact
|
81
|
+
Hash[arr]
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def init
|
87
|
+
source.root.elements.each do |e|
|
88
|
+
instance_variable_set("@#{e.name}", e.text)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require_relative '../xml_parser_factory'
|
2
|
+
require_relative 'validatable_xml_response'
|
3
|
+
require 'ostruct'
|
4
|
+
|
5
|
+
module NoaaWeatherClient
|
6
|
+
module Responses
|
7
|
+
class Forecast
|
8
|
+
include Enumerable
|
9
|
+
include ValidatableXmlResponse
|
10
|
+
|
11
|
+
def initialize(hashed_response)
|
12
|
+
@body = XmlParserFactory.build_parser.parse hashed_response[:ndf_dgen_by_day_response][:dwml_by_day_out]
|
13
|
+
validate! @body, :dwml
|
14
|
+
end
|
15
|
+
|
16
|
+
def each
|
17
|
+
days.each { |d| yield d }
|
18
|
+
end
|
19
|
+
|
20
|
+
def size
|
21
|
+
days.size
|
22
|
+
end
|
23
|
+
|
24
|
+
def days
|
25
|
+
@days ||= build_days
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
attr_reader :body
|
31
|
+
|
32
|
+
def time_layout
|
33
|
+
@time_layout ||= body.css("time-layout[summarization='24hourly']").first
|
34
|
+
end
|
35
|
+
|
36
|
+
def layout_key
|
37
|
+
@layout_key ||= time_layout.css('layout-key').text
|
38
|
+
end
|
39
|
+
|
40
|
+
def parameters
|
41
|
+
@paramteters ||= body.css('parameters')
|
42
|
+
end
|
43
|
+
|
44
|
+
def time_layout_periods
|
45
|
+
unless @time_layout_periods
|
46
|
+
periods = time_layout.element_children.to_a
|
47
|
+
periods.shift #remove layout-key
|
48
|
+
@time_layout_periods = [].tap do |arr|
|
49
|
+
periods.each_slice(2) do |slice|
|
50
|
+
name = slice.first['period-name']
|
51
|
+
arr << TimePeriod.new(*slice, name)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
@time_layout_periods
|
56
|
+
end
|
57
|
+
|
58
|
+
def build_days
|
59
|
+
unless @days
|
60
|
+
@day = [].tap do |arr|
|
61
|
+
time_layout_periods.each_with_index do |p, i|
|
62
|
+
params = build_day_params p, i
|
63
|
+
arr << Day.new(params)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def build_day_params(period, index)
|
70
|
+
{
|
71
|
+
start_time: Time.parse(period.start_time.to_s),
|
72
|
+
end_time: Time.parse(period.end_time.to_s),
|
73
|
+
name: Time.parse(period.start_time.to_s).strftime("%A"),
|
74
|
+
maximum_temperature: parameters.css('temperature[type=maximum] value')[index].text.to_f,
|
75
|
+
minimum_temperature: parameters.css('temperature[type=minimum] value')[index].text.to_f,
|
76
|
+
weather_summary: parameters.css('weather weather-conditions')[index]['weather-summary']
|
77
|
+
}
|
78
|
+
end
|
79
|
+
|
80
|
+
class Day < OpenStruct; end
|
81
|
+
TimePeriod = Struct.new(:start_time, :end_time, :name)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|