class AIPP::LF::AD2

Airports (IFR capable) and their CTR, AD navigational aids etc

Constants

NO_DESIGNATED_POINTS

Airports without VFR reporting points TODO: designated points on map but no list (LFLD LFSN LFBS) or no AD info (LFRL)

NO_VAC

Airports without VAC (e.g. military installations)

SOURCE_TYPES

Map source types to type and optional local type

SYNONYMS

Map synonyms for correlate

Public Instance Methods

parse() click to toggle source
   # File lib/aipp/regions/LF/AD-2.rb
40       def parse
41         index_html = prepare(html: read("AD-0.6"))   # index for AD-2.xxxx files
42         index_html.css('#AD-0\.6\.eAIP > .toc-block:nth-of-type(3) .toc-block a').each do |a|
43           @id = a.attribute('href').value[-4,4]
44           begin
45             aip_file = "AD-2.#{@id}"
46             html = prepare(html: read(aip_file))
47             # Airport
48             @remarks = []
49             @airport = AIXM.airport(
50               source: source(position: html.css('tr[id*="CODE_ICAO"]').first.line, aip_file: aip_file),
51               organisation: organisation_lf,   # TODO: not yet implemented
52               id: @id,
53               name: html.css('tr[id*="CODE_ICAO"] td span:nth-of-type(2)').text.strip.uptrans,
54               xy: xy_from(html.css('#AD-2\.2-Position_Geo_Arp td:nth-of-type(3)').text)
55             ).tap do |airport|
56               airport.z = elevation_from(html.css('#AD-2\.2-Altitude_Reference td:nth-of-type(3)').text)
57               airport.declination = declination_from(html.css('#AD-2\.2-Declinaison_Magnetique td:nth-of-type(3)').text)
58   #           airport.transition_z = AIXM.z(5000, :qnh)   # TODO: default - exceptions may exist
59               airport.timetable = timetable_from!(html.css('#AD-2\.3-Gestionnaire_AD td:nth-of-type(3)').text)
60             end
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             text = html.css('#AD-2\.2-Observations td:nth-of-type(3)').text
64             @airport.remarks = ([remarks_from(text)] + @remarks).compact.join("\n\n").blank_to_nil
65             add @airport
66             # Airspaces
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))
70             # Radio
71             trs = html.css('div[id*="-AD-2\.18"] tbody tr')
72             addresses_from(trs).each { @airport.add_address(_1) }
73             units_from(trs, airport: @airport).each(&method(:add))
74             # Navigational aids
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))
78             # Designated points
79             unless NO_VAC.include?(@id) || NO_DESIGNATED_POINTS.include?(@id)
80               pdf = read("VAC-#{@id}")
81               designated_points_from(pdf).tap do |designated_points|
82                 fix_designated_point_remarks(designated_points)
83 #               debug(designated_points)
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))
88               end
89             end
90           rescue => error
91             warn("error parsing airport #{@id}: #{error.message}", pry: error)
92           end
93         end
94       end

Private Instance Methods

airspace_from(tr) click to toggle source
    # File lib/aipp/regions/LF/AD-2.rb
202 def airspace_from(tr)
203   spans = tr.css(:span)
204   source_type = spans[1].text.blank_to_nil
205   fail "unknown type `#{source_type}'" unless SOURCE_TYPES.has_key? source_type
206   AIXM.airspace(
207     name: [spans[2].text, anglicise(name: spans[3]&.text)].compact.join(' '),
208     type: SOURCE_TYPES.dig(source_type, :type),
209     local_type: SOURCE_TYPES.dig(source_type, :local_type)
210   ).tap do |airspace|
211     airspace.source = source(position: tr.line)
212   end
213 end
airspaces_from(tbody) click to toggle source
    # File lib/aipp/regions/LF/AD-2.rb
184 def airspaces_from(tbody)
185   return [] if tbody.text.blank?
186   airspace = nil
187   tbody.css('tr').to_enum.with_object([]) do |tr, array|
188     if tr.attr(:class) =~ /keep-with-next-row/
189       airspace = airspace_from tr
190     else
191       tds = tr.css('td')
192       airspace.geometry = geometry_from tds[0].text
193       fail("geometry is not closed") unless airspace.geometry.closed?
194       airspace.add_layer layer_from(tds[2].text, tds[1].text.strip)
195       airspace.layers.first.timetable = timetable_from! tds[4].text
196       airspace.layers.first.remarks = remarks_from(tds[4].text)
197       array << airspace
198     end
199   end
200 end
declination_from(text) click to toggle source
    # File lib/aipp/regions/LF/AD-2.rb
 98 def declination_from(text)
 99   value, direction = text.strip.split('°')
