aipp 1.0.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +2 -2
  3. data/CHANGELOG.md +17 -1
  4. data/README.md +269 -150
  5. data/exe/aip2aixm +2 -8
  6. data/exe/aip2ofmx +2 -8
  7. data/exe/notam2aixm +5 -0
  8. data/exe/notam2ofmx +5 -0
  9. data/lib/aipp/aip/README.md +10 -0
  10. data/lib/aipp/aip/executable.rb +40 -0
  11. data/lib/aipp/aip/parser.rb +9 -0
  12. data/lib/aipp/aip/runner.rb +85 -0
  13. data/lib/aipp/border.rb +2 -2
  14. data/lib/aipp/debugger.rb +14 -19
  15. data/lib/aipp/downloader/file.rb +57 -0
  16. data/lib/aipp/downloader/graphql.rb +29 -0
  17. data/lib/aipp/downloader/http.rb +48 -0
  18. data/lib/aipp/downloader.rb +78 -29
  19. data/lib/aipp/environment.rb +88 -0
  20. data/lib/aipp/executable.rb +36 -53
  21. data/lib/aipp/notam/README.md +25 -0
  22. data/lib/aipp/notam/executable.rb +27 -0
  23. data/lib/aipp/notam/parser.rb +9 -0
  24. data/lib/aipp/notam/runner.rb +28 -0
  25. data/lib/aipp/parser.rb +133 -160
  26. data/lib/aipp/patcher.rb +4 -5
  27. data/lib/aipp/regions/LF/README.md +6 -2
  28. data/lib/aipp/regions/LF/aip/aerodromes.rb +220 -0
  29. data/lib/aipp/regions/LF/aip/d_p_r_airspaces.rb +53 -0
  30. data/lib/aipp/regions/LF/aip/dangerous_activities.rb +48 -0
  31. data/lib/aipp/regions/LF/aip/designated_points.rb +44 -0
  32. data/lib/aipp/regions/LF/aip/helipads.rb +119 -0
  33. data/lib/aipp/regions/LF/aip/navigational_aids.rb +82 -0
  34. data/lib/aipp/regions/LF/aip/obstacles.rb +150 -0
  35. data/lib/aipp/regions/LF/aip/serviced_airspaces.rb +67 -0
  36. data/lib/aipp/regions/LF/aip/services.rb +169 -0
  37. data/lib/aipp/regions/LF/fixtures/aerodromes.yml +2 -2
  38. data/lib/aipp/regions/LF/helpers/base.rb +32 -32
  39. data/lib/aipp/regions/LS/README.md +59 -0
  40. data/lib/aipp/regions/LS/helpers/base.rb +111 -0
  41. data/lib/aipp/regions/LS/notam/ENR.rb +173 -0
  42. data/lib/aipp/runner.rb +152 -0
  43. data/lib/aipp/version.rb +1 -1
  44. data/lib/aipp.rb +30 -11
  45. data/lib/core_ext/array.rb +13 -0
  46. data/lib/core_ext/nokogiri.rb +56 -8
  47. data/lib/core_ext/string.rb +63 -1
  48. data.tar.gz.sig +0 -0
  49. metadata +115 -64
  50. metadata.gz.sig +0 -0
  51. data/lib/aipp/aip.rb +0 -166
  52. data/lib/aipp/regions/LF/aerodromes.rb +0 -223
  53. data/lib/aipp/regions/LF/d_p_r_airspaces.rb +0 -56
  54. data/lib/aipp/regions/LF/dangerous_activities.rb +0 -49
  55. data/lib/aipp/regions/LF/designated_points.rb +0 -47
  56. data/lib/aipp/regions/LF/helipads.rb +0 -122
  57. data/lib/aipp/regions/LF/navigational_aids.rb +0 -85
  58. data/lib/aipp/regions/LF/obstacles.rb +0 -153
  59. data/lib/aipp/regions/LF/serviced_airspaces.rb +0 -70
  60. data/lib/aipp/regions/LF/services.rb +0 -172
