aipp 0.2.4 → 1.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 -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
|