aipp 0.2.4 → 1.0.0
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.
- checksums.yaml +4 -4
 - checksums.yaml.gz.sig +2 -0
 - data/CHANGELOG.md +38 -0
 - data/README.md +222 -88
 - data/exe/aip2aixm +2 -2
 - data/exe/aip2ofmx +2 -2
 - data/lib/aipp/aip.rb +113 -31
 - data/lib/aipp/border.rb +77 -46
 - data/lib/aipp/debugger.rb +101 -0
 - data/lib/aipp/downloader.rb +39 -26
 - data/lib/aipp/executable.rb +41 -22
 - data/lib/aipp/parser.rb +94 -21
 - data/lib/aipp/patcher.rb +5 -2
 - data/lib/aipp/pdf.rb +1 -1
 - data/lib/aipp/regions/LF/README.md +49 -0
 - data/lib/aipp/regions/LF/aerodromes.rb +223 -0
 - data/lib/aipp/regions/LF/d_p_r_airspaces.rb +56 -0
 - data/lib/aipp/regions/LF/dangerous_activities.rb +49 -0
 - data/lib/aipp/regions/LF/designated_points.rb +47 -0
 - data/lib/aipp/regions/LF/fixtures/aerodromes.yml +608 -0
 - data/lib/aipp/regions/LF/helipads.rb +122 -0
 - data/lib/aipp/regions/LF/helpers/base.rb +218 -0
 - data/lib/aipp/regions/LF/helpers/surface.rb +49 -0
 - data/lib/aipp/regions/LF/helpers/usage_limitation.rb +20 -0
 - data/lib/aipp/regions/LF/navigational_aids.rb +85 -0
 - data/lib/aipp/regions/LF/obstacles.rb +153 -0
 - data/lib/aipp/regions/LF/serviced_airspaces.rb +70 -0
 - data/lib/aipp/regions/LF/services.rb +172 -0
 - data/lib/aipp/t_hash.rb +4 -5
 - data/lib/aipp/version.rb +1 -1
 - data/lib/aipp.rb +11 -5
 - data/lib/core_ext/enumerable.rb +9 -9
 - data/lib/core_ext/hash.rb +21 -5
 - data/lib/core_ext/nokogiri.rb +54 -0
 - data/lib/core_ext/string.rb +38 -66
 - data.tar.gz.sig +2 -0
 - metadata +180 -188
 - metadata.gz.sig +0 -0
 - data/.gitignore +0 -8
 - data/.ruby-version +0 -1
 - data/.travis.yml +0 -8
 - data/.yardopts +0 -3
 - data/Guardfile +0 -7
 - data/TODO.md +0 -6
 - data/aipp.gemspec +0 -44
 - data/gems.rb +0 -3
 - data/lib/aipp/airac.rb +0 -55
 - data/lib/aipp/regions/LF/AD-1.3.rb +0 -162
 - data/lib/aipp/regions/LF/AD-1.6.rb +0 -31
 - data/lib/aipp/regions/LF/AD-2.rb +0 -313
 - data/lib/aipp/regions/LF/AD-3.1.rb +0 -185
 - data/lib/aipp/regions/LF/ENR-2.1.rb +0 -92
 - data/lib/aipp/regions/LF/ENR-4.1.rb +0 -97
 - data/lib/aipp/regions/LF/ENR-4.3.rb +0 -28
 - data/lib/aipp/regions/LF/ENR-5.1.rb +0 -75
 - data/lib/aipp/regions/LF/ENR-5.5.rb +0 -53
 - data/lib/aipp/regions/LF/fixtures/AD-1.3.yml +0 -511
 - data/lib/aipp/regions/LF/fixtures/AD-2.yml +0 -185
 - data/lib/aipp/regions/LF/fixtures/AD-3.1.yml +0 -10
 - data/lib/aipp/regions/LF/helpers/AD_radio.rb +0 -90
 - data/lib/aipp/regions/LF/helpers/URL.rb +0 -26
 - data/lib/aipp/regions/LF/helpers/common.rb +0 -217
 - data/lib/core_ext/object.rb +0 -43
 - data/rakefile.rb +0 -12
 - data/spec/fixtures/archive.zip +0 -0
 - data/spec/fixtures/border.geojson +0 -201
 - data/spec/fixtures/document.pdf +0 -0
 - data/spec/fixtures/document.pdf.json +0 -1
 - data/spec/fixtures/new.html +0 -6
 - data/spec/fixtures/new.pdf +0 -0
 - data/spec/fixtures/new.txt +0 -1
 - data/spec/lib/aipp/airac_spec.rb +0 -98
 - data/spec/lib/aipp/border_spec.rb +0 -135
 - data/spec/lib/aipp/downloader_spec.rb +0 -81
 - data/spec/lib/aipp/patcher_spec.rb +0 -46
 - data/spec/lib/aipp/pdf_spec.rb +0 -124
 - data/spec/lib/aipp/t_hash_spec.rb +0 -44
 - data/spec/lib/aipp/version_spec.rb +0 -7
 - data/spec/lib/core_ext/enumberable_spec.rb +0 -76
 - data/spec/lib/core_ext/hash_spec.rb +0 -27
 - data/spec/lib/core_ext/integer_spec.rb +0 -15
 - data/spec/lib/core_ext/nil_class_spec.rb +0 -11
 - data/spec/lib/core_ext/string_spec.rb +0 -112
 - data/spec/sounds/failure.mp3 +0 -0
 - data/spec/sounds/success.mp3 +0 -0
 - data/spec/spec_helper.rb +0 -28
 
| 
         @@ -0,0 +1,122 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module AIPP
         
     | 
| 
      
 2 
     | 
    
         
            +
              module LF
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
                class Helipads < AIP
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
                  include AIPP::LF::Helpers::Base
         
     | 
| 
      
 7 
     | 
    
         
            +
                  include AIPP::LF::Helpers::UsageLimitation
         
     | 
| 
      
 8 
     | 
    
         
            +
                  include AIPP::LF::Helpers::Surface
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
                  DEPENDS = %w(aerodromes)
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                  HOSTILITIES = {
         
     | 
| 
      
 13 
     | 
    
         
            +
                    'hostile habitée' => 'Zone hostile habitée / hostile populated area',
         
     | 
| 
      
 14 
     | 
    
         
            +
                    'hostile non habitée' => 'Zone hostile non habitée / hostile unpopulated area',
         
     | 
| 
      
 15 
     | 
    
         
            +
                    'non hostile' => 'Zone non hostile / non-hostile area'
         
     | 
| 
      
 16 
     | 
    
         
            +
                  }.freeze
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                  ELEVATED = {
         
     | 
| 
      
 19 
     | 
    
         
            +
                    true => 'En terrasse / on deck',
         
     | 
| 
      
 20 
     | 
    
         
            +
                    false => 'En surface / on ground'
         
     | 
| 
      
 21 
     | 
    
         
            +
                  }.freeze
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
                  def parse
         
     | 
| 
      
 24 
     | 
    
         
            +
                    cache.helistation.css(%Q(Helistation[lk^="[LF]"])).each do |helistation_node|
         
     | 
| 
      
 25 
     | 
    
         
            +
                      # Build airport if necessary
         
     | 
| 
      
 26 
     | 
    
         
            +
                      next unless limitation_type = LIMITATION_TYPES.fetch(helistation_node.(:Statut))
         
     | 
| 
      
 27 
     | 
    
         
            +
                      name = helistation_node.(:Nom)
         
     | 
