metar-parser 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,352 @@
1
+ require 'rubygems' if RUBY_VERSION < '1.9'
2
+ require 'aasm'
3
+ require File.join(File.dirname(__FILE__), 'data')
4
+
5
+ module Metar
6
+
7
+ class ParseError < StandardError
8
+ end
9
+
10
+ class Parser
11
+ include AASM
12
+
13
+ aasm_initial_state :start
14
+
15
+ aasm_state :start, :after_enter => :seek_location
16
+ aasm_state :location, :after_enter => :seek_datetime
17
+ aasm_state :datetime, :after_enter => [:seek_cor_auto, :seek_wind]
18
+ aasm_state :wind, :after_enter => :seek_variable_wind
19
+ aasm_state :variable_wind, :after_enter => :seek_visibility
20
+ aasm_state :visibility, :after_enter => :seek_runway_visible_range
21
+ aasm_state :runway_visible_range, :after_enter => :seek_present_weather
22
+ aasm_state :present_weather, :after_enter => :seek_sky_conditions
23
+ aasm_state :sky_conditions, :after_enter => :seek_vertical_visibility
24
+ aasm_state :vertical_visibility, :after_enter => :seek_temperature_dew_point
25
+ aasm_state :temperature_dew_point, :after_enter => :seek_sea_level_pressure
26
+ aasm_state :sea_level_pressure, :after_enter => :seek_remarks
27
+ aasm_state :remarks, :after_enter => :seek_end
28
+ aasm_state :end
29
+
30
+ aasm_event :location do
31
+ transitions :from => :start, :to => :location
32
+ end
33
+
34
+ aasm_event :datetime do
35
+ transitions :from => :location, :to => :datetime
36
+ end
37
+
38
+ aasm_event :wind do
39
+ transitions :from => :datetime, :to => :wind
40
+ end
41
+
42
+ aasm_event :cavok do
43
+ transitions :from => :variable_wind, :to => :sky_conditions
44
+ end
45
+
46
+ aasm_event :variable_wind do
47
+ transitions :from => :wind, :to => :variable_wind
48
+ end
49
+
50
+ aasm_event :visibility do
51
+ transitions :from => [:wind, :variable_wind], :to => :visibility
52
+ end
53
+
54
+ aasm_event :runway_visible_range do
55
+ transitions :from => [:visibility], :to => :runway_visible_range
56
+ end
57
+
58
+ aasm_event :present_weather do
59
+ transitions :from => [:runway_visible_range],
60
+ :to => :present_weather
61
+ end
62
+
63
+ aasm_event :sky_conditions do
64
+ transitions :from => [:present_weather, :visibility, :sky_conditions],
65
+ :to => :sky_conditions
66
+ end
67
+
68
+ aasm_event :vertical_visibility do
69
+ transitions :from => [:present_weather, :visibility, :sky_conditions],
70
+ :to => :vertical_visibility
71
+ end
72
+
73
+ aasm_event :temperature_dew_point do
74
+ transitions :from => [:wind, :sky_conditions, :vertical_visibility], :to => :temperature_dew_point
75
+ end
76
+
77
+ aasm_event :sea_level_pressure do
78
+ transitions :from => :temperature_dew_point, :to => :sea_level_pressure
79
+ end
80
+
81
+ aasm_event :remarks do
82
+ transitions :from => [:temperature_dew_point, :sea_level_pressure],
83
+ :to => :remarks
84
+ end
85
+
86
+ aasm_event :done do
87
+ transitions :from => [:remarks], :to => :end
88
+ end
89
+
90
+ def Parser.for_cccc(cccc)
91
+ station = Metar::Station.new(cccc)
92
+ raw = Metar::Raw.new(station)
93
+ parser = new(raw)
94
+ parser.analyze
95
+ parser
96
+ end
97
+
98
+ attr_reader :station_code, :time, :observer, :wind, :variable_wind, :visibility, :runway_visible_range,
99
+ :present_weather, :sky_conditions, :vertical_visibility, :temperature, :dew_point, :sea_level_pressure, :remarks
100
+
101
+ def initialize(raw)
102
+ @metar = raw.metar.clone
103
+ @time = raw.time.clone
104
+ analyze
105
+ end
106
+
107
+ def attributes
108
+ h = {
109
+ :station_code => @location.clone,
110
+ :time => @time.to_s,
111
+ :observer => Report.symbol_to_s(@observer)
112
+ }
113
+ h[:wind] = @wind if @wind
114
+ h[:variable_wind] = @variable_wind.clone if @variable_wind
115
+ h[:visibility] = @visibility if @visibility
116
+ h[:runway_visible_range] = @runway_visible_range if @runway_visible_range
117
+ h[:present_weather] = @present_weather if @present_weather
118
+ h[:sky_conditions] = @sky_conditions if @sky_conditions
119
+ h[:vertical_visibility] = @vertical_visibility if @vertical_visibility
120
+ h[:temperature] = @temperature
121
+ h[:dew_point] = @dew_point
122
+ h[:sea_level_pressure] = @sea_level_pressure
123
+ h[:remarks] = @remarks.clone if @remarks
124
+ h
125
+ end
126
+
127
+ def date
128
+ Date.new(@time.year, @time.month, @time.day)
129
+ end
130
+
131
+ private
132
+
133
+ def analyze
134
+ @chunks = @metar.split(' ')
135
+
136
+ @location = nil
137
+ @observer = :real
138
+ @wind = nil
139
+ @variable_wind = nil
140
+ @visibility = nil
141
+ @runway_visible_range = nil
142
+ @present_weather = nil
143
+ @sky_conditions = nil
144
+ @vertical_visibility = nil
145
+ @temperature = nil
146
+ @dew_point = nil
147
+ @sea_level_pressure = nil
148
+ @remarks = nil
149
+
150
+ aasm_enter_initial_state
151
+ end
152
+
153
+ def seek_location
154
+ if @chunks[0] =~ /^[A-Z][A-Z0-9]{3}$/
155
+ @location = @chunks.shift
156
+ else
157
+ raise ParseError.new("Expecting location, found '#{ @chunks[0] }'")
158
+ end
159
+ location!
160
+ end
161
+
162
+ def seek_datetime
163
+ case
164
+ when @chunks[0] =~ /^\d{6}Z$/
165
+ @datetime = @chunks.shift
166
+ else
167
+ raise ParseError.new("Expecting datetime, found '#{ @chunks[0] }'")
168
+ end
169
+ datetime!
170
+ end
171
+
172
+ def seek_cor_auto
173
+ case
174
+ when @chunks[0] == 'AUTO' # WMO 15.4
175
+ @chunks.shift
176
+ @observer = :auto
177
+ when @chunks[0] == 'COR'
178
+ @chunks.shift
179
+ @observer = :corrected
180
+ end
181
+ end
182
+
183
+ def seek_wind
184
+ wind = Wind.parse(@chunks[0])
185
+ if wind
186
+ @chunks.shift
187
+ @wind = wind
188
+ end
189
+ wind!
190
+ end
191
+
192
+ def seek_variable_wind
193
+ variable_wind = VariableWind.parse(@chunks[0])
194
+ if variable_wind
195
+ @chunks.shift
196
+ @variable_wind = variable_wind
197
+ end
198
+ variable_wind!
199
+ end
200
+
201
+ def seek_visibility
202
+ if @chunks[0] == 'CAVOK'
203
+ @chunks.shift
204
+ @visibility = Visibility.new(M9t::Distance.kilometers(10), nil, :more_than)
205
+ @present_weather ||= []
206
+ @present_weather << Metar::WeatherPhenomenon.new('No significant weather')
207
+ @sky_conditions ||= []
208
+ @sky_conditions << 'No significant cloud' # TODO: What does NSC stand for?
209
+ cavok!
210
+ return
211
+ end
212
+
213
+ if @observer == :auto # WMO 15.4
214
+ if @chunks[0] == '////'
215
+ @chunks.shift
216
+ @visibility = Visibility.new('Not observed')
217
+ visibility!
218
+ return
219
+ end
220
+ end
221
+
222
+ if @chunks[0] == '1' or @chunks[0] == '2'
223
+ visibility = Visibility.parse(@chunks[0] + ' ' + @chunks[1])
224
+ if visibility
225
+ @chunks.shift
226
+ @chunks.shift
227
+ @visibility = visibility
228
+ end
229
+ else
230
+ visibility = Visibility.parse(@chunks[0])
231
+ if visibility
232
+ @chunks.shift
233
+ @visibility = visibility
234
+ end
235
+ end
236
+ visibility!
237
+ end
238
+
239
+ def collect_runway_visible_range
240
+ runway_visible_range = RunwayVisibleRange.parse(@chunks[0])
241
+ if runway_visible_range
242
+ @chunks.shift
243
+ @runway_visible_range ||= []
244
+ @runway_visible_range << runway_visible_range
245
+ collect_runway_visible_range
246
+ end
247
+ end
248
+
249
+ def seek_runway_visible_range
250
+ collect_runway_visible_range
251
+ runway_visible_range!
252
+ end
253
+
254
+ def collect_present_weather
255
+ wtp = WeatherPhenomenon.parse(@chunks[0])
256
+ if wtp
257
+ @chunks.shift
258
+ @present_weather ||= []
259
+ @present_weather << wtp
260
+ collect_present_weather
261
+ end
262
+ end
263
+
264
+ def seek_present_weather
265
+ if @observer == :auto
266
+ if @chunks[0] == '//' # WMO 15.4
267
+ @present_weather ||= []
268
+ @present_weather << Metar::WeatherPhenomenon.new('not observed')
269
+ present_weather!
270
+ return
271
+ end
272
+ end
273
+
274
+ collect_present_weather
275
+ present_weather!
276
+ end
277
+
278
+ def collect_sky_conditions
279
+ sky_condition = SkyCondition.parse(@chunks[0])
280
+ if sky_condition
281
+ @chunks.shift
282
+ @sky_conditions ||= []
283
+ @sky_conditions << sky_condition
284
+ collect_sky_conditions
285
+ end
286
+ end
287
+
288
+ def seek_sky_conditions
289
+ if @observer == :auto # WMO 15.4
290
+ if @chunks[0] == '///' or @chunks[0] == '//////'
291
+ @chunks.shift
292
+ @sky_conditions ||= []
293
+ @sky_conditions << 'Not observed'
294
+ sky_conditions!
295
+ return
296
+ end
297
+ end
298
+
299
+ collect_sky_conditions
300
+ sky_conditions!
301
+ end
302
+
303
+ def seek_vertical_visibility
304
+ vertical_visibility = VerticalVisibility.parse(@chunks[0])
305
+ if vertical_visibility
306
+ @chunks.shift
307
+ @vertical_visibility = vertical_visibility
308
+ end
309
+ vertical_visibility!
310
+ end
311
+
312
+ def seek_temperature_dew_point
313
+ case
314
+ when @chunks[0] =~ /^(M?\d+|XX|\/\/)\/(M?\d+|XX|\/\/)?$/
315
+ @chunks.shift
316
+ @temperature = Metar::Temperature.parse($1)
317
+ @dew_point = Metar::Temperature.parse($2)
318
+ else
319
+ raise ParseError.new("Expecting temperature/dew point, found '#{ @chunks[0] }'")
320
+ end
321
+ temperature_dew_point!
322
+ end
323
+
324
+ def seek_sea_level_pressure
325
+ sea_level_pressure = Pressure.parse(@chunks[0])
326
+ if sea_level_pressure
327
+ @chunks.shift
328
+ @sea_level_pressure = sea_level_pressure
329
+ end
330
+ sea_level_pressure!
331
+ end
332
+
333
+ def seek_remarks
334
+ if @chunks[0] == 'RMK'
335
+ @chunks.shift
336
+ end
337
+ @remarks ||= []
338
+ @remarks += @chunks.clone
339
+ @chunks = []
340
+ remarks!
341
+ end
342
+
343
+ def seek_end
344
+ if @chunks.length > 0
345
+ raise ParseError.new("Unexpected tokens found at end of string: found '#{ @chunks.join(' ') }'")
346
+ end
347
+ done!
348
+ end
349
+
350
+ end
351
+
352
+ end
data/lib/metar/raw.rb ADDED
@@ -0,0 +1,49 @@
1
+ require 'rubygems' if RUBY_VERSION < '1.9'
2
+ require 'net/ftp'
3
+
4
+ module Metar
5
+
6
+ class Raw
7
+
8
+ class << self
9
+
10
+ @connection = nil
11
+
12
+ def cache_connection
13
+ @connection = connection
14
+ end
15
+
16
+ def connection
17
+ return @connection if @connection
18
+ connection = Net::FTP.new('tgftp.nws.noaa.gov')
19
+ connection.login
20
+ connection.chdir('data/observations/metar/stations')
21
+ connection.passive = true
22
+ connection
23
+ end
24
+
25
+ def fetch(cccc)
26
+ s = ''
27
+ connection.retrbinary("RETR #{ cccc }.TXT", 1024) do |chunk|
28
+ s << chunk
29
+ end
30
+ s
31
+ end
32
+
33
+ end
34
+
35
+ attr_reader :cccc, :raw, :metar, :time
36
+ alias :to_s :metar
37
+
38
+ # Station is a string containing the CCCC code, or
39
+ # an object with a 'cccc' method which returns the code
40
+ def initialize(station, raw = nil)
41
+ @cccc = station.respond_to?(:cccc) ? station.cccc : station
42
+ @raw = raw || Raw.fetch(@cccc)
43
+ time, @metar = @raw.split("\n")
44
+ @time = Time.parse(time)
45
+ end
46
+
47
+ end
48
+
49
+ end
@@ -0,0 +1,95 @@
1
+ # encoding: utf-8
2
+
3
+ module Metar
4
+ class Report
5
+
6
+ def initialize(parser)
7
+ @parser = parser
8
+ end
9
+
10
+ def date
11
+ I18n.l(@parser.date)
12
+ end
13
+
14
+ def time
15
+ "%u:%u" % [@parser.time.hour, @parser.time.min]
16
+ end
17
+
18
+ def wind
19
+ @parser.wind.to_s
20
+ end
21
+
22
+ def variable_wind
23
+ @parser.variable_wind.to_s
24
+ end
25
+
26
+ def visibility
27
+ @parser.visibility.to_s
28
+ end
29
+
30
+ def runway_visible_range
31
+ @parser.runway_visible_range.collect { |rvr| rvr.to_s }.join(', ')
32
+ end
33
+
34
+ def present_weather
35
+ @parser.present_weather.to_s
36
+ end
37
+
38
+ def sky_conditions
39
+ @parser.sky_conditions.collect { |sky| sky.to_s }.join(', ')
40
+ end
41
+
42
+ def vertical_visibility
43
+ @parser.vertical_visibility.to_s
44
+ end
45
+
46
+ def temperature
47
+ @parser.temperature.to_s
48
+ end
49
+
50
+ def dew_point
51
+ @parser.dew_point.to_s
52
+ end
53
+
54
+ def sea_level_pressure
55
+ @parser.sea_level_pressure.to_s
56
+ end
57
+
58
+ end
59
+ end
60
+
61
+ __END__
62
+
63
+
64
+ def attributes_to_s
65
+ attrib = attributes
66
+ texts = {}
67
+ texts[:wind] = attrib[:wind] if attrib[:wind]
68
+ texts[:variable_wind] = attrib[:variable_wind] if attrib[:variable_wind]
69
+ texts[:visibility] = "%u meters" % attrib[:visibility].value if attrib[:visibility]
70
+ texts[:runway_visible_range] = attrib[:runway_visible_range].join(', ') if attrib[:runway_visible_range]
71
+ texts[:present_weather] = attrib[:present_weather].join(', ') if attrib[:present_weather]
72
+ texts[:sky_conditions] = attrib[:sky_conditions].join(', ') if attrib[:sky_conditions]
73
+ texts[:temperature] = "%u celcius" % attrib[:temperature] if attrib[:temperature]
74
+ texts[:dew_point] = "%u celcius" % attrib[:dew_point] if attrib[:dew_point]
75
+ texts[:remarks] = attrib[:remarks].join(', ') if attrib[:remarks]
76
+
77
+ texts
78
+ end
79
+
80
+ def to_s
81
+ # If attributes supplied an ordered hash, the hoop-jumping below
82
+ # wouldn't be necessary
83
+ attr = attributes_to_s
84
+ [:station_code, :time, :observer, :wind, :variable_wind, :visibility, :runway_visible_range,
85
+ :present_weather, :sky_conditions, :temperature, :dew_point, :remarks].collect do |key|
86
+ attr[key] ? self.symbol_to_s(key) + ": " + attr[key] : nil
87
+ end.compact.join("\n")
88
+ end
89
+
90
+ private
91
+
92
+ # :symbol_etc => 'Symbol etc'
93
+ def self.symbol_to_s(sym)
94
+ sym.to_s.gsub(/^([a-z])/) {$1.upcase}.gsub(/_([a-z])/) {" #$1"}
95
+ end
@@ -0,0 +1,176 @@
1
+ require 'rubygems' if RUBY_VERSION < '1.9'
2
+ require 'open-uri'
3
+
4
+ # A Station can be created without downloading data from the Internet.
5
+ # The class downloads and caches the NOAA station list when it is first requested.
6
+ # As soon of any of the attributes are read, the data is downloaded (if necessary), and attributes are set.
7
+
8
+ module Metar
9
+
10
+ class Station
11
+ NOAA_STATION_LIST_URL = 'http://weather.noaa.gov/data/nsd_cccc.txt'
12
+
13
+ class << self
14
+
15
+ @nsd_cccc = nil # Contains the text of the station list
16
+ attr_accessor :nsd_cccc # Allow tests to run from local file
17
+
18
+ def download_local
19
+ nsd_cccc = Metar::Station.download_stations
20
+ File.open(Metar::Station.local_nsd_path, 'w') do |fil|
21
+ fil.write nsd_cccc
22
+ end
23
+ end
24
+
25
+ # Load local copy of the station list
26
+ # and download it first if missing
27
+ def load_local
28
+ download_local if not File.exist?(Metar::Station.local_nsd_path)
29
+ @nsd_cccc = File.open(Metar::Station.local_nsd_path) do |fil|
30
+ fil.read
31
+ end
32
+ end
33
+
34
+ def all
35
+ all_structures.collect do |h|
36
+ options = h.clone
37
+ cccc = options.delete(:cccc)
38
+ new(cccc, h)
39
+ end
40
+ end
41
+
42
+ def find_by_cccc(cccc)
43
+ all.find { |station| station.cccc == cccc }
44
+ end
45
+
46
+ # Does the given CCCC code exist?
47
+ def exist?(cccc)
48
+ not find_data_by_cccc(cccc).nil?
49
+ end
50
+
51
+ def to_longitude(s)
52
+ s =~ /^(\d+)-(\d+)([EW])/ or return nil
53
+ ($3 == 'E' ? 1.0 : -1.0) * ($1.to_f + $2.to_f / 60.0)
54
+ end
55
+
56
+ def to_latitude(s)
57
+ s =~ /^(\d+)-(\d+)([SN])/ or return nil
58
+ ($3 == 'N' ? 1.0 : -1.0) * ($1.to_f + $2.to_f / 60.0)
59
+ end
60
+ end
61
+
62
+ attr_reader :cccc, :loaded
63
+ alias :code :cccc
64
+ # loaded? indicates whether the data has been collected from the Web
65
+ alias :loaded? :loaded
66
+
67
+ # No check is made on the existence of the station
68
+ def initialize(cccc, options = {})
69
+ raise "Station identifier must not be nil" if cccc.nil?
70
+ raise "Station identifier must be a text" if not cccc.respond_to?('to_s')
71
+ @cccc = cccc
72
+ @name = options[:name]
73
+ @state = options[:state]
74
+ @country = options[:country]
75
+ @longitude = options[:longitude]
76
+ @latitude = options[:latitude]
77
+ @raw = options[:raw]
78
+ @loaded = false
79
+ end
80
+
81
+ # Lazy loaded attributes
82
+ ## TODO: DRY this up by generating these methods
83
+ def name
84
+ load! if not @loaded
85
+ @name
86
+ end
87
+
88
+ def state
89
+ load! if not @loaded
90
+ @state
91
+ end
92
+
93
+ def country
94
+ load! if not @loaded
95
+ @country
96
+ end
97
+
98
+ def latitude
99
+ load! if not @loaded
100
+ @latitude
101
+ end
102
+
103
+ def longitude
104
+ load! if not @loaded
105
+ @longitude
106
+ end
107
+
108
+ def raw
109
+ load! if not @loaded
110
+ @raw
111
+ end
112
+
113
+ def exist?
114
+ Station.exist?(@cccc)
115
+ end
116
+
117
+ private
118
+
119
+ class << self
120
+
121
+ @structures = nil
122
+
123
+ def download_stations
124
+ open(NOAA_STATION_LIST_URL) { |fil| fil.read }
125
+ end
126
+
127
+ # Path for saving a local copy of the nsc_cccc station list
128
+ def local_nsd_path
129
+ File.join(File.expand_path(File.dirname(__FILE__) + '/../../'), 'data', 'nsd_cccc.txt')
130
+ end
131
+
132
+ def all_structures
133
+ return @structures if @structures
134
+
135
+ @nsd_cccc ||= download_stations
136
+ @structures = []
137
+
138
+ @nsd_cccc.each_line do |station|
139
+ fields = station.split(';')
140
+ @structures << {
141
+ :cccc => fields[0],
142
+ :name => fields[3],
143
+ :state => fields[4],
144
+ :country => fields[5],
145
+ :latitude => fields[7],
146
+ :longitude => fields[8],
147
+ :raw => station.clone
148
+ }
149
+ end
150
+
151
+ @structures
152
+ end
153
+
154
+ def find_data_by_cccc(cccc)
155
+ all_structures.find { |station| station[:cccc] == cccc }
156
+ end
157
+
158
+ end
159
+
160
+ # Get data from the NOAA data file (very slow on first call!)
161
+ def load!
162
+ noaa_data = Station.find_data_by_cccc(@cccc)
163
+ raise "Station identifier '#{ @cccc }' not found" if noaa_data.nil?
164
+ @name = noaa_data[:name]
165
+ @state = noaa_data[:state]
166
+ @country = noaa_data[:country]
167
+ @longitude = Station.to_longitude(noaa_data[:longitude])
168
+ @latitude = Station.to_latitude(noaa_data[:latitude])
169
+ @raw = noaa_data[:raw]
170
+ @loaded = true
171
+ self
172
+ end
173
+
174
+ end
175
+
176
+ end
data/lib/metar.rb ADDED
@@ -0,0 +1,16 @@
1
+ require File.join(File.dirname(__FILE__), 'metar', 'raw')
2
+ require File.join(File.dirname(__FILE__), 'metar', 'station')
3
+ require File.join(File.dirname(__FILE__), 'metar', 'parser')
4
+ require File.join(File.dirname(__FILE__), 'metar', 'report')
5
+
6
+ module Metar
7
+
8
+ module VERSION #:nodoc:
9
+ MAJOR = 0
10
+ MINOR = 1
11
+ TINY = 1
12
+
13
+ STRING = [MAJOR, MINOR, TINY].join('.')
14
+ end
15
+
16
+ end