aipp 0.2.4 → 0.2.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +26 -0
  3. data/.ruby-version +1 -1
  4. data/CHANGELOG.md +9 -0
  5. data/README.md +59 -7
  6. data/aipp.gemspec +3 -2
  7. data/lib/aipp.rb +2 -0
  8. data/lib/aipp/aip.rb +4 -12
  9. data/lib/aipp/downloader.rb +21 -21
  10. data/lib/aipp/executable.rb +4 -1
  11. data/lib/aipp/parser.rb +58 -5
  12. data/lib/aipp/regions/LF/AD-1.3.rb +27 -13
  13. data/lib/aipp/regions/LF/AD-1.6.rb +2 -2
  14. data/lib/aipp/regions/LF/AD-2.rb +30 -6
  15. data/lib/aipp/regions/LF/AD-3.1.rb +2 -2
  16. data/lib/aipp/regions/LF/ENR-2.1.rb +1 -1
  17. data/lib/aipp/regions/LF/ENR-4.1.rb +20 -78
  18. data/lib/aipp/regions/LF/ENR-4.3.rb +1 -1
  19. data/lib/aipp/regions/LF/ENR-5.1.rb +44 -26
  20. data/lib/aipp/regions/LF/ENR-5.5.rb +1 -1
  21. data/lib/aipp/regions/LF/helpers/{common.rb → base.rb} +2 -2
  22. data/lib/aipp/regions/LF/helpers/navigational_aid.rb +104 -0
  23. data/lib/aipp/regions/LF/helpers/{AD_radio.rb → radio_AD.rb} +24 -12
  24. data/lib/aipp/version.rb +1 -1
  25. data/lib/core_ext/object.rb +1 -1
  26. data/spec/fixtures/{archive.zip → source.zip} +0 -0
  27. data/spec/lib/aipp/airac_spec.rb +18 -18
  28. data/spec/lib/aipp/border_spec.rb +19 -19
  29. data/spec/lib/aipp/downloader_spec.rb +25 -25
  30. data/spec/lib/aipp/patcher_spec.rb +4 -4
  31. data/spec/lib/aipp/pdf_spec.rb +23 -23
  32. data/spec/lib/aipp/t_hash_spec.rb +6 -6
  33. data/spec/lib/aipp/version_spec.rb +1 -1
  34. data/spec/lib/core_ext/enumberable_spec.rb +15 -15
  35. data/spec/lib/core_ext/hash_spec.rb +4 -4
  36. data/spec/lib/core_ext/integer_spec.rb +2 -2
  37. data/spec/lib/core_ext/nil_class_spec.rb +1 -1
  38. data/spec/lib/core_ext/string_spec.rb +28 -28
  39. data/spec/spec_helper.rb +1 -0
  40. metadata +27 -12
  41. data/.travis.yml +0 -8
@@ -4,7 +4,7 @@ module AIPP
4
4
  # Aerodromes
5
5
  class AD13 < AIP
6
6
 
7
- include AIPP::LF::Helpers::Common
7
+ include AIPP::LF::Helpers::Base
8
8
 
9
9
  DEPENDS = %w(AD-2)
10
10
 
@@ -25,7 +25,13 @@ module AIPP
25
25
  "SAINT CYR LA CAMPAGNE" => 'LF9013',
26
26
  "SEPTFONDS" => 'LF9014',
27
27
  "TALMONT VENDEE AIR PARK" => 'LF9015'
28
- }
28
+ }.freeze
29
+
30
+ PURPOSES = {
31
+ "s" => :scheduled,
32
+ "ns" => :not_scheduled,
33
+ "p" => :private
34
+ }.freeze
29
35
 
30
36
  def parse
31
37
  ad2_exists = false
@@ -76,15 +82,23 @@ module AIPP
76
82
  when /usa.+restr|priv/ then :reservation_required
77
83
  end
78
84
  @airport.add_usage_limitation(limitation) do |l|
