aipp 0.2.4 → 1.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 -0
- data/CHANGELOG.md +38 -0
- data/README.md +222 -88
- data/exe/aip2aixm +2 -2
- data/exe/aip2ofmx +2 -2
- data/lib/aipp/aip.rb +113 -31
- data/lib/aipp/border.rb +77 -46
- data/lib/aipp/debugger.rb +101 -0
- data/lib/aipp/downloader.rb +39 -26
- data/lib/aipp/executable.rb +41 -22
- data/lib/aipp/parser.rb +94 -21
- data/lib/aipp/patcher.rb +5 -2
- data/lib/aipp/pdf.rb +1 -1
- data/lib/aipp/regions/LF/README.md +49 -0
- data/lib/aipp/regions/LF/aerodromes.rb +223 -0
- data/lib/aipp/regions/LF/d_p_r_airspaces.rb +56 -0
- data/lib/aipp/regions/LF/dangerous_activities.rb +49 -0
- data/lib/aipp/regions/LF/designated_points.rb +47 -0
- data/lib/aipp/regions/LF/fixtures/aerodromes.yml +608 -0
- data/lib/aipp/regions/LF/helipads.rb +122 -0
- data/lib/aipp/regions/LF/helpers/base.rb +218 -0
- data/lib/aipp/regions/LF/helpers/surface.rb +49 -0
- data/lib/aipp/regions/LF/helpers/usage_limitation.rb +20 -0
- data/lib/aipp/regions/LF/navigational_aids.rb +85 -0
- data/lib/aipp/regions/LF/obstacles.rb +153 -0
- data/lib/aipp/regions/LF/serviced_airspaces.rb +70 -0
- data/lib/aipp/regions/LF/services.rb +172 -0
- data/lib/aipp/t_hash.rb +4 -5
- data/lib/aipp/version.rb +1 -1
- data/lib/aipp.rb +11 -5
- data/lib/core_ext/enumerable.rb +9 -9
- data/lib/core_ext/hash.rb +21 -5
- data/lib/core_ext/nokogiri.rb +54 -0
- data/lib/core_ext/string.rb +38 -66
- data.tar.gz.sig +2 -0
- metadata +180 -188
- metadata.gz.sig +0 -0
- data/.gitignore +0 -8
- data/.ruby-version +0 -1
- data/.travis.yml +0 -8
- data/.yardopts +0 -3
- data/Guardfile +0 -7
- data/TODO.md +0 -6
- data/aipp.gemspec +0 -44
- data/gems.rb +0 -3
- data/lib/aipp/airac.rb +0 -55
- data/lib/aipp/regions/LF/AD-1.3.rb +0 -162
- data/lib/aipp/regions/LF/AD-1.6.rb +0 -31
- data/lib/aipp/regions/LF/AD-2.rb +0 -313
- data/lib/aipp/regions/LF/AD-3.1.rb +0 -185
- data/lib/aipp/regions/LF/ENR-2.1.rb +0 -92
- data/lib/aipp/regions/LF/ENR-4.1.rb +0 -97
- data/lib/aipp/regions/LF/ENR-4.3.rb +0 -28
- data/lib/aipp/regions/LF/ENR-5.1.rb +0 -75
- data/lib/aipp/regions/LF/ENR-5.5.rb +0 -53
- data/lib/aipp/regions/LF/fixtures/AD-1.3.yml +0 -511
- data/lib/aipp/regions/LF/fixtures/AD-2.yml +0 -185
- data/lib/aipp/regions/LF/fixtures/AD-3.1.yml +0 -10
- data/lib/aipp/regions/LF/helpers/AD_radio.rb +0 -90
- data/lib/aipp/regions/LF/helpers/URL.rb +0 -26
- data/lib/aipp/regions/LF/helpers/common.rb +0 -217
- data/lib/core_ext/object.rb +0 -43
- data/rakefile.rb +0 -12
- data/spec/fixtures/archive.zip +0 -0
- data/spec/fixtures/border.geojson +0 -201
- data/spec/fixtures/document.pdf +0 -0
- data/spec/fixtures/document.pdf.json +0 -1
- data/spec/fixtures/new.html +0 -6
- data/spec/fixtures/new.pdf +0 -0
- data/spec/fixtures/new.txt +0 -1
- data/spec/lib/aipp/airac_spec.rb +0 -98
- data/spec/lib/aipp/border_spec.rb +0 -135
- data/spec/lib/aipp/downloader_spec.rb +0 -81
- data/spec/lib/aipp/patcher_spec.rb +0 -46
- data/spec/lib/aipp/pdf_spec.rb +0 -124
- data/spec/lib/aipp/t_hash_spec.rb +0 -44
- data/spec/lib/aipp/version_spec.rb +0 -7
- data/spec/lib/core_ext/enumberable_spec.rb +0 -76
- data/spec/lib/core_ext/hash_spec.rb +0 -27
- data/spec/lib/core_ext/integer_spec.rb +0 -15
- data/spec/lib/core_ext/nil_class_spec.rb +0 -11
- data/spec/lib/core_ext/string_spec.rb +0 -112
- data/spec/sounds/failure.mp3 +0 -0
- data/spec/sounds/success.mp3 +0 -0
- data/spec/spec_helper.rb +0 -28
@@ -0,0 +1,122 @@
|
|
1
|
+
module AIPP
|
2
|
+
module LF
|
3
|
+
|
4
|
+
class Helipads < AIP
|
5
|
+
|
6
|
+
include AIPP::LF::Helpers::Base
|
7
|
+
include AIPP::LF::Helpers::UsageLimitation
|
8
|
+
include AIPP::LF::Helpers::Surface
|
9
|
+
|
10
|
+
DEPENDS = %w(aerodromes)
|
11
|
+
|
12
|
+
HOSTILITIES = {
|
13
|
+
'hostile habitée' => 'Zone hostile habitée / hostile populated area',
|
14
|
+
'hostile non habitée' => 'Zone hostile non habitée / hostile unpopulated area',
|
15
|
+
'non hostile' => 'Zone non hostile / non-hostile area'
|
16
|
+
}.freeze
|
17
|
+
|
18
|
+
ELEVATED = {
|
19
|
+
true => 'En terrasse / on deck',
|
20
|
+
false => 'En surface / on ground'
|
21
|
+
}.freeze
|
22
|
+
|
23
|
+
def parse
|
24
|
+
cache.helistation.css(%Q(Helistation[lk^="[LF]"])).each do |helistation_node|
|
25
|
+
# Build airport if necessary
|
26
|
+
next unless limitation_type = LIMITATION_TYPES.fetch(helistation_node.(:Statut))
|
27
|
+
name = helistation_node.(:Nom)
|
28
|
+
airport = find_by(:airport, name: name).first || add(
|
29
|
+
AIXM.airport(
|
30
|
+
source: source(section: 'AD', position: helistation_node.line),
|
31
|
+
organisation: organisation_lf,
|
32
|
+
id: options[:region],
|
33
|
+
name: name,
|
34
|
+
xy: xy_from(helistation_node.(:Geometrie))
|
35
|
+
).tap do |airport|
|
36
|
+
airport.z = AIXM.z(helistation_node.(:AltitudeFt).to_i, :qnh)
|
37
|
+
airport.add_usage_limitation(type: limitation_type.fetch(:limitation)) do |limitation|
|
38
|
+
limitation.remarks = limitation_type[:remarks]
|
39
|
+
[:private].each do |purpose| # TODO: check and simplify
|
40
|
+
limitation.add_condition do |condition|
|
41
|
+
condition.realm = limitation_type.fetch(:realm)
|
42
|
+
condition.origin = :any
|
43
|
+
condition.rule = case
|
44
|
+
when helistation_node.(:Ifr?) then :ifr_and_vfr
|
45
|
+
else :vfr
|
46
|
+
end
|
47
|
+
condition.purpose = purpose
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
)
|
53
|
+
# TODO: link to VAC once supported downstream
|
54
|
+
# # Link to VAC
|
55
|
+
# if helistation_node.(:Atlas?)
|
56
|
+
# vac = "VAC-#{airport.id}" if airport.id.match?(/^LF[A-Z]{2}$/)
|
57
|
+
# vac ||= "VACH-H#{airport.name[0, 3].upcase}"
|
58
|
+
# airport.remarks = [
|
59
|
+
# airport.remarks.to_s,
|
60
|
+
# link_to('VAC-HP', url_for(vac))
|
61
|
+
# ].join("\n")
|
62
|
+
# end
|
63
|
+
# Add helipad and FATO
|
64
|
+
airport.add_helipad(
|
65
|
+
AIXM.helipad(
|
66
|
+
name: 'TLOF',
|
67
|
+
xy: xy_from(helistation_node.(:Geometrie))
|
68
|
+
).tap do |helipad|
|
69
|
+
helipad.z = AIXM.z(helistation_node.(:AltitudeFt).to_i, :qnh)
|
70
|
+
helipad.dimensions = dimensions_from(helistation_node.(:DimTlof))
|
71
|
+
end.tap do |helipad|
|
72
|
+
airport.add_helipad(helipad)
|
73
|
+
helipad.performance_class = performance_class_from(helistation_node.(:ClassePerf))
|
74
|
+
helipad.surface = surface_from(helistation_node)
|
75
|
+
helipad.marking = helistation_node.(:Balisage) unless helistation_node.(:Balisage)&.match?(/^nil$/i)
|
76
|
+
helipad.add_lighting(AIXM.lighting(position: :other)) if helistation_node.(:Nuit?) || helistation_node.(:Balisage)&.match?(/feu/i)
|
77
|
+
helipad.remarks = {
|
78
|
+
'position/positioning' => [
|
79
|
+
(HOSTILITIES.fetch(helistation_node.(:ZoneHabitee)) if helistation_node.(:ZoneHabitee)),
|
80
|
+
(ELEVATED.fetch(helistation_node.(:EnTerrasse?)) if helistation_node.(:EnTerrasse)),
|
81
|
+
].compact.join("\n"),
|
82
|
+
'hauteur/height' => given(helistation_node.(:HauteurFt)) { "#{_1} ft" },
|
83
|
+
'exploitant/operator' => helistation_node.(:Exploitant)
|
84
|
+
}.to_remarks
|
85
|
+
if fato_dimensions = dimensions_from(helistation_node.(:DimFato))
|
86
|
+
AIXM.fato(name: 'FATO').tap do |fato|
|
87
|
+
fato.dimensions = fato_dimensions
|
88
|
+
airport.add_fato(fato)
|
89
|
+
helipad.fato = fato
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def dimensions_from(content)
|
100
|
+
if content
|
101
|
+
dims = content.remove(/[^x\d.,]/i).split(/x/i).map { _1.to_ff.floor }
|
102
|
+
case dims.size
|
103
|
+
when 1
|
104
|
+
AIXM.r(AIXM.d(dims[0], :m))
|
105
|
+
when 2
|
106
|
+
AIXM.r(AIXM.d(dims[0], :m), AIXM.d(dims[1], :m))
|
107
|
+
when 4
|
108
|
+
AIXM.r(AIXM.d(dims.min, :m))
|
109
|
+
else
|
110
|
+
warn("ignoring dimensions `#{content}'", severe: false)
|
111
|
+
nil
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def performance_class_from(content)
|
117
|
+
content.remove(/\d{2,}/).scan(/\d/).map(&:to_i).min&.to_s if content
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,218 @@
|
|
1
|
+
module AIPP
|
2
|
+
module LF
|
3
|
+
module Helpers
|
4
|
+
module Base
|
5
|
+
|
6
|
+
using AIXM::Refinements
|
7
|
+
|
8
|
+
# Supported version of the XML_SIA database dump
|
9
|
+
VERSION = '5'.freeze
|
10
|
+
|
11
|
+
# Mandatory Interface
|
12
|
+
|
13
|
+
def setup
|
14
|
+
AIXM.config.voice_channel_separation = :any
|
15
|
+
unless cache.espace
|
16
|
+
xml = read('XML_SIA')
|
17
|
+
%i(Ad Bordure Espace Frequence Helistation NavFix Obstacle Partie RadioNav Rwy RwyLgt Service Volume).each do |section|
|
18
|
+
cache[section.downcase] = xml.css("#{section}S")
|
19
|
+
end
|
20
|
+
warn("XML_SIA database dump version mismatch") unless xml.at_css('SiaExport').attr(:Version) == VERSION
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def url_for(aip_file)
|
25
|
+
sia_date = options[:airac].date.strftime('%d_%^b_%Y') # 04_JAN_2018
|
26
|
+
xml_date = options[:airac].date.xmlschema # 2018-01-04
|
27
|
+
sia_url = "https://www.sia.aviation-civile.gouv.fr/dvd/eAIP_#{sia_date}"
|
28
|
+
case aip_file
|
29
|
+
when /^Obstacles$/ # obstacles spreadsheet
|
30
|
+
"#{sia_url}/FRANCE/ObstaclesDataZone1MFRANCE_#{xml_date.remove('-')}.xlsx"
|
31
|
+
when /^VAC\-(\w+)/ # aerodrome VAC PDF
|
32
|
+
"#{sia_url}/Atlas-VAC/PDF_AIPparSSection/VAC/AD/AD-2.#{$1}.pdf"
|
33
|
+
when /^VACH\-(\w+)/ # helipad VAC PDF
|
34
|
+
"#{sia_url}/Atlas-VAC/PDF_AIPparSSection/VACH/AD/AD-3.#{$1}.pdf"
|
35
|
+
when /^[A-Z]+-/ # eAIP HTML page (e.g. ENR-5.5)
|
36
|
+
"#{sia_url}/FRANCE/AIRAC-#{xml_date}/html/eAIP/FR-#{aip_file}-fr-FR.html"
|
37
|
+
else # SIA XML database dump
|
38
|
+
"XML_SIA_#{xml_date}.xml"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Templates
|
43
|
+
|
44
|
+
def organisation_lf
|
45
|
+
unless cache.organisation_lf
|
46
|
+
cache.organisation_lf = AIXM.organisation(
|
47
|
+
source: source(position: 1, aip_file: "GEN-3.1"),
|
48
|
+
name: 'FRANCE',
|
49
|
+
type: 'S'
|
50
|
+
).tap do |organisation|
|
51
|
+
organisation.id = 'LF'
|
52
|
+
end
|
53
|
+
add cache.organisation_lf
|
54
|
+
end
|
55
|
+
cache.organisation_lf
|
56
|
+
end
|
57
|
+
|
58
|
+
# Parsersettes
|
59
|
+
|
60
|
+
# Build a source string
|
61
|
+
#
|
62
|
+
# @param position [Integer] line on which to find the information
|
63
|
+
# @param section [String] override autodetected section (e.g. "ENR")
|
64
|
+
# @param aip_file [String] override autodetected aip_file
|
65
|
+
# @return [String] source string
|
66
|
+
def source(position:, section: nil, aip_file: nil)
|
67
|
+
aip_file ||= 'XML_SIA'
|
68
|
+
section ||= aip_file.split(/-(?=\d)/).first
|
69
|
+
[
|
70
|
+
options[:region],
|
71
|
+
section,
|
72
|
+
aip_file,
|
73
|
+
options[:airac].date.xmlschema,
|
74
|
+
position
|
75
|
+
].join('|')
|
76
|
+
end
|
77
|
+
|
78
|
+
# Convert content to boolean
|
79
|
+
#
|
80
|
+
# @param content [String] either "oui" or "non"
|
81
|
+
# @return [Boolean]
|
82
|
+
def b_from(content)
|
83
|
+
case content
|
84
|
+
when 'oui' then true
|
85
|
+
when 'non' then false
|
86
|
+
else fail "`#{content}' is not boolean content"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Build coordinates from content
|
91
|
+
#
|
92
|
+
# @param content [String] source content
|
93
|
+
# @return [AIXM::XY]
|
94
|
+
def xy_from(content)
|
95
|
+
parts = content.split(/[\s,]+/)
|
96
|
+
AIXM.xy(lat: parts[0].to_f, long: parts[1].to_f)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Build altitude/elevation from value and unit
|
100
|
+
#
|
101
|
+
# @param value [String, Numeric, nil] numeric value
|
102
|
+
# @param unit [String] unit like "ft ASFC" or absolute like "SFC"
|
103
|
+
# @return [AIXM::Z]
|
104
|
+
def z_from(value: nil, unit: 'ft ASFC')
|
105
|
+
if value
|
106
|
+
case unit
|
107
|
+
when 'SFC' then AIXM::GROUND
|
108
|
+
when 'UNL' then AIXM::UNLIMITED
|
109
|
+
when 'ft ASFC' then AIXM.z(value.to_i, :qfe)
|
110
|
+
when 'ft AMSL' then AIXM.z(value.to_i, :qnh)
|
111
|
+
when 'FL' then AIXM.z(value.to_i, :qne)
|
112
|
+
else fail "z `#{[value, unit].join(' ')}' not recognized"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Build distance from content
|
118
|
+
#
|
119
|
+
# @param content [String] source content
|
120
|
+
# @return [AIXM::D]
|
121
|
+
def d_from(content)
|
122
|
+
parts = content.split(/\s/)
|
123
|
+
AIXM.d(parts[0].to_f, parts[1])
|
124
|
+
end
|
125
|
+
|
126
|
+
# Build geometry from content
|
127
|
+
#
|
128
|
+
# @param content [String] source content
|
129
|
+
# @return [AIXM::Component::Geometry]
|
130
|
+
def geometry_from(content)
|
131
|
+
AIXM.geometry.tap do |geometry|
|
132
|
+
buffer = {}
|
133
|
+
content.split("\n").each do |element|
|
134
|
+
parts = element.split(',', 3).last.split(/[():,]/)
|
135
|
+
# Write explicit geometry from previous iteration
|
136
|
+
if (bordure_name, xy = buffer.delete(:fnt))
|
137
|
+
border = borders[bordure_name]
|
138
|
+
geometry.add_segments border.segment(
|
139
|
+
from_position: border.nearest(xy: xy),
|
140
|
+
to_position: border.nearest(xy: xy_from(parts[0]))
|
141
|
+
).map(&:to_point)
|
142
|
+
end
|
143
|
+
# Write current iteration
|
144
|
+
geometry.add_segment(
|
145
|
+
case parts[1]
|
146
|
+
when 'grc'
|
147
|
+
AIXM.point(
|
148
|
+
xy: xy_from(parts[0])
|
149
|
+
)
|
150
|
+
when 'rhl'
|
151
|
+
AIXM.rhumb_line(
|
152
|
+
xy: xy_from(parts[0])
|
153
|
+
)
|
154
|
+
when 'cwa', 'cca'
|
155
|
+
AIXM.arc(
|
156
|
+
xy: xy_from(parts[0]),
|
157
|
+
center_xy: xy_from(parts[5]),
|
158
|
+
clockwise: (parts[1] == 'cwa')
|
159
|
+
)
|
160
|
+
when 'cir'
|
161
|
+
AIXM.circle(
|
162
|
+
center_xy: xy_from(parts[0]),
|
163
|
+
radius: d_from(parts[3..4].join(' '))
|
164
|
+
)
|
165
|
+
when 'fnt'
|
166
|
+
bordure = cache.bordure.at_css(%Q(Bordure[pk="#{parts[3]}"]))
|
167
|
+
bordure_name = bordure.(:Code)
|
168
|
+
if bordure_name.match? /:/ # explicit geometry
|
169
|
+
borders[bordure_name] ||= AIPP::Border.from_array([bordure.(:Geometrie).split])
|
170
|
+
buffer[:fnt] = [bordure_name, xy_from(parts[2])]
|
171
|
+
AIXM.point(
|
172
|
+
xy: xy_from(parts[0])
|
173
|
+
)
|
174
|
+
else
|
175
|
+
AIXM.border( # named border
|
176
|
+
xy: xy_from(parts[0]),
|
177
|
+
name: bordure_name
|
178
|
+
)
|
179
|
+
end
|
180
|
+
else
|
181
|
+
fail "geometry `#{parts[1]}' not recognized"
|
182
|
+
end
|
183
|
+
)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# Build timetable from content
|
189
|
+
#
|
190
|
+
# @param content [String] source content
|
191
|
+
# @return [AIXM::Component::Timetable]
|
192
|
+
def timetable_from(content)
|
193
|
+
AIXM.timetable(code: content) if AIXM::H_RE.match? content
|
194
|
+
end
|
195
|
+
|
196
|
+
# Build layer from "volume" node
|
197
|
+
#
|
198
|
+
# @param volume_node [Nokogiri::XML::Element] source node
|
199
|
+
# @return [AIXM::Component::Layer]
|
200
|
+
def layer_from(volume_node)
|
201
|
+
AIXM.layer(
|
202
|
+
class: volume_node.(:Classe),
|
203
|
+
vertical_limit: AIXM.vertical_limit(
|
204
|
+
upper_z: z_from(value: volume_node.(:Plafond), unit: volume_node.(:PlafondRefUnite)),
|
205
|
+
max_z: z_from(value: volume_node.(:Plafond2)),
|
206
|
+
lower_z: z_from(value: volume_node.(:Plancher), unit: volume_node.(:PlancherRefUnite)),
|
207
|
+
min_z: z_from(value: volume_node.(:Plancher2))
|
208
|
+
)
|
209
|
+
).tap do |layer|
|
210
|
+
layer.timetable = timetable_from(volume_node.(:HorCode))
|
211
|
+
layer.remarks = volume_node.(:Remarque)
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module AIPP
|
2
|
+
module LF
|
3
|
+
module Helpers
|
4
|
+
module Surface
|
5
|
+
|
6
|
+
# Map surface to OFMX composition, preparation and remarks
|
7
|
+
SURFACES = {
|
8
|
+
/^revêtue?$/ => { preparation: :paved },
|
9
|
+
/^non revêtue?$/ => { preparation: :natural },
|
10
|
+
'macadam' => { composition: :macadam },
|
11
|
+
/^bitume ?(traité|psp)?$/ => { composition: :bitumen },
|
12
|
+
'ciment' => { composition: :concrete, preparation: :paved },
|
13
|
+
/^b[eéè]ton ?(armé|bitume|bitumeux|bitumineux)?$/ => { composition: :concrete, preparation: :paved },
|
14
|
+
/^béton( de)? ciment$/ => { composition: :concrete, preparation: :paved },
|
15
|
+
'béton herbe' => { composition: :concrete_and_grass },
|
16
|
+
'béton avec résine' => { composition: :concrete, preparation: :paved, remarks: 'Avec résine / with resin' },
|
17
|
+
"béton + asphalte d'étanchéité sablé" => { composition: :concrete_and_asphalt, preparation: :paved, remarks: 'Étanchéité sablé / sandblasted waterproofing' },
|
18
|
+
'béton armé + support bitumastic' => { composition: :concrete, preparation: :paved, remarks: 'Support bitumastic / bitumen support' },
|
19
|
+
/résine (époxy )?su[er] béton/ => { composition: :concrete, preparation: :paved, remarks: 'Avec couche résine / with resin seal coat' },
|
20
|
+
/^(asphalte|tarmac)$/ => { composition: :asphalt, preparation: :paved },
|
21
|
+
'enrobé' => { preparation: :other, remarks: 'Enrobé / coated' },
|
22
|
+
'enrobé anti-kérozène' => { preparation: :other, remarks: 'Enrobé anti-kérozène / anti-kerosene coating' },
|
23
|
+
/^enrobé bitum(e|iné|ineux)$/ => { composition: :bitumen, preparation: :paved, remarks: 'Enrobé / coated' },
|
24
|
+
'enrobé béton' => { composition: :concrete, preparation: :paved, remarks: 'Enrobé / coated' },
|
25
|
+
/^résine( époxy)?$/ => { composition: :other, remarks: 'Résine / resin' },
|
26
|
+
'tole acier larmé' => { composition: :metal, preparation: :grooved },
|
27
|
+
/^(structure métallique|structure et caillebotis métallique|aluminium)$/ => { composition: :metal },
|
28
|
+
'matériaux composites ignifugés' => { composition: :other, remarks: 'Matériaux composites ignifugés / fire resistant mixed materials' },
|
29
|
+
/^(gazon|herbe)$/ => { composition: :grass },
|
30
|
+
'neige' => { composition: :snow },
|
31
|
+
'neige damée' => { composition: :snow, preparation: :rolled },
|
32
|
+
'surface en bois' => { composition: :wood }
|
33
|
+
}.freeze
|
34
|
+
|
35
|
+
def surface_from(node)
|
36
|
+
AIXM.surface.tap do |surface|
|
37
|
+
SURFACES.metch(node.(:Revetement), default: {}).tap do |surface_attributes|
|
38
|
+
surface.composition = surface_attributes[:composition]
|
39
|
+
surface.preparation = surface_attributes[:preparation]
|
40
|
+
surface.remarks = surface_attributes[:remarks]
|
41
|
+
end
|
42
|
+
surface.pcn = node.(:Resistance)&.first_match(AIXM::PCN_RE)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module AIPP
|
2
|
+
module LF
|
3
|
+
module Helpers
|
4
|
+
module UsageLimitation
|
5
|
+
|
6
|
+
# Map limitation type descriptions to AIXM limitation, realm and remarks
|
7
|
+
LIMITATION_TYPES = {
|
8
|
+
'OFF' => nil, # skip decommissioned aerodromes/helistations
|
9
|
+
'CAP' => { limitation: :permitted, realm: :civilian },
|
10
|
+
'ADM' => { limitation: :permitted, realm: :other, remarks: "Goverment ACFT only / Réservé aux ACFT de l'État" },
|
11
|
+
'MIL' => { limitation: :permitted, realm: :military },
|
12
|
+
'PRV' => { limitation: :reservation_required, realm: :civilian },
|
13
|
+
'RST' => { limitation: :reservation_required, realm: :civilian },
|
14
|
+
'TPD' => { limitation: :reservation_required, realm: :civilian }
|
15
|
+
}.freeze
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module AIPP
|
2
|
+
module LF
|
3
|
+
|
4
|
+
class NavigationalAids < AIP
|
5
|
+
|
6
|
+
include AIPP::LF::Helpers::Base
|
7
|
+
|
8
|
+
SOURCE_TYPES = {
|
9
|
+
'DME-ATT' => [:dme],
|
10
|
+
'TACAN' => [:tacan],
|
11
|
+
'VOR' => [:vor],
|
12
|
+
'VOR-DME' => [:vor, :dme],
|
13
|
+
'VORTAC' => [:vor, :tacan],
|
14
|
+
'NDB' => [:ndb]
|
15
|
+
}.freeze
|
16
|
+
|
17
|
+
def parse
|
18
|
+
SOURCE_TYPES.each do |source_type, (primary_type, secondary_type)|
|
19
|
+
verbose_info("processing #{source_type}")
|
20
|
+
cache.navfix.css(%Q(NavFix[lk^="[LF][#{source_type} "])).each do |navfix_node|
|
21
|
+
attributes = {
|
22
|
+
source: source(section: 'ENR', position: navfix_node.line),
|
23
|
+
organisation: organisation_lf,
|
24
|
+
id: navfix_node.(:Ident),
|
25
|
+
xy: xy_from(navfix_node.(:Geometrie))
|
26
|
+
}
|
27
|
+
if radionav_node = cache.radionav.at_css(%Q(RadioNav:has(NavFix[pk="#{navfix_node.attr(:pk)}"])))
|
28
|
+
attributes.merge! send(primary_type, radionav_node)
|
29
|
+
add(
|
30
|
+
AIXM.send(primary_type, **attributes).tap do |navigational_aid|
|
31
|
+
navigational_aid.name = radionav_node.(:NomPhraseo) || radionav_node.(:Station)
|
32
|
+
navigational_aid.timetable = timetable_from(radionav_node.(:HorCode))
|
33
|
+
navigational_aid.remarks = {
|
34
|
+
"location/situation" => radionav_node.(:Situation),
|
35
|
+
"range/portée" => range_from(radionav_node)
|
36
|
+
}.to_remarks
|
37
|
+
navigational_aid.send("associate_#{secondary_type}") if secondary_type
|
38
|
+
end
|
39
|
+
)
|
40
|
+
else
|
41
|
+
verbose_info("skipping incomplete #{source_type} #{attributes[:id]}")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def dme(radionav_node)
|
50
|
+
{
|
51
|
+
ghost_f: AIXM.f(radionav_node.(:Frequence).to_f, :mhz),
|
52
|
+
z: AIXM.z(radionav_node.(:AltitudeFt).to_i, :qnh)
|
53
|
+
}
|
54
|
+
end
|
55
|
+
alias_method :tacan, :dme
|
56
|
+
|
57
|
+
def vor(radionav_node)
|
58
|
+
{
|
59
|
+
type: :conventional,
|
60
|
+
north: :magnetic,
|
61
|
+
name: radionav_node.(:Station),
|
62
|
+
f: AIXM.f(radionav_node.(:Frequence).to_f, :mhz),
|
63
|
+
z: AIXM.z(radionav_node.(:AltitudeFt).to_i, :qnh),
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
def ndb(radionav_node)
|
68
|
+
{
|
69
|
+
type: :en_route,
|
70
|
+
f: AIXM.f(radionav_node.(:Frequence).to_f, :khz),
|
71
|
+
z: AIXM.z(radionav_node.(:AltitudeFt).to_i, :qnh)
|
72
|
+
}
|
73
|
+
end
|
74
|
+
|
75
|
+
def range_from(radionav_node)
|
76
|
+
[
|
77
|
+
radionav_node.(:Portee).blank_to_nil&.concat('NM'),
|
78
|
+
radionav_node.(:FlPorteeVert).blank_to_nil&.prepend('FL'),
|
79
|
+
radionav_node.(:Couverture).blank_to_nil
|
80
|
+
].compact.join(' / ')
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
module AIPP
|
2
|
+
module LF
|
3
|
+
|
4
|
+
class Obstacles < AIP
|
5
|
+
|
6
|
+
include AIPP::LF::Helpers::Base
|
7
|
+
|
8
|
+
# Map type descriptions to AIXM types and remarks
|
9
|
+
TYPES = {
|
10
|
+
'Antenne' => [:antenna],
|
11
|
+
'Autre' => [:other],
|
12
|
+
'Bâtiment' => [:building],
|
13
|
+
'Câble' => [:other, 'Cable / Câble'],
|
14
|
+
'Centrale thermique' => [:building, 'Thermal power plant / Centrale thermique'],
|
15
|
+
"Château d'eau" => [:tower, "Water tower / Château d'eau"],
|
16
|
+
'Cheminée' => [:chimney],
|
17
|
+
'Derrick' => [:tower, 'Derrick'],
|
18
|
+
'Eglise' => [:tower, 'Church / Eglise'],
|
19
|
+
'Eolienne' => [:wind_turbine],
|
20
|
+
'Eolienne(s)' => [:wind_turbine],
|
21
|
+
'Grue' => [:tower, 'Crane / Grue'],
|
22
|
+
'Mât' => [:mast],
|
23
|
+
'Phare marin' => [:tower, 'Lighthouse / Phare marin'],
|
24
|
+
'Pile de pont' => [:other, 'Bridge piers / Pile de pont'],
|
25
|
+
'Portique' => [:building, 'Arch / Portique'],
|
26
|
+
'Pylône' => [:mast, 'Pylon / Pylône'],
|
27
|
+
'Silo' => [:tower, 'Silo'],
|
28
|
+
'Terril' => [:other, 'Spoil heap / Teril'],
|
29
|
+
'Torchère' => [:chimney, 'Flare / Torchère'],
|
30
|
+
'Tour' => [:tower],
|
31
|
+
'Treillis métallique' => [:other, 'Metallic grid / Treillis métallique']
|
32
|
+
}.freeze
|
33
|
+
|
34
|
+
def parse
|
35
|
+
if options[:region_options].include? 'lf_obstacles_xlsx'
|
36
|
+
info("reading obstacles from XLSX")
|
37
|
+
@xlsx = read('Obstacles')
|
38
|
+
parse_from_xlsx
|
39
|
+
else
|
40
|
+
parse_from_xml
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def parse_from_xlsx
|
47
|
+
# Build obstacles
|
48
|
+
@xlsx.sheet(@xlsx.sheets.find(/^data/i).first).each(
|
49
|
+
name: 'IDENTIFICATEUR',
|
50
|
+
type: 'TYPE',
|
51
|
+
count: 'NOMBRE',
|
52
|
+
longitude: 'LONGITUDE DECIMALE',
|
53
|
+
latitude: 'LATITUDE DECIMALE',
|
54
|
+
elevation: 'ALTITUDE AU SOMMET',
|
55
|
+
height: 'HAUTEUR HORS SOL',
|
56
|
+
height_unit: 'UNITE',
|
57
|
+
horizontal_accuracy: 'PRECISION HORIZONTALE',
|
58
|
+
vertical_accuracy: 'PRECISION VERTICALE',
|
59
|
+
visibility: 'BALISAGE',
|
60
|
+
remarks: 'REMARK',
|
61
|
+
effective_on: 'DATE DE MISE EN VIGUEUR'
|
62
|
+
).with_index(0) do |row, index|
|
63
|
+
next unless row[:effective_on].to_s.match? /\d{8}/
|
64
|
+
type, type_remarks = TYPES.fetch(row[:type])
|
65
|
+
count = row[:count].to_i
|
66
|
+
obstacle = AIXM.obstacle(
|
67
|
+
source: source(section: 'ENR', position: index),
|
68
|
+
name: row[:name],
|
69
|
+
type: type,
|
70
|
+
xy: AIXM.xy(lat: row[:latitude].to_f, long: row[:longitude].to_f),
|
71
|
+
z: AIXM.z(row[:elevation].to_i, :qnh)
|
72
|
+
).tap do |obstacle|
|
73
|
+
obstacle.height = AIXM.d(row[:height].to_i, row[:height_unit])
|
74
|
+
if row[:horizontal_accuracy]
|
75
|
+
accuracy = row[:horizontal_accuracy].split
|
76
|
+
obstacle.xy_accuracy = AIXM.d(accuracy.first.to_i, accuracy.last)
|
77
|
+
end
|
78
|
+
if row[:vertical_accuracy]
|
79
|
+
accuracy = row[:horizontal_accuracy].split
|
80
|
+
obstacle.z_accuracy = AIXM.d(accuracy.first.to_i, accuracy.last)
|
81
|
+
end
|
82
|
+
obstacle.marking = row[:visibility].match?(/jour/i)
|
83
|
+
obstacle.lighting = row[:visibility].match?(/nuit/i)
|
84
|
+
obstacle.remarks = {
|
85
|
+
'type' => type_remarks,
|
86
|
+
'number/nombre' => (count if count > 1),
|
87
|
+
'details' => row[:remarks],
|
88
|
+
'effective/mise en vigueur' => (row[:effective_on].to_s.unpack("a4a2a2").join("-") if row[:updated_on])
|
89
|
+
}.to_remarks
|
90
|
+
# Group obstacles
|
91
|
+
if aixm.features.find_by(:obstacle, xy: obstacle.xy).any?
|
92
|
+
warn("duplicate obstacle #{obstacle.name}", severe: false)
|
93
|
+
else
|
94
|
+
if count > 1
|
95
|
+
obstacle_group = AIXM.obstacle_group(
|
96
|
+
source: obstacle.source,
|
97
|
+
name: obstacle.name
|
98
|
+
).tap do |obstacle_group|
|
99
|
+
obstacle_group.remarks = "#{count} obstacles"
|
100
|
+
end
|
101
|
+
obstacle_group.add_obstacle obstacle
|
102
|
+
add obstacle_group
|
103
|
+
else
|
104
|
+
add obstacle
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def parse_from_xml
|
112
|
+
cache.obstacle.css(%Q(Obstacle[lk^="[LF]"])).each do |node|
|
113
|
+
# Build obstacles
|
114
|
+
type, type_remarks = TYPES.fetch(node.(:TypeObst))
|
115
|
+
count = node.(:Combien).to_i
|
116
|
+
obstacle = AIXM.obstacle(
|
117
|
+
source: source(section: 'ENR', position: node.line),
|
118
|
+
name: node.(:NumeroNom),
|
119
|
+
type: type,
|
120
|
+
xy: xy_from(node.(:Geometrie)),
|
121
|
+
z: AIXM.z(node.(:AmslFt).to_i, :qnh)
|
122
|
+
).tap do |obstacle|
|
123
|
+
obstacle.height = AIXM.d(node.(:AglFt).to_i, :ft)
|
124
|
+
obstacle.marking = node.(:Balisage).match?(/jour/i)
|
125
|
+
obstacle.lighting = node.(:Balisage).match?(/nuit/i)
|
126
|
+
obstacle.remarks = {
|
127
|
+
'type' => type_remarks,
|
128
|
+
'number/nombre' => (count if count > 1)
|
129
|
+
}.to_remarks
|
130
|
+
end
|
131
|
+
# Group obstacles
|
132
|
+
if aixm.features.find_by(:obstacle, xy: obstacle.xy).any?
|
133
|
+
warn("duplicate obstacle #{obstacle.name}", severe: false)
|
134
|
+
else
|
135
|
+
if count > 1
|
136
|
+
obstacle_group = AIXM.obstacle_group(
|
137
|
+
source: obstacle.source,
|
138
|
+
name: obstacle.name
|
139
|
+
).tap do |obstacle_group|
|
140
|
+
obstacle_group.remarks = "#{count} obstacles"
|
141
|
+
end
|
142
|
+
obstacle_group.add_obstacle obstacle
|
143
|
+
add obstacle_group
|
144
|
+
else
|
145
|
+
add obstacle
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|