aipp 0.2.3 → 0.2.4

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: 6d97409845a7b08285c77294aa116873fdcb668bd0f10d13d2ceecd9ef3922a9
4
- data.tar.gz: 2594b4ad9bd0b9deba766704778ff1710eaaf71ce675bb4fd378481f4f13dfc6
3
+ metadata.gz: 70da2e53874e0c550cbbacc863ff421cbd38dfc2cb1b7e4d2c77e2821839fdea
4
+ data.tar.gz: 62e9d5c5413756490d03f057e8851aba54c7d8802f4012a2f9626f78eb241d6f
5
5
  SHA512:
6
- metadata.gz: aa0ac5dafcc6e36a113e2a97c270f768265379b09c34a833e785adf44f45e5dfcd5bb3951b0d6741ae15bc765386eb55c3118154a48ce568f9fae14f06bf6ae5
7
- data.tar.gz: 92dd8d6134a1882b0ceada8e130ed834e3dc866e4b84e2dd513084c5313a42d8fd89b47d3726ff36e1f38bb8837fa190790bcc3f99d752e7504757e1a2b4eb84
6
+ metadata.gz: 6508c2a1565fdd92ed4fe2593b883278940e10fa10e68a0ffc75391b7e6d8ba8c66ca19c7e91495da39f60c4257e7882611a235ad9c8a5314b928140625a6e90
7
+ data.tar.gz: ade8abd0e5f521faf63b684d719c1e71a85278ec57f56edf4c83349c1ca4fc68422c9948c74f059c064cbb6ec21d66c91216f6418d3749bd53dd6eb1ecca24cc
data/.gitignore CHANGED
@@ -1,5 +1,5 @@
1
1
  .DS_Store
