aipp 0.2.4 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +2 -0
  3. data/CHANGELOG.md +38 -0
  4. data/README.md +222 -88
  5. data/exe/aip2aixm +2 -2
  6. data/exe/aip2ofmx +2 -2
  7. data/lib/aipp/aip.rb +113 -31
  8. data/lib/aipp/border.rb +77 -46
  9. data/lib/aipp/debugger.rb +101 -0
  10. data/lib/aipp/downloader.rb +39 -26
  11. data/lib/aipp/executable.rb +41 -22
  12. data/lib/aipp/parser.rb +94 -21
  13. data/lib/aipp/patcher.rb +5 -2
  14. data/lib/aipp/pdf.rb +1 -1
  15. data/lib/aipp/regions/LF/README.md +49 -0
  16. data/lib/aipp/regions/LF/aerodromes.rb +223 -0
  17. data/lib/aipp/regions/LF/d_p_r_airspaces.rb +56 -0
  18. data/lib/aipp/regions/LF/dangerous_activities.rb +49 -0
  19. data/lib/aipp/regions/LF/designated_points.rb +47 -0
  20. data/lib/aipp/regions/LF/fixtures/aerodromes.yml +608 -0
  21. data/lib/aipp/regions/LF/helipads.rb +122 -0
  22. data/lib/aipp/regions/LF/helpers/base.rb +218 -0
  23. data/lib/aipp/regions/LF/helpers/surface.rb +49 -0
  24. data/lib/aipp/regions/LF/helpers/usage_limitation.rb +20 -0
  25. data/lib/aipp/regions/LF/navigational_aids.rb +85 -0
  26. data/lib/aipp/regions/LF/obstacles.rb +153 -0
  27. data/lib/aipp/regions/LF/serviced_airspaces.rb +70 -0
  28. data/lib/aipp/regions/LF/services.rb +172 -0
  29. data/lib/aipp/t_hash.rb +4 -5
  30. data/lib/aipp/version.rb +1 -1
  31. data/lib/aipp.rb +11 -5
  32. data/lib/core_ext/enumerable.rb +9 -9
  33. data/lib/core_ext/hash.rb +21 -5
  34. data/lib/core_ext/nokogiri.rb +54 -0
  35. data/lib/core_ext/string.rb +38 -66
  36. data.tar.gz.sig +2 -0
  37. metadata +180 -188
  38. metadata.gz.sig +0 -0
  39. data/.gitignore +0 -8
  40. data/.ruby-version +0 -1
  41. data/.travis.yml +0 -8
  42. data/.yardopts +0 -3
  43. data/Guardfile +0 -7
  44. data/TODO.md +0 -6
  45. data/aipp.gemspec +0 -44
  46. data/gems.rb +0 -3
  47. data/lib/aipp/airac.rb +0 -55
  48. data/lib/aipp/regions/LF/AD-1.3.rb +0 -162
  49. data/lib/aipp/regions/LF/AD-1.6.rb +0 -31
  50. data/lib/aipp/regions/LF/AD-2.rb +0 -313
  51. data/lib/aipp/regions/LF/AD-3.1.rb +0 -185
  52. data/lib/aipp/regions/LF/ENR-2.1.rb +0 -92
  53. data/lib/aipp/regions/LF/ENR-4.1.rb +0 -97
  54. data/lib/aipp/regions/LF/ENR-4.3.rb +0 -28
  55. data/lib/aipp/regions/LF/ENR-5.1.rb +0 -75
  56. data/lib/aipp/regions/LF/ENR-5.5.rb +0 -53
  57. data/lib/aipp/regions/LF/fixtures/AD-1.3.yml +0 -511
  58. data/lib/aipp/regions/LF/fixtures/AD-2.yml +0 -185
  59. data/lib/aipp/regions/LF/fixtures/AD-3.1.yml +0 -10
  60. data/lib/aipp/regions/LF/helpers/AD_radio.rb +0 -90
  61. data/lib/aipp/regions/LF/helpers/URL.rb +0 -26
  62. data/lib/aipp/regions/LF/helpers/common.rb +0 -217
  63. data/lib/core_ext/object.rb +0 -43
  64. data/rakefile.rb +0 -12
  65. data/spec/fixtures/archive.zip +0 -0
  66. data/spec/fixtures/border.geojson +0 -201
  67. data/spec/fixtures/document.pdf +0 -0
  68. data/spec/fixtures/document.pdf.json +0 -1
  69. data/spec/fixtures/new.html +0 -6
  70. data/spec/fixtures/new.pdf +0 -0
  71. data/spec/fixtures/new.txt +0 -1
  72. data/spec/lib/aipp/airac_spec.rb +0 -98
  73. data/spec/lib/aipp/border_spec.rb +0 -135
  74. data/spec/lib/aipp/downloader_spec.rb +0 -81
  75. data/spec/lib/aipp/patcher_spec.rb +0 -46
  76. data/spec/lib/aipp/pdf_spec.rb +0 -124
  77. data/spec/lib/aipp/t_hash_spec.rb +0 -44
  78. data/spec/lib/aipp/version_spec.rb +0 -7
  79. data/spec/lib/core_ext/enumberable_spec.rb +0 -76
  80. data/spec/lib/core_ext/hash_spec.rb +0 -27
  81. data/spec/lib/core_ext/integer_spec.rb +0 -15
  82. data/spec/lib/core_ext/nil_class_spec.rb +0 -11
  83. data/spec/lib/core_ext/string_spec.rb +0 -112
  84. data/spec/sounds/failure.mp3 +0 -0
  85. data/spec/sounds/success.mp3 +0 -0
  86. data/spec/spec_helper.rb +0 -28
