aipp 0.2.4 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +2 -0
  3. data/CHANGELOG.md +38 -0
  4. data/README.md +222 -88
  5. data/exe/aip2aixm +2 -2
  6. data/exe/aip2ofmx +2 -2
  7. data/lib/aipp/aip.rb +113 -31
  8. data/lib/aipp/border.rb +77 -46
  9. data/lib/aipp/debugger.rb +101 -0
  10. data/lib/aipp/downloader.rb +39 -26
  11. data/lib/aipp/executable.rb +41 -22
  12. data/lib/aipp/parser.rb +94 -21
  13. data/lib/aipp/patcher.rb +5 -2
  14. data/lib/aipp/pdf.rb +1 -1
  15. data/lib/aipp/regions/LF/README.md +49 -0
  16. data/lib/aipp/regions/LF/aerodromes.rb +223 -0
  17. data/lib/aipp/regions/LF/d_p_r_airspaces.rb +56 -0
  18. data/lib/aipp/regions/LF/dangerous_activities.rb +49 -0
  19. data/lib/aipp/regions/LF/designated_points.rb +47 -0
  20. data/lib/aipp/regions/LF/fixtures/aerodromes.yml +608 -0
  21. data/lib/aipp/regions/LF/helipads.rb +122 -0
  22. data/lib/aipp/regions/LF/helpers/base.rb +218 -0
  23. data/lib/aipp/regions/LF/helpers/surface.rb +49 -0
  24. data/lib/aipp/regions/LF/helpers/usage_limitation.rb +20 -0
  25. data/lib/aipp/regions/LF/navigational_aids.rb +85 -0
  26. data/lib/aipp/regions/LF/obstacles.rb +153 -0
  27. data/lib/aipp/regions/LF/serviced_airspaces.rb +70 -0
  28. data/lib/aipp/regions/LF/services.rb +172 -0
  29. data/lib/aipp/t_hash.rb +4 -5
  30. data/lib/aipp/version.rb +1 -1
  31. data/lib/aipp.rb +11 -5
  32. data/lib/core_ext/enumerable.rb +9 -9
  33. data/lib/core_ext/hash.rb +21 -5
  34. data/lib/core_ext/nokogiri.rb +54 -0
  35. data/lib/core_ext/string.rb +38 -66
  36. data.tar.gz.sig +2 -0
  37. metadata +180 -188
  38. metadata.gz.sig +0 -0
  39. data/.gitignore +0 -8
  40. data/.ruby-version +0 -1
  41. data/.travis.yml +0 -8
  42. data/.yardopts +0 -3
  43. data/Guardfile +0 -7
  44. data/TODO.md +0 -6
  45. data/aipp.gemspec +0 -44
  46. data/gems.rb +0 -3
  47. data/lib/aipp/airac.rb +0 -55
  48. data/lib/aipp/regions/LF/AD-1.3.rb +0 -162
  49. data/lib/aipp/regions/LF/AD-1.6.rb +0 -31
  50. data/lib/aipp/regions/LF/AD-2.rb +0 -313
  51. data/lib/aipp/regions/LF/AD-3.1.rb +0 -185
  52. data/lib/aipp/regions/LF/ENR-2.1.rb +0 -92
  53. data/lib/aipp/regions/LF/ENR-4.1.rb +0 -97
  54. data/lib/aipp/regions/LF/ENR-4.3.rb +0 -28
  55. data/lib/aipp/regions/LF/ENR-5.1.rb +0 -75
  56. data/lib/aipp/regions/LF/ENR-5.5.rb +0 -53
  57. data/lib/aipp/regions/LF/fixtures/AD-1.3.yml +0 -511
  58. data/lib/aipp/regions/LF/fixtures/AD-2.yml +0 -185
  59. data/lib/aipp/regions/LF/fixtures/AD-3.1.yml +0 -10
  60. data/lib/aipp/regions/LF/helpers/AD_radio.rb +0 -90
  61. data/lib/aipp/regions/LF/helpers/URL.rb +0 -26
  62. data/lib/aipp/regions/LF/helpers/common.rb +0 -217
  63. data/lib/core_ext/object.rb +0 -43
  64. data/rakefile.rb +0 -12
  65. data/spec/fixtures/archive.zip +0 -0
  66. data/spec/fixtures/border.geojson +0 -201
  67. data/spec/fixtures/document.pdf +0 -0
  68. data/spec/fixtures/document.pdf.json +0 -1
  69. data/spec/fixtures/new.html +0 -6
  70. data/spec/fixtures/new.pdf +0 -0
  71. data/spec/fixtures/new.txt +0 -1
  72. data/spec/lib/aipp/airac_spec.rb +0 -98
  73. data/spec/lib/aipp/border_spec.rb +0 -135
  74. data/spec/lib/aipp/downloader_spec.rb +0 -81
  75. data/spec/lib/aipp/patcher_spec.rb +0 -46
  76. data/spec/lib/aipp/pdf_spec.rb +0 -124
  77. data/spec/lib/aipp/t_hash_spec.rb +0 -44
  78. data/spec/lib/aipp/version_spec.rb +0 -7
  79. data/spec/lib/core_ext/enumberable_spec.rb +0 -76
  80. data/spec/lib/core_ext/hash_spec.rb +0 -27
  81. data/spec/lib/core_ext/integer_spec.rb +0 -15
  82. data/spec/lib/core_ext/nil_class_spec.rb +0 -11
  83. data/spec/lib/core_ext/string_spec.rb +0 -112
  84. data/spec/sounds/failure.mp3 +0 -0
  85. data/spec/sounds/success.mp3 +0 -0
  86. data/spec/spec_helper.rb +0 -28
