aipp 0.2.5 → 0.2.6

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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/CHANGELOG.md +8 -0
  5. data/README.md +40 -14
  6. data/exe/aip2aixm +1 -1
  7. data/exe/aip2ofmx +1 -1
  8. data/lib/aipp.rb +2 -0
  9. data/lib/aipp/aip.rb +17 -12
  10. data/lib/aipp/downloader.rb +1 -1
  11. data/lib/aipp/executable.rb +15 -12
  12. data/lib/aipp/parser.rb +58 -43
  13. data/lib/aipp/pdf.rb +1 -1
  14. data/lib/aipp/regions/LF/AD-1.3.rb +7 -6
  15. data/lib/aipp/regions/LF/AD-1.6.rb +7 -5
  16. data/lib/aipp/regions/LF/AD-2.rb +16 -9
  17. data/lib/aipp/regions/LF/AD-3.1.rb +6 -6
  18. data/lib/aipp/regions/LF/ENR-2.1.rb +81 -6
  19. data/lib/aipp/regions/LF/ENR-4.1.rb +3 -1
  20. data/lib/aipp/regions/LF/ENR-4.3.rb +2 -3
  21. data/lib/aipp/regions/LF/ENR-5.1.rb +15 -2
  22. data/lib/aipp/regions/LF/ENR-5.4.rb +90 -0
  23. data/lib/aipp/regions/LF/ENR-5.5.rb +12 -10
  24. data/lib/aipp/regions/LF/fixtures/AD-1.3.yml +2 -2
  25. data/lib/aipp/regions/LF/helpers/base.rb +17 -9
  26. data/lib/aipp/regions/LF/helpers/radio_AD.rb +21 -13
  27. data/lib/aipp/t_hash.rb +3 -3
  28. data/lib/aipp/version.rb +1 -1
  29. data/lib/core_ext/enumerable.rb +7 -7
  30. data/lib/core_ext/string.rb +9 -4
  31. metadata +156 -168
  32. metadata.gz.sig +1 -0
  33. data/.github/workflows/test.yml +0 -26
  34. data/.gitignore +0 -8
  35. data/.ruby-version +0 -1
  36. data/.yardopts +0 -3
  37. data/Guardfile +0 -7
  38. data/TODO.md +0 -6
  39. data/aipp.gemspec +0 -45
  40. data/gems.rb +0 -3
  41. data/rakefile.rb +0 -12
  42. data/spec/fixtures/border.geojson +0 -201
  43. data/spec/fixtures/document.pdf +0 -0
  44. data/spec/fixtures/document.pdf.json +0 -1
  45. data/spec/fixtures/new.html +0 -6
  46. data/spec/fixtures/new.pdf +0 -0
  47. data/spec/fixtures/new.txt +0 -1
  48. data/spec/fixtures/source.zip +0 -0
  49. data/spec/lib/aipp/airac_spec.rb +0 -98
  50. data/spec/lib/aipp/border_spec.rb +0 -135
  51. data/spec/lib/aipp/downloader_spec.rb +0 -81
  52. data/spec/lib/aipp/patcher_spec.rb +0 -46
  53. data/spec/lib/aipp/pdf_spec.rb +0 -124
  54. data/spec/lib/aipp/t_hash_spec.rb +0 -44
  55. data/spec/lib/aipp/version_spec.rb +0 -7
  56. data/spec/lib/core_ext/enumberable_spec.rb +0 -76
  57. data/spec/lib/core_ext/hash_spec.rb +0 -27
  58. data/spec/lib/core_ext/integer_spec.rb +0 -15
  59. data/spec/lib/core_ext/nil_class_spec.rb +0 -11
  60. data/spec/lib/core_ext/string_spec.rb +0 -112
  61. data/spec/sounds/failure.mp3 +0 -0
  62. data/spec/sounds/success.mp3 +0 -0
  63. data/spec/spec_helper.rb +0 -29
@@ -15,13 +15,15 @@ module AIPP
15
15
  }
16
16
 
17
17
  def parse
