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