79
- l.add_condition do |c|
80
- c.realm = :military if raw_limitation.match?(/milit/)
81
- c.origin = :national if raw_conditions.include?('ntl')
82
- c.origin = :international if raw_conditions.include?('intl')
83
- c.rule = :ifr if raw_conditions.include?('ifr')
84
- c.rule = :vfr if raw_conditions.include?('vfr')
85
- c.purpose = :scheduled if raw_conditions.include?('s')
86
- c.purpose = :not_scheduled if raw_conditions.include?('ns')
87
- c.purpose = :private if raw_conditions.include?('p')
85
+ (%w(s ns p) & raw_conditions).each do |raw_purpose|
86
+ l.add_condition do |c|
87
+ c.realm = raw_limitation.match?(/milit/) ? :military : :civilian
88
+ if (%w(intl ntl) - raw_conditions).empty?
89
+ c.origin = :any
90
+ else
91
+ c.origin = :national if raw_conditions.include?('ntl')
92
+ c.origin = :international if raw_conditions.include?('intl')
93
+ end
94
+ if (%w(ifr vfr) - raw_conditions).empty?
95
+ c.rule = :ifr_and_vfr
96
+ else
97
+ c.rule = :ifr if raw_conditions.include?('ifr')
98
+ c.rule = :vfr if raw_conditions.include?('vfr')
99
+ end
100
+ c.purpose = PURPOSES[raw_purpose]
101
+ end
88
102
  end
89
103
  l.remarks = "Usage restreint (voir VAC) / restricted use (see VAC)" if raw_limitation.match?(/usa.+restr/)
90
104
  l.remarks = "Propriété privée / privately owned" if raw_limitation.match?(/priv/)
@@ -99,7 +113,7 @@ module AIPP
99
113
  @runway = runway # TODO: needed for now for surface composition patches to work
100
114
  runway.length = AIXM.d(tds[1].css('span[id$="VAL_LEN"]').text.to_i, :m)
101
115
  runway.width = AIXM.d(tds[1].css('span[id$="VAL_WID"]').text.to_i, :m)
102
- unless (text = tds[1].css('span[id*="SURFACE"]').text).blank?
116
+ unless (text = tds[1].css('span[id*="SURFACE"]').text.compact).blank?
103
117
  surface = SURFACES.metch(text)
104
118
  runway.surface.composition = surface[:composition]
105
119
  runway.surface.preparation = surface[:preparation]
@@ -109,7 +123,7 @@ module AIPP
109
123
  values = tds[2].text.remove('°').strip.split
110
124
  runway.forth.geographic_orientation = AIXM.a(values.first.to_i)
111
125
  runway.back.geographic_orientation = AIXM.a(values.last.to_i)
112
- parts = tds[3].text.strip.split(/\n\s+\n\s+/)
126
+ parts = tds[3].text.strip.split(/\n\s+\n\s+/, 2)
113
127
  runway.forth.xy = (xy_from(parts[0]) unless parts[0].blank?)
114
128
  runway.back.xy = (xy_from(parts[1]) unless parts[1].blank?)
115
129
  values = tds[4].text.strip.split
@@ -4,8 +4,8 @@ module AIPP
4
4
  # Aerodromes radiocommunication facilities (VFR only)
5
5
  class AD16 < AIP
6
6
 
7
- include AIPP::LF::Helpers::Common
8
- include AIPP::LF::Helpers::ADRadio
7
+ include AIPP::LF::Helpers::Base
8
+ include AIPP::LF::Helpers::RadioAD
9
9
 
10
10
  DEPENDS = %w(AD-1.3)
11
11
 
@@ -4,8 +4,9 @@ module AIPP
4
4
  # Airports (IFR capable) and their CTR, AD navigational aids etc
5
5
  class AD2 < AIP
6
6
 
7
- include AIPP::LF::Helpers::Common
8
- include AIPP::LF::Helpers::ADRadio
7
+ include AIPP::LF::Helpers::Base
8
+ include AIPP::LF::Helpers::NavigationalAid
9
+ include AIPP::LF::Helpers::RadioAD
9
10
  using AIXM::Refinements
