notam 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +12 -0
- data/LICENSE.txt +21 -0
- data/README.md +223 -0
- data/lib/locales/en.yml +947 -0
- data/lib/notam/errors.rb +5 -0
- data/lib/notam/item/a.rb +37 -0
- data/lib/notam/item/b.rb +26 -0
- data/lib/notam/item/c.rb +39 -0
- data/lib/notam/item/d.rb +56 -0
- data/lib/notam/item/e.rb +31 -0
- data/lib/notam/item/f.rb +34 -0
- data/lib/notam/item/footer.rb +40 -0
- data/lib/notam/item/g.rb +34 -0
- data/lib/notam/item/header.rb +75 -0
- data/lib/notam/item/q.rb +89 -0
- data/lib/notam/item.rb +161 -0
- data/lib/notam/message.rb +123 -0
- data/lib/notam/schedule.rb +253 -0
- data/lib/notam/translation.rb +1063 -0
- data/lib/notam/version.rb +5 -0
- data/lib/notam.rb +28 -0
- data/lib/tasks/fixtures.rake +59 -0
- data/lib/tasks/yard.rake +11 -0
- data.tar.gz.sig +0 -0
- metadata +259 -0
- metadata.gz.sig +0 -0
data/lib/notam/errors.rb
ADDED
data/lib/notam/item/a.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NOTAM
|
4
|
+
|
5
|
+
# The A item defines the locations (ICAO codes) affected by this NOTAM.
|
6
|
+
class A < Item
|
7
|
+
|
8
|
+
RE = %r(
|
9
|
+
\A
|
10
|
+
A\)\s?
|
11
|
+
(?<locations>(?:#{ICAO_RE}\s?)+)
|
12
|
+
(?<parts>(?<part_index>\d+)\s+OF\s+(?<part_index_max>\d+))?
|
13
|
+
\z
|
14
|
+
)x.freeze
|
15
|
+
|
16
|
+
# @return [Array<String>]
|
17
|
+
def locations
|
18
|
+
captures['locations'].split(/\s/)
|
19
|
+
end
|
20
|
+
|
21
|
+
# @return [Integer, nil]
|
22
|
+
def part_index
|
23
|
+
captures['parts'] ? captures['part_index'].to_i : 1
|
24
|
+
end
|
25
|
+
|
26
|
+
# @return [Integer, nil]
|
27
|
+
def part_index_max
|
28
|
+
captures['parts'] ? captures['part_index_max'].to_i : 1
|
29
|
+
end
|
30
|
+
|
31
|
+
# @see NOTAM::Item#merge
|
32
|
+
def merge
|
33
|
+
super(:locations, :part_index, :part_index_max)
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
data/lib/notam/item/b.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NOTAM
|
4
|
+
|
5
|
+
# The B item defines when the NOTAM goes into effect.
|
6
|
+
class B < Item
|
7
|
+
|
8
|
+
RE = %r(
|
9
|
+
\A
|
10
|
+
B\)\s?
|
11
|
+
(?<effective_at>#{TIME_RE})
|
12
|
+
\z
|
13
|
+
)x.freeze
|
14
|
+
|
15
|
+
# @return [Time]
|
16
|
+
def effective_at
|
17
|
+
time(captures['effective_at'])
|
18
|
+
end
|
19
|
+
|
20
|
+
# @see NOTAM::Item#merge
|
21
|
+
def merge
|
22
|
+
super(:effective_at)
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
data/lib/notam/item/c.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NOTAM
|
4
|
+
|
5
|
+
# The C item defines when the NOTAM expires.
|
6
|
+
class C < Item
|
7
|
+
|
8
|
+
RE = %r(
|
9
|
+
\A
|
10
|
+
C\)\s?
|
11
|
+
(?<permanent>
|
12
|
+
PERM|
|
13
|
+
(?<expiration_at>#{TIME_RE}) \s? (?<estimated>EST)?
|
14
|
+
)
|
15
|
+
\z
|
16
|
+
)x.freeze
|
17
|
+
|
18
|
+
# @return [Time, nil]
|
19
|
+
def expiration_at
|
20
|
+
time(captures['expiration_at']) unless no_expiration?
|
21
|
+
end
|
22
|
+
|
23
|
+
# @return [Boolean]
|
24
|
+
def estimated_expiration?
|
25
|
+
!captures['estimated'].nil?
|
26
|
+
end
|
27
|
+
|
28
|
+
# @return [Boolean]
|
29
|
+
def no_expiration?
|
30
|
+
captures['permanent'] == 'PERM'
|
31
|
+
end
|
32
|
+
|
33
|
+
# @see NOTAM::Item#merge
|
34
|
+
def merge
|
35
|
+
super(:expiration_at, :estimated_expiration?, :no_expiration?)
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
data/lib/notam/item/d.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NOTAM
|
4
|
+
|
5
|
+
# The D item contains timesheets to further narrow when exactly the NOTAM
|
6
|
+
# is effective.
|
7
|
+
class D < Item
|
8
|
+
|
9
|
+
# Activity schedules
|
10
|
+
#
|
11
|
+
# @return [Array<NOTAM::Schedule>]
|
12
|
+
attr_reader :schedules
|
13
|
+
|
14
|
+
# @see NOTAM::Item#parse
|
15
|
+
def parse
|
16
|
+
base_date = AIXM.date(data[:effective_at])
|
17
|
+
@schedules = cleanup(text).split(',').map do |string|
|
18
|
+
Schedule.parse(string, base_date: base_date)
|
19
|
+
end
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
# Whether the D item is active at the given time.
|
24
|
+
#
|
25
|
+
# @param at [Time]
|
26
|
+
# @return [Boolean]
|
27
|
+
def active?(at:)
|
28
|
+
schedules.any? { _1.active?(at: at, xy: data[:center_point]) }
|
29
|
+
end
|
30
|
+
|
31
|
+
def five_day_schedules
|
32
|
+
schedules.map do |schedule|
|
33
|
+
schedule
|
34
|
+
.slice(AIXM.date(data[:effective_at]), AIXM.date(data[:effective_at] + 4 * 86_400))
|
35
|
+
.resolve(on: data[:effective_at], xy: data[:center_point])
|
36
|
+
end.map { _1 unless _1.empty? }.compact
|
37
|
+
end
|
38
|
+
|
39
|
+
# @see NOTAM::Item#merge
|
40
|
+
def merge
|
41
|
+
super(:schedules, :five_day_schedules)
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
# @return [String]
|
47
|
+
def cleanup(string)
|
48
|
+
string
|
49
|
+
.gsub(/\s+/, ' ') # collapse whitespaces to single space
|
50
|
+
.gsub(/ ?([-,]) ?/, '\1') # remove spaces around dashes and commas
|
51
|
+
.sub(/\AD\) /, '') # remove item identifier
|
52
|
+
.strip
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
data/lib/notam/item/e.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NOTAM
|
4
|
+
|
5
|
+
# The E item contains a textual description of what this NOTAM is all about.
|
6
|
+
class E < Item
|
7
|
+
|
8
|
+
RE = %r(
|
9
|
+
\A
|
10
|
+
E\)\s?
|
11
|
+
(?<content>.+)
|
12
|
+
\z
|
13
|
+
)mx.freeze
|
14
|
+
|
15
|
+
def content
|
16
|
+
captures['content']
|
17
|
+
end
|
18
|
+
|
19
|
+
def translated_content
|
20
|
+
content.split(/\b/).map do |word|
|
21
|
+
(NOTAM::expand(word, translate: true) || word).upcase
|
22
|
+
end.join
|
23
|
+
end
|
24
|
+
|
25
|
+
# @see NOTAM::Item#merge
|
26
|
+
def merge
|
27
|
+
super(:content, :translated_content)
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
data/lib/notam/item/f.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NOTAM
|
4
|
+
|
5
|
+
# The F item defines the upper limit for this NOTAM.
|
6
|
+
class F < Item
|
7
|
+
|
8
|
+
RE = %r(
|
9
|
+
\A
|
10
|
+
F\)\s?
|
11
|
+
(?<all>
|
12
|
+
SFC|GND|UNL|
|
13
|
+
(?<value>\d+)\s?(?<unit>FT|M)\s?(?<base>AMSL|AGL)|
|
14
|
+
(?<unit>FL)\s?(?<value>\d+)
|
15
|
+
)
|
16
|
+
\z
|
17
|
+
)x.freeze
|
18
|
+
|
19
|
+
# @return [AIXM::Z]
|
20
|
+
def upper_limit
|
21
|
+
case captures['all']
|
22
|
+
when 'UNL' then AIXM::UNLIMITED
|
23
|
+
when 'SFC', 'GND' then AIXM::GROUND
|
24
|
+
else limit(*captures.values_at('value', 'unit', 'base'))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# @see NOTAM::Item#merge
|
29
|
+
def merge
|
30
|
+
super(:upper_limit)
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NOTAM
|
4
|
+
|
5
|
+
# The footer items contain meta information.
|
6
|
+
class Footer < Item
|
7
|
+
|
8
|
+
RE = %r(
|
9
|
+
\A
|
10
|
+
(?<key>CREATED|SOURCE):\s*
|
11
|
+
(?<value>.+)
|
12
|
+
\z
|
13
|
+
)x.freeze
|
14
|
+
|
15
|
+
# @return [String]
|
16
|
+
def key
|
17
|
+
captures['key'].downcase.to_sym
|
18
|
+
end
|
19
|
+
|
20
|
+
# @return [String, Time]
|
21
|
+
def value
|
22
|
+
case key
|
23
|
+
when :created then Time.parse(captures['value'] + ' UTC')
|
24
|
+
else captures['value']
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# @see NOTAM::Item#merge
|
29
|
+
def merge
|
30
|
+
data[key] = value
|
31
|
+
self
|
32
|
+
end
|
33
|
+
|
34
|
+
# @return [String]
|
35
|
+
def inspect
|
36
|
+
%Q(#<#{self.class} "#{truncated_text(start: 0)}">)
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
data/lib/notam/item/g.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NOTAM
|
4
|
+
|
5
|
+
# The G item defines the lower limit for this NOTAM.
|
6
|
+
class G < Item
|
7
|
+
|
8
|
+
RE = %r(
|
9
|
+
\A
|
10
|
+
G\)\s?
|
11
|
+
(?<all>
|
12
|
+
SFC|GND|UNL|
|
13
|
+
(?<value>\d+)\s?(?<unit>FT|M)\s?(?<base>AMSL|AGL)|
|
14
|
+
(?<unit>FL)\s?(?<value>\d+)
|
15
|
+
)
|
16
|
+
\z
|
17
|
+
)x.freeze
|
18
|
+
|
19
|
+
# @return [AIXM::Z]
|
20
|
+
def lower_limit
|
21
|
+
case captures['all']
|
22
|
+
when 'UNL' then AIXM::UNLIMITED
|
23
|
+
when 'SFC', 'GND' then AIXM::GROUND
|
24
|
+
else limit(*captures.values_at('value', 'unit', 'base'))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# @see NOTAM::Item#merge
|
29
|
+
def merge
|
30
|
+
super(:lower_limit)
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
using AIXM::Refinements
|
4
|
+
|
5
|
+
module NOTAM
|
6
|
+
|
7
|
+
# The header item contains the NOTAM ID as well as information as to whether
|
8
|
+
# its a new NOTAM or merely replacing or cancelling another one.
|
9
|
+
class Header < Item
|
10
|
+
|
11
|
+
RE = %r(
|
12
|
+
\A
|
13
|
+
(?<id>#{ID_RE})\s+
|
14
|
+
NOTAM(?<operation>[NRC])\s*
|
15
|
+
(?<old_id>#{ID_RE.decapture})?
|
16
|
+
\z
|
17
|
+
)x.freeze
|
18
|
+
|
19
|
+
# @return [String] ID of this message
|
20
|
+
def id
|
21
|
+
captures['id']
|
22
|
+
end
|
23
|
+
|
24
|
+
# @return [String] series letter
|
25
|
+
def id_series
|
26
|
+
captures['id_series']
|
27
|
+
end
|
28
|
+
|
29
|
+
# @return [Integer] serial number
|
30
|
+
def id_number
|
31
|
+
captures['id_number'].to_i
|
32
|
+
end
|
33
|
+
|
34
|
+
# @return [Integer] year identifier
|
35
|
+
def id_year
|
36
|
+
captures['id_year'].to_i + (Date.today.year / 1000 * 1000)
|
37
|
+
end
|
38
|
+
|
39
|
+
# @return [Boolean] +true+ if this is a new message, +false+ if it
|
40
|
+
# replaces or cancels another message
|
41
|
+
def new?
|
42
|
+
captures['operation'] == 'N'
|
43
|
+
end
|
44
|
+
|
45
|
+
# @return [String, nil] message being replaced by this message or +nil+
|
46
|
+
# if this message is a new or cancelling one
|
47
|
+
def replaces
|
48
|
+
captures['old_id'] if captures['operation'] == 'R'
|
49
|
+
end
|
50
|
+
|
51
|
+
# @return [String, nil] message being cancelled by this message or +nil+
|
52
|
+
# if this message is a new or replacing one
|
53
|
+
def cancels
|
54
|
+
captures['old_id'] if captures['operation'] == 'C'
|
55
|
+
end
|
56
|
+
|
57
|
+
# @see NOTAM::Item#parse
|
58
|
+
def parse
|
59
|
+
super
|
60
|
+
fail! "invalid operation" unless (new? && !captures['old_id']) || replaces || cancels
|
61
|
+
self
|
62
|
+
end
|
63
|
+
|
64
|
+
# @see NOTAM::Item#merge
|
65
|
+
def merge
|
66
|
+
super(:id, :id_series, :id_number, :id_year, :new?, :replaces, :cancels)
|
67
|
+
end
|
68
|
+
|
69
|
+
# @return [String]
|
70
|
+
def inspect
|
71
|
+
%Q(#<#{self.class} "#{truncated_text(start: 0)}">)
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
data/lib/notam/item/q.rb
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NOTAM
|
4
|
+
|
5
|
+
# The Q item provides the context such as the FIR or conditions.
|
6
|
+
class Q < Item
|
7
|
+
|
8
|
+
RE = %r(
|
9
|
+
\A
|
10
|
+
Q\)\s?
|
11
|
+
(?<fir>#{ICAO_RE})/
|
12
|
+
Q(?<subject>[A-Z]{2})(?<condition>[A-Z]{2})/
|
13
|
+
(?<traffic>IV|(?:[IVK]\s?))/
|
14
|
+
(?<purpose>NBO|BO\s?|(?:[BMK]\s{0,2}))/
|
15
|
+
(?<scope>A[EW]|(?:[AEWK]\s?))/
|
16
|
+
(?<lower_limit>\d{3})/
|
17
|
+
(?<upper_limit>\d{3})/
|
18
|
+
(?<lat_deg>\d{2})(?<lat_min>\d{2})(?<lat_dir>[NS])
|
19
|
+
(?<long_deg>\d{3})(?<long_min>\d{2})(?<long_dir>[EW])
|
20
|
+
(?<radius>\d{3})
|
21
|
+
\z
|
22
|
+
)x.freeze
|
23
|
+
|
24
|
+
# @return [String]
|
25
|
+
def fir
|
26
|
+
captures['fir']
|
27
|
+
end
|
28
|
+
|
29
|
+
# @return [Symbol]
|
30
|
+
def subject
|
31
|
+
NOTAM.subject_for(captures['subject'])
|
32
|
+
end
|
33
|
+
|
34
|
+
# @return [Symbol]
|
35
|
+
def condition
|
36
|
+
NOTAM.condition_for(captures['condition'])
|
37
|
+
end
|
38
|
+
|
39
|
+
# @return [Symbol]
|
40
|
+
def traffic
|
41
|
+
NOTAM.traffic_for(captures['traffic'].strip)
|
42
|
+
end
|
43
|
+
|
44
|
+
# @return [Array<Symbol>]
|
45
|
+
def purpose
|
46
|
+
captures['purpose'].strip.chars.map { NOTAM.purpose_for(_1) }
|
47
|
+
end
|
48
|
+
|
49
|
+
# @return [Array<Symbol>]
|
50
|
+
def scope
|
51
|
+
captures['scope'].strip.chars.map { NOTAM.scope_for(_1) }
|
52
|
+
end
|
53
|
+
|
54
|
+
# @return [AIXM::Z] lower limit (QNE flight level) or {AIXM::GROUND}
|
55
|
+
# (aka: 0ft QFE)
|
56
|
+
def lower_limit
|
57
|
+
if (limit = captures['lower_limit'].to_i).zero?
|
58
|
+
AIXM::GROUND
|
59
|
+
else
|
60
|
+
AIXM.z(captures['lower_limit'].to_i, :qne)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# @return [AIXM::Z] upper limit (QNE flight level) or {AIXM::UNLIMITED}
|
65
|
+
# (aka: FL999 QNE)
|
66
|
+
def upper_limit
|
67
|
+
AIXM.z(captures['upper_limit'].to_i, :qne)
|
68
|
+
end
|
69
|
+
|
70
|
+
# @return [AIXM::XY] approximately affected area center point
|
71
|
+
def center_point
|
72
|
+
AIXM.xy(
|
73
|
+
lat: %Q(#{captures['lat_deg']}°#{captures['lat_min']}'00"#{captures['lat_dir']}),
|
74
|
+
long: %Q(#{captures['long_deg']}°#{captures['long_min']}'00"#{captures['long_dir']})
|
75
|
+
)
|
76
|
+
end
|
77
|
+
|
78
|
+
# @return [AIXM::D] approximately affected area radius
|
79
|
+
def radius
|
80
|
+
AIXM.d(captures['radius'].to_i, :nm)
|
81
|
+
end
|
82
|
+
|
83
|
+
# @see NOTAM::Item#merge
|
84
|
+
def merge
|
85
|
+
super(:fir, :subject, :condition, :traffic, :purpose, :scope, :lower_limit, :upper_limit, :center_point, :radius)
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
end
|
data/lib/notam/item.rb
ADDED
@@ -0,0 +1,161 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NOTAM
|
4
|
+
|
5
|
+
# Items are the building blocks of a NOTAM message. They usually consist of
|
6
|
+
# only one line of plain text each, however, D and E items may span over
|
7
|
+
# multiple lines of plain text.
|
8
|
+
class Item
|
9
|
+
|
10
|
+
RE = /[QA-G]\)\s/.freeze
|
11
|
+
|
12
|
+
ID_RE = /(?<id_series>[A-Z])(?<id_number>\d{4})\/(?<id_year>\d{2})/.freeze
|
13
|
+
ICAO_RE = /[A-Z]{4}/.freeze
|
14
|
+
TIME_RE = /(?:\d{2})(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\d|3[01])(?:[01]\d|[2][0-4])(?:[0-5]\d)/.freeze
|
15
|
+
|
16
|
+
# Raw NOTAM item text
|
17
|
+
#
|
18
|
+
# @return [String]
|
19
|
+
attr_reader :text
|
20
|
+
|
21
|
+
# Captures from the default item regexp +RE+
|
22
|
+
#
|
23
|
+
# @return [MatchData]
|
24
|
+
attr_reader :captures
|
25
|
+
|
26
|
+
# Parsed NOTAM message payload
|
27
|
+
#
|
28
|
+
# @return [Hash]
|
29
|
+
attr_reader :data
|
30
|
+
|
31
|
+
# Analyses the raw NOTAM item text and initialize the corresponding item
|
32
|
+
# object.
|
33
|
+
#
|
34
|
+
# @note Some NOTAM items (most notably {NOTAM::D}) depend on previous items
|
35
|
+
# for meaningful parsing and may fail if this information is not made
|
36
|
+
# available by passing the NOTAM message payload parsed so far as +data+.
|
37
|
+
#
|
38
|
+
# @example
|
39
|
+
# NOTAM::Item.new('A0135/20 NOTAMN') # => #<NOTAM::Head id="A0135/20">
|
40
|
+
# NOTAM::Item.new('B) 0208231540') # => #<NOTAM::B>
|
41
|
+
# NOTAM::Item.new('foobar') # => NOTAM::ParseError
|
42
|
+
#
|
43
|
+
# @param text [String]
|
44
|
+
# @param data [Hash]
|
45
|
+
# @return [NOTAM::Header, NOTAM::Q, NOTAM::A, NOTAM::B, NOTAM::C,
|
46
|
+
# NOTAM::D, NOTAM::E, NOTAM::F, NOTAM::G, NOTAM::Footer]
|
47
|
+
def initialize(text, data: {})
|
48
|
+
@text, @data = text.strip, data
|
49
|
+
end
|
50
|
+
|
51
|
+
class << self
|
52
|
+
# @!visibility private
|
53
|
+
def new(text, data: {})
|
54
|
+
NOTAM.const_get(type(text)).allocate.instance_eval do
|
55
|
+
initialize(text, data: data)
|
56
|
+
self
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Analyses the raw NOTAM item text and detect its type.
|
61
|
+
#
|
62
|
+
# @example
|
63
|
+
# NOTAM::Item.type('A0135/20 NOTAMN') # => :Header
|
64
|
+
# NOTAM::Item.type('B) 0208231540') # => :B
|
65
|
+
# NOTAM::Item.type('SOURCE: LFNT') # => :Footer
|
66
|
+
# NOTAM::Item.type('foobar') # => NOTAM::ParseError
|
67
|
+
#
|
68
|
+
# @raise [NOTAM::ParseError]
|
69
|
+
# @return [String]
|
70
|
+
def type(text)
|
71
|
+
case text.strip
|
72
|
+
when /\A([A-GQ])\)/ then $1
|
73
|
+
when NOTAM::Header::RE then 'Header'
|
74
|
+
when NOTAM::Footer::RE then 'Footer'
|
75
|
+
else fail(NOTAM::ParseError, 'item not recognized')
|
76
|
+
end.to_sym
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Matches the raw NOTAM item text against +RE+ and populates {#captures}.
|
81
|
+
#
|
82
|
+
# @note May be extended or overwritten in subclasses, but must always
|
83
|
+
# return +self+!
|
84
|
+
#
|
85
|
+
# @example
|
86
|
+
# NOTAM::Item.new('A0135/20 NOTAMN').parse # => #<NOTAM::Header id="A0135/20">
|
87
|
+
# NOTAM::Item.new('foobar').parse # => NOTAM::ParseError
|
88
|
+
#
|
89
|
+
# @abstract
|
90
|
+
# @raise [NOTAM::ParseError]
|
91
|
+
# @return [self]
|
92
|
+
def parse
|
93
|
+
if match_data = self.class::RE.match(text)
|
94
|
+
@captures = match_data.named_captures
|
95
|
+
self
|
96
|
+
else
|
97
|
+
fail! 'text does not match regexp'
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Merges the return values of the given methods into the +data+ hash.
|
102
|
+
#
|
103
|
+
# @note Must be extended in subclasses.
|
104
|
+
#
|
105
|
+
# @abstract
|
106
|
+
# @params methods [Array<Symbol>]
|
107
|
+
# @return [self]
|
108
|
+
def merge(*methods)
|
109
|
+
fail 'nothing to merge' unless methods.any?
|
110
|
+
methods.each { @data[_1] = send(_1) }
|
111
|
+
@data.compact!
|
112
|
+
self
|
113
|
+
end
|
114
|
+
|
115
|
+
# Type of the item
|
116
|
+
#
|
117
|
+
# @return [Symbol] either +:Header+, +:Q+, +(:A..:G)+ or +:Footer+
|
118
|
+
def type
|
119
|
+
self.class.to_s[7..].to_sym
|
120
|
+
end
|
121
|
+
|
122
|
+
# Raise {NOTAM::ParseError} along with some debugging information.
|
123
|
+
#
|
124
|
+
# @param message [String] optional error message
|
125
|
+
# @raise [NOTAM::ParseError]
|
126
|
+
def fail!(message=nil)
|
127
|
+
fail(NOTAM::ParseError, [message, text].compact.join(': '))
|
128
|
+
end
|
129
|
+
|
130
|
+
# @return [String]
|
131
|
+
def inspect
|
132
|
+
%Q(#<#{self.class} "#{truncated_text}">)
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
|
137
|
+
def time(timestamp)
|
138
|
+
short_year, month, day, hour, minute = timestamp.scan(/\d{2}/).map(&:to_i)
|
139
|
+
millenium = Time.now.year / 100 * 100
|
140
|
+
Time.utc(millenium + short_year, month, day, hour, minute)
|
141
|
+
end
|
142
|
+
|
143
|
+
def limit(value, unit, base)
|
144
|
+
if captures['base']
|
145
|
+
d = AIXM.d(value.to_i, unit).to_ft
|
146
|
+
AIXM.z(d.dim.round, { 'AMSL' => :qnh, 'AGL' => :qfe }[base])
|
147
|
+
else
|
148
|
+
AIXM.z(value.to_i, :qne)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def truncated_text(start: 3, length: 40)
|
153
|
+
if text.length > start + length - 1
|
154
|
+
text[start, length - 1] + '…'
|
155
|
+
else
|
156
|
+
text[start..]
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
end
|
161
|
+
end
|