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 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
@@ -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