aipp 0.2.2 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
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