aipp 0.2.4 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +2 -0
- data/CHANGELOG.md +38 -0
- data/README.md +222 -88
- data/exe/aip2aixm +2 -2
- data/exe/aip2ofmx +2 -2
- data/lib/aipp/aip.rb +113 -31
- data/lib/aipp/border.rb +77 -46
- data/lib/aipp/debugger.rb +101 -0
- data/lib/aipp/downloader.rb +39 -26
- data/lib/aipp/executable.rb +41 -22
- data/lib/aipp/parser.rb +94 -21
- data/lib/aipp/patcher.rb +5 -2
- data/lib/aipp/pdf.rb +1 -1
- data/lib/aipp/regions/LF/README.md +49 -0
- data/lib/aipp/regions/LF/aerodromes.rb +223 -0
- data/lib/aipp/regions/LF/d_p_r_airspaces.rb +56 -0
- data/lib/aipp/regions/LF/dangerous_activities.rb +49 -0
- data/lib/aipp/regions/LF/designated_points.rb +47 -0
- data/lib/aipp/regions/LF/fixtures/aerodromes.yml +608 -0
- data/lib/aipp/regions/LF/helipads.rb +122 -0
- data/lib/aipp/regions/LF/helpers/base.rb +218 -0
- data/lib/aipp/regions/LF/helpers/surface.rb +49 -0
- data/lib/aipp/regions/LF/helpers/usage_limitation.rb +20 -0
- data/lib/aipp/regions/LF/navigational_aids.rb +85 -0
- data/lib/aipp/regions/LF/obstacles.rb +153 -0
- data/lib/aipp/regions/LF/serviced_airspaces.rb +70 -0
- data/lib/aipp/regions/LF/services.rb +172 -0
- data/lib/aipp/t_hash.rb +4 -5
- data/lib/aipp/version.rb +1 -1
- data/lib/aipp.rb +11 -5
- data/lib/core_ext/enumerable.rb +9 -9
- data/lib/core_ext/hash.rb +21 -5
- data/lib/core_ext/nokogiri.rb +54 -0
- data/lib/core_ext/string.rb +38 -66
- data.tar.gz.sig +2 -0
- metadata +180 -188
- metadata.gz.sig +0 -0
- data/.gitignore +0 -8
- data/.ruby-version +0 -1
- data/.travis.yml +0 -8
- data/.yardopts +0 -3
- data/Guardfile +0 -7
- data/TODO.md +0 -6
- data/aipp.gemspec +0 -44
- data/gems.rb +0 -3
- data/lib/aipp/airac.rb +0 -55
- data/lib/aipp/regions/LF/AD-1.3.rb +0 -162
- data/lib/aipp/regions/LF/AD-1.6.rb +0 -31
- data/lib/aipp/regions/LF/AD-2.rb +0 -313
- data/lib/aipp/regions/LF/AD-3.1.rb +0 -185
- data/lib/aipp/regions/LF/ENR-2.1.rb +0 -92
- data/lib/aipp/regions/LF/ENR-4.1.rb +0 -97
- data/lib/aipp/regions/LF/ENR-4.3.rb +0 -28
- data/lib/aipp/regions/LF/ENR-5.1.rb +0 -75
- data/lib/aipp/regions/LF/ENR-5.5.rb +0 -53
- data/lib/aipp/regions/LF/fixtures/AD-1.3.yml +0 -511
- data/lib/aipp/regions/LF/fixtures/AD-2.yml +0 -185
- data/lib/aipp/regions/LF/fixtures/AD-3.1.yml +0 -10
- data/lib/aipp/regions/LF/helpers/AD_radio.rb +0 -90
- data/lib/aipp/regions/LF/helpers/URL.rb +0 -26
- data/lib/aipp/regions/LF/helpers/common.rb +0 -217
- data/lib/core_ext/object.rb +0 -43
- data/rakefile.rb +0 -12
- data/spec/fixtures/archive.zip +0 -0
- data/spec/fixtures/border.geojson +0 -201
- data/spec/fixtures/document.pdf +0 -0
- data/spec/fixtures/document.pdf.json +0 -1
- data/spec/fixtures/new.html +0 -6
- data/spec/fixtures/new.pdf +0 -0
- data/spec/fixtures/new.txt +0 -1
- data/spec/lib/aipp/airac_spec.rb +0 -98
- data/spec/lib/aipp/border_spec.rb +0 -135
- data/spec/lib/aipp/downloader_spec.rb +0 -81
- data/spec/lib/aipp/patcher_spec.rb +0 -46
- data/spec/lib/aipp/pdf_spec.rb +0 -124
- data/spec/lib/aipp/t_hash_spec.rb +0 -44
- data/spec/lib/aipp/version_spec.rb +0 -7
- data/spec/lib/core_ext/enumberable_spec.rb +0 -76
- data/spec/lib/core_ext/hash_spec.rb +0 -27
- data/spec/lib/core_ext/integer_spec.rb +0 -15
- data/spec/lib/core_ext/nil_class_spec.rb +0 -11
- data/spec/lib/core_ext/string_spec.rb +0 -112
- data/spec/sounds/failure.mp3 +0 -0
- data/spec/sounds/success.mp3 +0 -0
- data/spec/spec_helper.rb +0 -28
data/lib/aipp/executable.rb
CHANGED
@@ -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
|
-
|
10
|
-
|
11
|
-
|
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}")]) {
|
18
|
-
o.on('-r', '--region STRING', String, 'region (e.g. "LF")') {
|
19
|
-
o.on('-a', '--aip STRING', String, 'process this AIP only (e.g. "ENR-5.1")') {
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
o.on('-
|
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
|
-
|
37
|
-
|
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
|
-
|
47
|
-
|
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')
|
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
|
30
|
+
@options[:storage].mkpath
|
28
31
|
@config = {}
|
29
|
-
@aixm = AIXM.document(
|
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("
|
40
|
-
@config = YAML.load_file(config_file, fallback: {})
|
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("
|
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 "
|
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 "
|
59
|
-
border = AIPP::Border.
|
60
|
-
@borders[
|
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 "
|
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 "
|
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/).
|
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],
|
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("
|
82
|
-
("AIPP::%s::%s" % [options[:region], aip.remove(/\W/).
|
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("
|
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
|
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
|
-
|
106
|
-
|
107
|
-
File.write(
|
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("
|
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
|
-
|
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
@@ -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
|