aipp 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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