aipp 0.2.1 → 0.2.2
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
- data/.ruby-version +1 -1
- data/.travis.yml +1 -2
- data/CHANGELOG.md +15 -0
- data/README.md +122 -37
- data/TODO.md +4 -0
- data/aipp.gemspec +8 -3
- data/lib/aipp.rb +14 -2
- data/lib/aipp/aip.rb +44 -29
- data/lib/aipp/downloader.rb +115 -0
- data/lib/aipp/executable.rb +6 -6
- data/lib/aipp/parser.rb +23 -23
- data/lib/aipp/patcher.rb +47 -0
- data/lib/aipp/pdf.rb +123 -0
- data/lib/aipp/regions/LF/AD-1.3.rb +162 -0
- data/lib/aipp/regions/LF/AD-1.3.yml +511 -0
- data/lib/aipp/regions/LF/AD-1.6.rb +31 -0
- data/lib/aipp/regions/LF/AD-2.rb +316 -0
- data/lib/aipp/regions/LF/AD-2.yml +185 -0
- data/lib/aipp/regions/LF/AD-3.1.rb-NEW +11 -0
- data/lib/aipp/regions/LF/ENR-2.1.rb +25 -24
- data/lib/aipp/regions/LF/ENR-4.1.rb +24 -23
- data/lib/aipp/regions/LF/ENR-4.3.rb +8 -6
- data/lib/aipp/regions/LF/ENR-5.1.rb +32 -22
- data/lib/aipp/regions/LF/ENR-5.5.rb-NEW +11 -0
- data/lib/aipp/regions/LF/helpers/AD_radio.rb +90 -0
- data/lib/aipp/regions/LF/helpers/URL.rb +26 -0
- data/lib/aipp/regions/LF/helpers/common.rb +186 -0
- data/lib/aipp/version.rb +1 -1
- data/lib/core_ext/enumerable.rb +52 -0
- data/lib/core_ext/nil_class.rb +10 -0
- data/lib/core_ext/object.rb +42 -0
- data/lib/core_ext/string.rb +105 -0
- data/spec/fixtures/archive.zip +0 -0
- data/spec/fixtures/document.pdf +0 -0
- data/spec/fixtures/document.pdf.json +1 -0
- data/spec/fixtures/new.html +6 -0
- data/spec/fixtures/new.pdf +0 -0
- data/spec/fixtures/new.txt +1 -0
- data/spec/lib/aipp/downloader_spec.rb +81 -0
- data/spec/lib/aipp/patcher_spec.rb +46 -0
- data/spec/lib/aipp/pdf_spec.rb +124 -0
- data/spec/lib/core_ext/enumberable_spec.rb +76 -0
- data/spec/lib/core_ext/nil_class_spec.rb +11 -0
- data/spec/lib/core_ext/string_spec.rb +88 -0
- data/spec/spec_helper.rb +1 -0
- metadata +123 -23
- data/lib/aipp/progress.rb +0 -40
- data/lib/aipp/refinements.rb +0 -114
- data/lib/aipp/regions/LF/helper.rb +0 -177
- data/spec/lib/aipp/refinements_spec.rb +0 -123
| @@ -0,0 +1,31 @@ | |
| 1 | 
            +
            module AIPP
         | 
| 2 | 
            +
              module LF
         | 
| 3 | 
            +
             | 
| 4 | 
            +
                # Aerodromes radiocommunication facilities (VFR only)
         | 
| 5 | 
            +
                class AD16 < AIP
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                  include AIPP::LF::Helpers::Common
         | 
| 8 | 
            +
                  include AIPP::LF::Helpers::ADRadio
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  DEPENDS = %w(AD-1.3)
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  ID_FIXES = {
         | 
| 13 | 
            +
                    'LF04' => 'LF9004',   # illegal ID as per AIXM
         | 
| 14 | 
            +
                    'LFPY' => nil         # decommissioned - see https://fr.wikipedia.org/wiki/Base_a%C3%A9rienne_217_Br%C3%A9tigny-sur-Orge
         | 
| 15 | 
            +
                  }
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                  def parse
         | 
| 18 | 
            +
                    prepare(html: read).css('tbody').first do |tbody|
         | 
| 19 | 
            +
                      tbody.css('tr').group_by_chunks { |e| e.attr(:id).match?(/-TXT_NAME-/) }.each do |tr, trs|
         | 
| 20 | 
            +
                        id = tr.css('span[id*="CODE_ICAO"]').text.cleanup
         | 
| 21 | 
            +
                        next unless id = ID_FIXES.fetch(id, id)
         | 
| 22 | 
            +
                        @airport = select(:airport, id: id).first
         | 
| 23 | 
            +
                        addresses_from(trs).each { |a| @airport.add_address(a) }
         | 
| 24 | 
            +
                        units_from(trs).each(&method(:write))
         | 
| 25 | 
            +
                      end
         | 
| 26 | 
            +
                    end
         | 
| 27 | 
            +
                  end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
              end
         | 
| 31 | 
            +
            end
         | 
| @@ -0,0 +1,316 @@ | |
| 1 | 
            +
            module AIPP
         | 
| 2 | 
            +
              module LF
         | 
| 3 | 
            +
             | 
| 4 | 
            +
                # Airports (IFR capable) and their CTR, AD navigational aids etc
         | 
| 5 | 
            +
                class AD2 < AIP
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                  include AIPP::LF::Helpers::Common
         | 
| 8 | 
            +
                  include AIPP::LF::Helpers::ADRadio
         | 
| 9 | 
            +
                  using AIXM::Refinements
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  # Map source types to type and optional local type
         | 
