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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/.travis.yml +1 -2
  4. data/CHANGELOG.md +15 -0
  5. data/README.md +122 -37
  6. data/TODO.md +4 -0
  7. data/aipp.gemspec +8 -3
  8. data/lib/aipp.rb +14 -2
  9. data/lib/aipp/aip.rb +44 -29
  10. data/lib/aipp/downloader.rb +115 -0
  11. data/lib/aipp/executable.rb +6 -6
  12. data/lib/aipp/parser.rb +23 -23
  13. data/lib/aipp/patcher.rb +47 -0
  14. data/lib/aipp/pdf.rb +123 -0
  15. data/lib/aipp/regions/LF/AD-1.3.rb +162 -0
  16. data/lib/aipp/regions/LF/AD-1.3.yml +511 -0
  17. data/lib/aipp/regions/LF/AD-1.6.rb +31 -0
  18. data/lib/aipp/regions/LF/AD-2.rb +316 -0
  19. data/lib/aipp/regions/LF/AD-2.yml +185 -0
  20. data/lib/aipp/regions/LF/AD-3.1.rb-NEW +11 -0
  21. data/lib/aipp/regions/LF/ENR-2.1.rb +25 -24
  22. data/lib/aipp/regions/LF/ENR-4.1.rb +24 -23
  23. data/lib/aipp/regions/LF/ENR-4.3.rb +8 -6
  24. data/lib/aipp/regions/LF/ENR-5.1.rb +32 -22
  25. data/lib/aipp/regions/LF/ENR-5.5.rb-NEW +11 -0
  26. data/lib/aipp/regions/LF/helpers/AD_radio.rb +90 -0
  27. data/lib/aipp/regions/LF/helpers/URL.rb +26 -0
  28. data/lib/aipp/regions/LF/helpers/common.rb +186 -0
  29. data/lib/aipp/version.rb +1 -1
  30. data/lib/core_ext/enumerable.rb +52 -0
  31. data/lib/core_ext/nil_class.rb +10 -0
  32. data/lib/core_ext/object.rb +42 -0
  33. data/lib/core_ext/string.rb +105 -0
  34. data/spec/fixtures/archive.zip +0 -0
  35. data/spec/fixtures/document.pdf +0 -0
  36. data/spec/fixtures/document.pdf.json +1 -0
  37. data/spec/fixtures/new.html +6 -0
  38. data/spec/fixtures/new.pdf +0 -0
  39. data/spec/fixtures/new.txt +1 -0
  40. data/spec/lib/aipp/downloader_spec.rb +81 -0
  41. data/spec/lib/aipp/patcher_spec.rb +46 -0
  42. data/spec/lib/aipp/pdf_spec.rb +124 -0
  43. data/spec/lib/core_ext/enumberable_spec.rb +76 -0
  44. data/spec/lib/core_ext/nil_class_spec.rb +11 -0
  45. data/spec/lib/core_ext/string_spec.rb +88 -0
  46. data/spec/spec_helper.rb +1 -0
  47. metadata +123 -23
  48. data/lib/aipp/progress.rb +0 -40
  49. data/lib/aipp/refinements.rb +0 -114
  50. data/lib/aipp/regions/LF/helper.rb +0 -177
  51. data/spec/lib/aipp/refinements_spec.rb +0 -123