100   value = value.to_f * (direction == 'W' ? -1 : 1)
101 end
designated_point_from(buffer, pdf) click to toggle source
    # File lib/aipp/regions/LF/AD-2.rb
264 def designated_point_from(buffer, pdf)
265   if buffer[:id] && buffer[:xy]&.size == 2
266     buffer[:remarks].gsub!(/ {20}/, "\n")   # recognize empty column space
267     buffer[:remarks].remove!(/\(\d+\)/)   # remove footnotes
268     buffer[:remarks] = buffer[:remarks].unglue   # separate glued words
269     AIXM.designated_point(
270       source: source(position: buffer[:page], aip_file: pdf.file.basename('.*').to_s),
271       type: :vfr_mandatory_reporting_point,
272       id: buffer[:id].remove(/\W/),
273       xy: AIXM.xy(lat: buffer[:xy].first, long: buffer[:xy].last)
274     ).tap do |designated_point|
275       designated_point.airport = @airport
276       designated_point.remarks = buffer[:remarks].compact.blank_to_nil
277       buffer.clear
278     end
279   end
280 end
designated_points_from(pdf, recursive=false) click to toggle source
    # File lib/aipp/regions/LF/AD-2.rb
237 def designated_points_from(pdf, recursive=false)
238   from = (pdf.text =~ /^(.*?coordinates.*?names?)/i)
239   return [] if recursive && !from
240   warn("no designated points section begin found for #{@id}", pry: binding) unless from
241   from += $1.length
242   to = from + (pdf.text.from(from) =~ /\n\s*\n\s*\n|^.*(?:ifr|vfr|ad\s*equipment|special\s*activities|training\s*flights|mto\s*minima)/i)
243   warn("no designated points section end found for #{@id}", pry: binding) unless to
244   from, to = from + pdf.range.min, to + pdf.range.min   # offset when recursive
245   buffer = {}
246   pdf.from(from).to(to).each_line.with_object([]) do |(line, page, last), designated_points|
247     line.remove!(/\u2190/)   # remove arrow symbols
248     has_id = $1 if line.sub!(/^\s{,20}([A-Z][A-Z\d ]{1,3})(?=\W)/, '')
249     has_xy = line.match?(AIXM::DMS_RE)
250     designated_points << designated_point_from(buffer, pdf) if has_id || has_xy
251     if has_xy
252       2.times { (buffer[:xy] ||= []) << $1 if line.sub!(AIXM::DMS_RE, '') }
253       buffer[:xy]&.compact!
254       line.remove!(/\d{3,4}\D.+?MTG/)   # remove extra columns (e.g. LFML)
255       line.remove!(/[\s#{AIXM::MIN}#{AIXM::SEC}]*[-\u2013]/)   # remove dash between coordinates
256     end
257     buffer[:page] = page
258     buffer[:id] = has_id if has_id
259     buffer[:remarks] = [buffer[:remarks], line].join("\n")
260     designated_points << designated_point_from(buffer, pdf) if last
261   end.compact + designated_points_from(pdf.from(to).to(:end), true)
262 end
fix_designated_point_remarks(designated_points) click to toggle source

Assign scattered similar remarks to one and the same designated point

    # File lib/aipp/regions/LF/AD-2.rb
283 def fix_designated_point_remarks(designated_points)
284   one = nil
285   designated_points.map do |two|
286     if one
287       one_lines, two_lines = one.remarks&.lines, two.remarks&.lines
288       if one_lines && two_lines
289         if one_lines.count > 1 && (line = one_lines.last) !~ %r(\s/\s)
290           # Move up
291           if line.correlate(remainder = one_lines[0..-2].join, SYNONYMS) < line.correlate(two.remarks)
292             two.remarks = [line, two.remarks].join("\n").compact
293             one.remarks = remainder.compact
294           end
295         elsif two_lines.count > 1 && (line = two_lines.first) !~ %r(\s/\s)
296           # Move down
297           line = two_lines.first
298           if line.correlate(remainder = two_lines[1..-1].join, SYNONYMS) < line.correlate(one.remarks)
299             one.remarks = [one.remarks, line].join("\n").compact
300             two.remarks = remainder.compact
301           end
302         end
303       end
304     end
305     one = two
306   end.map do |designated_point|
307     designated_point.remarks = designated_point.remarks&.cleanup.blank_to_nil
308   end
309 end
helipads_from(tbody) click to toggle source
    # File lib/aipp/regions/LF/AD-2.rb
162 def helipads_from(tbody)
163   text_fr = tbody.css('td:nth-of-type(3)').text.compact
164   text_en = tbody.css('td:nth-of-type(4)').text.compact
165   case text_fr
166   when /NIL/, /\A\W*\z/
167     []
168   when /instructions?\s+twr/i
169     @remarks << "HELICOPTER:\nSur instructions TWR.\nOn TWR clearance."
170     []
171   when AIXM::DMS_RE
172     text_fr.scan(AIXM::DMS_RE).each_slice(2).with_index(1).map do |(lat, long), index|
173       AIXM.helipad(
174         name: "H#{index}",
175         xy: AIXM.xy(lat: lat.first, long: long.first)
176       )
177     end
178   else
179     @remarks << ['HELICOPTER:', text_fr.blank_to_nil, text_en.blank_to_nil].compact.join("\n")
180     []
181   end
182 end
navigational_aids_from(tbody) click to toggle source
remarks_from(text) click to toggle source
    # File lib/aipp/regions/LF/AD-2.rb
103 def remarks_from(text)
104   text.sub(/NIL|\(\*\)\s+/, '').strip.gsub(/(\s)\s+/, '\1').blank_to_nil
105 end
runways_from(tbody) click to toggle source
    # File lib/aipp/regions/LF/AD-2.rb
107 def runways_from(tbody)
108   directions_map = tbody.css('tr[id*="TXT_DESIG"]').map do |tr|
109     [AIXM.a(tr.css('td:first-of-type').text.strip), tr]
110   end.to_h
111   remarks_map = tbody.css('tr[id*="TXT_RMK_NAT"]').map do |tr|
112     [tr.text.strip[/\A\((\d+)\)/, 1].to_i, tr.css('span')]
113   end.to_h
114   directions = directions_map.keys
115   grouped_directions = directions.map do |direction|
116     inverted_direction = direction.invert
117     if directions.include? inverted_direction
118       [direction, inverted_direction].map(&:to_s).sort.join('/')
119     else
120       direction.to_s
121     end
122   end.uniq
123   grouped_directions.map do |runway_name|
124     AIXM.runway(name: runway_name).tap do |runway|
125       %i(forth back).each do |direction_attr|
126         if direction = runway.send(direction_attr)
127           tr = directions_map[direction.name]
128           if direction_attr == :forth
129             length, width = tr.css('td:nth-of-type(3)').text.strip.split('x')
130             runway.length = AIXM.d(length.strip.to_i, :m)
131             runway.width = AIXM.d(width.strip.to_i, :m)
132             unless (text = tr.css('td:nth-of-type(5)').text.strip.split(%r<\W+/\W+>).first.compact).blank?
133               surface = SURFACES.metch(text)
134               runway.surface.composition = surface[:composition]
135               runway.surface.preparation = surface[:preparation]
136               runway.surface.remarks = surface[:remarks]
137             end
138             if (text = tr.css('td:nth-of-type(4)').text).match?(AIXM::PCN_RE)
139               runway.surface.pcn = text
140             end
141           end
142           text = tr.css('td:nth-of-type(6)').text.strip
143           direction.xy = (xy_from(text) unless text.match?(/\A(\(.*)?\z/m))
144           if (text = tr.css('td:nth-of-type(7)').text.strip[/thr:\s+(\d+\s+\w+)/i, 1]).present?
145             direction.z = elevation_from(text)
146           end
147           if (text = tr.css('td:nth-of-type(2)').text.strip.sub(/\A(\d+).*$/m, '\1')).present?
148             direction.geographic_orientation = AIXM.a(text.to_i)
149           end
150           if (text = tr.css('td:nth-of-type(6)').text[/\((.+)\)/m, 1]).present?
151             direction.displaced_threshold = xy_from(text)
152           end
153           if (text = tr.css('td:nth-of-type(10)').text.strip[/\A\((\d+)\)/, 1]).present?
154             direction.remarks = remarks_from(remarks_map.fetch(text.to_i).text)
155           end
156         end
157       end
158     end
159   end
160 end