| 
      
 28 
     | 
    
         
            +
                      airport = find_by(:airport, name: name).first || add(
         
     | 
| 
      
 29 
     | 
    
         
            +
                        AIXM.airport(
         
     | 
| 
      
 30 
     | 
    
         
            +
                          source: source(section: 'AD', position: helistation_node.line),
         
     | 
| 
      
 31 
     | 
    
         
            +
                          organisation: organisation_lf,
         
     | 
| 
      
 32 
     | 
    
         
            +
                          id: options[:region],
         
     | 
| 
      
 33 
     | 
    
         
            +
                          name: name,
         
     | 
| 
      
 34 
     | 
    
         
            +
                          xy: xy_from(helistation_node.(:Geometrie))
         
     | 
| 
      
 35 
     | 
    
         
            +
                        ).tap do |airport|
         
     | 
| 
      
 36 
     | 
    
         
            +
                          airport.z = AIXM.z(helistation_node.(:AltitudeFt).to_i, :qnh)
         
     | 
| 
      
 37 
     | 
    
         
            +
                          airport.add_usage_limitation(type: limitation_type.fetch(:limitation)) do |limitation|
         
     | 
| 
      
 38 
     | 
    
         
            +
                            limitation.remarks = limitation_type[:remarks]
         
     | 
| 
      
 39 
     | 
    
         
            +
                            [:private].each do |purpose|   # TODO: check and simplify
         
     | 
| 
      
 40 
     | 
    
         
            +
                              limitation.add_condition do |condition|
         
     | 
| 
      
 41 
     | 
    
         
            +
                                condition.realm = limitation_type.fetch(:realm)
         
     | 
| 
      
 42 
     | 
    
         
            +
                                condition.origin = :any
         
     | 
| 
      
 43 
     | 
    
         
            +
                                condition.rule = case
         
     | 
| 
      
 44 
     | 
    
         
            +
                                  when helistation_node.(:Ifr?) then :ifr_and_vfr
         
     | 
| 
      
 45 
     | 
    
         
            +
                                  else :vfr
         
     | 
| 
      
 46 
     | 
    
         
            +
                                end
         
     | 
| 
      
 47 
     | 
    
         
            +
                                condition.purpose = purpose
         
     | 
| 
      
 48 
     | 
    
         
            +
                              end
         
     | 
| 
      
 49 
     | 
    
         
            +
                            end
         
     | 
| 
      
 50 
     | 
    
         
            +
                          end
         
     | 
| 
      
 51 
     | 
    
         
            +
                        end
         
     | 
| 
      
 52 
     | 
    
         
            +
                      )
         
     | 
| 
      
 53 
     | 
    
         
            +
            # TODO: link to VAC once supported downstream
         
     | 
| 
      
 54 
     | 
    
         
            +
            #         # Link to VAC
         
     | 
| 
      
 55 
     | 
    
         
            +
            #         if helistation_node.(:Atlas?)
         
     | 
| 
      
 56 
     | 
    
         
            +
            #           vac = "VAC-#{airport.id}" if airport.id.match?(/^LF[A-Z]{2}$/)
         
     | 
| 
      
 57 
     | 
    
         
            +
            #           vac ||= "VACH-H#{airport.name[0, 3].upcase}"
         
     | 
| 
      
 58 
     | 
    
         
            +
            #           airport.remarks = [
         
     | 
| 
      
 59 
     | 
    
         
            +
            #             airport.remarks.to_s,
         
     | 
| 
      
 60 
     | 
    
         
            +
            #             link_to('VAC-HP', url_for(vac))
         
     | 
| 
      
 61 
     | 
    
         
            +
            #           ].join("\n")
         
     | 
| 
      
 62 
     | 
    
         
            +
            #         end
         
     | 
| 
      
 63 
     | 
    
         
            +
                      # Add helipad and FATO
         
     | 
| 
      
 64 
     | 
    
         
            +
                      airport.add_helipad(
         
     | 
| 
      
 65 
     | 
    
         
            +
                        AIXM.helipad(
         
     | 
| 
      
 66 
     | 
    
         
            +
                          name: 'TLOF',
         
     | 
| 
      
 67 
     | 
    
         
            +
                          xy: xy_from(helistation_node.(:Geometrie))
         
     | 
| 
      
 68 
     | 
    
         
            +
                        ).tap do |helipad|
         
     | 
| 
      
 69 
     | 
    
         
            +
                          helipad.z = AIXM.z(helistation_node.(:AltitudeFt).to_i, :qnh)
         
     | 
| 
      
 70 
     | 
    
         
            +
                          helipad.dimensions = dimensions_from(helistation_node.(:DimTlof))
         
     | 
| 
      
 71 
     | 
    
         
            +
                        end.tap do |helipad|
         
     | 
| 
      
 72 
     | 
    
         
            +
                          airport.add_helipad(helipad)
         
     | 
| 
      
 73 
     | 
    
         
            +
                          helipad.performance_class = performance_class_from(helistation_node.(:ClassePerf))
         
     | 
| 
      
 74 
     | 
    
         
            +
                          helipad.surface = surface_from(helistation_node)
         
     | 
| 
      
 75 
     | 
    
         
            +
                          helipad.marking = helistation_node.(:Balisage) unless helistation_node.(:Balisage)&.match?(/^nil$/i)
         
     | 
| 
      
 76 
     | 
    
         
            +
                          helipad.add_lighting(AIXM.lighting(position: :other)) if helistation_node.(:Nuit?) || helistation_node.(:Balisage)&.match?(/feu/i)
         
     | 
| 
      
 77 
     | 
    
         
            +
                          helipad.remarks = {
         
     | 
| 
      
 78 
     | 
    
         
            +
                            'position/positioning' => [
         
     | 
| 
      
 79 
     | 
    
         
            +
                              (HOSTILITIES.fetch(helistation_node.(:ZoneHabitee)) if helistation_node.(:ZoneHabitee)),
         
     | 
| 
      
 80 
     | 
    
         
            +
                              (ELEVATED.fetch(helistation_node.(:EnTerrasse?)) if helistation_node.(:EnTerrasse)),
         
     | 
| 
      
 81 
     | 
    
         
            +
                            ].compact.join("\n"),
         
     | 
| 
      
 82 
     | 
    
         
            +
                            'hauteur/height' => given(helistation_node.(:HauteurFt)) { "#{_1} ft" },
         
     | 
| 
      
 83 
     | 
    
         
            +
                            'exploitant/operator' => helistation_node.(:Exploitant)
         
     | 
| 
      
 84 
     | 
    
         
            +
                          }.to_remarks
         
     | 
| 
      
 85 
     | 
    
         
            +
                          if fato_dimensions = dimensions_from(helistation_node.(:DimFato))
         
     | 
| 
      
 86 
     | 
    
         
            +
                            AIXM.fato(name: 'FATO').tap do |fato|
         
     | 
| 
      
 87 
     | 
    
         
            +
                              fato.dimensions = fato_dimensions
         
     | 
| 
      
 88 
     | 
    
         
            +
                              airport.add_fato(fato)
         
     | 
| 
      
 89 
     | 
    
         
            +
                              helipad.fato = fato
         
     | 
| 
      
 90 
     | 
    
         
            +
                            end
         
     | 
| 
      
 91 
     | 
    
         
            +
                          end
         
     | 
| 
      
 92 
     | 
    
         
            +
                        end
         
     | 
| 
      
 93 
     | 
    
         
            +
                      )
         
     | 
| 
      
 94 
     | 
    
         
            +
                    end
         
     | 
| 
      
 95 
     | 
    
         
            +
                  end
         
     | 
| 
      
 96 
     | 
    
         
            +
             
     | 
| 
      
 97 
     | 
    
         
            +
                  private
         
     | 
| 
      
 98 
     | 
    
         
            +
             
     | 
| 
      
 99 
     | 
    
         
            +
                  def dimensions_from(content)
         
     | 
| 
      
 100 
     | 
    
         
            +
                    if content
         
     | 
| 
      
 101 
     | 
    
         
            +
                      dims = content.remove(/[^x\d.,]/i).split(/x/i).map { _1.to_ff.floor }
         
     | 
| 
      
 102 
     | 
    
         
            +
                      case dims.size
         
     | 
| 
      
 103 
     | 
    
         
            +
                      when 1
         
     | 
| 
      
 104 
     | 
    
         
            +
                        AIXM.r(AIXM.d(dims[0], :m))
         
     | 
| 
      
 105 
     | 
    
         
            +
                      when 2
         
     | 
| 
      
 106 
     | 
    
         
            +
                        AIXM.r(AIXM.d(dims[0], :m), AIXM.d(dims[1], :m))
         
     | 
| 
      
 107 
     | 
    
         
            +
                      when 4
         
     | 
| 
      
 108 
     | 
    
         
            +
                        AIXM.r(AIXM.d(dims.min, :m))
         
     | 
| 
      
 109 
     | 
    
         
            +
                      else
         
     | 
| 
      
 110 
     | 
    
         
            +
                        warn("ignoring dimensions `#{content}'", severe: false)
         
     | 
| 
      
 111 
     | 
    
         
            +
                        nil
         
     | 
| 
      
 112 
     | 
    
         
            +
                      end
         
     | 
| 
      
 113 
     | 
    
         
            +
                    end
         
     | 
| 
      
 114 
     | 
    
         
            +
                  end
         
     | 
| 
      
 115 
     | 
    
         
            +
             
     | 
| 
      
 116 
     | 
    
         
            +
                  def performance_class_from(content)
         
     | 
| 
      
 117 
     | 
    
         
            +
                    content.remove(/\d{2,}/).scan(/\d/).map(&:to_i).min&.to_s if content
         
     | 