@@ -0,0 +1,115 @@
1
+ module AIPP
2
+
3
+ # AIP downloader infrastructure
4
+ #
5
+ # The downloader operates in the +storage+ directory where it creates two
6
+ # subdirectories "archive" and "work". The initializer looks for +archive+
7
+ # in "archives" and (if found) unzips its contents into "work". When reading
8
+ # a +document+, the downloader looks for the +document+ in "work" and
9
+ # (unless found) downloads it from +url+. HTML documents are parsed to
10
+ # +Nokogiri::HTML5::Document+, PDF documents are parsed to +AIPP::PDF+.
11
+ # Finally, the contents of "work" are written back to +archive+.
12
+ #
13
+ # @example
14
+ # AIPP::Downloader.new(storage: options[:storage], archive: "2018-11-08") do |downloader|
15
+ # html = downloader.read(
16
+ # document: 'ENR-5.1',
17
+ # url: 'https://www.sia.aviation-civile.gouv.fr/dvd/eAIP_08_NOV_2018/FRANCE/AIRAC-2018-11-08/html/eAIP/FR-ENR-5.1-fr-FR.html'
18
+ # )
19
+ # pdf = downloader.read(
20
+ # document: 'VAC-LFMV',
21
+ # url: 'https://www.sia.aviation-civile.gouv.fr/dvd/eAIP_08_NOV_2018/Atlas-VAC/PDF_AIPparSSection/VAC/AD/AD-2.LFMV.pdf'
22
+ # )
23
+ # end
24
+ class Downloader
25
+
26
+ # @return [Pathname] directory to operate within
27
+ attr_reader :storage
28
+
29
+ # @return [String] name of the archive (without extension ".zip")
30
+ attr_reader :archive
31
+
32
+ # @return [Pathname] full path to the archive
33
+ attr_reader :archive_file
34
+
35
+ # @param storage [Pathname] directory to operate within
36
+ # @param archive [String] name of the archive (without extension ".zip")
37
+ def initialize(storage:, archive:)
38
+ @storage, @archive = storage, archive
39
+ fail(ArgumentError, 'bad storage directory') unless Dir.exist? storage
40
+ @archive_file = archives_path.join("#{@archive}.zip")
41
+ prepare
42
+ unzip if @archive_file.exist?
43
+ yield self
44
+ zip
45
+ ensure
46
+ teardown
47
+ end
48
+
49
+ # Download and read +document+
50
+ #
51
+ # @param document [String] document to read (without extension)
52
+ # @param url [String] URL to download the document from
53
+ # @param type [Symbol, nil] document type: +nil+ (default) to derive it from
54
+ # the URL, :html, or :pdf
55
+ # @return [Nokogiri::HTML5::Document, AIPP::PDF]
56
+ def read(document:, url:, type: nil)
57
+ type ||= Pathname(URI(url).path).extname[1..-1].to_sym
58
+ file = work_path.join([document, type].join('.'))
59
+ unless file.exist?
60
+ debug "Downloading #{document}"
61
+ IO.copy_stream(Kernel.open(url), file)
62
+ end
63
+ convert file
64
+ end
65
+
66
+ private
67
+
68
+ def archives_path
69
+ @storage.join('archives')
70
+ end
71
+
72
+ def work_path
73
+ @storage.join('work')
74
+ end
75
+
76
+ def prepare
77
+ teardown
78
+ archives_path.mkpath
79
+ work_path.mkpath
80
+ end
81
+
82
+ def teardown
83
+ if work_path.exist?
84
+ work_path.children.each(&:delete)
85
+ work_path.delete
86
+ end
87
+ end
88
+
89
+ def unzip
90
+ Zip::File.open(archive_file).each do |entry|
91
+ entry.extract(work_path.join(entry.name))
92
+ end
93
+ end
94
+
95
+ def zip
96
+ backup_file = archive_file.sub(/$/, '.old') if archive_file.exist?
97
+ archive_file.rename(backup_file) if backup_file
98
+ Zip::File.open(archive_file, Zip::File::CREATE) do |zip|
99
+ work_path.children.each do |entry|
100
+ zip.add(entry.basename.to_s, entry) unless entry.basename.to_s[0] == '.'
101
+ end
102
+ end
103
+ backup_file&.delete
104
+ end
105
+
106
+ def convert(file)
107
+ case file.extname
108
+ when '.html' then Nokogiri.HTML5(file)
109
+ when '.pdf' then AIPP::PDF.new(file)
110
+ else
111
+ fail(ArgumentError, "invalid document type")
112
+ end
113
+ end
114
+ end
115
+ end
@@ -8,7 +8,7 @@ module AIPP
8
8
  @options = options
9
9
  @options[:airac] = AIPP::AIRAC.new
10
10
  @options[:storage] = Pathname(Dir.home).join('.aipp')
11
- @options[:verbose] = @options[:force] = @options[:pry_on_error] = false
11
+ @options[:force] = $PRY_ON_WARN = $PRY_ON_ERROR = false
12
12
  OptionParser.new do |o|
13
13
  o.banner = <<~END
14
14
  Download online AIP and convert it to #{options[:schema].upcase}.
@@ -18,10 +18,10 @@ module AIPP
18
18
  o.on('-r', '--region STRING', String, 'region (e.g. "LF")') { |v| @options[:region] = v.upcase }
