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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +2 -2
- data/CHANGELOG.md +17 -1
- data/README.md +269 -150
- data/exe/aip2aixm +2 -8
- data/exe/aip2ofmx +2 -8
- data/exe/notam2aixm +5 -0
- data/exe/notam2ofmx +5 -0
- data/lib/aipp/aip/README.md +10 -0
- data/lib/aipp/aip/executable.rb +40 -0
- data/lib/aipp/aip/parser.rb +9 -0
- data/lib/aipp/aip/runner.rb +85 -0
- data/lib/aipp/border.rb +2 -2
- data/lib/aipp/debugger.rb +14 -19
- data/lib/aipp/downloader/file.rb +57 -0
- data/lib/aipp/downloader/graphql.rb +29 -0
- data/lib/aipp/downloader/http.rb +48 -0
- data/lib/aipp/downloader.rb +78 -29
- data/lib/aipp/environment.rb +88 -0
- data/lib/aipp/executable.rb +36 -53
- data/lib/aipp/notam/README.md +25 -0
- data/lib/aipp/notam/executable.rb +27 -0
- data/lib/aipp/notam/parser.rb +9 -0
- data/lib/aipp/notam/runner.rb +28 -0
- data/lib/aipp/parser.rb +133 -160
- data/lib/aipp/patcher.rb +4 -5
- data/lib/aipp/regions/LF/README.md +6 -2
- data/lib/aipp/regions/LF/aip/aerodromes.rb +220 -0
- data/lib/aipp/regions/LF/aip/d_p_r_airspaces.rb +53 -0
- data/lib/aipp/regions/LF/aip/dangerous_activities.rb +48 -0
- data/lib/aipp/regions/LF/aip/designated_points.rb +44 -0
- data/lib/aipp/regions/LF/aip/helipads.rb +119 -0
- data/lib/aipp/regions/LF/aip/navigational_aids.rb +82 -0
- data/lib/aipp/regions/LF/aip/obstacles.rb +150 -0
- data/lib/aipp/regions/LF/aip/serviced_airspaces.rb +67 -0
- data/lib/aipp/regions/LF/aip/services.rb +169 -0
- data/lib/aipp/regions/LF/fixtures/aerodromes.yml +2 -2
- data/lib/aipp/regions/LF/helpers/base.rb +32 -32
- data/lib/aipp/regions/LS/README.md +59 -0
- data/lib/aipp/regions/LS/helpers/base.rb +111 -0
- data/lib/aipp/regions/LS/notam/ENR.rb +173 -0
- data/lib/aipp/runner.rb +152 -0
- data/lib/aipp/version.rb +1 -1
- data/lib/aipp.rb +30 -11
- data/lib/core_ext/array.rb +13 -0
- data/lib/core_ext/nokogiri.rb +56 -8
- data/lib/core_ext/string.rb +63 -1
- data.tar.gz.sig +0 -0
- metadata +115 -64
- metadata.gz.sig +0 -0
- data/lib/aipp/aip.rb +0 -166
- data/lib/aipp/regions/LF/aerodromes.rb +0 -223
- data/lib/aipp/regions/LF/d_p_r_airspaces.rb +0 -56
- data/lib/aipp/regions/LF/dangerous_activities.rb +0 -49
- data/lib/aipp/regions/LF/designated_points.rb +0 -47
- data/lib/aipp/regions/LF/helipads.rb +0 -122
- data/lib/aipp/regions/LF/navigational_aids.rb +0 -85
- data/lib/aipp/regions/LF/obstacles.rb +0 -153
- data/lib/aipp/regions/LF/serviced_airspaces.rb +0 -70
- 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
|