18
- prepare(html: read).css('tbody').first do |tbody|
19
- tbody.css('tr').group_by_chunks { |e| e.attr(:id).match?(/-TXT_NAME-/) }.each do |tr, trs|
18
+ document = prepare(html: read)
19
+ document.css('tbody').each do |tbody|
20
+ tbody.css('tr').group_by_chunks { _1.attr(:id).match?(/-TXT_NAME-/) }.each do |tr, trs|
21
+ trs = Nokogiri::XML::NodeSet.new(document, trs) # convert array to node set
20
22
  id = tr.css('span[id*="CODE_ICAO"]').text.cleanup
21
23
  next unless id = ID_FIXES.fetch(id, id)
22
- @airport = select(:airport, id: id).first
23
- addresses_from(trs).each { |a| @airport.add_address(a) }
24
- units_from(trs).each(&method(:add))
24
+ @airport = find_by(:airport, id: id).first
25
+ addresses_from(trs).each { @airport.add_address(_1) }
26
+ units_from(trs, airport: @airport).each(&method(:add))
25
27
  end
26
28
  end
27
29
  end
@@ -50,7 +50,7 @@ module AIPP
50
50
  source: source(position: html.css('tr[id*="CODE_ICAO"]').first.line, aip_file: aip_file),
51
51
  organisation: organisation_lf, # TODO: not yet implemented
52
52
  id: @id,
53
- name: html.css('tr[id*="CODE_ICAO"] td span:nth-of-type(2)').text.uptrans,
53
+ name: html.css('tr[id*="CODE_ICAO"] td span:nth-of-type(2)').text.strip.uptrans,
54
54
  xy: xy_from(html.css('#AD-2\.2-Position_Geo_Arp td:nth-of-type(3)').text)
55
55
  ).tap do |airport|
56
56
  airport.z = elevation_from(html.css('#AD-2\.2-Altitude_Reference td:nth-of-type(3)').text)
@@ -58,26 +58,33 @@ module AIPP
58
58
  # airport.transition_z = AIXM.z(5000, :qnh) # TODO: default - exceptions may exist
59
59
  airport.timetable = timetable_from!(html.css('#AD-2\.3-Gestionnaire_AD td:nth-of-type(3)').text)
60
60
  end
61
- runways_from(html.css('div[id*="-AD-2\.12"] tbody')).each { |r| @airport.add_runway(r) if r }
62
- helipads_from(html.css('div[id*="-AD-2\.16"] tbody')).each { |h| @airport.add_helipad(h) if h }
61
+ runways_from(html.css('div[id*="-AD-2\.12"] tbody')).each { @airport.add_runway(_1) if _1 }
62
+ helipads_from(html.css('div[id*="-AD-2\.16"] tbody')).each { @airport.add_helipad(_1) if _1 }
63
63
  text = html.css('#AD-2\.2-Observations td:nth-of-type(3)').text
64
64
  @airport.remarks = ([remarks_from(text)] + @remarks).compact.join("\n\n").blank_to_nil
65
65
  add @airport
66
66
  # Airspaces
67
- airspaces_from(html.css('div[id*="-AD-2\.17"] tbody')).each(&method(:add))
67
+ airspaces_from(html.css('div[id*="-AD-2\.17"] tbody')).
68
+ reject { aixm.features.find_by(_1.class, type: _1.type, id: _1.id).any? }.
69
+ each(&method(:add))
68
70
  # Radio
69
71
  trs = html.css('div[id*="-AD-2\.18"] tbody tr')
70
- addresses_from(trs).each { |a| @airport.add_address(a) }
71
- units_from(trs).each(&method(:add))
72
+ addresses_from(trs).each { @airport.add_address(_1) }
73
+ units_from(trs, airport: @airport).each(&method(:add))
72
74
  # Navigational aids
73
- navigational_aids_from(html.css('div[id*="-AD-2\.19"] tbody')).each(&method(:add))
75
+ navigational_aids_from(html.css('div[id*="-AD-2\.19"] tbody')).
76
+ reject { aixm.features.find_by(_1.class, id: _1.id, xy: _1.xy).any? }.
77
+ each(&method(:add))
74
78
  # Designated points
75
79
  unless NO_VAC.include?(@id) || NO_DESIGNATED_POINTS.include?(@id)
76
80
  pdf = read("VAC-#{@id}")
77
81
  designated_points_from(pdf).tap do |designated_points|
78
82
  fix_designated_point_remarks(designated_points)
