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,167 +0,0 @@
1
- module AIPP
2
- module LF
3
-
4
- # FIR, TMA etc
5
- class ENR21 < AIP
6
-
7
- include AIPP::LF::Helpers::Base
8
-
9
- # Airspaces to be ignored
10
- NAME_BLACKLIST_RE = /deleg/i.freeze
11
-
12
- # Map source types to type and optional local type
13
- SOURCE_TYPES = {
14
- 'FIR' => { type: 'FIR' },
15
- 'UIR' => { type: 'UIR' },
16
- 'UTA' => { type: 'UTA' },
17
- 'CTA' => { type: 'CTA' },
18
- 'LTA' => { type: 'CTA', local_type: 'LTA' },
19
- 'TMA' => { type: 'TMA' },
20
- 'SIV' => { type: 'SECTOR', local_type: 'SIV' } # providing FIS
21
- }.freeze
22
-
23
- # Map airspace "<type> <name>" to location indicator
24
- LOCATION_INDICATORS = {
25
- 'FIR BORDEAUX' => 'LFBB',
26
- 'FIR BREST' => 'LFRR',
27
- 'FIR MARSEILLE' => 'LFMM',
28
- 'FIR PARIS' => 'LFFF',
29
- 'FIR REIMS' => 'LFRR'
30
- }.freeze
31
-
32
- # Fix incomplete SIV service columns
33
- SERVICE_FIXES = {
34
- "IROISE INFO 135.825 / 119.575 (1)" => "APP IROISE\nIROISE INFO 135.825 / 119.575 (1)",
35
- "APP TOULOUSE\nTOULOUSE INFO" => "APP TOULOUSE\nTOULOUSE INFO 121.250"
36
- }.freeze
37
-
38
- def parse
39
- prepare(html: read).css('tbody').each do |tbody|
40
- airspace = nil
41
- tbody.css('tr').to_enum.with_index(1).each do |tr, index|
42
- if tr.attr(:id).match?(/--TXT_NAME/)
43
- if airspace
44
- if airspace.name.match? NAME_BLACKLIST_RE
45
- verbose_info "Ignoring #{airspace.type} #{airspace.name}" unless airspace.type == :terminal_control_area
46
- else
47
- add airspace
48
- end
49
- end
50
- airspace = airspace_from tr.css(:td).first
51
- verbose_info "Parsing #{airspace.type} #{airspace.name}" unless airspace.type == :terminal_control_area
52
- next
53
- end
54
- begin
55
- tds = tr.css('td')
56
- if airspace.type == :terminal_control_area && tds[0].text.blank_to_nil
57
- if airspace.layers.any?
58
- if airspace.name.match? NAME_BLACKLIST_RE
59
- verbose_info "Ignoring #{airspace.type} #{airspace.name}"
60
- else
61
- add airspace
62
- end
63
- end
64
- airspace = airspace_from tds[0]
65
- verbose_info "Parsing #{airspace.type} #{airspace.name}"
66
- end
67
- if airspace
68
- remarks = tds[-1].text
69
- if tds[0].text.blank_to_nil
70
- airspace.geometry = geometry_from tds[0].text
71
- fail("geometry is not closed") unless airspace.geometry.closed?
72
- end
73
- layer = layer_from(tds[-3].text)
74
- layer.class = class_from(tds[1].text) if tds.count == 5
75
- layer.location_indicator = LOCATION_INDICATORS.fetch("#{airspace.type} #{airspace.name}", nil)
76
- if airspace.local_type == 'SIV' # services parsed for SIV only
77
- layer.add_services services_from(tds[-2], remarks)
78
- end
79
- layer.timetable = timetable_from! remarks
80
- layer.remarks = remarks_from remarks
81
- airspace.add_layer layer
82
- end
83
- rescue => error
84
- warn("error parsing #{airspace.type} `#{airspace.name}' at ##{index}: #{error.message}", pry: error)
85
- end
86
- end
87
- add airspace if airspace
88
- end
89
- end
90
-
91
- private
92
-
93
- def airspace_from(td)
94
- spans = td.children.split { _1.name == 'br' }.first.css(:span).drop_while { _1.text.match? '\s' }
95
- source_type = spans[0].text.blank_to_nil
96
- fail "unknown type `#{source_type}'" unless SOURCE_TYPES.has_key? source_type
97
- AIXM.airspace(
98
- name: anglicise(name: spans[1..-1].join(' ')),
99
- type: SOURCE_TYPES.dig(source_type, :type),
100
- local_type: SOURCE_TYPES.dig(source_type, :local_type)
101
- ).tap do |airspace|
102
- airspace.source = source(position: td.line)
103
- end
104
- end
105
-
106
- def class_from(text)
107
- text.strip
108
- end
109
-
110
- def services_from(td, remarks)
111
- text = td.text.cleanup
112
- text = SERVICE_FIXES.fetch(text, text) # fix incomplete service columns
113
- text.gsub!(/(info|app)\s+([\d.]{3,})/i, "\\1\n\\2") # put frequencies on separate line
114
- text.gsub!(/(\d)\s*\/\s*(\d)/, "\\1\n\\2") # split frequencies onto separate lines
115
- units, services = [], []
116
- text.split("\n").each do |line|
117
- case line
118
- when /^(.+(?:info|app))$/i # service
119
- callsign = $1
120
- service = AIXM.service(
121
- # TODO: add source as soon as it is supported by components
122
- # source: source(position: td.line),
123
- type: :flight_information_service
124
- ).tap do |service|
125
- service.timetable = AIXM::H24 if remarks.match? /h\s?24/i
126
- end
127
- services << [service, callsign]
128
- units.shift.add_service service
129
- when /^(.*?)(\d{3}[.\d]*)(.*)$/ # frequency
130
- label, freq, footnote = $1, $2, $3
131
- service, callsign = services.last
132
- frequency = AIXM.frequency(
133
- transmission_f: AIXM.f(freq.to_f, :mhz),
134
- callsigns: { en: callsign, fr: callsign }
135
- ).tap do |frequency|
136
- frequency.type = :standard
137
- frequency.timetable = AIXM::H24 if remarks.match? /h\s?24/i
138
- frequency.remarks = [
139
- (remarks.extract(/#{Regexp.escape(footnote.strip)}\s*([^\n]+)/).join(' / ') unless footnote.empty?),
140
- label.strip
141
- ].map(&:blank_to_nil).compact.join(' / ').blank_to_nil
142
- end
143
- service.add_frequency frequency
144
- when /.*(?<!info|app|\d{3}|\))$/i # unit
145
- unit = AIXM.unit(
146
- source: source(position: td.line),
147
- organisation: organisation_lf, # TODO: not yet implemented
148
- name: line,
149
- type: :flight_information_centre,
150
- class: :icao
151
- )
152
- units << ((u = find(unit).first) ? (unit = u) : (add unit))
153
- else
154
- fail("cannot parse `#{text}'")
155
- end
156
- end
157
- services = services.map(&:first)
158
- fail("at least one service has no frequency") if services.any? { _1.frequencies.none? }
159
- services
160
- end
161
-
162
- def remarks_from(text)
163
- text.strip.gsub(/(\s)\s+/, '\1').blank_to_nil
164
- end
165
- end
166
- end
167
- end
@@ -1,41 +0,0 @@
1
- module AIPP
2
- module LF
3
-
4
- # ENR Navaids
5
- class ENR41 < AIP
6
-
7
- include AIPP::LF::Helpers::Base
8
- include AIPP::LF::Helpers::NavigationalAid
9
-
10
- def parse
11
- prepare(html: read).css('tbody').each do |tbody|
12
- tbody.css('tr').to_enum.with_index(1).each do |tr, index|
13
- tds = tr.css('td')
14
- navigational_aid = navigational_aid_from(
15
- {
16
- name: tds[0],
17
- type: tds[1],
18
- id: tds[2],
19
- f: tds[3],
20
- schedule: tds[4],
21
- xy: tds[5],
22
- z: tds[6]
23
- },
24
- source: source(position: tr.line),
25
- sections: {
26
- range: tds[5].css('span[id*="PORTEE"], span[id*="COUVERTURE"]'),
27
- situation: tds[7],
28
- observations: tds[9]
29
- }
30
- )
31
- if navigational_aid && aixm.features.find_by(navigational_aid.class, id: navigational_aid.id, xy: navigational_aid.xy).none?
32
- add navigational_aid
33
- end
34
- rescue => error
35
- warn("error parsing navigational aid at ##{index}: #{error.message}", pry: error)
36
- end
37
- end
38
- end
39
- end
40
- end
41
- end
@@ -1,27 +0,0 @@
1
- module AIPP
2
- module LF
3
-
4
- # Designated Points
5
- class ENR43 < AIP
6
-
7
- include AIPP::LF::Helpers::Base
8
-
9
- def parse
10
- prepare(html: read).css('tbody').each do |tbody|
11
- tbody.css('tr').to_enum.with_index(1).each do |tr, index|
12
- tds = tr.css('td')
13
- add AIXM.designated_point(
14
- source: source(position: tr.line),
15
- type: :icao,
16
- id: tds[0].text.strip,
17
- xy: xy_from(tds[1].text)
18
- )
19
- rescue => error
20
- warn("error parsing designated point at ##{index}: #{error.message}", pry: error)
21
- end
22
- end
23
- end
24
-
25
- end
26
- end
27
- end
@@ -1,106 +0,0 @@
1
- module AIPP
2
- module LF
3
-
4
- # D/P/R Zones
5
- class ENR51 < AIP
6
-
7
- include AIPP::LF::Helpers::Base
8
-
9
- # Map sections to whether to parse them
10
- SECTIONS = {
11
- '5.1-1': true,
12
- '5.1-2': false,
13
- '5.1-3': true,
14
- '5.1-4': true,
15
- '5.1-5-1': false,
16
- '5.1-5-2': true
17
- }
18
-
19
- # Map source types to type and optional local type
20
- SOURCE_TYPES = {
21
- 'D' => { type: 'D' },
22
- 'P' => { type: 'P' },
23
- 'R' => { type: 'R' },
24
- 'ZIT' => { type: 'P', local_type: 'ZIT' }
25
- }.freeze
26
-
27
- # Radius to use for zones consisting of one point only
28
- POINT_RADIUS = AIXM.d(1, :km).freeze
29
-
30
- def parse
31
- skip = false
32
- prepare(html: read).css('h4, thead ~ tbody').each do |tag|
33
- case tag.name
34
- when 'h4'
35
- section = tag.text.match(/^ENR ([\d.-]+)/).captures.first
36
- skip = !SECTIONS.fetch(section.to_sym)
37
- verbose_info "#{skip ? :Skipping : :Parsing} section #{section}"
38
- when 'tbody'
39
- next if skip
40
- airspace = nil
41
- tag.css('tr').to_enum.with_index(1).each do |tr, index|
42
- tds = tr.css('td')
43
- case
44
- when tr.attr(:id).match?(/TXT_NAME/) # airspace
45
- airspace = airspace_from tr
46
- when tds.count == 1 # big comment on separate row
47
- airspace.layers.first.remarks.
48
- concat("\n", tds.text.cleanup).
49
- remove!(/\((\d)\)\s*\(\1\)\W*/)
50
- else # layer
51
- begin
52
- tds = tr.css('td')
53
- airspace.geometry = geometry_from tds[0].text
54
- if airspace.geometry.point? # convert point to circle
55
- airspace.geometry = AIXM.geometry(
56
- AIXM.circle(
57
- center_xy: airspace.geometry.segments.first.xy,
58
- radius: POINT_RADIUS
59
- )
60
- )
61
- end
62
- fail("geometry is not closed") unless airspace.geometry.closed?
63
- airspace.add_layer layer_from(tds[1].text)
64
- airspace.layers.first.timetable = timetable_from! tds[2].text
65
- airspace.layers.first.remarks = remarks_from(tds[2], tds[3], tds[4])
66
- if aixm.features.find_by(:airspace, type: airspace.type, id: airspace.id).none?
67
- add airspace
68
- end
69
- rescue => error
70
- warn("error parsing airspace `#{airspace.name}' at ##{index}: #{error.message}", pry: error)
71
- end
72
- end
73
- end
74
- end
75
- end
76
- end
77
-
78
- private
79
-
80
- def airspace_from(tr)
81
- region, source_type, name = tr.text.cleanup.gsub(/\s/, ' ').split(nil, 3)
82
- fail "unknown type `#{source_type}'" unless SOURCE_TYPES.has_key? source_type
83
- AIXM.airspace(
84
- name: [region, source_type, name].join(' '),
85
- type: SOURCE_TYPES.dig(source_type, :type),
86
- local_type: SOURCE_TYPES.dig(source_type, :local_type)
87
- ).tap do |airspace|
88
- airspace.source = source(position: tr.line)
89
- end
90
- end
91
-
92
- def remarks_from(*parts)
93
- part_titles = ['TIMETABLE', 'RESTRICTION', 'AUTHORITY/CONDITIONS']
94
- [].tap do |remarks|
95
- parts.each.with_index do |part, index|
96
- if part = part.text.gsub(/ +/, ' ').gsub(/(\n ?)+/, "\n").strip.blank_to_nil
97
- unless index.zero? && part == 'H24'
98
- remarks << "**#{part_titles[index]}**\n#{part}"
99
- end
100
- end
101
- end
102
- end.join("\n\n").blank_to_nil
103
- end
104
- end
105
- end
106
- end
@@ -1,90 +0,0 @@
1
- module AIPP
2
- module LF
3
-
4
- # Obstacles
5
- class ENR54 < AIP
6
-
7
- include AIPP::LF::Helpers::Base
8
-
9
- # Obstacles to be ignored
10
- NAME_BLACKLIST = %w(51076 52055 59000 72039).freeze # all duplicates
11
-
12
- # Map type descriptions to AIXM types and remarks
13
- TYPES = {
14
- 'Antenne' => [:antenna],
15
- 'Bâtiment' => [:building],
16
- 'Câble' => [:other, 'Cable / Câble'],
17
- 'Centrale thermique' => [:building, 'Thermal power plant / Centrale thermique'],
18
- "Château d'eau" => [:tower, "Water tower / Château d'eau"],
19
- 'Cheminée' => [:chimney],
20
- 'Derrick' => [:tower, 'Derrick'],
21
- 'Eglise' => [:tower, 'Church / Eglise'],
22
- 'Eolienne(s)' => [:wind_turbine],
23
- 'Grue' => [:tower, 'Crane / Grue'],
24
- 'Mât' => [:mast],
25
- 'Phare marin' => [:tower, 'Lighthouse / Phare marin'],
26
- 'Pile de pont' => [:other, 'Bridge piers / Pile de pont'],
27
- 'Portique' => [:building, 'Arch / Portique'],
28
- 'Pylône' => [:mast, 'Pylon / Pylône'],
29
- 'Silo' => [:tower, 'Silo'],
30
- 'Terril' => [:other, 'Spoil heap / Teril'],
31
- 'Torchère' => [:chimney, 'Flare / Torchère'],
32
- 'Tour' => [:tower],
33
- 'Treillis métallique' => [:other, 'Metallic grid / Treillis métallique']
34
- }.freeze
35
-
36
- def parse
37
- tbody = prepare(html: read).css('tbody').last
38
- tbody.css('tr').to_enum.with_index(1).each do |tr, index|
39
- tds = tr.css('td')
40
- name = tds[0].text.cleanup
41
- next if NAME_BLACKLIST.include? name
42
- elevation, height = tds[4].text.cleanup.split(/[()]/).map { _1.cleanup.remove("\n") }
43
- type, type_remarks = TYPES.fetch(tds[2].text.cleanup)
44
- count = tds[3].text.cleanup.to_i
45
- visibility = tds[5].text.cleanup
46
- obstacle = AIXM.obstacle(
47
- source: source(position: tr.line),
48
- name: name,
49
- type: type,
50
- xy: xy_from(tds[1].text),
51
- z: z_from(elevation + 'AMSL')
52
- ).tap do |obstacle|
53
- obstacle.height = d_from(height)
54
- obstacle.marking = visibility.match?(/jour/i)
55
- obstacle.lighting = visibility.match?(/nuit/i)
56
- obstacle.remarks = remarks_from(type_remarks, (count if count > 1), tds[6].text)
57
- end
58
- if count > 1
59
- obstacle_group = AIXM.obstacle_group(
60
- source: obstacle.source,
61
- name: obstacle.name
62
- ).tap do |obstacle_group|
63
- obstacle_group.remarks = "#{count} obstacles"
64
- end
65
- obstacle_group.add_obstacle obstacle
66
- add obstacle_group
67
- else
68
- add obstacle
69
- end
70
- rescue => error
71
- warn("error parsing obstacle at ##{index}: #{error.message}", pry: error)
72
- end
73
- end
74
-
75
- private
76
-
77
- def remarks_from(*parts)
78
- part_titles = ['TYPE', 'NUMBER/NOMBRE', 'DETAILS']
79
- [].tap do |remarks|
80
- parts.each.with_index do |part, index|
81
- if part
82
- part = part.to_s.cleanup.blank_to_nil
83
- remarks << "**#{part_titles[index]}**\n#{part}"
84
- end
85
- end
86
- end.join("\n\n").blank_to_nil
87
- end
88
- end
89
- end
90
- end
@@ -1,55 +0,0 @@
1
- module AIPP
2
- module LF
3
-
4
- # Sporting and Recreational Activities
5
- class ENR55 < AIP
6
-
7
- include AIPP::LF::Helpers::Base
8
-
9
- # Map raw activities to activity and airspace type
10
- ACTIVITIES = {
11
- 'activité particulière' => { activity: :other, airspace_type: :dangerous_activities_area },
12
- 'aéromodélisme' => { activity: :aeromodelling, airspace_type: :dangerous_activities_area },
13
- 'parachutage' => { activity: :parachuting, airspace_type: :dangerous_activities_area },
14
- 'treuillage' => { activity: :glider_winch, airspace_type: :dangerous_activities_area },
15
- 'voltige' => { activity: :acrobatics, airspace_type: :dangerous_activities_area }
16
- }.freeze
17
-
18
- def parse
19
- prepare(html: read).css('tbody').each do |tbody|
20
- tbody.css('tr').to_enum.each_slice(2).with_index(1) do |trs, index|
21
- begin
22
- id, activity_and_name, upper_limit, timetable = trs.first.css('td')
23
- activity, name = activity_and_name.css('span')
24
- lateral_limit, lower_limit, remarks = trs.last.css('td')
25
- lateral_limit.search('br').each { _1.replace("|||") }
26
- geometry, lateral_limit = lateral_limit.text.split('|||', 2)
27
- lateral_limit&.gsub!('|||', "\n")
28
- remarks = [remarks&.text&.cleanup&.blank_to_nil]
29
- s = timetable&.text&.cleanup and remarks.prepend('**SCHEDULE**', s, '')
30
- s = lateral_limit&.cleanup and remarks.prepend('**LATERAL LIMIT**', s, '')
31
- airspace = AIXM.airspace(
32
- source: source(position: trs.first.line),
33
- id: id.text.strip,
34
- type: ACTIVITIES.fetch(activity.text.downcase).fetch(:airspace_type),
35
- name: [id.text.strip, name.text.cleanup].join(' ')
36
- ).tap do |airspace|
37
- airspace.geometry = geometry_from(geometry)
38
- airspace.add_layer(
39
- layer_from([upper_limit.text, lower_limit.text].join('---').cleanup).tap do |layer|
40
- layer.activity = ACTIVITIES.fetch(activity.text.downcase).fetch(:activity)
41
- layer.remarks = remarks.compact.join("\n")
42
- end
43
- )
44
- end
45
- rescue => error
46
- warn("error parsing #{airspace.type} `#{airspace.name}' at ##{index}: #{error.message}", pry: error)
47
- end
48
- add airspace
49
- end
50
- end
51
- end
52
-
53
- end
54
- end
55
- end