@@ -2,26 +2,40 @@ module AIPP
2
2
 
3
3
  # Executable instantiated by the console tools
4
4
  class Executable
5
+ include AIPP::Debugger
6
+
5
7
  attr_reader :options
6
8
 
7
9
  def initialize(**options)
8
- @options = options
9
- @options[:airac] = AIPP::AIRAC.new
10
- @options[:storage] = Pathname(Dir.home).join('.aipp')
11
- @options[:force] = $VERBOSE_INFO = $PRY_ON_WARN = $PRY_ON_ERROR = false
10
+ @options = options.merge(
11
+ airac: AIRAC::Cycle.new,
12
+ region_options: [],
13
+ storage: Pathname(Dir.home).join('.aipp'),
14
+ force: false,
15
+ mid: false,
16
+ verbose: false,
17
+ debug_on_warning: false,
18
+ debug_on_error: false
19
+ )
12
20
  OptionParser.new do |o|
13
21
  o.banner = <<~END
14
22
  Download online AIP and convert it to #{options[:schema].upcase}.
15
23
  Usage: #{File.basename($0)} [options]
16
24
  END
17
- o.on('-d', '--airac DATE', String, %Q[AIRAC date (default: "#{@options[:airac].date.xmlschema}")]) { |v| @options[:airac] = AIPP::AIRAC.new(v) }
18
- o.on('-r', '--region STRING', String, 'region (e.g. "LF")') { |v| @options[:region] = v.upcase }
19
- o.on('-a', '--aip STRING', String, 'process this AIP only (e.g. "ENR-5.1")') { |v| @options[:aip] = v.upcase }
20
- o.on('-s', '--storage DIR', String, 'storage directory (default: "~/.aipp")') { |v| @options[:storage] = Pathname(v) }
21
- o.on('-f', '--[no-]force', 'ignore XML schema validation (default: false)') { |v| @options[:force] = v }
22
- o.on('-v', '--[no-]verbose', 'verbose output (default: false)') { |v| $VERBOSE_INFO = 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
+ o.on('-d', '--airac (DATE|INTEGER)', String, %Q[AIRAC date or delta e.g. "+1" (default: "#{@options[:airac].date.xmlschema}")]) { @options[:airac] = airac_for(_1) }
26
+ o.on('-r', '--region STRING', String, 'region (e.g. "LF")') { @options[:region] = _1.upcase }
27
+ o.on('-a', '--aip STRING', String, 'process this AIP only (e.g. "ENR-5.1")') { @options[:aip] = _1 }
28
+ if options[:schema] == :ofmx
29
+ o.on('-g', '--[no-]grouped-obstacles', 'group obstacles (default: false)') { @options[:grouped_obstacles] = _1 }
30
+ o.on('-m', '--[no-]mid', 'insert mid attributes into all Uid elements (default: false)') { @options[:mid] = _1 }
31
+ end
32
+ o.on('-o', '--region-options STRING', String, %Q[comma separated region specific options]) { @options[:region_options] = _1.split(',') }
33
+ o.on('-s', '--storage DIR', String, 'storage directory (default: "~/.aipp")') { @options[:storage] = Pathname(_1) }
34
+ o.on('-h', '--[no-]check-links', 'check all links with HEAD requests') { @options[:check_links] = _1 }
35
+ o.on('-f', '--[no-]force', 'ignore XML schema validation (default: false)') { @options[:force] = _1 }
36
+ o.on('-v', '--[no-]verbose', 'verbose output including unsevere warnings (default: false)') { @options[:verbose] = _1 }
37
+ o.on('-w', '--debug-on-warning [ID]', Integer, 'open debug session on warning with ID (default: false)') { @options[:debug_on_warning] = _1 || true }
38
+ o.on('-e', '--[no-]debug-on-error', 'open debug session on error (default: false)') { @options[:debug_on_error] = _1 }
25
39
  o.on('-A', '--about', 'show author/license information and exit') { about }
26
40
  o.on('-R', '--readme', 'show README and exit') { readme }
27
41
  o.on('-L', '--list', 'list implemented regions and AIPs') { list }
@@ -29,28 +43,33 @@ module AIPP
29
43
  end.parse!
30
44
  end
31
45
 
32
- # Load necessary files and execute the parser.
33
- #
34
- # @raise [RuntimeError] if the region does not exist
35
46
  def run
36
- Pry.rescue do
37
- fail(OptionParser::MissingArgument, :region) unless options[:region]
47
+ with_debugger(**options.slice(:verbose, :debug_on_warning, :debug_on_error)) do
48
+ starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
38
49
  AIPP::Parser.new(options: options).tap do |parser|
39
50
  parser.read_config
40
51
  parser.read_region
41
52
  parser.parse_aip
42
53
  parser.validate_aixm
54
+ parser.write_build
43
55
  parser.write_aixm
44
56
  parser.write_config
45
57
  end
46
- rescue => error
47
- puts "ERROR: #{error.message}"
48
- Pry::rescued(error) if $PRY_ON_ERROR
58
+ ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
59
+ info("finished after %s" % Time.at(ending - starting).utc.strftime("%H:%M:%S"))
49
60
  end
50
61
  end
51
62
 
52
63
  private
53
64
 
65
+ def airac_for(argument)
66
+ if argument.match?(/^[+-]\d+$/) # delta
67
+ AIRAC::Cycle.new + argument.to_i
68
+ else # date
69
+ AIRAC::Cycle.new(argument)
70
+ end
71
+ end
72
+
54
73
  def about
55
74
  puts 'Written by Sven Schwyn (bitcetera.com) and distributed under MIT license.'
56
75
  exit
@@ -66,7 +85,7 @@ module AIPP
66
85
  regions_path = Pathname($0).dirname.join('..', 'gems', "aipp-#{AIPP::VERSION}", 'lib', 'aipp', 'regions')
67
86
  hash = Dir.each_child(regions_path).each.with_object({}) do |region, hash|
68
87
  hash[region] = Dir.children(regions_path.join(region)).sort.map do |aip|
69
- File.basename(aip, '.rb') unless aip == 'helper.rb'
88
+ File.basename(aip, '.rb') if File.file?(regions_path.join(region, aip))
70
89
  end.compact
71
90
  end
72
91
  puts hash.to_yaml.sub(/\A\W*/, '')
@@ -77,6 +96,6 @@ module AIPP
77
96
  puts AIPP::VERSION
78
97
  exit
79
98
  end
80
- end
81
99
 
100
+ end
82
101
  end
data/lib/aipp/parser.rb CHANGED
@@ -2,6 +2,9 @@ module AIPP
2
2
 
3
3
  # AIP parser infrastructure
4
4
  class Parser
5
+ extend Forwardable
6
+ include AIPP::Debugger
7
+ using AIXM::Refinements
5
8
 
6
9
  # @return [Hash] passed command line arguments
7
10
  attr_reader :options
@@ -24,62 +27,68 @@ module AIPP
24
27
  def initialize(options:)
25
28
  @options = options
26
29
  @options[:storage] = options[:storage].join(options[:region])
27
- @options[:storage].mkpath unless @options[:storage].exist?
30
+ @options[:storage].mkpath
28
31
  @config = {}
29
- @aixm = AIXM.document(region: @options[:region], effective_at: @options[:airac].date)
32
+ @aixm = AIXM.document(effective_at: @options[:airac].date)
30
33
  @dependencies = THash.new
31
34
  @fixtures = {}
32
35
  @borders = {}
33
36
  @cache = OpenStruct.new
34
37
  AIXM.send("#{options[:schema]}!")
38
+ AIXM.config.region = options[:region]
39
+ end
40
+
41
+ # @return [String]
42
+ def inspect
43
+ "#<AIPP::Parser>"
35
44
  end
36
45
 
37
46
  # Read the configuration from config.yml.
38
47
  def read_config
39
- info("Reading config.yml")
40
- @config = YAML.load_file(config_file, fallback: {}).transform_keys(&:to_sym) if config_file.exist?
48
+ info("reading config.yml")
49
+ @config = YAML.load_file(config_file, symbolize_names: true, fallback: {}) if config_file.exist?
41
50
  @config[:namespace] ||= SecureRandom.uuid
42
51
  @aixm.namespace = @config[:namespace]
43
52
  end
44
53
 
45
54
  # Read the region directory and build the dependency list.
46
55
  def read_region
47
- info("Reading region #{options[:region]}")
56
+ info("reading region #{options[:region]}")
48
57
  dir = Pathname(__FILE__).dirname.join('regions', options[:region])
49
58
  fail("unknown region `#{options[:region]}'") unless dir.exist?
50
59
  # Fixtures
51
60
  dir.glob('fixtures/*.yml').each do |file|
52
- verbose_info "Reading fixture fixtures/#{file.basename}"
61
+ verbose_info "reading fixture fixtures/#{file.basename}"
53
62
  fixture = YAML.load_file(file)
54
63
  @fixtures[file.basename('.yml').to_s] = fixture
55
64
  end
56
65
  # Borders
57
66
  dir.glob('borders/*.geojson').each do |file|
58
- verbose_info "Reading border borders/#{file.basename}"
59
- border = AIPP::Border.new(file)
60
- @borders[border.name] = border
67
+ verbose_info "reading border borders/#{file.basename}"
68
+ border = AIPP::Border.from_file(file)
69
+ @borders[file.basename] = border
61
70
  end
62
71
  # Helpers
63
72
  dir.glob('helpers/*.rb').each do |file|
64
- verbose_info "Reading helper helpers/#{file.basename}"
73
+ verbose_info "reading helper helpers/#{file.basename}"
65
74
  require file
66
75
  end
67
76
  # Parsers
68
77
  dir.glob('*.rb').each do |file|
69
- verbose_info "Requiring #{file.basename}"
78
+ verbose_info "requiring #{file.basename}"
70
79
  require file
71
80
  aip = file.basename('.*').to_s
72
- @dependencies[aip] = ("AIPP::%s::%s::DEPENDS" % [options[:region], aip.remove(/\W/).classify]).constantize
81
+ @dependencies[aip] = ("AIPP::%s::%s::DEPENDS" % [options[:region], aip.remove(/\W/).camelcase]).constantize
73
82
  end
74
83
  end
75
84
 
76
85
  # Parse AIP by invoking the parser classes for the current region.
77
86
  def parse_aip
78
87
  info("AIRAC #{options[:airac].id} effective #{options[:airac].date}", color: :green)
79
- AIPP::Downloader.new(storage: options[:storage], archive: options[:airac].date.xmlschema) do |downloader|
88
+ AIPP::Downloader.new(storage: options[:storage], source: options[:airac].date.xmlschema) do |downloader|
80
89
  @dependencies.tsort(options[:aip]).each do |aip|
81
- info("Parsing #{aip}")
82
- ("AIPP::%s::%s" % [options[:region], aip.remove(/\W/).classify]).constantize.new(
90
+ info("parsing #{aip}")
91
+ ("AIPP::%s::%s" % [options[:region], aip.remove(/\W/).camelcase]).constantize.new(
83
92
  aip: aip,
84
93
  downloader: downloader,
85
94
  fixture: @fixtures[aip],
@@ -87,34 +96,98 @@ module AIPP
87
96
  ).attach_patches.tap(&:parse).detach_patches
88
97
  end
89
98
  end
99
+ if options[:grouped_obstacles]
100
+ info("grouping obstacles")
101
+ aixm.group_obstacles!
102
+ end
103
+ info("counting #{aixm.features.count} features")
90
104
  end
91
105
 
92
106
  # Validate the AIXM document.
93
107
  #
94
108
  # @raise [RuntimeError] if the document is not valid
95
109
  def validate_aixm
96
- info("Validating #{options[:schema].upcase}")
110
+ info("detecting duplicates")
111
+ if (duplicates = aixm.features.duplicates).any?
112
+ message = "duplicates found:\n" + duplicates.map { "#{_1.inspect} from #{_1.source}" }.join("\n")
113
+ @options[:force] ? warn(message) : fail(message)
114
+ end
115
+ info("validating #{options[:schema].upcase}")
97
116
  unless aixm.valid?
98
117
  message = "invalid #{options[:schema].upcase} document:\n" + aixm.errors.map(&:message).join("\n")
99
- @options[:force] ? warn(message, pry: binding) : fail(message)
118
+ @options[:force] ? warn(message) : fail(message)
119
+ end
120
+ end
121
+
122
+ # Write the AIXM document and context information.
123
+ def write_build
124
+ if @options[:aip]
125
+ info ("skipping build")
126
+ else
127
+ info("writing build")
128
+ builds_path.mkpath
129
+ build_file = builds_path.join("#{@options[:airac].date.xmlschema}.zip")
130
+ Dir.mktmpdir do |tmp_dir|
131
+ tmp_dir = Pathname(tmp_dir)
132
+ # AIXM/OFMX file
133
+ AIXM.config.mid = true
134
+ File.write(tmp_dir.join(aixm_file), aixm.to_xml)
135
+ # Build details
136
+ File.write(
137
+ tmp_dir.join('build.yaml'), {
138
+ version: AIPP::VERSION,
139
+ config: @config,
140
+ options: @options
141
+ }.to_yaml
142
+ )
143
+ # Manifest
144
+ manifest = ['AIP','Feature', 'Comment', 'Short Uid Hash', 'Short Feature Hash'].to_csv
145
+ manifest += aixm.features.map do |feature|
146
+ xml = feature.to_xml
147
+ element = xml.first_match(/<(\w{3})\s/)
148
+ [
149
+ feature.source.split('|')[2],
150
+ element,
151
+ xml.match(/<!-- (.*?) -->/)[1],
152
+ AIXM::PayloadHash.new(xml.match(%r(<#{element}Uid\s.*?</#{element}Uid>)m).to_s).to_uuid[0,8],
153
+ AIXM::PayloadHash.new(xml).to_uuid[0,8]
154
+ ].to_csv
155
+ end.sort.join
156
+ File.write(tmp_dir.join('manifest.csv'), manifest)
157
+ # Zip it
158
+ build_file.delete if build_file.exist?
159
+ Zip::File.open(build_file, Zip::File::CREATE) do |zip|
160
+ tmp_dir.children.each do |entry|
161
+ zip.add(entry.basename.to_s, entry) unless entry.basename.to_s[0] == '.'
162
+ end
163
+ end
164
+ end
100
165
  end
101
166
  end
102
167
 
103
168
  # Write the AIXM document.
104
169
  def write_aixm
105
- file = "#{options[:region]}_#{options[:airac].date.xmlschema}.#{options[:schema]}"
106
- info("Writing #{file}")
107
- File.write(file, aixm.to_xml)
170
+ info("writing #{aixm_file}")
171
+ AIXM.config.mid = options[:mid]
172
+ File.write(aixm_file, aixm.to_xml)
108
173
  end
109
174
 
110
175
  # Write the configuration to config.yml.
111
176
  def write_config
112
- info("Writing config.yml")
177
+ info("writing config.yml")
113
178
  File.write(config_file, config.to_yaml)
114
179
  end
115
180
 
116
181
  private
117
182
 
183
+ def aixm_file
184
+ "#{options[:region]}_#{options[:airac].date.xmlschema}.#{options[:schema]}"
185
+ end
186
+
187
+ def builds_path
188
+ options[:storage].join('builds')
189
+ end
190
+
118
191
  def config_file
119
192
  options[:storage].join('config.yml')
120
193
  end
data/lib/aipp/patcher.rb CHANGED
@@ -18,14 +18,16 @@ module AIPP
18
18
 
19
19
  def attach_patches
20
20
  parser = self
21
+ verbose_info_method = method(:verbose_info)
21
22
  self.class.patches[self.class]&.each do |(klass, attribute, block)|
22
23
  klass.instance_eval do
23
24
  alias_method :"original_#{attribute}=", :"#{attribute}="
24
25
  define_method(:"#{attribute}=") do |value|
25
- catch :abort do
26
+ error = catch :abort do
26
27
  value = block.call(parser, self, value)
27
- verbose_info("PATCH: #{self.inspect}", color: :magenta)
28
+ verbose_info_method.call("Patching #{self.inspect} with #{attribute}=#{value.inspect}", color: :magenta)
28
29
  end
30
+ fail "patching #{self.inspect} with #{attribute}=#{value.inspect} failed: #{error}" if error
29
31
  send(:"original_#{attribute}=", value)
30
32
  end
31
33
  end
@@ -36,6 +38,7 @@ module AIPP
36
38
  def detach_patches
37
39
  self.class.patches[self.class]&.each do |(klass, attribute, _)|
38
40
  klass.instance_eval do
41
+ remove_method :"#{attribute}="
39
42
  alias_method :"#{attribute}=", :"original_#{attribute}="
40
43
  remove_method :"original_#{attribute}="
41
44
  end
data/lib/aipp/pdf.rb CHANGED
@@ -117,7 +117,7 @@ module AIPP
117
117
  end
118
118
 
119
119
  def page_for(index:)
120
- @page_ranges.index(@page_ranges.bsearch { |i| i >= index }) + 1
120
+ @page_ranges.index(@page_ranges.bsearch { _1 >= index }) + 1
121
121
  end
122
122
  end
123
123
  end
@@ -0,0 +1,49 @@
1
+ # AIP LF – Mainland France
2
+
3
+ ## Prerequisites
4
+
5
+ This parser requires the XML data dump from SIA. It is available free of charge, but has to be ordered before it can be downloaded. It's therefore necessary to perform the following steps before running the parser for any given AIRAC for the first time:
6
+
7
+ 1. Browse to the [SIA web shop](https://www.sia.aviation-civile.gouv.fr/produits-numeriques-en-libre-disposition/les-bases-de-donnees-sia.html).
8
+ 2. Shop the desired dump named «données aéronautiques XML AIRAC ii/yy».
9
+ 3. Browse to [your customer page](https://www.sia.aviation-civile.gouv.fr/customer/account/#orders-and-proposals).
10
+ 4. In section «mes produits téléchargeables» download the desired dump.
11
+ 5. Unzip the downloaded ZIP archive.
12
+ 6. Move the file «XML_SIA_yyyy-mm-dd.xml» to the directory in which you will execute the parser.
13
+
14
+ ⚠️ The SIA web shop misbehaves with some browsers, you should try Brave or Chrome.
15
+
16
+ ## Region Options
17
+
18
+ ### Obstacles XLSX
19
+
20
+ While the XML data dump contains all obstacles, some details of the source XLSX file are omitted. Unfortunately, the latter is only available for the current AIRAC cycle, therefore the XML data dump is used by default. Add `-o lf_obstacles_xlsx` to use the source XLSX file instead.
21
+
22
+ ## Charset
23
+
24
+ The XML data dump from SIA is ISO-8859-1 encoded. Nokogiri which parses the XML converts this to UTF-8 on the fly, however, when grepping the dump on a shell, you might run into trouble:
25
+
26
+ ```shell
27
+ grep "<Revetement>" XML_SIA_2021-12-02.xml | sort | uniq
28
+
29
+ sort: Illegal byte sequence
30
+ ```
31
+
32
+ For this to work, you have to convert the dump to UTF-8 and use this converted dump for grepping:
33
+
34
+ ```shell
35
+ iconv -f ISO-8859-1 -t UTF-8 XML_SIA_2021-12-02.xml >XML_SIA_2021-12-02_UTF.xml
36
+ grep "<Revetement>" XML_SIA_2021-12-02_UTF.xml | sort | uniq
37
+
38
+ <Revetement>Aluminium</Revetement>
39
+ <Revetement>Asphalte</Revetement>
40
+ <Revetement>Béton ( 4t )</Revetement>
41
+ (...)
42
+ ```
43
+
44
+ ## References
45
+
46
+ * [SIA – AIP publisher](https://www.sia.aviation-civile.gouv.fr)
47
+ * [SIA XML usage guide](https://www.sia.aviation-civile.gouv.fr/faqs)
48
+ * [OpenData – public data files](https://www.data.gouv.fr)
49
+ * [Protected Planet – protected area data files](https://www.protectedplanet.net)
@@ -0,0 +1,223 @@
1
+ module AIPP
2
+ module LF
3
+
4
+ class Aerodromes < AIP
5
+
6
+ include AIPP::LF::Helpers::Base
7
+ include AIPP::LF::Helpers::UsageLimitation
8
+ include AIPP::LF::Helpers::Surface
9
+
10
+ APPROACH_LIGHTING_TYPES = {
11
+ 'CAT I' => :cat_1,
12
+ 'CAT II' => :cat_2,
13
+ 'CAT III' => :cat_3,
14
+ 'CAT II-III' => :cat_2_and_3
15
+ }.freeze
16
+
17
+ LIGHTING_POSITIONS = {
18
+ threshold: 'Thr',
19
+ touch_down_zone: 'Tdz',
20
+ center_line: 'Axe',
21
+ edge: 'Bord',
22
+ runway_end: 'Fin',
23
+ stopway_center_line: 'Swy'
24
+ }.freeze
25
+
26
+ LIGHTING_COLORS = {
27
+ 'W' => :white,
28
+ 'R' => :red,
29
+ 'G' => :green,
30
+ 'B' => :blue,
31
+ 'Y' => :yellow
32
+ }.freeze
33
+
34
+ ICAO_LIGHTING_COLORS = {
35
+ center_line: :white,
36
+ edge: :white
37
+ }.freeze
38
+
39
+ def parse
40
+ cache.ad.css(%Q(Ad[lk^="[LF]"])).each do |ad_node|
41
+ # Build airport
42
+ next unless limitation_type = LIMITATION_TYPES.fetch(ad_node.(:AdStatut))
43
+ airport = AIXM.airport(
44
+ source: source(section: 'AD', position: ad_node.line),
45
+ organisation: organisation_lf,
46
+ id: id_from(ad_node.(:AdCode)),
47
+ name: ad_node.(:AdNomComplet),
48
+ xy: xy_from(ad_node.(:Geometrie))
49
+ ).tap do |airport|
50
+ airport.meta = ad_node.attr('pk')
51
+ airport.z = given(ad_node.(:AdRefAltFt)) { AIXM.z(_1.to_i, :qnh) }
52
+ airport.declination = ad_node.(:AdMagVar)&.to_f
53
+ airport.add_usage_limitation(type: limitation_type.fetch(:limitation)) do |limitation|
54
+ limitation.remarks = limitation_type[:remarks]
55
+ [
56
+ (:scheduled if ad_node.(:TfcRegulier?)),
57
+ (:not_scheduled if ad_node.(:TfcNonRegulier?)),
58
+ (:private if ad_node.(:TfcPrive?)),
59
+ (:other unless ad_node.(:TfcRegulier?) || ad_node.(:TfcNonRegulier?) || ad_node.(:TfcPrive?))
60
+ ].compact.each do |purpose|
61
+ limitation.add_condition do |condition|
62
+ condition.realm = limitation_type.fetch(:realm)
63
+ condition.origin = case
64
+ when ad_node.(:TfcIntl?) && ad_node.(:TfcNtl?) then :any
65
+ when ad_node.(:TfcIntl?) then :international
66
+ when ad_node.(:TfcNtl?) then :national
67
+ else :other
68
+ end
69
+ condition.rule = case
70
+ when ad_node.(:TfcIfr?) && ad_node.(:TfcVfr?) then :ifr_and_vfr
71
+ when ad_node.(:TfcIfr?) then :ifr
72
+ when ad_node.(:TfcVfr?) then :vfr
73
+ else
74
+ warn("falling back to VFR rule for `#{airport.id}'", severe: false)
75
+ :vfr
76
+ end
77
+ condition.purpose = purpose
78
+ end
79
+ end
80
+ end
81
+ # TODO: link to VAC once supported downstream
82
+ # # Link to VAC
83
+ # airport.remarks = [
84
+ # airport.remarks.to_s,
85
+ # link_to('VAC-AD', url_for("VAC-#{airport.id}"))
86
+ # ].join("\n")
87
+ cache.rwy.css(%Q(Rwy:has(Ad[pk="#{ad_node.attr(:pk)}"]))).each do |rwy_node|
88
+ add_runway_to(airport, rwy_node)
89
+ end
90
+ end
91
+ add airport
92
+ end
93
+ end
94
+
95
+ private
96
+
97
+ def id_from(content)
98
+ case content
99
+ when /^\d{2}$/ then 'LF00' + content # private aerodromes without official ID
100
+ else 'LF' + content
101
+ end
102
+ end
103
+
104
+ def add_runway_to(airport, rwy_node)
105
+ AIXM.runway(
106
+ name: rwy_node.(:Rwy)
107
+ ).tap do |runway|
108
+ rwylgt_nodes = cache.rwylgt.css(%Q(RwyLgt:has(Rwy[pk="#{rwy_node.attr(:pk)}"])))
109
+ airport.add_runway(runway)
110
+ runway.dimensions = AIXM.r(AIXM.d(rwy_node.(:Longueur)&.to_i, :m), AIXM.d(rwy_node.(:Largeur)&.to_i, :m))
111
+ runway.surface = surface_from(rwy_node)
112
+ runway.forth.geographic_bearing = given(rwy_node.(:OrientationGeo)) { AIXM.a(_1.to_f) }
113
+ runway.forth.xy = given(rwy_node.(:LatThr1), rwy_node.(:LongThr1)) { AIXM.xy(lat: _1.to_f, long: _2.to_f) }
114
+ runway.forth.displaced_threshold = given(rwy_node.(:LatDThr1), rwy_node.(:LongDThr1)) { AIXM.xy(lat: _1.to_f, long: _2.to_f) }
115
+ runway.forth.z = given(rwy_node.(:AltFtDThr1)) { AIXM.z(_1.to_i, :qnh) }
116
+ runway.forth.z ||= given(rwy_node.(:AltFtThr1)) { AIXM.z(_1.to_i, :qnh) }
117
+ if rwylgt_node = rwylgt_nodes[0]
118
+ runway.forth.vasis = vasis_from(rwylgt_node)
119
+ given(approach_lighting_from(rwylgt_node)) { runway.forth.add_approach_lighting(_1) }
120
+ LIGHTING_POSITIONS.each_key do |position|
121
+ given(lighting_from(rwylgt_node, position)) { runway.forth.add_lighting(_1) }
122
+ end
123
+ end
124
+ if rwy_node.(:Rwy).match? '/'
125
+ runway.back.xy = given(rwy_node.(:LatThr2), rwy_node.(:LongThr2)) { AIXM.xy(lat: _1.to_f, long: _2.to_f) }
126
+ runway.back.displaced_threshold = given(rwy_node.(:LatDThr2), rwy_node.(:LongDThr2)) { AIXM.xy(lat: _1.to_f, long: _2.to_f) }
127
+ runway.back.z = given(rwy_node.(:AltFtDThr2)) { AIXM.z(_1.to_i, :qnh) }
128
+ runway.back.z ||= given(rwy_node.(:AltFtThr2)) { AIXM.z(_1.to_i, :qnh) }
129
+ if rwylgt_node = rwylgt_nodes[1]
130
+ runway.back.vasis = vasis_from(rwylgt_node)
131
+ given(approach_lighting_from(rwylgt_node)) { runway.back.add_approach_lighting(_1) }
132
+ LIGHTING_POSITIONS.each_key do |position|
133
+ given(lighting_from(rwylgt_node, position)) { runway.back.add_lighting(_1) }
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
139
+
140
+ def vasis_from(rwylgt_node)
141
+ if rwylgt_node.(:PapiVasis)
142
+ AIXM.vasis.tap do |vasis|
143
+ vasis.type = rwylgt_node.(:PapiVasis)
144
+ vasis.slope_angle = AIXM.a(rwylgt_node.(:PapiVasisPente).to_f)
145
+ vasis.meht = AIXM.z(rwylgt_node.(:MehtFt).to_i, :qfe)
146
+ end
147
+ end
148
+ end
149
+
150
+ def approach_lighting_from(rwylgt_node)
151
+ if rwylgt_node.(:LgtApchCat)
152
+ AIXM.approach_lighting(
153
+ type: APPROACH_LIGHTING_TYPES.fetch(rwylgt_node.(:LgtApchCat) , :other)
154
+ ).tap do |approach_lighting|
155
+ approach_lighting.length = AIXM.d(rwylgt_node.(:LgtApchLongueur).to_i, :m) if rwylgt_node.(:LgtApchLongueur)
156
+ approach_lighting.intensity = rwylgt_node.(:LgtApchIntensite)&.first_match(/LIH/, /LIM/, /LIL/, default: :other)
157
+ approach_lighting.remarks = {
158
+ 'type' => (rwylgt_node.(:LgtApchCat) if approach_lighting.type == :other),
159
+ 'intensité/intensity' => (rwylgt_node.(:LgtApchIntensite) if approach_lighting.intensity == :other)
160
+ }.to_remarks
161
+ end
162
+ end
163
+ end
164
+
165
+ def lighting_from(rwylgt_node, position)
166
+ prefix = "Lgt" + LIGHTING_POSITIONS.fetch(position)
167
+ if rwylgt_node.(:"#{prefix}Couleur") || rwylgt_node.(:"#{prefix}Longueur")
168
+ AIXM.lighting(position: position).tap do |lighting|
169
+ couleur, intensite = rwylgt_node.(:"#{prefix}Couleur"), rwylgt_node.(:"#{prefix}Intensite")
170
+ lighting.intensity = if intensite
171
+ intensite.first_match(/LIH/, /LIM/, /LIL/, default: :other)
172
+ elsif couleur
173
+ couleur.first_match(/LIH/, /LIM/, /LIL/).tap { couleur.remove!(/LIH|LIM|LIL/) }
174
+ end
175
+ lighting.color = if couleur
176
+ if couleur.match? /ICAO|EASA|OACI|AESA/
177
+ ICAO_LIGHTING_COLORS[position]
178
+ else
179
+ couleur.remove(/[^#{LIGHTING_COLORS.keys.join}]/).compact
180
+ LIGHTING_COLORS.fetch(couleur, :other)
181
+ end
182
+ end
183
+ lighting.description = {
184
+ 'couleur/color' => (rwylgt_node.(:"#{prefix}Couleur") if [nil, :other].include?(lighting.color)),
185
+ 'longueur/length' => rwylgt_node.(:"#{prefix}Longueur"),
186
+ 'espace/spacing' => rwylgt_node.(:"#{prefix}Espace")
187
+ }.to_remarks
188
+ lighting.remarks = rwylgt_node.(:LgtRem)
189
+ end
190
+ end
191
+ end
192
+
193
+ patch AIXM::Feature::Airport, :xy do |parser, object, value|
194
+ throw(:abort) unless coordinate = parser.fixture.dig(object.id, 'xy')
195
+ lat, long = coordinate.split(/\s+/)
196
+ AIXM.xy(lat: lat, long: long)
197
+ end
198
+
199
+ patch AIXM::Feature::Airport, :z do |parser, object, value|
200
+ throw(:abort) unless value.nil?
201
+ throw(:abort, 'fixture missing') unless elevation = parser.fixture.dig(object.id, 'z')
202
+ AIXM.z(elevation, :qnh)
203
+ end
204
+
205
+ patch AIXM::Component::Runway, :dimensions do |parser, object, value|
206
+ throw(:abort) unless value.surface.zero?
207
+ throw(:abort, 'fixture missing') unless dimensions = parser.fixture.dig(object.airport.id, object.name, 'dimensions')
208
+ length, width = dimensions.split(/\D+/)
209
+ length = length&.match?(/^\d+$/) ? AIXM.d(length.to_i, :m) : value.length
210
+ width = width&.match?(/^\d+$/) ? AIXM.d(width.to_i, :m) : value.width
211
+ AIXM.r(length, width).tap { |r| throw(:abort, 'fixture incomplete') if r.surface.zero? }
212
+ end
213
+
214
+ patch AIXM::Component::Runway::Direction, :xy do |parser, object, value|
215
+ throw(:abort) unless value.nil?
216
+ throw(:abort, 'fixture missing') unless coordinate = parser.fixture.dig(object.runway.airport.id, object.name.to_s(:runway), 'xy')
217
+ lat, long = coordinate.split(/\s+/)
218
+ AIXM.xy(lat: lat, long: long)
219
+ end
220
+
221
+ end
222
+ end
223
+ end