19
19
  o.on('-a', '--aip STRING', String, 'process this AIP only (e.g. "ENR-5.1")') { |v| @options[:aip] = v.upcase }
20
20
  o.on('-s', '--storage DIR', String, 'storage directory (default: "~/.aipp")') { |v| @options[:storage] = Pathname(v) }
21
- o.on('-v', '--[no-]verbose', 'verbose process output (default: false)') { |v| @options[:verbose] = v }
22
21
  o.on('-f', '--[no-]force', 'ignore XML schema validation (default: false)') { |v| @options[:force] = v }
23
- o.on('-W', '--pry-on-warn ID', Integer, 'open pry on warn with ID (default: nil)') { |v| @options[:pry_on_warn] = v }
24
- o.on('-E', '--[no-]pry-on-error', 'open pry on error (default: false)') { |v| @options[:pry_on_error] = v }
22
+ o.on('-v', '--[no-]verbose', 'verbose output and Ruby debug mode (default: false)') { |v| $DEBUG = v }
23
+ o.on('-w', '--pry-on-warn [ID]', Integer, 'open pry on warn with ID (default: nil)') { |v| $PRY_ON_WARN = v || true }
24
+ o.on('-e', '--[no-]pry-on-error', 'open pry on error (default: false)') { |v| $PRY_ON_ERROR = v }
25
25
  o.on('-A', '--about', 'show author/license information and exit') { about }
26
26
  o.on('-R', '--readme', 'show README and exit') { readme }
27
27
  o.on('-L', '--list', 'list implemented regions and AIPs') { list }
@@ -33,7 +33,7 @@ module AIPP
33
33
  #
34
34
  # @raise [RuntimeError] if the region does not exist
35
35
  def run
36
- Pry::rescue do
36
+ Pry.rescue do
37
37
  fail(OptionParser::MissingArgument, :region) unless options[:region]
38
38
  AIPP::Parser.new(options: options).tap do |parser|
39
39
  parser.read_config
@@ -45,7 +45,7 @@ module AIPP
45
45
  end
46
46
  rescue => error
47
47
  puts "ERROR: #{error.message}"
48
- Pry::rescued(error) if options[:pry_on_error]
48
+ Pry::rescued(error) if $PRY_ON_ERROR
49
49
  end
50
50
  end
51
51
 
@@ -2,8 +2,6 @@ module AIPP
2
2
 
3
3
  # AIP parser infrastructure
4
4
  class Parser
5
- using AIPP::Refinements
6
- include AIPP::Progress
7
5
 
8
6
  # @return [Hash] passed command line arguments
9
7
  attr_reader :options
@@ -20,16 +18,16 @@ module AIPP
20
18
  def initialize(options:)
21
19
  @options = options
22
20
  @options[:storage] = options[:storage].join(options[:region])
23
- @options[:storage].mkpath
21
+ @options[:storage].mkpath unless @options[:storage].exist?
24
22
  @config = {}
25
23
  @aixm = AIXM.document(region: @options[:region], effective_at: @options[:airac].date)
26
- @cache = OpenStruct.new
27
24
  @dependencies = THash.new
25
+ @cache = OpenStruct.new
28
26
  end
29
27
 
30
28
  # Read the configuration from config.yml.
31
29
  def read_config
32
- info("Reading config.yml", force: true)
30
+ info("Reading config.yml")
33
31
  @config = YAML.load_file(config_file, fallback: {}).transform_keys(&:to_sym) if config_file.exist?
34
32
  @config[:namespace] ||= SecureRandom.uuid
35
33
  @aixm.namespace = @config[:namespace]
@@ -37,29 +35,30 @@ module AIPP
37
35
 
38
36
  # Read the region directory and build the dependency list.
39
37
  def read_region
40
- info("Reading region #{options[:region]}", force: true)
38
+ info("Reading region #{options[:region]}")
41
39
  dir = Pathname(__FILE__).dirname.join('regions', options[:region])
42
40
  fail("unknown region `#{options[:region]}'") unless dir.exist?
41
+ dir.glob('helpers/*.rb').each { |f| require f }
43
42
  dir.glob('*.rb').each do |file|
44
- info("Requiring #{file.basename}")
43
+ debug("Requiring #{file.basename}")
45
44
  require file