| 
      
 118 
     | 
    
         
            +
                  end
         
     | 
| 
      
 119 
     | 
    
         
            +
             
     | 
| 
      
 120 
     | 
    
         
            +
                end
         
     | 
| 
      
 121 
     | 
    
         
            +
              end
         
     | 
| 
      
 122 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,218 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module AIPP
         
     | 
| 
      
 2 
     | 
    
         
            +
              module LF
         
     | 
| 
      
 3 
     | 
    
         
            +
                module Helpers
         
     | 
| 
      
 4 
     | 
    
         
            +
                  module Base
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
                    using AIXM::Refinements
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                    # Supported version of the XML_SIA database dump
         
     | 
| 
      
 9 
     | 
    
         
            +
                    VERSION = '5'.freeze
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
                    # Mandatory Interface
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                    def setup
         
     | 
| 
      
 14 
     | 
    
         
            +
                      AIXM.config.voice_channel_separation = :any
         
     | 
| 
      
 15 
     | 
    
         
            +
                      unless cache.espace
         
     | 
| 
      
 16 
     | 
    
         
            +
                        xml = read('XML_SIA')
         
     | 
| 
      
 17 
     | 
    
         
            +
                        %i(Ad Bordure Espace Frequence Helistation NavFix Obstacle Partie RadioNav Rwy RwyLgt Service Volume).each do |section|
         
     | 
| 
      
 18 
     | 
    
         
            +
                          cache[section.downcase] = xml.css("#{section}S")
         
     | 
| 
      
 19 
     | 
    
         
            +
                        end
         
     | 
| 
      
 20 
     | 
    
         
            +
                        warn("XML_SIA database dump version mismatch") unless xml.at_css('SiaExport').attr(:Version) == VERSION
         
     | 
| 
      
 21 
     | 
    
         
            +
                      end
         
     | 
| 
      
 22 
     | 
    
         
            +
                    end
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                    def url_for(aip_file)
         
     | 
| 
      
 25 
     | 
    
         
            +
                      sia_date = options[:airac].date.strftime('%d_%^b_%Y')   # 04_JAN_2018
         
     | 
| 
      
 26 
     | 
    
         
            +
                      xml_date = options[:airac].date.xmlschema               # 2018-01-04
         
     | 
| 
      
 27 
     | 
    
         
            +
                      sia_url = "https://www.sia.aviation-civile.gouv.fr/dvd/eAIP_#{sia_date}"
         
     | 
| 
      
 28 
     | 
    
         
            +
                      case aip_file
         
     | 
| 
      
 29 
     | 
    
         
            +
                      when /^Obstacles$/   # obstacles spreadsheet
         
     | 
| 
      
 30 
     | 
    
         
            +
                        "#{sia_url}/FRANCE/ObstaclesDataZone1MFRANCE_#{xml_date.remove('-')}.xlsx"
         
     | 
| 
      
 31 
     | 
    
         
            +
                      when /^VAC\-(\w+)/   # aerodrome VAC PDF
         
     | 
| 
      
 32 
     | 
    
         
            +
                        "#{sia_url}/Atlas-VAC/PDF_AIPparSSection/VAC/AD/AD-2.#{$1}.pdf"
         
     | 
| 
      
 33 
     | 
    
         
            +
                      when /^VACH\-(\w+)/   # helipad VAC PDF
         
     | 
| 
      
 34 
     | 
    
         
            +
                        "#{sia_url}/Atlas-VAC/PDF_AIPparSSection/VACH/AD/AD-3.#{$1}.pdf"
         
     | 
| 
      
 35 
     | 
    
         
            +
                      when /^[A-Z]+-/   # eAIP HTML page (e.g. ENR-5.5)
         
     | 
| 
      
 36 
     | 
    
         
            +
                        "#{sia_url}/FRANCE/AIRAC-#{xml_date}/html/eAIP/FR-#{aip_file}-fr-FR.html"
         
     | 
| 
      
 37 
     | 
    
         
            +
                      else   # SIA XML database dump
         
     | 
| 
      
 38 
     | 
    
         
            +
                        "XML_SIA_#{xml_date}.xml"
         
     | 
| 
      
 39 
     | 
    
         
            +
                      end
         
     | 
| 
      
 40 
     | 
    
         
            +
                    end
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
                    # Templates
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
                    def organisation_lf
         
     | 
| 
      
 45 
     | 
    
         
            +
                      unless cache.organisation_lf
         
     | 
| 
      
 46 
     | 
    
         
            +
                        cache.organisation_lf = AIXM.organisation(
         
     | 
| 
      
 47 
     | 
    
         
            +
                          source: source(position: 1, aip_file: "GEN-3.1"),
         
     | 
| 
      
 48 
     | 
    
         
            +
                          name: 'FRANCE',
         
     | 
| 
      
 49 
     | 
    
         
            +
                          type: 'S'
         
     | 
| 
      
 50 
     | 
    
         
            +
                        ).tap do |organisation|
         
     | 
| 
      
 51 
     | 
    
         
            +
                          organisation.id = 'LF'
         
     | 
| 
      
 52 
     | 
    
         
            +
                        end
         
     | 
| 
      
 53 
     | 
    
         
            +
                        add cache.organisation_lf
         
     | 
| 
      
 54 
     | 
    
         
            +
                      end
         
     | 
| 
      
 55 
     | 
    
         
            +
                      cache.organisation_lf
         
     | 
| 
      
 56 
     | 
    
         
            +
                    end
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
                    # Parsersettes
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
                    # Build a source string
         
     | 
| 
      
 61 
     | 
    
         
            +
                    #
         
     | 
| 
      
 62 
     | 
    
         
            +
                    # @param position [Integer] line on which to find the information
         
     | 
| 
      
 63 
     | 
    
         
            +
                    # @param section [String] override autodetected section (e.g. "ENR")
         
     | 
| 
      
 64 
     | 
    
         
            +
                    # @param aip_file [String] override autodetected aip_file
         
     | 
| 
      
 65 
     | 
    
         
            +
                    # @return [String] source string
         
     | 
| 
      
 66 
     | 
    
         
            +
                    def source(position:, section: nil, aip_file: nil)
         
     | 
| 
      
 67 
     | 
    
         
            +
                      aip_file ||= 'XML_SIA'
         
     | 
| 
      
 68 
     | 
    
         
            +
                      section ||= aip_file.split(/-(?=\d)/).first
         
     | 
| 
      
 69 
     | 
    
         
            +
                      [
         
     | 
| 
      
 70 
     | 
    
         
            +
                        options[:region],
         
     | 
| 
      
 71 
     | 
    
         
            +
                        section,
         
     | 
| 
      
 72 
     | 
    
         
            +
                        aip_file,
         
     | 
| 
      
 73 
     | 
    
         
            +
                        options[:airac].date.xmlschema,
         
     | 
| 
      
 74 
     | 
    
         
            +
                        position
         
     | 
| 
      
 75 
     | 
    
         
            +
                      ].join('|')
         
     | 
| 
      
 76 
     | 
    
         
            +
                    end
         
     | 
| 
      
 77 
     | 
    
         
            +
             
     | 
| 
      
 78 
     | 
    
         
            +
                    # Convert content to boolean
         
     | 
| 
      
 79 
     | 
    
         
            +
                    #
         
     | 
| 
      
 80 
     | 
    
         
            +
                    # @param content [String] either "oui" or "non"
         
     | 
| 
      
 81 
     | 
    
         
            +
                    # @return [Boolean]
         
     | 
| 
      
 82 
     | 
    
         
            +
                    def b_from(content)
         
     | 
| 
      
 83 
     | 
    
         
            +
                      case content
         
     | 
| 
      
 84 
     | 
    
         
            +
                        when 'oui' then true
         
     | 
| 
      
 85 
     | 
    
         
            +
                        when 'non' then false
         
     | 
| 
      
 86 
     | 
    
         
            +
                        else fail "`#{content}' is not boolean content"
         
     | 
| 
      
 87 
     | 
    
         
            +
                      end
         
     | 
| 
      
 88 
     | 
    
         
            +
                    end
         
     | 
| 
      
 89 
     | 
    
         
            +
             
     | 
| 
      
 90 
     | 
    
         
            +
                    # Build coordinates from content
         
     | 
| 
      
 91 
     | 
    
         
            +
                    #
         
     | 
| 
      
 92 
     | 
    
         
            +
                    # @param content [String] source content
         
     | 
| 
      
 93 
     | 
    
         
            +
                    # @return [AIXM::XY]
         
     | 