| 12 | 
            +
                  SOURCE_TYPES = {
         | 
| 13 | 
            +
                    'CTR' => { type: 'CTR' },
         | 
| 14 | 
            +
                    'RMZ' => { type: 'RAS', local_type: 'RMZ' },
         | 
| 15 | 
            +
                    'TMZ' => { type: 'RAS', local_type: 'TMZ' },
         | 
| 16 | 
            +
                    'RMZ-TMZ' => { type: 'RAS', local_type: 'RMZ-TMZ' }
         | 
| 17 | 
            +
                  }.freeze
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  # Airports without VAC (e.g. military installations)
         | 
| 20 | 
            +
                  NO_VAC = %w(LFOA LFBC LFQE LFOE LFSX LFBM LFSO LFMO LFQP LFSI LFKS LFPV).freeze
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                  # Airports without VFR reporting points
         | 
| 23 | 
            +
                  # TODO: designated points on map but no list (LFLD LFSN LFBS) or no AD info (LFRL)
         | 
| 24 | 
            +
                  NO_DESIGNATED_POINTS = %w(LFAB LFAC LFAV LFAY LFBK LFBN LFBX LFCC LFCI LFCK LFCY LFDH LFDJ LFDN LFEC LFEY LFGA LFHP LFHV LFHY LFJR LFJY LFLA LFLH LFLO LFLV LFLW LFMQ LFMQ LFNB LFOH LFOQ LFOU LFOV LFOZ LFPO LFQA LFQB LFQG LFQM LFRC LFRI LFRM LFRT LFRU LFSD LFSG LFSM LFLD LFSN LFBS LFRL).freeze
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                  # Map synonyms for +correlate+
         | 
| 27 | 
            +
                  SYNONYMS = [
         | 
| 28 | 
            +
                    'nord', 'north',
         | 
| 29 | 
            +
                    'est', 'east',
         | 
| 30 | 
            +
                    'sud', 'south',
         | 
| 31 | 
            +
                    'ouest', 'west',
         | 
| 32 | 
            +
                    'inst', 'instruction',
         | 
| 33 | 
            +
                    'junction', 'intersection',
         | 
| 34 | 
            +
                    'harbour', 'port',
         | 
| 35 | 
            +
                    'mouth', 'embouchure',
         | 
| 36 | 
            +
                    'tower', 'chateau'
         | 
| 37 | 
            +
                  ].freeze
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                  def parse
         | 
| 40 | 
            +
                    index_html = prepare(html: read("AD-0.6"))   # index for AD-2.xxxx files
         | 
| 41 | 
            +
                    index_html.css('#AD-0\.6\.eAIP > .toc-block:nth-of-type(3) .toc-block a').each do |a|
         | 
| 42 | 
            +
                      @id = a.attribute('href').value[-4,4]
         | 
| 43 | 
            +
                      begin
         | 
| 44 | 
            +
                        aip_file = "AD-2.#{@id}"
         | 
| 45 | 
            +
                        html = prepare(html: read(aip_file))
         | 
| 46 | 
            +
                        # Airport
         | 
| 47 | 
            +
                        @remarks = []
         | 
| 48 | 
            +
                        @airport = AIXM.airport(
         | 
| 49 | 
            +
                          source: source(position: html.css('tr[id*="CODE_ICAO"]').first.line, aip_file: aip_file),
         | 
| 50 | 
            +
                          organisation: organisation_lf,   # TODO: not yet implemented
         | 
| 51 | 
            +
                          id: @id,
         | 
| 52 | 
            +
                          name: html.css('tr[id*="CODE_ICAO"] td span:nth-of-type(2)').text.uptrans,
         | 
| 53 | 
            +
                          xy: xy_from(html.css('#AD-2\.2-Position_Geo_Arp td:nth-of-type(3)').text)
         | 
| 54 | 
            +
                        ).tap do |airport|
         | 
| 55 | 
            +
                          airport.z = elevation_from(html.css('#AD-2\.2-Altitude_Reference td:nth-of-type(3)').text)
         | 
| 56 | 
            +
                          airport.declination = declination_from(html.css('#AD-2\.2-Declinaison_Magnetique td:nth-of-type(3)').text)
         | 
| 57 | 
            +
                          airport.transition_z = AIXM.z(5000, :qnh)   # TODO: default - exceptions may exist
         | 
| 58 | 
            +
                          airport.timetable = timetable_from(html.css('#AD-2\.3-Gestionnaire_AD td:nth-of-type(3)').text)
         | 
| 59 | 
            +
                        end
         | 
| 60 | 
            +
                        runways_from(html.css('div[id*="-AD-2\.12"] tbody')).each { |r| @airport.add_runway(r) if r }
         | 
| 61 | 
            +
                        helipads_from(html.css('div[id*="-AD-2\.16"] tbody')).each { |h| @airport.add_helipad(h) if h }
         | 
| 62 | 
            +
                        text = html.css('#AD-2\.2-Observations td:nth-of-type(3)').text
         | 
| 63 | 
            +
                        @airport.remarks = ([remarks_from(text)] + @remarks).compact.join("\n\n").blank_to_nil
         | 
| 64 | 
            +
                        write @airport
         | 
| 65 | 
            +
                        # Airspaces
         | 
| 66 | 
            +
                        airspaces_from(html.css('div[id*="-AD-2\.17"] tbody')).each(&method(:write))
         | 
| 67 | 
            +
                        # Radio
         | 
| 68 | 
            +
                        trs = html.css('div[id*="-AD-2\.18"] tbody tr')
         | 
| 69 | 
            +
                        addresses_from(trs).each { |a| @airport.add_address(a) }
         | 
