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