aipp 0.2.6 → 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 +0 -0
- data/CHANGELOG.md +21 -0
- data/README.md +147 -91
- data/exe/aip2aixm +2 -2
- data/exe/aip2ofmx +2 -2
- data/lib/aipp/aip.rb +96 -11
- data/lib/aipp/border.rb +77 -46
- data/lib/aipp/debugger.rb +101 -0
- data/lib/aipp/downloader.rb +18 -5
- data/lib/aipp/executable.rb +33 -20
- data/lib/aipp/parser.rb +42 -37
- data/lib/aipp/patcher.rb +5 -2
- 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 +167 -174
- 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 +3 -4
- data/lib/aipp/version.rb +1 -1
- data/lib/aipp.rb +7 -5
- data/lib/core_ext/enumerable.rb +2 -2
- data/lib/core_ext/hash.rb +21 -5
- data/lib/core_ext/nokogiri.rb +54 -0
- data/lib/core_ext/string.rb +32 -65
- data.tar.gz.sig +0 -0
- metadata +70 -81
- metadata.gz.sig +0 -0
- data/lib/aipp/airac.rb +0 -55
- data/lib/aipp/regions/LF/AD-1.3.rb +0 -177
- data/lib/aipp/regions/LF/AD-1.6.rb +0 -33
- data/lib/aipp/regions/LF/AD-2.rb +0 -344
- data/lib/aipp/regions/LF/AD-3.1.rb +0 -185
- data/lib/aipp/regions/LF/ENR-2.1.rb +0 -167
- data/lib/aipp/regions/LF/ENR-4.1.rb +0 -41
- data/lib/aipp/regions/LF/ENR-4.3.rb +0 -27
- data/lib/aipp/regions/LF/ENR-5.1.rb +0 -106
- data/lib/aipp/regions/LF/ENR-5.4.rb +0 -90
- data/lib/aipp/regions/LF/ENR-5.5.rb +0 -55
- 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/URL.rb +0 -26
- data/lib/aipp/regions/LF/helpers/navigational_aid.rb +0 -104
- data/lib/aipp/regions/LF/helpers/radio_AD.rb +0 -110
- data/lib/core_ext/object.rb +0 -43
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
@@ -22,6 +22,11 @@ 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
|
@@ -46,19 +51,25 @@ module AIPP
|
|
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
|
@@ -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
|
data/lib/aipp/executable.rb
CHANGED
@@ -2,31 +2,40 @@ module AIPP
|
|
2
2
|
|
3
3
|
# Executable instantiated by the console tools
|
4
4
|
class Executable
|
5
|
+
include AIPP::Debugger
|
6
|
+
|
5
7
|
attr_reader :options
|
6
8
|
|
7
9
|
def initialize(**options)
|
8
|
-
@options = options
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
10
|
+
@options = options.merge(
|
11
|
+
airac: AIRAC::Cycle.new,
|
12
|
+
region_options: [],
|
13
|
+
storage: Pathname(Dir.home).join('.aipp'),
|
14
|
+
force: false,
|
15
|
+
mid: false,
|
16
|
+
verbose: false,
|
17
|
+
debug_on_warning: false,
|
18
|
+
debug_on_error: false
|
19
|
+
)
|
13
20
|
OptionParser.new do |o|
|
14
21
|
o.banner = <<~END
|
15
22
|
Download online AIP and convert it to #{options[:schema].upcase}.
|
16
23
|
Usage: #{File.basename($0)} [options]
|
17
24
|
END
|
18
|
-
o.on('-d', '--airac DATE', String, %Q[AIRAC date (default: "#{@options[:airac].date.xmlschema}")]) { @options[:airac] =
|
25
|
+
o.on('-d', '--airac (DATE|INTEGER)', String, %Q[AIRAC date or delta e.g. "+1" (default: "#{@options[:airac].date.xmlschema}")]) { @options[:airac] = airac_for(_1) }
|
19
26
|
o.on('-r', '--region STRING', String, 'region (e.g. "LF")') { @options[:region] = _1.upcase }
|
20
|
-
o.on('-a', '--aip STRING', String, 'process this AIP only (e.g. "ENR-5.1")') { @options[:aip] = _1
|
27
|
+
o.on('-a', '--aip STRING', String, 'process this AIP only (e.g. "ENR-5.1")') { @options[:aip] = _1 }
|
21
28
|
if options[:schema] == :ofmx
|
22
|
-
o.on('-g', '--[no-]grouped-obstacles', 'group obstacles (
|
29
|
+
o.on('-g', '--[no-]grouped-obstacles', 'group obstacles (default: false)') { @options[:grouped_obstacles] = _1 }
|
23
30
|
o.on('-m', '--[no-]mid', 'insert mid attributes into all Uid elements (default: false)') { @options[:mid] = _1 }
|
24
31
|
end
|
32
|
+
o.on('-o', '--region-options STRING', String, %Q[comma separated region specific options]) { @options[:region_options] = _1.split(',') }
|
25
33
|
o.on('-s', '--storage DIR', String, 'storage directory (default: "~/.aipp")') { @options[:storage] = Pathname(_1) }
|
34
|
+
o.on('-h', '--[no-]check-links', 'check all links with HEAD requests') { @options[:check_links] = _1 }
|
26
35
|
o.on('-f', '--[no-]force', 'ignore XML schema validation (default: false)') { @options[:force] = _1 }
|
27
|
-
o.on('-v', '--[no-]verbose', 'verbose output (default: false)') {
|
28
|
-
o.on('-w', '--
|
29
|
-
o.on('-e', '--[no-]
|
36
|
+
o.on('-v', '--[no-]verbose', 'verbose output including unsevere warnings (default: false)') { @options[:verbose] = _1 }
|
37
|
+
o.on('-w', '--debug-on-warning [ID]', Integer, 'open debug session on warning with ID (default: false)') { @options[:debug_on_warning] = _1 || true }
|
38
|
+
o.on('-e', '--[no-]debug-on-error', 'open debug session on error (default: false)') { @options[:debug_on_error] = _1 }
|
30
39
|
o.on('-A', '--about', 'show author/license information and exit') { about }
|
31
40
|
o.on('-R', '--readme', 'show README and exit') { readme }
|
32
41
|
o.on('-L', '--list', 'list implemented regions and AIPs') { list }
|
@@ -34,12 +43,9 @@ module AIPP
|
|
34
43
|
end.parse!
|
35
44
|
end
|
36
45
|
|
37
|
-
# Load necessary files and execute the parser.
|
38
|
-
#
|
39
|
-
# @raise [RuntimeError] if the region does not exist
|
40
46
|
def run
|
41
|
-
|
42
|
-
|
47
|
+
with_debugger(**options.slice(:verbose, :debug_on_warning, :debug_on_error)) do
|
48
|
+
starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
43
49
|
AIPP::Parser.new(options: options).tap do |parser|
|
44
50
|
parser.read_config
|
45
51
|
parser.read_region
|
@@ -49,14 +55,21 @@ module AIPP
|
|
49
55
|
parser.write_aixm
|
50
56
|
parser.write_config
|
51
57
|
end
|
52
|
-
|
53
|
-
|
54
|
-
Pry::rescued(error) if $PRY_ON_ERROR
|
58
|
+
ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
59
|
+
info("finished after %s" % Time.at(ending - starting).utc.strftime("%H:%M:%S"))
|
55
60
|
end
|
56
61
|
end
|
57
62
|
|
58
63
|
private
|
59
64
|
|
65
|
+
def airac_for(argument)
|
66
|
+
if argument.match?(/^[+-]\d+$/) # delta
|
67
|
+
AIRAC::Cycle.new + argument.to_i
|
68
|
+
else # date
|
69
|
+
AIRAC::Cycle.new(argument)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
60
73
|
def about
|
61
74
|
puts 'Written by Sven Schwyn (bitcetera.com) and distributed under MIT license.'
|
62
75
|
exit
|
@@ -83,6 +96,6 @@ module AIPP
|
|
83
96
|
puts AIPP::VERSION
|
84
97
|
exit
|
85
98
|
end
|
86
|
-
end
|
87
99
|
|
100
|
+
end
|
88
101
|
end
|
data/lib/aipp/parser.rb
CHANGED
@@ -2,7 +2,8 @@ module AIPP
|
|
2
2
|
|
3
3
|
# AIP parser infrastructure
|
4
4
|
class Parser
|
5
|
-
|
5
|
+
extend Forwardable
|
6
|
+
include AIPP::Debugger
|
6
7
|
using AIXM::Refinements
|
7
8
|
|
8
9
|
# @return [Hash] passed command line arguments
|
@@ -37,42 +38,47 @@ module AIPP
|
|
37
38
|
AIXM.config.region = options[:region]
|
38
39
|
end
|
39
40
|
|
41
|
+
# @return [String]
|
42
|
+
def inspect
|
43
|
+
"#<AIPP::Parser>"
|
44
|
+
end
|
45
|
+
|
40
46
|
# Read the configuration from config.yml.
|
41
47
|
def read_config
|
42
|
-
info("
|
43
|
-
@config = YAML.load_file(config_file, fallback: {})
|
48
|
+
info("reading config.yml")
|
49
|
+
@config = YAML.load_file(config_file, symbolize_names: true, fallback: {}) if config_file.exist?
|
44
50
|
@config[:namespace] ||= SecureRandom.uuid
|
45
51
|
@aixm.namespace = @config[:namespace]
|
46
52
|
end
|
47
53
|
|
48
54
|
# Read the region directory and build the dependency list.
|
49
55
|
def read_region
|
50
|
-
info("
|
56
|
+
info("reading region #{options[:region]}")
|
51
57
|
dir = Pathname(__FILE__).dirname.join('regions', options[:region])
|
52
58
|
fail("unknown region `#{options[:region]}'") unless dir.exist?
|
53
59
|
# Fixtures
|
54
60
|
dir.glob('fixtures/*.yml').each do |file|
|
55
|
-
verbose_info "
|
61
|
+
verbose_info "reading fixture fixtures/#{file.basename}"
|
56
62
|
fixture = YAML.load_file(file)
|
57
63
|
@fixtures[file.basename('.yml').to_s] = fixture
|
58
64
|
end
|
59
65
|
# Borders
|
60
66
|
dir.glob('borders/*.geojson').each do |file|
|
61
|
-
verbose_info "
|
62
|
-
border = AIPP::Border.
|
63
|
-
@borders[
|
67
|
+
verbose_info "reading border borders/#{file.basename}"
|
68
|
+
border = AIPP::Border.from_file(file)
|
69
|
+
@borders[file.basename] = border
|
64
70
|
end
|
65
71
|
# Helpers
|
66
72
|
dir.glob('helpers/*.rb').each do |file|
|
67
|
-
verbose_info "
|
73
|
+
verbose_info "reading helper helpers/#{file.basename}"
|
68
74
|
require file
|
69
75
|
end
|
70
76
|
# Parsers
|
71
77
|
dir.glob('*.rb').each do |file|
|
72
|
-
verbose_info "
|
78
|
+
verbose_info "requiring #{file.basename}"
|
73
79
|
require file
|
74
80
|
aip = file.basename('.*').to_s
|
75
|
-
@dependencies[aip] = ("AIPP::%s::%s::DEPENDS" % [options[:region], aip.remove(/\W/).
|
81
|
+
@dependencies[aip] = ("AIPP::%s::%s::DEPENDS" % [options[:region], aip.remove(/\W/).camelcase]).constantize
|
76
82
|
end
|
77
83
|
end
|
78
84
|
|
@@ -81,8 +87,8 @@ module AIPP
|
|
81
87
|
info("AIRAC #{options[:airac].id} effective #{options[:airac].date}", color: :green)
|
82
88
|
AIPP::Downloader.new(storage: options[:storage], source: options[:airac].date.xmlschema) do |downloader|
|
83
89
|
@dependencies.tsort(options[:aip]).each do |aip|
|
84
|
-
info("
|
85
|
-
("AIPP::%s::%s" % [options[:region], aip.remove(/\W/).
|
90
|
+
info("parsing #{aip}")
|
91
|
+
("AIPP::%s::%s" % [options[:region], aip.remove(/\W/).camelcase]).constantize.new(
|
86
92
|
aip: aip,
|
87
93
|
downloader: downloader,
|
88
94
|
fixture: @fixtures[aip],
|
@@ -91,34 +97,34 @@ module AIPP
|
|
91
97
|
end
|
92
98
|
end
|
93
99
|
if options[:grouped_obstacles]
|
94
|
-
info("
|
100
|
+
info("grouping obstacles")
|
95
101
|
aixm.group_obstacles!
|
96
102
|
end
|
97
|
-
info("
|
103
|
+
info("counting #{aixm.features.count} features")
|
98
104
|
end
|
99
105
|
|
100
106
|
# Validate the AIXM document.
|
101
107
|
#
|
102
108
|
# @raise [RuntimeError] if the document is not valid
|
103
109
|
def validate_aixm
|
104
|
-
info("
|
110
|
+
info("detecting duplicates")
|
105
111
|
if (duplicates = aixm.features.duplicates).any?
|
106
112
|
message = "duplicates found:\n" + duplicates.map { "#{_1.inspect} from #{_1.source}" }.join("\n")
|
107
|
-
@options[:force] ? warn(message
|
113
|
+
@options[:force] ? warn(message) : fail(message)
|
108
114
|
end
|
109
|
-
info("
|
115
|
+
info("validating #{options[:schema].upcase}")
|
110
116
|
unless aixm.valid?
|
111
117
|
message = "invalid #{options[:schema].upcase} document:\n" + aixm.errors.map(&:message).join("\n")
|
112
|
-
@options[:force] ? warn(message
|
118
|
+
@options[:force] ? warn(message) : fail(message)
|
113
119
|
end
|
114
120
|
end
|
115
121
|
|
116
122
|
# Write the AIXM document and context information.
|
117
123
|
def write_build
|
118
124
|
if @options[:aip]
|
119
|
-
info ("
|
125
|
+
info ("skipping build")
|
120
126
|
else
|
121
|
-
info("
|
127
|
+
info("writing build")
|
122
128
|
builds_path.mkpath
|
123
129
|
build_file = builds_path.join("#{@options[:airac].date.xmlschema}.zip")
|
124
130
|
Dir.mktmpdir do |tmp_dir|
|
@@ -135,20 +141,19 @@ module AIPP
|
|
135
141
|
}.to_yaml
|
136
142
|
)
|
137
143
|
# Manifest
|
138
|
-
manifest
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
end
|
150
|
-
manifest
|
151
|
-
File.write(tmp_dir.join('manifest.csv'), manifest.join)
|
144
|
+
manifest = ['AIP','Feature', 'Comment', 'Short Uid Hash', 'Short Feature Hash'].to_csv
|
145
|
+
manifest += aixm.features.map do |feature|
|
146
|
+
xml = feature.to_xml
|
147
|
+
element = xml.first_match(/<(\w{3})\s/)
|
148
|
+
[
|
149
|
+
feature.source.split('|')[2],
|
150
|
+
element,
|
151
|
+
xml.match(/<!-- (.*?) -->/)[1],
|
152
|
+
AIXM::PayloadHash.new(xml.match(%r(<#{element}Uid\s.*?</#{element}Uid>)m).to_s).to_uuid[0,8],
|
153
|
+
AIXM::PayloadHash.new(xml).to_uuid[0,8]
|
154
|
+
].to_csv
|
155
|
+
end.sort.join
|
156
|
+
File.write(tmp_dir.join('manifest.csv'), manifest)
|
152
157
|
# Zip it
|
153
158
|
build_file.delete if build_file.exist?
|
154
159
|
Zip::File.open(build_file, Zip::File::CREATE) do |zip|
|
@@ -162,14 +167,14 @@ module AIPP
|
|
162
167
|
|
163
168
|
# Write the AIXM document.
|
164
169
|
def write_aixm
|
165
|
-
info("
|
170
|
+
info("writing #{aixm_file}")
|
166
171
|
AIXM.config.mid = options[:mid]
|
167
172
|
File.write(aixm_file, aixm.to_xml)
|
168
173
|
end
|
169
174
|
|
170
175
|
# Write the configuration to config.yml.
|
171
176
|
def write_config
|
172
|
-
info("
|
177
|
+
info("writing config.yml")
|
173
178
|
File.write(config_file, config.to_yaml)
|
174
179
|
end
|
175
180
|
|
data/lib/aipp/patcher.rb
CHANGED
@@ -18,14 +18,16 @@ module AIPP
|
|
18
18
|
|
19
19
|
def attach_patches
|
20
20
|
parser = self
|
21
|
+
verbose_info_method = method(:verbose_info)
|
21
22
|
self.class.patches[self.class]&.each do |(klass, attribute, block)|
|
22
23
|
klass.instance_eval do
|
23
24
|
alias_method :"original_#{attribute}=", :"#{attribute}="
|
24
25
|
define_method(:"#{attribute}=") do |value|
|
25
|
-
catch :abort do
|
26
|
+
error = catch :abort do
|
26
27
|
value = block.call(parser, self, value)
|
27
|
-
|
28
|
+
verbose_info_method.call("Patching #{self.inspect} with #{attribute}=#{value.inspect}", color: :magenta)
|
28
29
|
end
|
30
|
+
fail "patching #{self.inspect} with #{attribute}=#{value.inspect} failed: #{error}" if error
|
29
31
|
send(:"original_#{attribute}=", value)
|
30
32
|
end
|
31
33
|
end
|
@@ -36,6 +38,7 @@ module AIPP
|
|
36
38
|
def detach_patches
|
37
39
|
self.class.patches[self.class]&.each do |(klass, attribute, _)|
|
38
40
|
klass.instance_eval do
|
41
|
+
remove_method :"#{attribute}="
|
39
42
|
alias_method :"#{attribute}=", :"original_#{attribute}="
|
40
43
|
remove_method :"original_#{attribute}="
|
41
44
|
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# AIP LF – Mainland France
|
2
|
+
|
3
|
+
## Prerequisites
|
4
|
+
|
5
|
+
This parser requires the XML data dump from SIA. It is available free of charge, but has to be ordered before it can be downloaded. It's therefore necessary to perform the following steps before running the parser for any given AIRAC for the first time:
|
6
|
+
|
7
|
+
1. Browse to the [SIA web shop](https://www.sia.aviation-civile.gouv.fr/produits-numeriques-en-libre-disposition/les-bases-de-donnees-sia.html).
|
8
|
+
2. Shop the desired dump named «données aéronautiques XML AIRAC ii/yy».
|
9
|
+
3. Browse to [your customer page](https://www.sia.aviation-civile.gouv.fr/customer/account/#orders-and-proposals).
|
10
|
+
4. In section «mes produits téléchargeables» download the desired dump.
|
11
|
+
5. Unzip the downloaded ZIP archive.
|
12
|
+
6. Move the file «XML_SIA_yyyy-mm-dd.xml» to the directory in which you will execute the parser.
|
13
|
+
|
14
|
+
⚠️ The SIA web shop misbehaves with some browsers, you should try Brave or Chrome.
|
15
|
+
|
16
|
+
## Region Options
|
17
|
+
|
18
|
+
### Obstacles XLSX
|
19
|
+
|
20
|
+
While the XML data dump contains all obstacles, some details of the source XLSX file are omitted. Unfortunately, the latter is only available for the current AIRAC cycle, therefore the XML data dump is used by default. Add `-o lf_obstacles_xlsx` to use the source XLSX file instead.
|
21
|
+
|
22
|
+
## Charset
|
23
|
+
|
24
|
+
The XML data dump from SIA is ISO-8859-1 encoded. Nokogiri which parses the XML converts this to UTF-8 on the fly, however, when grepping the dump on a shell, you might run into trouble:
|
25
|
+
|
26
|
+
```shell
|
27
|
+
grep "<Revetement>" XML_SIA_2021-12-02.xml | sort | uniq
|
28
|
+
|
29
|
+
sort: Illegal byte sequence
|
30
|
+
```
|
31
|
+
|
32
|
+
For this to work, you have to convert the dump to UTF-8 and use this converted dump for grepping:
|
33
|
+
|
34
|
+
```shell
|
35
|
+
iconv -f ISO-8859-1 -t UTF-8 XML_SIA_2021-12-02.xml >XML_SIA_2021-12-02_UTF.xml
|
36
|
+
grep "<Revetement>" XML_SIA_2021-12-02_UTF.xml | sort | uniq
|
37
|
+
|
38
|
+
<Revetement>Aluminium</Revetement>
|
39
|
+
<Revetement>Asphalte</Revetement>
|
40
|
+
<Revetement>Béton ( 4t )</Revetement>
|
41
|
+
(...)
|
42
|
+
```
|
43
|
+
|
44
|
+
## References
|
45
|
+
|
46
|
+
* [SIA – AIP publisher](https://www.sia.aviation-civile.gouv.fr)
|
47
|
+
* [SIA XML usage guide](https://www.sia.aviation-civile.gouv.fr/faqs)
|
48
|
+
* [OpenData – public data files](https://www.data.gouv.fr)
|
49
|
+
* [Protected Planet – protected area data files](https://www.protectedplanet.net)
|