| 70 | 
            +
                        units_from(trs).each(&method(:write))
         | 
| 71 | 
            +
                        # Landing aids
         | 
| 72 | 
            +
                        # TODO: LOC/GP/DME as of section 2.19
         | 
| 73 | 
            +
                        # Designated points
         | 
| 74 | 
            +
                        unless NO_VAC.include?(@id) || NO_DESIGNATED_POINTS.include?(@id)
         | 
| 75 | 
            +
                          pdf = read("VAC-#{@id}")
         | 
| 76 | 
            +
                          designated_points_from(pdf).tap do |designated_points|
         | 
| 77 | 
            +
                            fix_designated_point_remarks(designated_points)
         | 
| 78 | 
            +
            #               debug(designated_points)
         | 
| 79 | 
            +
                            designated_points.each(&method(:write))
         | 
| 80 | 
            +
                          end
         | 
| 81 | 
            +
                        end
         | 
| 82 | 
            +
                      rescue => error
         | 
| 83 | 
            +
                        warn("error parsing airport #{@id}: #{error.message}", pry: error)
         | 
| 84 | 
            +
                      end
         | 
| 85 | 
            +
                    end
         | 
| 86 | 
            +
                  end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                  private
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                  def elevation_from(text)
         | 
| 91 | 
            +
                    value, unit = text.strip.split
         | 
| 92 | 
            +
                    AIXM.z(AIXM.d(value.to_i, unit).to_ft.dist, :qnh)
         | 
| 93 | 
            +
                  end
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                  def declination_from(text)
         | 
| 96 | 
            +
                    value, direction = text.strip.split('°')
         | 
| 97 | 
            +
                    value = value.to_f * (direction == 'W' ? -1 : 1)
         | 
| 98 | 
            +
                  end
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                  def remarks_from(text)
         | 
| 101 | 
            +
                    text.sub(/NIL|\(\*\)\s+/, '').strip.gsub(/(\s)\s+/, '\1').blank_to_nil
         | 
| 102 | 
            +
                  end
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                  def runways_from(tbody)
         | 
| 105 | 
            +
                    directions_map = tbody.css('tr[id*="TXT_DESIG"]').map do |tr|
         | 
| 106 | 
            +
                      [AIXM.a(tr.css('td:first-of-type').text.strip), tr]
         | 
| 107 | 
            +
                    end.to_h
         | 
| 108 | 
            +
                    remarks_map = tbody.css('tr[id*="TXT_RMK_NAT"]').map do |tr|
         | 
| 109 | 
            +
                      [tr.text.strip[/\A\((\d+)\)/, 1].to_i, tr.css('span')]
         | 
| 110 | 
            +
                    end.to_h
         | 
| 111 | 
            +
                    directions = directions_map.keys
         | 
| 112 | 
            +
                    grouped_directions = directions.map do |direction|
         | 
| 113 | 
            +
                      inverted_direction = direction.invert
         | 
| 114 | 
            +
                      if directions.include? inverted_direction
         | 
| 115 | 
            +
                        [direction, inverted_direction].map(&:to_s).sort.join('/')
         | 
| 116 | 
            +
                      else
         | 
| 117 | 
            +
                        direction.to_s
         | 
| 118 | 
            +
                      end
         | 
| 119 | 
            +
                    end.uniq
         | 
| 120 | 
            +
                    grouped_directions.map do |runway_name|
         | 
| 121 | 
            +
                      AIXM.runway(name: runway_name).tap do |runway|
         | 
| 122 | 
            +
                        %i(forth back).each do |direction_attr|
         | 
| 123 | 
            +
                          if direction = runway.send(direction_attr)
         | 
| 124 | 
            +
                            tr = directions_map[direction.name]
         | 
| 125 | 
            +
                            if direction_attr == :forth
         | 
| 126 | 
            +
                              length, width = tr.css('td:nth-of-type(3)').text.strip.split('x')
         | 
| 127 | 
            +
                              runway.length = AIXM.d(length.strip.to_i, :m)
         | 
| 128 | 
            +
                              runway.width = AIXM.d(width.strip.to_i, :m)
         | 
| 129 | 
            +
                              text = tr.css('td:nth-of-type(5)').text.strip.split(%r<\W+/\W+>).first
         | 
| 130 | 
            +
                              runway.surface.composition = COMPOSITIONS.fetch(text)[:composition]
         | 
| 131 | 
            +
                              runway.surface.preparation = COMPOSITIONS.fetch(text)[:preparation]
         | 
| 132 | 
            +
                              if (text = tr.css('td:nth-of-type(4)').text).match?(AIXM::PCN_RE)
         | 
| 133 | 
            +
                                runway.surface.pcn = text
         | 
| 134 | 
            +
                              end
         | 
| 135 | 
            +
                            end
         | 
| 136 | 
            +
                            text = tr.css('td:nth-of-type(6)').text.strip
         | 
