aipp 0.2.4 → 1.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 -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
|