ratis 2.5.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +157 -0
- data/lib/ratis.rb +49 -0
- data/lib/ratis/closest_stop.rb +43 -0
- data/lib/ratis/config.rb +21 -0
- data/lib/ratis/core_ext.rb +35 -0
- data/lib/ratis/errors.rb +70 -0
- data/lib/ratis/itinerary.rb +55 -0
- data/lib/ratis/landmark.rb +29 -0
- data/lib/ratis/landmark_category.rb +22 -0
- data/lib/ratis/location.rb +71 -0
- data/lib/ratis/next_bus.rb +93 -0
- data/lib/ratis/point_2_point.rb +112 -0
- data/lib/ratis/request.rb +39 -0
- data/lib/ratis/route.rb +31 -0
- data/lib/ratis/route_stops.rb +40 -0
- data/lib/ratis/schedule.rb +7 -0
- data/lib/ratis/schedule_group.rb +7 -0
- data/lib/ratis/schedule_nearby.rb +79 -0
- data/lib/ratis/schedule_trip.rb +7 -0
- data/lib/ratis/service.rb +7 -0
- data/lib/ratis/stop.rb +12 -0
- data/lib/ratis/timetable.rb +36 -0
- data/lib/ratis/version.rb +33 -0
- data/lib/ratis/walk.rb +37 -0
- metadata +217 -0
@@ -0,0 +1,22 @@
|
|
1
|
+
module Ratis
|
2
|
+
|
3
|
+
class LandmarkCategory
|
4
|
+
|
5
|
+
attr_accessor :type, :description
|
6
|
+
|
7
|
+
def self.all
|
8
|
+
|
9
|
+
response = Request.get 'Getcategories'
|
10
|
+
return [] unless response.success?
|
11
|
+
|
12
|
+
response.to_array(:getcategories_response, :types, :typeinfo).map do |typeinfo|
|
13
|
+
atis_landmark_category = LandmarkCategory.new
|
14
|
+
atis_landmark_category.type = typeinfo[:type]
|
15
|
+
atis_landmark_category.description = typeinfo[:description]
|
16
|
+
atis_landmark_category
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Ratis
|
2
|
+
|
3
|
+
class Location
|
4
|
+
|
5
|
+
attr_accessor :name, :area, :response, :areacode, :latitude, :longitude, :landmark_id, :address, :startaddr, :endaddr, :address_string
|
6
|
+
|
7
|
+
def self.where(conditions)
|
8
|
+
location = conditions.delete :location
|
9
|
+
media = (conditions.delete(:media) || :w).to_s.upcase
|
10
|
+
max_answers = conditions.delete(:max_answers) || 20
|
11
|
+
|
12
|
+
raise ArgumentError.new('You must provide a location') unless location
|
13
|
+
raise ArgumentError.new('You must provide media of A|W|I') unless ['A','W','I'].include? media
|
14
|
+
raise ArgumentError.new('You must provide a numeric max_answers') unless (Integer max_answers rescue false)
|
15
|
+
Ratis.all_conditions_used? conditions
|
16
|
+
|
17
|
+
response = Request.get 'Locate', {'Location' => location, 'Media' => media, 'Maxanswers' => max_answers}
|
18
|
+
return [] unless response.success?
|
19
|
+
|
20
|
+
meta = response.to_hash[:locate_response]
|
21
|
+
locations = response.to_array :locate_response, :location
|
22
|
+
|
23
|
+
locations.map do |location_hash|
|
24
|
+
location = Ratis::Location.new
|
25
|
+
location.name = location_hash[:name]
|
26
|
+
location.area = location_hash[:area]
|
27
|
+
location.response = meta[:responsecode]
|
28
|
+
location.areacode = location_hash[:areacode]
|
29
|
+
location.latitude = location_hash[:latitude]
|
30
|
+
location.longitude = location_hash[:longitude]
|
31
|
+
location.landmark_id = location_hash[:landmarkid] || 0
|
32
|
+
location.address = location_hash[:address] || ''
|
33
|
+
location.startaddr = location_hash[:startaddr] || ''
|
34
|
+
location.endaddr = location_hash[:endaddr] || ''
|
35
|
+
location.address_string = build_address_string location_hash
|
36
|
+
location
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_a
|
41
|
+
[latitude, longitude, name, landmark_id]
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_hash
|
45
|
+
keys = [:latitude, :longitude, :name, :area, :address, :startaddr, :endaddr, :address_string, :landmark_id]
|
46
|
+
Hash[keys.map { |k| [k, send(k)] }]
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def self.build_address_string(location_hash)
|
52
|
+
address_string = ''
|
53
|
+
address = location_hash[:address]
|
54
|
+
name = location_hash[:name]
|
55
|
+
area = location_hash[:area]
|
56
|
+
|
57
|
+
if !address.blank?
|
58
|
+
address_string << "#{address} #{name} (in #{area})"
|
59
|
+
else
|
60
|
+
startaddr = location_hash[:startaddr]
|
61
|
+
if !startaddr.blank?
|
62
|
+
endaddr = location_hash[:endaddr]
|
63
|
+
address_string << "#{startaddr} - #{endaddr} #{name} (in #{area})"
|
64
|
+
else
|
65
|
+
address_string << "#{name} (in #{area})"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module Ratis
|
2
|
+
|
3
|
+
class NextBus
|
4
|
+
|
5
|
+
attr_accessor :stops, :runs
|
6
|
+
|
7
|
+
# Initializes a NextBus object with stops and runs.
|
8
|
+
# @return [NextBus]
|
9
|
+
#
|
10
|
+
# == Parameters:
|
11
|
+
#
|
12
|
+
# [stops] <em>Optional</em> -
|
13
|
+
# An array of stops. Defaults to empty array.
|
14
|
+
# [runs] <em>Optional</em> -
|
15
|
+
# An array of runs. Defaults to empty array.
|
16
|
+
|
17
|
+
def initialize(_stops = [], _runs = [])
|
18
|
+
@stops, @runs = _stops, _runs
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns results of NextBus query containing arrays of stops and runs.
|
22
|
+
# @return [NextBus] containing next buses.
|
23
|
+
#
|
24
|
+
# == Parameters:
|
25
|
+
#
|
26
|
+
# Takes hash of conditions
|
27
|
+
#
|
28
|
+
# [option1] <b>Required</b> -
|
29
|
+
# Description of required param
|
30
|
+
# [option2] <em>Optional</em> -
|
31
|
+
# Description of optional param
|
32
|
+
# [option3] <em>Optional</em> -
|
33
|
+
# Description of optional param
|
34
|
+
# [option4] <em>Optional</em> -
|
35
|
+
# Description of optional param
|
36
|
+
|
37
|
+
def self.where(conditions)
|
38
|
+
stop_id = conditions.delete :stop_id
|
39
|
+
app_id = conditions.delete(:app_id) || 'na'
|
40
|
+
|
41
|
+
raise ArgumentError.new('You must provide a stop ID') unless stop_id
|
42
|
+
Ratis.all_conditions_used? conditions
|
43
|
+
|
44
|
+
response = Request.get 'Nextbus2', { 'Stopid' => stop_id, 'Appid' => app_id }
|
45
|
+
return [] unless response.success?
|
46
|
+
|
47
|
+
stops = response.to_array :nextbus2_response, :stops, :stop
|
48
|
+
runs = response.to_array :nextbus2_response, :runs, :run
|
49
|
+
|
50
|
+
NextBus.new stops, runs
|
51
|
+
end
|
52
|
+
|
53
|
+
# Gets description of first stop
|
54
|
+
# @return [String] Description of first stop or nil.
|
55
|
+
|
56
|
+
def first_stop_description
|
57
|
+
stops.first ? stops.first[:description] : nil
|
58
|
+
end
|
59
|
+
|
60
|
+
# Details of NextBus instance in a hash.
|
61
|
+
# @return [Hash] NextBus details in a hash.
|
62
|
+
|
63
|
+
def to_hash
|
64
|
+
{ :stopname => first_stop_description,
|
65
|
+
:signs => runs.map { |run| run[:sign] }.uniq,
|
66
|
+
:runs => runs.map do |run|
|
67
|
+
{ :time => run[:estimatedtime],
|
68
|
+
:sign => run[:sign],
|
69
|
+
:adherence => run[:adherence],
|
70
|
+
:route => run[:route]
|
71
|
+
}
|
72
|
+
end
|
73
|
+
}
|
74
|
+
end
|
75
|
+
|
76
|
+
# Details of NextBus instance in a hash to be transformed to xml
|
77
|
+
# @private
|
78
|
+
|
79
|
+
def to_hash_for_xml
|
80
|
+
{ :stopname => first_stop_description,
|
81
|
+
:runs => runs.map do |run|
|
82
|
+
{ :time => run[:estimatedtime],
|
83
|
+
:scheduled_time => run[:triptime],
|
84
|
+
:sign => run[:sign],
|
85
|
+
:adherence => run[:adherence],
|
86
|
+
:route => run[:route]
|
87
|
+
}
|
88
|
+
end
|
89
|
+
}
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
module Ratis
|
2
|
+
|
3
|
+
class Point2Point
|
4
|
+
|
5
|
+
def self.where(conditions)
|
6
|
+
routes_only = conditions.delete(:routes_only)
|
7
|
+
|
8
|
+
origin_lat = conditions.delete(:origin_lat).to_f
|
9
|
+
origin_long = conditions.delete(:origin_long).to_f
|
10
|
+
destination_lat = conditions.delete(:destination_lat).to_f
|
11
|
+
destination_long = conditions.delete(:destination_long).to_f
|
12
|
+
|
13
|
+
date = conditions.delete :date
|
14
|
+
start_time = conditions.delete :start_time
|
15
|
+
end_time = conditions.delete :end_time
|
16
|
+
|
17
|
+
raise ArgumentError.new("You must specify routes only with true, false, 'y' or 'n'") unless routes_only.y_or_n rescue false
|
18
|
+
|
19
|
+
raise ArgumentError.new('You must provide an origin latitude') unless Ratis.valid_latitude? origin_lat
|
20
|
+
raise ArgumentError.new('You must provide an origin longitude') unless Ratis.valid_longitude? origin_long
|
21
|
+
raise ArgumentError.new('You must provide an destination latitude') unless Ratis.valid_latitude? destination_lat
|
22
|
+
raise ArgumentError.new('You must provide an destination longitude') unless Ratis.valid_longitude? destination_long
|
23
|
+
|
24
|
+
raise ArgumentError.new('You must provide a date DD/MM/YYYY') unless DateTime.strptime(date, '%d/%m/%Y') rescue false
|
25
|
+
raise ArgumentError.new('You must provide a start time as 24-hour HHMM') unless DateTime.strptime(start_time, '%H%M') rescue false
|
26
|
+
raise ArgumentError.new('You must provide an end time as 24-hour HHMM') unless DateTime.strptime(end_time, '%H%M') rescue false
|
27
|
+
|
28
|
+
Ratis.all_conditions_used? conditions
|
29
|
+
|
30
|
+
response = Request.get 'Point2point', 'Routesonly' => routes_only.y_or_n.upcase,
|
31
|
+
'Originlat' => origin_lat, 'Originlong' => origin_long,
|
32
|
+
'Destinationlat' => destination_lat, 'Destinationlong' => destination_long,
|
33
|
+
'Date' => date, 'Starttime' => start_time, 'Endtime' => end_time
|
34
|
+
|
35
|
+
return nil unless response.success?
|
36
|
+
|
37
|
+
return parse_routes_only_yes response if routes_only.y_or_n.downcase == 'y'
|
38
|
+
return parse_routes_only_no response if routes_only.y_or_n.downcase == 'n'
|
39
|
+
|
40
|
+
nil
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def self.parse_routes_only_yes(response)
|
46
|
+
response.to_array(:point2point_response, :routes, :service).map do |service|
|
47
|
+
atis_service = Service.new
|
48
|
+
atis_service.route = service[:route]
|
49
|
+
atis_service.direction = service[:direction]
|
50
|
+
atis_service.service_type = service[:servicetype]
|
51
|
+
atis_service.signage = service[:signage]
|
52
|
+
atis_service.route_type = service[:routetype]
|
53
|
+
atis_service
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.parse_routes_only_no(response)
|
58
|
+
return nil unless response.success?
|
59
|
+
|
60
|
+
atis_schedule = Schedule.new
|
61
|
+
atis_schedule.groups = response.to_array(:point2point_response, :groups, :group).map do |group|
|
62
|
+
atis_schedule_group = ScheduleGroup.new
|
63
|
+
|
64
|
+
# Point2point 1.3 uses inconsistent tag naming, thus: <onstop> <onwalk...>, but <offstop> <offstopwalk...>
|
65
|
+
# this docs says this is fixed in 1.4, so watch out
|
66
|
+
atis_schedule_group.on_stop = atis_stop_from_hash 'on', group[:onstop]
|
67
|
+
atis_schedule_group.off_stop = atis_stop_from_hash 'offstop', group[:offstop]
|
68
|
+
|
69
|
+
atis_schedule_group.trips = group.to_array(:trips, :trip).map do |trip|
|
70
|
+
atis_trip = ScheduleTrip.new
|
71
|
+
atis_trip.on_time = trip[:ontime]
|
72
|
+
atis_trip.off_time = trip[:offtime]
|
73
|
+
|
74
|
+
atis_trip.service = trip.to_array(:service).map do |service|
|
75
|
+
atis_service = Service.new
|
76
|
+
|
77
|
+
atis_service.route = service[:route]
|
78
|
+
atis_service.direction = service[:direction]
|
79
|
+
atis_service.service_type = service[:servicetype]
|
80
|
+
atis_service.signage = service[:signage]
|
81
|
+
atis_service.route_type = service[:routetype]
|
82
|
+
atis_service.exception = service[:exception]
|
83
|
+
atis_service
|
84
|
+
end.first
|
85
|
+
|
86
|
+
atis_trip
|
87
|
+
end
|
88
|
+
|
89
|
+
atis_schedule_group
|
90
|
+
end
|
91
|
+
|
92
|
+
atis_schedule
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.atis_stop_from_hash(prefix, stop)
|
96
|
+
return nil if stop.blank?
|
97
|
+
|
98
|
+
atis_stop = Stop.new
|
99
|
+
atis_stop.description = stop[:description]
|
100
|
+
atis_stop.atis_stop_id = stop[:atisstopid].to_i
|
101
|
+
atis_stop.latitude = stop[:lat].to_f
|
102
|
+
atis_stop.longitude = stop[:long].to_f
|
103
|
+
|
104
|
+
# It appears that both *walk and *walkdist are used for the walk distance, covering both here
|
105
|
+
atis_stop.walk_dist = (stop["#{prefix}walk".to_sym] || stop["#{prefix}walkdist".to_sym]).to_f
|
106
|
+
atis_stop.walk_dir = stop["#{prefix}walkdir".to_sym]
|
107
|
+
atis_stop.walk_hint = stop["#{prefix}walkhint".to_sym]
|
108
|
+
atis_stop
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Ratis
|
2
|
+
|
3
|
+
class Request
|
4
|
+
|
5
|
+
extend Savon::Model
|
6
|
+
|
7
|
+
def initialize(config = nil)
|
8
|
+
config = Ratis.config if config.nil?
|
9
|
+
raise Errors::ConfigError('It appears that Ratis.configure has not been called') unless config.valid?
|
10
|
+
self.class.client do
|
11
|
+
wsdl.endpoint = Ratis.config.endpoint
|
12
|
+
wsdl.namespace = Ratis.config.namespace
|
13
|
+
http.proxy = Ratis.config.proxy unless Ratis.config.proxy.blank?
|
14
|
+
http.open_timeout = Ratis.config.timeout unless Ratis.config.timeout.blank?
|
15
|
+
end
|
16
|
+
rescue ArgumentError => e
|
17
|
+
raise ArgumentError.new 'Invalid ATIS SOAP server configuration: ' + e.message
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.get(action, params = {})
|
21
|
+
begin
|
22
|
+
req = new Ratis.config
|
23
|
+
response = client.request action, :soap_action => "#{Ratis.config.namespace}##{action}", :xmlns => Ratis.config.namespace do
|
24
|
+
soap.body = params unless params.blank?
|
25
|
+
end
|
26
|
+
|
27
|
+
version = response.to_hash["#{action.downcase}_response".to_sym][:version]
|
28
|
+
|
29
|
+
response
|
30
|
+
rescue Errno::ECONNREFUSED => e
|
31
|
+
raise Errno::ECONNREFUSED.new 'Refused request to ATIS SOAP server'
|
32
|
+
rescue Savon::SOAP::Fault => e
|
33
|
+
raise Errors::SoapError.new e
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
data/lib/ratis/route.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
module Ratis
|
2
|
+
|
3
|
+
class Route
|
4
|
+
|
5
|
+
attr_accessor :short_name, :directions
|
6
|
+
|
7
|
+
def self.all
|
8
|
+
response = Request.get 'Allroutes'
|
9
|
+
return [] unless response.success?
|
10
|
+
|
11
|
+
routes = response.to_hash[:allroutes_response][:routes].split(/\n/)
|
12
|
+
atis_routes = routes.map do |r|
|
13
|
+
r.strip!
|
14
|
+
next if r.blank?
|
15
|
+
r = r.split(/, /)
|
16
|
+
Route.new r[0].strip, r[1..-1].map(&:strip)
|
17
|
+
end
|
18
|
+
atis_routes.compact
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(short_name, directions)
|
22
|
+
self.short_name = short_name
|
23
|
+
self.directions = directions
|
24
|
+
end
|
25
|
+
|
26
|
+
def timetable(conditions)
|
27
|
+
Timetable.where conditions.merge :route_short_name => short_name
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Ratis
|
2
|
+
|
3
|
+
################################# EXAMPLE ############################################
|
4
|
+
#
|
5
|
+
# Ratis::RouteStops.all :route => 8, :direction => 'S', :order => 'S'
|
6
|
+
#
|
7
|
+
######################################################################################
|
8
|
+
|
9
|
+
class RouteStops
|
10
|
+
|
11
|
+
def self.all(conditions)
|
12
|
+
route = conditions.delete :route
|
13
|
+
direction = conditions.delete(:direction).to_s.upcase
|
14
|
+
order = conditions.delete(:order).to_s.upcase
|
15
|
+
|
16
|
+
raise ArgumentError.new('You must provide a route') unless route
|
17
|
+
raise ArgumentError.new('You must provide a direction') unless direction
|
18
|
+
|
19
|
+
Ratis.all_conditions_used? conditions
|
20
|
+
|
21
|
+
request_params = {'Route' => route, 'Direction' => direction }
|
22
|
+
request_params.merge! order ? { 'Order' => order } : {}
|
23
|
+
response = Request.get 'Routestops', request_params
|
24
|
+
|
25
|
+
return [] unless response.success?
|
26
|
+
|
27
|
+
response.to_hash[:routestops_response][:stops][:stop].map do |s|
|
28
|
+
stop = Stop.new
|
29
|
+
stop.description = s[:description]
|
30
|
+
stop.area = s[:area]
|
31
|
+
stop.atis_stop_id = s[:atisstopid]
|
32
|
+
stop.stop_seq = s[:stopseq]
|
33
|
+
stop.latitude, stop.longitude = s[:point].split ','
|
34
|
+
stop
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Ratis
|
2
|
+
|
3
|
+
class ScheduleNearby
|
4
|
+
|
5
|
+
attr_accessor :atstops
|
6
|
+
|
7
|
+
def self.where(conditions)
|
8
|
+
latitude = conditions.delete :latitude
|
9
|
+
longitude = conditions.delete :longitude
|
10
|
+
date = conditions.delete :date
|
11
|
+
time = conditions.delete :time
|
12
|
+
window = conditions.delete :window
|
13
|
+
walk_distance = conditions.delete :walk_distance
|
14
|
+
landmark_id = conditions.delete :landmark_id
|
15
|
+
stop_id = conditions.delete(:stop_id) || ''
|
16
|
+
app_id = conditions.delete(:app_id) || 'na'
|
17
|
+
|
18
|
+
raise ArgumentError.new('You must provide latitude') unless latitude
|
19
|
+
raise ArgumentError.new('You must provide longitude') unless longitude
|
20
|
+
raise ArgumentError.new('You must provide date') unless date
|
21
|
+
raise ArgumentError.new('You must provide time') unless time
|
22
|
+
raise ArgumentError.new('You must provide window') unless window
|
23
|
+
raise ArgumentError.new('You must provide walk_distance') unless walk_distance
|
24
|
+
raise ArgumentError.new('You must provide landmark_id') unless landmark_id
|
25
|
+
Ratis.all_conditions_used? conditions
|
26
|
+
|
27
|
+
response = Request.get 'Schedulenearby',
|
28
|
+
{'Locationlat' => latitude, 'Locationlong' => longitude,
|
29
|
+
'Date' => date, 'Time' => time, 'Window' => window, 'Walkdist' => walk_distance,
|
30
|
+
'Landmarkid' => landmark_id, 'Stopid' => stop_id, 'Appid' => app_id
|
31
|
+
}
|
32
|
+
|
33
|
+
return [] unless response.success?
|
34
|
+
|
35
|
+
atstops = response.to_array :schedulenearby_response, :atstop
|
36
|
+
atstops.map do |atstop|
|
37
|
+
atstop[:services] = atstop.to_array :service
|
38
|
+
|
39
|
+
atstop[:services].map do |service|
|
40
|
+
service[:tripinfos] = service.to_array :tripinfo
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
schedule_nearby = ScheduleNearby.new
|
45
|
+
schedule_nearby.atstops = atstops
|
46
|
+
|
47
|
+
schedule_nearby
|
48
|
+
end
|
49
|
+
|
50
|
+
def to_hash
|
51
|
+
{
|
52
|
+
:atstops => atstops.map do |atstop|
|
53
|
+
{
|
54
|
+
:description => atstop[:description],
|
55
|
+
:walkdist => atstop[:walkdist],
|
56
|
+
:walkdir => atstop[:walkdir],
|
57
|
+
:stopid => atstop[:stopid],
|
58
|
+
:lat => atstop[:lat],
|
59
|
+
:long => atstop[:long],
|
60
|
+
:services => atstop[:services].map do |service|
|
61
|
+
{
|
62
|
+
:route => service[:route],
|
63
|
+
:routetype => service[:routetype],
|
64
|
+
:operator => service[:operator],
|
65
|
+
:sign => service[:sign],
|
66
|
+
:times => service[:times],
|
67
|
+
:trips => service[:tripinfos].map do |tripinfo|
|
68
|
+
{ :triptime => tripinfo[:triptime] }
|
69
|
+
end
|
70
|
+
}
|
71
|
+
end
|
72
|
+
}
|
73
|
+
end
|
74
|
+
}
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|