| 137 | 
            +
                            direction.xy = (xy_from(text) unless text.match?(/\A(\(.*)?\z/m))
         | 
| 138 | 
            +
                            if (text = tr.css('td:nth-of-type(7)').text.strip[/thr:\s+(\d+\s+\w+)/i, 1]).present?
         | 
| 139 | 
            +
                              direction.z = elevation_from(text)
         | 
| 140 | 
            +
                            end
         | 
| 141 | 
            +
                            if (text = tr.css('td:nth-of-type(2)').text.strip.sub(/\A(\d+).*$/m, '\1')).present?
         | 
| 142 | 
            +
                              direction.geographic_orientation = AIXM.a(text.to_i)
         | 
| 143 | 
            +
                            end
         | 
| 144 | 
            +
                            if (text = tr.css('td:nth-of-type(6)').text[/\((.+)\)/m, 1]).present?
         | 
| 145 | 
            +
                              direction.displaced_threshold = xy_from(text)
         | 
| 146 | 
            +
                            end
         | 
| 147 | 
            +
                            if (text = tr.css('td:nth-of-type(10)').text.strip[/\A\((\d+)\)/, 1]).present?
         | 
| 148 | 
            +
                              direction.remarks = remarks_from(remarks_map.fetch(text.to_i).text)
         | 
| 149 | 
            +
                            end
         | 
| 150 | 
            +
                          end
         | 
| 151 | 
            +
                        end
         | 
| 152 | 
            +
                      end
         | 
| 153 | 
            +
                    end
         | 
| 154 | 
            +
                  end
         | 
| 155 | 
            +
             | 
| 156 | 
            +
                  def helipads_from(tbody)
         | 
| 157 | 
            +
                    text_fr = tbody.css('td:nth-of-type(3)').text.compact
         | 
| 158 | 
            +
                    text_en = tbody.css('td:nth-of-type(4)').text.compact
         | 
| 159 | 
            +
                    case text_fr
         | 
| 160 | 
            +
                    when /NIL/, /\A\W*\z/
         | 
| 161 | 
            +
                      []
         | 
| 162 | 
            +
                    when /instructions?\s+twr/i
         | 
| 163 | 
            +
                      @remarks << "HELICOPTER:\nSur instructions TWR.\nOn TWR clearance."
         | 
| 164 | 
            +
                      []
         | 
| 165 | 
            +
                    when AIXM::DMS_RE
         | 
| 166 | 
            +
                      text_fr.scan(AIXM::DMS_RE).each_slice(2).with_index(1).map do |(lat, long), index|
         | 
| 167 | 
            +
                        AIXM.helipad(name: "H#{index}").tap do |helipad|
         | 
| 168 | 
            +
                          helipad.xy = AIXM.xy(lat: lat.first, long: long.first)
         | 
| 169 | 
            +
                        end
         | 
| 170 | 
            +
                      end
         | 
| 171 | 
            +
                    else
         | 
| 172 | 
            +
                      @remarks << ['HELICOPTER:', text_fr.blank_to_nil, text_en.blank_to_nil].compact.join("\n")
         | 
| 173 | 
            +
                      []
         | 
| 174 | 
            +
                    end
         | 
| 175 | 
            +
                  end
         | 
| 176 | 
            +
             | 
| 177 | 
            +
                  def airspaces_from(tbody)
         | 
| 178 | 
            +
                    return [] if tbody.text.blank?
         | 
| 179 | 
            +
                    airspace = nil
         | 
| 180 | 
            +
                    tbody.css('tr').to_enum.with_object([]) do |tr, array|
         | 
| 181 | 
            +
                      if tr.attr(:class) =~ /keep-with-next-row/
         | 
| 182 | 
            +
                        airspace = airspace_from tr
         | 
| 183 | 
            +
                      else
         | 
| 184 | 
            +
                        tds = tr.css('td')
         | 
| 185 | 
            +
                        airspace.geometry = geometry_from tds[0].text
         | 
| 186 | 
            +
                        fail("geometry is not closed") unless airspace.geometry.closed?
         | 
| 187 | 
            +
                        airspace.layers << layer_from(tds[2].text, tds[1].text.strip)
         | 
| 188 | 
            +
                        airspace.layers.first.timetable = timetable_from tds[4].text
         | 
| 189 | 
            +
                        airspace.layers.first.remarks = remarks_from(tds[4].text)
         | 
| 190 | 
            +
                        array << airspace
         | 
| 191 | 
            +
                      end
         | 
| 192 | 
            +
                    end
         | 
| 193 | 
            +
                  end
         | 
| 194 | 
            +
             | 
| 195 | 
            +
                  def airspace_from(tr)
         | 
| 196 | 
            +
                    spans = tr.css(:span)
         | 
| 197 | 
            +
                    source_type = spans[1].text.blank_to_nil
         | 
| 198 | 
            +
                    fail "unknown type `#{source_type}'" unless SOURCE_TYPES.has_key? source_type
         | 
| 199 | 
            +
                    AIXM.airspace(
         | 
| 200 | 
            +
                      name: [spans[2].text, anglicise(name: spans[3]&.text)].compact.join(' '),
         | 
| 201 | 
            +
                      type: SOURCE_TYPES.dig(source_type, :type),
         | 
| 202 | 
            +
                      local_type: SOURCE_TYPES.dig(source_type, :local_type)
         | 
| 203 | 
            +
                    ).tap do |airspace|
         | 
| 204 | 
            +
                      airspace.source = source(position: tr.line)
         | 
| 205 | 
            +
                    end
         | 
| 206 | 
            +
                  end
         | 
| 207 | 
            +
             | 
| 208 | 
            +
                  def designated_points_from(pdf, recursive=false)
         | 
| 209 | 
            +
                    from = (pdf.text =~ /^(.*?coordinates.*?names?)/i)
         | 
| 210 | 
            +
                    return [] if recursive && !from
         | 
| 211 | 
            +
                    warn("no designated points section begin found for #{@id}", pry: binding) unless from
         | 
| 212 | 
            +
                    from += $1.length
         | 
| 213 | 
            +
                    to = from + (pdf.text.from(from) =~ /\n\s*\n\s*\n|^.*(?:ifr|vfr|ad\s*equipment|special\s*activities|training\s*flights|mto\s*minima)/i)
         | 
| 214 | 
            +
                    warn("no designated points section end found for #{@id}", pry: binding) unless to
         | 
| 215 | 
            +
                    from, to = from + pdf.range.min, to + pdf.range.min   # offset when recursive
         | 
| 216 | 
            +
                    buffer = {}
         | 
| 217 | 
            +
                    pdf.from(from).to(to).each_line.with_object([]) do |(line, page, last), designated_points|
         | 
| 218 | 
            +
                      line.remove!(/\u2190/)   # remove arrow symbols
         | 
| 219 | 
            +
                      has_id = $1 if line.sub!(/^\s{,20}([A-Z][A-Z\d ]{1,3})(?=\W)/, '')
         | 
| 220 | 
            +
                      has_xy = line.match?(AIXM::DMS_RE)
         | 
| 221 | 
            +
                      designated_points << designated_point_from(buffer, pdf) if has_id || has_xy
         | 
| 222 | 
            +
                      if has_xy
         | 
| 223 | 
            +
                        2.times { (buffer[:xy] ||= []) << $1 if line.sub!(AIXM::DMS_RE, '') }
         | 
| 224 | 
            +
                        buffer[:xy]&.compact!
         | 
| 225 | 
            +
                        line.remove!(/\d{3,4}\D.+?MTG/)   # remove extra columns (e.g. LFML)
         | 
| 226 | 
            +
                        line.remove!(/[\s#{AIXM::MIN}#{AIXM::SEC}]*[-\u2013]/)   # remove dash between coordinates
         | 
| 227 | 
            +
                      end
         | 
| 228 | 
            +
                      buffer[:page] = page
         | 
| 229 | 
            +
                      buffer[:id] = has_id if has_id
         | 
| 230 | 
            +
                      buffer[:remarks] = [buffer[:remarks], line].join("\n")
         | 
| 231 | 
            +
                      designated_points << designated_point_from(buffer, pdf) if last
         | 
| 232 | 
            +
                    end.compact + designated_points_from(pdf.from(to).to(:end), true)
         | 
| 233 | 
            +
                  end
         | 
| 234 | 
            +
             | 
| 235 | 
            +
                  def designated_point_from(buffer, pdf)
         | 
| 236 | 
            +
                    if buffer[:id] && buffer[:xy]&.size == 2
         | 
| 237 | 
            +
                      buffer[:remarks].gsub!(/ {20}/, "\n")   # recognize empty column space
         | 
| 238 | 
            +
                      buffer[:remarks].remove!(/\(\d+\)/)   # remove footnotes
         | 
| 239 | 
            +
                      buffer[:remarks] = buffer[:remarks].unglue   # separate glued words
         | 
| 240 | 
            +
                      AIXM.designated_point(
         | 
| 241 | 
            +
                        source: source(position: buffer[:page], aip_file: pdf.file.basename('.*').to_s),
         | 
| 242 | 
            +
                        type: :vfr_mandatory_reporting_point,
         | 
| 243 | 
            +
                        id: buffer[:id].remove(/\W/),
         | 
| 244 | 
            +
                        xy: AIXM.xy(lat: buffer[:xy].first, long: buffer[:xy].last)
         | 
| 245 | 
            +
                      ).tap do |designated_point|
         | 
| 246 | 
            +
                        designated_point.airport = @airport
         | 
| 247 | 
            +
                        designated_point.remarks = buffer[:remarks].compact.blank_to_nil
         | 
| 248 | 
            +
                        buffer.clear
         | 
| 249 | 
            +
                      end
         | 
| 250 | 
            +
                    end
         | 
| 251 | 
            +
                  end
         | 
| 252 | 
            +
             | 
| 253 | 
            +
                  # Assign scattered similar remarks to one and the same designated point
         | 
| 254 | 
            +
                  def fix_designated_point_remarks(designated_points)
         | 
| 255 | 
            +
                    one = nil
         | 
| 256 | 
            +
                    designated_points.map do |two|
         | 
| 257 | 
            +
                      if one
         | 
| 258 | 
            +
                        one_lines, two_lines = one.remarks&.lines, two.remarks&.lines
         | 
| 259 | 
            +
                        if one_lines && two_lines
         | 
| 260 | 
            +
                          if one_lines.count > 1 && (line = one_lines.last) !~ %r(\s/\s)
         | 
| 261 | 
            +
                            # Move up
         | 
| 262 | 
            +
                            if line.correlate(remainder = one_lines[0..-2].join, SYNONYMS) < line.correlate(two.remarks)
         | 
| 263 | 
            +
                              two.remarks = [line, two.remarks].join("\n").compact
         | 
| 264 | 
            +
                              one.remarks = remainder.compact
         | 
| 265 | 
            +
                            end
         | 
| 266 | 
            +
                          elsif two_lines.count > 1 && (line = two_lines.first) !~ %r(\s/\s)
         | 
| 267 | 
            +
                            # Move down
         | 
| 268 | 
            +
                            line = two_lines.first
         | 
| 269 | 
            +
                            if line.correlate(remainder = two_lines[1..-1].join, SYNONYMS) < line.correlate(one.remarks)
         | 
| 270 | 
            +
                              one.remarks = [one.remarks, line].join("\n").compact
         | 
| 271 | 
            +
                              two.remarks = remainder.compact
         | 
| 272 | 
            +
                            end
         | 
| 273 | 
            +
                          end
         | 
| 274 | 
            +
                        end
         | 
| 275 | 
            +
                      end
         | 
| 276 | 
            +
                      one = two
         | 
| 277 | 
            +
                    end.map do |designated_point|
         | 
| 278 | 
            +
                      designated_point.remarks = designated_point.remarks&.cleanup.blank_to_nil
         | 
| 279 | 
            +
                    end
         | 
| 280 | 
            +
                  end
         | 
| 281 | 
            +
             | 
| 282 | 
            +
            #     def debug(dp)
         | 
| 283 | 
            +
            #       f = "/Users/sschwyn/Desktop/okay/#{@id}.txt"
         | 
| 284 | 
            +
            #       result = "\n--- #{@id} ---\n\n".red
         | 
| 285 | 
            +
            #       dp.each do |d|
         | 
| 286 | 
            +
            #         result += d.id.red + "\t#{d.xy.lat} - #{d.xy.long}\n"
         | 
| 287 | 
            +
            #         result += "#{d.remarks}\n\n".blue
         | 
| 288 | 
            +
            #       end
         | 
| 289 | 
            +
            #       result += "#{dp.count} point(s) for #{@id}".red
         | 
| 290 | 
            +
            #       unless File.exist?(f) && result == File.read(f)
         | 
| 291 | 
            +
            #         puts result
         | 
| 292 | 
            +
            #         gets
         | 
| 293 | 
            +
            #         puts "\e[H\e[2J"
         | 
| 294 | 
            +
            #       end
         | 
| 295 | 
            +
            #       File.write(f, result)
         | 
| 296 | 
            +
            #     end
         | 
| 297 | 
            +
             | 
| 298 | 
            +
                  patch AIXM::Component::Runway::Direction, :xy do |parser, object, value|
         | 
| 299 | 
            +
                    throw :abort unless value.nil?
         | 
| 300 | 
            +
                    @fixtures ||= YAML.load_file(Pathname(__FILE__).dirname.join('AD-2.yml'))
         | 
| 301 | 
            +
                    airport_id = parser.instance_variable_get(:@airport).id
         | 
| 302 | 
            +
                    direction_name = object.name.to_s
         | 
| 303 | 
            +
                    throw :abort if (xy = @fixtures.dig('runways', airport_id, direction_name, 'xy')).nil?
         | 
| 304 | 
            +
                    lat, long = xy.split(/\s+/)
         | 
| 305 | 
            +
                    AIXM.xy(lat: lat, long: long)
         | 
| 306 | 
            +
                  end
         | 
| 307 | 
            +
             | 
| 308 | 
            +
                  patch AIXM::Feature::NavigationalAid, :remarks do |parser, object, value|
         | 
| 309 | 
            +
                    @fixtures ||= YAML.load_file(Pathname(__FILE__).dirname.join('AD-2.yml'))
         | 
| 310 | 
            +
                    airport_id, designated_point_id = object.airport.id, object.id
         | 
| 311 | 
            +
                    @fixtures.dig('designated_points', airport_id, designated_point_id, 'remarks') || throw(:abort)
         | 
| 312 | 
            +
                  end
         | 
| 313 | 
            +
             | 
| 314 | 
            +
                end
         | 
| 315 | 
            +
              end
         | 
| 316 | 
            +
            end
         | 
| @@ -0,0 +1,185 @@ | |
| 1 | 
            +
            # On Google Maps, click in the middle of the runway beginning, then click on
         | 
| 2 | 
            +
            # the coordinates in the little window displayed at the bottom. The DMS
         | 
| 3 | 
            +
            # coordinates will then appear in the left column ready for copy/paste.
         | 
| 4 | 
            +
            ---
         | 
| 5 | 
            +
            runways:
         | 
| 6 | 
            +
              LFLP:
         | 
| 7 | 
            +
                "04R":
         | 
| 8 | 
            +
                  xy: 45°55'32.1"N 6°05'49.2"E
         | 
| 9 | 
            +
                "22L":
         | 
| 10 | 
            +
                  xy: 45°55'53.4"N 6°06'13.4"E
         | 
| 11 | 
            +
              LFOQ:
         | 
| 12 | 
            +
                "02L":
         | 
| 13 | 
            +
                  xy: 47°40'11.2"N 1°12'14.1"E
         | 
| 14 | 
            +
                "20R":
         | 
| 15 | 
            +
                  xy: 47°40'39.6"N 1°12'30.7"E
         | 
| 16 | 
            +
                "02R":
         | 
| 17 | 
            +
                  xy: 47°40'10.4"N 1°12'17.4"E
         | 
| 18 | 
            +
                "20L":
         | 
| 19 | 
            +
                  xy: 47°40'38.3"N 1°12'33.8"E
         | 
| 20 | 
            +
                "12L":
         | 
| 21 | 
            +
                  xy: 47°40'49.9"N 1°12'02.3"E
         | 
| 22 | 
            +
                "30R":
         | 
| 23 | 
            +
                  xy: 47°40'38.9"N 1°12'36.2"E
         | 
| 24 | 
            +
                "12R":
         | 
| 25 | 
            +
                  xy: 47°40'49.9"N 1°12'02.3"E
         | 
| 26 | 
            +
                "30L":
         | 
| 27 | 
            +
                  xy: 47°40'39.1"N 1°12'28.3"E
         | 
| 28 | 
            +
              LFMK:
         | 
| 29 | 
            +
                "10L":
         | 
| 30 | 
            +
                  xy: 43°13'01.3"N 2°18'19.7"E
         | 
| 31 | 
            +
                "28R":
         | 
| 32 | 
            +
                  xy: 43°12'58.5"N 2°18'54.6"E
         | 
| 33 | 
            +
              LFLH:
         | 
| 34 | 
            +
                "17L":
         | 
| 35 | 
            +
                  xy: 46°49'57.5"N 4°49'14.4"E
         | 
| 36 | 
            +
                "35R":
         | 
| 37 | 
            +
                  xy: 46°49'29.8"N 4°49'21.2"E
         | 
| 38 | 
            +
              LFLB:
         | 
| 39 | 
            +
                "18L":
         | 
| 40 | 
            +
                  xy: 45°38'16.9"N 5°52'52.2"E
         | 
| 41 | 
            +
                "36R":
         | 
| 42 | 
            +
                  xy: 45°37'54.4"N 5°52'54.0"E
         | 
| 43 | 
            +
              LFBG:
         | 
| 44 | 
            +
                "05R":
         | 
| 45 | 
            +
                  xy: 45°38'56.8"N 0°19'07.6"W
         | 
| 46 | 
            +
                "23L":
         | 
| 47 | 
            +
                  xy: 45°39'15.7"N 0°18'36.3"W
         | 
| 48 | 
            +
              LFAB:
         | 
| 49 | 
            +
                "13L":
         | 
| 50 | 
            +
                  xy: 49°53'12.1"N 1°04'40.1"E
         | 
| 51 | 
            +
                "31R":
         | 
| 52 | 
            +
                  xy: 49°52'59.8"N 1°05'06.2"E
         | 
| 53 | 
            +
              LFRL:
         | 
| 54 | 
            +
                "13":
         | 
| 55 | 
            +
                  xy: 48°17'01.8"N 4°26'46.5"W
         | 
| 56 | 
            +
                "31":
         | 
| 57 | 
            +
                  xy: 48°16'48.7"N 4°26'21.5"W
         | 
| 58 | 
            +
              LFRM:
         | 
| 59 | 
            +
                "02R":
         | 
| 60 | 
            +
                  xy: 47°56'40.1"N 0°12'01.6"E
         | 
| 61 | 
            +
                "20L":
         | 
| 62 | 
            +
                  xy: 47°57'09.6"N 0°12'18.0"E
         | 
| 63 | 
            +
              LFSM:
         | 
| 64 | 
            +
                "08L":
         | 
| 65 | 
            +
                  xy: 47°29'10.8"N 6°47'08.0"E
         | 
| 66 | 
            +
                "26R":
         | 
| 67 | 
            +
                  xy: 47°29'15.9"N 6°47'48.2"E
         | 
| 68 | 
            +
              LFBR:
         | 
| 69 | 
            +
                "12L":
         | 
| 70 | 
            +
                  xy: 43°27'08.9"N 1°15'45.3"E
         | 
| 71 | 
            +
                "30R":
         | 
| 72 | 
            +
                  xy: 43°27'04.4"N 1°15'57.6"E
         | 
| 73 | 
            +
                "12R":
         | 
| 74 | 
            +
                  xy: 43°27'02.1"N 1°15'31.3"E
         | 
| 75 | 
            +
                "30L":
         | 
| 76 | 
            +
                  xy: 43°26'50.1"N 1°16'04.0"E
         | 
| 77 | 
            +
              LFSN:
         | 
| 78 | 
            +
                "03R":
         | 
| 79 | 
            +
                  xy: 48°41'15.5"N 6°13'44.5"E
         | 
| 80 | 
            +
                "21L":
         | 
| 81 | 
            +
                  xy: 48°41'31.1"N 6°13'57.3"E
         | 
| 82 | 
            +
              LFQG:
         | 
| 83 | 
            +
                "12L":
         | 
| 84 | 
            +
                  xy: 47°00'19.6"N 3°06'30.2"E
         | 
| 85 | 
            +
                "30R":
         | 
| 86 | 
            +
                  xy: 47°00'03.5"N 3°07'07.8"E
         | 
| 87 | 
            +
              LFOJ:
         | 
| 88 | 
            +
                "04":
         | 
| 89 | 
            +
                  xy: 47°59'24.5"N 1°45'29.9"E
         | 
| 90 | 
            +
                "22":
         | 
| 91 | 
            +
                  xy: 47°59'49.3"N 1°45'58.2"E
         | 
| 92 | 
            +
              LFBX:
         | 
| 93 | 
            +
                "11L":
         | 
| 94 | 
            +
                  xy: 45°11'59.1"N 0°48'34.6"E
         | 
| 95 | 
            +
                "29R":
         | 
| 96 | 
            +
                  xy: 45°11'48.2"N 0°49'10.1"E
         | 
| 97 | 
            +
              LFQA:
         | 
| 98 | 
            +
                "07R":
         | 
| 99 | 
            +
                  xy: 49°12'23.4"N 4°09'03.1"E
         | 
| 100 | 
            +
                "25L":
         | 
| 101 | 
            +
                  xy: 49°12'36.9"N 4°09'50.2"E
         | 
| 102 | 
            +
              LFRN:
         | 
| 103 | 
            +
                "14L":
         | 
| 104 | 
            +
                  xy: 48°04'09.4"N 1°44'18.7"W
         | 
| 105 | 
            +
                "32R":
         | 
| 106 | 
            +
                  xy: 48°03'55.4"N 1°44'02.4"W
         | 
| 107 | 
            +
              LFOP:
         | 
| 108 | 
            +
                "05":
         | 
| 109 | 
            +
                  xy: 49°23'05.4"N 1°11'11.4"E
         | 
| 110 | 
            +
                "23":
         | 
| 111 | 
            +
                  xy: 49°23'26.9"N 1°11'44.1"E
         | 
| 112 | 
            +
              LFSI:
         | 
| 113 | 
            +
                "11L":
         | 
| 114 | 
            +
                  xy: 48°38'11.9"N 4°54'06.0"E
         | 
| 115 | 
            +
                "29R":
         | 
| 116 | 
            +
                  xy: 48°37'57.2"N 4°54'58.1"E
         | 
| 117 | 
            +
              LFMY:
         | 
| 118 | 
            +
                "09":
         | 
| 119 | 
            +
                  xy: 43°36'32.8"N 5°06'33.4"E
         | 
| 120 | 
            +
                "27":
         | 
| 121 | 
            +
                  xy: 43°36'32.3"N 5°07'08.8"E
         | 
| 122 | 
            +
                "16L":
         | 
| 123 | 
            +
                  xy: 43°36'53.1"N 5°06'57.6"E
         | 
| 124 | 
            +
                "34R":
         | 
| 125 | 
            +
                  xy: 43°36'22.9"N 5°07'09.5"E
         | 
| 126 | 
            +
                "16R":
         | 
| 127 | 
            +
                  xy: 43°36'46.4"N 5°06'52.0"E
         | 
| 128 | 
            +
                "34L":
         | 
| 129 | 
            +
                  xy: 43°36'12.1"N 5°07'05.4"E
         | 
| 130 | 
            +
              LFQB:
         | 
| 131 | 
            +
                "05":
         | 
| 132 | 
            +
                  xy: 48°19'01.1"N 4°00'28.2"E
         | 
| 133 | 
            +
                "23":
         | 
| 134 | 
            +
                  xy: 48°19'15.8"N 4°00'56.2"E
         | 
| 135 | 
            +
                "17R":
         | 
| 136 | 
            +
                  xy: 48°19'32.1"N 4°00'44.3"E
         | 
| 137 | 
            +
                "35L":
         | 
| 138 | 
            +
                  xy: 48°19'03.1"N 4°00'49.1"E
         | 
| 139 | 
            +
              LFLU:
         | 
| 140 | 
            +
                "01L":
         | 
| 141 | 
            +
                  xy: 44°54'56.1"N 4°58'10.9"E
         | 
| 142 | 
            +
                "19R":
         | 
| 143 | 
            +
                  xy: 44°55'34.4"N 4°58'18.1"E
         | 
| 144 | 
            +
                "01R":
         | 
| 145 | 
            +
                  xy: 44°55'09.4"N 4°58'17.0"E
         | 
| 146 | 
            +
                "19L":
         | 
| 147 | 
            +
                  xy: 44°55'22.3"N 4°58'19.4"E
         | 
| 148 | 
            +
              LFAV:
         | 
| 149 | 
            +
                "06":
         | 
| 150 | 
            +
                  xy: 50°19'40.0"N 3°27'19.8"E
         | 
| 151 | 
            +
                "24":
         | 
| 152 | 
            +
                  xy: 50°19'49.6"N 3°27'47.5"E
         | 
| 153 | 
            +
              LFRV:
         | 
| 154 | 
            +
                "08":
         | 
| 155 | 
            +
                  xy: 47°43'26.6"N 2°43'51.6"W
         | 
| 156 | 
            +
                "26":
         | 
| 157 | 
            +
                  xy: 47°43'31.0"N 2°43'04.4"W
         | 
| 158 | 
            +
            designated_points:
         | 
| 159 | 
            +
              LFQQ:
         | 
| 160 | 
            +
                SA:
         | 
| 161 | 
            +
                  remarks: "Rond-point à l'Est de PONT A MARCQ, et au Nord du golf.\nTraffic circle East of PONT A MARCQ and North of the golf.\nPont de l’autoroute A1 et pont ligne TGV sur le canal de la\nDEULE."
         | 
| 162 | 
            +
                SW:
         | 
| 163 | 
            +
                  remarks: "Bridge of A1 motorway and bridge of the high speed train\n(TGV) line over the DEULE canal."
         | 
| 164 | 
            +
              LFML:
         | 
| 165 | 
            +
                LP:
         | 
| 166 | 
            +
                  remarks: "St Cannat La Pile"
         | 
| 167 | 
            +
              LFPG:
         | 
| 168 | 
            +
                RH1:
         | 
| 169 | 
            +
                  remarks: "Péage autoroute A1.\nMotorway Toll A1."
         | 
| 170 | 
            +
                RH2:
         | 
| 171 | 
            +
                  remarks: "Est du bourg (radial 181 PGS).\nEast Town (PGS 181 radial)."
         | 
| 172 | 
            +
              LFLN:
         | 
| 173 | 
            +
                S:
         | 
| 174 | 
            +
                  remarks: "Iguerande"
         | 
| 175 | 
            +
                SA:
         | 
| 176 | 
            +
                  remarks: "ABM W ville d’Avrilly, ABM E château d’eau d’Avrilly\nABM W of town of Avrilly, ABM E water tower of Avrilly"
         | 
| 177 | 
            +
                W:
         | 
| 178 | 
            +
                  remarks: "Le Donjon"
         | 
| 179 | 
            +
                WA:
         | 
| 180 | 
            +
                  remarks: "Le Pin, château d’eau au SE de la localité\nLe Pin, water tower SE of the town"
         | 
| 181 | 
            +
              LFBO:
         | 
| 182 | 
            +
                WF:
         | 
| 183 | 
            +
                  remarks: "Lac de la Bordette.Forêt de Bouconne / La Bordette lake.\nBouconne forest"
         | 
| 184 | 
            +
                WD:
         | 
| 185 | 
            +
                  remarks: "Echangeur N124.Zone commerciale En Jacca.A côté de\nLeroy Merlin.\nInterchange N124.Commercial zone En Jacca.Near Leroy\nMerlin."
         |