10
11
 
11
12
  # Map source types to type and optional local type
@@ -21,7 +22,7 @@ module AIPP
21
22
 
22
23
  # Airports without VFR reporting points
23
24
  # TODO: designated points on map but no list (LFLD LFSN LFBS) or no AD info (LFRL)
24
- NO_DESIGNATED_POINTS = %w(LFAB LFAC LFAV LFAY LFBK LFBN LFBX LFCC LFCI LFCK LFCY LFDH LFDJ LFDN LFEC LFEY LFGA LFHP LFHV LFHY LFJR LFJY LFLA LFLH LFLO LFLV LFLW LFMQ LFMQ LFNB LFOH LFOQ LFOU LFOV LFOZ LFPO LFQA LFQB LFQG LFQM LFRC LFRI LFRM LFRT LFRU LFSD LFSG LFSM LFLD LFSN LFBS LFRL).freeze
25
+ NO_DESIGNATED_POINTS = %w(LFAB LFAC LFAV LFAY LFBK LFBN LFBX LFCC LFCI LFCK LFCY LFDH LFDJ LFDN LFEC LFFK LFEV LFEY LFGA LFHP LFHV LFHY LFJR LFJY LFLA LFLH LFLO LFLV LFLW LFMQ LFMQ LFNB LFOH LFOQ LFOU LFOV LFOZ LFPO LFQA LFQB LFQG LFQM LFRC LFRI LFRM LFRT LFRU LFSD LFSG LFSM LFLD LFSN LFBS LFRL).freeze
25
26
 
26
27
  # Map synonyms for +correlate+
