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