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/lib/metar/parser.rb
ADDED
@@ -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
|
data/lib/metar/report.rb
ADDED
@@ -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
|