27
28
  SYNONYMS = [
@@ -68,8 +69,8 @@ module AIPP
68
69
  trs = html.css('div[id*="-AD-2\.18"] tbody tr')
69
70
  addresses_from(trs).each { |a| @airport.add_address(a) }
70
71
  units_from(trs).each(&method(:add))
71
- # Landing aids
72
- # TODO: LOC/GP/DME as of section 2.19
72
+ # Navigational aids
73
+ navigational_aids_from(html.css('div[id*="-AD-2\.19"] tbody')).each(&method(:add))
73
74
  # Designated points
74
75
  unless NO_VAC.include?(@id) || NO_DESIGNATED_POINTS.include?(@id)
75
76
  pdf = read("VAC-#{@id}")
@@ -121,7 +122,7 @@ module AIPP
121
122
  length, width = tr.css('td:nth-of-type(3)').text.strip.split('x')
122
123
  runway.length = AIXM.d(length.strip.to_i, :m)
123
124
  runway.width = AIXM.d(width.strip.to_i, :m)
124
- unless (text = tr.css('td:nth-of-type(5)').text.strip.split(%r<\W+/\W+>).first).blank?
125
+ unless (text = tr.css('td:nth-of-type(5)').text.strip.split(%r<\W+/\W+>).first.compact).blank?
125
126
  surface = SURFACES.metch(text)
126
127
  runway.surface.composition = surface[:composition]
127
128
  runway.surface.preparation = surface[:preparation]
@@ -204,6 +205,28 @@ module AIPP
204
205
  end
205
206
  end
206
207
 
208
+ def navigational_aids_from(tbody)
209
+ tbody.css('tr').to_enum.with_object([]) do |tr, array|
210
+ tds = tr.css('td')
211
+ array << navigational_aid_from(
212
+ {
213
+ name: OpenStruct.new(text: @airport.name), # simulate td
214
+ type: tds[0],
215
+ id: tds[1],
216
+ f: tds[2],
217
+ schedule: tds[3],
218
+ xy: tds[4],
219
+ z: tds[5]
220
+ },
221
+ source: source(position: tr.line),
222
+ sections: {
223
+ range: tds[6],
224
+ situation: tds[8]
225
+ }
226
+ )
227
+ end.compact
228
+ end
229
+
207
230
  def designated_points_from(pdf, recursive=false)
208
231
  from = (pdf.text =~ /^(.*?coordinates.*?names?)/i)
209
232
  return [] if recursive && !from
@@ -304,6 +327,7 @@ module AIPP
304
327
  end
305
328
 
306
329
  patch AIXM::Feature::NavigationalAid, :remarks do |parser, object, value|
330
+ throw :abort unless object.is_a? AIXM::Feature::NavigationalAid::DesignatedPoint
307
331
  airport_id, designated_point_id = object.airport.id, object.id
308
332
  parser.fixture.dig('designated_points', airport_id, designated_point_id, 'remarks') || throw(:abort)
309
333
  end
@@ -4,7 +4,7 @@ module AIPP
4
4
  # Helipads
5
5
  class AD31 < AIP
6
6
 
7
- include AIPP::LF::Helpers::Common
7
+ include AIPP::LF::Helpers::Base
8
8
  using AIXM::Refinements
9
9
 
10
10
  DEPENDS = %w(AD-2)
@@ -52,7 +52,7 @@ module AIPP
52
52
  end
53
53
  end
54
54
  # FATOs and helipads
55
- text = trs[2].css('span[id*="ADHP.REVETEMENT"]').text.remove(/tlof\s*|\s*\(.*?\)/i).downcase
55
+ text = trs[2].css('span[id*="ADHP.REVETEMENT"]').text.remove(/tlof\s*|\s*\(.*?\)/i).downcase.compact
56
56
  surface = text.blank? ? {} : SURFACES.metch(text)
57
57
  lighting = lighting_from(trs[1].css('span[id*="ADHP.BALISAGE"]').text.cleanup)
58
58
  fatos_from(trs[1].css('span[id*="ADHP.DIM_FATO"]').text).each { |f| @airport.add_fato f }
@@ -4,7 +4,7 @@ module AIPP
4
4
  # FIR, TMA etc
5
5
  class ENR21 < AIP
6
6
 
7
- include AIPP::LF::Helpers::Common
7
+ include AIPP::LF::Helpers::Base
8
8
 
9
9
  # Map source types to type and optional local type
10
10
  SOURCE_TYPES = {
@@ -4,94 +4,36 @@ module AIPP
4
4
  # ENR Navaids
5
5
  class ENR41 < AIP
6
6
 
7
- include AIPP::LF::Helpers::Common
7
+ include AIPP::LF::Helpers::Base
8
+ include AIPP::LF::Helpers::NavigationalAid
8
9
 
9
10
  def parse
10
11
  prepare(html: read).css('tbody').each do |tbody|
11
12
  tbody.css('tr').to_enum.with_index(1).each do |tr, index|
12
13
  tds = tr.css('td')
13
- master, slave = tds[1].text.strip.gsub(/[^\w-]/, '').downcase.split('-')
14
- navaid = AIXM.send(master, base_from(tds).merge(send("#{master}_from", tds)))
15
- navaid.source = source(position: tr.line)
16
- navaid.timetable = timetable_from! tds[4].text
17
- navaid.remarks = remarks_from(tds[5], tds[7], tds[9])
18
- navaid.send("associate_#{slave}", channel: channel_from(tds[3].text)) if slave
19
- add navaid
14
+ navigational_aid = navigational_aid_from(
15
+ {
16
+ name: tds[0],
17
+ type: tds[1],
18
+ id: tds[2],
19
+ f: tds[3],
20
+ schedule: tds[4],
21
+ xy: tds[5],
22
+ z: tds[6]
23
+ },
24
+ source: source(position: tr.line),
25
+ sections: {
26
+ range: tds[5].css('span[id*="PORTEE"], span[id*="COUVERTURE"]'),
27
+ situation: tds[7],
28
+ observations: tds[9]
29
+ }
30
+ )
31
+ add navigational_aid if navigational_aid
20
32
  rescue => error
21
33
  warn("error parsing navigational aid at ##{index}: #{error.message}", pry: error)
22
34
  end
23
35
  end
24
36
  end
25
-
26
- private
27
-
28
- def base_from(tds)
29
- {
30
- organisation: organisation_lf,
31
- id: tds[2].text.strip,
32
- name: tds[0].text.strip,
33
- xy: xy_from(tds[5].text),
34
- z: z_from(tds[6].text)
35
- }
36
- end
37
-
38
- def vor_from(tds)
39
- {
40
- type: :conventional,
41
- f: f_from(tds[3].text),
42
- north: :magnetic,
43
- }
44
- end
45
-
46
- def dme_from(tds)
47
- {
48
- channel: channel_from(tds[3].text)
49
- }
50
- end
51
-
52
- def ndb_from(tds)
53
- {
54
- type: :en_route,
55
- f: f_from(tds[3].text)
56
- }
57
- end
58
-
59
- def tacan_from(tds)
60
- {
61
- channel: channel_from(tds[3].text)
62
- }
63
- end
64
-
65
- def z_from(text)
66
- parts = text.strip.split(/\s+/)
67
- AIXM.z(parts[0].to_i, :qnh) if parts[1] == 'ft'
68
- end
69
-
70
- def f_from(text)
71
- parts = text.strip.split(/\s+/)
72
- AIXM.f(parts[0].to_f, parts[1]) if parts[1] =~ /hz$/i
73
- end
74
-
75
- def channel_from(text)
76
- parts = text.strip.split(/\s+/)
77
- parts.last if parts[-2].downcase == 'ch'
78
- end
79
-
80
- def remarks_from(*parts)
81
- part_titles = ['RANGE', 'SITUATION', 'OBSERVATIONS']
82
- [].tap do |remarks|
83
- parts.each.with_index do |part, index|
84
- text = if index == 0
85
- part = part.text.strip.split(/\s+/)
86
- part.shift(2)
87
- part.join(' ').blank_to_nil
88
- else
89
- part.text.strip.blank_to_nil
90
- end
91
- remarks << "**#{part_titles[index]}**\n#{text}" if text
92
- end
93
- end.join("\n\n").blank_to_nil
94
- end
95
37
  end
96
38
  end
97
39
  end
@@ -4,7 +4,7 @@ module AIPP
4
4
  # Designated Points
5
5
  class ENR43 < AIP
6
6
 
7
- include AIPP::LF::Helpers::Common
7
+ include AIPP::LF::Helpers::Base
8
8
 
9
9
  def parse
10
10
  prepare(html: read).css('tbody').each do |tbody|
@@ -4,7 +4,17 @@ module AIPP
4
4
  # D/P/R Zones
5
5
  class ENR51 < AIP
6
6
 
7
- include AIPP::LF::Helpers::Common
7
+ include AIPP::LF::Helpers::Base
8
+
9
+ # Map sections to whether to parse them
10
+ SECTIONS = {
11
+ '5.1-1': true,
12
+ '5.1-2': false,
13
+ '5.1-3': true,
14
+ '5.1-4': true,
15
+ '5.1-5-1': false,
16
+ '5.1-5-2': true
17
+ }
8
18
 
9
19
  # Map source types to type and optional local type
10
20
  SOURCE_TYPES = {
@@ -15,28 +25,37 @@ module AIPP
15
25
  }.freeze
16
26
 
17
27
  def parse
18
- prepare(html: read).css('tbody:has(tr[id^=mid])').each do |tbody|
19
- airspace = nil
20
- tbody.css('tr').to_enum.with_index(1).each do |tr, index|
21
- tds = tr.css('td')
22
- case
23
- when tr.attr(:id).match?(/TXT_NAME/) # airspace
24
- airspace = airspace_from tr
25
- when tds.count == 1 # big comment on separate row
26
- airspace.layers.first.remarks.
27
- concat("\n", tds.text.cleanup).
28
- remove!(/\((\d)\)\s*\(\1\)\W*/)
29
- else # layer
30
- begin
31
- tds = tr.css('td')
32
- airspace.geometry = geometry_from tds[0].text
33
- fail("geometry is not closed") unless airspace.geometry.closed?
34
- airspace.layers << layer_from(tds[1].text)
35
- airspace.layers.first.timetable = timetable_from! tds[2].text
36
- airspace.layers.first.remarks = remarks_from(tds[2], tds[3], tds[4])
37
- add airspace
38
- rescue => error
39
- warn("error parsing airspace `#{airspace.name}' at ##{index}: #{error.message}", pry: error)
28
+ skip = false
29
+ prepare(html: read).css('h4, thead ~ tbody').each do |tag|
30
+ case tag.name
31
+ when 'h4'
32
+ section = tag.text.match(/^ENR ([\d.-]+)/).captures.first
33
+ skip = !SECTIONS.fetch(section.to_sym)
34
+ verbose_info "#{skip ? :Skipping : :Parsing} section #{section}"
35
+ when 'tbody'
36
+ next if skip
37
+ airspace = nil
38
+ tag.css('tr').to_enum.with_index(1).each do |tr, index|
39
+ tds = tr.css('td')
40
+ case
41
+ when tr.attr(:id).match?(/TXT_NAME/) # airspace
42
+ airspace = airspace_from tr
43
+ when tds.count == 1 # big comment on separate row
44
+ airspace.layers.first.remarks.
45
+ concat("\n", tds.text.cleanup).
46
+ remove!(/\((\d)\)\s*\(\1\)\W*/)
47
+ else # layer
48
+ begin
49
+ tds = tr.css('td')
50
+ airspace.geometry = geometry_from tds[0].text
51
+ fail("geometry is not closed") unless airspace.geometry.closed?
52
+ airspace.layers << layer_from(tds[1].text)
53
+ airspace.layers.first.timetable = timetable_from! tds[2].text
54
+ airspace.layers.first.remarks = remarks_from(tds[2], tds[3], tds[4])
55
+ add airspace
56
+ rescue => error
57
+ warn("error parsing airspace `#{airspace.name}' at ##{index}: #{error.message}", pry: error)
58
+ end
40
59
  end
41
60
  end
42
61
  end
@@ -46,11 +65,10 @@ module AIPP
46
65
  private
47
66
 
48
67
  def airspace_from(tr)
49
- spans = tr.css('span:not([class*=strong])')
50
- source_type = spans[1].text.blank_to_nil
68
+ region, source_type, name = tr.text.cleanup.gsub(/\s/, ' ').split(nil, 3)
51
69
  fail "unknown type `#{source_type}'" unless SOURCE_TYPES.has_key? source_type
52
70
  AIXM.airspace(
53
- name: spans.map { |s| s.text.strip.blank_to_nil }.compact.join(' '),
71
+ name: [region, source_type, name].join(' '),
54
72
  type: SOURCE_TYPES.dig(source_type, :type),
55
73
  local_type: SOURCE_TYPES.dig(source_type, :local_type)
56
74
  ).tap do |airspace|
@@ -4,7 +4,7 @@ module AIPP
4
4
  # Sporting and Recreational Activities
5
5
  class ENR55 < AIP
6
6
 
7
- include AIPP::LF::Helpers::Common
7
+ include AIPP::LF::Helpers::Base
8
8
 
9
9
  # Map raw activities to activity and airspace type
10
10
  ACTIVITIES = {
@@ -1,7 +1,7 @@
1
1
  module AIPP
2
2
  module LF
3
3
  module Helpers
4
- module Common
4
+ module Base
5
5
 
6
6
  using AIXM::Refinements
7
7
 
@@ -39,7 +39,7 @@ module AIPP
39
39
  'macadam' => { composition: :macadam },
40
40
  /^bitume ?(traité|psp)?$/ => { composition: :bitumen },
41
41
  'ciment' => { composition: :concrete, preparation: :paved },
42
- /^b[]ton ?(armé|bitume|bitumineux)?$/ => { composition: :concrete, preparation: :paved },
42
+ /^b[eéè]ton ?(armé|bitume|bitumineux)?$/ => { composition: :concrete, preparation: :paved },
43
43
  /^béton( de)? ciment$/ => { composition: :concrete, preparation: :paved },
44
44
  'béton herbe' => { composition: :concrete_and_grass },
45
45
  'béton avec résine' => { composition: :concrete, preparation: :paved, remarks: 'Avec résine / with resin' },
@@ -0,0 +1,104 @@
1
+ module AIPP
2
+ module LF
3
+ module Helpers
4
+ module NavigationalAid
5
+
6
+ def navigational_aid_from(tds, source:, sections:)
7
+ NavigationalAid.new(tds, source: source, sections: sections).build
8
+ end
9
+
10
+ class NavigationalAid
11
+ include AIPP::LF::Helpers::Base
12
+
13
+ # Map atypical navigational aid denominations
14
+ NAVIGATIONAL_AIDS = {
15
+ 'vor' => 'vor',
16
+ 'dme' => 'dme',
17
+ 'ndb' => 'ndb',
18
+ 'tacan' => 'tacan',
19
+ 'l' => 'ndb' # L denominates a NDB of class locator
20
+ }.freeze
21
+
22
+ def initialize(tds, source:, sections:)
23
+ @tds, @source, @sections = tds, source, sections
24
+ end
25
+
26
+ def build
27
+ master, slave = @tds[:type].text.strip.gsub(/[^\w-]/, '').downcase.split('-')
28
+ master = NAVIGATIONAL_AIDS.fetch(master, master)
29
+ slave = NAVIGATIONAL_AIDS.fetch(slave, slave)
30
+ return nil unless NAVIGATIONAL_AIDS.keys.include? master
31
+ AIXM.send(master, common.merge(send(master))).tap do |navigational_aid|
32
+ navigational_aid.source = @source
33
+ navigational_aid.remarks = remarks
34
+ navigational_aid.timetable = timetable_from!(@tds[:schedule].text)
35
+ navigational_aid.send("associate_#{slave}", channel: channel_from(@tds[:f].text)) if slave
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def common
42
+ {
43
+ organisation: organisation_lf,
44
+ id: @tds[:id].text.strip,
45
+ name: @tds[:name].text.strip,
46
+ xy: xy_from(@tds[:xy].text),
47
+ z: z_from(@tds[:z].text)
48
+ }
49
+ end
50
+
51
+ def vor
52
+ {
53
+ type: :conventional,
54
+ f: f_from(@tds[:f].text),
55
+ north: :magnetic,
56
+ }
57
+ end
58
+
59
+ def dme
60
+ {
61
+ channel: channel_from(@tds[:f].text)
62
+ }
63
+ end
64
+
65
+ def ndb
66
+ {
67
+ type: @tds[:type].text.strip == 'L' ? :locator : :en_route,
68
+ f: f_from(@tds[:f].text)
69
+ }
70
+ end
71
+
72
+ def tacan
73
+ {
74
+ channel: channel_from(@tds[:f].text)
75
+ }
76
+ end
77
+
78
+ def remarks
79
+ @sections.map do |section, td|
80
+ if text = td.text.strip.blank_to_nil
81
+ "**#{section.upcase}**\n#{text}"
82
+ end
83
+ end.compact.join("\n\n").blank_to_nil
84
+ end
85
+
86
+ def z_from(text)
87
+ parts = text.strip.split(/\s+/)
88
+ AIXM.z(parts[0].to_i, :qnh) if parts[1] == 'ft'
89
+ end
90
+
91
+ def f_from(text)
92
+ parts = text.strip.split(/\s+/)
93
+ AIXM.f(parts[0].to_f, parts[1]) if parts[1] =~ /hz$/i
94
+ end
95
+
96
+ def channel_from(text)
97
+ parts = text.strip.split(/\s+/)
98
+ parts.last if parts[-2].downcase == 'ch'
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end