vatsim_online 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/Guardfile +5 -0
- data/LICENSE +22 -0
- data/README.md +202 -0
- data/Rakefile +7 -0
- data/lib/vatsim_online.rb +16 -0
- data/lib/vatsim_online/data_downloader.rb +58 -0
- data/lib/vatsim_online/station.rb +27 -0
- data/lib/vatsim_online/station_parser.rb +51 -0
- data/lib/vatsim_online/version.rb +3 -0
- data/spec/data_downloader_spec.rb +91 -0
- data/spec/data_downloader_spec_helper.rb +4 -0
- data/spec/spec_helper.rb +3 -0
- data/spec/station_parser_spec.rb +111 -0
- data/spec/station_parser_spec_helper.rb +7 -0
- data/spec/vatsim_data.txt +608 -0
- data/spec/vatsim_online_spec.rb +54 -0
- data/vatsim_online.gemspec +26 -0
- metadata +192 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Svilen Vassilev
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,202 @@
|
|
1
|
+
# Vatsim Online
|
2
|
+
|
3
|
+
A Ruby gem for selectively pulling, parsing and displaying Vatsim online
|
4
|
+
stations data. Essentially it's a "Who's online" library, capable of displaying
|
5
|
+
online ATC and/or pilots for given airports, areas or globally. Stations are
|
6
|
+
returned as objects, exposing a rich set of attributes. Vatsim data is pulled
|
7
|
+
on preset intervals and cached locally to avoid flooding the servers.
|
8
|
+
|
9
|
+
### Badges of (dis)honour
|
10
|
+
|
11
|
+
* Testing (Travis CI): [![Build Status](https://secure.travis-ci.org/tarakanbg/vatsim_online.png?branch=master)](http://travis-ci.org/tarakanbg/vatsim_online)
|
12
|
+
* Code Analysis (CodeClimate): [![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/tarakanbg/vatsim_online)
|
13
|
+
* Dependencies: (Gemnasium) [![Gemnasium](https://gemnasium.com/tarakanbg/vatsim_online.png?travis)](https://gemnasium.com/tarakanbg/vatsim_online)
|
14
|
+
|
15
|
+
## Installation
|
16
|
+
|
17
|
+
Add this line to your application's Gemfile:
|
18
|
+
|
19
|
+
gem 'vatsim_online'
|
20
|
+
|
21
|
+
And then execute:
|
22
|
+
|
23
|
+
$ bundle
|
24
|
+
|
25
|
+
Or install it yourself as:
|
26
|
+
|
27
|
+
$ gem install vatsim_online
|
28
|
+
|
29
|
+
## Usage
|
30
|
+
|
31
|
+
This gem provides one public method: `vatsim_online`, which can be applied to
|
32
|
+
any string (or variable containing a string) representing a full or partial ICAO
|
33
|
+
code. The provided ICAO code or fragment will be used as a search criteria and
|
34
|
+
matched against the current vatsim data.
|
35
|
+
|
36
|
+
For example if you want to retrieve all active stations (ATC positions and pilots)
|
37
|
+
for Vienna airport (ICAO: LOWW), then you can use:
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
# Attaching the method directly to a string:
|
41
|
+
"LOWW".vatsim_online
|
42
|
+
|
43
|
+
# Attaching the method to a variable containing a string:
|
44
|
+
icao = "LOWW"
|
45
|
+
icao.vatsim_online
|
46
|
+
```
|
47
|
+
If you want to retrieve the currently active stations for an entire region
|
48
|
+
(FIR/ARTCC), then you can use the first 2-3 letters of the region's ICAO name.
|
49
|
+
For example if you want to pull all the stations active in Austria (ICAO code
|
50
|
+
for the FIR is LOVV), you can use `"LO"` as your ICAO search string: all Austrian
|
51
|
+
airports and ATC station callsigns start with `"LO"`:
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
# Attaching the method directly to a string:
|
55
|
+
"LO".vatsim_online
|
56
|
+
|
57
|
+
# Attaching the method to a variable containing a string:
|
58
|
+
icao = "LO"
|
59
|
+
icao.vatsim_online
|
60
|
+
```
|
61
|
+
When parsing the pilot stations for particular airport or area, the library will
|
62
|
+
return the pilots that are flying **to or from** the given area or airport,
|
63
|
+
not the current enroute stations. The discovery algorithm is based on **origin
|
64
|
+
and destination**.
|
65
|
+
|
66
|
+
|
67
|
+
### Anatomy of method returns
|
68
|
+
|
69
|
+
The `vatsim_online` method returns a **hash** of 2 elements: the matching atc
|
70
|
+
stations and pilots stations. Each of those is an **array**, cosnsisting of
|
71
|
+
station **objects**. Each of these objects includes a number of **attributes**:
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
icao_vatsim_online # => {:atc => [a1, a2, a3 ...], :pilots => [p1, p2, p3 ...]}
|
75
|
+
|
76
|
+
icao_vatsim_online[:atc] #=> [a1, a2, a3 ...]
|
77
|
+
icao_vatsim_online[:pilots] #=> [p1, p2, p3 ...]
|
78
|
+
|
79
|
+
icao_vatsim_online[:atc].first #=> a1
|
80
|
+
icao_vatsim_online[:pilots].first #=> p1
|
81
|
+
|
82
|
+
a1.callsign #=> "LQSA_TWR"
|
83
|
+
a1.frequency #=> "118.25"
|
84
|
+
a1.name #=> "Svilen Vassilev"
|
85
|
+
...
|
86
|
+
|
87
|
+
p1.callsign #=> "ACH217S"
|
88
|
+
p1.departure #=> "LQSA"
|
89
|
+
p1.destination #=> "LDSP"
|
90
|
+
p1.remarks #=> "/V/ RMK/CHARTS"
|
91
|
+
...
|
92
|
+
```
|
93
|
+
|
94
|
+
### Station attributes
|
95
|
+
|
96
|
+
Here's a complete list of the station object attributes that can be accessed:
|
97
|
+
|
98
|
+
* callsign
|
99
|
+
* name
|
100
|
+
* role
|
101
|
+
* frequency
|
102
|
+
* altitude
|
103
|
+
* groundspeed
|
104
|
+
* aircraft
|
105
|
+
* departure
|
106
|
+
* destination
|
107
|
+
* rating
|
108
|
+
* facility
|
109
|
+
* remarks
|
110
|
+
* route
|
111
|
+
* atis
|
112
|
+
* logon
|
113
|
+
|
114
|
+
### Customizing the request
|
115
|
+
|
116
|
+
The `vatsim online` method can be customized by passing in a hash-style collection
|
117
|
+
of arguments. The currently supported arguments and their defaults are:
|
118
|
+
|
119
|
+
* :atc => true (Possible values: true, false. Default value: true)
|
120
|
+
* :pilots => true (Possible values: true, false. Default value: true)
|
121
|
+
|
122
|
+
Both options can be used to exclude all ATC or pilots stations respectively from
|
123
|
+
the request, in order to speed it up and avoid processing useless data.
|
124
|
+
|
125
|
+
**Examples:**
|
126
|
+
|
127
|
+
```ruby
|
128
|
+
# Lets exclude all ATC from our request and get the pilots only
|
129
|
+
"LO".vatsim_online(:atc => false)[:pilots] #=> [p1, p2, p3...]
|
130
|
+
|
131
|
+
# Lets exclude all pilots from our request and get the ATC only
|
132
|
+
"LO".vatsim_online(:pilots => false)[:atc] #=> [a1, a2, a3...]
|
133
|
+
|
134
|
+
"LO".vatsim_online(:atc => false)[:pilots].first.callsign #=> "ACH0838"
|
135
|
+
"LO".vatsim_online(:pilots => false)[:atc].first.callsign #=> "LOVV_CTR"
|
136
|
+
|
137
|
+
```
|
138
|
+
|
139
|
+
### Example of Ruby on Rails implementation
|
140
|
+
|
141
|
+
Here's a possible scenario of using this gem in a Ruby on Rails application.
|
142
|
+
Verbosity is kept on purpose for clarity.
|
143
|
+
|
144
|
+
**In your controller:**
|
145
|
+
```ruby
|
146
|
+
def index
|
147
|
+
# We want to retrieve all Austrian online stations (ATC and pilots)
|
148
|
+
icao = "LO"
|
149
|
+
stations = icao.vatsim_online
|
150
|
+
|
151
|
+
# Now we will assign the ATCs and the pilots to separate instance variables,
|
152
|
+
# to be able to loop through them separately in the view
|
153
|
+
@atc = stations[:atc]
|
154
|
+
@pilots = stations[:pilots]
|
155
|
+
end
|
156
|
+
```
|
157
|
+
|
158
|
+
**In your view (HAML is used for clarity):**
|
159
|
+
|
160
|
+
```haml
|
161
|
+
- for atc in @atc
|
162
|
+
%li
|
163
|
+
= atc.callsign
|
164
|
+
= atc.frequency
|
165
|
+
= atc.rating
|
166
|
+
= atc.name
|
167
|
+
= atc.atis
|
168
|
+
|
169
|
+
- for pilot in @pilots
|
170
|
+
%li
|
171
|
+
= pilot.callsign
|
172
|
+
= atc.name
|
173
|
+
= atc.origin
|
174
|
+
= atc.destination
|
175
|
+
= atc.route
|
176
|
+
= atc.altitude
|
177
|
+
= atc.groundspeed
|
178
|
+
= atc.remarks
|
179
|
+
```
|
180
|
+
|
181
|
+
### Notes
|
182
|
+
|
183
|
+
* Vatsim status and data files are cached locally to reduce the load on vatsim
|
184
|
+
servers. Random server is chosen to retrieve the data each time. By default the
|
185
|
+
status file is updated once every 4 hours and the data file once every 3 minutes
|
186
|
+
regardless of the number of incoming requests.
|
187
|
+
* The data is cached in your default TEMP directory (OS specific)
|
188
|
+
* All the data retrieval and caching logic is encapsulated in a separate class
|
189
|
+
`VatsimTools::DataDownloader` which can be mixed in other applications and
|
190
|
+
libraries too.
|
191
|
+
* The ICAO string used as a search criteria **is not** case sensitive
|
192
|
+
* Pilot stations returned are based on origin and destination airports, the
|
193
|
+
current algorithm does not evaluate enroute flights.
|
194
|
+
|
195
|
+
## Contributing
|
196
|
+
|
197
|
+
1. Fork it
|
198
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
199
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
200
|
+
4. Make sure all tests are passing!
|
201
|
+
5. Push to the branch (`git push origin my-new-feature`)
|
202
|
+
6. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
%w{vatsim_online/version vatsim_online/station vatsim_online/data_downloader
|
2
|
+
vatsim_online/station_parser}.each { |lib| require lib }
|
3
|
+
|
4
|
+
class String
|
5
|
+
def vatsim_online(args={})
|
6
|
+
VatsimOnline.vatsim_online(self, args)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
module VatsimOnline
|
11
|
+
|
12
|
+
def self.vatsim_online(icao, args)
|
13
|
+
VatsimTools::StationParser.new(icao,args).sorted_station_objects
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module VatsimTools
|
2
|
+
|
3
|
+
class DataDownloader
|
4
|
+
|
5
|
+
%w{curb tempfile time_diff tmpdir csv}.each { |lib| require lib }
|
6
|
+
|
7
|
+
STATUS_URL = "http://status.vatsim.net/status.txt"
|
8
|
+
LOCAL_STATUS = "#{Dir.tmpdir}/vatsim_status.txt"
|
9
|
+
LOCAL_DATA = "#{Dir.tmpdir}/vatsim_data.txt"
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
data_file
|
13
|
+
end
|
14
|
+
|
15
|
+
def create_status_tempfile
|
16
|
+
status = Tempfile.new('vatsim_status')
|
17
|
+
File.rename status.path, LOCAL_STATUS
|
18
|
+
File.open(LOCAL_STATUS, "w+") {|f| f.write(Curl::Easy.perform(STATUS_URL).body_str) }
|
19
|
+
end
|
20
|
+
|
21
|
+
def read_status_tempfile
|
22
|
+
status = File.open(LOCAL_STATUS)
|
23
|
+
difference = Time.diff(status.ctime, Time.now)[:hour]
|
24
|
+
difference > 3 ? create_status_tempfile : status.read
|
25
|
+
end
|
26
|
+
|
27
|
+
def status_file
|
28
|
+
File.exists?(LOCAL_STATUS) ? read_status_tempfile : create_status_tempfile
|
29
|
+
LOCAL_STATUS
|
30
|
+
end
|
31
|
+
|
32
|
+
def servers
|
33
|
+
urls = []
|
34
|
+
CSV.foreach(status_file, :col_sep =>'=') {|row| urls << row[1] if row[0] == "url0"}
|
35
|
+
urls
|
36
|
+
end
|
37
|
+
|
38
|
+
def create_local_data_file
|
39
|
+
data = Tempfile.new('vatsim_data', :encoding => 'iso-8859-15')
|
40
|
+
File.rename data.path, LOCAL_DATA
|
41
|
+
data = Curl::Easy.perform(servers.sample).body_str.gsub(/["]/, '\s').force_encoding('iso-8859-15')
|
42
|
+
File.open(LOCAL_DATA, "w+") {|f| f.write(data)}
|
43
|
+
end
|
44
|
+
|
45
|
+
def read_local_datafile
|
46
|
+
data = File.open(LOCAL_DATA)
|
47
|
+
difference = Time.diff(data.ctime, Time.now)[:minute]
|
48
|
+
difference > 2 ? create_local_data_file : data.read
|
49
|
+
end
|
50
|
+
|
51
|
+
def data_file
|
52
|
+
File.exists?(LOCAL_DATA) ? read_local_datafile : create_local_data_file
|
53
|
+
LOCAL_DATA
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module VatsimTools
|
2
|
+
class Station
|
3
|
+
attributes = %w{callsign name role frequency altitude groundspeed aircraft
|
4
|
+
departure destination rating facility remarks route atis logon}
|
5
|
+
attributes.each {|attribute| attr_accessor attribute.to_sym }
|
6
|
+
|
7
|
+
|
8
|
+
def initialize(station)
|
9
|
+
@callsign = station[0]
|
10
|
+
@name = station[2]
|
11
|
+
@role = station[3]
|
12
|
+
@frequency = station[4]
|
13
|
+
@altitude = station[7]
|
14
|
+
@groundspeed = station[8]
|
15
|
+
@aircraft = station[9]
|
16
|
+
@departure = station[11]
|
17
|
+
@destination = station[13]
|
18
|
+
@rating = station[16]
|
19
|
+
@facility = station[18]
|
20
|
+
@remarks = station[29]
|
21
|
+
@route = station[30]
|
22
|
+
@atis = station[35]
|
23
|
+
@logon = station[37]
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module VatsimTools
|
2
|
+
|
3
|
+
class StationParser
|
4
|
+
|
5
|
+
%w{tmpdir csv}.each { |lib| require lib }
|
6
|
+
require_relative "data_downloader"
|
7
|
+
require_relative "station"
|
8
|
+
|
9
|
+
attr_accessor :role
|
10
|
+
attr_accessor :icao
|
11
|
+
|
12
|
+
LOCAL_DATA = "#{Dir.tmpdir}/vatsim_data.txt"
|
13
|
+
|
14
|
+
def initialize(icao, args = nil)
|
15
|
+
VatsimTools::DataDownloader.new
|
16
|
+
args.class == Hash ? @role = determine_role(args) : @role = "all"
|
17
|
+
@icao = icao.upcase
|
18
|
+
end
|
19
|
+
|
20
|
+
def determine_role(args)
|
21
|
+
args[:atc] == false ? role = "pilot" : role = "all"
|
22
|
+
args[:pilots] == false ? role = "atc" : role = role
|
23
|
+
role = "all" if args[:pilots] == false && args[:atc] == false
|
24
|
+
role
|
25
|
+
end
|
26
|
+
|
27
|
+
def stations
|
28
|
+
stations = []
|
29
|
+
CSV.foreach(LOCAL_DATA, :col_sep =>':', encoding: "iso-8859-15") do |row|
|
30
|
+
callsign, origin, destination, client = row[0].to_s, row[11].to_s, row[13].to_s, row[3].to_s
|
31
|
+
stations << row if (callsign[0...@icao.length] == @icao && client == "ATC") unless @role == "pilot"
|
32
|
+
stations << row if (origin[0...@icao.length] == @icao || destination[0...@icao.length] == @icao) unless @role == "atc"
|
33
|
+
end
|
34
|
+
stations
|
35
|
+
end
|
36
|
+
|
37
|
+
def station_objects
|
38
|
+
station_objects= []
|
39
|
+
stations.each {|station| station_objects << VatsimTools::Station.new(station) }
|
40
|
+
station_objects
|
41
|
+
end
|
42
|
+
|
43
|
+
def sorted_station_objects
|
44
|
+
atc = []; pilots = []
|
45
|
+
station_objects.each {|sobj| sobj.role == "ATC" ? atc << sobj : pilots << sobj}
|
46
|
+
{:atc => atc, :pilots => pilots}
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'spec_helper.rb'
|
2
|
+
require 'data_downloader_spec_helper.rb'
|
3
|
+
|
4
|
+
describe VatsimTools::DataDownloader do
|
5
|
+
|
6
|
+
target = VatsimTools::DataDownloader
|
7
|
+
LOCAL_STATUS = "#{Dir.tmpdir}/vatsim_status.txt"
|
8
|
+
LOCAL_DATA = "#{Dir.tmpdir}/vatsim_data.txt"
|
9
|
+
|
10
|
+
describe "create_status_tempfile" do
|
11
|
+
it "should create a file" do
|
12
|
+
delete_local_files
|
13
|
+
File.exists?(LOCAL_STATUS).should be false
|
14
|
+
target.new.create_status_tempfile
|
15
|
+
File.exists?(LOCAL_STATUS).should be true
|
16
|
+
File.open(LOCAL_STATUS).path.should eq("#{Dir.tmpdir}/vatsim_status.txt")
|
17
|
+
File.open(LOCAL_STATUS).size.should be > 100
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe "read_status_tempfile" do
|
22
|
+
it "should confirm a file exists" do
|
23
|
+
target.new.read_status_tempfile
|
24
|
+
File.exists?(LOCAL_STATUS).should be true
|
25
|
+
File.open(LOCAL_STATUS).size.should be > 100
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "status_file" do
|
30
|
+
it "should return status.txt path" do
|
31
|
+
delete_local_files
|
32
|
+
File.exists?(LOCAL_STATUS).should be false
|
33
|
+
target.new.status_file.class.should eq(String)
|
34
|
+
target.new.status_file.should include("vatsim_status.txt")
|
35
|
+
target.new.status_file.should eq(LOCAL_STATUS)
|
36
|
+
target.new.status_file.should eq("#{Dir.tmpdir}/vatsim_status.txt")
|
37
|
+
File.exists?(LOCAL_STATUS).should be true
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe "servers" do
|
42
|
+
it "should contain an array of server URLs" do
|
43
|
+
File.exists?(LOCAL_STATUS).should be true
|
44
|
+
target.new.servers.class.should eq(Array)
|
45
|
+
target.new.servers.size.should eq(5)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe "create_local_data_file" do
|
50
|
+
it "should confirm a file exists" do
|
51
|
+
target.new.create_local_data_file
|
52
|
+
File.exists?(LOCAL_DATA).should be true
|
53
|
+
File.open(LOCAL_DATA).path.should eq("#{Dir.tmpdir}/vatsim_data.txt")
|
54
|
+
File.open(LOCAL_DATA).size.should be > 100
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "read_local_datafile" do
|
59
|
+
it "should confirm a file exists" do
|
60
|
+
target.new.read_local_datafile
|
61
|
+
File.exists?(LOCAL_DATA).should be true
|
62
|
+
File.open(LOCAL_DATA).size.should be > 100
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe "data_file" do
|
67
|
+
it "should contain file path" do
|
68
|
+
delete_local_files
|
69
|
+
File.exists?(LOCAL_DATA).should be false
|
70
|
+
target.new.data_file.class.should eq(String)
|
71
|
+
target.new.data_file.should include("vatsim_data.txt")
|
72
|
+
target.new.data_file.should eq(LOCAL_DATA)
|
73
|
+
target.new.data_file.should eq("#{Dir.tmpdir}/vatsim_data.txt")
|
74
|
+
File.exists?(LOCAL_DATA).should be true
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe "new" do
|
79
|
+
it "should return" do
|
80
|
+
delete_local_files
|
81
|
+
File.exists?(LOCAL_DATA).should be false
|
82
|
+
File.exists?(LOCAL_STATUS).should be false
|
83
|
+
target.new
|
84
|
+
File.exists?(LOCAL_DATA).should be true
|
85
|
+
File.exists?(LOCAL_STATUS).should be true
|
86
|
+
File.open(LOCAL_DATA).size.should be > 100
|
87
|
+
File.open(LOCAL_STATUS).size.should be > 100
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|