| 
      
 94 
     | 
    
         
            +
                    def xy_from(content)
         
     | 
| 
      
 95 
     | 
    
         
            +
                      parts = content.split(/[\s,]+/)
         
     | 
| 
      
 96 
     | 
    
         
            +
                      AIXM.xy(lat: parts[0].to_f, long: parts[1].to_f)
         
     | 
| 
      
 97 
     | 
    
         
            +
                    end
         
     | 
| 
      
 98 
     | 
    
         
            +
             
     | 
| 
      
 99 
     | 
    
         
            +
                    # Build altitude/elevation from value and unit
         
     | 
| 
      
 100 
     | 
    
         
            +
                    #
         
     | 
| 
      
 101 
     | 
    
         
            +
                    # @param value [String, Numeric, nil] numeric value
         
     | 
| 
      
 102 
     | 
    
         
            +
                    # @param unit [String] unit like "ft ASFC" or absolute like "SFC"
         
     | 
| 
      
 103 
     | 
    
         
            +
                    # @return [AIXM::Z]
         
     | 
| 
      
 104 
     | 
    
         
            +
                    def z_from(value: nil, unit: 'ft ASFC')
         
     | 
| 
      
 105 
     | 
    
         
            +
                      if value
         
     | 
| 
      
 106 
     | 
    
         
            +
                        case unit
         
     | 
| 
      
 107 
     | 
    
         
            +
                          when 'SFC' then AIXM::GROUND
         
     | 
| 
      
 108 
     | 
    
         
            +
                          when 'UNL' then AIXM::UNLIMITED
         
     | 
| 
      
 109 
     | 
    
         
            +
                          when 'ft ASFC' then AIXM.z(value.to_i, :qfe)
         
     | 
| 
      
 110 
     | 
    
         
            +
                          when 'ft AMSL' then AIXM.z(value.to_i, :qnh)
         
     | 
| 
      
 111 
     | 
    
         
            +
                          when 'FL' then AIXM.z(value.to_i, :qne)
         
     | 
| 
      
 112 
     | 
    
         
            +
                          else fail "z `#{[value, unit].join(' ')}' not recognized"
         
     | 
| 
      
 113 
     | 
    
         
            +
                        end
         
     | 
| 
      
 114 
     | 
    
         
            +
                      end
         
     | 
| 
      
 115 
     | 
    
         
            +
                    end
         
     | 
| 
      
 116 
     | 
    
         
            +
             
     | 
| 
      
 117 
     | 
    
         
            +
                    # Build distance from content
         
     | 
| 
      
 118 
     | 
    
         
            +
                    #
         
     | 
| 
      
 119 
     | 
    
         
            +
                    # @param content [String] source content
         
     | 
| 
      
 120 
     | 
    
         
            +
                    # @return [AIXM::D]
         
     | 
| 
      
 121 
     | 
    
         
            +
                    def d_from(content)
         
     | 
| 
      
 122 
     | 
    
         
            +
                      parts = content.split(/\s/)
         
     | 
| 
      
 123 
     | 
    
         
            +
                      AIXM.d(parts[0].to_f, parts[1])
         
     | 
| 
      
 124 
     | 
    
         
            +
                    end
         
     | 
| 
      
 125 
     | 
    
         
            +
             
     | 
| 
      
 126 
     | 
    
         
            +
                    # Build geometry from content
         
     | 
| 
      
 127 
     | 
    
         
            +
                    #
         
     | 
| 
      
 128 
     | 
    
         
            +
                    # @param content [String] source content
         
     | 
| 
      
 129 
     | 
    
         
            +
                    # @return [AIXM::Component::Geometry]
         
     | 
| 
      
 130 
     | 
    
         
            +
                    def geometry_from(content)
         
     | 
| 
      
 131 
     | 
    
         
            +
                      AIXM.geometry.tap do |geometry|
         
     | 
| 
      
 132 
     | 
    
         
            +
                        buffer = {}
         
     | 
| 
      
 133 
     | 
    
         
            +
                        content.split("\n").each do |element|
         
     | 
| 
      
 134 
     | 
    
         
            +
                          parts = element.split(',', 3).last.split(/[():,]/)
         
     | 
| 
      
 135 
     | 
    
         
            +
                          # Write explicit geometry from previous iteration
         
     | 
| 
      
 136 
     | 
    
         
            +
                          if (bordure_name, xy = buffer.delete(:fnt))
         
     | 
| 
      
 137 
     | 
    
         
            +
                            border = borders[bordure_name]
         
     | 
| 
      
 138 
     | 
    
         
            +
                            geometry.add_segments border.segment(
         
     | 
| 
      
 139 
     | 
    
         
            +
                              from_position: border.nearest(xy: xy),
         
     | 
| 
      
 140 
     | 
    
         
            +
                              to_position: border.nearest(xy: xy_from(parts[0]))
         
     | 
| 
      
 141 
     | 
    
         
            +
                            ).map(&:to_point)
         
     | 
| 
      
 142 
     | 
    
         
            +
                          end
         
     | 
| 
      
 143 
     | 
    
         
            +
                          # Write current iteration
         
     | 
| 
      
 144 
     | 
    
         
            +
                          geometry.add_segment(
         
     | 
| 
      
 145 
     | 
    
         
            +
                            case parts[1]
         
     | 
| 
      
 146 
     | 
    
         
            +
                            when 'grc'
         
     | 
| 
      
 147 
     | 
    
         
            +
                              AIXM.point(
         
     | 
| 
      
 148 
     | 
    
         
            +
                                xy: xy_from(parts[0])
         
     | 
| 
      
 149 
     | 
    
         
            +
                              )
         
     | 
| 
      
 150 
     | 
    
         
            +
                            when 'rhl'
         
     | 
| 
      
 151 
     | 
    
         
            +
                              AIXM.rhumb_line(
         
     | 
| 
      
 152 
     | 
    
         
            +
                                xy: xy_from(parts[0])
         
     | 
| 
      
 153 
     | 
    
         
            +
                              )
         
     | 
| 
      
 154 
     | 
    
         
            +
                            when 'cwa', 'cca'
         
     | 
| 
      
 155 
     | 
    
         
            +
                              AIXM.arc(
         
     | 
| 
      
 156 
     | 
    
         
            +
                                xy: xy_from(parts[0]),
         
     | 
| 
      
 157 
     | 
    
         
            +
                                center_xy: xy_from(parts[5]),
         
     | 
| 
      
 158 
     | 
    
         
            +
                                clockwise: (parts[1] == 'cwa')
         
     | 
| 
      
 159 
     | 
    
         
            +
                              )
         
     | 
| 
      
 160 
     | 
    
         
            +
                            when 'cir'
         
     | 
| 
      
 161 
     | 
    
         
            +
                              AIXM.circle(
         
     | 
| 
      
 162 
     | 
    
         
            +
                                center_xy: xy_from(parts[0]),
         
     | 
| 
      
 163 
     | 
    
         
            +
                                radius: d_from(parts[3..4].join(' '))
         
     | 
| 
      
 164 
     | 
    
         
            +
                              )
         
     | 
| 
      
 165 
     | 
    
         
            +
                            when 'fnt'
         
     | 
| 
      
 166 
     | 
    
         
            +
                              bordure = cache.bordure.at_css(%Q(Bordure[pk="#{parts[3]}"]))
         
     | 
| 
      
 167 
     | 
    
         
            +
                              bordure_name = bordure.(:Code)
         
     | 
| 
      
 168 
     | 
    
         
            +
                              if bordure_name.match? /:/   # explicit geometry
         
     | 
| 
      
 169 
     | 
    
         
            +
                                borders[bordure_name] ||= AIPP::Border.from_array([bordure.(:Geometrie).split])
         
     | 
| 
      
 170 
     | 
    
         
            +
                                buffer[:fnt] = [bordure_name, xy_from(parts[2])]
         
     | 
| 
      
 171 
     | 
    
         
            +
                                AIXM.point(
         
     | 
| 
      
 172 
     | 
    
         
            +
                                  xy: xy_from(parts[0])
         
     | 
| 
      
 173 
     | 
    
         
            +
                                )
         
     | 
| 
      
 174 
     | 
    
         
            +
                              else
         
     | 
| 
      
 175 
     | 
    
         
            +
                                AIXM.border(   # named border
         
     | 
| 
      
 176 
     | 
    
         
            +
                                  xy: xy_from(parts[0]),
         
     | 
| 
      
 177 
     | 
    
         
            +
                                  name: bordure_name
         
     | 
| 
      
 178 
     | 
    
         
            +
                                )
         
     | 
| 
      
 179 
     | 
    
         
            +
                              end
         
     | 
| 
      
 180 
     | 
    
         
            +
                            else
         
     | 
| 
      
 181 
     | 
    
         
            +
                              fail "geometry `#{parts[1]}' not recognized"
         
     | 
| 
      
 182 
     | 
    
         
            +
                            end
         
     | 
| 
      
 183 
     | 
    
         
            +
                          )
         
     | 
