aipp 1.0.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|