@@ -0,0 +1,220 @@
1
+ module AIPP::LF::AIP
2
+ class Aerodromes < AIPP::AIP::Parser
3
+
4
+ include AIPP::LF::Helpers::Base
5
+ include AIPP::LF::Helpers::UsageLimitation
6
+ include AIPP::LF::Helpers::Surface
7
+
8
+ APPROACH_LIGHTING_TYPES = {
9
+ 'CAT I' => :cat_1,
10
+ 'CAT II' => :cat_2,
11
+ 'CAT III' => :cat_3,
12
+ 'CAT II-III' => :cat_2_and_3
13
+ }.freeze
14
+
15
+ LIGHTING_POSITIONS = {
16
+ threshold: 'Thr',
17
+ touch_down_zone: 'Tdz',
18
+ center_line: 'Axe',
19
+ edge: 'Bord',
20
+ runway_end: 'Fin',
21
+ stopway_center_line: 'Swy'
22
+ }.freeze
23
+
24
+ LIGHTING_COLORS = {
25
+ 'W' => :white,
26
+ 'R' => :red,
27
+ 'G' => :green,
28
+ 'B' => :blue,
29
+ 'Y' => :yellow
30
+ }.freeze
31
+
32
+ ICAO_LIGHTING_COLORS = {
33
+ center_line: :white,
34
+ edge: :white
35
+ }.freeze
36
+
37
+ def parse
38
+ AIPP.cache.ad.css(%Q(Ad[lk^="[LF]"])).each do |ad_node|
39
+ # Build airport
40
+ next unless limitation_type = LIMITATION_TYPES.fetch(ad_node.(:AdStatut))
41
+ airport = AIXM.airport(
42
+ source: source(part: 'AD', position: ad_node.line),
43
+ organisation: organisation_lf,
44
+ id: id_from(ad_node.(:AdCode)),
45
+ name: ad_node.(:AdNomComplet),
46
+ xy: xy_from(ad_node.(:Geometrie))
47
+ ).tap do |airport|
48
+ airport.meta = ad_node.attr('pk')
49
+ airport.z = given(ad_node.(:AdRefAltFt)) { AIXM.z(_1.to_i, :qnh) }
50
+ airport.declination = ad_node.(:AdMagVar)&.to_f
51
+ airport.add_usage_limitation(type: limitation_type.fetch(:limitation)) do |limitation|
52
+ limitation.remarks = limitation_type[:remarks]
53
+ [
54
+ (:scheduled if ad_node.(:TfcRegulier?)),
55
+ (:not_scheduled if ad_node.(:TfcNonRegulier?)),
56
+ (:private if ad_node.(:TfcPrive?)),
57
+ (:other unless ad_node.(:TfcRegulier?) || ad_node.(:TfcNonRegulier?) || ad_node.(:TfcPrive?))
58
+ ].compact.each do |purpose|
59
+ limitation.add_condition do |condition|
60
+ condition.realm = limitation_type.fetch(:realm)
61
+ condition.origin = case
62
+ when ad_node.(:TfcIntl?) && ad_node.(:TfcNtl?) then :any
63
+ when ad_node.(:TfcIntl?) then :international
64
+ when ad_node.(:TfcNtl?) then :national
65
+ else :other
66
+ end
67
+ condition.rule = case
68
+ when ad_node.(:TfcIfr?) && ad_node.(:TfcVfr?) then :ifr_and_vfr
69
+ when ad_node.(:TfcIfr?) then :ifr
70
+ when ad_node.(:TfcVfr?) then :vfr
71
+ else
72
+ warn("falling back to VFR rule for `#{airport.id}'", severe: false)
73
+ :vfr
74
+ end
75
+ condition.purpose = purpose
76
+ end
77
+ end
78
+ end
79
+ # TODO: link to VAC once supported downstream
80
+ # # Link to VAC
81
+ # airport.remarks = [
82
+ # airport.remarks.to_s,
83
+ # link_to('VAC-AD', origin_for("VAC-#{airport.id}").file)
84
+ # ].join("\n")
85
+ AIPP.cache.rwy.css(%Q(Rwy:has(Ad[pk="#{ad_node.attr(:pk)}"]))).each do |rwy_node|
86
+ add_runway_to(airport, rwy_node)
87
+ end
88
+ end
89
+ add airport
90
+ end
91
+ end
92
+
93
+ private
94
+
95
+ def id_from(content)
96
+ case content
97
+ when /^\d{2}$/ then 'LF00' + content # private aerodromes without official ID
98
+ else 'LF' + content
99
+ end
100
+ end
101
+
102
+ def add_runway_to(airport, rwy_node)
103
+ AIXM.runway(
104
+ name: rwy_node.(:Rwy)
105
+ ).tap do |runway|
106
+ rwylgt_nodes = AIPP.cache.rwylgt.css(%Q(RwyLgt:has(Rwy[pk="#{rwy_node.attr(:pk)}"])))
107
+ airport.add_runway(runway)
108
+ runway.dimensions = AIXM.r(AIXM.d(rwy_node.(:Longueur)&.to_i, :m), AIXM.d(rwy_node.(:Largeur)&.to_i, :m))
109
+ runway.surface = surface_from(rwy_node)
110
+ runway.forth.geographic_bearing = given(rwy_node.(:OrientationGeo)) { AIXM.a(_1.to_f) }
111
+ runway.forth.xy = given(rwy_node.(:LatThr1), rwy_node.(:LongThr1)) { AIXM.xy(lat: _1.to_f, long: _2.to_f) }
112
+ runway.forth.displaced_threshold = given(rwy_node.(:LatDThr1), rwy_node.(:LongDThr1)) { AIXM.xy(lat: _1.to_f, long: _2.to_f) }
113
+ runway.forth.z = given(rwy_node.(:AltFtDThr1)) { AIXM.z(_1.to_i, :qnh) }
114
+ runway.forth.z ||= given(rwy_node.(:AltFtThr1)) { AIXM.z(_1.to_i, :qnh) }
115
+ if rwylgt_node = rwylgt_nodes[0]
116
+ runway.forth.vasis = vasis_from(rwylgt_node)
117
+ given(approach_lighting_from(rwylgt_node)) { runway.forth.add_approach_lighting(_1) }
118
+ LIGHTING_POSITIONS.each_key do |position|
119
+ given(lighting_from(rwylgt_node, position)) { runway.forth.add_lighting(_1) }
120
+ end
121
+ end
122
+ if rwy_node.(:Rwy).match? '/'
123
+ runway.back.xy = given(rwy_node.(:LatThr2), rwy_node.(:LongThr2)) { AIXM.xy(lat: _1.to_f, long: _2.to_f) }
124
+ runway.back.displaced_threshold = given(rwy_node.(:LatDThr2), rwy_node.(:LongDThr2)) { AIXM.xy(lat: _1.to_f, long: _2.to_f) }
125
+ runway.back.z = given(rwy_node.(:AltFtDThr2)) { AIXM.z(_1.to_i, :qnh) }
126
+ runway.back.z ||= given(rwy_node.(:AltFtThr2)) { AIXM.z(_1.to_i, :qnh) }
127
+ if rwylgt_node = rwylgt_nodes[1]
128
+ runway.back.vasis = vasis_from(rwylgt_node)
129
+ given(approach_lighting_from(rwylgt_node)) { runway.back.add_approach_lighting(_1) }
130
+ LIGHTING_POSITIONS.each_key do |position|
131
+ given(lighting_from(rwylgt_node, position)) { runway.back.add_lighting(_1) }
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
137
+
138
+ def vasis_from(rwylgt_node)
139
+ if rwylgt_node.(:PapiVasis)
140
+ AIXM.vasis.tap do |vasis|
141
+ vasis.type = rwylgt_node.(:PapiVasis)
142
+ vasis.slope_angle = AIXM.a(rwylgt_node.(:PapiVasisPente).to_f)
143
+ vasis.meht = AIXM.z(rwylgt_node.(:MehtFt).to_i, :qfe)
144
+ end
145
+ end
146
+ end
147
+
148
+ def approach_lighting_from(rwylgt_node)
149
+ if rwylgt_node.(:LgtApchCat)
150
+ AIXM.approach_lighting(
151
+ type: APPROACH_LIGHTING_TYPES.fetch(rwylgt_node.(:LgtApchCat) , :other)
152
+ ).tap do |approach_lighting|
153
+ approach_lighting.length = AIXM.d(rwylgt_node.(:LgtApchLongueur).to_i, :m) if rwylgt_node.(:LgtApchLongueur)
154
+ approach_lighting.intensity = rwylgt_node.(:LgtApchIntensite)&.first_match(/LIH/, /LIM/, /LIL/, default: :other)
155
+ approach_lighting.remarks = {
156
+ 'type' => (rwylgt_node.(:LgtApchCat) if approach_lighting.type == :other),
157
+ 'intensité/intensity' => (rwylgt_node.(:LgtApchIntensite) if approach_lighting.intensity == :other)
158
+ }.to_remarks
159
+ end
160
+ end
161
+ end
162
+
163
+ def lighting_from(rwylgt_node, position)
164
+ prefix = "Lgt" + LIGHTING_POSITIONS.fetch(position)
165
+ if rwylgt_node.(:"#{prefix}Couleur") || rwylgt_node.(:"#{prefix}Longueur")
166
+ AIXM.lighting(position: position).tap do |lighting|
167
+ couleur, intensite = rwylgt_node.(:"#{prefix}Couleur"), rwylgt_node.(:"#{prefix}Intensite")
168
+ lighting.intensity = if intensite
169
+ intensite.first_match(/LIH/, /LIM/, /LIL/, default: :other)
170
+ elsif couleur
171
+ couleur.first_match(/LIH/, /LIM/, /LIL/).tap { couleur.remove!(/LIH|LIM|LIL/) }
172
+ end
173
+ lighting.color = if couleur
174
+ if couleur.match? /ICAO|EASA|OACI|AESA/
175
+ ICAO_LIGHTING_COLORS[position]
176
+ else
177
+ couleur.remove(/[^#{LIGHTING_COLORS.keys.join}]/).compact
178
+ LIGHTING_COLORS.fetch(couleur, :other)
179
+ end
180
+ end
181
+ lighting.description = {
182
+ 'couleur/color' => (rwylgt_node.(:"#{prefix}Couleur") if [nil, :other].include?(lighting.color)),
183
+ 'longueur/length' => rwylgt_node.(:"#{prefix}Longueur"),
184
+ 'espace/spacing' => rwylgt_node.(:"#{prefix}Espace")
185
+ }.to_remarks
186
+ lighting.remarks = rwylgt_node.(:LgtRem)
187
+ end
188
+ end
189
+ end
190
+
191
+ patch AIXM::Feature::Airport, :xy do |object, value|
192
+ throw(:abort) unless coordinate = AIPP.fixtures.aerodromes.dig(object.id, 'xy')
193
+ lat, long = coordinate.split(/\s+/)
194
+ AIXM.xy(lat: lat, long: long)
195
+ end
196
+
197
+ patch AIXM::Feature::Airport, :z do |object, value|
198
+ throw(:abort) unless value.nil?
199
+ throw(:abort, 'fixture missing') unless elevation = AIPP.fixtures.aerodromes.dig(object.id, 'z')
200
+ AIXM.z(elevation, :qnh)
201
+ end
202
+
203
+ patch AIXM::Component::Runway, :dimensions do |object, value|
204
+ throw(:abort) unless value.surface.zero?
205
+ throw(:abort, 'fixture missing') unless dimensions = AIPP.fixtures.aerodromes.dig(object.airport.id, object.name, 'dimensions')
206
+ length, width = dimensions.split(/\D+/)
207
+ length = length&.match?(/^\d+$/) ? AIXM.d(length.to_i, :m) : value.length
208
+ width = width&.match?(/^\d+$/) ? AIXM.d(width.to_i, :m) : value.width
209
+ AIXM.r(length, width).tap { |r| throw(:abort, 'fixture incomplete') if r.surface.zero? }
210
+ end
211
+
212
+ patch AIXM::Component::Runway::Direction, :xy do |object, value|
213
+ throw(:abort) unless value.nil?
214
+ throw(:abort, 'fixture missing') unless coordinate = AIPP.fixtures.aerodromes.dig(object.runway.airport.id, object.name.to_s(:runway), 'xy')
215
+ lat, long = coordinate.split(/\s+/)
216
+ AIXM.xy(lat: lat, long: long)
217
+ end
218
+
219
+ end
220
+ end
@@ -0,0 +1,53 @@
1
+ module AIPP::LF::AIP
2
+ class DPRAirspaces < AIPP::AIP::Parser
3
+
4
+ include AIPP::LF::Helpers::Base
5
+
6
+ # Map source types to type and optional local type
7
+ SOURCE_TYPES = {
8
+ 'D' => { type: 'D' },
9
+ 'P' => { type: 'P' },
10
+ 'R' => { type: 'R' },
11
+ 'ZIT' => { type: 'P', local_type: 'ZIT' }
12
+ }.freeze
13
+
14
+ # Radius to use for zones consisting of one point only
15
+ POINT_RADIUS = AIXM.d(1, :km).freeze
16
+
17
+ def parse
18
+ SOURCE_TYPES.each do |source_type, target|
19
+ verbose_info("processing #{source_type}")
20
+ AIPP.cache.espace.css(%Q(Espace[lk^="[LF][#{source_type} "])).each do |espace_node|
21
+ # UPSTREAM: Espace[pk=300343] has no Partie/Volume (reported)
22
+ next if espace_node['pk'] == '300343'
23
+ partie_node = AIPP.cache.partie.at_css(%Q(Partie:has(Espace[pk="#{espace_node['pk']}"])))
24
+ volume_node = AIPP.cache.volume.at_css(%Q(Volume:has(Partie[pk="#{partie_node['pk']}"])))
25
+ name = "#{AIPP.options.region}-#{source_type}#{espace_node.(:Nom)}".remove(/\s/)
26
+ add(
27
+ AIXM.airspace(
28
+ source: source(part: 'ENR', position: espace_node.line),
29
+ name: "#{name} #{partie_node.(:NomUsuel)}".strip,
30
+ type: target[:type],
31
+ local_type: target[:local_type]
32
+ ).tap do |airspace|
33
+ airspace.geometry = geometry_from(partie_node.(:Contour))
34
+ if airspace.geometry.point? # convert point to circle
35
+ airspace.geometry = AIXM.geometry(
36
+ AIXM.circle(
37
+ center_xy: airspace.geometry.segments.first.xy,
38
+ radius: POINT_RADIUS
39
+ )
40
+ )
41
+ end
42
+ fail("geometry is not closed") unless airspace.geometry.closed?
43
+ airspace.add_layer layer_from(volume_node)
44
+ airspace.layers.first.timetable = timetable_from(volume_node.(:HorCode))
45
+ airspace.layers.first.remarks = volume_node.(:Activite)
46
+ end
47
+ )
48
+ end
49
+ end
50
+ end
51
+
52
+ end
53
+ end
@@ -0,0 +1,48 @@
1
+ module AIPP::LF::AIP
2
+ class DangerousActivities < AIPP::AIP::Parser
3
+
4
+ include AIPP::LF::Helpers::Base
5
+
6
+ # Map raw activities to type of activity airspace
7
+ ACTIVITIES = {
8
+ 'AP' => { activity: :other, airspace: :dangerous_activities_area },
9
+ 'Aer' => { activity: :aeromodelling, airspace: :dangerous_activities_area },
10
+ 'Bal' => { activity: :balloon, airspace: :dangerous_activities_area },
11
+ 'Pje' => { activity: :parachuting, airspace: :dangerous_activities_area },
12
+ 'TrPVL' => { activity: :glider_winch, airspace: :dangerous_activities_area },
13
+ 'TrPla' => { activity: :glider_winch, airspace: :dangerous_activities_area },
14
+ 'TrVL' => { activity: :glider_winch, airspace: :dangerous_activities_area },
15
+ 'Vol' => { activity: :acrobatics, airspace: :dangerous_activities_area }
16
+ }.freeze
17
+
18
+ def parse
19
+ ACTIVITIES.each do |code, type|
20
+ verbose_info("processing #{code}")
21
+ AIPP.cache.espace.css(%Q(Espace[lk^="[LF][#{code} "])).each do |espace_node|
22
+ # HACK: Missing partie/volume as of AIRAC 2204 (reported)
23
+ next if espace_node['pk'] == '1360'
24
+ partie_node = AIPP.cache.partie.at_css(%Q(Partie:has(Espace[pk="#{espace_node['pk']}"])))
25
+ volume_node = AIPP.cache.volume.at_css(%Q(Volume:has(Partie[pk="#{partie_node['pk']}"])))
26
+ add(
27
+ AIXM.airspace(
28
+ source: source(part: 'ENR', position: espace_node.line),
29
+ id: espace_node.(:Nom),
30
+ type: type[:airspace],
31
+ local_type: code.upcase,
32
+ name: [espace_node.(:Nom), partie_node.(:NomUsuel)].join(' ')
33
+ ).tap do |airspace|
34
+ airspace.geometry = geometry_from partie_node.(:Contour)
35
+ layer_from(volume_node).then do |layer|
36
+ layer.activity = type[:activity]
37
+ airspace.add_layer layer
38
+ end
39
+ airspace.layers.first.timetable = timetable_from(volume_node.(:HorCode))
40
+ airspace.layers.first.remarks = volume_node.(:Remarque)
41
+ end
42
+ )
43
+ end
44
+ end
45
+ end
46
+
47
+ end
48
+ end
@@ -0,0 +1,44 @@
1
+ module AIPP::LF::AIP
2
+ class DesignatedPoints < AIPP::AIP::Parser
3
+
4
+ include AIPP::LF::Helpers::Base
5
+
6
+ depends_on :Aerodromes
7
+
8
+ SOURCE_TYPES = {
9
+ 'VFR' => :vfr_reporting_point,
10
+ 'WPT' => :icao
11
+ }.freeze
12
+
13
+ def parse
14
+ SOURCE_TYPES.each do |source_type, type|
15
+ verbose_info("processing #{source_type}")
16
+ AIPP.cache.navfix.css(%Q(NavFix[lk^="[LF][#{source_type} "])).each do |navfix_node|
17
+ ident = navfix_node.(:Ident)
18
+ add(
19
+ AIXM.designated_point(
20
+ source: source(part: 'ENR', position: navfix_node.line),
21
+ type: type,
22
+ id: ident.split('-').last.remove(/[^a-z\d]/i), # only use last segment of ID
23
+ name: ident,
24
+ xy: xy_from(navfix_node.(:Geometrie))
25
+ ).tap do |designated_point|
26
+ designated_point.remarks = navfix_node.(:Description)
27
+ if ident.match? /-/
28
+ airport = find_by(:airport, id: "LF#{ident.split('-').first}").first
29
+ designated_point.airport = airport
30
+ end
31
+ end
32
+ )
33
+ end
34
+ end
35
+ AIXM::Concerns::Memoize.method :to_uid do
36
+ aixm.features.find_by(:designated_point).duplicates.each do |duplicates|
37
+ duplicates.first.name += '/' + duplicates[1..].map(&:name).join('/')
38
+ aixm.remove_features(duplicates[1..])
39
+ end
40
+ end
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,119 @@
1
+ module AIPP::LF::AIP
2
+ class Helipads < AIPP::AIP::Parser
3
+
4
+ include AIPP::LF::Helpers::Base
5
+ include AIPP::LF::Helpers::UsageLimitation
6
+ include AIPP::LF::Helpers::Surface
7
+
8
+ depends_on :Aerodromes
9
+
10
+ HOSTILITIES = {
11
+ 'hostile habitée' => 'Zone hostile habitée / hostile populated area',
12
+ 'hostile non habitée' => 'Zone hostile non habitée / hostile unpopulated area',
13
+ 'non hostile' => 'Zone non hostile / non-hostile area'
14
+ }.freeze
15
+
16
+ ELEVATED = {
17
+ true => 'En terrasse / on deck',
18
+ false => 'En surface / on ground'
19
+ }.freeze
20
+
21
+ def parse
22
+ AIPP.cache.helistation.css(%Q(Helistation[lk^="[LF]"])).each do |helistation_node|
23
+ # Build airport if necessary
24
+ next unless limitation_type = LIMITATION_TYPES.fetch(helistation_node.(:Statut))
25
+ name = helistation_node.(:Nom)
26
+ airport = find_by(:airport, name: name).first || add(
27
+ AIXM.airport(
28
+ source: source(part: 'AD', position: helistation_node.line),
29
+ organisation: organisation_lf,
30
+ id: AIPP.options.region,
31
+ name: name,
32
+ xy: xy_from(helistation_node.(:Geometrie))
33
+ ).tap do |airport|
34
+ airport.z = AIXM.z(helistation_node.(:AltitudeFt).to_i, :qnh)
35
+ airport.add_usage_limitation(type: limitation_type.fetch(:limitation)) do |limitation|
36
+ limitation.remarks = limitation_type[:remarks]
37
+ [:private].each do |purpose| # TODO: check and simplify
38
+ limitation.add_condition do |condition|
39
+ condition.realm = limitation_type.fetch(:realm)
40
+ condition.origin = :any
41
+ condition.rule = case
42
+ when helistation_node.(:Ifr?) then :ifr_and_vfr
43
+ else :vfr
44
+ end
45
+ condition.purpose = purpose
46
+ end
47
+ end
48
+ end
49
+ end
50
+ )
51
+ # TODO: link to VAC once supported downstream
52
+ # # Link to VAC
53
+ # if helistation_node.(:Atlas?)
54
+ # vac = "VAC-#{airport.id}" if airport.id.match?(/^LF[A-Z]{2}$/)
55
+ # vac ||= "VACH-H#{airport.name[0, 3].upcase}"
56
+ # airport.remarks = [
57
+ # airport.remarks.to_s,
58
+ # link_to('VAC-HP', origin_for(vac).file)
59
+ # ].join("\n")
60
+ # end
61
+ # Add helipad and FATO
62
+ airport.add_helipad(
63
+ AIXM.helipad(
64
+ name: 'TLOF',
65
+ xy: xy_from(helistation_node.(:Geometrie))
66
+ ).tap do |helipad|
67
+ helipad.z = AIXM.z(helistation_node.(:AltitudeFt).to_i, :qnh)
68
+ helipad.dimensions = dimensions_from(helistation_node.(:DimTlof))
69
+ end.tap do |helipad|
70
+ airport.add_helipad(helipad)
71
+ helipad.performance_class = performance_class_from(helistation_node.(:ClassePerf))
72
+ helipad.surface = surface_from(helistation_node)
73
+ helipad.marking = helistation_node.(:Balisage) unless helistation_node.(:Balisage)&.match?(/^nil$/i)
74
+ helipad.add_lighting(AIXM.lighting(position: :other)) if helistation_node.(:Nuit?) || helistation_node.(:Balisage)&.match?(/feu/i)
75
+ helipad.remarks = {
76
+ 'position/positioning' => [
77
+ (HOSTILITIES.fetch(helistation_node.(:ZoneHabitee)) if helistation_node.(:ZoneHabitee)),
78
+ (ELEVATED.fetch(helistation_node.(:EnTerrasse?)) if helistation_node.(:EnTerrasse)),
79
+ ].compact.join("\n"),
80
+ 'hauteur/height' => given(helistation_node.(:HauteurFt)) { "#{_1} ft" },
81
+ 'exploitant/operator' => helistation_node.(:Exploitant)
82
+ }.to_remarks
83
+ if fato_dimensions = dimensions_from(helistation_node.(:DimFato))
84
+ AIXM.fato(name: 'FATO').tap do |fato|
85
+ fato.dimensions = fato_dimensions
86
+ airport.add_fato(fato)
87
+ helipad.fato = fato
88
+ end
89
+ end
90
+ end
91
+ )
92
+ end
93
+ end
94
+
95
+ private
96
+
97
+ def dimensions_from(content)
98
+ if content
99
+ dims = content.remove(/[^x\d.,]/i).split(/x/i).map { _1.to_ff.floor }
100
+ case dims.size
101
+ when 1
102
+ AIXM.r(AIXM.d(dims[0], :m))
103
+ when 2
104
+ AIXM.r(AIXM.d(dims[0], :m), AIXM.d(dims[1], :m))
105
+ when 4
106
+ AIXM.r(AIXM.d(dims.min, :m))
107
+ else
108
+ warn("ignoring dimensions `#{content}'", severe: false)
109
+ nil
110
+ end
111
+ end
112
+ end
113
+
114
+ def performance_class_from(content)
115
+ content.remove(/\d{2,}/).scan(/\d/).map(&:to_i).min&.to_s if content
116
+ end
117
+
118
+ end
119
+ end
@@ -0,0 +1,82 @@
1
+ module AIPP::LF::AIP
2
+ class NavigationalAids < AIPP::AIP::Parser
3
+
4
+ include AIPP::LF::Helpers::Base
5
+
6
+ SOURCE_TYPES = {
7
+ 'DME-ATT' => [:dme],
8
+ 'TACAN' => [:tacan],
9
+ 'VOR' => [:vor],
10
+ 'VOR-DME' => [:vor, :dme],
11
+ 'VORTAC' => [:vor, :tacan],
12
+ 'NDB' => [:ndb]
13
+ }.freeze
14
+
15
+ def parse
16
+ SOURCE_TYPES.each do |source_type, (primary_type, secondary_type)|
17
+ verbose_info("processing #{source_type}")
18
+ AIPP.cache.navfix.css(%Q(NavFix[lk^="[LF][#{source_type} "])).each do |navfix_node|
19
+ attributes = {
20
+ source: source(part: 'ENR', position: navfix_node.line),
21
+ organisation: organisation_lf,
22
+ id: navfix_node.(:Ident),
23
+ xy: xy_from(navfix_node.(:Geometrie))
24
+ }
25
+ if radionav_node = AIPP.cache.radionav.at_css(%Q(RadioNav:has(NavFix[pk="#{navfix_node.attr(:pk)}"])))
26
+ attributes.merge! send(primary_type, radionav_node)
27
+ add(
28
+ AIXM.send(primary_type, **attributes).tap do |navigational_aid|
29
+ navigational_aid.name = radionav_node.(:NomPhraseo) || radionav_node.(:Station)
30
+ navigational_aid.timetable = timetable_from(radionav_node.(:HorCode))
31
+ navigational_aid.remarks = {
32
+ "location/situation" => radionav_node.(:Situation),
33
+ "range/portée" => range_from(radionav_node)
34
+ }.to_remarks
35
+ navigational_aid.send("associate_#{secondary_type}") if secondary_type
36
+ end
37
+ )
38
+ else
39
+ verbose_info("skipping incomplete #{source_type} #{attributes[:id]}")
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def dme(radionav_node)
48
+ {
49
+ ghost_f: AIXM.f(radionav_node.(:Frequence).to_f, :mhz),
50
+ z: AIXM.z(radionav_node.(:AltitudeFt).to_i, :qnh)
51
+ }
52
+ end
53
+ alias_method :tacan, :dme
54
+
55
+ def vor(radionav_node)
56
+ {
57
+ type: :conventional,
58
+ north: :magnetic,
59
+ name: radionav_node.(:Station),
60
+ f: AIXM.f(radionav_node.(:Frequence).to_f, :mhz),
61
+ z: AIXM.z(radionav_node.(:AltitudeFt).to_i, :qnh),
62
+ }
63
+ end
64
+
65
+ def ndb(radionav_node)
66
+ {
67
+ type: :en_route,
68
+ f: AIXM.f(radionav_node.(:Frequence).to_f, :khz),
69
+ z: AIXM.z(radionav_node.(:AltitudeFt).to_i, :qnh)
70
+ }
71
+ end
72
+
73
+ def range_from(radionav_node)
74
+ [
75
+ radionav_node.(:Portee).blank_to_nil&.concat('NM'),
76
+ radionav_node.(:FlPorteeVert).blank_to_nil&.prepend('FL'),
77
+ radionav_node.(:Couverture).blank_to_nil
78
+ ].compact.join(' / ')
79
+ end
80
+
81
+ end
82
+ end