aipp 1.0.0 → 2.0.0
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
- checksums.yaml.gz.sig +2 -2
- data/CHANGELOG.md +17 -1
- data/README.md +269 -150
- data/exe/aip2aixm +2 -8
- data/exe/aip2ofmx +2 -8
- data/exe/notam2aixm +5 -0
- data/exe/notam2ofmx +5 -0
- data/lib/aipp/aip/README.md +10 -0
- data/lib/aipp/aip/executable.rb +40 -0
- data/lib/aipp/aip/parser.rb +9 -0
- data/lib/aipp/aip/runner.rb +85 -0
- data/lib/aipp/border.rb +2 -2
- data/lib/aipp/debugger.rb +14 -19
- data/lib/aipp/downloader/file.rb +57 -0
- data/lib/aipp/downloader/graphql.rb +29 -0
- data/lib/aipp/downloader/http.rb +48 -0
- data/lib/aipp/downloader.rb +78 -29
- data/lib/aipp/environment.rb +88 -0
- data/lib/aipp/executable.rb +36 -53
- data/lib/aipp/notam/README.md +25 -0
- data/lib/aipp/notam/executable.rb +27 -0
- data/lib/aipp/notam/parser.rb +9 -0
- data/lib/aipp/notam/runner.rb +28 -0
- data/lib/aipp/parser.rb +133 -160
- data/lib/aipp/patcher.rb +4 -5
- data/lib/aipp/regions/LF/README.md +6 -2
- data/lib/aipp/regions/LF/aip/aerodromes.rb +220 -0
- data/lib/aipp/regions/LF/aip/d_p_r_airspaces.rb +53 -0
- data/lib/aipp/regions/LF/aip/dangerous_activities.rb +48 -0
- data/lib/aipp/regions/LF/aip/designated_points.rb +44 -0
- data/lib/aipp/regions/LF/aip/helipads.rb +119 -0
- data/lib/aipp/regions/LF/aip/navigational_aids.rb +82 -0
- data/lib/aipp/regions/LF/aip/obstacles.rb +150 -0
- data/lib/aipp/regions/LF/aip/serviced_airspaces.rb +67 -0
- data/lib/aipp/regions/LF/aip/services.rb +169 -0
- data/lib/aipp/regions/LF/fixtures/aerodromes.yml +2 -2
- data/lib/aipp/regions/LF/helpers/base.rb +32 -32
- data/lib/aipp/regions/LS/README.md +59 -0
- data/lib/aipp/regions/LS/helpers/base.rb +111 -0
- data/lib/aipp/regions/LS/notam/ENR.rb +173 -0
- data/lib/aipp/runner.rb +152 -0
- data/lib/aipp/version.rb +1 -1
- data/lib/aipp.rb +30 -11
- data/lib/core_ext/array.rb +13 -0
- data/lib/core_ext/nokogiri.rb +56 -8
- data/lib/core_ext/string.rb +63 -1
- data.tar.gz.sig +0 -0
- metadata +115 -64
- metadata.gz.sig +0 -0
- data/lib/aipp/aip.rb +0 -166
- data/lib/aipp/regions/LF/aerodromes.rb +0 -223
- data/lib/aipp/regions/LF/d_p_r_airspaces.rb +0 -56
- data/lib/aipp/regions/LF/dangerous_activities.rb +0 -49
- data/lib/aipp/regions/LF/designated_points.rb +0 -47
- data/lib/aipp/regions/LF/helipads.rb +0 -122
- data/lib/aipp/regions/LF/navigational_aids.rb +0 -85
- data/lib/aipp/regions/LF/obstacles.rb +0 -153
- data/lib/aipp/regions/LF/serviced_airspaces.rb +0 -70
- data/lib/aipp/regions/LF/services.rb +0 -172
data/exe/aip2aixm
CHANGED
@@ -1,11 +1,5 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
require '
|
3
|
+
require 'aipp'
|
4
4
|
|
5
|
-
|
6
|
-
source 'https://rubygems.org'
|
7
|
-
ruby '>= 3.0.0'
|
8
|
-
gem 'aipp', '~> 1'
|
9
|
-
end
|
10
|
-
|
11
|
-
AIPP::Executable.new(schema: File.basename($0)[4..-1].to_sym).run
|
5
|
+
AIPP::AIP::Executable.new(File.basename($0)).run
|
data/exe/aip2ofmx
CHANGED
@@ -1,11 +1,5 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
require '
|
3
|
+
require 'aipp'
|
4
4
|
|
5
|
-
|
6
|
-
source 'https://rubygems.org'
|
7
|
-
ruby '>= 3.0.0'
|
8
|
-
gem 'aipp', '~> 1'
|
9
|
-
end
|
10
|
-
|
11
|
-
AIPP::Executable.new(schema: File.basename($0)[4..-1].to_sym).run
|
5
|
+
AIPP::AIP::Executable.new(File.basename($0)).run
|
data/exe/notam2aixm
ADDED
data/exe/notam2ofmx
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
# AIPP AIP Module
|
2
|
+
|
3
|
+
## Cache Time Window
|
4
|
+
|
5
|
+
The default time window for AIP is the AIRAC cycle. This means:
|
6
|
+
|
7
|
+
* Source data is downloaded and cached once for every AIRAC cycle.
|
8
|
+
* The effective date and time is rounded down to the first day midnight of the AIRAC cycle.
|
9
|
+
|
10
|
+
To force a rebuild within this time window, you have to clean the cache using the `-c` command line argument.
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module AIPP
|
2
|
+
module AIP
|
3
|
+
|
4
|
+
class Executable < AIPP::Executable
|
5
|
+
|
6
|
+
def initialize(exe_file)
|
7
|
+
super
|
8
|
+
AIPP.options.merge(
|
9
|
+
module: 'AIP',
|
10
|
+
airac: AIRAC::Cycle.new,
|
11
|
+
region_options: []
|
12
|
+
)
|
13
|
+
OptionParser.new do |o|
|
14
|
+
o.banner = <<~END
|
15
|
+
Download online AIP and convert it to #{AIPP.options.schema.upcase}.
|
16
|
+
Usage: #{File.basename($0)} [options]
|
17
|
+
END
|
18
|
+
common_options(o)
|
19
|
+
o.on('-a', '--airac (DATE|INTEGER)', String, %Q[AIRAC date or delta e.g. "+1" (default: "#{AIPP.options.airac.date.xmlschema}")]) { AIPP.options.airac = airac_for(_1) }
|
20
|
+
if AIPP.options.schema == :ofmx
|
21
|
+
o.on('-g', '--[no-]grouped-obstacles', 'group obstacles (default: false)') { AIPP.options.grouped_obstacles = _1 }
|
22
|
+
end
|
23
|
+
o.on('-O', '--region-options STRING', String, %Q[comma separated region specific options]) { AIPP.options.region_options = _1.split(',') }
|
24
|
+
developer_options(o)
|
25
|
+
end.parse!
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def airac_for(argument)
|
31
|
+
if argument.match?(/^[+-]\d+$/) # delta
|
32
|
+
AIRAC::Cycle.new + argument.to_i
|
33
|
+
else # date
|
34
|
+
AIRAC::Cycle.new(argument)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module AIPP
|
2
|
+
module AIP
|
3
|
+
|
4
|
+
class Runner < AIPP::Runner
|
5
|
+
|
6
|
+
def effective_at
|
7
|
+
AIPP.options.airac.effective.begin
|
8
|
+
end
|
9
|
+
|
10
|
+
def expiration_at
|
11
|
+
AIPP.options.airac.effective.end
|
12
|
+
end
|
13
|
+
|
14
|
+
def run
|
15
|
+
info("AIP AIRAC #{AIPP.options.airac.id} effective #{effective_at}", color: :green)
|
16
|
+
read_config
|
17
|
+
read_region
|
18
|
+
read_parsers
|
19
|
+
parse_sections
|
20
|
+
validate_aixm
|
21
|
+
write_build
|
22
|
+
write_aixm(AIPP.options.output_file || output_file)
|
23
|
+
write_config
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
# Parse AIP by invoking the parser classes for the current region.
|
29
|
+
def parse_sections
|
30
|
+
super
|
31
|
+
if AIPP.options.grouped_obstacles
|
32
|
+
info("grouping obstacles")
|
33
|
+
aixm.group_obstacles!
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Write the AIXM document and context information.
|
38
|
+
def write_build
|
39
|
+
if AIPP.options.section
|
40
|
+
super
|
41
|
+
else
|
42
|
+
info("writing build")
|
43
|
+
builds_dir.mkpath
|
44
|
+
build_file = builds_dir.join("#{AIPP.options.airac.date.xmlschema}.zip")
|
45
|
+
Dir.mktmpdir do |tmp_dir|
|
46
|
+
tmp_dir = Pathname(tmp_dir)
|
47
|
+
# AIXM/OFMX file
|
48
|
+
AIXM.config.mid = true
|
49
|
+
File.write(tmp_dir.join(output_file), aixm.to_xml)
|
50
|
+
# Build details
|
51
|
+
File.write(
|
52
|
+
tmp_dir.join('build.yaml'), {
|
53
|
+
version: AIPP::VERSION,
|
54
|
+
config: AIPP.config,
|
55
|
+
options: AIPP.options,
|
56
|
+
}.to_yaml
|
57
|
+
)
|
58
|
+
# Manifest
|
59
|
+
manifest = ['AIP','Feature', 'Comment', 'Short Uid Hash', 'Short Feature Hash'].to_csv
|
60
|
+
manifest += aixm.features.map do |feature|
|
61
|
+
xml = feature.to_xml
|
62
|
+
element = xml.first_match(/<(\w{3})\s/)
|
63
|
+
[
|
64
|
+
feature.source.split('|')[2],
|
65
|
+
element,
|
66
|
+
xml.match(/<!-- (.*?) -->/)[1],
|
67
|
+
AIXM::PayloadHash.new(xml.match(%r(<#{element}Uid\s.*?</#{element}Uid>)m).to_s).to_uuid[0,8],
|
68
|
+
AIXM::PayloadHash.new(xml).to_uuid[0,8]
|
69
|
+
].to_csv
|
70
|
+
end.sort.join
|
71
|
+
File.write(tmp_dir.join('manifest.csv'), manifest)
|
72
|
+
# Zip it
|
73
|
+
build_file.delete if build_file.exist?
|
74
|
+
Zip::File.open(build_file, Zip::File::CREATE) do |zip|
|
75
|
+
tmp_dir.children.each do |entry|
|
76
|
+
zip.add(entry.basename.to_s, entry) unless entry.basename.to_s[0] == '.'
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
end
|
data/lib/aipp/border.rb
CHANGED
@@ -35,7 +35,7 @@ module AIPP
|
|
35
35
|
# }
|
36
36
|
#
|
37
37
|
# Please note that GeoJSON orders coordinate tuples in mathematical order
|
38
|
-
# as
|
38
|
+
# as +[longitude, latitude]+!
|
39
39
|
#
|
40
40
|
# @param file [Pathname, String] GeoJSON file
|
41
41
|
#
|
@@ -60,7 +60,7 @@ module AIPP
|
|
60
60
|
# New border object from array of points
|
61
61
|
#
|
62
62
|
# The array must contain coordinate tuples in geographical order as
|
63
|
-
#
|
63
|
+
# +latitude longitude+ separated by whitespace and/or commas.
|
64
64
|
#
|
65
65
|
# @param array [Array<Array<String>>] one or more arrays of coordinate pairs
|
66
66
|
#
|
data/lib/aipp/debugger.rb
CHANGED
@@ -22,14 +22,14 @@ module AIPP
|
|
22
22
|
# @param verbose [Boolean] print verbose info, print unsevere warnings
|
23
23
|
# and re-raise rescued errors
|
24
24
|
# @yield Block the debugger is watching
|
25
|
-
def with_debugger(
|
26
|
-
|
25
|
+
def with_debugger(&)
|
26
|
+
AIPP.cache.debug_counter = 0
|
27
27
|
case
|
28
|
-
when id =
|
28
|
+
when id = AIPP.options.debug_on_warning
|
29
29
|
puts instructions_for(@id == true ? 'warning' : "warning #{id}")
|
30
30
|
DEBUGGER__::start(no_sigint_hook: true, nonstop: true)
|
31
31
|
call_with_rescue(&)
|
32
|
-
when
|
32
|
+
when AIPP.options.debug_on_error
|
33
33
|
puts instructions_for('error')
|
34
34
|
DEBUGGER__::start(no_sigint_hook: true, nonstop: true, postmortem: true)
|
35
35
|
call_without_rescue(&)
|
@@ -39,20 +39,17 @@ module AIPP
|
|
39
39
|
end
|
40
40
|
end
|
41
41
|
|
42
|
-
|
43
|
-
DEBUGGER__.instance_variable_set(:@options__, {})
|
44
|
-
end
|
42
|
+
alias_method :original_warn, :warn
|
45
43
|
|
46
44
|
# Issue a warning and maybe open a debug session.
|
47
45
|
#
|
48
46
|
# @param message [String] warning message
|
49
47
|
# @param severe [Boolean] whether this problem must be fixed or not
|
50
|
-
alias_method :original_warn, :warn
|
51
48
|
def warn(message, severe: true)
|
52
|
-
if severe ||
|
53
|
-
|
54
|
-
original_warn "WARNING #{
|
55
|
-
debugger if
|
49
|
+
if severe || AIPP.options.verbose
|
50
|
+
AIPP.cache.debug_counter += 1
|
51
|
+
original_warn "WARNING #{AIPP.cache.debug_counter}: #{message.upcase_first} #{'(unsevere)' unless severe}".red
|
52
|
+
debugger if AIPP.options.debug_on_warning == true || AIPP.options.debug_on_warning == AIPP.cache.debug_counter
|
56
53
|
end
|
57
54
|
end
|
58
55
|
|
@@ -61,7 +58,9 @@ module AIPP
|
|
61
58
|
# @param message [String] informational message
|
62
59
|
# @param color [Symbol] message color
|
63
60
|
def info(message, color: nil)
|
64
|
-
|
61
|
+
unless AIPP.options.quiet
|
62
|
+
puts color ? message.upcase_first.send(color) : message.upcase_first
|
63
|
+
end
|
65
64
|
end
|
66
65
|
|
67
66
|
# Issue a verbose informational message.
|
@@ -69,21 +68,17 @@ module AIPP
|
|
69
68
|
# @param message [String] verbose informational message
|
70
69
|
# @param color [Symbol] message color
|
71
70
|
def verbose_info(message, color: :blue)
|
72
|
-
info(message, color: color) if
|
71
|
+
info(message, color: color) if AIPP.options.verbose
|
73
72
|
end
|
74
73
|
|
75
74
|
private
|
76
75
|
|
77
|
-
def debugger_options
|
78
|
-
DEBUGGER__.instance_variable_get(:@options__)
|
79
|
-
end
|
80
|
-
|
81
76
|
def call_with_rescue(&block)
|
82
77
|
block.call
|
83
78
|
rescue => error
|
84
79
|
message = error.respond_to?(:original_message) ? error.original_message : error.message
|
85
80
|
puts "ERROR: #{message}".magenta
|
86
|
-
raise if
|
81
|
+
raise if AIPP.options.verbose
|
87
82
|
end
|
88
83
|
|
89
84
|
def call_without_rescue(&block)
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module AIPP
|
2
|
+
class Downloader
|
3
|
+
|
4
|
+
# Local file
|
5
|
+
class File
|
6
|
+
def initialize(archive: nil, file:, type: nil)
|
7
|
+
@archive = Pathname(archive) if archive
|
8
|
+
@file, @type = Pathname(file), type&.to_s
|
9
|
+
end
|
10
|
+
|
11
|
+
def fetch_to(path)
|
12
|
+
path.join(fetched_file).tap do |target|
|
13
|
+
if @archive
|
14
|
+
fail NotFoundError unless @archive.exist?
|
15
|
+
extract(@file, from: @archive, as: target)
|
16
|
+
else
|
17
|
+
fail NotFoundError unless @file.exist?
|
18
|
+
FileUtils.cp(@file, target)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
self
|
22
|
+
end
|
23
|
+
|
24
|
+
def fetched_file
|
25
|
+
[name, type].join('.')
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def name
|
31
|
+
@file.basename(@file.extname).to_s
|
32
|
+
end
|
33
|
+
|
34
|
+
def type
|
35
|
+
@type || @file.extname[1..] || fail("type must be declared")
|
36
|
+
end
|
37
|
+
|
38
|
+
def extract(file, from:, as:)
|
39
|
+
if respond_to?(extractor = 'un' + from.extname[1..], true)
|
40
|
+
send(extractor, file, from: from, as: as) or fail NotFoundError
|
41
|
+
else
|
42
|
+
fail "archive type not recognized"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# @return [Boolean] whether a file was extracted
|
47
|
+
def unzip(file, from:, as:)
|
48
|
+
Zip::File.open(from).inject(nil) do |_, entry|
|
49
|
+
if file.to_s == entry.name
|
50
|
+
break entry.extract(as)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module AIPP
|
2
|
+
class Downloader
|
3
|
+
|
4
|
+
# Remote file via HTTP
|
5
|
+
class GraphQL < File
|
6
|
+
def initialize(client:, query:, variables:)
|
7
|
+
@client, @query, @variables = client, query, variables
|
8
|
+
end
|
9
|
+
|
10
|
+
def fetch_to(path)
|
11
|
+
@client.query(@query, variables: @variables).tap do |result|
|
12
|
+
::File.write(path.join(fetched_file), result.data.to_h.to_json)
|
13
|
+
end
|
14
|
+
self
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def name
|
20
|
+
[@client, @query, @variables].map(&:to_s).join('|').to_digest
|
21
|
+
end
|
22
|
+
|
23
|
+
def type
|
24
|
+
:json
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module AIPP
|
2
|
+
class Downloader
|
3
|
+
|
4
|
+
# Remote file via HTTP
|
5
|
+
class HTTP < File
|
6
|
+
ARCHIVE_MIME_TYPES = {
|
7
|
+
'application/zip' => :zip
|
8
|
+
}.freeze
|
9
|
+
|
10
|
+
def initialize(archive: nil, file:, type: nil, headers: {})
|
11
|
+
@archive = URI(archive) if archive
|
12
|
+
@file, @type, @headers = URI(file), type&.to_s, headers
|
13
|
+
@digest = (archive || file).to_digest
|
14
|
+
end
|
15
|
+
|
16
|
+
# @param path [Pathname] directory where to write the fetched file
|
17
|
+
# @return [File] fetched file
|
18
|
+
def fetch_to(path)
|
19
|
+
response = Excon.get((@archive || @file).to_s, headers: @headers)
|
20
|
+
fail NotFoundError if response.status == 404
|
21
|
+
mime_type = ARCHIVE_MIME_TYPES.fetch(response.headers['Content-Type'], :dat)
|
22
|
+
downloaded_file = path.join([@digest, mime_type].join('.'))
|
23
|
+
::File.write(downloaded_file, response.body)
|
24
|
+
path.join(fetched_file).tap do |target|
|
25
|
+
if @archive
|
26
|
+
extract(@file, from: downloaded_file, as: target)
|
27
|
+
::File.delete(downloaded_file)
|
28
|
+
else
|
29
|
+
::File.rename(downloaded_file, target)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def name
|
38
|
+
path = Pathname(@file.path)
|
39
|
+
path.basename(path.extname).to_s.blank_to_nil || @digest
|
40
|
+
end
|
41
|
+
|
42
|
+
def type
|
43
|
+
@type || Pathname(@file.path).extname[1..].blank_to_nil || fail("type must be declared")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
data/lib/aipp/downloader.rb
CHANGED
@@ -4,28 +4,53 @@ module AIPP
|
|
4
4
|
#
|
5
5
|
# The downloader operates in the +storage+ directory where it creates two
|
6
6
|
# subdirectories "sources" and "work". The initializer looks for the +source+
|
7
|
-
# archive in "sources" and (if found)
|
7
|
+
# archive in "sources" and (if found) unpacks its contents into "work". When
|
8
8
|
# reading a +document+, the downloader looks for the +document+ in "work" and
|
9
|
-
# (
|
10
|
-
#
|
11
|
-
#
|
9
|
+
# (if not found or the clean option is set) downloads it from +origin+.
|
10
|
+
# Finally, the contents of "work" are packed back into the +source+ archive.
|
11
|
+
#
|
12
|
+
# Origins are defined as instances of downloader origin objects:
|
13
|
+
#
|
14
|
+
# * {AIXM::Downloader::File} – local file or archive
|
15
|
+
# * {AIXM::Downloader::HTTP} – remote file or archive via HTTP
|
16
|
+
# * {AIXM::Downloader::GraphQL} – GraphQL query
|
17
|
+
#
|
18
|
+
# The following archives are recognized:
|
19
|
+
#
|
20
|
+
# [.zip] ZIP archive
|
21
|
+
#
|
22
|
+
# The following file types are recognised:
|
23
|
+
#
|
24
|
+
# [.ofmx] Parsed by Nokogiri returning an instance of {Nokogiri::XML::Document}[https://www.rubydoc.info/gems/nokogiri/Nokogiri/XML/Document]
|
25
|
+
# [.xml] Parsed by Nokogiri returning an instance of {Nokogiri::XML::Document}[https://www.rubydoc.info/gems/nokogiri/Nokogiri/XML/Document]
|
26
|
+
# [.html] Parsed by Nokogiri returning an instance of {Nokogiri::HTML5::Document}[https://www.rubydoc.info/gems/nokogiri/Nokogiri/HTML5/Document]
|
27
|
+
# [.pdf] Converted to text – see {AIPP::PDF}
|
28
|
+
# [.json] Deserialized JSON e.g. as response to a GraphQL query
|
29
|
+
# [.xlsx] Parsed by Roo returning an instance of {Roo::Excelx}[https://www.rubydoc.info/gems/roo/Roo/Excelx]
|
30
|
+
# [.ods] Parsed by Roo returning an instance of {Roo::OpenOffice}[https://www.rubydoc.info/gems/roo/Roo/OpenOffice]
|
31
|
+
# [.csv] Parsed by Roo returning an instance of {Roo::CSV}[https://www.rubydoc.info/gems/roo/Roo/CSV]
|
32
|
+
# [.txt] Instance of +String+
|
12
33
|
#
|
13
34
|
# @example
|
14
|
-
# AIPP::Downloader.new(storage: options
|
35
|
+
# AIPP::Downloader.new(storage: AIPP.options.storage, source: "2018-11-08") do |downloader|
|
15
36
|
# html = downloader.read(
|
16
37
|
# document: 'ENR-5.1',
|
17
|
-
#
|
38
|
+
# origin: AIPP::Downloader::HTTP.new(
|
39
|
+
# file: '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'
|
40
|
+
# )
|
18
41
|
# )
|
19
42
|
# pdf = downloader.read(
|
20
43
|
# document: 'VAC-LFMV',
|
21
|
-
#
|
44
|
+
# origin: AIPP::Downloader::HTTP.new(
|
45
|
+
# file: 'https://www.sia.aviation-civile.gouv.fr/dvd/eAIP_08_NOV_2018/Atlas-VAC/PDF_AIPparSSection/VAC/AD/AD-2.LFMV.pdf'
|
46
|
+
# )
|
22
47
|
# )
|
23
48
|
# end
|
24
49
|
class Downloader
|
25
|
-
extend Forwardable
|
26
50
|
include AIPP::Debugger
|
27
51
|
|
28
|
-
# Error when
|
52
|
+
# Error raised when any kind of downloader fails to find the resource e.g.
|
53
|
+
# because the local file does not exist or the remote file is unavailable.
|
29
54
|
class NotFoundError < StandardError; end
|
30
55
|
|
31
56
|
# @return [Pathname] directory to operate within
|
@@ -44,9 +69,15 @@ module AIPP
|
|
44
69
|
fail(ArgumentError, 'bad storage directory') unless Dir.exist? storage
|
45
70
|
@source_file = sources_path.join("#{@source}.zip")
|
46
71
|
prepare
|
47
|
-
|
72
|
+
if @source_file.exist?
|
73
|
+
if AIPP.options.clean
|
74
|
+
@source_file.delete
|
75
|
+
else
|
76
|
+
unpack
|
77
|
+
end
|
78
|
+
end
|
48
79
|
yield self
|
49
|
-
|
80
|
+
pack
|
50
81
|
ensure
|
51
82
|
teardown
|
52
83
|
end
|
@@ -59,17 +90,14 @@ module AIPP
|
|
59
90
|
# Download and read +document+
|
60
91
|
#
|
61
92
|
# @param document [String] document to read (without extension)
|
62
|
-
# @param
|
63
|
-
#
|
64
|
-
#
|
65
|
-
|
66
|
-
|
67
|
-
type ||= Pathname(URI(url).path).extname[1..-1].to_sym
|
68
|
-
file = work_path.join([document, type].join('.'))
|
93
|
+
# @param origin [AIPP::Downloader::File, AIPP::Downloader::HTTP,
|
94
|
+
# AIPP::Downloader::GraphQL] origin to download the document from
|
95
|
+
# @return [Object]
|
96
|
+
def read(document:, origin:)
|
97
|
+
file = work_path.join(origin.fetched_file)
|
69
98
|
unless file.exist?
|
70
99
|
verbose_info "downloading #{document}"
|
71
|
-
|
72
|
-
IO.copy_stream(uri, file)
|
100
|
+
origin.fetch_to(work_path)
|
73
101
|
end
|
74
102
|
convert file
|
75
103
|
end
|
@@ -97,13 +125,11 @@ module AIPP
|
|
97
125
|
end
|
98
126
|
end
|
99
127
|
|
100
|
-
def
|
101
|
-
|
102
|
-
entry.extract(work_path.join(entry.name))
|
103
|
-
end
|
128
|
+
def unpack
|
129
|
+
extract(source_file) or fail
|
104
130
|
end
|
105
131
|
|
106
|
-
def
|
132
|
+
def pack
|
107
133
|
backup_file = source_file.sub(/$/, '.old') if source_file.exist?
|
108
134
|
source_file.rename(backup_file) if backup_file
|
109
135
|
Zip::File.open(source_file, Zip::File::CREATE) do |zip|
|
@@ -114,15 +140,38 @@ module AIPP
|
|
114
140
|
backup_file&.delete
|
115
141
|
end
|
116
142
|
|
143
|
+
def extract(archive, only_entry: nil)
|
144
|
+
case archive.extname
|
145
|
+
when '.zip' then unzip(archive, only_entry: only_entry)
|
146
|
+
else fail(ArgumentError, "unrecognized archive type")
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# @return [Boolean] whether at least one file was extracted
|
151
|
+
def unzip(archive, only_entry:)
|
152
|
+
Zip::File.open(archive).inject(false) do |_, entry|
|
153
|
+
case
|
154
|
+
when only_entry && only_entry == entry.name
|
155
|
+
break !!entry.extract(work_path.join(Pathname(entry.name).basename))
|
156
|
+
when !only_entry
|
157
|
+
!!entry.extract(work_path.join(entry.name))
|
158
|
+
else
|
159
|
+
false
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
117
164
|
def convert(file)
|
118
165
|
case file.extname
|
119
|
-
when '.xml' then Nokogiri.XML(File.open(file))
|
120
|
-
when '.html' then Nokogiri.HTML5(File.open(file))
|
166
|
+
when '.xml', '.ofmx' then Nokogiri.XML(::File.open(file), &:noblanks)
|
167
|
+
when '.html' then Nokogiri.HTML5(::File.open(file))
|
168
|
+
when '.json' then JSON.load_file(file)
|
121
169
|
when '.pdf' then AIPP::PDF.new(file)
|
122
170
|
when '.xlsx', '.ods', '.csv' then Roo::Spreadsheet.open(file.to_s)
|
123
|
-
|
124
|
-
fail(ArgumentError, "
|
171
|
+
when '.txt' then ::File.read(file)
|
172
|
+
else fail(ArgumentError, "unrecognized file type")
|
125
173
|
end
|
126
174
|
end
|
175
|
+
|
127
176
|
end
|
128
177
|
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module AIPP
|
2
|
+
|
3
|
+
# Runtime environment
|
4
|
+
#
|
5
|
+
# Runtime environment objects inherit from OpenStruct but feature some
|
6
|
+
# extensions:
|
7
|
+
#
|
8
|
+
# * Use +replace+ to replace the current key/value table with the given hash.
|
9
|
+
# * Use +merge+ to merge the given hash into the current key/value hash.
|
10
|
+
# * When reading a value using square brackets, the key is implicitly
|
11
|
+
# converted to Symbol.
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# AIPP.config # => AIPP::Environment::Config
|
15
|
+
# AIPP.config.foo # => nil
|
16
|
+
# AIPP.config.foo = :bar # => :bar
|
17
|
+
# AIPP.config.replace(fii: :bir)
|
18
|
+
# AIPP.config.foo # => nil
|
19
|
+
# AIPP.config.fii # => :bir
|
20
|
+
# AIPP.config.read! # method defined on Config class
|
21
|
+
class Environment
|
22
|
+
include Singleton
|
23
|
+
|
24
|
+
# Cache to store transient objects
|
25
|
+
class Cache < OpenStruct
|
26
|
+
def [](key)
|
27
|
+
super(key.to_s.to_sym)
|
28
|
+
end
|
29
|
+
|
30
|
+
def replace(hash)
|
31
|
+
@table = hash
|
32
|
+
end
|
33
|
+
|
34
|
+
def merge(hash)
|
35
|
+
@table.merge! hash
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Borders read from directory containing GeoJSON files
|
40
|
+
class Borders < Cache
|
41
|
+
def read!(dir)
|
42
|
+
@table.clear
|
43
|
+
dir.glob('*.geojson').each do |file|
|
44
|
+
@table[file.basename('.geojson').to_s.to_sym] = AIPP::Border.from_file(file)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Fixtures read from directory containing YAML files
|
50
|
+
class Fixtures < Cache
|
51
|
+
def read!(dir)
|
52
|
+
@table.clear
|
53
|
+
dir.glob('*.yml').each do |file|
|
54
|
+
@table[file.basename('.yml').to_s.to_sym] = YAML.load_file(file)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Options set via the CLI executable
|
60
|
+
class Options < Cache
|
61
|
+
end
|
62
|
+
|
63
|
+
# Config read from config.yml file
|
64
|
+
class Config < Cache
|
65
|
+
def read!(file)
|
66
|
+
@table = YAML.safe_load_file(file, symbolize_names: true, fallback: {}) if file.exist?
|
67
|
+
@table[:namespace] ||= SecureRandom.uuid
|
68
|
+
end
|
69
|
+
|
70
|
+
def write!(file)
|
71
|
+
File.write(file, @table.transform_keys(&:to_s).to_yaml)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def initialize
|
76
|
+
[Cache, Borders, Fixtures, Options, Config].each do |klass|
|
77
|
+
attribute = klass.to_s.split('::').last.downcase
|
78
|
+
instance_variable_set("@#{attribute}", klass.new)
|
79
|
+
AIPP.define_singleton_method(attribute) do
|
80
|
+
Environment.instance.instance_variable_get "@#{attribute}"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
AIPP::Environment.instance
|