lib/aipp/regions/LF/helpers/base.rb in aipp-0.2.6 vs lib/aipp/regions/LF/helpers/base.rb in aipp-1.0.0

- old
+ new

@@ -3,220 +3,213 @@ module Helpers module Base using AIXM::Refinements - # Map border names to OFMX - BORDERS = { - 'franco-allemande' => 'FRANCE_GERMANY', - 'franco-espagnole' => 'FRANCE_SPAIN', - 'franco-italienne' => 'FRANCE_ITALY', - 'franco-suisse' => 'FRANCE_SWITZERLAND', - 'franco-luxembourgeoise' => 'FRANCE_LUXEMBOURG', - 'franco-belge' => 'BELGIUM_FRANCE', - 'germano-suisse' => 'GERMANY_SWITZERLAND', - 'hispano-andorrane' => 'ANDORRA_SPAIN', - 'la côte atlantique française' => 'FRANCE_ATLANTIC_COAST', - 'côte méditérrannéenne' => 'FRANCE_MEDITERRANEAN_COAST', - 'limite des eaux territoriales atlantique françaises' => 'FRANCE_ATLANTIC_TERRITORIAL_SEA', - 'parc national des écrins' => 'FRANCE_ECRINS_NATIONAL_PARK' - }.freeze + # Supported version of the XML_SIA database dump + VERSION = '5'.freeze - # Intersection points between three countries - INTERSECTIONS = { - 'FRANCE_SPAIN|ANDORRA_SPAIN' => AIXM.xy(lat: 42.502720, long: 1.725965), - 'ANDORRA_SPAIN|FRANCE_SPAIN' => AIXM.xy(lat: 42.603571, long: 1.442681), - 'FRANCE_SWITZERLAND|FRANCE_ITALY' => AIXM.xy(lat: 45.922701, long: 7.044125), - 'BELGIUM_FRANCE|FRANCE_LUXEMBOURG' => AIXM.xy(lat: 49.546428, long: 5.818415), - 'FRANCE_LUXEMBOURG|FRANCE_GERMANY' => AIXM.xy(lat: 49.469438, long: 6.367516), - 'FRANCE_GERMANY|FRANCE_SWITZERLAND' => AIXM.xy(lat: 47.589831, long: 7.589049), - 'GERMANY_SWITZERLAND|FRANCE_GERMANY' => AIXM.xy(lat: 47.589831, long: 7.589049) - }.freeze + # Mandatory Interface - # Map surface to OFMX composition, preparation and remarks - SURFACES = { - /^revêtue?$/ => { preparation: :paved }, - /^non revêtue?$/ => { preparation: :natural }, - 'macadam' => { composition: :macadam }, - /^bitume ?(traité|psp)?$/ => { composition: :bitumen }, - 'ciment' => { composition: :concrete, preparation: :paved }, - /^b[eéè]ton ?(armé|bitume|bitumineux)?$/ => { composition: :concrete, preparation: :paved }, - /^béton( de)? ciment$/ => { composition: :concrete, preparation: :paved }, - 'béton herbe' => { composition: :concrete_and_grass }, - 'béton avec résine' => { composition: :concrete, preparation: :paved, remarks: 'Avec résine / with resin' }, - "béton + asphalte d'étanchéité sablé" => { composition: :concrete_and_asphalt, preparation: :paved, remarks: 'Étanchéité sablé / sandblasted waterproofing' }, - 'béton armé + support bitumastic' => { composition: :concrete, preparation: :paved, remarks: 'Support bitumastic / bitumen support' }, - /résine (époxy )?su[er] béton/ => { composition: :concrete, preparation: :paved, remarks: 'Avec couche résine / with resin seal coat' }, - /^(asphalte|tarmac)$/ => { composition: :asphalt, preparation: :paved }, - 'enrobé' => { preparation: :other, remarks: 'Enrobé / coated' }, - 'enrobé anti-kérozène' => { preparation: :other, remarks: 'Enrobé anti-kérozène / anti-kerosene coating' }, - /^enrobé bitum(e|iné|ineux)$/ => { composition: :bitumen, preparation: :paved, remarks: 'Enrobé / coated' }, - 'enrobé béton' => { composition: :concrete, preparation: :paved, remarks: 'Enrobé / coated' }, - /^résine( époxy)?$/ => { composition: :other, remarks: 'Résine / resin' }, - 'tole acier larmé' => { composition: :metal, preparation: :grooved }, - /^(structure métallique|aluminium)$/ => { composition: :metal }, - 'matériaux composites ignifugés' => { composition: :other, remarks: 'Matériaux composites ignifugés / fire resistant mixed materials' }, - /^(gazon|herbe)$/ => { composition: :grass }, - 'neige' => { composition: :snow }, - 'neige damée' => { composition: :snow, preparation: :rolled } - }.freeze - - # Transform French text fragments to English - ANGLICISE_MAP = { - /[^A-Z0-9 .\-]/ => '', - /0(\d)/ => '\1', - /(\d)-(\d)/ => '\1.\2', - /PARTIE/ => '', - /DELEG\./ => 'DELEG ', - /FRANCAISE?/ => 'FR', - /ANGLAISE?/ => 'UK', - /BELGE/ => 'BE', - /LUXEMBOURGEOISE?/ => 'LU', - /ALLEMANDE?/ => 'DE', - /SUISSE/ => 'CH', - /ITALIEN(?:NE)?/ => 'IT', - /ESPAGNOLE?/ => 'ES', - /ANDORRANE?/ => 'AD', - /NORD/ => 'N', - /EST/ => 'E', - /SUD/ => 'S', - /OEST/ => 'W', - /ANGLO NORMANDES/ => 'ANGLO-NORMANDES', - / +/ => ' ' - }.freeze - - # Templates - - def organisation_lf - @organisation_lf ||= AIXM.organisation( - name: 'FRANCE', - type: 'S' - ).tap do |organisation| - organisation.id = 'LF' + def setup + AIXM.config.voice_channel_separation = :any + unless cache.espace + xml = read('XML_SIA') + %i(Ad Bordure Espace Frequence Helistation NavFix Obstacle Partie RadioNav Rwy RwyLgt Service Volume).each do |section| + cache[section.downcase] = xml.css("#{section}S") + end + warn("XML_SIA database dump version mismatch") unless xml.at_css('SiaExport').attr(:Version) == VERSION end end - # Transformations - - def prepare(html:) - html.tap do |node| - node.css('del, *[class*="AmdtDeletedAIRAC"]').each(&:remove) # remove deleted entries + def url_for(aip_file) + sia_date = options[:airac].date.strftime('%d_%^b_%Y') # 04_JAN_2018 + xml_date = options[:airac].date.xmlschema # 2018-01-04 + sia_url = "https://www.sia.aviation-civile.gouv.fr/dvd/eAIP_#{sia_date}" + case aip_file + when /^Obstacles$/ # obstacles spreadsheet + "#{sia_url}/FRANCE/ObstaclesDataZone1MFRANCE_#{xml_date.remove('-')}.xlsx" + when /^VAC\-(\w+)/ # aerodrome VAC PDF + "#{sia_url}/Atlas-VAC/PDF_AIPparSSection/VAC/AD/AD-2.#{$1}.pdf" + when /^VACH\-(\w+)/ # helipad VAC PDF + "#{sia_url}/Atlas-VAC/PDF_AIPparSSection/VACH/AD/AD-3.#{$1}.pdf" + when /^[A-Z]+-/ # eAIP HTML page (e.g. ENR-5.5) + "#{sia_url}/FRANCE/AIRAC-#{xml_date}/html/eAIP/FR-#{aip_file}-fr-FR.html" + else # SIA XML database dump + "XML_SIA_#{xml_date}.xml" end end - def anglicise(name:) - name&.uptrans&.tap do |string| - ANGLICISE_MAP.each do |regexp, replacement| - string.gsub!(regexp, replacement) + # Templates + + def organisation_lf + unless cache.organisation_lf + cache.organisation_lf = AIXM.organisation( + source: source(position: 1, aip_file: "GEN-3.1"), + name: 'FRANCE', + type: 'S' + ).tap do |organisation| + organisation.id = 'LF' end + add cache.organisation_lf end + cache.organisation_lf end - # Parsers + # Parsersettes - def source(position:, aip_file: nil) - aip_file ||= @aip + # Build a source string + # + # @param position [Integer] line on which to find the information + # @param section [String] override autodetected section (e.g. "ENR") + # @param aip_file [String] override autodetected aip_file + # @return [String] source string + def source(position:, section: nil, aip_file: nil) + aip_file ||= 'XML_SIA' + section ||= aip_file.split(/-(?=\d)/).first [ options[:region], - aip_file.split('-').first, + section, aip_file, options[:airac].date.xmlschema, position ].join('|') end - def xy_from(text) - parts = text.strip.split(/\s+/) - AIXM.xy(lat: parts[0], long: parts[1]) + # Convert content to boolean + # + # @param content [String] either "oui" or "non" + # @return [Boolean] + def b_from(content) + case content + when 'oui' then true + when 'non' then false + else fail "`#{content}' is not boolean content" + end end - def z_from(limit) - case limit - when nil then nil - when 'SFC' then AIXM::GROUND - when 'UNL' then AIXM::UNLIMITED - when /(\d+)ftASFC/ then AIXM.z($1.to_i, :qfe) - when /(\d+)ftAMSL/ then AIXM.z($1.to_i, :qnh) - when /FL(\d+)/ then AIXM.z($1.to_i, :qne) - else fail "z `#{limit}' not recognized" - end + # Build coordinates from content + # + # @param content [String] source content + # @return [AIXM::XY] + def xy_from(content) + parts = content.split(/[\s,]+/) + AIXM.xy(lat: parts[0].to_f, long: parts[1].to_f) end - def d_from(text) - case text - when nil then nil - when /(\d+)(\w+)/ then AIXM.d($1.to_i, $2.to_sym) - else fail "d `#{text}' not recognized" + # Build altitude/elevation from value and unit + # + # @param value [String, Numeric, nil] numeric value + # @param unit [String] unit like "ft ASFC" or absolute like "SFC" + # @return [AIXM::Z] + def z_from(value: nil, unit: 'ft ASFC') + if value + case unit + when 'SFC' then AIXM::GROUND + when 'UNL' then AIXM::UNLIMITED + when 'ft ASFC' then AIXM.z(value.to_i, :qfe) + when 'ft AMSL' then AIXM.z(value.to_i, :qnh) + when 'FL' then AIXM.z(value.to_i, :qne) + else fail "z `#{[value, unit].join(' ')}' not recognized" + end end end - def elevation_from(text) - value, unit = text.strip.split - AIXM.z(AIXM.d(value.to_i, unit).to_ft.dist, :qnh) + # Build distance from content + # + # @param content [String] source content + # @return [AIXM::D] + def d_from(content) + parts = content.split(/\s/) + AIXM.d(parts[0].to_f, parts[1]) end - def layer_from(text_for_limit, text_for_class=nil) - above, below = text_for_limit.gsub(/ /, '').split(/\n+/).select(&:blank_to_nil).split { _1.match? '---+' } - AIXM.layer( - class: text_for_class, - vertical_limit: AIXM.vertical_limit( - upper_z: z_from(above[0]), - max_z: z_from(above[1]), - lower_z: z_from(below[0]), - min_z: z_from(below[1]) - ) - ) - end - - def geometry_from(text) + # Build geometry from content + # + # @param content [String] source content + # @return [AIXM::Component::Geometry] + def geometry_from(content) AIXM.geometry.tap do |geometry| buffer = {} - text.gsub(/\s+/, ' ').strip.split(/ - /).append('end').each do |element| - case element - when /arc (anti-)?horaire .+ sur (\S+) , (\S+)/i - geometry.add_segment AIXM.arc( - xy: buffer.delete(:xy), - center_xy: AIXM.xy(lat: $2, long: $3), - clockwise: $1.nil? - ) - when /cercle de ([\d\.]+) (NM|km|m) .+ sur (\S+) , (\S+)/i - geometry.add_segment AIXM.circle( - center_xy: AIXM.xy(lat: $3, long: $4), - radius: AIXM.d($1.to_f, $2) - ) - when /end|(\S+) , (\S+)/ - geometry.add_segment AIXM.point(xy: buffer[:xy]) if buffer.has_key?(:xy) - buffer[:xy] = AIXM.xy(lat: $1, long: $2) if $1 - if border = buffer.delete(:border) - from = border.nearest(xy: geometry.segments.last.xy) - to = border.nearest(xy: buffer[:xy], geometry_index: from.geometry_index) - geometry.add_segments border.segment(from_position: from, to_position: to).map(&:to_point) - end - when /^frontière ([\w-]+)/i, /^(\D[^(]+)/i - border_name = BORDERS.fetch($1.downcase.strip) - if borders.has_key? border_name # border from GeoJSON - buffer[:border] = borders[border_name] - else # named border - buffer[:xy] ||= INTERSECTIONS.fetch("#{buffer[:border_name]}|#{border_name}") - buffer[:border_name] = border_name - if border_name == 'FRANCE_SPAIN' # specify which part of this split border - border_name += buffer[:xy].lat < 42.55 ? '_EAST' : '_WEST' - end - geometry.add_segment AIXM.border( - xy: buffer.delete(:xy), - name: border_name + content.split("\n").each do |element| + parts = element.split(',', 3).last.split(/[():,]/) + # Write explicit geometry from previous iteration + if (bordure_name, xy = buffer.delete(:fnt)) + border = borders[bordure_name] + geometry.add_segments border.segment( + from_position: border.nearest(xy: xy), + to_position: border.nearest(xy: xy_from(parts[0])) + ).map(&:to_point) + end + # Write current iteration + geometry.add_segment( + case parts[1] + when 'grc' + AIXM.point( + xy: xy_from(parts[0]) ) + when 'rhl' + AIXM.rhumb_line( + xy: xy_from(parts[0]) + ) + when 'cwa', 'cca' + AIXM.arc( + xy: xy_from(parts[0]), + center_xy: xy_from(parts[5]), + clockwise: (parts[1] == 'cwa') + ) + when 'cir' + AIXM.circle( + center_xy: xy_from(parts[0]), + radius: d_from(parts[3..4].join(' ')) + ) + when 'fnt' + bordure = cache.bordure.at_css(%Q(Bordure[pk="#{parts[3]}"])) + bordure_name = bordure.(:Code) + if bordure_name.match? /:/ # explicit geometry + borders[bordure_name] ||= AIPP::Border.from_array([bordure.(:Geometrie).split]) + buffer[:fnt] = [bordure_name, xy_from(parts[2])] + AIXM.point( + xy: xy_from(parts[0]) + ) + else + AIXM.border( # named border + xy: xy_from(parts[0]), + name: bordure_name + ) + end + else + fail "geometry `#{parts[1]}' not recognized" end - else - fail "geometry `#{element}' not recognized" - end + ) end end end - def timetable_from!(text) - if text.gsub!(/^\s*#{AIXM::H_RE}\s*$/, '') - AIXM.timetable(code: Regexp.last_match&.to_s&.strip) + # Build timetable from content + # + # @param content [String] source content + # @return [AIXM::Component::Timetable] + def timetable_from(content) + AIXM.timetable(code: content) if AIXM::H_RE.match? content + end + + # Build layer from "volume" node + # + # @param volume_node [Nokogiri::XML::Element] source node + # @return [AIXM::Component::Layer] + def layer_from(volume_node) + AIXM.layer( + class: volume_node.(:Classe), + vertical_limit: AIXM.vertical_limit( + upper_z: z_from(value: volume_node.(:Plafond), unit: volume_node.(:PlafondRefUnite)), + max_z: z_from(value: volume_node.(:Plafond2)), + lower_z: z_from(value: volume_node.(:Plancher), unit: volume_node.(:PlancherRefUnite)), + min_z: z_from(value: volume_node.(:Plancher2)) + ) + ).tap do |layer| + layer.timetable = timetable_from(volume_node.(:HorCode)) + layer.remarks = volume_node.(:Remarque) end end end end