46
- if (aip = file.basename('.*').to_s) == 'helper'
47
- extend [:AIPP, options[:region], :Helper].constantize
48
- else
49
- @dependencies[aip] = [:AIPP, options[:region], aip.classify, :DEPENDS].constantize
50
- end
45
+ aip = file.basename('.*').to_s
46
+ @dependencies[aip] = ("AIPP::%s::%s::DEPENDS" % [options[:region], aip.remove(/\W/).classify]).constantize
51
47
  end
52
48
  end
53
49
 
54
50
  # Parse AIP by invoking the parser classes for the current region.
55
51
  def parse_aip
56
- info("AIRAC #{options[:airac].id} effective #{options[:airac].date}", force: true, color: :green)
57
- @dependencies.tsort(options[:aip]).each do |aip|
58
- info("Parsing #{aip}", force: true)
59
- [:AIPP, options[:region], aip.classify].constantize.new(
60
- aip: aip,
61
- parser: self
62
- ).parse
52
+ info("AIRAC #{options[:airac].id} effective #{options[:airac].date}", color: :green)
53
+ AIPP::Downloader.new(storage: options[:storage], archive: options[:airac].date.xmlschema) do |downloader|
54
+ @dependencies.tsort(options[:aip]).each do |aip|
55
+ info("Parsing #{aip}")
56
+ ("AIPP::%s::%s" % [options[:region], aip.remove(/\W/).classify]).constantize.new(
57
+ aip: aip,
58
+ downloader: downloader,
59
+ parser: self
60
+ ).attach_patches.tap(&:parse).detach_patches
61
+ end
63
62
  end
64
63
  end
65
64
 
@@ -67,23 +66,24 @@ module AIPP
67
66
  #
68
67
  # @raise [RuntimeError] if the document is not valid
69
68
  def validate_aixm
70
- info("Validating #{options[:schema].upcase}", force: true)
69
+ info("Validating #{options[:schema].upcase}")
71
70
  unless aixm.valid?
72
- send(@options[:force] ? :warn : :fail, "invalid AIXM document:\n#{aixm.errors}")
71
+ message = "invalid AIXM document:\n" + aixm.errors.map(&:message).join("\n")
72
+ @options[:force] ? warn(message, pry: binding) : fail(message)
73
73
  end
74
74
  end
75
75
 
76
76
  # Write the AIXM document.
77
77
  def write_aixm
78
78
  file = "#{options[:region]}_#{options[:airac].date.xmlschema}.#{options[:schema]}"
79
- info("Writing #{file}", force: true)
79
+ info("Writing #{file}")
80
80
  AIXM.send("#{options[:schema]}!")
81
81
  File.write(file, aixm.to_xml)
82
82
  end
83
83
 
84
84
  # Write the configuration to config.yml.
85
85
  def write_config
86
- info("Writing config.yml", force: true)
86
+ info("Writing config.yml")
87
87
  File.write(config_file, config.to_yaml)
88
88
  end
89
89
 