data/lib/aipp/airac.rb DELETED
@@ -1,55 +0,0 @@
1
- module AIPP
2
-
3
- # AIRAC cycle date calculations
4
- #
5
- # @example
6
- # airac = AIPP::AIRAC.new('2018-01-01')
7
- # airac.date # => #<Date: 2017-12-07 ((2458095j,0s,0n),+0s,2299161j)>
8
- # airac.id # => 1713
9
- # airac.next_date # => #<Date: 2018-01-04 ((2458123j,0s,0n),+0s,2299161j)>
10
- # airac.next_id # => 1801
11
- class AIRAC
12
- # First AIRAC date following the last cycle length modification
13
- ROOT_DATE = Date.parse('2015-06-25').freeze
14
-
15
- # Length of one AIRAC cycle
16
- DAYS_PER_CYCLE = 28
17
-
18
- # @return [Date] AIRAC effective on date
19
- attr_reader :date
20
-
21
- # @return [Integer] AIRAC cycle ID
22
- attr_reader :id
23
-
24
- # @param any_date [Date] any date within the AIRAC cycle (default: today)
25
- def initialize(any_date = nil)
26
- any_date = any_date ? Date.parse(any_date.to_s) : Date.today
27
- fail(ArgumentError, "cannot calculate dates before #{ROOT_DATE}") if any_date < ROOT_DATE
28
- @date = date_for(any_date)
29
- @id = id_for(@date)
30
- end
31
-
32
- # @return [Date] next AIRAC effective on date
33
- def next_date
34
- date + DAYS_PER_CYCLE
35
- end
36
-
37
- # @return [Integer] next AIRAC cycle ID
38
- def next_id
39
- id_for next_date
40
- end
41
-
42
- private
43
-
44
- # Find the AIRAC date for +any_date+
45
- def date_for(any_date)
46
- ROOT_DATE + (any_date - ROOT_DATE).to_i / DAYS_PER_CYCLE * DAYS_PER_CYCLE
47
- end
48
-
49
- # Find the AIRAC ID for the AIRAC +date+
50
- def id_for(date)
51
- (date.year % 100) * 100 + ((date.yday - 1) / 28) + 1
52
- end
53
-
54
- end
55
- end
@@ -1,162 +0,0 @@
1
- module AIPP
2
- module LF
3
-
4
- # Aerodromes
5
- class AD13 < AIP
6
-
7
- include AIPP::LF::Helpers::Common
8
-
9
- DEPENDS = %w(AD-2)
10
-
11
- # Map names of id-less airports to unofficial ids
12
- ID_LESS_AIRPORTS = {
13
- "ALBE" => 'LF9001',
14
- "BEAUMONT DE LOMAGNE" => 'LF9002',
15
- "BERDOUES" => 'LF9003',
16
- "BOULOC" => 'LF9004',
17
- "BUXEUIL ST REMY / CREUSE" => 'LF9005',
18
- "CALVIAC" => 'LF9006',
19
- "CAYLUS" => 'LF9007',
20
- "CORBONOD" => 'LF9008',
21
- "L'ISLE EN DODON" => 'LF9009',
22
- "LACAVE LE FRAU" => 'LF9010',
23
- "LUCON CHASNAIS" => 'LF9011',
24
- "PEYRELEVADE" => 'LF9012',
25
- "SAINT CYR LA CAMPAGNE" => 'LF9013',
26
- "SEPTFONDS" => 'LF9014',
27
- "TALMONT VENDEE AIR PARK" => 'LF9015'
28
- }
29
-
30
- def parse
31
- ad2_exists = false
32
- tbody = prepare(html: read).css('tbody').first # skip altiports
33
- tbody.css('tr').to_enum.with_index(1).each do |tr, index|
34
- if tr.attr(:id).match?(/-TXT_NAME-/)
35
- add @airport if @airport && !ad2_exists
36
- @airport = airport_from tr
37
- verbose_info "Parsing #{@airport.id}"
38
- ad2_exists = false
39
- if airport = select(:airport, id: @airport.id).first
40
- ad2_exists = true
41
- @airport = airport
42
- end
43
- add_usage_limitations_from tr
44
- next
45
- end
46
- @airport.add_runway(runway_from(tr)) unless ad2_exists
47
- rescue => error
48
- warn("error parsing #{@airport.id} at ##{index}: #{error.message}", pry: error)
49
- end
50
- add @airport if @airport && !ad2_exists
51
- end
52
-
53
- private
54
-
55
- def airport_from(tr)
56
- tds = tr.css('td')
57
- id = tds[0].text.strip.blank_to_nil || ID_LESS_AIRPORTS.fetch(tds[1].text.strip)
58
- AIXM.airport(
59
- source: source(position: tr.line),
60
- organisation: organisation_lf, # TODO: not yet implemented
61
- id: id,
62
- name: tds[1].text.strip,
63
- xy: xy_from(tds[3].text)
64
- ).tap do |airport|
65
- airport.z = AIXM.z(tds[4].text.strip.to_i, :qnh)
66
- airport.declination = tds[2].text.remove('°').strip.to_f
67
- # airport.transition_z = AIXM.z(5000, :qnh) # TODO: default - exceptions exist
68
- end
69
- end
70
-
71
- def add_usage_limitations_from(tr)
72
- raw_limitation = tr.css('td:nth-of-type(8)').text.cleanup.downcase
73
- raw_conditions = tr.css('td:nth-of-type(6)').text.cleanup.downcase.split(%r([\s/]+))
74
- limitation = case raw_limitation
75
- when /ouv.+cap|milit/ then :permitted
76
- when /usa.+restr|priv/ then :reservation_required
77
- end
78
- @airport.add_usage_limitation(limitation) do |l|
79
- l.add_condition do |c|
80
- c.realm = :military if raw_limitation.match?(/milit/)
81
- c.origin = :national if raw_conditions.include?('ntl')
82
- c.origin = :international if raw_conditions.include?('intl')
83
- c.rule = :ifr if raw_conditions.include?('ifr')
84
- c.rule = :vfr if raw_conditions.include?('vfr')
85
- c.purpose = :scheduled if raw_conditions.include?('s')
86
- c.purpose = :not_scheduled if raw_conditions.include?('ns')
87
- c.purpose = :private if raw_conditions.include?('p')
88
- end
89
- l.remarks = "Usage restreint (voir VAC) / restricted use (see VAC)" if raw_limitation.match?(/usa.+restr/)
90
- l.remarks = "Propriété privée / privately owned" if raw_limitation.match?(/priv/)
91
- end
92
- end
93
-
94
- def runway_from(tr)
95
- tds = tr.css('td')
96
- AIXM.runway(
97
- name: tds[0].text.strip.split.join('/')
98
- ).tap do |runway|
99
- @runway = runway # TODO: needed for now for surface composition patches to work
100
- runway.length = AIXM.d(tds[1].css('span[id$="VAL_LEN"]').text.to_i, :m)
101
- runway.width = AIXM.d(tds[1].css('span[id$="VAL_WID"]').text.to_i, :m)
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
108
- runway.remarks = tds[7].text.cleanup.blank_to_nil
109
- values = tds[2].text.remove('°').strip.split
110
- runway.forth.geographic_orientation = AIXM.a(values.first.to_i)
111
- runway.back.geographic_orientation = AIXM.a(values.last.to_i)
112
- parts = tds[3].text.strip.split(/\n\s+\n\s+/)
113
- runway.forth.xy = (xy_from(parts[0]) unless parts[0].blank?)
114
- runway.back.xy = (xy_from(parts[1]) unless parts[1].blank?)
115
- values = tds[4].text.strip.split
116
- runway.forth.z = AIXM.z(values.first.to_i, :qnh)
117
- runway.back.z = AIXM.z(values.last.to_i, :qnh)
118
- displaced_thresholds = displaced_thresholds_from(tds[5])
119
- runway.forth.displaced_threshold = displaced_thresholds.first
120
- runway.back.displaced_threshold = displaced_thresholds.last
121
- end
122
- end
123
-
124
- def displaced_thresholds_from(td)
125
- values = td.text.strip.split
126
- case values.count
127
- when 1 then []
128
- when 2 then [AIXM.xy(lat: values[0], long: values[1]), nil]
129
- when 3 then [nil, AIXM.xy(lat: values[1], long: values[2])]
130
- when 4 then [AIXM.xy(lat: values[0], long: values[1]), AIXM.xy(lat: values[2], long: values[3])]
131
- else fail "cannot parse displaced thresholds"
132
- end
133
- end
134
-
135
- patch AIXM::Component::Runway, :width do |parser, object, value|
136
- throw :abort unless value.zero?
137
- airport_id = parser.instance_variable_get(:@airport).id
138
- runway_name = object.name.to_s
139
- throw :abort if (width = parser.fixture.dig('runways', airport_id, runway_name, 'width')).nil?
140
- AIXM.d(width.to_i, :m)
141
- end
142
-
143
- patch AIXM::Component::Runway::Direction, :xy do |parser, object, value|
144
- throw :abort unless value.nil?
145
- airport_id = parser.instance_variable_get(:@airport).id
146
- direction_name = object.name.to_s
147
- throw :abort if (xy = parser.fixture.dig('runways', airport_id, direction_name, 'xy')).nil?
148
- lat, long = xy.split(/\s+/)
149
- AIXM.xy(lat: lat, long: long)
150
- end
151
-
152
- patch AIXM::Component::Surface, :composition do |parser, object, value|
153
- throw :abort unless value.blank?
154
- airport_id = parser.instance_variable_get(:@airport).id
155
- runway_name = parser.instance_variable_get(:@runway).name
156
- throw :abort if (composition = parser.fixture.dig('runways', airport_id, runway_name, 'composition')).nil?
157
- composition
158
- end
159
-
160
- end
161
- end
162
- end
@@ -1,31 +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::Common
8
- include AIPP::LF::Helpers::ADRadio
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
- prepare(html: read).css('tbody').first do |tbody|
19
- tbody.css('tr').group_by_chunks { |e| e.attr(:id).match?(/-TXT_NAME-/) }.each do |tr, trs|
20
- id = tr.css('span[id*="CODE_ICAO"]').text.cleanup
21
- next unless id = ID_FIXES.fetch(id, id)
22
- @airport = select(:airport, id: id).first
23
- addresses_from(trs).each { |a| @airport.add_address(a) }
24
- units_from(trs).each(&method(:add))
25
- end
26
- end
27
- end
28
-
29
- end
30
- end
31
- end
@@ -1,313 +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::Common
8
- include AIPP::LF::Helpers::ADRadio
9
- using AIXM::Refinements
10
-
11
- # Map source types to type and optional local type
12
- SOURCE_TYPES = {
13
- 'CTR' => { type: 'CTR' },
14
- 'RMZ' => { type: 'RAS', local_type: 'RMZ' },
15
- 'TMZ' => { type: 'RAS', local_type: 'TMZ' },
16
- 'RMZ-TMZ' => { type: 'RAS', local_type: 'RMZ-TMZ' }
17
- }.freeze
18
-
19
- # Airports without VAC (e.g. military installations)
20
- NO_VAC = %w(LFOA LFBC LFQE LFOE LFSX LFBM LFSO LFMO LFQP LFSI LFKS LFPV).freeze
21
-
22
- # Airports without VFR reporting points
23
- # TODO: designated points on map but no list (LFLD LFSN LFBS) or no AD info (LFRL)
24
- NO_DESIGNATED_POINTS = %w(LFAB LFAC LFAV LFAY LFBK LFBN LFBX LFCC LFCI LFCK LFCY LFDH LFDJ LFDN LFEC 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
25
-
26
- # Map synonyms for +correlate+
27
- SYNONYMS = [
28
- 'nord', 'north',
29
- 'est', 'east',
30
- 'sud', 'south',
31
- 'ouest', 'west',
32
- 'inst', 'instruction',
33
- 'junction', 'intersection',
34
- 'harbour', 'port',
35
- 'mouth', 'embouchure',
36
- 'tower', 'chateau'
37
- ].freeze
38
-
39
- def parse
40
- index_html = prepare(html: read("AD-0.6")) # index for AD-2.xxxx files
41
- index_html.css('#AD-0\.6\.eAIP > .toc-block:nth-of-type(3) .toc-block a').each do |a|
42
- @id = a.attribute('href').value[-4,4]
43
- begin
44
- aip_file = "AD-2.#{@id}"
45
- html = prepare(html: read(aip_file))
46
- # Airport
47
- @remarks = []
48
- @airport = AIXM.airport(
49
- source: source(position: html.css('tr[id*="CODE_ICAO"]').first.line, aip_file: aip_file),
50
- organisation: organisation_lf, # TODO: not yet implemented
51
- id: @id,
52
- name: html.css('tr[id*="CODE_ICAO"] td span:nth-of-type(2)').text.uptrans,
53
- xy: xy_from(html.css('#AD-2\.2-Position_Geo_Arp td:nth-of-type(3)').text)
54
- ).tap do |airport|
55
- airport.z = elevation_from(html.css('#AD-2\.2-Altitude_Reference td:nth-of-type(3)').text)
56
- airport.declination = declination_from(html.css('#AD-2\.2-Declinaison_Magnetique td:nth-of-type(3)').text)
57
- # airport.transition_z = AIXM.z(5000, :qnh) # TODO: default - exceptions may exist
58
- airport.timetable = timetable_from!(html.css('#AD-2\.3-Gestionnaire_AD td:nth-of-type(3)').text)
59
- end
60
- runways_from(html.css('div[id*="-AD-2\.12"] tbody')).each { |r| @airport.add_runway(r) if r }
61
- helipads_from(html.css('div[id*="-AD-2\.16"] tbody')).each { |h| @airport.add_helipad(h) if h }
62
- text = html.css('#AD-2\.2-Observations td:nth-of-type(3)').text
63
- @airport.remarks = ([remarks_from(text)] + @remarks).compact.join("\n\n").blank_to_nil
64
- add @airport
65
- # Airspaces
66
- airspaces_from(html.css('div[id*="-AD-2\.17"] tbody')).each(&method(:add))
67
- # Radio
68
- trs = html.css('div[id*="-AD-2\.18"] tbody tr')
69
- addresses_from(trs).each { |a| @airport.add_address(a) }
70
- units_from(trs).each(&method(:add))
71
- # Landing aids
72
- # TODO: LOC/GP/DME as of section 2.19
73
- # Designated points
74
- unless NO_VAC.include?(@id) || NO_DESIGNATED_POINTS.include?(@id)
75
- pdf = read("VAC-#{@id}")
76
- designated_points_from(pdf).tap do |designated_points|
77
- fix_designated_point_remarks(designated_points)
78
- # debug(designated_points)
79
- designated_points.each(&method(:add))
80
- end
81
- end
82
- rescue => error
83
- warn("error parsing airport #{@id}: #{error.message}", pry: error)
84
- end
85
- end
86
- end
87
-
88
- private
89
-
90
- def declination_from(text)
91
- value, direction = text.strip.split('°')
92
- value = value.to_f * (direction == 'W' ? -1 : 1)
93
- end
94
-
95
- def remarks_from(text)
96
- text.sub(/NIL|\(\*\)\s+/, '').strip.gsub(/(\s)\s+/, '\1').blank_to_nil
97
- end
98
-
99
- def runways_from(tbody)
100
- directions_map = tbody.css('tr[id*="TXT_DESIG"]').map do |tr|
101
- [AIXM.a(tr.css('td:first-of-type').text.strip), tr]
102
- end.to_h
103
- remarks_map = tbody.css('tr[id*="TXT_RMK_NAT"]').map do |tr|
104
- [tr.text.strip[/\A\((\d+)\)/, 1].to_i, tr.css('span')]
105
- end.to_h
106
- directions = directions_map.keys
107
- grouped_directions = directions.map do |direction|
108
- inverted_direction = direction.invert
109
- if directions.include? inverted_direction
110
- [direction, inverted_direction].map(&:to_s).sort.join('/')
111
- else
112
- direction.to_s
113
- end
114
- end.uniq
115
- grouped_directions.map do |runway_name|
116
- AIXM.runway(name: runway_name).tap do |runway|
117
- %i(forth back).each do |direction_attr|
118
- if direction = runway.send(direction_attr)
119
- tr = directions_map[direction.name]
120
- if direction_attr == :forth
121
- length, width = tr.css('td:nth-of-type(3)').text.strip.split('x')
122
- runway.length = AIXM.d(length.strip.to_i, :m)
123
- runway.width = AIXM.d(width.strip.to_i, :m)
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
130
- if (text = tr.css('td:nth-of-type(4)').text).match?(AIXM::PCN_RE)
131
- runway.surface.pcn = text
132
- end
133
- end
134
- text = tr.css('td:nth-of-type(6)').text.strip
135
- direction.xy = (xy_from(text) unless text.match?(/\A(\(.*)?\z/m))
136
- if (text = tr.css('td:nth-of-type(7)').text.strip[/thr:\s+(\d+\s+\w+)/i, 1]).present?
137
- direction.z = elevation_from(text)
138
- end
139
- if (text = tr.css('td:nth-of-type(2)').text.strip.sub(/\A(\d+).*$/m, '\1')).present?
140
- direction.geographic_orientation = AIXM.a(text.to_i)
141
- end
142
- if (text = tr.css('td:nth-of-type(6)').text[/\((.+)\)/m, 1]).present?
143
- direction.displaced_threshold = xy_from(text)
144
- end
145
- if (text = tr.css('td:nth-of-type(10)').text.strip[/\A\((\d+)\)/, 1]).present?
146
- direction.remarks = remarks_from(remarks_map.fetch(text.to_i).text)
147
- end
148
- end
149
- end
150
- end
151
- end
152
- end
153
-
154
- def helipads_from(tbody)
155
- text_fr = tbody.css('td:nth-of-type(3)').text.compact
156
- text_en = tbody.css('td:nth-of-type(4)').text.compact
157
- case text_fr
158
- when /NIL/, /\A\W*\z/
159
- []
160
- when /instructions?\s+twr/i
161
- @remarks << "HELICOPTER:\nSur instructions TWR.\nOn TWR clearance."
162
- []
163
- when AIXM::DMS_RE
164
- text_fr.scan(AIXM::DMS_RE).each_slice(2).with_index(1).map do |(lat, long), index|
165
- AIXM.helipad(
166
- name: "H#{index}",
167
- xy: AIXM.xy(lat: lat.first, long: long.first)
168
- )
169
- end
170
- else
171
- @remarks << ['HELICOPTER:', text_fr.blank_to_nil, text_en.blank_to_nil].compact.join("\n")
172
- []
173
- end
174
- end
175
-
176
- def airspaces_from(tbody)
177
- return [] if tbody.text.blank?
178
- airspace = nil
179
- tbody.css('tr').to_enum.with_object([]) do |tr, array|
180
- if tr.attr(:class) =~ /keep-with-next-row/
181
- airspace = airspace_from tr
182
- else
183
- tds = tr.css('td')
184
- airspace.geometry = geometry_from tds[0].text
185
- fail("geometry is not closed") unless airspace.geometry.closed?
186
- airspace.layers << layer_from(tds[2].text, tds[1].text.strip)
187
- airspace.layers.first.timetable = timetable_from! tds[4].text
188
- airspace.layers.first.remarks = remarks_from(tds[4].text)
189
- array << airspace
190
- end
191
- end
192
- end
193
-
194
- def airspace_from(tr)
195
- spans = tr.css(:span)
196
- source_type = spans[1].text.blank_to_nil
197
- fail "unknown type `#{source_type}'" unless SOURCE_TYPES.has_key? source_type
198
- AIXM.airspace(
199
- name: [spans[2].text, anglicise(name: spans[3]&.text)].compact.join(' '),
200
- type: SOURCE_TYPES.dig(source_type, :type),
201
- local_type: SOURCE_TYPES.dig(source_type, :local_type)
202
- ).tap do |airspace|
203
- airspace.source = source(position: tr.line)
204
- end
205
- end
206
-
207
- def designated_points_from(pdf, recursive=false)
208
- from = (pdf.text =~ /^(.*?coordinates.*?names?)/i)
209
- return [] if recursive && !from
210
- warn("no designated points section begin found for #{@id}", pry: binding) unless from
211
- from += $1.length
212
- 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)
213
- warn("no designated points section end found for #{@id}", pry: binding) unless to
214
- from, to = from + pdf.range.min, to + pdf.range.min # offset when recursive
215
- buffer = {}
216
- pdf.from(from).to(to).each_line.with_object([]) do |(line, page, last), designated_points|
217
- line.remove!(/\u2190/) # remove arrow symbols
218
- has_id = $1 if line.sub!(/^\s{,20}([A-Z][A-Z\d ]{1,3})(?=\W)/, '')
219
- has_xy = line.match?(AIXM::DMS_RE)
220
- designated_points << designated_point_from(buffer, pdf) if has_id || has_xy
221
- if has_xy
222
- 2.times { (buffer[:xy] ||= []) << $1 if line.sub!(AIXM::DMS_RE, '') }
223
- buffer[:xy]&.compact!
224
- line.remove!(/\d{3,4}\D.+?MTG/) # remove extra columns (e.g. LFML)
225
- line.remove!(/[\s#{AIXM::MIN}#{AIXM::SEC}]*[-\u2013]/) # remove dash between coordinates
226
- end
227
- buffer[:page] = page
228
- buffer[:id] = has_id if has_id
229
- buffer[:remarks] = [buffer[:remarks], line].join("\n")
230
- designated_points << designated_point_from(buffer, pdf) if last
231
- end.compact + designated_points_from(pdf.from(to).to(:end), true)
232
- end
233
-
234
- def designated_point_from(buffer, pdf)
235
- if buffer[:id] && buffer[:xy]&.size == 2
236
- buffer[:remarks].gsub!(/ {20}/, "\n") # recognize empty column space
237
- buffer[:remarks].remove!(/\(\d+\)/) # remove footnotes
238
- buffer[:remarks] = buffer[:remarks].unglue # separate glued words
239
- AIXM.designated_point(
240
- source: source(position: buffer[:page], aip_file: pdf.file.basename('.*').to_s),
241
- type: :vfr_mandatory_reporting_point,
242
- id: buffer[:id].remove(/\W/),
243
- xy: AIXM.xy(lat: buffer[:xy].first, long: buffer[:xy].last)
244
- ).tap do |designated_point|
245
- designated_point.airport = @airport
246
- designated_point.remarks = buffer[:remarks].compact.blank_to_nil
247
- buffer.clear
248
- end
249
- end
250
- end
251
-
252
- # Assign scattered similar remarks to one and the same designated point
253
- def fix_designated_point_remarks(designated_points)
254
- one = nil
255
- designated_points.map do |two|
256
- if one
257
- one_lines, two_lines = one.remarks&.lines, two.remarks&.lines
258
- if one_lines && two_lines
259
- if one_lines.count > 1 && (line = one_lines.last) !~ %r(\s/\s)
260
- # Move up
261
- if line.correlate(remainder = one_lines[0..-2].join, SYNONYMS) < line.correlate(two.remarks)
262
- two.remarks = [line, two.remarks].join("\n").compact
263
- one.remarks = remainder.compact
264
- end
265
- elsif two_lines.count > 1 && (line = two_lines.first) !~ %r(\s/\s)
266
- # Move down
267
- line = two_lines.first
268
- if line.correlate(remainder = two_lines[1..-1].join, SYNONYMS) < line.correlate(one.remarks)
269
- one.remarks = [one.remarks, line].join("\n").compact
270
- two.remarks = remainder.compact
271
- end
272
- end
273
- end
274
- end
275
- one = two
276
- end.map do |designated_point|
277
- designated_point.remarks = designated_point.remarks&.cleanup.blank_to_nil
278
- end
279
- end
280
-
281
- # def debug(dp)
282
- # f = "/Users/sschwyn/Desktop/okay/#{@id}.txt"
283
- # result = "\n--- #{@id} ---\n\n".red
284
- # dp.each do |d|
285
- # result += d.id.red + "\t#{d.xy.lat} - #{d.xy.long}\n"
286
- # result += "#{d.remarks}\n\n".blue
287
- # end
288
- # result += "#{dp.count} point(s) for #{@id}".red
289
- # unless File.exist?(f) && result == File.read(f)
290
- # puts result
291
- # gets
292
- # puts "\e[H\e[2J"
293
- # end
294
- # File.write(f, result)
295
- # end
296
-
297
- patch AIXM::Component::Runway::Direction, :xy do |parser, object, value|
298
- throw :abort unless value.nil?
299
- airport_id = parser.instance_variable_get(:@airport).id
300
- direction_name = object.name.to_s
301
- throw :abort if (xy = parser.fixture.dig('runways', airport_id, direction_name, 'xy')).nil?
302
- lat, long = xy.split(/\s+/)
303
- AIXM.xy(lat: lat, long: long)
304
- end
305
-
306
- patch AIXM::Feature::NavigationalAid, :remarks do |parser, object, value|
307
- airport_id, designated_point_id = object.airport.id, object.id
308
- parser.fixture.dig('designated_points', airport_id, designated_point_id, 'remarks') || throw(:abort)
309
- end
310
-
311
- end
312
- end
313
- end