aipp 0.2.1 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|