aipp 0.2.2 → 0.2.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8728d9b2f455af64005813f575779ce93a2bf19167afb0574ce603ef0ed3582f
4
- data.tar.gz: 6d901cc39dee66f917e56508520bf78383863375f90a72264909b60996c7d57d
3
+ metadata.gz: 6d97409845a7b08285c77294aa116873fdcb668bd0f10d13d2ceecd9ef3922a9
4
+ data.tar.gz: 2594b4ad9bd0b9deba766704778ff1710eaaf71ce675bb4fd378481f4f13dfc6
5
5
  SHA512:
6
- metadata.gz: 908575b22b787ca1743333146b113b89aea0a1e8919f0ae50abc2bf306bc11fcaec8a12780a56f3591eb96bb7790924a7959d15f0b4d06f68847e527f14782ec
7
- data.tar.gz: 12a04dab59f3d229624938c21b64f1c05f15352ed1f5617c5fb05745b0230ef2b80a1fd54024d65375e452fdf8d79d7320ff5fa1d396f03ef1764c4ce4a91b63
6
+ metadata.gz: aa0ac5dafcc6e36a113e2a97c270f768265379b09c34a833e785adf44f45e5dfcd5bb3951b0d6741ae15bc765386eb55c3118154a48ce568f9fae14f06bf6ae5
7
+ data.tar.gz: 92dd8d6134a1882b0ceada8e130ed834e3dc866e4b84e2dd513084c5313a42d8fd89b47d3726ff36e1f38bb8837fa190790bcc3f99d752e7504757e1a2b4eb84
@@ -1,4 +1,4 @@
1
1
  ---
2
2
  language: ruby
3
3
  rvm:
4
- - 2.6.0
4
+ - 2.6.3
@@ -1,3 +1,12 @@
1
+ ## 0.2.3
2
+
3
+ #### Additons
4
+ * Borders defined as GeoJSON (used by LF/ENR-2.1)
5
+ * LF/AD-5.5
6
+
7
+ #### Breaking Changes
8
+ * Renamed `AIPP::AIP#write` method to `AIPP::AIP#add`
9
+
1
10
  ## 0.2.2
2
11
 
3
12
  #### Changes
data/README.md CHANGED
@@ -51,9 +51,9 @@ module AIPP
51
51
  DEPENDS = %w(ENR-2.1 ENR-2.2) # declare dependencies to other AIPs
52
52
 
53
53
  def parse
54
- html = read # read the Nokogiri::HTML5 document
55
- feature = (...) # build the feature
56
- write(feature: feature) # write the feature to AIXM::Document
54
+ html = read # read the Nokogiri::HTML5 document
55
+ feature = (...) # build the feature
56
+ add(feature: feature) # add the feature to AIXM::Document
57
57
  end
58
58
 
59
59
  end
@@ -84,16 +84,50 @@ end
84
84
  Inside the `parse` method, you have access to the following methods:
85
85
 