| 
      
 184 
     | 
    
         
            +
                        end
         
     | 
| 
      
 185 
     | 
    
         
            +
                      end
         
     | 
| 
      
 186 
     | 
    
         
            +
                    end
         
     | 
| 
      
 187 
     | 
    
         
            +
             
     | 
| 
      
 188 
     | 
    
         
            +
                    # Build timetable from content
         
     | 
| 
      
 189 
     | 
    
         
            +
                    #
         
     | 
| 
      
 190 
     | 
    
         
            +
                    # @param content [String] source content
         
     | 
| 
      
 191 
     | 
    
         
            +
                    # @return [AIXM::Component::Timetable]
         
     | 
| 
      
 192 
     | 
    
         
            +
                    def timetable_from(content)
         
     | 
| 
      
 193 
     | 
    
         
            +
                      AIXM.timetable(code: content) if AIXM::H_RE.match? content
         
     | 
| 
      
 194 
     | 
    
         
            +
                    end
         
     | 
| 
      
 195 
     | 
    
         
            +
             
     | 
| 
      
 196 
     | 
    
         
            +
                    # Build layer from "volume" node
         
     | 
| 
      
 197 
     | 
    
         
            +
                    #
         
     | 
| 
      
 198 
     | 
    
         
            +
                    # @param volume_node [Nokogiri::XML::Element] source node
         
     | 
| 
      
 199 
     | 
    
         
            +
                    # @return [AIXM::Component::Layer]
         
     | 
| 
      
 200 
     | 
    
         
            +
                    def layer_from(volume_node)
         
     | 
| 
      
 201 
     | 
    
         
            +
                      AIXM.layer(
         
     | 
| 
      
 202 
     | 
    
         
            +
                        class: volume_node.(:Classe),
         
     | 
| 
      
 203 
     | 
    
         
            +
                        vertical_limit: AIXM.vertical_limit(
         
     | 
| 
      
 204 
     | 
    
         
            +
                          upper_z: z_from(value: volume_node.(:Plafond), unit: volume_node.(:PlafondRefUnite)),
         
     | 
| 
      
 205 
     | 
    
         
            +
                          max_z: z_from(value: volume_node.(:Plafond2)),
         
     | 
| 
      
 206 
     | 
    
         
            +
                          lower_z: z_from(value: volume_node.(:Plancher), unit: volume_node.(:PlancherRefUnite)),
         
     | 
| 
      
 207 
     | 
    
         
            +
                          min_z: z_from(value: volume_node.(:Plancher2))
         
     | 
| 
      
 208 
     | 
    
         
            +
                        )
         
     | 
| 
      
 209 
     | 
    
         
            +
                      ).tap do |layer|
         
     | 
| 
      
 210 
     | 
    
         
            +
                        layer.timetable = timetable_from(volume_node.(:HorCode))
         
     | 
| 
      
 211 
     | 
    
         
            +
                        layer.remarks = volume_node.(:Remarque)
         
     | 
| 
      
 212 
     | 
    
         
            +
                      end
         
     | 
| 
      
 213 
     | 
    
         
            +
                    end
         
     | 
| 
      
 214 
     | 
    
         
            +
             
     | 
| 
      
 215 
     | 
    
         
            +
                  end
         
     | 
| 
      
 216 
     | 
    
         
            +
                end
         
     | 
| 
      
 217 
     | 
    
         
            +
              end
         
     | 
| 
      
 218 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,49 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module AIPP
         
     | 
| 
      
 2 
     | 
    
         
            +
              module LF
         
     | 
| 
      
 3 
     | 
    
         
            +
                module Helpers
         
     | 
| 
      
 4 
     | 
    
         
            +
                  module Surface
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
                    # Map surface to OFMX composition, preparation and remarks
         
     | 
| 
      
 7 
     | 
    
         
            +
                    SURFACES = {
         
     | 
| 
      
 8 
     | 
    
         
            +
                      /^revêtue?$/ => { preparation: :paved },
         
     | 
| 
      
 9 
     | 
    
         
            +
                      /^non revêtue?$/ => { preparation: :natural },
         
     | 
| 
      
 10 
     | 
    
         
            +
                      'macadam' => { composition: :macadam },
         
     | 
| 
      
 11 
     | 
    
         
            +
                      /^bitume ?(traité|psp)?$/ =>  { composition: :bitumen },
         
     | 
| 
      
 12 
     | 
    
         
            +
                      'ciment' => { composition: :concrete, preparation: :paved },
         
     | 
| 
      
 13 
     | 
    
         
            +
                      /^b[eéè]ton ?(armé|bitume|bitumeux|bitumineux)?$/ => { composition: :concrete, preparation: :paved },
         
     | 
| 
      
 14 
     | 
    
         
            +
                      /^béton( de)? ciment$/ => { composition: :concrete, preparation: :paved },
         
     | 
| 
      
 15 
     | 
    
         
            +
                      'béton herbe' => { composition: :concrete_and_grass },
         
     | 
| 
      
 16 
     | 
    
         
            +
                      'béton avec résine' => { composition: :concrete, preparation: :paved, remarks: 'Avec résine / with resin' },
         
     | 
| 
      
 17 
     | 
    
         
            +
                      "béton + asphalte d'étanchéité sablé" => { composition: :concrete_and_asphalt, preparation: :paved, remarks: 'Étanchéité sablé / sandblasted waterproofing' },
         
     | 
| 
      
 18 
     | 
    
         
            +
                      'béton armé + support bitumastic' => { composition: :concrete, preparation: :paved, remarks: 'Support bitumastic / bitumen support' },
         
     | 
| 
      
 19 
     | 
    
         
            +
                      /résine (époxy )?su[er] béton/ => { composition: :concrete, preparation: :paved, remarks: 'Avec couche résine / with resin seal coat' },
         
     | 
| 
      
 20 
     | 
    
         
            +
                      /^(asphalte|tarmac)$/ => { composition: :asphalt, preparation: :paved },
         
     | 
| 
      
 21 
     | 
    
         
            +
                      'enrobé' => { preparation: :other, remarks: 'Enrobé / coated' },
         
     | 
| 
      
 22 
     | 
    
         
            +
                      'enrobé anti-kérozène' => { preparation: :other, remarks: 'Enrobé anti-kérozène / anti-kerosene coating' },
         
     | 
| 
      
 23 
     | 
    
         
            +
                      /^enrobé bitum(e|iné|ineux)$/ => { composition: :bitumen, preparation: :paved, remarks: 'Enrobé / coated' },
         
     | 
| 
      
 24 
     | 
    
         
            +
                      'enrobé béton' => { composition: :concrete, preparation: :paved, remarks: 'Enrobé / coated' },
         
     | 
| 
      
 25 
     | 
    
         
            +
                      /^résine( époxy)?$/ => { composition: :other, remarks: 'Résine / resin' },
         
     | 
| 
      
 26 
     | 
    
         
            +
                      'tole acier larmé' => { composition: :metal, preparation: :grooved },
         
     | 
| 
      
 27 
     | 
    
         
            +
                      /^(structure métallique|structure et caillebotis métallique|aluminium)$/ => { composition: :metal },
         
     | 
| 
      
 28 
     | 
    
         
            +
                      'matériaux composites ignifugés' => { composition: :other, remarks: 'Matériaux composites ignifugés / fire resistant mixed materials' },
         
     | 
| 
      
 29 
     | 
    
         
            +
                      /^(gazon|herbe)$/ => { composition: :grass },
         
     | 
| 
      
 30 
     | 
    
         
            +
                      'neige' => { composition: :snow },
         
     | 
| 
      
 31 
     | 
    
         
            +
                      'neige damée' => { composition: :snow, preparation: :rolled },
         
     | 
| 
      
 32 
     | 
    
         
            +
                      'surface en bois' => { composition: :wood }
         
     | 
| 
      
 33 
     | 
    
         
            +
                    }.freeze
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
                    def surface_from(node)
         
     | 
| 
      
 36 
     | 
    
         
            +
                      AIXM.surface.tap do |surface|
         
     | 
| 
      
 37 
     | 
    
         
            +
                        SURFACES.metch(node.(:Revetement), default: {}).tap do |surface_attributes|
         
     | 
