aipp 0.2.6 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +21 -0
  4. data/README.md +147 -91
  5. data/exe/aip2aixm +2 -2
  6. data/exe/aip2ofmx +2 -2
  7. data/lib/aipp/aip.rb +96 -11
  8. data/lib/aipp/border.rb +77 -46
  9. data/lib/aipp/debugger.rb +101 -0
  10. data/lib/aipp/downloader.rb +18 -5
  11. data/lib/aipp/executable.rb +33 -20
  12. data/lib/aipp/parser.rb +42 -37
  13. data/lib/aipp/patcher.rb +5 -2
  14. data/lib/aipp/regions/LF/README.md +49 -0
  15. data/lib/aipp/regions/LF/aerodromes.rb +223 -0
  16. data/lib/aipp/regions/LF/d_p_r_airspaces.rb +56 -0
  17. data/lib/aipp/regions/LF/dangerous_activities.rb +49 -0
  18. data/lib/aipp/regions/LF/designated_points.rb +47 -0
  19. data/lib/aipp/regions/LF/fixtures/aerodromes.yml +608 -0
  20. data/lib/aipp/regions/LF/helipads.rb +122 -0
  21. data/lib/aipp/regions/LF/helpers/base.rb +167 -174
  22. data/lib/aipp/regions/LF/helpers/surface.rb +49 -0
  23. data/lib/aipp/regions/LF/helpers/usage_limitation.rb +20 -0
  24. data/lib/aipp/regions/LF/navigational_aids.rb +85 -0
  25. data/lib/aipp/regions/LF/obstacles.rb +153 -0
  26. data/lib/aipp/regions/LF/serviced_airspaces.rb +70 -0
  27. data/lib/aipp/regions/LF/services.rb +172 -0
  28. data/lib/aipp/t_hash.rb +3 -4
  29. data/lib/aipp/version.rb +1 -1
  30. data/lib/aipp.rb +7 -5
  31. data/lib/core_ext/enumerable.rb +2 -2
  32. data/lib/core_ext/hash.rb +21 -5
  33. data/lib/core_ext/nokogiri.rb +54 -0
  34. data/lib/core_ext/string.rb +32 -65
  35. data.tar.gz.sig +0 -0
  36. metadata +70 -81
  37. metadata.gz.sig +0 -0
  38. data/lib/aipp/airac.rb +0 -55
  39. data/lib/aipp/regions/LF/AD-1.3.rb +0 -177
  40. data/lib/aipp/regions/LF/AD-1.6.rb +0 -33
  41. data/lib/aipp/regions/LF/AD-2.rb +0 -344
  42. data/lib/aipp/regions/LF/AD-3.1.rb +0 -185
  43. data/lib/aipp/regions/LF/ENR-2.1.rb +0 -167
  44. data/lib/aipp/regions/LF/ENR-4.1.rb +0 -41
  45. data/lib/aipp/regions/LF/ENR-4.3.rb +0 -27
  46. data/lib/aipp/regions/LF/ENR-5.1.rb +0 -106
  47. data/lib/aipp/regions/LF/ENR-5.4.rb +0 -90
  48. data/lib/aipp/regions/LF/ENR-5.5.rb +0 -55
  49. data/lib/aipp/regions/LF/fixtures/AD-1.3.yml +0 -511
  50. data/lib/aipp/regions/LF/fixtures/AD-2.yml +0 -185
  51. data/lib/aipp/regions/LF/fixtures/AD-3.1.yml +0 -10
  52. data/lib/aipp/regions/LF/helpers/URL.rb +0 -26
  53. data/lib/aipp/regions/LF/helpers/navigational_aid.rb +0 -104
  54. data/lib/aipp/regions/LF/helpers/radio_AD.rb +0 -110
  55. 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
- # Border GeoJSON file reader
3
+ # Custom border geometries
4
4
  #