86
86
  * [`read`](https://www.rubydoc.info/gems/aipp/AIPP/AIP#read-instance_method) – download and read an AIP file
87
- * [`write`](https://www.rubydoc.info/gems/aipp/AIPP/AIP#write-instance_method) – write a [`AIXM::Feature`]([AIXM Rubygem](https://github.com/svoop/aixm)
88
- * [`select`](https://www.rubydoc.info/gems/aipp/AIPP/AIP#find-instance_method – search previously written [`AIXM::Feature`s]([AIXM Rubygem](https://github.com/svoop/aixm)
87
+ * [`add`](https://www.rubydoc.info/gems/aipp/AIPP/AIP#add-instance_method) – add a [`AIXM::Feature`](https://www.rubydoc.info/gems/aixm/AIXM/Feature)
88
+ * [`select`](https://www.rubydoc.info/gems/aipp/AIPP/AIP#find-instance_method – search previously written [`AIXM::Feature`s](https://www.rubydoc.info/gems/aixm/AIXM/Feature)
89
89
  * some core extensions from ActiveSupport – [`Object#blank`](https://www.rubydoc.info/gems/activesupport/Object#blank%3F-instance_method) and [`String`](https://www.rubydoc.info/gems/activesupport/String)
90
- * core extensions from this gem – [`Object`](https://www.rubydoc.info/gems/aipp/Object), [`String`](https://www.rubydoc.info/gems/aipp/String), [`NilClass`](https://www.rubydoc.info/gems/aipp/NilClass) and [`Enumerable`](https://www.rubydoc.info/gems/aipp/Enumerable)
90
+ * core extensions from this gem – [`Object`](https://www.rubydoc.info/gems/aipp/Object), [`Integer`](https://www.rubydoc.info/gems/aipp/Integer), [`String`](https://www.rubydoc.info/gems/aipp/String), [`NilClass`](https://www.rubydoc.info/gems/aipp/NilClass) and [`Enumerable`](https://www.rubydoc.info/gems/aipp/Enumerable)
91
+
92
+ As well as the following methods:
93
+
94
+ * [`options`](https://www.rubydoc.info/gems/aipp/AIPP/Parser#options-instance_method) – arguments read from <tt>aip2aixm</tt> or <tt>aip2ofmx</tt> respectively
95
+ * [`config`](https://www.rubydoc.info/gems/aipp/AIPP/Parser#config-instance_method) – configuration read from <tt>config.yml</tt>
96
+ * [`borders`](https://www.rubydoc.info/gems/aipp/AIPP/Parser#borders-instance_method) – borders defined as GeoJSON read from the region (see below)
97
+ * [`cache`](https://www.rubydoc.info/gems/aipp/AIPP/Parser#cache-instance_method) – virgin `OStruct` instance to make objects available across AIPs
98
+
99
+ ### Borders
100
+
101
+ AIXM knows named borders for country boundaries. However, you might need additional borders which don't exist as named boarders.
102
+
103
+ To define additional borders, create simple GeoJSON files in the <tt>lib/aipp/regions/{REGION}/borders/</tt> directory, for example this `custom_border.geojson`:
104
+
105
+ ```json
106
+ {
107
+ "type": "GeometryCollection",
108
+ "geometries": [
109
+ {
110
+ "type": "LineString",
111
+ "coordinates": [
112
+ [6.009531650000042, 45.12013319700009],
113
+ [6.015747738000073, 45.12006702600007]
114
+ ]
115
+ }
116
+ ]
117
+ }
118
+ ```
119
+
120
+ ⚠️ The GeoJSON file must consist of exactly one `GeometryCollection` which may contain any number of `LineString` geometries. Only `LineString` geometries are recognized! To define a closed polygon, the first coordinates of a `LineString` must be identical to the last coordinates.
121
+
122
+ The [`borders`](https://www.rubydoc.info/gems/aipp/AIPP/Parser#borders-instance_method) method gives you access to a map from the border name (upcased file name) to the corresponding [`AIPP::Border`](https://www.rubydoc.info/gems/aipp/AIPP/Border) object:
123
+
124
+ ```ruby
125
+ borders # => { "CUSTOM_BORDER" => #<AIPP::Border file=custom_border.geojson> }
126
+ ```
91
127
 
92
- As well as the following objects:
128
+ The border object implements simple nearest point and segment calculations to create arrays of [`AIXM::XY`](https://www.rubydoc.info/gems/aixm/AIXM/XY) which can be used with [`AIXM::Component::Geometry`](https://www.rubydoc.info/gems/aixm/AIXM/Component/Geometry).
93
129
 
94
- * `options` arguments read from <tt>aip2aixm</tt> or <tt>aip2ofmx</tt> respectively
95
- * `config` – configuration read from <tt>config.yml</tt>
96
- * `cache` – virgin `OStruct` instance to make objects available across AIPs
130
+ See [`AIPP::Border`](https://www.rubydoc.info/gems/aipp/AIPP/Border) for more on this.
97
131
 
98
132
  ### Helpers
99
133
 
@@ -213,12 +247,12 @@ info("my message") # displays "my message" in black
213
247
  info("my message", color: :green) # displays "my message" in green
214
248
  ```
215
249
 
216
- #### debug
250
+ #### verbose info
217
251
 
218
- Use `debug` for in-depth info messages which are only shown if the `--verbose` mode is set:
252
+ Use `verbose_info` for in-depth info messages which are only shown if the `--verbose` mode is set:
219
253
 
220
254
  ```ruby
221
- debug("my message") # displays "my message" in blue
255
+ verbose_info("my message") # displays "my message" in blue
222
256
  ```
223
257
 
224
258
  ### Pry
data/TODO.md CHANGED
@@ -1,4 +1,7 @@
1
1
  ## LF
2
2
 
3
- ### AD-2
4
- * Landing aids (LOC/GP/DME) as of AD-2 section 2.19
3
+ * AD-3.1: helipads
4
+ * ENR-5.4: obstacles
5
+ * ENR-5.2: military and training areas
6
+ * ENR-2.2: TMZ and RMZ
7
+ * AD-2: landing aids (LOC/GP/DME) as of section 2.19
@@ -30,7 +30,7 @@ Gem::Specification.new do |spec|
30
30
  spec.add_development_dependency 'guard'
31
31
  spec.add_development_dependency 'guard-minitest'
32
32
 
33
- spec.add_runtime_dependency 'aixm', '>= 0.3.4'
33
+ spec.add_runtime_dependency 'aixm', '>= 0.3.5'
34
34
  spec.add_runtime_dependency 'activesupport', '~> 5'
35
35
  spec.add_runtime_dependency 'nokogiri', '~> 1'
36
36
  spec.add_runtime_dependency 'nokogumbo', '~> 2'
@@ -21,12 +21,14 @@ require 'active_support'
21
21
  require 'active_support/core_ext/object/blank'
22
22
  require 'active_support/core_ext/string'
23
23
  require_relative 'core_ext/object'
24
+ require_relative 'core_ext/integer'
24
25
  require_relative 'core_ext/string'
25
26
  require_relative 'core_ext/nil_class'
26
27
  require_relative 'core_ext/enumerable'
27
28
 
28
29
  require_relative 'aipp/version'
29
30
  require_relative 'aipp/pdf'
31
+ require_relative 'aipp/border'
30
32
  require_relative 'aipp/t_hash'
31
33
  require_relative 'aipp/executable'
32
34
  require_relative 'aipp/airac'
@@ -20,7 +20,7 @@ module AIPP
20
20
  # @see AIPP::Parser#options
21
21
  # @!method cache
22
22
  # @see AIPP::Parser#cache
23
- def_delegators :@parser, :aixm, :config, :options, :cache
23
+ def_delegators :@parser, :aixm, :config, :options, :borders, :cache
24
24
  private :aixm
25
25
 
26
26
  def initialize(aip:, downloader:, parser:)
@@ -45,10 +45,11 @@ module AIPP
45
45
  @downloader.read(document: aip_file, url: url_for(aip_file))
46
46
  end
47
47
 
48
- # Write feature to AIXM
48
+ # Add feature to AIXM
49
49
  #
50
50
  # @param feature [AIXM::Feature] e.g. airport or airspace
51
- def write(feature)
51
+ def add(feature)
52
+ verbose_info "Adding #{feature.class}"
52
53
  aixm.features << feature
53
54
  end
54
55
 
@@ -0,0 +1,146 @@
1
+ module AIPP
2
+
3
+ # Border GeoJSON file reader
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>]]
25
+ class Border
26
+ attr_reader :file
27
+ attr_reader :geometries
28
+
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
33
+ end
34
+
35
+ # @return [String]
36
+ def inspect
37
+ %Q(#<#{self.class} file=#{@file}>)
38
+ end
39
+
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
+ # @return [String]
47
+ def name
48
+ @file.basename('.geojson').to_s.gsub(/\W/, '').upcase
49
+ end
50
+
51
+ # Whether the given geometry is closed or not
52
+ #
53
+ # A geometry is considered closed when it's first coordinate equals the
54
+ # last coordinate.
55
+ #
56
+ # @param geometry_index [Integer] geometry to check
57
+ # @return [Boolean] true if the geometry is closed or false otherwise
58
+ def closed?(geometry_index:)
59
+ geometry = @geometries[geometry_index]
60
+ geometry.first == geometry.last
61
+ end
62
+
63
+ # Find a position on a geometry nearest to the given coordinates
64
+ #
65
+ # @param geometry_index [Integer] index of the geometry on which to search
66
+ # or +nil+ to search on all geometries
67
+ # @param xy [AIXM::XY] coordinates to approximate
68
+ # @return [AIPP::Border::Position] position nearest to the given coordinates
69
+ def nearest(geometry_index: nil, xy:)
70
+ position = nil
71
+ min_distance = 21_000_000 # max distance on earth in meters
72
+ @geometries.each.with_index do |geometry, g_index|
73
+ next unless geometry_index.nil? || geometry_index == g_index
74
+ geometry.each.with_index do |coordinates, c_index|
75
+ distance = xy.distance(coordinates).dist
76
+ if distance < min_distance
77
+ position = Position.new(geometries: geometries, geometry_index: g_index, coordinates_index: c_index)
78
+ min_distance = distance
79
+ end
80
+ end
81
+ end
82
+ position
83
+ end
84
+
85
+ # Get a segment of a geometry between the given starting and ending
86
+ # positions
87
+ #
88
+ # The segment ends either at the given ending position or at the last
89
+ # coordinates of the geometry. However, if the geometry is closed, the
90
+ # segment always continues up to the given ending position.
91
+ #
92
+ # @param from_position [AIPP::Border::Position] starting position
93
+ # @param to_position [AIPP::Border::Position] ending position
94
+ # @return [Array<AIXM::XY>] array of coordinates describing the segment
95
+ def segment(from_position:, to_position:)
96
+ fail(ArgumentError, "both positions must be on the same geometry") unless from_position.geometry_index == to_position.geometry_index
97
+ geometry_index = from_position.geometry_index
98
+ geometry = @geometries[geometry_index]
99
+ if closed?(geometry_index: geometry_index)
100
+ up = from_position.coordinates_index.upto(to_position.coordinates_index)
101
+ down = from_position.coordinates_index.downto(0) + (geometry.count - 2).downto(to_position.coordinates_index)
102
+ geometry.values_at(*(up.count < down.count ? up : down).to_a)
103
+ else
104
+ geometry.values_at(*from_position.coordinates_index.up_or_downto(to_position.coordinates_index).to_a)
105
+ end
106
+ end
107
+
108
+ private
109
+
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
+ # Position defines an exact point on a border
119
+ #
120
+ # @example
121
+ # position = AIPP::Border::Position.new(
122
+ # geometries: border.geometries, geometry_index: 0, coordinates_index: 0
123
+ # )
124
+ # position.xy # => #<AIXM::XY 45.12013320N 006.00953165E>
125
+ class Position
126
+ attr_accessor :geometry_index
127
+ attr_accessor :coordinates_index
128
+
129
+ def initialize(geometries:, geometry_index:, coordinates_index:)
130
+ @geometries, @geometry_index, @coordinates_index = geometries, geometry_index, coordinates_index
131
+ end
132
+
133
+ # @return [String]
134
+ def inspect
135
+ %Q(#<#{self.class} xy=#{xy}>)
136
+ end
137
+
138
+ # Coordinates for this position
139
+ #
140
+ # @return [AIXM::XY, nil] coordinates or nil if the indexes don't exist
141
+ def xy
142
+ @geometries.dig(@geometry_index, @coordinates_index)
143
+ end
144
+ end
145
+ end
146
+ end
@@ -57,7 +57,7 @@ module AIPP
57
57
  type ||= Pathname(URI(url).path).extname[1..-1].to_sym
58
58
  file = work_path.join([document, type].join('.'))
59
59
  unless file.exist?
60
- debug "Downloading #{document}"
60
+ verbose_info "Downloading #{document}"
61
61
  IO.copy_stream(Kernel.open(url), file)
62
62
  end
63
63
  convert file
@@ -8,7 +8,7 @@ module AIPP
8
8
  @options = options
9
9
  @options[:airac] = AIPP::AIRAC.new
10
10
  @options[:storage] = Pathname(Dir.home).join('.aipp')
11
- @options[:force] = $PRY_ON_WARN = $PRY_ON_ERROR = false
11
+ @options[:force] = $VERBOSE_INFO = $PRY_ON_WARN = $PRY_ON_ERROR = false
12
12
  OptionParser.new do |o|
13
13
  o.banner = <<~END
14
14
  Download online AIP and convert it to #{options[:schema].upcase}.
@@ -19,7 +19,7 @@ module AIPP
19
19
  o.on('-a', '--aip STRING', String, 'process this AIP only (e.g. "ENR-5.1")') { |v| @options[:aip] = v.upcase }
20
20
  o.on('-s', '--storage DIR', String, 'storage directory (default: "~/.aipp")') { |v| @options[:storage] = Pathname(v) }
21
21
  o.on('-f', '--[no-]force', 'ignore XML schema validation (default: false)') { |v| @options[:force] = v }
22
- o.on('-v', '--[no-]verbose', 'verbose output and Ruby debug mode (default: false)') { |v| $DEBUG = v }
22
+ o.on('-v', '--[no-]verbose', 'verbose output (default: false)') { |v| $VERBOSE_INFO = v }
23
23
  o.on('-w', '--pry-on-warn [ID]', Integer, 'open pry on warn with ID (default: nil)') { |v| $PRY_ON_WARN = v || true }
24
24
  o.on('-e', '--[no-]pry-on-error', 'open pry on error (default: false)') { |v| $PRY_ON_ERROR = v }
25
25
  o.on('-A', '--about', 'show author/license information and exit') { about }
@@ -12,6 +12,9 @@ module AIPP
12
12
  # @return [AIXM::Document] target document
13
13
  attr_reader :aixm
14
14
 
15
+ # @return [Hash] map from border names to border objects
16
+ attr_reader :borders
17
+
15
18
  # @return [OpenStruct] object cache
16
19
  attr_reader :cache
17
20
 
@@ -22,6 +25,7 @@ module AIPP
22
25
  @config = {}
23
26
  @aixm = AIXM.document(region: @options[:region], effective_at: @options[:airac].date)
24
27
  @dependencies = THash.new
28
+ @borders = {}
25
29
  @cache = OpenStruct.new
26
30
  end
27
31
 
@@ -38,9 +42,16 @@ module AIPP
38
42
  info("Reading region #{options[:region]}")
39
43
  dir = Pathname(__FILE__).dirname.join('regions', options[:region])
40
44
  fail("unknown region `#{options[:region]}'") unless dir.exist?
45
+ # Borders
46
+ dir.glob('borders/*.geojson').each do |file|
47
+ border = AIPP::Border.new(file)
48
+ @borders[border.name] = border
49
+ end
50
+ # Helpers
41
51
  dir.glob('helpers/*.rb').each { |f| require f }
52
+ # Parsers
42
53
  dir.glob('*.rb').each do |file|
43
- debug("Requiring #{file.basename}")
54
+ verbose_info "Requiring #{file.basename}"
44
55
  require file
45
56
  aip = file.basename('.*').to_s
46
57
  @dependencies[aip] = ("AIPP::%s::%s::DEPENDS" % [options[:region], aip.remove(/\W/).classify]).constantize
@@ -24,7 +24,7 @@ module AIPP
24
24
  define_method(:"#{attribute}=") do |value|
25
25
  catch :abort do
26
26
  value = block.call(parser, self, value)
27
- debug("PATCH: #{self.inspect}", color: :magenta)
27
+ verbose_info("PATCH: #{self.inspect}", color: :magenta)
28
28
  end
29
29
  send(:"original_#{attribute}=", value)
30
30
  end
@@ -97,13 +97,13 @@ module AIPP
97
97
  end
98
98
 
99
99
  def read_cache
100
- cache_file = "#{@file}.json"
101
- if File.exist?(cache_file) && (File.stat(@file).mtime - File.stat(cache_file).mtime).abs < 1
102
- JSON.load File.read(cache_file)
100
+ cache_file = Pathname.new("#{@file}.json")
101
+ if cache_file.exist? && (@file.stat.mtime - cache_file.stat.mtime).abs < 1
102
+ JSON.load cache_file
103
103
  else
104
104
  read.tap do |data|
105
- File.write(cache_file, data.to_json)
106
- FileUtils.touch(cache_file, mtime: File.stat(@file).mtime)
105
+ cache_file.write data.to_json
106
+ FileUtils.touch(cache_file, mtime: @file.stat.mtime)
107
107
  end
108
108
  end
109
109
  end
@@ -32,9 +32,9 @@ module AIPP
32
32
  tbody = prepare(html: read).css('tbody').first # skip altiports
33
33
  tbody.css('tr').to_enum.with_index(1).each do |tr, index|
34
34
  if tr.attr(:id).match?(/-TXT_NAME-/)
35
- write @airport if @airport && !ad2_exists
35
+ add @airport if @airport && !ad2_exists
36
36
  @airport = airport_from tr
37
- debug "Parsing #{@airport.id}"
37
+ verbose_info "Parsing #{@airport.id}"
38
38
  ad2_exists = false
39
39
  if airport = select(:airport, id: @airport.id).first
40
40
  ad2_exists = true
@@ -47,7 +47,7 @@ module AIPP
47
47
  rescue => error
48
48
  warn("error parsing #{@airport.id} at ##{index}: #{error.message}", pry: error)
49
49
  end
50
- write @airport if @airport && !ad2_exists
50
+ add @airport if @airport && !ad2_exists
51
51
  end
52
52
 
53
53
  private
@@ -64,7 +64,7 @@ module AIPP
64
64
  ).tap do |airport|
65
65
  airport.z = AIXM.z(tds[4].text.strip.to_i, :qnh)
66
66
  airport.declination = tds[2].text.remove('°').strip.to_f
67
- airport.transition_z = AIXM.z(5000, :qnh) # TODO: default - exceptions may exist
67
+ # airport.transition_z = AIXM.z(5000, :qnh) # TODO: default - exceptions exist
68
68
  end
69
69
  end
70
70