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/aip.rb
CHANGED
@@ -3,13 +3,18 @@ module AIPP
|
|
3
3
|
# @abstract
|
4
4
|
class AIP
|
5
5
|
extend Forwardable
|
6
|
+
include AIPP::Debugger
|
6
7
|
include AIPP::Patcher
|
7
8
|
|
8
9
|
DEPENDS = []
|
9
10
|
|
10
|
-
# @return [String] AIP name (
|
11
|
+
# @return [String] AIP name (equal to the parser file name without its
|
12
|
+
# file extension such as "ENR-2.1" implemented in the file "ENR-2.1.rb")
|
11
13
|
attr_reader :aip
|
12
14
|
|
15
|
+
# @return [String] AIP file as passed and possibly updated by `url_for`
|
16
|
+
attr_reader :aip_file
|
17
|
+
|
13
18
|
# @return [Object] Fixture read from YAML file
|
14
19
|
attr_reader :fixture
|
15
20
|
|
@@ -30,55 +35,132 @@ module AIPP
|
|
30
35
|
|
31
36
|
def initialize(aip:, downloader:, fixture:, parser:)
|
32
37
|
@aip, @downloader, @fixture, @parser = aip, downloader, fixture, parser
|
33
|
-
|
38
|
+
setup if respond_to? :setup
|
39
|
+
end
|
40
|
+
|
41
|
+
# @return [String]
|
42
|
+
def inspect
|
43
|
+
"#<AIPP::AIP #{aip}>"
|
34
44
|
end
|
35
45
|
|
36
46
|
# Read an AIP source file
|
37
47
|
#
|
38
|
-
#
|
39
|
-
#
|
40
|
-
# * read from local archive
|
48
|
+
# Read the cached source file if it exists in the source archive. Otherwise,
|
49
|
+
# download it from URL and cache it.
|
41
50
|
#
|
42
|
-
# An URL builder method +url_for(aip_file)+ must be
|
43
|
-
#
|
51
|
+
# An URL builder method +url_for(aip_file)+ must be implemented by the AIP
|
52
|
+
# parser definition (e.g. +ENR-2.1.rb+).
|
44
53
|
#
|
45
|
-
# @param aip_file [String] e.g. "ENR-2.1" or "AD-2.LFMV" (default: +aip+
|
46
|
-
#
|
47
|
-
#
|
54
|
+
# @param aip_file [String] e.g. "ENR-2.1" or "AD-2.LFMV" (default: +aip+
|
55
|
+
# with section stripped e.g. "AD-1.3-2" -> "AD-1.3")
|
56
|
+
# @return [Nokogiri::XML::Document, Nokogiri::HTML5::Document,
|
57
|
+
# Roo::Spreadsheet, String] XML/HTML as Nokogiri document, XLSX/ODS/CSV
|
58
|
+
# as Roo document, PDF and TXT as String
|
48
59
|
def read(aip_file=nil)
|
49
|
-
aip_file
|
50
|
-
|
60
|
+
@aip_file = aip_file || aip.remove(/(?<![A-Z])-\d+$/)
|
61
|
+
url = url_for(@aip_file) # may update aip_file string
|
62
|
+
@downloader.read(document: @aip_file, url: url)
|
51
63
|
end
|
52
64
|
|
53
65
|
# Add feature to AIXM
|
54
66
|
#
|
55
67
|
# @param feature [AIXM::Feature] e.g. airport or airspace
|
68
|
+
# @return [AIXM::Feature] added feature
|
56
69
|
def add(feature)
|
57
|
-
verbose_info "
|
58
|
-
aixm.
|
70
|
+
verbose_info "adding #{feature.inspect}"
|
71
|
+
aixm.add_feature feature
|
72
|
+
feature
|
59
73
|
end
|
60
74
|
|
61
|
-
#
|
62
|
-
# given class and attribute
|
75
|
+
# @!method find_by(klass, attributes={})
|
76
|
+
# Find objects of the given class and optionally with the given attribute
|
77
|
+
# values previously written to AIXM.
|
78
|
+
#
|
79
|
+
# @note This method is delegated to +AIXM::Association::Array+.
|
80
|
+
# @see https://www.rubydoc.info/gems/aixm/AIXM/Association/Array#find_by-instance_method
|
81
|
+
#
|
82
|
+
# @!method find(object)
|
83
|
+
# Find equal objects previously written to AIXM.
|
84
|
+
#
|
85
|
+
# @note This method is delegated to +AIXM::Association::Array+.
|
86
|
+
# @see https://www.rubydoc.info/gems/aixm/AIXM/Association/Array#find-instance_method
|
87
|
+
%i(find_by find).each do |method|
|
88
|
+
define_method method do |*args|
|
89
|
+
aixm.features.send(method, *args)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# @overload given(*objects)
|
94
|
+
# Return +objects+ unless at least one of them equals nil
|
95
|
+
#
|
96
|
+
# @example
|
97
|
+
# # Instead of this:
|
98
|
+
# first, last = unless ((first = expensive_first).nil? || (last = expensive_last).nil?)
|
99
|
+
# [first, last]
|
100
|
+
# end
|
101
|
+
#
|
102
|
+
# # Use the following:
|
103
|
+
# first, last = given(expensive_first, expensive_last)
|
104
|
+
#
|
105
|
+
# @param *objects [Array<Object>] any objects really
|
106
|
+
# @return [Object] nil if at least one of the objects is nil, given
|
107
|
+
# objects otherwise
|
108
|
+
#
|
109
|
+
# @overload given(*objects)
|
110
|
+
# Yield +objects+ unless at least one of them equals nil
|
111
|
+
#
|
112
|
+
# @example
|
113
|
+
# # Instead of this:
|
114
|
+
# name = unless ((first = expensive_first.nil? || (last = expensive_last.nil?)
|
115
|
+
# "#{first} #{last}"
|
116
|
+
# end
|
117
|
+
#
|
118
|
+
# # Use any of the following:
|
119
|
+
# name = given(expensive_first, expensive_last) { |f, l| "#{f} #{l}" }
|
120
|
+
# name = given(expensive_first, expensive_last) { "#{_1} #{_2}" }
|
121
|
+
#
|
122
|
+
# @param *objects [Array<Object>] any objects really
|
123
|
+
# @yield [Array<Object>] objects passed as parameter
|
124
|
+
# @return [Object] nil if at least one of the objects is nil, return of
|
125
|
+
# block otherwise
|
126
|
+
def given(*objects)
|
127
|
+
if objects.none?(&:nil?)
|
128
|
+
block_given? ? yield(*objects) : objects
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Build and optionally check a Markdown link
|
63
133
|
#
|
64
134
|
# @example
|
65
|
-
#
|
66
|
-
#
|
67
|
-
#
|
68
|
-
#
|
69
|
-
#
|
70
|
-
#
|
71
|
-
#
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
135
|
+
# options[:check_links] = false
|
136
|
+
# link_to('foo', 'https://bar.com/exists') # => "[foo](https://bar.com/exists)"
|
137
|
+
# link_to('foo', 'https://bar.com/not-found') # => "[foo](https://bar.com/not-found)"
|
138
|
+
# options[:check_links] = true
|
139
|
+
# link_to('foo', 'https://bar.com/exists') # => "[foo](https://bar.com/exists)"
|
140
|
+
# link_to('foo', 'https://bar.com/not-found') # => nil
|
141
|
+
#
|
142
|
+
# @params body [String] body text of the link
|
143
|
+
# @params url [String] URL of the link
|
144
|
+
# @return [String, nil] Markdown link
|
145
|
+
def link_to(body, url)
|
146
|
+
"[#{body}](#{url})" if !options[:check_links] || url_exists?(url)
|
147
|
+
end
|
148
|
+
|
149
|
+
private
|
150
|
+
|
151
|
+
def url_exists?(url)
|
152
|
+
uri = URI.parse(url)
|
153
|
+
Net::HTTP.new(uri.host, uri.port).tap do |request|
|
154
|
+
request.use_ssl = (uri.scheme == 'https')
|
155
|
+
path = uri.path.present? ? uri.path : '/'
|
156
|
+
result = request.request_head(path)
|
157
|
+
if result.kind_of? Net::HTTPRedirection
|
158
|
+
url_exist?(result['location'])
|
159
|
+
else
|
160
|
+
result.code == '200'
|
79
161
|
end
|
80
162
|
end
|
81
163
|
end
|
82
|
-
end
|
83
164
|
|
165
|
+
end
|
84
166
|
end
|
data/lib/aipp/border.rb
CHANGED
@@ -1,51 +1,90 @@
|
|
1
1
|
module AIPP
|
2
2
|
|
3
|
-
#
|
3
|
+
# Custom border geometries
|
4
4
|
#
|
5
|
-
# The border
|
6
|
-
#
|
7
|
-
#
|
8
|
-
# {
|
9
|
-
# "type": "GeometryCollection",
|
10
|
-
# "geometries": [
|
11
|
-
# {
|
12
|
-
# "type": "LineString",
|
13
|
-
# "coordinates": [
|
14
|
-
# [6.009531650000042, 45.12013319700009],
|
15
|
-
# [6.015747738000073, 45.12006702600007]
|
16
|
-
# ]
|
17
|
-
# }
|
18
|
-
# ]
|
19
|
-
# }
|
20
|
-
#
|
21
|
-
# @example
|
22
|
-
# border = AIPP::Border.new("/path/to/file.geojson")
|
23
|
-
# border.geometries
|
24
|
-
# # => [[#<AIXM::XY 45.12013320N 006.00953165E>, <AIXM::XY 45.12006703N 006.01574774E>]]
|
5
|
+
# The border consists of one ore more open or closed geometries which are
|
6
|
+
# defined by either a GeoJSON file or arrays of coordinate pairs.
|
25
7
|
class Border
|
26
|
-
|
8
|
+
|
9
|
+
# @return [Array<AIXM::XY>]
|
27
10
|
attr_reader :geometries
|
28
11
|
|
29
|
-
def initialize(
|
30
|
-
@
|
31
|
-
fail(ArgumentError, "file must have extension .geojson") unless @file.extname == '.geojson'
|
32
|
-
@geometries = load_geometries
|
12
|
+
def initialize(geometries)
|
13
|
+
@geometries = geometries
|
33
14
|
end
|
34
15
|
|
35
|
-
|
36
|
-
|
37
|
-
|
16
|
+
class << self
|
17
|
+
undef_method :new
|
18
|
+
|
19
|
+
# New border object from GeoJSON file
|
20
|
+
#
|
21
|
+
# The border GeoJSON files must be a geometry collection of one or more
|
22
|
+
# line strings:
|
23
|
+
#
|
24
|
+
# {
|
25
|
+
# "type": "GeometryCollection",
|
26
|
+
# "geometries": [
|
27
|
+
# {
|
28
|
+
# "type": "LineString",
|
29
|
+
# "coordinates": [
|
30
|
+
# [6.009531650000042, 45.12013319700009],
|
31
|
+
# [6.015747738000073, 45.12006702600007]
|
32
|
+
# ]
|
33
|
+
# }
|
34
|
+
# ]
|
35
|
+
# }
|
36
|
+
#
|
37
|
+
# Please note that GeoJSON orders coordinate tuples in mathematical order
|
38
|
+
# as `[longitude, latitude]`!
|
39
|
+
#
|
40
|
+
# @param file [Pathname, String] GeoJSON file
|
41
|
+
#
|
42
|
+
# @example
|
43
|
+
# border = AIPP::Border.from_file("/path/to/national_park.geojson")
|
44
|
+
# border.geometries
|
45
|
+
# # => [[#<AIXM::XY 45.12013320N 006.00953165E>, <AIXM::XY 45.12006703N 006.01574774E>]]
|
46
|
+
def from_file(file)
|
47
|
+
file = Pathname(file) unless file.is_a? Pathname
|
48
|
+
fail(ArgumentError, "file must have extension .geojson") unless file.extname == '.geojson'
|
49
|
+
geometries = JSON.load(file)['geometries'].map do |collection|
|
50
|
+
collection['coordinates'].map do |long, lat|
|
51
|
+
AIXM.xy(lat: lat, long: long)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
allocate.instance_eval do
|
55
|
+
initialize(geometries)
|
56
|
+
self
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# New border object from array of points
|
61
|
+
#
|
62
|
+
# The array must contain coordinate tuples in geographical order as
|
63
|
+
# `latitude longitude` separated by whitespace and/or commas.
|
64
|
+
#
|
65
|
+
# @param array [Array<Array<String>>] one or more arrays of coordinate pairs
|
66
|
+
#
|
67
|
+
# @example
|
68
|
+
# border = AIPP::Border.from_array([["45.1201332 6.00953165", "45.12006703 6.01574774"]])
|
69
|
+
# border.geometries
|
70
|
+
# # => [[#<AIXM::XY 45.12013320N 006.00953165E>, <AIXM::XY 45.12006703N 006.01574774E>]]
|
71
|
+
def from_array(array)
|
72
|
+
geometries = array.map do |collection|
|
73
|
+
collection.map do |coordinates|
|
74
|
+
lat, long = coordinates.split(/[\s,]+/)
|
75
|
+
AIXM.xy(lat: lat.to_f, long: long.to_f)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
allocate.instance_eval do
|
79
|
+
initialize(geometries)
|
80
|
+
self
|
81
|
+
end
|
82
|
+
end
|
38
83
|
end
|
39
84
|
|
40
|
-
# Name of the border
|
41
|
-
#
|
42
|
-
# By convention, the name of the border is taken from the filename with
|
43
|
-
# both the extension .geojson and all non alphanumeric characters dropped
|
44
|
-
# and the resulting string upcased.
|
45
|
-
#
|
46
85
|
# @return [String]
|
47
|
-
def
|
48
|
-
|
86
|
+
def inspect
|
87
|
+
%Q(#<#{self.class} #{@geometries.count} geometries>)
|
49
88
|
end
|
50
89
|
|
51
90
|
# Whether the given geometry is closed or not
|
@@ -72,7 +111,7 @@ module AIPP
|
|
72
111
|
@geometries.each.with_index do |geometry, g_index|
|
73
112
|
next unless geometry_index.nil? || geometry_index == g_index
|
74
113
|
geometry.each.with_index do |coordinates, c_index|
|
75
|
-
distance = xy.distance(coordinates).
|
114
|
+
distance = xy.distance(coordinates).dim
|
76
115
|
if distance < min_distance
|
77
116
|
position = Position.new(geometries: geometries, geometry_index: g_index, coordinates_index: c_index)
|
78
117
|
min_distance = distance
|
@@ -107,14 +146,6 @@ module AIPP
|
|
107
146
|
|
108
147
|
private
|
109
148
|
|
110
|
-
def load_geometries
|
111
|
-
JSON.load(@file)['geometries'].map do |line_string|
|
112
|
-
line_string['coordinates'].map do |long, lat|
|
113
|
-
AIXM.xy(long: long, lat: lat)
|
114
|
-
end
|
115
|
-
end
|
116
|
-
end
|
117
|
-
|
118
149
|
# Position defines an exact point on a border
|
119
150
|
#
|
120
151
|
# @example
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module AIPP
|
2
|
+
module Debugger
|
3
|
+
|
4
|
+
# Start a debugger session and watch for warnings etc
|
5
|
+
#
|
6
|
+
# @note The debugger session persists beyond the scope of the given block
|
7
|
+
# because there's no +DEBUGGER__.stop+ as of now.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# include AIPP::Debugger
|
11
|
+
# with_debugger(verbose: true) do
|
12
|
+
# (...)
|
13
|
+
# warn("all hell broke loose", severe: true)
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# @overload with_debugger(debug_on_warning:, debug_on_error:, verbose:, &block)
|
17
|
+
# @param debug_on_warning [Boolean, Integer] start a debugger session
|
18
|
+
# which opens a console on the warning with the given integer ID or on
|
19
|
+
# all warnings if +true+ is given
|
20
|
+
# @param debug_on_error [Boolean] start a debugger session which opens
|
21
|
+
# a console when an error is raised (postmortem)
|
22
|
+
# @param verbose [Boolean] print verbose info, print unsevere warnings
|
23
|
+
# and re-raise rescued errors
|
24
|
+
# @yield Block the debugger is watching
|
25
|
+
def with_debugger(**options, &)
|
26
|
+
DEBUGGER__.instance_variable_set(:@options__, options.merge(counter: 0))
|
27
|
+
case
|
28
|
+
when id = debugger_options[:debug_on_warning]
|
29
|
+
puts instructions_for(@id == true ? 'warning' : "warning #{id}")
|
30
|
+
DEBUGGER__::start(no_sigint_hook: true, nonstop: true)
|
31
|
+
call_with_rescue(&)
|
32
|
+
when debugger_options[:debug_on_error]
|
33
|
+
puts instructions_for('error')
|
34
|
+
DEBUGGER__::start(no_sigint_hook: true, nonstop: true, postmortem: true)
|
35
|
+
call_without_rescue(&)
|
36
|
+
else
|
37
|
+
DEBUGGER__::start(no_sigint_hook: true, nonstop: true)
|
38
|
+
call_with_rescue(&)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.included(*)
|
43
|
+
DEBUGGER__.instance_variable_set(:@options__, {})
|
44
|
+
end
|
45
|
+
|
46
|
+
# Issue a warning and maybe open a debug session.
|
47
|
+
#
|
48
|
+
# @param message [String] warning message
|
49
|
+
# @param severe [Boolean] whether this problem must be fixed or not
|
50
|
+
alias_method :original_warn, :warn
|
51
|
+
def warn(message, severe: true)
|
52
|
+
if severe || debugger_options[:verbose]
|
53
|
+
debugger_options[:counter] += 1
|
54
|
+
original_warn "WARNING #{debugger_options[:counter]}: #{message.upcase_first} #{'(unsevere)' unless severe}".red
|
55
|
+
debugger if debugger_options[:debug_on_warning] == true || debugger_options[:debug_on_warning] == debugger_options[:counter]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Issue an informational message.
|
60
|
+
#
|
61
|
+
# @param message [String] informational message
|
62
|
+
# @param color [Symbol] message color
|
63
|
+
def info(message, color: nil)
|
64
|
+
puts color ? message.upcase_first.send(color) : message.upcase_first
|
65
|
+
end
|
66
|
+
|
67
|
+
# Issue a verbose informational message.
|
68
|
+
#
|
69
|
+
# @param message [String] verbose informational message
|
70
|
+
# @param color [Symbol] message color
|
71
|
+
def verbose_info(message, color: :blue)
|
72
|
+
info(message, color: color) if debugger_options[:verbose]
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def debugger_options
|
78
|
+
DEBUGGER__.instance_variable_get(:@options__)
|
79
|
+
end
|
80
|
+
|
81
|
+
def call_with_rescue(&block)
|
82
|
+
block.call
|
83
|
+
rescue => error
|
84
|
+
message = error.respond_to?(:original_message) ? error.original_message : error.message
|
85
|
+
puts "ERROR: #{message}".magenta
|
86
|
+
raise if debugger_options[:verbose]
|
87
|
+
end
|
88
|
+
|
89
|
+
def call_without_rescue(&block)
|
90
|
+
block.call
|
91
|
+
end
|
92
|
+
|
93
|
+
def instructions_for(trigger)
|
94
|
+
<<~END.strip.red
|
95
|
+
Debug on #{trigger} enabled.
|
96
|
+
Remember: Type "up" to enter caller frames.
|
97
|
+
END
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
data/lib/aipp/downloader.rb
CHANGED
@@ -3,15 +3,15 @@ module AIPP
|
|
3
3
|
# AIP downloader infrastructure
|
4
4
|
#
|
5
5
|
# The downloader operates in the +storage+ directory where it creates two
|
6
|
-
# subdirectories "
|
7
|
-
# in "
|
8
|
-
# a +document+, the downloader looks for the +document+ in "work" and
|
6
|
+
# subdirectories "sources" and "work". The initializer looks for the +source+
|
7
|
+
# archive in "sources" and (if found) unzips its contents into "work". When
|
8
|
+
# reading a +document+, the downloader looks for the +document+ in "work" and
|
9
9
|
# (unless found) downloads it from +url+. HTML documents are parsed to
|
10
10
|
# +Nokogiri::HTML5::Document+, PDF documents are parsed to +AIPP::PDF+.
|
11
|
-
# Finally, the contents of "work" are written back to +archive
|
11
|
+
# Finally, the contents of "work" are written back to the +source+ archive.
|
12
12
|
#
|
13
13
|
# @example
|
14
|
-
# AIPP::Downloader.new(storage: options[:storage],
|
14
|
+
# AIPP::Downloader.new(storage: options[:storage], source: "2018-11-08") do |downloader|
|
15
15
|
# html = downloader.read(
|
16
16
|
# document: 'ENR-5.1',
|
17
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'
|
@@ -22,51 +22,62 @@ module AIPP
|
|
22
22
|
# )
|
23
23
|
# end
|
24
24
|
class Downloader
|
25
|
+
extend Forwardable
|
26
|
+
include AIPP::Debugger
|
27
|
+
|
28
|
+
# Error when URL results in "404 Not Found" HTTP status
|
29
|
+
class NotFoundError < StandardError; end
|
25
30
|
|
26
31
|
# @return [Pathname] directory to operate within
|
27
32
|
attr_reader :storage
|
28
33
|
|
29
|
-
# @return [String] name of the archive (without extension ".zip")
|
30
|
-
attr_reader :
|
34
|
+
# @return [String] name of the source archive (without extension ".zip")
|
35
|
+
attr_reader :source
|
31
36
|
|
32
|
-
# @return [Pathname] full path to the archive
|
33
|
-
attr_reader :
|
37
|
+
# @return [Pathname] full path to the source archive
|
38
|
+
attr_reader :source_file
|
34
39
|
|
35
40
|
# @param storage [Pathname] directory to operate within
|
36
|
-
# @param
|
37
|
-
def initialize(storage:,
|
38
|
-
@storage, @
|
41
|
+
# @param source [String] name of the source archive (without extension ".zip")
|
42
|
+
def initialize(storage:, source:)
|
43
|
+
@storage, @source = storage, source
|
39
44
|
fail(ArgumentError, 'bad storage directory') unless Dir.exist? storage
|
40
|
-
@
|
45
|
+
@source_file = sources_path.join("#{@source}.zip")
|
41
46
|
prepare
|
42
|
-
unzip if @
|
47
|
+
unzip if @source_file.exist?
|
43
48
|
yield self
|
44
49
|
zip
|
45
50
|
ensure
|
46
51
|
teardown
|
47
52
|
end
|
48
53
|
|
54
|
+
# @return [String]
|
55
|
+
def inspect
|
56
|
+
"#<AIPP::Downloader>"
|
57
|
+
end
|
58
|
+
|
49
59
|
# Download and read +document+
|
50
60
|
#
|
51
61
|
# @param document [String] document to read (without extension)
|
52
62
|
# @param url [String] URL to download the document from
|
53
63
|
# @param type [Symbol, nil] document type: +nil+ (default) to derive it from
|
54
|
-
# the URL, :html, or :
|
55
|
-
# @return [Nokogiri::HTML5::Document, AIPP::PDF]
|
64
|
+
# the URL, :html, :pdf, :xlsx, :ods or :csv
|
65
|
+
# @return [Nokogiri::HTML5::Document, AIPP::PDF, Roo::Spreadsheet]
|
56
66
|
def read(document:, url:, type: nil)
|
57
67
|
type ||= Pathname(URI(url).path).extname[1..-1].to_sym
|
58
68
|
file = work_path.join([document, type].join('.'))
|
59
69
|
unless file.exist?
|
60
|
-
verbose_info "
|
61
|
-
|
70
|
+
verbose_info "downloading #{document}"
|
71
|
+
uri = URI.open(url)
|
72
|
+
IO.copy_stream(uri, file)
|
62
73
|
end
|
63
74
|
convert file
|
64
75
|
end
|
65
76
|
|
66
77
|
private
|
67
78
|
|
68
|
-
def
|
69
|
-
@storage.join('
|
79
|
+
def sources_path
|
80
|
+
@storage.join('sources')
|
70
81
|
end
|
71
82
|
|
72
83
|
def work_path
|
@@ -75,7 +86,7 @@ module AIPP
|
|
75
86
|
|
76
87
|
def prepare
|
77
88
|
teardown
|
78
|
-
|
89
|
+
sources_path.mkpath
|
79
90
|
work_path.mkpath
|
80
91
|
end
|
81
92
|
|
@@ -87,15 +98,15 @@ module AIPP
|
|
87
98
|
end
|
88
99
|
|
89
100
|
def unzip
|
90
|
-
Zip::File.open(
|
101
|
+
Zip::File.open(source_file).each do |entry|
|
91
102
|
entry.extract(work_path.join(entry.name))
|
92
103
|
end
|
93
104
|
end
|
94
105
|
|
95
106
|
def zip
|
96
|
-
backup_file =
|
97
|
-
|
98
|
-
Zip::File.open(
|
107
|
+
backup_file = source_file.sub(/$/, '.old') if source_file.exist?
|
108
|
+
source_file.rename(backup_file) if backup_file
|
109
|
+
Zip::File.open(source_file, Zip::File::CREATE) do |zip|
|
99
110
|
work_path.children.each do |entry|
|
100
111
|
zip.add(entry.basename.to_s, entry) unless entry.basename.to_s[0] == '.'
|
101
112
|
end
|
@@ -105,8 +116,10 @@ module AIPP
|
|
105
116
|
|
106
117
|
def convert(file)
|
107
118
|
case file.extname
|
108
|
-
when '.
|
119
|
+
when '.xml' then Nokogiri.XML(File.open(file))
|
120
|
+
when '.html' then Nokogiri.HTML5(File.open(file))
|
109
121
|
when '.pdf' then AIPP::PDF.new(file)
|
122
|
+
when '.xlsx', '.ods', '.csv' then Roo::Spreadsheet.open(file.to_s)
|
110
123
|
else
|
111
124
|
fail(ArgumentError, "invalid document type")
|
112
125
|
end
|