aipp 0.2.5 → 0.2.6

Sign up to get free protection for your applications and to get access to all the features.
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)