| 
      
 38 
     | 
    
         
            +
                          surface.composition = surface_attributes[:composition]
         
     | 
| 
      
 39 
     | 
    
         
            +
                          surface.preparation = surface_attributes[:preparation]
         
     | 
| 
      
 40 
     | 
    
         
            +
                          surface.remarks = surface_attributes[:remarks]
         
     | 
| 
      
 41 
     | 
    
         
            +
                        end
         
     | 
| 
      
 42 
     | 
    
         
            +
                        surface.pcn = node.(:Resistance)&.first_match(AIXM::PCN_RE)
         
     | 
| 
      
 43 
     | 
    
         
            +
                      end
         
     | 
| 
      
 44 
     | 
    
         
            +
                    end
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
                  end
         
     | 
| 
      
 47 
     | 
    
         
            +
                end
         
     | 
| 
      
 48 
     | 
    
         
            +
              end
         
     | 
| 
      
 49 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,20 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module AIPP
         
     | 
| 
      
 2 
     | 
    
         
            +
              module LF
         
     | 
| 
      
 3 
     | 
    
         
            +
                module Helpers
         
     | 
| 
      
 4 
     | 
    
         
            +
                  module UsageLimitation
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
                    # Map limitation type descriptions to AIXM limitation, realm and remarks
         
     | 
| 
      
 7 
     | 
    
         
            +
                    LIMITATION_TYPES = {
         
     | 
| 
      
 8 
     | 
    
         
            +
                      'OFF' => nil,   # skip decommissioned aerodromes/helistations
         
     | 
| 
      
 9 
     | 
    
         
            +
                      'CAP' => { limitation: :permitted, realm: :civilian },
         
     | 
| 
      
 10 
     | 
    
         
            +
                      'ADM' => { limitation: :permitted, realm: :other, remarks: "Goverment ACFT only / Réservé aux ACFT de l'État" },
         
     | 
| 
      
 11 
     | 
    
         
            +
                      'MIL' => { limitation: :permitted, realm: :military },
         
     | 
| 
      
 12 
     | 
    
         
            +
                      'PRV' => { limitation: :reservation_required, realm: :civilian },
         
     | 
| 
      
 13 
     | 
    
         
            +
                      'RST' => { limitation: :reservation_required, realm: :civilian },
         
     | 
| 
      
 14 
     | 
    
         
            +
                      'TPD' => { limitation: :reservation_required, realm: :civilian }
         
     | 
| 
      
 15 
     | 
    
         
            +
                    }.freeze
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                  end
         
     | 
| 
      
 18 
     | 
    
         
            +
                end
         
     | 
| 
      
 19 
     | 
    
         
            +
              end
         
     | 
| 
      
 20 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,85 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module AIPP
         
     | 
| 
      
 2 
     | 
    
         
            +
              module LF
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
                class NavigationalAids < AIP
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
                  include AIPP::LF::Helpers::Base
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                  SOURCE_TYPES = {
         
     | 
| 
      
 9 
     | 
    
         
            +
                    'DME-ATT' => [:dme],
         
     | 
| 
      
 10 
     | 
    
         
            +
                    'TACAN' => [:tacan],
         
     | 
| 
      
 11 
     | 
    
         
            +
                    'VOR' => [:vor],
         
     | 
| 
      
 12 
     | 
    
         
            +
                    'VOR-DME' => [:vor, :dme],
         
     | 
| 
      
 13 
     | 
    
         
            +
                    'VORTAC' => [:vor, :tacan],
         
     | 
| 
      
 14 
     | 
    
         
            +
                    'NDB' => [:ndb]
         
     | 
| 
      
 15 
     | 
    
         
            +
                  }.freeze
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                  def parse
         
     | 
| 
      
 18 
     | 
    
         
            +
                    SOURCE_TYPES.each do |source_type, (primary_type, secondary_type)|
         
     | 
| 
      
 19 
     | 
    
         
            +
                      verbose_info("processing #{source_type}")
         
     | 
| 
      
 20 
     | 
    
         
            +
                      cache.navfix.css(%Q(NavFix[lk^="[LF][#{source_type} "])).each do |navfix_node|
         
     | 
| 
      
 21 
     | 
    
         
            +
                        attributes = {
         
     | 
| 
      
 22 
     | 
    
         
            +
                          source: source(section: 'ENR', position: navfix_node.line),
         
     | 
| 
      
 23 
     | 
    
         
            +
                          organisation: organisation_lf,
         
     | 
| 
      
 24 
     | 
    
         
            +
                          id: navfix_node.(:Ident),
         
     | 
| 
      
 25 
     | 
    
         
            +
                          xy: xy_from(navfix_node.(:Geometrie))
         
     | 
| 
      
 26 
     | 
    
         
            +
                        }
         
     | 
| 
      
 27 
     | 
    
         
            +
                        if radionav_node = cache.radionav.at_css(%Q(RadioNav:has(NavFix[pk="#{navfix_node.attr(:pk)}"])))
         
     | 
| 
      
 28 
     | 
    
         
            +
                          attributes.merge! send(primary_type, radionav_node)
         
     | 
| 
      
 29 
     | 
    
         
            +
                          add(
         
     | 
| 
      
 30 
     | 
    
         
            +
                            AIXM.send(primary_type, **attributes).tap do |navigational_aid|
         
     | 
| 
      
 31 
     | 
    
         
            +
                              navigational_aid.name = radionav_node.(:NomPhraseo) || radionav_node.(:Station)
         
     | 
| 
      
 32 
     | 
    
         
            +
                              navigational_aid.timetable = timetable_from(radionav_node.(:HorCode))
         
     | 
| 
      
 33 
     | 
    
         
            +
                              navigational_aid.remarks = {
         
     | 
| 
      
 34 
     | 
    
         
            +
                                "location/situation" => radionav_node.(:Situation),
         
     | 
| 
      
 35 
     | 
    
         
            +
                                "range/portée" => range_from(radionav_node)
         
     | 
| 
      
 36 
     | 
    
         
            +
                              }.to_remarks
         
     | 
| 
      
 37 
     | 
    
         
            +
                              navigational_aid.send("associate_#{secondary_type}") if secondary_type
         
     | 
| 
      
 38 
     | 
    
         
            +
                            end
         
     | 
| 
      
 39 
     | 
    
         
            +
                          )
         
     | 
| 
      
 40 
     | 
    
         
            +
                        else
         
     | 
| 
      
 41 
     | 
    
         
            +
                          verbose_info("skipping incomplete #{source_type} #{attributes[:id]}")
         
     | 
| 
      
 42 
     | 
    
         
            +
                        end
         
     | 
| 
      
 43 
     | 
    
         
            +
                      end
         
     | 
| 
      
 44 
     | 
    
         
            +
                    end
         
     | 
| 
      
 45 
     | 
    
         
            +
                  end
         
     | 
| 
      
 46 
     | 
    
         
            +
             
     | 
| 
      
 47 
     | 
    
         
            +
                  private
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
                  def dme(radionav_node)
         
     | 
| 
      
 50 
     | 
    
         
            +
                    {
         
     | 
| 
      
 51 
     | 
    
         
            +
                      ghost_f: AIXM.f(radionav_node.(:Frequence).to_f, :mhz),
         
     | 
| 
      
 52 
     | 
    
         
            +
                      z: AIXM.z(radionav_node.(:AltitudeFt).to_i, :qnh)
         
     | 
| 
      
 53 
     | 
    
         
            +
                    }
         
     | 
| 
      
 54 
     | 
    
         
            +
                  end
         
     | 
| 
      
 55 
     | 
    
         
            +
                  alias_method :tacan, :dme
         
     | 
| 
      
 56 
     | 
    
         
            +
             
     | 
| 
      
 57 
     | 
    
         
            +
                  def vor(radionav_node)
         
     | 
| 
      
 58 
     | 
    
         
            +
                    {
         
     | 
| 
      
 59 
     | 
    
         
            +
                      type: :conventional,
         
     | 
| 
      
 60 
     | 
    
         
            +
                      north: :magnetic,
         
     | 
| 
      
 61 
     | 
    
         
            +
                      name: radionav_node.(:Station),
         
     | 
| 
      
 62 
     | 
    
         
            +
                      f: AIXM.f(radionav_node.(:Frequence).to_f, :mhz),
         
     | 
| 
      
 63 
     | 
    
         
            +
                      z: AIXM.z(radionav_node.(:AltitudeFt).to_i, :qnh),
         
     | 
| 
      
 64 
     | 
    
         
            +
                    }
         
     | 
| 
      
 65 
     | 
    
         
            +
                  end
         
     | 
