notam 0.1.0
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.
- 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
|