@@ -0,0 +1,47 @@
1
+ module AIPP
2
+ module Patcher
3
+
4
+ def self.included(klass)
5
+ klass.extend(ClassMethods)
6
+ klass.class_variable_set(:@@patches, {})
7
+ end
8
+
9
+ module ClassMethods
10
+ def patches
11
+ class_variable_get(:@@patches)
12
+ end
13
+
14
+ def patch(klass, attribute, &block)
15
+ (patches[self] ||= []) << [klass, attribute, block]
16
+ end
17
+ end
18
+
19
+ def attach_patches
20
+ parser = self
21
+ self.class.patches[self.class]&.each do |(klass, attribute, block)|
22
+ klass.instance_eval do
23
+ alias_method :"original_#{attribute}=", :"#{attribute}="
24
+ define_method(:"#{attribute}=") do |value|
25
+ catch :abort do
26
+ value = block.call(parser, self, value)
27
+ debug("PATCH: #{self.inspect}", color: :magenta)
28
+ end
29
+ send(:"original_#{attribute}=", value)
30
+ end
31
+ end
32
+ end
33
+ self
34
+ end
35
+
36
+ def detach_patches
37
+ self.class.patches[self.class]&.each do |(klass, attribute, _)|
38
+ klass.instance_eval do
39
+ alias_method :"#{attribute}=", :"original_#{attribute}="
40
+ remove_method :"original_#{attribute}="
41
+ end
42
+ end
43
+ self
44
+ end
45
+
46
+ end
47
+ end
@@ -0,0 +1,123 @@
1
+ module AIPP
2
+
3
+ # PDF to text reader with support for pages and fencing
4
+ #
5
+ # @example
6
+ # pdf = AIPP::PDF.new("/path/to/file.pdf")
7
+ # pdf.file # => #<Pathname:/path/to/file.pdf>
8
+ # pdf.from(100).to(200).each_line_with_position do |line, page, last|
9
+ # line # => line content (e.g. "first line")
10
+ # page # => page number (e.g. 1)
11
+ # last # => last line boolean (true for last line, false otherwise)
12
+ # end
13
+ class PDF
14
+ attr_reader :file
15
+
16
+ def initialize(file, cache: true)
17
+ @file = file.is_a?(Pathname) ? file : Pathname(file)
18
+ @text, @page_ranges = cache ? read_cache : read
19
+ @from = 0
20
+ @to = @last = @text.length - 1
21
+ end
22
+
23
+ # @return [String]
24
+ def inspect
25
+ %Q(#<#{self.class} file=#{@file} range=#{range}>)
26
+ end
27
+
28
+ # Fence the PDF beginning with this index
29
+ #
30
+ # @param index [Integer, Symbol] either an integer position within the
31
+ # +text+ string or +:begin+ to indicate "first existing position"
32
+ # @return [self]
33
+ def from(index)
34
+ index = 0 if index == :begin
35
+ fail ArgumentError unless (0..@to).include? index
36
+ @from = index
37
+ self
38
+ end
39
+
40
+ # Fence the PDF ending with this index
41
+ #
42
+ # @param index [Integer, Symbol] either an integer position within the
43
+ # +text+ string or +:end+ to indicate "last existing position"
44
+ # @return [self]
45
+ def to(index)
46
+ index = @last if index == :end
47
+ fail ArgumentError unless (@from..@last).include? index
48
+ @to = index
49
+ self
50
+ end
51
+
52
+ # Get the current fencing range
53
+ #
54
+ # @return [Range<Integer>]
55
+ def range
56
+ (@from..@to)
57
+ end
58
+
59
+ # Text string of the PDF with fencing applied
60
+ #
61
+ # @return [String] PDF converted to string
62
+ def text
63
+ @text[range]
64
+ end
65
+
66
+ # Text split to individual lines
67
+ #
68
+ # @return [Array] lines
69
+ def lines
70
+ text.split(/(?<=[\n\f])/)
71
+ end
72
+
73
+ # Executes the block for every line and passes the line content, page
74
+ # number and end of document boolean.
75
+ #
76
+ # If no block is given, an enumerator is returned instead.
77
+ #
78
+ # @yieldparam line [String] content of the line
79
+ # @yieldparam page [Integer] page number the line is found on within the PDF
80
+ # @yieldparam last [Boolean] true for the last line, false otherwise
81
+ # @return [Enumerator]
82
+ def each_line
83
+ return enum_for(:each) unless block_given?
84
+ offset, last_line_index = @from, lines.count - 1
85
+ lines.each_with_index do |line, line_index|
86
+ yield(line, page_for(index: offset), line_index == last_line_index)
87
+ offset += line.length
88
+ end
89
+ end
90
+ alias_method :each, :each_line
91
+
92
+ private
93
+
94
+ def read
95
+ pages = ::PDF::Reader.new(@file).pages
96
+ [pages.map(&:text).join("\f"), page_ranges_for(pages)]
97
+ end
98
+
99
+ def read_cache
100
+ cache_file = "#{@file}.json"
101
+ if File.exist?(cache_file) && (File.stat(@file).mtime - File.stat(cache_file).mtime).abs < 1
102
+ JSON.load File.read(cache_file)
103
+ else
104
+ read.tap do |data|
105
+ File.write(cache_file, data.to_json)
106
+ FileUtils.touch(cache_file, mtime: File.stat(@file).mtime)
107
+ end
108
+ end
109
+ end
110
+
111
+ def page_ranges_for(pages)
112
+ [].tap do |page_ranges|
113
+ pages.each_with_index do |page, index|
114
+ page_ranges << (page_ranges.last || 0) + page.text.length + index
115
+ end
116
+ end
117
+ end
118
+
119
+ def page_for(index:)
120
+ @page_ranges.index(@page_ranges.bsearch { |i| i >= index }) + 1
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,162 @@
1
+ module AIPP
2
+ module LF
3
+
4
+ # Aerodromes
5
+ class AD13 < AIP
6
+
7
+ include AIPP::LF::Helpers::Common
8
+
9
+ DEPENDS = %w(AD-2)
10
+
11
+ # Map names of id-less airports to unofficial ids
12
+ ID_LESS_AIRPORTS = {
13
+ "ALBE" => 'LF9001',
14
+ "BEAUMONT DE LOMAGNE" => 'LF9002',
15
+ "BERDOUES" => 'LF9003',
16
+ "BOULOC" => 'LF9004',
17
+ "BUXEUIL ST REMY / CREUSE" => 'LF9005',
18
+ "CALVIAC" => 'LF9006',
19
+ "CAYLUS" => 'LF9007',
20
+ "CORBONOD" => 'LF9008',
21
+ "L'ISLE EN DODON" => 'LF9009',
22
+ "LACAVE LE FRAU" => 'LF9010',
23
+ "LUCON CHASNAIS" => 'LF9011',
24
+ "PEYRELEVADE" => 'LF9012',
25
+ "SAINT CYR LA CAMPAGNE" => 'LF9013',
26
+ "SEPTFONDS" => 'LF9014',
27
+ "TALMONT VENDEE AIR PARK" => 'LF9015'
28
+ }
29
+
30
+ def parse
31
+ ad2_exists = false
32
+ tbody = prepare(html: read).css('tbody').first # skip altiports
33
+ tbody.css('tr').to_enum.with_index(1).each do |tr, index|
34
+ if tr.attr(:id).match?(/-TXT_NAME-/)
35
+ write @airport if @airport && !ad2_exists
36
+ @airport = airport_from tr
37
+ debug "Parsing #{@airport.id}"
38
+ ad2_exists = false
39
+ if airport = select(:airport, id: @airport.id).first
40
+ ad2_exists = true
41
+ @airport = airport
42
+ end
43
+ add_usage_limitations_from tr
44
+ next
45
+ end
46
+ @airport.add_runway(runway_from(tr)) unless ad2_exists
47
+ rescue => error
48
+ warn("error parsing #{@airport.id} at ##{index}: #{error.message}", pry: error)
49
+ end
50
+ write @airport if @airport && !ad2_exists
51
+ end
52
+
53
+ private
54
+
55
+ def airport_from(tr)
56
+ tds = tr.css('td')
57
+ id = tds[0].text.strip.blank_to_nil || ID_LESS_AIRPORTS.fetch(tds[1].text.strip)
58
+ AIXM.airport(
59
+ source: source(position: tr.line),
60
+ organisation: organisation_lf, # TODO: not yet implemented
61
+ id: id,
62
+ name: tds[1].text.strip,
63
+ xy: xy_from(tds[3].text)
64
+ ).tap do |airport|
65
+ airport.z = AIXM.z(tds[4].text.strip.to_i, :qnh)
66
+ airport.declination = tds[2].text.remove('°').strip.to_f
67
+ airport.transition_z = AIXM.z(5000, :qnh) # TODO: default - exceptions may exist
68
+ end
69
+ end
70
+
71
+ def add_usage_limitations_from(tr)
72
+ raw_limitation = tr.css('td:nth-of-type(8)').text.cleanup.downcase
73
+ raw_conditions = tr.css('td:nth-of-type(6)').text.cleanup.downcase.split(%r([\s/]+))
74
+ limitation = case raw_limitation
75
+ when /ouv.+cap|milit/ then :permitted
76
+ when /usa.+restr|priv/ then :reservation_required
77
+ end
78
+ @airport.add_usage_limitation(limitation) do |l|
79
+ l.add_condition do |c|
80
+ c.realm = :military if raw_limitation.match?(/milit/)
81
+ c.origin = :national if raw_conditions.include?('ntl')
82
+ c.origin = :international if raw_conditions.include?('intl')
83
+ c.rule = :ifr if raw_conditions.include?('ifr')
84
+ c.rule = :vfr if raw_conditions.include?('vfr')
85
+ c.purpose = :scheduled if raw_conditions.include?('s')
86
+ c.purpose = :not_scheduled if raw_conditions.include?('ns')
87
+ c.purpose = :private if raw_conditions.include?('p')
88
+ end
89
+ l.remarks = "Usage restreint (voir VAC) / restricted use (see VAC)" if raw_limitation.match?(/usa.+restr/)
90
+ l.remarks = "Propriété privée / privately owned" if raw_limitation.match?(/priv/)
91
+ end
92
+ end
93
+
94
+ def runway_from(tr)
95
+ tds = tr.css('td')
96
+ surface = tds[1].css('span[id*="SURFACE"]').text
97
+ AIXM.runway(
98
+ name: tds[0].text.strip.split.join('/')
99
+ ).tap do |runway|
100
+ @runway = runway # TODO: needed for now for surface composition patches to work
101
+ runway.length = AIXM.d(tds[1].css('span[id$="VAL_LEN"]').text.to_i, :m)
102
+ runway.width = AIXM.d(tds[1].css('span[id$="VAL_WID"]').text.to_i, :m)
103
+ runway.surface.composition = (COMPOSITIONS.fetch(surface)[:composition] unless surface.blank?)
104
+ runway.surface.preparation = (COMPOSITIONS.fetch(surface)[:preparation] unless surface.blank?)
105
+ runway.remarks = tds[7].text.cleanup.blank_to_nil
106
+ values = tds[2].text.remove('°').strip.split
107
+ runway.forth.geographic_orientation = AIXM.a(values.first.to_i)
108
+ runway.back.geographic_orientation = AIXM.a(values.last.to_i)
109
+ parts = tds[3].text.strip.split(/\n\s+\n\s+/)
110
+ runway.forth.xy = (xy_from(parts[0]) unless parts[0].blank?)
111
+ runway.back.xy = (xy_from(parts[1]) unless parts[1].blank?)
112
+ values = tds[4].text.strip.split
113
+ runway.forth.z = AIXM.z(values.first.to_i, :qnh)
114
+ runway.back.z = AIXM.z(values.last.to_i, :qnh)
115
+ displaced_thresholds = displaced_thresholds_from(tds[5])
116
+ runway.forth.displaced_threshold = displaced_thresholds.first
117
+ runway.back.displaced_threshold = displaced_thresholds.last
118
+ end
119
+ end
120
+
121
+ def displaced_thresholds_from(td)
122
+ values = td.text.strip.split
123
+ case values.count
124
+ when 1 then []
125
+ when 2 then [AIXM.xy(lat: values[0], long: values[1]), nil]
126
+ when 3 then [nil, AIXM.xy(lat: values[1], long: values[2])]
127
+ when 4 then [AIXM.xy(lat: values[0], long: values[1]), AIXM.xy(lat: values[2], long: values[3])]
128
+ else fail "cannot parse displaced thresholds"
129
+ end
130
+ end
131
+
132
+ patch AIXM::Component::Runway, :width do |parser, object, value|
133
+ throw :abort unless value.zero?
134
+ @fixtures ||= YAML.load_file(Pathname(__FILE__).dirname.join('AD-1.3.yml'))
135
+ airport_id = parser.instance_variable_get(:@airport).id
136
+ runway_name = object.name.to_s
137
+ throw :abort if (width = @fixtures.dig('runways', airport_id, runway_name, 'width')).nil?
138
+ AIXM.d(width.to_i, :m)
139
+ end
140
+
141
+ patch AIXM::Component::Runway::Direction, :xy do |parser, object, value|
142
+ throw :abort unless value.nil?
143
+ @fixtures ||= YAML.load_file(Pathname(__FILE__).dirname.join('AD-1.3.yml'))
144
+ airport_id = parser.instance_variable_get(:@airport).id
145
+ direction_name = object.name.to_s
146
+ throw :abort if (xy = @fixtures.dig('runways', airport_id, direction_name, 'xy')).nil?
147
+ lat, long = xy.split(/\s+/)
148
+ AIXM.xy(lat: lat, long: long)
149
+ end
150
+
151
+ patch AIXM::Component::Surface, :composition do |parser, object, value|
152
+ throw :abort unless value.blank?
153
+ @fixtures ||= YAML.load_file(Pathname(__FILE__).dirname.join('AD-1.3.yml'))
154
+ airport_id = parser.instance_variable_get(:@airport).id
155
+ runway_name = parser.instance_variable_get(:@runway).name
156
+ throw :abort if (composition = @fixtures.dig('runways', airport_id, runway_name, 'composition')).nil?
157
+ composition
158
+ end
159
+
160
+ end
161
+ end
162
+ end