| 
      
 66 
     | 
    
         
            +
             
     | 
| 
      
 67 
     | 
    
         
            +
                  def ndb(radionav_node)
         
     | 
| 
      
 68 
     | 
    
         
            +
                    {
         
     | 
| 
      
 69 
     | 
    
         
            +
                      type: :en_route,
         
     | 
| 
      
 70 
     | 
    
         
            +
                      f: AIXM.f(radionav_node.(:Frequence).to_f, :khz),
         
     | 
| 
      
 71 
     | 
    
         
            +
                      z: AIXM.z(radionav_node.(:AltitudeFt).to_i, :qnh)
         
     | 
| 
      
 72 
     | 
    
         
            +
                    }
         
     | 
| 
      
 73 
     | 
    
         
            +
                  end
         
     | 
| 
      
 74 
     | 
    
         
            +
             
     | 
| 
      
 75 
     | 
    
         
            +
                  def range_from(radionav_node)
         
     | 
| 
      
 76 
     | 
    
         
            +
                    [
         
     | 
| 
      
 77 
     | 
    
         
            +
                      radionav_node.(:Portee).blank_to_nil&.concat('NM'),
         
     | 
| 
      
 78 
     | 
    
         
            +
                      radionav_node.(:FlPorteeVert).blank_to_nil&.prepend('FL'),
         
     | 
| 
      
 79 
     | 
    
         
            +
                      radionav_node.(:Couverture).blank_to_nil
         
     | 
| 
      
 80 
     | 
    
         
            +
                    ].compact.join(' / ')
         
     | 
| 
      
 81 
     | 
    
         
            +
                  end
         
     | 
| 
      
 82 
     | 
    
         
            +
             
     | 
| 
      
 83 
     | 
    
         
            +
                end
         
     | 
| 
      
 84 
     | 
    
         
            +
              end
         
     | 
| 
      
 85 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,153 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module AIPP
         
     | 
| 
      
 2 
     | 
    
         
            +
              module LF
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
                class Obstacles < AIP
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
                  include AIPP::LF::Helpers::Base
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                  # Map type descriptions to AIXM types and remarks
         
     | 
| 
      
 9 
     | 
    
         
            +
                  TYPES = {
         
     | 
| 
      
 10 
     | 
    
         
            +
                    'Antenne' => [:antenna],
         
     | 
| 
      
 11 
     | 
    
         
            +
                    'Autre' => [:other],
         
     | 
| 
      
 12 
     | 
    
         
            +
                    'Bâtiment' => [:building],
         
     | 
| 
      
 13 
     | 
    
         
            +
                    'Câble' => [:other, 'Cable / Câble'],
         
     | 
| 
      
 14 
     | 
    
         
            +
                    'Centrale thermique' => [:building, 'Thermal power plant / Centrale thermique'],
         
     | 
| 
      
 15 
     | 
    
         
            +
                    "Château d'eau" => [:tower, "Water tower / Château d'eau"],
         
     | 
| 
      
 16 
     | 
    
         
            +
                    'Cheminée' => [:chimney],
         
     | 
| 
      
 17 
     | 
    
         
            +
                    'Derrick' => [:tower, 'Derrick'],
         
     | 
| 
      
 18 
     | 
    
         
            +
                    'Eglise' => [:tower, 'Church / Eglise'],
         
     | 
| 
      
 19 
     | 
    
         
            +
                    'Eolienne' => [:wind_turbine],
         
     | 
| 
      
 20 
     | 
    
         
            +
                    'Eolienne(s)' => [:wind_turbine],
         
     | 
| 
      
 21 
     | 
    
         
            +
                    'Grue' => [:tower, 'Crane / Grue'],
         
     | 
| 
      
 22 
     | 
    
         
            +
                    'Mât' => [:mast],
         
     | 
| 
      
 23 
     | 
    
         
            +
                    'Phare marin' => [:tower, 'Lighthouse / Phare marin'],
         
     | 
| 
      
 24 
     | 
    
         
            +
                    'Pile de pont' => [:other, 'Bridge piers / Pile de pont'],
         
     | 
| 
      
 25 
     | 
    
         
            +
                    'Portique' => [:building, 'Arch / Portique'],
         
     | 
| 
      
 26 
     | 
    
         
            +
                    'Pylône' => [:mast, 'Pylon / Pylône'],
         
     | 
| 
      
 27 
     | 
    
         
            +
                    'Silo' => [:tower, 'Silo'],
         
     | 
| 
      
 28 
     | 
    
         
            +
                    'Terril' => [:other, 'Spoil heap / Teril'],
         
     | 
| 
      
 29 
     | 
    
         
            +
                    'Torchère' => [:chimney, 'Flare / Torchère'],
         
     | 
| 
      
 30 
     | 
    
         
            +
                    'Tour' => [:tower],
         
     | 
| 
      
 31 
     | 
    
         
            +
                    'Treillis métallique' => [:other, 'Metallic grid / Treillis métallique']
         
     | 
| 
      
 32 
     | 
    
         
            +
                  }.freeze
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                  def parse
         
     | 
| 
      
 35 
     | 
    
         
            +
                    if options[:region_options].include? 'lf_obstacles_xlsx'
         
     | 
| 
      
 36 
     | 
    
         
            +
                      info("reading obstacles from XLSX")
         
     | 
| 
      
 37 
     | 
    
         
            +
                      @xlsx = read('Obstacles')
         
     | 
| 
      
 38 
     | 
    
         
            +
                      parse_from_xlsx
         
     | 
| 
      
 39 
     | 
    
         
            +
                    else
         
     | 
| 
      
 40 
     | 
    
         
            +
                      parse_from_xml
         
     | 
| 
      
 41 
     | 
    
         
            +
                    end
         
     | 
| 
      
 42 
     | 
    
         
            +
                  end
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
                  private
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
                  def parse_from_xlsx
         
     | 
| 
      
 47 
     | 
    
         
            +
                    # Build obstacles
         
     | 
| 
      
 48 
     | 
    
         
            +
                    @xlsx.sheet(@xlsx.sheets.find(/^data/i).first).each(
         
     | 
| 
      
 49 
     | 
    
         
            +
                      name: 'IDENTIFICATEUR',
         
     | 
| 
      
 50 
     | 
    
         
            +
                      type: 'TYPE',
         
     | 
| 
      
 51 
     | 
    
         
            +
                      count: 'NOMBRE',
         
     | 
| 
      
 52 
     | 
    
         
            +
                      longitude: 'LONGITUDE DECIMALE',
         
     | 
| 
      
 53 
     | 
    
         
            +
                      latitude: 'LATITUDE DECIMALE',
         
     | 
| 
      
 54 
     | 
    
         
            +
                      elevation: 'ALTITUDE AU SOMMET',
         
     | 
| 
      
 55 
     | 
    
         
            +
                      height: 'HAUTEUR HORS SOL',
         
     | 
| 
      
 56 
     | 
    
         
            +
                      height_unit: 'UNITE',
         
     | 
| 
      
 57 
     | 
    
         
            +
                      horizontal_accuracy: 'PRECISION HORIZONTALE',
         
     | 
| 
      
 58 
     | 
    
         
            +
                      vertical_accuracy: 'PRECISION VERTICALE',
         
     | 
| 
      
 59 
     | 
    
         
            +
                      visibility: 'BALISAGE',
         
     | 
| 
      
 60 
     | 
    
         
            +
                      remarks: 'REMARK',
         
     | 
| 
      
 61 
     | 
    
         
            +
                      effective_on: 'DATE DE MISE EN VIGUEUR'
         
     | 
| 
      
 62 
     | 
    
         
            +
                    ).with_index(0) do |row, index|
         
     | 
| 
      
 63 
     | 
    
         
            +
                      next unless row[:effective_on].to_s.match? /\d{8}/
         
     | 
| 
      
 64 
     | 
    
         
            +
                      type, type_remarks = TYPES.fetch(row[:type])
         
     | 
| 
      
 65 
     | 
    
         
            +
                      count = row[:count].to_i
         
     | 
| 
      
 66 
     | 
    
         
            +
                      obstacle = AIXM.obstacle(
         
     | 
| 
      
 67 
     | 
    
         
            +
                        source: source(section: 'ENR', position: index),
         
     | 
| 
      
 68 
     | 
    
         
            +
                        name: row[:name],
         
     | 
| 
      
 69 
     | 
    
         
            +
                        type: type,
         
     | 
| 
      
 70 
     | 
    
         
            +
                        xy: AIXM.xy(lat: row[:latitude].to_f, long: row[:longitude].to_f),
         
     | 
| 
      
 71 
     | 
    
         
            +
                        z: AIXM.z(row[:elevation].to_i, :qnh)
         
     | 
| 
      
 72 
     | 
    
         
            +
                      ).tap do |obstacle|
         
     | 