79
83
  # debug(designated_points)
80
- designated_points.each(&method(:add))
84
+ designated_points.
85
+ uniq(&:to_uid).
86
+ reject { aixm.features.find_by(_1.class, id: _1.id, xy: _1.xy).any? }.
87
+ each(&method(:add))
81
88
  end
82
89
  end
83
90
  rescue => error
@@ -184,7 +191,7 @@ module AIPP
184
191
  tds = tr.css('td')
185
192
  airspace.geometry = geometry_from tds[0].text
186
193
  fail("geometry is not closed") unless airspace.geometry.closed?
187
- airspace.layers << layer_from(tds[2].text, tds[1].text.strip)
194
+ airspace.add_layer layer_from(tds[2].text, tds[1].text.strip)
188
195
  airspace.layers.first.timetable = timetable_from! tds[4].text
189
196
  airspace.layers.first.remarks = remarks_from(tds[4].text)
190
197
  array << airspace
@@ -26,7 +26,7 @@ module AIPP
26
26
  prepare(html: read).css('tbody').each do |tbody|
27
27
  tbody.css('tr').to_enum.each_slice(3).with_index(1) do |trs, index|
28
28
  name = trs[0].css('span[id*="ADHP.TXT_NAME"]').text.cleanup.remove(/[^\w' ]/)
29
- if select(:airport, name: name).any?
29
+ if find_by(:airport, name: name).any?
30
30
  verbose_info "Skipping #{name} in favor of AD-2"
31
31
  next
32
32
  end
@@ -42,12 +42,12 @@ module AIPP
42
42
  end
43
43
  # Usage restrictions
44
44
  if trs[0].css('span[id*="ADHP.STATUT"]').text.match?(/usage\s+restreint/i)
45
- @airport.add_usage_limitation(:reservation_required) do |reservation_required|
45
+ @airport.add_usage_limitation(type: :reservation_required) do |reservation_required|
46
46
  reservation_required.remarks = "Usage restreint / restricted use"
47
47
  end
48
48
  end
49
49
  if trs[0].css('span[id*="ADHP.STATUT"]').text.match?(/r.serv.\s+aux\s+administrations/i)
50
- @airport.add_usage_limitation(:other) do |other|
50
+ @airport.add_usage_limitation(type: :other) do |other|
51
51
  other.remarks = "Réservé aux administrations de l'État / reserved for State administrations"
52
52
  end
53
53
  end
@@ -55,7 +55,7 @@ module AIPP
55
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
- fatos_from(trs[1].css('span[id*="ADHP.DIM_FATO"]').text).each { |f| @airport.add_fato f }
58
+ fatos_from(trs[1].css('span[id*="ADHP.DIM_FATO"]').text).each { @airport.add_fato(_1) }
59
59
  helipads_from(trs[1].css('span[id*="ADHP.DIM_TLOF"]').text).each do |helipad|
60
60
  helipad.surface.composition = surface[:composition]
61
61
  helipad.surface.preparation = surface[:preparation]
@@ -69,7 +69,7 @@ module AIPP
69
69
  splitted = operator.text.split(/( (?<!\p{L})t[ée]l | fax | standard | [\d\s]{10,} | \.\s | \( )/ix, 2)
70
70
  @airport.operator = splitted[0].full_strip.truncate(60, omission: '…').blank_to_nil
71
71
  raw_addresses = splitted[1..].join.cleanup.full_strip
72
- addresses_from(splitted[1..].join, source(position: operator.first.line)).each { |a| @airport.add_address(a) }
72
+ addresses_from(splitted[1..].join, source(position: operator.first.line)).each { @airport.add_address(_1) }
73
73
  # Remarks
74
74
  @airport.remarks = [].tap do |remarks|
75
75
  hostility = trs[2].css('span[id*="ADHP.ZONE_HABITEE"]').text.cleanup.downcase.blank_to_nil
@@ -109,7 +109,7 @@ module AIPP
109
109
  end
110
110
 
111
111
  def dimensions_from(text)
112
- dims = text.remove(/[^x\d.,]/i).split(/x/i).map { |s| s.to_ff.floor }
112
+ dims = text.remove(/[^x\d.,]/i).split(/x/i).map { _1.to_ff.floor }
113
113
  case dims.size
114
114
  when 1
115
115
  [dim = AIXM.d(dims[0], :m), dim]
@@ -6,6 +6,9 @@ module AIPP
6
6
 
7
7
  include AIPP::LF::Helpers::Base
8
8
 
9
+ # Airspaces to be ignored
10
+ NAME_BLACKLIST_RE = /deleg/i.freeze
11
+
9
12
  # Map source types to type and optional local type
10
13
  SOURCE_TYPES = {
11
14
  'FIR' => { type: 'FIR' },
@@ -26,12 +29,24 @@ module AIPP
26
29
  'FIR REIMS' => 'LFRR'
27
30
  }.freeze
28
31
 
32
+ # Fix incomplete SIV service columns
33
+ SERVICE_FIXES = {
34
+ "IROISE INFO 135.825 / 119.575 (1)" => "APP IROISE\nIROISE INFO 135.825 / 119.575 (1)",
35
+ "APP TOULOUSE\nTOULOUSE INFO" => "APP TOULOUSE\nTOULOUSE INFO 121.250"
36
+ }.freeze
37
+
29
38
  def parse
30
39
  prepare(html: read).css('tbody').each do |tbody|
31
40
  airspace = nil
32
41
  tbody.css('tr').to_enum.with_index(1).each do |tr, index|
33
42
  if tr.attr(:id).match?(/--TXT_NAME/)
34
- add airspace if airspace
43
+ if airspace
44
+ if airspace.name.match? NAME_BLACKLIST_RE
45
+ verbose_info "Ignoring #{airspace.type} #{airspace.name}" unless airspace.type == :terminal_control_area
46
+ else
47
+ add airspace
48
+ end
49
+ end
35
50
  airspace = airspace_from tr.css(:td).first
36
51
  verbose_info "Parsing #{airspace.type} #{airspace.name}" unless airspace.type == :terminal_control_area
37
52
  next
@@ -39,11 +54,18 @@ module AIPP
39
54
  begin
40
55
  tds = tr.css('td')
41
56
  if airspace.type == :terminal_control_area && tds[0].text.blank_to_nil
42
- add airspace if airspace.layers.any?
57
+ if airspace.layers.any?
58
+ if airspace.name.match? NAME_BLACKLIST_RE
59
+ verbose_info "Ignoring #{airspace.type} #{airspace.name}"
60
+ else
61
+ add airspace
62
+ end
63
+ end
43
64
  airspace = airspace_from tds[0]
44
65
  verbose_info "Parsing #{airspace.type} #{airspace.name}"
45
66
  end
46
67
  if airspace
68
+ remarks = tds[-1].text
47
69
  if tds[0].text.blank_to_nil
48
70
  airspace.geometry = geometry_from tds[0].text
49
71
  fail("geometry is not closed") unless airspace.geometry.closed?
@@ -51,11 +73,12 @@ module AIPP
51
73
  layer = layer_from(tds[-3].text)
52
74
  layer.class = class_from(tds[1].text) if tds.count == 5
53
75
  layer.location_indicator = LOCATION_INDICATORS.fetch("#{airspace.type} #{airspace.name}", nil)
54
- # TODO: unit, call sign and frequency from tds[-2] and with extracted remarks such as (1), (2) etc
55
- remarks = tds[-1].text
76
+ if airspace.local_type == 'SIV' # services parsed for SIV only
77
+ layer.add_services services_from(tds[-2], remarks)
78
+ end
56
79
  layer.timetable = timetable_from! remarks
57
80
  layer.remarks = remarks_from remarks
58
- airspace.layers << layer
81
+ airspace.add_layer layer
59
82
  end
60
83
  rescue => error
61
84
  warn("error parsing #{airspace.type} `#{airspace.name}' at ##{index}: #{error.message}", pry: error)
@@ -68,7 +91,7 @@ module AIPP
68
91
  private
69
92
 
70
93
  def airspace_from(td)
71
- spans = td.children.split { |e| e.name == 'br' }.first.css(:span).drop_while { |e| e.text.match? '\s' }
94
+ spans = td.children.split { _1.name == 'br' }.first.css(:span).drop_while { _1.text.match? '\s' }
72
95
  source_type = spans[0].text.blank_to_nil
73
96
  fail "unknown type `#{source_type}'" unless SOURCE_TYPES.has_key? source_type
74
97
  AIXM.airspace(
@@ -84,6 +107,58 @@ module AIPP
84
107
  text.strip
85
108
  end
86
109
 
110
+ def services_from(td, remarks)
111
+ text = td.text.cleanup
112
+ text = SERVICE_FIXES.fetch(text, text) # fix incomplete service columns
113
+ text.gsub!(/(info|app)\s+([\d.]{3,})/i, "\\1\n\\2") # put frequencies on separate line
114
+ text.gsub!(/(\d)\s*\/\s*(\d)/, "\\1\n\\2") # split frequencies onto separate lines
115
+ units, services = [], []
116
+ text.split("\n").each do |line|
117
+ case line
118
+ when /^(.+(?:info|app))$/i # service
119
+ callsign = $1
120
+ service = AIXM.service(
121
+ # TODO: add source as soon as it is supported by components
122
+ # source: source(position: td.line),
123
+ type: :flight_information_service
124
+ ).tap do |service|
125
+ service.timetable = AIXM::H24 if remarks.match? /h\s?24/i
126
+ end
127
+ services << [service, callsign]
128
+ units.shift.add_service service
129
+ when /^(.*?)(\d{3}[.\d]*)(.*)$/ # frequency
130
+ label, freq, footnote = $1, $2, $3
131
+ service, callsign = services.last
132
+ frequency = AIXM.frequency(
133
+ transmission_f: AIXM.f(freq.to_f, :mhz),
134
+ callsigns: { en: callsign, fr: callsign }
135
+ ).tap do |frequency|
136
+ frequency.type = :standard
137
+ frequency.timetable = AIXM::H24 if remarks.match? /h\s?24/i
138
+ frequency.remarks = [
139
+ (remarks.extract(/#{Regexp.escape(footnote.strip)}\s*([^\n]+)/).join(' / ') unless footnote.empty?),
140
+ label.strip
141
+ ].map(&:blank_to_nil).compact.join(' / ').blank_to_nil
142
+ end
143
+ service.add_frequency frequency
144
+ when /.*(?<!info|app|\d{3}|\))$/i # unit
145
+ unit = AIXM.unit(
146
+ source: source(position: td.line),
147
+ organisation: organisation_lf, # TODO: not yet implemented
148
+ name: line,
149
+ type: :flight_information_centre,
150
+ class: :icao
151
+ )
152
+ units << ((u = find(unit).first) ? (unit = u) : (add unit))
153
+ else
154
+ fail("cannot parse `#{text}'")
155
+ end
156
+ end
157
+ services = services.map(&:first)
158
+ fail("at least one service has no frequency") if services.any? { _1.frequencies.none? }
159
+ services
160
+ end
161
+
87
162
  def remarks_from(text)
88
163
  text.strip.gsub(/(\s)\s+/, '\1').blank_to_nil
89
164
  end
@@ -28,7 +28,9 @@ module AIPP
28
28
  observations: tds[9]
29
29
  }
30
30
  )
31
- add navigational_aid if navigational_aid
31
+ if navigational_aid && aixm.features.find_by(navigational_aid.class, id: navigational_aid.id, xy: navigational_aid.xy).none?
32
+ add navigational_aid
33
+ end
32
34
  rescue => error
33
35
  warn("error parsing navigational aid at ##{index}: #{error.message}", pry: error)
34
36
  end
@@ -10,13 +10,12 @@ module AIPP
10
10
  prepare(html: read).css('tbody').each do |tbody|
11
11
  tbody.css('tr').to_enum.with_index(1).each do |tr, index|
12
12
  tds = tr.css('td')
13
- designated_point = AIXM.designated_point(
13
+ add AIXM.designated_point(
14
+ source: source(position: tr.line),
14
15
  type: :icao,
15
16
  id: tds[0].text.strip,
16
17
  xy: xy_from(tds[1].text)
17
18
  )
18
- designated_point.source = source(position: tr.line)
19
- add designated_point
20
19
  rescue => error
21
20
  warn("error parsing designated point at ##{index}: #{error.message}", pry: error)
22
21
  end
@@ -24,6 +24,9 @@ module AIPP
24
24
  'ZIT' => { type: 'P', local_type: 'ZIT' }
25
25
  }.freeze
26
26
 
27
+ # Radius to use for zones consisting of one point only
28
+ POINT_RADIUS = AIXM.d(1, :km).freeze
29
+
27
30
  def parse
28
31
  skip = false
29
32
  prepare(html: read).css('h4, thead ~ tbody').each do |tag|
@@ -48,11 +51,21 @@ module AIPP
48
51
  begin
49
52
  tds = tr.css('td')
50
53
  airspace.geometry = geometry_from tds[0].text
54
+ if airspace.geometry.point? # convert point to circle
55
+ airspace.geometry = AIXM.geometry(
56
+ AIXM.circle(
57
+ center_xy: airspace.geometry.segments.first.xy,
58
+ radius: POINT_RADIUS
59
+ )
60
+ )
61
+ end
51
62
  fail("geometry is not closed") unless airspace.geometry.closed?
52
- airspace.layers << layer_from(tds[1].text)
63
+ airspace.add_layer layer_from(tds[1].text)
53
64
  airspace.layers.first.timetable = timetable_from! tds[2].text
54
65
  airspace.layers.first.remarks = remarks_from(tds[2], tds[3], tds[4])
55
- add airspace
66
+ if aixm.features.find_by(:airspace, type: airspace.type, id: airspace.id).none?
67
+ add airspace
68
+ end
56
69
  rescue => error
57
70
  warn("error parsing airspace `#{airspace.name}' at ##{index}: #{error.message}", pry: error)
58
71
  end
@@ -0,0 +1,90 @@
1
+ module AIPP
2
+ module LF
3
+
4
+ # Obstacles
5
+ class ENR54 < AIP
6
+
7
+ include AIPP::LF::Helpers::Base
8
+
9
+ # Obstacles to be ignored
10
+ NAME_BLACKLIST = %w(51076 52055 59000 72039).freeze # all duplicates
11
+
12
+ # Map type descriptions to AIXM types and remarks
13
+ TYPES = {
14
+ 'Antenne' => [:antenna],
15
+ 'Bâtiment' => [:building],
16
+ 'Câble' => [:other, 'Cable / Câble'],
17
+ 'Centrale thermique' => [:building, 'Thermal power plant / Centrale thermique'],
18
+ "Château d'eau" => [:tower, "Water tower / Château d'eau"],
19
+ 'Cheminée' => [:chimney],
20
+ 'Derrick' => [:tower, 'Derrick'],
21
+ 'Eglise' => [:tower, 'Church / Eglise'],
22
+ 'Eolienne(s)' => [:wind_turbine],
23
+ 'Grue' => [:tower, 'Crane / Grue'],
24
+ 'Mât' => [:mast],
25
+ 'Phare marin' => [:tower, 'Lighthouse / Phare marin'],
26
+ 'Pile de pont' => [:other, 'Bridge piers / Pile de pont'],
27
+ 'Portique' => [:building, 'Arch / Portique'],
28
+ 'Pylône' => [:mast, 'Pylon / Pylône'],
29
+ 'Silo' => [:tower, 'Silo'],
30
+ 'Terril' => [:other, 'Spoil heap / Teril'],
31
+ 'Torchère' => [:chimney, 'Flare / Torchère'],
32
+ 'Tour' => [:tower],
33
+ 'Treillis métallique' => [:other, 'Metallic grid / Treillis métallique']
34
+ }.freeze
35
+
36
+ def parse
37
+ tbody = prepare(html: read).css('tbody').last
38
+ tbody.css('tr').to_enum.with_index(1).each do |tr, index|
39
+ tds = tr.css('td')
40
+ name = tds[0].text.cleanup
41
+ next if NAME_BLACKLIST.include? name
42
+ elevation, height = tds[4].text.cleanup.split(/[()]/).map { _1.cleanup.remove("\n") }
43
+ type, type_remarks = TYPES.fetch(tds[2].text.cleanup)
44
+ count = tds[3].text.cleanup.to_i
45
+ visibility = tds[5].text.cleanup
46
+ obstacle = AIXM.obstacle(
47
+ source: source(position: tr.line),
48
+ name: name,
49
+ type: type,
50
+ xy: xy_from(tds[1].text),
51
+ z: z_from(elevation + 'AMSL')
52
+ ).tap do |obstacle|
53
+ obstacle.height = d_from(height)
54
+ obstacle.marking = visibility.match?(/jour/i)
55
+ obstacle.lighting = visibility.match?(/nuit/i)
56
+ obstacle.remarks = remarks_from(type_remarks, (count if count > 1), tds[6].text)
57
+ end
58
+ if count > 1
59
+ obstacle_group = AIXM.obstacle_group(
60
+ source: obstacle.source,
61
+ name: obstacle.name
62
+ ).tap do |obstacle_group|
63
+ obstacle_group.remarks = "#{count} obstacles"
64
+ end
65
+ obstacle_group.add_obstacle obstacle
66
+ add obstacle_group
67
+ else
68
+ add obstacle
69
+ end
70
+ rescue => error
71
+ warn("error parsing obstacle at ##{index}: #{error.message}", pry: error)
72
+ end
73
+ end
74
+
75
+ private
76
+
77
+ def remarks_from(*parts)
78
+ part_titles = ['TYPE', 'NUMBER/NOMBRE', 'DETAILS']
79
+ [].tap do |remarks|
80
+ parts.each.with_index do |part, index|
81
+ if part
82
+ part = part.to_s.cleanup.blank_to_nil
83
+ remarks << "**#{part_titles[index]}**\n#{part}"
84
+ end
85
+ end
86
+ end.join("\n\n").blank_to_nil
87
+ end
88
+ end
89
+ end
90
+ end
@@ -19,15 +19,15 @@ module AIPP
19
19
  prepare(html: read).css('tbody').each do |tbody|
20
20
  tbody.css('tr').to_enum.each_slice(2).with_index(1) do |trs, index|
21
21
  begin
22
- id, activity_and_name, upper_limits, timetable = trs.first.css('td')
22
+ id, activity_and_name, upper_limit, timetable = trs.first.css('td')
23
23
  activity, name = activity_and_name.css('span')
24
- lateral_limits, lower_limits, remarks = trs.last.css('td')
25
- lateral_limits.search('br').each { |br| br.replace("|||") }
26
- geometry, lateral_limits = lateral_limits.text.split('|||', 2)
27
- lateral_limits&.gsub!('|||', "\n")
24
+ lateral_limit, lower_limit, remarks = trs.last.css('td')
25
+ lateral_limit.search('br').each { _1.replace("|||") }
26
+ geometry, lateral_limit = lateral_limit.text.split('|||', 2)
27
+ lateral_limit&.gsub!('|||', "\n")
28
28
  remarks = [remarks&.text&.cleanup&.blank_to_nil]
29
29
  s = timetable&.text&.cleanup and remarks.prepend('**SCHEDULE**', s, '')
30
- s = lateral_limits&.cleanup and remarks.prepend('**LATERAL LIMITS**', s, '')
30
+ s = lateral_limit&.cleanup and remarks.prepend('**LATERAL LIMIT**', s, '')
31
31
  airspace = AIXM.airspace(
32
32
  source: source(position: trs.first.line),
33
33
  id: id.text.strip,
@@ -35,10 +35,12 @@ module AIPP
35
35
  name: [id.text.strip, name.text.cleanup].join(' ')
36
36
  ).tap do |airspace|
37
37
  airspace.geometry = geometry_from(geometry)
38
- airspace.layers << layer_from([upper_limits.text, lower_limits.text].join('---').cleanup).tap do |layer|
39
- layer.activity = ACTIVITIES.fetch(activity.text.downcase).fetch(:activity)
40
- layer.remarks = remarks.compact.join("\n")
41
- end
38
+ airspace.add_layer(
39
+ layer_from([upper_limit.text, lower_limit.text].join('---').cleanup).tap do |layer|
40
+ layer.activity = ACTIVITIES.fetch(activity.text.downcase).fetch(:activity)
41
+ layer.remarks = remarks.compact.join("\n")
42
+ end
43
+ )
42
44
  end
43
45
  rescue => error
44
46
  warn("error parsing #{airspace.type} `#{airspace.name}' at ##{index}: #{error.message}", pry: error)