2
- Gemfile.lock
2
+ gems.locked
3
3
  pkg/*
4
4
  *.gem
5
5
  .bundle
@@ -2,3 +2,7 @@
2
2
  language: ruby
3
3
  rvm:
4
4
  - 2.6.3
5
+ before_install:
6
+ - gem update --system
7
+ - gem install bundler
8
+ - bundle install
@@ -1,3 +1,9 @@
1
+ ## 0.2.4
2
+
3
+ #### Additions
4
+ * LF/AD-3.1
5
+ * Automatically load fixtures for patches
6
+
1
7
  ## 0.2.3
2
8
 
3
9
  #### Additons
data/README.md CHANGED
@@ -1,7 +1,5 @@
1
1
  [![Version](https://img.shields.io/gem/v/aipp.svg?style=flat)](https://rubygems.org/gems/aipp)
2
2
  [![Continuous Integration](https://img.shields.io/travis/svoop/aipp/master.svg?style=flat)](https://travis-ci.org/svoop/aipp)
3
- [![Code Climate](https://img.shields.io/codeclimate/github/svoop/aipp.svg?style=flat)](https://codeclimate.com/github/svoop/aipp)
4
- [![Gitter](https://img.shields.io/gitter/room/svoop/aipp.svg?style=flat)](https://gitter.im/svoop/aipp)
5
3
  [![Donorbox](https://img.shields.io/badge/donate-on_donorbox-yellow.svg)](https://donorbox.org/bitcetera)
6
4
 
7
5
  # AIPP
@@ -87,7 +85,7 @@ Inside the `parse` method, you have access to the following methods:
87
85
  * [`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
86
  * [`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
87
  * 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), [`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)
88
+ * core extensions from this gem – [`Object`](https://www.rubydoc.info/gems/aipp/Object), [`NilClass`](https://www.rubydoc.info/gems/aipp/NilClass), [`Integer`](https://www.rubydoc.info/gems/aipp/Integer), [`String`](https://www.rubydoc.info/gems/aipp/String), [`Hash`](https://www.rubydoc.info/gems/aipp/Hash) and [`Enumerable`](https://www.rubydoc.info/gems/aipp/Enumerable)
91
89
 
92
90
  As well as the following methods:
93
91
 
@@ -125,7 +123,7 @@ The [`borders`](https://www.rubydoc.info/gems/aipp/AIPP/Parser#borders-instance_
125
123
  borders # => { "CUSTOM_BORDER" => #<AIPP::Border file=custom_border.geojson> }
126
124
  ```
127
125
 
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).
126
+ 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).
129
127
 
130
128
  See [`AIPP::Border`](https://www.rubydoc.info/gems/aipp/AIPP/Border) for more on this.
131
129
 
@@ -183,9 +181,11 @@ module AIPP
183
181
  end
184
182
  ```
185
183
 
186
- ### Patches
184
+ ### Fixtures and Patches
187
185
 
188
- When parsed data is faulty or missing, you might have to use a different data source instead such as static data from a fixture file. This is where patches come in. You can patch any AIXM attribute setter by defining a patch block inside the AIP parser:
186
+ Fixtures is static data defined as YAML in the <tt>lib/aipp/regions/{REGION}/fixtures/</tt> directory. All fixtures are read automatically. Please note that the name of the AIP parser (e.g. `AD-1.3.rb`) must match the name of the corresponding fixture (e.g. `fixtures/AD-1.3.yml`).
187
+
188
+ When parsed data is faulty or missing, you may fall back to such static data instead. This is where patches come in. You can patch any AIXM attribute setter by defining a patch block inside the AIP parser and accessing the static data via `parser.fixture`:
189
189
 
190
190
  ```ruby
191
191
  module AIPP
@@ -197,7 +197,7 @@ module AIPP
197
197
  @fixtures ||= YAML.load_file(Pathname(__FILE__).dirname.join('AD-1.3.yml'))
198
198
  airport_id = parser.instance_variable_get(:@airport).id
199
199
  direction_name = object.name.to_s
200
- throw :abort if (xy = @fixtures.dig('runways', airport_id, direction_name, 'xy')).nil?
200
+ throw :abort if (xy = parser.fixture.dig('runways', airport_id, direction_name, 'xy')).nil?
201
201
  lat, long = xy.split(/\s+/)
202
202
  AIXM.xy(lat: lat, long: long)
203
203
  end
@@ -280,7 +280,7 @@ airac.next_id # => 1801
280
280
  * [OpenData – public data files](https://www.data.gouv.fr)
281
281
  * [Protected Planet – protected area data files](https://www.protectedplanet.net)
282
282
  * [Geo Maps – programmatically generated GeoJSON maps](https://github.com/simonepri/geo-maps)
283
- * [Open Flightmaps – open-source aeronautical maps](https://openflightmaps.org)
283
+ * [open flightmaps – open-source aeronautical maps](https://openflightmaps.org)
284
284
  * [AIXM Rubygem – AIXM/OFMX generator for Ruby](https://github.com/svoop/aixm)
285
285
 
286
286
  ## Development
data/TODO.md CHANGED
@@ -1,6 +1,5 @@
1
1
  ## LF
2
2
 
3
- * AD-3.1: helipads
4
3
  * ENR-5.4: obstacles
5
4
  * ENR-5.2: military and training areas
6
5
  * ENR-2.2: TMZ and RMZ
@@ -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.5'
33
+ spec.add_runtime_dependency 'aixm', '>= 0.3.6'
34
34
  spec.add_runtime_dependency 'activesupport', '~> 5'
35
35
  spec.add_runtime_dependency 'nokogiri', '~> 1'
36
36
  spec.add_runtime_dependency 'nokogumbo', '~> 2'
File without changes
@@ -25,6 +25,7 @@ require_relative 'core_ext/integer'
25
25
  require_relative 'core_ext/string'
26
26
  require_relative 'core_ext/nil_class'
27
27
  require_relative 'core_ext/enumerable'
28
+ require_relative 'core_ext/hash'
28
29
 
29
30
  require_relative 'aipp/version'
30
31
  require_relative 'aipp/pdf'
@@ -10,6 +10,9 @@ module AIPP
10
10
  # @return [String] AIP name (e.g. "ENR-2.1")
11
11
  attr_reader :aip
12
12
 
13
+ # @return [Object] Fixture read from YAML file
14
+ attr_reader :fixture
15
+
13
16
  # @!method close
14
17
  # @see AIPP::Downloader#close
15
18
  def_delegator :@downloader, :close
@@ -18,13 +21,15 @@ module AIPP
18
21
  # @see AIPP::Parser#config
19
22
  # @!method options
20
23
  # @see AIPP::Parser#options
24
+ # @!method borders
25
+ # @see AIPP::Parser#borders
21
26
  # @!method cache
22
27
  # @see AIPP::Parser#cache
23
28
  def_delegators :@parser, :aixm, :config, :options, :borders, :cache
24
29
  private :aixm
25
30
 
26
- def initialize(aip:, downloader:, parser:)
27
- @aip, @downloader, @parser = aip, downloader, parser
31
+ def initialize(aip:, downloader:, fixture:, parser:)
32
+ @aip, @downloader, @fixture, @parser = aip, downloader, fixture, parser
28
33
  self.class.include ("AIPP::%s::Helpers::URL" % options[:region]).constantize
29
34
  end
30
35
 
@@ -49,7 +54,7 @@ module AIPP
49
54
  #
50
55
  # @param feature [AIXM::Feature] e.g. airport or airspace
51
56
  def add(feature)
52
- verbose_info "Adding #{feature.class}"
57
+ verbose_info "Adding #{feature.inspect}"
53
58
  aixm.features << feature
54
59
  end
55
60
 
@@ -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 AIP name to fixtures
16
+ attr_reader :fixtures
17
+
15
18
  # @return [Hash] map from border names to border objects
16
19
  attr_reader :borders
17
20
 
@@ -25,8 +28,10 @@ module AIPP
25
28
  @config = {}
26
29
  @aixm = AIXM.document(region: @options[:region], effective_at: @options[:airac].date)
27
30
  @dependencies = THash.new
31
+ @fixtures = {}
28
32
  @borders = {}
29
33
  @cache = OpenStruct.new
34
+ AIXM.send("#{options[:schema]}!")
30
35
  end
31
36
 
32
37
  # Read the configuration from config.yml.
@@ -42,13 +47,23 @@ module AIPP
42
47
  info("Reading region #{options[:region]}")
43
48
  dir = Pathname(__FILE__).dirname.join('regions', options[:region])
44
49
  fail("unknown region `#{options[:region]}'") unless dir.exist?
50
+ # Fixtures
51
+ dir.glob('fixtures/*.yml').each do |file|
52
+ verbose_info "Reading fixture fixtures/#{file.basename}"
53
+ fixture = YAML.load_file(file)
54
+ @fixtures[file.basename('.yml').to_s] = fixture
55
+ end
45
56
  # Borders
46
57
  dir.glob('borders/*.geojson').each do |file|
58
+ verbose_info "Reading border borders/#{file.basename}"
47
59
  border = AIPP::Border.new(file)
48
60
  @borders[border.name] = border
49
61
  end
50
62
  # Helpers
51
- dir.glob('helpers/*.rb').each { |f| require f }
63
+ dir.glob('helpers/*.rb').each do |file|
64
+ verbose_info "Reading helper helpers/#{file.basename}"
65
+ require file
66
+ end
52
67
  # Parsers
53
68
  dir.glob('*.rb').each do |file|
54
69
  verbose_info "Requiring #{file.basename}"
@@ -67,6 +82,7 @@ module AIPP
67
82
  ("AIPP::%s::%s" % [options[:region], aip.remove(/\W/).classify]).constantize.new(
68
83
  aip: aip,
69
84
  downloader: downloader,
85
+ fixture: @fixtures[aip],
70
86
  parser: self
71
87
  ).attach_patches.tap(&:parse).detach_patches
72
88
  end
@@ -79,7 +95,7 @@ module AIPP
79
95
  def validate_aixm
80
96
  info("Validating #{options[:schema].upcase}")
81
97
  unless aixm.valid?
82
- message = "invalid AIXM document:\n" + aixm.errors.map(&:message).join("\n")
98
+ message = "invalid #{options[:schema].upcase} document:\n" + aixm.errors.map(&:message).join("\n")
83
99
  @options[:force] ? warn(message, pry: binding) : fail(message)
84
100
  end
85
101
  end
@@ -88,7 +104,6 @@ module AIPP
88
104
  def write_aixm
89
105
  file = "#{options[:region]}_#{options[:airac].date.xmlschema}.#{options[:schema]}"
90
106
  info("Writing #{file}")
91
- AIXM.send("#{options[:schema]}!")
92
107
  File.write(file, aixm.to_xml)
93
108
  end
94
109
 
@@ -93,15 +93,18 @@ module AIPP
93
93
 
94
94
  def runway_from(tr)
95
95
  tds = tr.css('td')
96
- surface = tds[1].css('span[id*="SURFACE"]').text
97
96
  AIXM.runway(
98
97
  name: tds[0].text.strip.split.join('/')
99
98
  ).tap do |runway|
100
99
  @runway = runway # TODO: needed for now for surface composition patches to work
101
100
  runway.length = AIXM.d(tds[1].css('span[id$="VAL_LEN"]').text.to_i, :m)
102
101
  runway.width = AIXM.d(tds[1].css('span[id$="VAL_WID"]').text.to_i, :m)
103
- runway.surface.composition = (COMPOSITIONS.fetch(surface)[:composition] unless surface.blank?)
104
- runway.surface.preparation = (COMPOSITIONS.fetch(surface)[:preparation] unless surface.blank?)
102
+ unless (text = tds[1].css('span[id*="SURFACE"]').text).blank?
103
+ surface = SURFACES.metch(text)
104
+ runway.surface.composition = surface[:composition]
105
+ runway.surface.preparation = surface[:preparation]
106
+ runway.surface.remarks = surface[:remarks]
107
+ end
105
108
  runway.remarks = tds[7].text.cleanup.blank_to_nil
106
109
  values = tds[2].text.remove('°').strip.split
107
110
  runway.forth.geographic_orientation = AIXM.a(values.first.to_i)
@@ -131,29 +134,26 @@ module AIPP
131
134
 
132
135
  patch AIXM::Component::Runway, :width do |parser, object, value|
133
136
  throw :abort unless value.zero?
134
- @fixtures ||= YAML.load_file(Pathname(__FILE__).dirname.join('AD-1.3.yml'))
135
137
  airport_id = parser.instance_variable_get(:@airport).id
136
138
  runway_name = object.name.to_s
137
- throw :abort if (width = @fixtures.dig('runways', airport_id, runway_name, 'width')).nil?
139
+ throw :abort if (width = parser.fixture.dig('runways', airport_id, runway_name, 'width')).nil?
138
140
  AIXM.d(width.to_i, :m)
139
141
  end
140
142
 
141
143
  patch AIXM::Component::Runway::Direction, :xy do |parser, object, value|
142
144
  throw :abort unless value.nil?
143
- @fixtures ||= YAML.load_file(Pathname(__FILE__).dirname.join('AD-1.3.yml'))
144
145
  airport_id = parser.instance_variable_get(:@airport).id
145
146
  direction_name = object.name.to_s
146
- throw :abort if (xy = @fixtures.dig('runways', airport_id, direction_name, 'xy')).nil?
147
+ throw :abort if (xy = parser.fixture.dig('runways', airport_id, direction_name, 'xy')).nil?
147
148
  lat, long = xy.split(/\s+/)
148
149
  AIXM.xy(lat: lat, long: long)
149
150
  end
150
151
 
151
152
  patch AIXM::Component::Surface, :composition do |parser, object, value|
152
153
  throw :abort unless value.blank?
153
- @fixtures ||= YAML.load_file(Pathname(__FILE__).dirname.join('AD-1.3.yml'))
154
154
  airport_id = parser.instance_variable_get(:@airport).id
155
155
  runway_name = parser.instance_variable_get(:@runway).name
156
- throw :abort if (composition = @fixtures.dig('runways', airport_id, runway_name, 'composition')).nil?
156
+ throw :abort if (composition = parser.fixture.dig('runways', airport_id, runway_name, 'composition')).nil?
157
157
  composition
158
158
  end
159
159
 
@@ -87,11 +87,6 @@ module AIPP
87
87
 
88
88
  private
89
89
 
90
- def elevation_from(text)
91
- value, unit = text.strip.split
92
- AIXM.z(AIXM.d(value.to_i, unit).to_ft.dist, :qnh)
93
- end
94
-
95
90
  def declination_from(text)
96
91
  value, direction = text.strip.split('°')
97
92
  value = value.to_f * (direction == 'W' ? -1 : 1)
@@ -126,9 +121,12 @@ module AIPP
126
121
  length, width = tr.css('td:nth-of-type(3)').text.strip.split('x')
127
122
  runway.length = AIXM.d(length.strip.to_i, :m)
128
123
  runway.width = AIXM.d(width.strip.to_i, :m)
129
- text = tr.css('td:nth-of-type(5)').text.strip.split(%r<\W+/\W+>).first
130
- runway.surface.composition = COMPOSITIONS.fetch(text)[:composition]
131
- runway.surface.preparation = COMPOSITIONS.fetch(text)[:preparation]
124
+ unless (text = tr.css('td:nth-of-type(5)').text.strip.split(%r<\W+/\W+>).first).blank?
125
+ surface = SURFACES.metch(text)
126
+ runway.surface.composition = surface[:composition]
127
+ runway.surface.preparation = surface[:preparation]
128
+ runway.surface.remarks = surface[:remarks]
129
+ end
132
130
  if (text = tr.css('td:nth-of-type(4)').text).match?(AIXM::PCN_RE)
133
131
  runway.surface.pcn = text
134
132
  end
@@ -164,9 +162,10 @@ module AIPP
164
162
  []
165
163
  when AIXM::DMS_RE
166
164
  text_fr.scan(AIXM::DMS_RE).each_slice(2).with_index(1).map do |(lat, long), index|
167
- AIXM.helipad(name: "H#{index}").tap do |helipad|
168
- helipad.xy = AIXM.xy(lat: lat.first, long: long.first)
169
- end
165
+ AIXM.helipad(
166
+ name: "H#{index}",
167
+ xy: AIXM.xy(lat: lat.first, long: long.first)
168
+ )
170
169
  end
171
170
  else
172
171
  @remarks << ['HELICOPTER:', text_fr.blank_to_nil, text_en.blank_to_nil].compact.join("\n")
@@ -297,18 +296,16 @@ module AIPP
297
296
 
298
297
  patch AIXM::Component::Runway::Direction, :xy do |parser, object, value|
299
298
  throw :abort unless value.nil?
300
- @fixtures ||= YAML.load_file(Pathname(__FILE__).dirname.join('AD-2.yml'))
301
299
  airport_id = parser.instance_variable_get(:@airport).id
302
300
  direction_name = object.name.to_s
303
- throw :abort if (xy = @fixtures.dig('runways', airport_id, direction_name, 'xy')).nil?
301
+ throw :abort if (xy = parser.fixture.dig('runways', airport_id, direction_name, 'xy')).nil?
304
302
  lat, long = xy.split(/\s+/)
305
303
  AIXM.xy(lat: lat, long: long)
306
304
  end
307
305
 
308
306
  patch AIXM::Feature::NavigationalAid, :remarks do |parser, object, value|
309
- @fixtures ||= YAML.load_file(Pathname(__FILE__).dirname.join('AD-2.yml'))
310
307
  airport_id, designated_point_id = object.airport.id, object.id
311
- @fixtures.dig('designated_points', airport_id, designated_point_id, 'remarks') || throw(:abort)
308
+ parser.fixture.dig('designated_points', airport_id, designated_point_id, 'remarks') || throw(:abort)
312
309
  end
313
310
 
314
311
  end
@@ -0,0 +1,185 @@
1
+ module AIPP
2
+ module LF
3
+
4
+ # Helipads
5
+ class AD31 < AIP
6
+
7
+ include AIPP::LF::Helpers::Common
8
+ using AIXM::Refinements
9
+
10
+ DEPENDS = %w(AD-2)
11
+
12
+ HOSTILITIES = {
13
+ 'zone hostile habitée' => 'Zone hostile habitée / hostile populated area',
14
+ 'zone hostile non habitée' => 'Zone hostile non habitée / hostile unpopulated area',
15
+ 'zone non hostile' => 'Zone non hostile / non-hostile area'
16
+ }.freeze
17
+
18
+ POSITIONINGS = {
19
+ 'en terrasse' => 'En terrasse / on deck',
20
+ 'en surface' => 'En surface / on ground'
21
+ }.freeze
22
+
23
+ DIMENSIONS_RE = /( diam.tre\s+\d+ | (?:\d[\s\d,.m]*x\s*){1,}[\s\d,.m]+ )/ix.freeze
24
+
25
+ def parse
26
+ prepare(html: read).css('tbody').each do |tbody|
27
+ tbody.css('tr').to_enum.each_slice(3).with_index(1) do |trs, index|
28
+ name = trs[0].css('span[id*="ADHP.TXT_NAME"]').text.cleanup.remove(/[^\w' ]/)
29
+ if select(:airport, name: name).any?
30
+ verbose_info "Skipping #{name} in favor of AD-2"
31
+ next
32
+ end
33
+ # Airport
34
+ @airport = AIXM.airport(
35
+ source: source(position: trs[0].line),
36
+ organisation: organisation_lf, # TODO: not yet implemented
37
+ id: options[:region],
38
+ name: name,
39
+ xy: xy_from(trs[1].css('td:nth-of-type(1)').text.cleanup)
40
+ ).tap do |airport|
41
+ airport.z = elevation_from(trs[1].css('td:nth-of-type(2)').text)
42
+ end
43
+ # Usage restrictions
44
+ if trs[0].css('span[id*="ADHP.STATUT"]').text.match?(/usage\s+restreint/i)
45
+ @airport.add_usage_limitation(:reservation_required) do |reservation_required|
46
+ reservation_required.remarks = "Usage restreint / restricted use"
47
+ end
48
+ end
49
+ if trs[0].css('span[id*="ADHP.STATUT"]').text.match?(/r.serv.\s+aux\s+administrations/i)
50
+ @airport.add_usage_limitation(:other) do |other|
51
+ other.remarks = "Réservé aux administrations de l'État / reserved for State administrations"
52
+ end
53
+ end
54
+ # FATOs and helipads
55
+ text = trs[2].css('span[id*="ADHP.REVETEMENT"]').text.remove(/tlof\s*|\s*\(.*?\)/i).downcase
56
+ surface = text.blank? ? {} : SURFACES.metch(text)
57
+ lighting = lighting_from(trs[1].css('span[id*="ADHP.BALISAGE"]').text.cleanup)
58
+ fatos_from(trs[1].css('span[id*="ADHP.DIM_FATO"]').text).each { |f| @airport.add_fato f }
59
+ helipads_from(trs[1].css('span[id*="ADHP.DIM_TLOF"]').text).each do |helipad|
60
+ helipad.surface.composition = surface[:composition]
61
+ helipad.surface.preparation = surface[:preparation]
62
+ helipad.surface.remarks = surface[:remarks]
63
+ helipad.surface.auw_weight = auw_weight_from(trs[2].css('span[id*="ADHP.RESISTANCE"]').text)
64
+ helipad.add_lighting(lighting) if lighting
65
+ @airport.add_helipad helipad
66
+ end
67
+ # Operator and addresses
68
+ operator = trs[0].css('span[id*="ADHP.EXPLOITANT"]')
69
+ splitted = operator.text.split(/( (?<!\p{L})t[ée]l | fax | standard | [\d\s]{10,} | \.\s | \( )/ix, 2)
70
+ @airport.operator = splitted[0].full_strip.truncate(60, omission: '…').blank_to_nil
71
+ raw_addresses = splitted[1..].join.cleanup.full_strip
72
+ addresses_from(splitted[1..].join, source(position: operator.first.line)).each { |a| @airport.add_address(a) }
73
+ # Remarks
74
+ @airport.remarks = [].tap do |remarks|
75
+ hostility = trs[2].css('span[id*="ADHP.ZONE_HABITEE"]').text.cleanup.downcase.blank_to_nil
76
+ hostility = HOSTILITIES.fetch(hostility) if hostility
77
+ positioning = trs[2].css('span[id*="ADHP.EN_TERRASSE"]').text.cleanup.downcase.blank_to_nil
78
+ positioning = POSITIONINGS.fetch(positioning) if positioning
79
+ remarks << ('**SITUATION**' if hostility || positioning) << hostility << positioning << ''
80
+ remarks << trs[2].css('td:nth-of-type(5)').text.cleanup
81
+ remarks << raw_addresses unless raw_addresses.blank?
82
+ end.compact.join("\n").strip
83
+ add(@airport) if @airport.fatos.any? || @airport.helipads.any?
84
+ end
85
+ end
86
+ end
87
+
88
+ private
89
+
90
+ def fatos_from(text)
91
+ [
92
+ if text.cleanup.match DIMENSIONS_RE
93
+ AIXM.fato(name: 'FATO').tap do |fato|
94
+ fato.length, fato.width = dimensions_from($1)
95
+ end
96
+ end
97
+ ].compact
98
+ end
99
+
100
+ def helipads_from(text)
101
+ [
102
+ if text.cleanup.match DIMENSIONS_RE
103
+ AIXM.helipad(name: 'TLOF', xy: @airport.xy).tap do |helipad|
104
+ helipad.z = @airport.z
105
+ helipad.length, helipad.width = dimensions_from($1)
106
+ end
107
+ end
108
+ ].compact
109
+ end
110
+
111
+ def dimensions_from(text)
112
+ dims = text.remove(/[^x\d.,]/i).split(/x/i).map { |s| s.to_ff.floor }
113
+ case dims.size
114
+ when 1
115
+ [dim = AIXM.d(dims[0], :m), dim]
116
+ when 2
117
+ [AIXM.d(dims[0], :m), AIXM.d(dims[1], :m)]
118
+ when 4
119
+ [dim = AIXM.d(dims.min, :m), dim]
120
+ else
121
+ warn("bad dimensions for #{@airport.name}", pry: binding)
122
+ end
123
+ end
124
+
125
+ def auw_weight_from(text)
126
+ if wgt = text.match(/(\d+(?:[,.]\d+)?)\s*t/i)&.captures&.first
127
+ AIXM.w(wgt.to_ff, :t)
128
+ end
129
+ end
130
+
131
+ def lighting_from(text)
132
+ return if text.blank? || text.match?(/nil|balisage\s*:\s*non/i)
133
+ description = text.remove(/balisage\s*:|oui\.?\s*:?/i).compact.full_strip
134
+ AIXM.lighting(position: :edge).tap do |lighting|
135
+ lighting.description = description unless description.blank?
136
+ end
137
+ end
138
+
139
+ def addresses_from(text, source)
140
+ [].tap do |addresses|
141
+ text.sub! /(?<!\p{L})t[ée]l\D*([\d\s.]{10,}(?:poste[\d\s.]{2,})?)[-\/]?/i do |m|
142
+ addresses << AIXM.address(
143
+ source: source,
144
+ type: :phone,
145
+ address: m.strip.sub(/poste/i, '-').remove(/[^\d-]|-$/)
146
+ )
147
+ end
148
+ text.sub! /fax\D*([\d\s.]{10,}(?:poste[\d\s.]{2,})?)[-\/]?/i do |m|
149
+ addresses << AIXM.address(
150
+ source: source,
151
+ type: :fax,
152
+ address: m.strip.sub(/poste/i, '-').remove(/[^\d-]|-$/)
153
+ )
154
+ end
155
+ text.sub! /e-mail\W*(\S+)[-\/]?/i do |m|
156
+ addresses << AIXM.address(
157
+ source: source,
158
+ type: :email,
159
+ address: m.strip
160
+ )
161
+ end
162
+ text.sub! /(\d[\d\s]{9,}(?:poste[\d\s.]{2,})?)[-\/]?/i do |m|
163
+ addresses << AIXM.address(
164
+ source: source,
165
+ type: :phone,
166
+ address: m.strip.sub(/poste/i, '-').remove(/[^\d-]|-$/)
167
+ )
168
+ end
169
+ end
170
+ end
171
+
172
+ patch AIXM::Feature::Airport, :xy do |parser, object, value|
173
+ throw :abort if value.seconds?
174
+ if xy = parser.fixture.dig(object.name, 'xy')
175
+ lat, long = xy.split(/\s+/)
176
+ AIXM.xy(lat: lat, long: long)
177
+ else
178
+ warn("coordinates for #{object.name} appear not to be exact", pry: binding)
179
+ throw :abort
180
+ end
181
+ end
182
+
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,10 @@
1
+ # On Google Maps, click in the middle of the runway beginning, then click on
2
+ # the coordinates in the little window displayed at the bottom. The DMS
3
+ # coordinates will then appear in the left column ready for copy/paste.
4
+ ---
5
+ FLAMANVILLE:
6
+ xy: 49°32'03.5"N 1°52'46.8"W
7
+ ILE D'YEU PORT JOINVILLE:
8
+ xy: 46°43'42.2"N 2°21'02.3"W
9
+ VARCES QUARTIER REYNIES:
10
+ xy: 45°06'09.1"N 5°40'56.1"E
@@ -30,19 +30,35 @@ module AIPP
30
30
  'FRANCE_LUXEMBOURG|FRANCE_GERMANY' => AIXM.xy(lat: 49.469438, long: 6.367516),
31
31
  'FRANCE_GERMANY|FRANCE_SWITZERLAND' => AIXM.xy(lat: 47.589831, long: 7.589049),
32
32
  'GERMANY_SWITZERLAND|FRANCE_GERMANY' => AIXM.xy(lat: 47.589831, long: 7.589049)
33
- }
33
+ }.freeze
34
34
 
35
- # Map surface compositions to OFMX composition and preparation
36
- COMPOSITIONS = {
37
- 'revêtue' => { preparation: :paved },
38
- 'non revêtue' => { preparation: :natural },
35
+ # Map surface to OFMX composition, preparation and remarks
36
+ SURFACES = {
37
+ /^revêtue?$/ => { preparation: :paved },
38
+ /^non revêtue?$/ => { preparation: :natural },
39
39
  'macadam' => { composition: :macadam },
40
- 'béton' => { composition: :concrete, preparation: :paved },
41
- 'béton bitumineux' => { composition: :bitumen, preparation: :paved },
42
- 'enrobé bitumineux' => { composition: :bitumen },
43
- 'asphalte' => { composition: :asphalt, preparation: :paved },
44
- 'gazon' => { composition: :grass }
45
- }
40
+ /^bitume ?(traité|psp)?$/ => { composition: :bitumen },
41
+ 'ciment' => { composition: :concrete, preparation: :paved },
42
+ /^b[eé]ton ?(armé|bitume|bitumineux)?$/ => { composition: :concrete, preparation: :paved },
43
+ /^béton( de)? ciment$/ => { composition: :concrete, preparation: :paved },
44
+ 'béton herbe' => { composition: :concrete_and_grass },
45
+ 'béton avec résine' => { composition: :concrete, preparation: :paved, remarks: 'Avec résine / with resin' },
46
+ "béton + asphalte d'étanchéité sablé" => { composition: :concrete_and_asphalt, preparation: :paved, remarks: 'Étanchéité sablé / sandblasted waterproofing' },
47
+ 'béton armé + support bitumastic' => { composition: :concrete, preparation: :paved, remarks: 'Support bitumastic / bitumen support' },
48
+ /résine (époxy )?su[er] béton/ => { composition: :concrete, preparation: :paved, remarks: 'Avec couche résine / with resin seal coat' },
49
+ /^(asphalte|tarmac)$/ => { composition: :asphalt, preparation: :paved },
50
+ 'enrobé' => { preparation: :other, remarks: 'Enrobé / coated' },
51
+ 'enrobé anti-kérozène' => { preparation: :other, remarks: 'Enrobé anti-kérozène / anti-kerosene coating' },
52
+ /^enrobé bitum(e|iné|ineux)$/ => { composition: :bitumen, preparation: :paved, remarks: 'Enrobé / coated' },
53
+ 'enrobé béton' => { composition: :concrete, preparation: :paved, remarks: 'Enrobé / coated' },
54
+ /^résine( époxy)?$/ => { composition: :other, remarks: 'Résine / resin' },
55
+ 'tole acier larmé' => { composition: :metal, preparation: :grooved },
56
+ /^(structure métallique|aluminium)$/ => { composition: :metal },
57
+ 'matériaux composites ignifugés' => { composition: :other, remarks: 'Matériaux composites ignifugés / fire resistant mixed materials' },
58
+ /^(gazon|herbe)$/ => { composition: :grass },
59
+ 'neige' => { composition: :snow },
60
+ 'neige damée' => { composition: :snow, preparation: :rolled }
61
+ }.freeze
46
62
 
47
63
  # Transform French text fragments to English
48
64
  ANGLICISE_MAP = {
@@ -125,6 +141,11 @@ module AIPP
125
141
  end
126
142
  end
127
143
 
144
+ def elevation_from(text)
145
+ value, unit = text.strip.split
146
+ AIXM.z(AIXM.d(value.to_i, unit).to_ft.dist, :qnh)
147
+ end
148
+
128
149
  def layer_from(text_for_limits, text_for_class=nil)
129
150
  above, below = text_for_limits.gsub(/ /, '').split(/\n+/).select(&:blank_to_nil).split { |e| e.match? '---+' }
130
151
  AIXM.layer(
@@ -1,3 +1,3 @@
1
1
  module AIPP
2
- VERSION = "0.2.3".freeze
2
+ VERSION = "0.2.4".freeze
3
3
  end
@@ -0,0 +1,31 @@
1
+ class Hash
2
+
3
+ # Returns a value from the hash for the matching key
4
+ #
5
+ # Similar to +fetch+, search the hash keys for the search string and return
6
+ # the corresponding value. Unlike +fetch+, however, if a hash key is a Regexp,
7
+ # the search string is matched against this Regexp. The hash is searched
8
+ # in it's natural order.
9
+ #
10
+ # @example
11
+ # h = { /aa/ => :aa, /a/ => :a, 'b' => :b }
12
+ # h.metch('abc') # => :a
13
+ # h.metch('bcd') # => KeyError
14
+ # h.metch('b') # => :b
15
+ # h.metch('x', :foobar) # => :foobar
16
+ #
17
+ # @param search [String] string to search or matche against
18
+ # @param default [Object] fallback value if no key matched
19
+ # @return [Object] hash value
20
+ # @raise [KeyError] no key matched and no default given
21
+ def metch(search, default=nil)
22
+ fetch search
23
+ rescue KeyError
24
+ each do |key, value|
25
+ next unless key.is_a? Regexp
26
+ return value if search.match? key
27
+ end
28
+ default ? default : raise(KeyError, "no match found: #{search.inspect}")
29
+ end
30
+
31
+ end
@@ -11,6 +11,7 @@ class Object
11
11
  # end
12
12
  # @example with binding context
13
13
  # warn("oops", pry: binding)
14
+ #
14
15
  # @param message [String] warning message
15
16
  # @param pry [Exception, Binding, nil] attach the Pry session to this error
16
17
  # or binding
@@ -13,19 +13,6 @@ class String
13
13
  self if present?
14
14
  end
15
15
 
16
- # Strip and collapse unnecessary whitespace
17
- #
18
- # @note While similar to +String#squish+ from ActiveSupport, newlines +\n+
19
- # are preserved and not collapsed into one space.
20
- #
21
- # @example
22
- # " foo\n\nbar \r".copact # => "foo\nbar"
23
- #
24
- # @return [String] compacted string
25
- def compact
26
- split("\n").map { |s| s.squish.blank_to_nil }.compact.join("\n")
27
- end
28
-
29
16
  # Fix messy oddities such as the use of two apostrophes instead of a quote
30
17
  #
31
18
  # @example
@@ -39,21 +26,17 @@ class String
39
26
  split(/\r?\n/).map { |s| s.strip.blank_to_nil }.compact.join("\n") # remove blank lines
40
27
  end
41
28
 
42
- # Add spaces between obviously glued words:
43
- # * camel glued words
44
- # * three-or-more-letter and number-only words
29
+ # Strip and collapse unnecessary whitespace
30
+ #
31
+ # @note While similar to +String#squish+ from ActiveSupport, newlines +\n+
32
+ # are preserved and not collapsed into one space.
45
33
  #
46
34
  # @example
47
- # "thisString has spaceProblems".unglue # => "this String has space problems"
48
- # "the first123meters of D25".unglue # => "the first 123 meters of D25"
35
+ # " foo\n\nbar \r".copact # => "foo\nbar"
49
36
  #
50
- # @return [String] unglued string
51
- def unglue
52
- self.dup.tap do |string|
53
- [/([[:lower:]])([[:upper:]])/, /([[:alpha:]]{3,})(\d)/, /(\d)([[:alpha:]]{3,})/].freeze.each do |regexp|
54
- string.gsub!(regexp, '\1 \2')
55
- end
56
- end
37
+ # @return [String] compacted string
38
+ def compact
39
+ split("\n").map { |s| s.squish.blank_to_nil }.compact.join("\n")
57
40
  end
58
41
 
59
42
  # Calculate the correlation of two strings by counting mutual words
@@ -102,4 +85,40 @@ class String
102
85
  end
103
86
  (self_words & other_words).count
104
87
  end
88
+
89
+ # Similar to +strip+, but remove any leading or trailing non-letters/numbers
90
+ # which includes whitespace
91
+ def full_strip
92
+ remove(/\A[^\p{L}\p{N}]*|[^\p{L}\p{N}]*\z/)
93
+ end
94
+
95
+ # Same as +to_f+ but accept both dot and comma as decimal separator
96
+ #
97
+ # @example
98
+ # "5.5".to_ff # => 5.5
99
+ # "5,6".to_ff # => 5.6
100
+ # "5,6".to_f # => 5.0 (sic!)
101
+ #
102
+ # @return [Float] number parsed from text
103
+ def to_ff
104
+ sub(/,/, '.').to_f
105
+ end
106
+
107
+ # Add spaces between obviously glued words:
108
+ # * camel glued words
109
+ # * three-or-more-letter and number-only words
110
+ #
111
+ # @example
112
+ # "thisString has spaceProblems".unglue # => "this String has space problems"
113
+ # "the first123meters of D25".unglue # => "the first 123 meters of D25"
114
+ #
115
+ # @return [String] unglued string
116
+ def unglue
117
+ self.dup.tap do |string|
118
+ [/([[:lower:]])([[:upper:]])/, /([[:alpha:]]{3,})(\d)/, /(\d)([[:alpha:]]{3,})/].freeze.each do |regexp|
119
+ string.gsub!(regexp, '\1 \2')
120
+ end
121
+ end
122
+ end
123
+
105
124
  end
File without changes
@@ -0,0 +1,27 @@
1
+ require_relative '../../spec_helper'
2
+
3
+ describe Hash do
4
+
5
+ describe :metch do
6
+ subject do
7
+ { /aa/ => :aa, /a/ => :a, 'b' => :b }
8
+ end
9
+
10
+ it "must return value of matching regexp key" do
11
+ subject.metch('abc').must_equal :a
12
+ end
13
+
14
+ it "must return value of equal non-regexp key" do
15
+ subject.metch('b').must_equal :b
16
+ end
17
+
18
+ it "fails with KeyError if nothing matches" do
19
+ -> { subject.metch('bcd') }.must_raise KeyError
20
+ end
21
+
22
+ it "returns fallback value if nothing matches" do
23
+ subject.metch('x', :foobar).must_equal :foobar
24
+ end
25
+ end
26
+
27
+ end
@@ -16,16 +16,6 @@ describe String do
16
16
  end
17
17
  end
18
18
 
19
- describe :compact do
20
- it "must remove unneccessary whitespace" do
21
- " foo\n\nbar \r".compact.must_equal "foo\nbar"
22
- "foo\n \nbar".compact.must_equal "foo\nbar"
23
- " ".compact.must_equal ""
24
- "\n \r \v ".compact.must_equal ""
25
- "okay".compact.must_equal "okay"
26
- end
27
- end
28
-
29
19
  describe :cleanup do
30
20
  it "must replace double apostrophes" do
31
21
  "the ''Terror'' was a fine ship".cleanup.must_equal 'the "Terror" was a fine ship'
@@ -37,17 +27,17 @@ describe String do
37
27
 
38
28
  it "must remove whitespace within quotes" do
39
29
  'the " best " way to fly'.cleanup.must_equal 'the "best" way to fly'
40
- %Q(the " best\nway " to fly).cleanup.must_equal %Q(the "best\nway" to fly)
30
+ %Q(the " best\nway " to fly).cleanup.must_equal %Q(the "best\nway" to fly)
41
31
  end
42
32
  end
43
33
 
44
- describe :unglue do
45
- it "must insert spaces between camel glued words" do
46
- "thisString has spaceProblems".unglue.must_equal "this String has space Problems"
47
- end
48
-
49
- it "must insert spaces between three-or-more-letter and number-only words" do
50
- "the first123meters of D25".unglue.must_equal "the first 123 meters of D25"
34
+ describe :compact do
35
+ it "must remove unneccessary whitespace" do
36
+ " foo\n\nbar \r".compact.must_equal "foo\nbar"
37
+ "foo\n \nbar".compact.must_equal "foo\nbar"
38
+ " ".compact.must_equal ""
39
+ "\n \r \v ".compact.must_equal ""
40
+ "okay".compact.must_equal "okay"
51
41
  end
52
42
  end
53
43
 
@@ -85,4 +75,38 @@ describe String do
85
75
  end
86
76
  end
87
77
 
78
+ describe :to_ff do
79
+ it "must convert normal float numbers as does to_f" do
80
+ "5".to_ff.must_equal "5".to_f
81
+ "5.1".to_ff.must_equal "5.1".to_f
82
+ " 5.2 ".to_ff.must_equal " 5.2 ".to_f
83
+ end
84
+
85
+ it "must convert comma float numbers as well" do
86
+ "5,1".to_ff.must_equal "5.1".to_f
87
+ " 5,2 ".to_ff.must_equal "5.2".to_f
88
+ end
89
+ end
90
+
91
+ describe :full_strip do
92
+ it "must behave like strip" do
93
+ subject = " foobar\t\t"
94
+ subject.full_strip.must_equal subject.strip
95
+ end
96
+
97
+ it "must remove non-letterlike characters as well" do
98
+ " - foobar :.".full_strip.must_equal "foobar"
99
+ end
100
+ end
101
+
102
+ describe :unglue do
103
+ it "must insert spaces between camel glued words" do
104
+ "thisString has spaceProblems".unglue.must_equal "this String has space Problems"
105
+ end
106
+
107
+ it "must insert spaces between three-or-more-letter and number-only words" do
108
+ "the first123meters of D25".unglue.must_equal "the first 123 meters of D25"
109
+ end
110
+ end
111
+
88
112
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aipp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sven Schwyn
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-05-07 00:00:00.000000000 Z
11
+ date: 2019-05-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -128,14 +128,14 @@ dependencies:
128
128
  requirements:
129
129
  - - ">="
130
130
  - !ruby/object:Gem::Version
131
- version: 0.3.5
131
+ version: 0.3.6
132
132
  type: :runtime
133
133
  prerelease: false
134
134
  version_requirements: !ruby/object:Gem::Requirement
135
135
  requirements:
136
136
  - - ">="
137
137
  - !ruby/object:Gem::Version
138
- version: 0.3.5
138
+ version: 0.3.6
139
139
  - !ruby/object:Gem::Dependency
140
140
  name: activesupport
141
141
  requirement: !ruby/object:Gem::Requirement
@@ -290,15 +290,14 @@ files:
290
290
  - ".travis.yml"
291
291
  - ".yardopts"
292
292
  - CHANGELOG.md
293
- - Gemfile
294
293
  - Guardfile
295
294
  - LICENSE.txt
296
295
  - README.md
297
- - Rakefile
298
296
  - TODO.md
299
297
  - aipp.gemspec
300
298
  - exe/aip2aixm
301
299
  - exe/aip2ofmx
300
+ - gems.rb
302
301
  - lib/aipp.rb
303
302
  - lib/aipp/aip.rb
304
303
  - lib/aipp/airac.rb
@@ -309,11 +308,9 @@ files:
309
308
  - lib/aipp/patcher.rb
310
309
  - lib/aipp/pdf.rb
311
310
  - lib/aipp/regions/LF/AD-1.3.rb
312
- - lib/aipp/regions/LF/AD-1.3.yml
313
311
  - lib/aipp/regions/LF/AD-1.6.rb
314
312
  - lib/aipp/regions/LF/AD-2.rb
315
- - lib/aipp/regions/LF/AD-2.yml
316
- - lib/aipp/regions/LF/AD-3.1.rb-NEW
313
+ - lib/aipp/regions/LF/AD-3.1.rb
317
314
  - lib/aipp/regions/LF/ENR-2.1.rb
318
315
  - lib/aipp/regions/LF/ENR-4.1.rb
319
316
  - lib/aipp/regions/LF/ENR-4.3.rb
@@ -323,16 +320,21 @@ files:
323
320
  - lib/aipp/regions/LF/borders/france_atlantic_territorial_sea.geojson
324
321
  - lib/aipp/regions/LF/borders/france_ecrins_national_park.geojson
325
322
  - lib/aipp/regions/LF/borders/france_mediterranean_coast.geojson
323
+ - lib/aipp/regions/LF/fixtures/AD-1.3.yml
324
+ - lib/aipp/regions/LF/fixtures/AD-2.yml
325
+ - lib/aipp/regions/LF/fixtures/AD-3.1.yml
326
326
  - lib/aipp/regions/LF/helpers/AD_radio.rb
327
327
  - lib/aipp/regions/LF/helpers/URL.rb
328
328
  - lib/aipp/regions/LF/helpers/common.rb
329
329
  - lib/aipp/t_hash.rb
330
330
  - lib/aipp/version.rb
331
331
  - lib/core_ext/enumerable.rb
332
+ - lib/core_ext/hash.rb
332
333
  - lib/core_ext/integer.rb
333
334
  - lib/core_ext/nil_class.rb
334
335
  - lib/core_ext/object.rb
335
336
  - lib/core_ext/string.rb
337
+ - rakefile.rb
336
338
  - spec/fixtures/archive.zip
337
339
  - spec/fixtures/border.geojson
338
340
  - spec/fixtures/document.pdf
@@ -348,6 +350,7 @@ files:
348
350
  - spec/lib/aipp/t_hash_spec.rb
349
351
  - spec/lib/aipp/version_spec.rb
350
352
  - spec/lib/core_ext/enumberable_spec.rb
353
+ - spec/lib/core_ext/hash_spec.rb
351
354
  - spec/lib/core_ext/integer_spec.rb
352
355
  - spec/lib/core_ext/nil_class_spec.rb
353
356
  - spec/lib/core_ext/string_spec.rb
@@ -393,6 +396,7 @@ test_files:
393
396
  - spec/lib/aipp/t_hash_spec.rb
394
397
  - spec/lib/aipp/version_spec.rb
395
398
  - spec/lib/core_ext/enumberable_spec.rb
399
+ - spec/lib/core_ext/hash_spec.rb
396
400
  - spec/lib/core_ext/integer_spec.rb
397
401
  - spec/lib/core_ext/nil_class_spec.rb
398
402
  - spec/lib/core_ext/string_spec.rb
@@ -1,11 +0,0 @@
1
- module AIPP
2
- module LF
3
-
4
- # Helipads
5
- class AD31 < AIP
6
-
7
- include AIPP::LF::Helpers::Common
8
-
9
- end
10
- end
11
- end