| 
      
 73 
     | 
    
         
            +
                        obstacle.height = AIXM.d(row[:height].to_i, row[:height_unit])
         
     | 
| 
      
 74 
     | 
    
         
            +
                        if row[:horizontal_accuracy]
         
     | 
| 
      
 75 
     | 
    
         
            +
                          accuracy = row[:horizontal_accuracy].split
         
     | 
| 
      
 76 
     | 
    
         
            +
                          obstacle.xy_accuracy = AIXM.d(accuracy.first.to_i, accuracy.last)
         
     | 
| 
      
 77 
     | 
    
         
            +
                        end
         
     | 
| 
      
 78 
     | 
    
         
            +
                        if row[:vertical_accuracy]
         
     | 
| 
      
 79 
     | 
    
         
            +
                          accuracy = row[:horizontal_accuracy].split
         
     | 
| 
      
 80 
     | 
    
         
            +
                          obstacle.z_accuracy = AIXM.d(accuracy.first.to_i, accuracy.last)
         
     | 
| 
      
 81 
     | 
    
         
            +
                        end
         
     | 
| 
      
 82 
     | 
    
         
            +
                        obstacle.marking = row[:visibility].match?(/jour/i)
         
     | 
| 
      
 83 
     | 
    
         
            +
                        obstacle.lighting = row[:visibility].match?(/nuit/i)
         
     | 
| 
      
 84 
     | 
    
         
            +
                        obstacle.remarks = {
         
     | 
| 
      
 85 
     | 
    
         
            +
                          'type' => type_remarks,
         
     | 
| 
      
 86 
     | 
    
         
            +
                          'number/nombre' => (count if count > 1),
         
     | 
| 
      
 87 
     | 
    
         
            +
                          'details' => row[:remarks],
         
     | 
| 
      
 88 
     | 
    
         
            +
                          'effective/mise en vigueur' => (row[:effective_on].to_s.unpack("a4a2a2").join("-") if row[:updated_on])
         
     | 
| 
      
 89 
     | 
    
         
            +
                        }.to_remarks
         
     | 
| 
      
 90 
     | 
    
         
            +
                        # Group obstacles
         
     | 
| 
      
 91 
     | 
    
         
            +
                        if aixm.features.find_by(:obstacle, xy: obstacle.xy).any?
         
     | 
| 
      
 92 
     | 
    
         
            +
                          warn("duplicate obstacle #{obstacle.name}", severe: false)
         
     | 
| 
      
 93 
     | 
    
         
            +
                        else
         
     | 
| 
      
 94 
     | 
    
         
            +
                          if count > 1
         
     | 
| 
      
 95 
     | 
    
         
            +
                            obstacle_group = AIXM.obstacle_group(
         
     | 
| 
      
 96 
     | 
    
         
            +
                              source: obstacle.source,
         
     | 
| 
      
 97 
     | 
    
         
            +
                              name: obstacle.name
         
     | 
| 
      
 98 
     | 
    
         
            +
                            ).tap do |obstacle_group|
         
     | 
| 
      
 99 
     | 
    
         
            +
                              obstacle_group.remarks = "#{count} obstacles"
         
     | 
| 
      
 100 
     | 
    
         
            +
                            end
         
     | 
| 
      
 101 
     | 
    
         
            +
                            obstacle_group.add_obstacle obstacle
         
     | 
| 
      
 102 
     | 
    
         
            +
                            add obstacle_group
         
     | 
| 
      
 103 
     | 
    
         
            +
                          else
         
     | 
| 
      
 104 
     | 
    
         
            +
                            add obstacle
         
     | 
| 
      
 105 
     | 
    
         
            +
                          end
         
     | 
| 
      
 106 
     | 
    
         
            +
                        end
         
     | 
| 
      
 107 
     | 
    
         
            +
                      end
         
     | 
| 
      
 108 
     | 
    
         
            +
                    end
         
     | 
| 
      
 109 
     | 
    
         
            +
                  end
         
     | 
| 
      
 110 
     | 
    
         
            +
             
     | 
| 
      
 111 
     | 
    
         
            +
                  def parse_from_xml
         
     | 
| 
      
 112 
     | 
    
         
            +
                    cache.obstacle.css(%Q(Obstacle[lk^="[LF]"])).each do |node|
         
     | 
| 
      
 113 
     | 
    
         
            +
                      # Build obstacles
         
     | 
| 
      
 114 
     | 
    
         
            +
                      type, type_remarks = TYPES.fetch(node.(:TypeObst))
         
     | 
| 
      
 115 
     | 
    
         
            +
                      count = node.(:Combien).to_i
         
     | 
| 
      
 116 
     | 
    
         
            +
                      obstacle = AIXM.obstacle(
         
     | 
| 
      
 117 
     | 
    
         
            +
                        source: source(section: 'ENR', position: node.line),
         
     | 
| 
      
 118 
     | 
    
         
            +
                        name: node.(:NumeroNom),
         
     | 
| 
      
 119 
     | 
    
         
            +
                        type: type,
         
     | 
| 
      
 120 
     | 
    
         
            +
                        xy: xy_from(node.(:Geometrie)),
         
     | 
| 
      
 121 
     | 
    
         
            +
                        z: AIXM.z(node.(:AmslFt).to_i, :qnh)
         
     | 
| 
      
 122 
     | 
    
         
            +
                      ).tap do |obstacle|
         
     | 
| 
      
 123 
     | 
    
         
            +
                        obstacle.height = AIXM.d(node.(:AglFt).to_i, :ft)
         
     | 
| 
      
 124 
     | 
    
         
            +
                        obstacle.marking = node.(:Balisage).match?(/jour/i)
         
     | 
| 
      
 125 
     | 
    
         
            +
                        obstacle.lighting = node.(:Balisage).match?(/nuit/i)
         
     | 
| 
      
 126 
     | 
    
         
            +
                        obstacle.remarks = {
         
     | 
| 
      
 127 
     | 
    
         
            +
                          'type' => type_remarks,
         
     | 
| 
      
 128 
     | 
    
         
            +
                          'number/nombre' => (count if count > 1)
         
     | 
| 
      
 129 
     | 
    
         
            +
                        }.to_remarks
         
     | 
| 
      
 130 
     | 
    
         
            +
                      end
         
     | 
| 
      
 131 
     | 
    
         
            +
                      # Group obstacles
         
     | 
| 
      
 132 
     | 
    
         
            +
                      if aixm.features.find_by(:obstacle, xy: obstacle.xy).any?
         
     | 
| 
      
 133 
     | 
    
         
            +
                        warn("duplicate obstacle #{obstacle.name}", severe: false)
         
     | 
| 
      
 134 
     | 
    
         
            +
                      else
         
     | 
| 
      
 135 
     | 
    
         
            +
                        if count > 1
         
     | 
| 
      
 136 
     | 
    
         
            +
                          obstacle_group = AIXM.obstacle_group(
         
     | 
| 
      
 137 
     | 
    
         
            +
                            source: obstacle.source,
         
     | 
| 
      
 138 
     | 
    
         
            +
                            name: obstacle.name
         
     | 
| 
      
 139 
     | 
    
         
            +
                          ).tap do |obstacle_group|
         
     | 
| 
      
 140 
     | 
    
         
            +
                            obstacle_group.remarks = "#{count} obstacles"
         
     | 
| 
      
 141 
     | 
    
         
            +
                          end
         
     | 
| 
      
 142 
     | 
    
         
            +
                          obstacle_group.add_obstacle obstacle
         
     | 
| 
      
 143 
     | 
    
         
            +
                          add obstacle_group
         
     | 
| 
      
 144 
     | 
    
         
            +
                        else
         
     | 
| 
      
 145 
     | 
    
         
            +
                          add obstacle
         
     | 
| 
      
 146 
     | 
    
         
            +
                        end
         
     | 
| 
      
 147 
     | 
    
         
            +
                      end
         
     | 
| 
      
 148 
     | 
    
         
            +
                    end
         
     | 
| 
      
 149 
     | 
    
         
            +
                  end
         
     | 
| 
      
 150 
     | 
    
         
            +
             
     | 
| 
      
 151 
     | 
    
         
            +
                end
         
     | 
| 
      
 152 
     | 
    
         
            +
              end
         
     | 
| 
      
 153 
     | 
    
         
            +
            end
         
     |