metar-parser 0.1.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.
- data/COPYING +20 -0
- data/README.rdoc +40 -0
- data/Rakefile +53 -0
- data/bin/download_raw.rb +57 -0
- data/bin/parse_raw.rb +28 -0
- data/lib/metar/data.rb +374 -0
- data/lib/metar/parser.rb +352 -0
- data/lib/metar/raw.rb +49 -0
- data/lib/metar/report.rb +95 -0
- data/lib/metar/station.rb +176 -0
- data/lib/metar.rb +16 -0
- data/locales/en.yml +111 -0
- data/locales/it.yml +111 -0
- data/test/all_tests.rb +6 -0
- data/test/metar_test_helper.rb +14 -0
- data/test/unit/data_test.rb +183 -0
- data/test/unit/parser_test.rb +101 -0
- data/test/unit/raw_test.rb +28 -0
- data/test/unit/report_test.rb +91 -0
- data/test/unit/station_test.rb +68 -0
- metadata +128 -0
data/COPYING
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Joe Yates
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
= metar - Downloads and parses weather status
|
2
|
+
|
3
|
+
The information comes from the National Oceanic and Atmospheric Association's raw data source.
|
4
|
+
|
5
|
+
= Implementation
|
6
|
+
|
7
|
+
* Parses METAR strings using a state machine.
|
8
|
+
|
9
|
+
= Data format descrition
|
10
|
+
|
11
|
+
* WMO
|
12
|
+
* http://www.wmo.int/pages/prog/www/WMOCodes/Manual/Volume-I-selection/Sel2.pdf (pages 27-38)
|
13
|
+
* http://dcaa.slv.dk:8000/icaodocs/Annex%203%20-%20Meteorological%20Service%20for%20International%20Air%20Navigation/Cover%20sheet%20to%20AMDT%2074.pdf (Table A3-2. Template for METAR and SPECI)
|
14
|
+
* http://booty.org.uk/booty.weather/metinfo/codes/METAR_decode.htm
|
15
|
+
|
16
|
+
* United states:
|
17
|
+
* http://www.nws.noaa.gov/oso/oso1/oso12/fmh1/fmh1ch12.htm
|
18
|
+
* http://www.ofcm.gov/fmh-1/pdf/FMH1.pdf
|
19
|
+
* http://weather.cod.edu/notes/metar.html
|
20
|
+
* http://www.met.tamu.edu/class/METAR/metar-pg3.html - incomplete
|
21
|
+
|
22
|
+
= Other software
|
23
|
+
|
24
|
+
Other Ruby libraries offering METAR parsing:
|
25
|
+
* ruby-metar - http://github.com/brandonh/ruby-metar
|
26
|
+
* ruby-wx - http://hans.fugal.net/src/ruby-wx/doc/
|
27
|
+
There are many reports (WMO) that these libraries do not parse.
|
28
|
+
|
29
|
+
There are two gems which read the National Oceanic and Atmospheric Association's XML weather data feeds:
|
30
|
+
* noaa-weather - Ruby interface to NOAA SOAP interface
|
31
|
+
* noaa - http://github.com/outoftime/noaa
|
32
|
+
|
33
|
+
Interactive map:
|
34
|
+
* http://www.spatiality.at/metarr/frontend/
|
35
|
+
|
36
|
+
= Testing
|
37
|
+
|
38
|
+
The tests use a local copy of the weather stations list: data/nsd_cccc.txt
|
39
|
+
|
40
|
+
If missing, the file gets downloaded before running tests.
|
data/Rakefile
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake/gempackagetask'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
require 'rake/testtask'
|
5
|
+
require 'rake/clean'
|
6
|
+
|
7
|
+
$:.unshift(File.dirname(__FILE__) + '/lib')
|
8
|
+
require 'metar'
|
9
|
+
|
10
|
+
RDOC_OPTS = ['--quiet', '--title', 'METAR Weather Report Parser', '--main', 'README.rdoc', '--inline-source']
|
11
|
+
RDOC_PATH = 'doc/rdoc'
|
12
|
+
CLEAN.include RDOC_PATH
|
13
|
+
|
14
|
+
task :default => :test
|
15
|
+
|
16
|
+
spec = Gem::Specification.new do |s|
|
17
|
+
s.name = 'metar-parser'
|
18
|
+
s.summary = 'Downloads and parses weather reports'
|
19
|
+
s.description = 'Downloads, parses and presents METAR weather reports'
|
20
|
+
s.version = Metar::VERSION::STRING
|
21
|
+
|
22
|
+
s.homepage = 'http://github.com/joeyates/metar-parser'
|
23
|
+
s.author = 'Joe Yates'
|
24
|
+
s.email = 'joe.g.yates@gmail.com'
|
25
|
+
|
26
|
+
s.files = ['README.rdoc', 'COPYING', 'Rakefile'] + FileList['{bin,lib,test}/**/*.rb'] + FileList['locales/**/*.{rb,yml}']
|
27
|
+
s.require_paths = ['lib']
|
28
|
+
s.add_dependency('aasm', '>= 2.1.5')
|
29
|
+
s.add_dependency('i18n', '>= 0.3.5')
|
30
|
+
s.add_dependency('m9t', '>= 0.1.11')
|
31
|
+
|
32
|
+
s.has_rdoc = true
|
33
|
+
s.rdoc_options += RDOC_OPTS
|
34
|
+
s.extra_rdoc_files = ['README.rdoc', 'COPYING']
|
35
|
+
|
36
|
+
s.test_file = 'test/all_tests.rb'
|
37
|
+
end
|
38
|
+
|
39
|
+
Rake::TestTask.new do |t|
|
40
|
+
t.libs << 'test'
|
41
|
+
t.test_files = FileList['test/unit/*_test.rb']
|
42
|
+
t.verbose = true
|
43
|
+
end
|
44
|
+
|
45
|
+
Rake::RDocTask.new do |rdoc|
|
46
|
+
rdoc.rdoc_dir = RDOC_PATH
|
47
|
+
rdoc.options += RDOC_OPTS
|
48
|
+
rdoc.main = 'README.rdoc'
|
49
|
+
rdoc.rdoc_files.add ['README.rdoc', 'COPYING', 'lib/**/*.rb']
|
50
|
+
end
|
51
|
+
|
52
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
53
|
+
end
|
data/bin/download_raw.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
=begin
|
4
|
+
|
5
|
+
This script downloads the current weather report for each station.
|
6
|
+
|
7
|
+
=end
|
8
|
+
|
9
|
+
require 'rubygems' if RUBY_VERSION < '1.9'
|
10
|
+
require 'yaml'
|
11
|
+
require File.join(File.expand_path(File.dirname(__FILE__) + '/../lib'), 'metar')
|
12
|
+
|
13
|
+
Metar::Station.load_local
|
14
|
+
|
15
|
+
('A'..'Z').each do |initial|
|
16
|
+
|
17
|
+
stations = {}
|
18
|
+
|
19
|
+
Metar::Raw.cache_connection
|
20
|
+
|
21
|
+
Metar::Station.all.each do |station|
|
22
|
+
|
23
|
+
next if station.cccc[0, 1] < initial
|
24
|
+
break if station.cccc[0, 1] > initial
|
25
|
+
|
26
|
+
print station.cccc
|
27
|
+
raw = nil
|
28
|
+
begin
|
29
|
+
raw = Metar::Raw.new(station.cccc)
|
30
|
+
rescue Net::FTPPermError => e
|
31
|
+
puts ": Not available - #{ e }"
|
32
|
+
next
|
33
|
+
rescue
|
34
|
+
puts ": Other error - #{ e }"
|
35
|
+
next
|
36
|
+
end
|
37
|
+
|
38
|
+
stations[station.cccc] = raw.raw.clone
|
39
|
+
puts ': OK'
|
40
|
+
end
|
41
|
+
|
42
|
+
filename = File.join(File.expand_path(File.dirname(__FILE__) + '/../data'), "stations.#{ initial }.yml")
|
43
|
+
File.open(filename, 'w') { |fil| fil.write stations.to_yaml }
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
# Merge into one file
|
48
|
+
stations = {}
|
49
|
+
('A'..'Z').each do |initial|
|
50
|
+
filename = File.join(File.expand_path(File.dirname(__FILE__) + '/../data'), "stations.#{ initial }.yml")
|
51
|
+
next if not File.exist?(filename)
|
52
|
+
h = YAML.load_file(filename)
|
53
|
+
stations.merge!(h)
|
54
|
+
end
|
55
|
+
|
56
|
+
filename = File.join(File.expand_path(File.dirname(__FILE__) + '/../data'), "stations.yml")
|
57
|
+
File.open(filename, 'w') { |fil| fil.write stations.to_yaml }
|
data/bin/parse_raw.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
=begin
|
4
|
+
|
5
|
+
Use the data downloaded by 'download_raw.rb' to bulk test the Report
|
6
|
+
|
7
|
+
=end
|
8
|
+
|
9
|
+
require 'rubygems' if RUBY_VERSION < '1.9'
|
10
|
+
require 'yaml'
|
11
|
+
require File.join(File.expand_path(File.dirname(__FILE__) + '/../lib'), 'metar')
|
12
|
+
|
13
|
+
filename = File.join(File.expand_path(File.dirname(__FILE__) + '/../data'), "stations.yml")
|
14
|
+
stations = YAML.load_file(filename)
|
15
|
+
|
16
|
+
stations.each_pair do |cccc, raw_text|
|
17
|
+
raw = Metar::Raw.new(cccc, raw_text)
|
18
|
+
report = nil
|
19
|
+
begin
|
20
|
+
report = Metar::Report.new(raw)
|
21
|
+
$stdout.print '.'
|
22
|
+
rescue => e
|
23
|
+
$stderr.puts "#{ raw.metar }"
|
24
|
+
$stderr.puts " Error: #{ e }"
|
25
|
+
$stdout.print 'E'
|
26
|
+
end
|
27
|
+
$stdout.flush
|
28
|
+
end
|
data/lib/metar/data.rb
ADDED
@@ -0,0 +1,374 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'rubygems' if RUBY_VERSION < '1.9'
|
3
|
+
require 'i18n'
|
4
|
+
require 'm9t'
|
5
|
+
|
6
|
+
module Metar
|
7
|
+
locales_path = File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'locales'))
|
8
|
+
I18n.load_path += Dir.glob("#{ locales_path }/*.yml")
|
9
|
+
|
10
|
+
# Adds a parse method to the M9t base class
|
11
|
+
class Speed < M9t::Speed
|
12
|
+
|
13
|
+
METAR_UNITS = {
|
14
|
+
'KMH' => :kilometers_per_hour,
|
15
|
+
'MPS' => :meters_per_second,
|
16
|
+
'KT' => :knots,
|
17
|
+
}
|
18
|
+
|
19
|
+
def Speed.parse(s)
|
20
|
+
case
|
21
|
+
when s =~ /^(\d+)(KT|MPS|KMH)$/
|
22
|
+
# Call the appropriate factory method for the supplied units
|
23
|
+
send(METAR_UNITS[$2], $1.to_i, { :units => METAR_UNITS[$2], :precision => 0 })
|
24
|
+
when s =~ /^(\d+)$/
|
25
|
+
kilometers_per_hour($1.to_i, { :units => :kilometers_per_hour, :precision => 0 })
|
26
|
+
else
|
27
|
+
nil
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
# Adds a parse method to the M9t base class
|
34
|
+
class Temperature < M9t::Temperature
|
35
|
+
|
36
|
+
def Temperature.parse(s)
|
37
|
+
if s =~ /^(M?)(\d+)$/
|
38
|
+
sign = $1
|
39
|
+
value = $2.to_i
|
40
|
+
value *= -1 if sign == 'M'
|
41
|
+
new(value, { :units => :degrees, :precision => 0, :abbreviated => true })
|
42
|
+
else
|
43
|
+
nil
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
class Distance < M9t::Distance
|
50
|
+
|
51
|
+
# Subclasses M9t::Distance
|
52
|
+
# Uses kilometers as desired default output unit
|
53
|
+
# nil is taken to mean 'data unavailable'
|
54
|
+
def initialize(meters = nil, options = {})
|
55
|
+
if meters
|
56
|
+
super(meters, { :units => :kilometers, :precision => 0, :abbreviated => true }.merge(options))
|
57
|
+
else
|
58
|
+
@value = nil
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Handles nil case differently to M9t::Distance
|
63
|
+
def to_s
|
64
|
+
return I18n.t('metar.distance.unknown') if @value.nil?
|
65
|
+
super
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
# Adds a parse method to the M9t base class
|
71
|
+
class Pressure < M9t::Pressure
|
72
|
+
|
73
|
+
def Pressure.parse(pressure)
|
74
|
+
case
|
75
|
+
when pressure =~ /^Q(\d{4})$/
|
76
|
+
hectopascals($1.to_f)
|
77
|
+
when pressure =~ /^A(\d{4})$/
|
78
|
+
inches_of_mercury($1.to_f / 100.0)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
class Visibility
|
85
|
+
|
86
|
+
def Visibility.parse(s)
|
87
|
+
case
|
88
|
+
when s == '9999'
|
89
|
+
new(Distance.new(10000), nil, :more_than)
|
90
|
+
when s =~ /(\d{4})NDV/ # WMO
|
91
|
+
new(Distance.new($1.to_f)) # Assuming meters
|
92
|
+
when (s =~ /^((1|2)\s|)([13])\/([248])SM$/) # US
|
93
|
+
miles = $1.to_f + $3.to_f / $4.to_f
|
94
|
+
new(Distance.miles(miles, {:units => :miles}))
|
95
|
+
when s =~ /^(\d+)SM$/ # US
|
96
|
+
new(Distance.miles($1.to_f, {:units => :miles}))
|
97
|
+
when s == 'M1/4SM' # US
|
98
|
+
new(Distance.miles(0.25, {:units => :miles}), nil, :less_than)
|
99
|
+
when s =~ /^(\d+)KM$/
|
100
|
+
new(Distance.kilometers($1))
|
101
|
+
when s =~ /^(\d+)$/ # Units?
|
102
|
+
new(Distance.kilometers($1))
|
103
|
+
when s =~ /^(\d+)(N|NE|E|SE|S|SW|W|NW)$/
|
104
|
+
new(Distance.kilometers($1), M9t::Direction.compass($2))
|
105
|
+
else
|
106
|
+
nil
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
attr_reader :distance, :direction, :comparator
|
111
|
+
|
112
|
+
def initialize(distance, direction = nil, comparator = nil)
|
113
|
+
@distance, @direction, @comparator = distance, direction, comparator
|
114
|
+
end
|
115
|
+
|
116
|
+
def to_s
|
117
|
+
case
|
118
|
+
when (@direction.nil? and @comparator.nil?)
|
119
|
+
@distance.to_s
|
120
|
+
when @comparator.nil?
|
121
|
+
"%s %s" % [@distance.to_s, @direction.to_s]
|
122
|
+
when @direction.nil?
|
123
|
+
"%s %s" % [I18n.t('comparison.' + @comparator.to_s), @distance.to_s]
|
124
|
+
else
|
125
|
+
"%s %s %s" % [I18n.t('comparison.' + @comparator.to_s), @distance.to_s, direction]
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
class Wind
|
131
|
+
|
132
|
+
def Wind.parse(s)
|
133
|
+
case
|
134
|
+
when s =~ /^(\d{3})(\d{2}(KT|MPS|KMH|))$/
|
135
|
+
new(M9t::Direction.new($1, { :abbreviated => true }), Speed.parse($2))
|
136
|
+
when s =~ /^(\d{3})(\d{2})G(\d{2,3}(KT|MPS|KMH|))$/
|
137
|
+
new(M9t::Direction.new($1, { :abbreviated => true }), Speed.parse($2))
|
138
|
+
when s =~ /^VRB(\d{2}(KT|MPS|KMH|))$/
|
139
|
+
new('variable direction', Speed.parse($1))
|
140
|
+
when s =~ /^\/{3}(\d{2}(KT|MPS|KMH|))$/
|
141
|
+
new('unknown direction', Speed.parse($1))
|
142
|
+
when s =~ /^\/{3}(\/{2}(KT|MPS|KMH|))$/
|
143
|
+
new('unknown direction', 'unknown')
|
144
|
+
else
|
145
|
+
nil
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
attr_reader :direction, :speed, :units
|
150
|
+
|
151
|
+
def initialize(direction, speed, units = :kilometers_per_hour)
|
152
|
+
@direction, @speed = direction, speed
|
153
|
+
end
|
154
|
+
|
155
|
+
def to_s
|
156
|
+
"#{ @direction } #{ @speed }"
|
157
|
+
end
|
158
|
+
|
159
|
+
end
|
160
|
+
|
161
|
+
class VariableWind
|
162
|
+
|
163
|
+
def VariableWind.parse(variable_wind)
|
164
|
+
if variable_wind =~ /^(\d+)V(\d+)$/
|
165
|
+
new(M9t::Direction.new($1), M9t::Direction.new($2))
|
166
|
+
else
|
167
|
+
nil
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
attr_reader :direction1, :direction2
|
172
|
+
|
173
|
+
def initialize(direction1, direction2)
|
174
|
+
@direction1, @direction2 = direction1, direction2
|
175
|
+
end
|
176
|
+
|
177
|
+
def to_s
|
178
|
+
"#{ @direction1 } - #{ @direction2 }"
|
179
|
+
end
|
180
|
+
|
181
|
+
end
|
182
|
+
|
183
|
+
class RunwayVisibleRange
|
184
|
+
|
185
|
+
TENDENCY = { '' => nil, 'N' => :no_change, 'U' => :improving, 'D' => :worsening }
|
186
|
+
COMPARATOR = { '' => nil, 'P' => :more_than, 'M' => :less_than }
|
187
|
+
UNITS = { '' => :meters, 'FT' => :feet }
|
188
|
+
|
189
|
+
def RunwayVisibleRange.parse(runway_visible_range)
|
190
|
+
case
|
191
|
+
when runway_visible_range =~ /^R(\d+[RLC]?)\/(P|M|)(\d{4})(N|U|D|)(FT|)$/
|
192
|
+
designator = $1
|
193
|
+
comparator = COMPARATOR[$2]
|
194
|
+
count = $3.to_f
|
195
|
+
tendency = TENDENCY[$4]
|
196
|
+
units = UNITS[$5]
|
197
|
+
distance = Distance.send(units, count, { :units => units })
|
198
|
+
visibility = Visibility.new(distance, nil, comparator)
|
199
|
+
new(designator, visibility, nil, tendency)
|
200
|
+
when runway_visible_range =~ /^R(\d+[RLC]?)\/(P|M|)(\d{4})V(P|M|)(\d{4})(N|U|D)?(FT)?$/
|
201
|
+
designator = $1
|
202
|
+
comparator1 = COMPARATOR[$2]
|
203
|
+
count1 = $3.to_f
|
204
|
+
comparator2 = COMPARATOR[$4]
|
205
|
+
count2 = $5.to_f
|
206
|
+
tendency = TENDENCY[$6]
|
207
|
+
units = UNITS[$7]
|
208
|
+
distance1 = Distance.send(units, count1, { :units => units })
|
209
|
+
distance2 = Distance.send(units, count2, { :units => units })
|
210
|
+
visibility1 = Visibility.new(distance1, nil, comparator1)
|
211
|
+
visibility2 = Visibility.new(distance2, nil, comparator2)
|
212
|
+
new(designator, visibility1, visibility2, tendency)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
attr_reader :designator, :visibility1, :visibility2, :tendency
|
217
|
+
def initialize(designator, visibility1, visibility2 = nil, tendency = nil)
|
218
|
+
@designator, @visibility1, @visibility2, @tendency = designator, visibility1, visibility2, tendency
|
219
|
+
end
|
220
|
+
|
221
|
+
def to_s
|
222
|
+
if @visibility2.nil?
|
223
|
+
I18n.t('metar.runway_visible_range.runway') + ' ' + @designator + ': ' + @visibility1.to_s
|
224
|
+
else
|
225
|
+
I18n.t('metar.runway_visible_range.runway') + ' ' + @designator + ': ' + I18n.t('metar.runway_visible_range.from') + ' ' + @visibility1.to_s + ' ' + I18n.t('metar.runway_visible_range.to') + ' ' + @visibility2.to_s
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
private
|
230
|
+
|
231
|
+
def RunwayVisibleRange.parse_visibility(distance)
|
232
|
+
end
|
233
|
+
|
234
|
+
end
|
235
|
+
|
236
|
+
class WeatherPhenomenon
|
237
|
+
|
238
|
+
Modifiers = {
|
239
|
+
'\+' => 'heavy',
|
240
|
+
'-' => 'light',
|
241
|
+
'VC' => 'nearby'
|
242
|
+
}
|
243
|
+
|
244
|
+
Descriptors = {
|
245
|
+
'BC' => 'patches of',
|
246
|
+
'BL' => 'blowing',
|
247
|
+
'DR' => 'low drifting',
|
248
|
+
'FZ' => 'freezing',
|
249
|
+
'MI' => 'shallow',
|
250
|
+
'PR' => 'partial',
|
251
|
+
'SH' => 'shower of',
|
252
|
+
'TS' => 'thunderstorm and',
|
253
|
+
}
|
254
|
+
|
255
|
+
Phenomena = {
|
256
|
+
'BR' => 'mist',
|
257
|
+
'DU' => 'dust',
|
258
|
+
'DZ' => 'drizzle',
|
259
|
+
'FG' => 'fog',
|
260
|
+
'FU' => 'smoke',
|
261
|
+
'GR' => 'hail',
|
262
|
+
'GS' => 'small hail',
|
263
|
+
'HZ' => 'haze',
|
264
|
+
'IC' => 'ice crystals',
|
265
|
+
'PL' => 'ice pellets',
|
266
|
+
'PO' => 'dust whirls',
|
267
|
+
'PY' => 'spray', # US only
|
268
|
+
'RA' => 'rain',
|
269
|
+
'SA' => 'sand',
|
270
|
+
'SH' => 'shower',
|
271
|
+
'SN' => 'snow',
|
272
|
+
'SG' => 'snow grains',
|
273
|
+
'SNRA' => 'snow and rain',
|
274
|
+
'SQ' => 'squall',
|
275
|
+
'UP' => 'unknown phenomenon', # => AUTO
|
276
|
+
'VA' => 'volcanic ash',
|
277
|
+
'FC' => 'funnel cloud',
|
278
|
+
'SS' => 'sand storm',
|
279
|
+
'DS' => 'dust storm',
|
280
|
+
'TS' => 'thunderstorm',
|
281
|
+
'TSGR' => 'thunderstorm and hail',
|
282
|
+
'TSGS' => 'thunderstorm and small hail',
|
283
|
+
'TSRA' => 'thunderstorm and rain',
|
284
|
+
'TSRA' => 'thunderstorm and snow',
|
285
|
+
'TSRA' => 'thunderstorm and unknown phenomenon', # => AUTO
|
286
|
+
}
|
287
|
+
|
288
|
+
# Accepts all standard (and some non-standard) present weather codes
|
289
|
+
def WeatherPhenomenon.parse(s)
|
290
|
+
codes = Phenomena.keys.join('|')
|
291
|
+
descriptors = Descriptors.keys.join('|')
|
292
|
+
modifiers = Modifiers.keys.join('|')
|
293
|
+
rxp = Regexp.new("^(#{ modifiers })?(#{ descriptors })?(#{ codes })$")
|
294
|
+
if rxp.match(s)
|
295
|
+
modifier_code = $1
|
296
|
+
descriptor_code = $2
|
297
|
+
phenomenon_code = $3
|
298
|
+
Metar::WeatherPhenomenon.new(Phenomena[phenomenon_code], Modifiers[modifier_code], Descriptors[descriptor_code])
|
299
|
+
else
|
300
|
+
nil
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
attr_reader :phenomenon, :modifier, :descriptor
|
305
|
+
def initialize(phenomenon, modifier = nil, descriptor = nil)
|
306
|
+
@phenomenon, @modifier, @descriptor = phenomenon, modifier, descriptor
|
307
|
+
end
|
308
|
+
|
309
|
+
def to_s
|
310
|
+
modifier = @modifier ? @modifier + ' ' : ''
|
311
|
+
descriptor = @descriptor ? @descriptor + ' ' : ''
|
312
|
+
I18n.t("metar.weather.%s%s%s" % [modifier, descriptor, @phenomenon])
|
313
|
+
end
|
314
|
+
|
315
|
+
end
|
316
|
+
|
317
|
+
class SkyCondition
|
318
|
+
|
319
|
+
QUANTITY = {'BKN' => 'broken', 'FEW' => 'few', 'OVC' => 'overcast', 'SCT' => 'scattered'}
|
320
|
+
def SkyCondition.parse(sky_condition)
|
321
|
+
case
|
322
|
+
when (sky_condition == 'NSC' or sky_condition == 'NCD') # WMO
|
323
|
+
new
|
324
|
+
when sky_condition == 'CLR'
|
325
|
+
new
|
326
|
+
when sky_condition == 'SKC'
|
327
|
+
new
|
328
|
+
when sky_condition =~ /^(BKN|FEW|OVC|SCT)(\d+)(CB|TCU|\/{3})?$/
|
329
|
+
quantity = QUANTITY[$1]
|
330
|
+
height = Distance.new($2.to_i * 30.0, { :units => :meters })
|
331
|
+
type = case $3
|
332
|
+
when nil
|
333
|
+
nil
|
334
|
+
when 'CB'
|
335
|
+
'cumulonimbus'
|
336
|
+
when 'TCU'
|
337
|
+
'towering cumulus'
|
338
|
+
when '///'
|
339
|
+
''
|
340
|
+
end
|
341
|
+
new(quantity, height, type)
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
attr_reader :quantity, :height, :type
|
346
|
+
def initialize(quantity = nil, height = nil, type = nil)
|
347
|
+
@quantity, @height, @type = quantity, height, type
|
348
|
+
end
|
349
|
+
|
350
|
+
def to_s
|
351
|
+
if @quantity == nil and @height == nil and @type == nil
|
352
|
+
I18n.t('metar.sky_conditions.clear skies')
|
353
|
+
else
|
354
|
+
type = @type ? ' ' + @type : ''
|
355
|
+
I18n.t("metar.sky_conditions.#{ @quantity }#{ type }") + ' ' + I18n.t('metar.altitude.at') + ' ' + height.to_s
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
end
|
360
|
+
|
361
|
+
class VerticalVisibility
|
362
|
+
|
363
|
+
def VerticalVisibility.parse(vertical_visibility)
|
364
|
+
case
|
365
|
+
when vertical_visibility =~ /^VV(\d{3})$/
|
366
|
+
Distance.new($1.to_f * 30.0, { :units => :meters })
|
367
|
+
when vertical_visibility == '///'
|
368
|
+
Distance.new
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
end
|
373
|
+
|
374
|
+
end
|