5
- # The border GeoJSON files must be a geometry collection of one or more
6
- # line strings:
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
- attr_reader :file
8
+
9
+ # @return [Array<AIXM::XY>]
27
10
  attr_reader :geometries
28
11
 
29
- def initialize(file)
30
- @file = file.is_a?(Pathname) ? file : Pathname(file)
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
- # @return [String]
36
- def inspect
37
- %Q(#<#{self.class} file=#{@file}>)
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 name
48
- @file.basename('.geojson').to_s.gsub(/\W/, '').upcase
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).dist
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
@@ -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 :pdf
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 "Downloading #{document}"
61
- IO.copy_stream(URI.open(url), file)
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 '.html' then Nokogiri.HTML5(file)
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
@@ -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
- @options[:airac] = AIPP::AIRAC.new
10
- @options[:storage] = Pathname(Dir.home).join('.aipp')
11
- @options[:force] = @options[:mid] = false
12
- $VERBOSE_INFO = $PRY_ON_WARN = $PRY_ON_ERROR = false
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] = AIPP::AIRAC.new(_1) }
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.upcase }
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 (time-consuming!)') { @options[:grouped_obstacles] = _1 }
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)') { $VERBOSE_INFO = _1 }
28
- o.on('-w', '--pry-on-warn [ID]', Integer, 'open pry on warn with ID (default: nil)') { $PRY_ON_WARN = _1 || true }
29
- o.on('-e', '--[no-]pry-on-error', 'open pry on error (default: false)') { $PRY_ON_ERROR = _1 }
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
- Pry.rescue do
42
- fail(OptionParser::MissingArgument, :region) unless options[:region]
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
- rescue => error
53
- puts "ERROR: #{error.message}".magenta
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("Reading config.yml")
43
- @config = YAML.load_file(config_file, fallback: {}).transform_keys(&:to_sym) if config_file.exist?
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("Reading region #{options[:region]}")
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 "Reading fixture fixtures/#{file.basename}"
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 "Reading border borders/#{file.basename}"
62
- border = AIPP::Border.new(file)
63
- @borders[border.name] = border
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 "Reading helper helpers/#{file.basename}"
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 "Requiring #{file.basename}"
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/).classify]).constantize
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("Parsing #{aip}")
85
- ("AIPP::%s::%s" % [options[:region], aip.remove(/\W/).classify]).constantize.new(
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("Grouping obstacles")
100
+ info("grouping obstacles")
95
101
  aixm.group_obstacles!
96
102
  end
97
- info("Counting #{aixm.features.count} features")
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("Detecting duplicates")
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, pry: binding) : fail(message)
113
+ @options[:force] ? warn(message) : fail(message)
108
114
  end
109
- info("Validating #{options[:schema].upcase}")
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, pry: binding) : fail(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 ("Skipping build")
125
+ info ("skipping build")
120
126
  else
121
- info("Writing build")
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, buffer, feature, aip, uid, comment = [], '', '', '', '', ''
139
- File.open(tmp_dir.join(aixm_file)).each do |line|
140
- buffer << line
141
- case line
142
- when /^ {2}<(\w{3}).*source=".*?\|.*?\|(.*?)\|/ then buffer, feature, aip = line, $1, $2
143
- when /^ {4}<#{feature}Uid[^>]+?mid="(.*?)"/ then uid = $1
144
- when /^ {2}<!-- (.*) -->/ then comment = $1
145
- when /^ {2}<\/#{feature}>/
146
- manifest << [aip, feature, uid[0,8], AIXM::PayloadHash.new(buffer).to_uuid[0,8], comment].to_csv
147
- feature, aip, uid = '', '', ''
148
- end
149
- end
150
- manifest = manifest.sort.prepend "AIP,Feature,Short Uid Hash,Short Feature Hash,Comment\n"
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("Writing #{aixm_file}")
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("Writing config.yml")
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
- verbose_info("PATCH: #{self.inspect}", color: :magenta)
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)