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,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
|
data/lib/aipp/executable.rb
CHANGED
@@ -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[:
|
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('-
|
24
|
-
o.on('-
|
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
|
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
|
48
|
+
Pry::rescued(error) if $PRY_ON_ERROR
|
49
49
|
end
|
50
50
|
end
|
51
51
|
|
data/lib/aipp/parser.rb
CHANGED
@@ -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"
|
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]}"
|
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
|
-
|
43
|
+
debug("Requiring #{file.basename}")
|
45
44
|
require file
|
46
|
-
|
47
|
-
|
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}",
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
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}"
|
69
|
+
info("Validating #{options[:schema].upcase}")
|
71
70
|
unless aixm.valid?
|
72
|
-
|
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}"
|
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"
|
86
|
+
info("Writing config.yml")
|
87
87
|
File.write(config_file, config.to_yaml)
|
88
88
|
end
|
89
89
|
|
data/lib/aipp/patcher.rb
ADDED
@@ -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
|
data/lib/aipp/pdf.rb
ADDED
@@ -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
|