metar-parser 0.1.1

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