metar-parser 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|