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
@@ -1,33 +0,0 @@
1
- module AIPP
2
- module LF
3
-
4
- # Aerodromes radiocommunication facilities (VFR only)
5
- class AD16 < AIP
6
-
7
- include AIPP::LF::Helpers::Base
8
- include AIPP::LF::Helpers::RadioAD
9
-
10
- DEPENDS = %w(AD-1.3)
11
-
12
- ID_FIXES = {
13
- 'LF04' => 'LF9004', # illegal ID as per AIXM
14
- 'LFPY' => nil # decommissioned - see https://fr.wikipedia.org/wiki/Base_a%C3%A9rienne_217_Br%C3%A9tigny-sur-Orge
15
- }
16
-
17
- def parse
18
- document = prepare(html: read)
19
- document.css('tbody').each do |tbody|
20
- tbody.css('tr').group_by_chunks { _1.attr(:id).match?(/-TXT_NAME-/) }.each do |tr, trs|
21
- trs = Nokogiri::XML::NodeSet.new(document, trs) # convert array to node set
22
- id = tr.css('span[id*="CODE_ICAO"]').text.cleanup
23
- next unless id = ID_FIXES.fetch(id, id)
24
- @airport = find_by(:airport, id: id).first
25
- addresses_from(trs).each { @airport.add_address(_1) }
26
- units_from(trs, airport: @airport).each(&method(:add))
27
- end
28
- end
29
- end
30
-
31
- end
32
- end
33
- end
@@ -1,344 +0,0 @@
1
- module AIPP
2
- module LF
3
-
4
- # Airports (IFR capable) and their CTR, AD navigational aids etc
5
- class AD2 < AIP
6
-
7
- include AIPP::LF::Helpers::Base
8
- include AIPP::LF::Helpers::NavigationalAid
9
- include AIPP::LF::Helpers::RadioAD
10
- using AIXM::Refinements
11
-
12
- # Map source types to type and optional local type
13
- SOURCE_TYPES = {
14
- 'CTR' => { type: 'CTR' },
15
- 'RMZ' => { type: 'RAS', local_type: 'RMZ' },
16
- 'TMZ' => { type: 'RAS', local_type: 'TMZ' },
17
- 'RMZ-TMZ' => { type: 'RAS', local_type: 'RMZ-TMZ' }
18
- }.freeze
19
-
20
- # Airports without VAC (e.g. military installations)
21
- NO_VAC = %w(LFOA LFBC LFQE LFOE LFSX LFBM LFSO LFMO LFQP LFSI LFKS LFPV).freeze
22
-
23
- # Airports without VFR reporting points
24
- # TODO: designated points on map but no list (LFLD LFSN LFBS) or no AD info (LFRL)
25
- NO_DESIGNATED_POINTS = %w(LFAB LFAC LFAV LFAY LFBK LFBN LFBX LFCC LFCI LFCK LFCY LFDH LFDJ LFDN LFEC LFFK LFEV LFEY LFGA LFHP LFHV LFHY LFJR LFJY LFLA LFLH LFLO LFLV LFLW LFMQ LFMQ LFNB LFOH LFOQ LFOU LFOV LFOZ LFPO LFQA LFQB LFQG LFQM LFRC LFRI LFRM LFRT LFRU LFSD LFSG LFSM LFLD LFSN LFBS LFRL).freeze
26
-
27
- # Map synonyms for +correlate+
28
- SYNONYMS = [
29
- 'nord', 'north',
30
- 'est', 'east',
31
- 'sud', 'south',
32
- 'ouest', 'west',
33
- 'inst', 'instruction',
34
- 'junction', 'intersection',
35
- 'harbour', 'port',
36
- 'mouth', 'embouchure',
37
- 'tower', 'chateau'
38
- ].freeze
39
-
40
- def parse
41
- index_html = prepare(html: read("AD-0.6")) # index for AD-2.xxxx files
42
- index_html.css('#AD-0\.6\.eAIP > .toc-block:nth-of-type(3) .toc-block a').each do |a|
43
- @id = a.attribute('href').value[-4,4]
44
- begin
45
- aip_file = "AD-2.#{@id}"
46
- html = prepare(html: read(aip_file))
47
- # Airport
48
- @remarks = []
49
- @airport = AIXM.airport(
50
- source: source(position: html.css('tr[id*="CODE_ICAO"]').first.line, aip_file: aip_file),
51
- organisation: organisation_lf, # TODO: not yet implemented
52
- id: @id,
53
- name: html.css('tr[id*="CODE_ICAO"] td span:nth-of-type(2)').text.strip.uptrans,
54
- xy: xy_from(html.css('#AD-2\.2-Position_Geo_Arp td:nth-of-type(3)').text)
55
- ).tap do |airport|
56
- airport.z = elevation_from(html.css('#AD-2\.2-Altitude_Reference td:nth-of-type(3)').text)
57
- airport.declination = declination_from(html.css('#AD-2\.2-Declinaison_Magnetique td:nth-of-type(3)').text)
58
- # airport.transition_z = AIXM.z(5000, :qnh) # TODO: default - exceptions may exist
59
- airport.timetable = timetable_from!(html.css('#AD-2\.3-Gestionnaire_AD td:nth-of-type(3)').text)
60
- end
61
- runways_from(html.css('div[id*="-AD-2\.12"] tbody')).each { @airport.add_runway(_1) if _1 }
62
- helipads_from(html.css('div[id*="-AD-2\.16"] tbody')).each { @airport.add_helipad(_1) if _1 }
63
- text = html.css('#AD-2\.2-Observations td:nth-of-type(3)').text
64
- @airport.remarks = ([remarks_from(text)] + @remarks).compact.join("\n\n").blank_to_nil
65
- add @airport
66
- # Airspaces
67
- airspaces_from(html.css('div[id*="-AD-2\.17"] tbody')).
68
- reject { aixm.features.find_by(_1.class, type: _1.type, id: _1.id).any? }.
69
- each(&method(:add))
70
- # Radio
71
- trs = html.css('div[id*="-AD-2\.18"] tbody tr')
72
- addresses_from(trs).each { @airport.add_address(_1) }
73
- units_from(trs, airport: @airport).each(&method(:add))
74
- # Navigational aids
75
- navigational_aids_from(html.css('div[id*="-AD-2\.19"] tbody')).
76
- reject { aixm.features.find_by(_1.class, id: _1.id, xy: _1.xy).any? }.
77
- each(&method(:add))
78
- # Designated points
79
- unless NO_VAC.include?(@id) || NO_DESIGNATED_POINTS.include?(@id)
80
- pdf = read("VAC-#{@id}")
81
- designated_points_from(pdf).tap do |designated_points|
82
- fix_designated_point_remarks(designated_points)
83
- # debug(designated_points)
84
- designated_points.
85
- uniq(&:to_uid).
86
- reject { aixm.features.find_by(_1.class, id: _1.id, xy: _1.xy).any? }.
87
- each(&method(:add))
88
- end
89
- end
90
- rescue => error
91
- warn("error parsing airport #{@id}: #{error.message}", pry: error)
92
- end
93
- end
94
- end
95
-
96
- private
97
-
98
- def declination_from(text)
99
- value, direction = text.strip.split('°')
100
- value = value.to_f * (direction == 'W' ? -1 : 1)
101
- end
102
-
103
- def remarks_from(text)
104
- text.sub(/NIL|\(\*\)\s+/, '').strip.gsub(/(\s)\s+/, '\1').blank_to_nil
105
- end
106
-
107
- def runways_from(tbody)
108
- directions_map = tbody.css('tr[id*="TXT_DESIG"]').map do |tr|
109
- [AIXM.a(tr.css('td:first-of-type').text.strip), tr]
110
- end.to_h
111
- remarks_map = tbody.css('tr[id*="TXT_RMK_NAT"]').map do |tr|
112
- [tr.text.strip[/\A\((\d+)\)/, 1].to_i, tr.css('span')]
113
- end.to_h
114
- directions = directions_map.keys
115
- grouped_directions = directions.map do |direction|
116
- inverted_direction = direction.invert
117
- if directions.include? inverted_direction
118
- [direction, inverted_direction].map(&:to_s).sort.join('/')
119
- else
120
- direction.to_s
121
- end
122
- end.uniq
123
- grouped_directions.map do |runway_name|
124
- AIXM.runway(name: runway_name).tap do |runway|
125
- %i(forth back).each do |direction_attr|
126
- if direction = runway.send(direction_attr)
127
- tr = directions_map[direction.name]
128
- if direction_attr == :forth
129
- length, width = tr.css('td:nth-of-type(3)').text.strip.split('x')
130
- runway.length = AIXM.d(length.strip.to_i, :m)
131
- runway.width = AIXM.d(width.strip.to_i, :m)
132
- unless (text = tr.css('td:nth-of-type(5)').text.strip.split(%r<\W+/\W+>).first.compact).blank?
133
- surface = SURFACES.metch(text)
134
- runway.surface.composition = surface[:composition]
135
- runway.surface.preparation = surface[:preparation]
136
- runway.surface.remarks = surface[:remarks]
137
- end
138
- if (text = tr.css('td:nth-of-type(4)').text).match?(AIXM::PCN_RE)
139
- runway.surface.pcn = text
140
- end
141
- end
142
- text = tr.css('td:nth-of-type(6)').text.strip
143
- direction.xy = (xy_from(text) unless text.match?(/\A(\(.*)?\z/m))
144
- if (text = tr.css('td:nth-of-type(7)').text.strip[/thr:\s+(\d+\s+\w+)/i, 1]).present?
145
- direction.z = elevation_from(text)
146
- end
147
- if (text = tr.css('td:nth-of-type(2)').text.strip.sub(/\A(\d+).*$/m, '\1')).present?
148
- direction.geographic_orientation = AIXM.a(text.to_i)
149
- end
150
- if (text = tr.css('td:nth-of-type(6)').text[/\((.+)\)/m, 1]).present?
151
- direction.displaced_threshold = xy_from(text)
152
- end
153
- if (text = tr.css('td:nth-of-type(10)').text.strip[/\A\((\d+)\)/, 1]).present?
154
- direction.remarks = remarks_from(remarks_map.fetch(text.to_i).text)
155
- end
156
- end
157
- end
158
- end
159
- end
160
- end
161
-
162
- def helipads_from(tbody)
163
- text_fr = tbody.css('td:nth-of-type(3)').text.compact
164
- text_en = tbody.css('td:nth-of-type(4)').text.compact
165
- case text_fr
166
- when /NIL/, /\A\W*\z/
167
- []
168
- when /instructions?\s+twr/i
169
- @remarks << "HELICOPTER:\nSur instructions TWR.\nOn TWR clearance."
170
- []
171
- when AIXM::DMS_RE
172
- text_fr.scan(AIXM::DMS_RE).each_slice(2).with_index(1).map do |(lat, long), index|
173
- AIXM.helipad(
174
- name: "H#{index}",
175
- xy: AIXM.xy(lat: lat.first, long: long.first)
176
- )
177
- end
178
- else
179
- @remarks << ['HELICOPTER:', text_fr.blank_to_nil, text_en.blank_to_nil].compact.join("\n")
180
- []
181
- end
182
- end
183
-
184
- def airspaces_from(tbody)
185
- return [] if tbody.text.blank?
186
- airspace = nil
187
- tbody.css('tr').to_enum.with_object([]) do |tr, array|
188
- if tr.attr(:class) =~ /keep-with-next-row/
189
- airspace = airspace_from tr
190
- else
191
- tds = tr.css('td')
192
- airspace.geometry = geometry_from tds[0].text
193
- fail("geometry is not closed") unless airspace.geometry.closed?
194
- airspace.add_layer layer_from(tds[2].text, tds[1].text.strip)
195
- airspace.layers.first.timetable = timetable_from! tds[4].text
196
- airspace.layers.first.remarks = remarks_from(tds[4].text)
197
- array << airspace
198
- end
199
- end
200
- end
201
-
202
- def airspace_from(tr)
203
- spans = tr.css(:span)
204
- source_type = spans[1].text.blank_to_nil
205
- fail "unknown type `#{source_type}'" unless SOURCE_TYPES.has_key? source_type
206
- AIXM.airspace(
207
- name: [spans[2].text, anglicise(name: spans[3]&.text)].compact.join(' '),
208
- type: SOURCE_TYPES.dig(source_type, :type),
209
- local_type: SOURCE_TYPES.dig(source_type, :local_type)
210
- ).tap do |airspace|
211
- airspace.source = source(position: tr.line)
212
- end
213
- end
214
-
215
- def navigational_aids_from(tbody)
216
- tbody.css('tr').to_enum.with_object([]) do |tr, array|
217
- tds = tr.css('td')
218
- array << navigational_aid_from(
219
- {
220
- name: OpenStruct.new(text: @airport.name), # simulate td
221
- type: tds[0],
222
- id: tds[1],
223
- f: tds[2],
224
- schedule: tds[3],
225
- xy: tds[4],
226
- z: tds[5]
227
- },
228
- source: source(position: tr.line),
229
- sections: {
230
- range: tds[6],
231
- situation: tds[8]
232
- }
233
- )
234
- end.compact
235
- end
236
-
237
- def designated_points_from(pdf, recursive=false)
238
- from = (pdf.text =~ /^(.*?coordinates.*?names?)/i)
239
- return [] if recursive && !from
240
- warn("no designated points section begin found for #{@id}", pry: binding) unless from
241
- from += $1.length
242
- to = from + (pdf.text.from(from) =~ /\n\s*\n\s*\n|^.*(?:ifr|vfr|ad\s*equipment|special\s*activities|training\s*flights|mto\s*minima)/i)
243
- warn("no designated points section end found for #{@id}", pry: binding) unless to
244
- from, to = from + pdf.range.min, to + pdf.range.min # offset when recursive
245
- buffer = {}
246
- pdf.from(from).to(to).each_line.with_object([]) do |(line, page, last), designated_points|
247
- line.remove!(/\u2190/) # remove arrow symbols
248
- has_id = $1 if line.sub!(/^\s{,20}([A-Z][A-Z\d ]{1,3})(?=\W)/, '')
249
- has_xy = line.match?(AIXM::DMS_RE)
250
- designated_points << designated_point_from(buffer, pdf) if has_id || has_xy
251
- if has_xy
252
- 2.times { (buffer[:xy] ||= []) << $1 if line.sub!(AIXM::DMS_RE, '') }
253
- buffer[:xy]&.compact!
254
- line.remove!(/\d{3,4}\D.+?MTG/) # remove extra columns (e.g. LFML)
255
- line.remove!(/[\s#{AIXM::MIN}#{AIXM::SEC}]*[-\u2013]/) # remove dash between coordinates
256
- end
257
- buffer[:page] = page
258
- buffer[:id] = has_id if has_id
259
- buffer[:remarks] = [buffer[:remarks], line].join("\n")
260
- designated_points << designated_point_from(buffer, pdf) if last
261
- end.compact + designated_points_from(pdf.from(to).to(:end), true)
262
- end
263
-
264
- def designated_point_from(buffer, pdf)
265
- if buffer[:id] && buffer[:xy]&.size == 2
266
- buffer[:remarks].gsub!(/ {20}/, "\n") # recognize empty column space
267
- buffer[:remarks].remove!(/\(\d+\)/) # remove footnotes
268
- buffer[:remarks] = buffer[:remarks].unglue # separate glued words
269
- AIXM.designated_point(
270
- source: source(position: buffer[:page], aip_file: pdf.file.basename('.*').to_s),
271
- type: :vfr_mandatory_reporting_point,
272
- id: buffer[:id].remove(/\W/),
273
- xy: AIXM.xy(lat: buffer[:xy].first, long: buffer[:xy].last)
274
- ).tap do |designated_point|
275
- designated_point.airport = @airport
276
- designated_point.remarks = buffer[:remarks].compact.blank_to_nil
277
- buffer.clear
278
- end
279
- end
280
- end
281
-
282
- # Assign scattered similar remarks to one and the same designated point
283
- def fix_designated_point_remarks(designated_points)
284
- one = nil
285
- designated_points.map do |two|
286
- if one
287
- one_lines, two_lines = one.remarks&.lines, two.remarks&.lines
288
- if one_lines && two_lines
289
- if one_lines.count > 1 && (line = one_lines.last) !~ %r(\s/\s)
290
- # Move up
291
- if line.correlate(remainder = one_lines[0..-2].join, SYNONYMS) < line.correlate(two.remarks)
292
- two.remarks = [line, two.remarks].join("\n").compact
293
- one.remarks = remainder.compact
294
- end
295
- elsif two_lines.count > 1 && (line = two_lines.first) !~ %r(\s/\s)
296
- # Move down
297
- line = two_lines.first
298
- if line.correlate(remainder = two_lines[1..-1].join, SYNONYMS) < line.correlate(one.remarks)
299
- one.remarks = [one.remarks, line].join("\n").compact
300
- two.remarks = remainder.compact
301
- end
302
- end
303
- end
304
- end
305
- one = two
306
- end.map do |designated_point|
307
- designated_point.remarks = designated_point.remarks&.cleanup.blank_to_nil
308
- end
309
- end
310
-
311
- # def debug(dp)
312
- # f = "/Users/sschwyn/Desktop/okay/#{@id}.txt"
313
- # result = "\n--- #{@id} ---\n\n".red
314
- # dp.each do |d|
315
- # result += d.id.red + "\t#{d.xy.lat} - #{d.xy.long}\n"
316
- # result += "#{d.remarks}\n\n".blue
317
- # end
318
- # result += "#{dp.count} point(s) for #{@id}".red
319
- # unless File.exist?(f) && result == File.read(f)
320
- # puts result
321
- # gets
322
- # puts "\e[H\e[2J"
323
- # end
324
- # File.write(f, result)
325
- # end
326
-
327
- patch AIXM::Component::Runway::Direction, :xy do |parser, object, value|
328
- throw :abort unless value.nil?
329
- airport_id = parser.instance_variable_get(:@airport).id
330
- direction_name = object.name.to_s
331
- throw :abort if (xy = parser.fixture.dig('runways', airport_id, direction_name, 'xy')).nil?
332
- lat, long = xy.split(/\s+/)
333
- AIXM.xy(lat: lat, long: long)
334
- end
335
-
336
- patch AIXM::Feature::NavigationalAid, :remarks do |parser, object, value|
337
- throw :abort unless object.is_a? AIXM::Feature::NavigationalAid::DesignatedPoint
338
- airport_id, designated_point_id = object.airport.id, object.id
339
- parser.fixture.dig('designated_points', airport_id, designated_point_id, 'remarks') || throw(:abort)
340
- end
341
-
342
- end
343
- end
344
- end
@@ -1,185 +0,0 @@
1
- module AIPP
2
- module LF
3
-
4
- # Helipads
5
- class AD31 < AIP
6
-
7
- include AIPP::LF::Helpers::Base
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 find_by(: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(type: :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(type: :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.compact
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 { @airport.add_fato(_1) }
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 { @airport.add_address(_1) }
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 { _1.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