aipp 0.2.6 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +21 -0
  4. data/README.md +147 -91
  5. data/exe/aip2aixm +2 -2
  6. data/exe/aip2ofmx +2 -2
  7. data/lib/aipp/aip.rb +96 -11
  8. data/lib/aipp/border.rb +77 -46
  9. data/lib/aipp/debugger.rb +101 -0
  10. data/lib/aipp/downloader.rb +18 -5
  11. data/lib/aipp/executable.rb +33 -20
  12. data/lib/aipp/parser.rb +42 -37
  13. data/lib/aipp/patcher.rb +5 -2
  14. data/lib/aipp/regions/LF/README.md +49 -0
  15. data/lib/aipp/regions/LF/aerodromes.rb +223 -0
  16. data/lib/aipp/regions/LF/d_p_r_airspaces.rb +56 -0
  17. data/lib/aipp/regions/LF/dangerous_activities.rb +49 -0
  18. data/lib/aipp/regions/LF/designated_points.rb +47 -0
  19. data/lib/aipp/regions/LF/fixtures/aerodromes.yml +608 -0
  20. data/lib/aipp/regions/LF/helipads.rb +122 -0
  21. data/lib/aipp/regions/LF/helpers/base.rb +167 -174
  22. data/lib/aipp/regions/LF/helpers/surface.rb +49 -0
  23. data/lib/aipp/regions/LF/helpers/usage_limitation.rb +20 -0
  24. data/lib/aipp/regions/LF/navigational_aids.rb +85 -0
  25. data/lib/aipp/regions/LF/obstacles.rb +153 -0
  26. data/lib/aipp/regions/LF/serviced_airspaces.rb +70 -0
  27. data/lib/aipp/regions/LF/services.rb +172 -0
  28. data/lib/aipp/t_hash.rb +3 -4
  29. data/lib/aipp/version.rb +1 -1
  30. data/lib/aipp.rb +7 -5
  31. data/lib/core_ext/enumerable.rb +2 -2
  32. data/lib/core_ext/hash.rb +21 -5
  33. data/lib/core_ext/nokogiri.rb +54 -0
  34. data/lib/core_ext/string.rb +32 -65
  35. data.tar.gz.sig +0 -0
  36. metadata +70 -81
  37. metadata.gz.sig +0 -0
  38. data/lib/aipp/airac.rb +0 -55
  39. data/lib/aipp/regions/LF/AD-1.3.rb +0 -177
  40. data/lib/aipp/regions/LF/AD-1.6.rb +0 -33
  41. data/lib/aipp/regions/LF/AD-2.rb +0 -344
  42. data/lib/aipp/regions/LF/AD-3.1.rb +0 -185
  43. data/lib/aipp/regions/LF/ENR-2.1.rb +0 -167
  44. data/lib/aipp/regions/LF/ENR-4.1.rb +0 -41
  45. data/lib/aipp/regions/LF/ENR-4.3.rb +0 -27
  46. data/lib/aipp/regions/LF/ENR-5.1.rb +0 -106
  47. data/lib/aipp/regions/LF/ENR-5.4.rb +0 -90
  48. data/lib/aipp/regions/LF/ENR-5.5.rb +0 -55
  49. data/lib/aipp/regions/LF/fixtures/AD-1.3.yml +0 -511
  50. data/lib/aipp/regions/LF/fixtures/AD-2.yml +0 -185
  51. data/lib/aipp/regions/LF/fixtures/AD-3.1.yml +0 -10
  52. data/lib/aipp/regions/LF/helpers/URL.rb +0 -26
  53. data/lib/aipp/regions/LF/helpers/navigational_aid.rb +0 -104
  54. data/lib/aipp/regions/LF/helpers/radio_AD.rb +0 -110
  55. data/lib/core_ext/object.rb +0 -43
@@ -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
@@ -5,217 +5,210 @@ module AIPP
5
5
 
6
6
  using AIXM::Refinements
7
7
 
8
- # Map border names to OFMX
9
- BORDERS = {
10
- 'franco-allemande' => 'FRANCE_GERMANY',
11
- 'franco-espagnole' => 'FRANCE_SPAIN',
12
- 'franco-italienne' => 'FRANCE_ITALY',
13
- 'franco-suisse' => 'FRANCE_SWITZERLAND',
14
- 'franco-luxembourgeoise' => 'FRANCE_LUXEMBOURG',
15
- 'franco-belge' => 'BELGIUM_FRANCE',
16
- 'germano-suisse' => 'GERMANY_SWITZERLAND',
17
- 'hispano-andorrane' => 'ANDORRA_SPAIN',
18
- 'la côte atlantique française' => 'FRANCE_ATLANTIC_COAST',
19
- 'côte méditérrannéenne' => 'FRANCE_MEDITERRANEAN_COAST',
20
- 'limite des eaux territoriales atlantique françaises' => 'FRANCE_ATLANTIC_TERRITORIAL_SEA',
21
- 'parc national des écrins' => 'FRANCE_ECRINS_NATIONAL_PARK'
22
- }.freeze
23
-
24
- # Intersection points between three countries
25
- INTERSECTIONS = {
26
- 'FRANCE_SPAIN|ANDORRA_SPAIN' => AIXM.xy(lat: 42.502720, long: 1.725965),
27
- 'ANDORRA_SPAIN|FRANCE_SPAIN' => AIXM.xy(lat: 42.603571, long: 1.442681),
28
- 'FRANCE_SWITZERLAND|FRANCE_ITALY' => AIXM.xy(lat: 45.922701, long: 7.044125),
29
- 'BELGIUM_FRANCE|FRANCE_LUXEMBOURG' => AIXM.xy(lat: 49.546428, long: 5.818415),
30
- 'FRANCE_LUXEMBOURG|FRANCE_GERMANY' => AIXM.xy(lat: 49.469438, long: 6.367516),
31
- 'FRANCE_GERMANY|FRANCE_SWITZERLAND' => AIXM.xy(lat: 47.589831, long: 7.589049),
32
- 'GERMANY_SWITZERLAND|FRANCE_GERMANY' => AIXM.xy(lat: 47.589831, long: 7.589049)
33
- }.freeze
34
-
35
- # Map surface to OFMX composition, preparation and remarks
36
- SURFACES = {
37
- /^revêtue?$/ => { preparation: :paved },
38
- /^non revêtue?$/ => { preparation: :natural },
39
- 'macadam' => { composition: :macadam },
40
- /^bitume ?(traité|psp)?$/ => { composition: :bitumen },
41
- 'ciment' => { composition: :concrete, preparation: :paved },
42
- /^b[eéè]ton ?(armé|bitume|bitumineux)?$/ => { composition: :concrete, preparation: :paved },
43
- /^béton( de)? ciment$/ => { composition: :concrete, preparation: :paved },
44
- 'béton herbe' => { composition: :concrete_and_grass },
45
- 'béton avec résine' => { composition: :concrete, preparation: :paved, remarks: 'Avec résine / with resin' },
46
- "béton + asphalte d'étanchéité sablé" => { composition: :concrete_and_asphalt, preparation: :paved, remarks: 'Étanchéité sablé / sandblasted waterproofing' },
47
- 'béton armé + support bitumastic' => { composition: :concrete, preparation: :paved, remarks: 'Support bitumastic / bitumen support' },
48
- /résine (époxy )?su[er] béton/ => { composition: :concrete, preparation: :paved, remarks: 'Avec couche résine / with resin seal coat' },
49
- /^(asphalte|tarmac)$/ => { composition: :asphalt, preparation: :paved },
50
- 'enrobé' => { preparation: :other, remarks: 'Enrobé / coated' },
51
- 'enrobé anti-kérozène' => { preparation: :other, remarks: 'Enrobé anti-kérozène / anti-kerosene coating' },
52
- /^enrobé bitum(e|iné|ineux)$/ => { composition: :bitumen, preparation: :paved, remarks: 'Enrobé / coated' },
53
- 'enrobé béton' => { composition: :concrete, preparation: :paved, remarks: 'Enrobé / coated' },
54
- /^résine( époxy)?$/ => { composition: :other, remarks: 'Résine / resin' },
55
- 'tole acier larmé' => { composition: :metal, preparation: :grooved },
56
- /^(structure métallique|aluminium)$/ => { composition: :metal },
57
- 'matériaux composites ignifugés' => { composition: :other, remarks: 'Matériaux composites ignifugés / fire resistant mixed materials' },
58
- /^(gazon|herbe)$/ => { composition: :grass },
59
- 'neige' => { composition: :snow },
60
- 'neige damée' => { composition: :snow, preparation: :rolled }
61
- }.freeze
62
-
63
- # Transform French text fragments to English
64
- ANGLICISE_MAP = {
65
- /[^A-Z0-9 .\-]/ => '',
66
- /0(\d)/ => '\1',
67
- /(\d)-(\d)/ => '\1.\2',
68
- /PARTIE/ => '',
69
- /DELEG\./ => 'DELEG ',
70
- /FRANCAISE?/ => 'FR',
71
- /ANGLAISE?/ => 'UK',
72
- /BELGE/ => 'BE',
73
- /LUXEMBOURGEOISE?/ => 'LU',
74
- /ALLEMANDE?/ => 'DE',
75
- /SUISSE/ => 'CH',
76
- /ITALIEN(?:NE)?/ => 'IT',
77
- /ESPAGNOLE?/ => 'ES',
78
- /ANDORRANE?/ => 'AD',
79
- /NORD/ => 'N',
80
- /EST/ => 'E',
81
- /SUD/ => 'S',
82
- /OEST/ => 'W',
83
- /ANGLO NORMANDES/ => 'ANGLO-NORMANDES',
84
- / +/ => ' '
85
- }.freeze
8
+ # Supported version of the XML_SIA database dump
9
+ VERSION = '5'.freeze
86
10
 
87
- # Templates
11
+ # Mandatory Interface
88
12
 
89
- def organisation_lf
90
- @organisation_lf ||= AIXM.organisation(
91
- name: 'FRANCE',
92
- type: 'S'
93
- ).tap do |organisation|
94
- organisation.id = 'LF'
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
95
21
  end
96
22
  end
97
23
 
98
- # Transformations
99
-
100
- def prepare(html:)
101
- html.tap do |node|
102
- node.css('del, *[class*="AmdtDeletedAIRAC"]').each(&:remove) # remove deleted entries
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"
103
39
  end
104
40
  end
105
41
 
106
- def anglicise(name:)
107
- name&.uptrans&.tap do |string|
108
- ANGLICISE_MAP.each do |regexp, replacement|
109
- string.gsub!(regexp, replacement)
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'
110
52
  end
53
+ add cache.organisation_lf
111
54
  end
55
+ cache.organisation_lf
112
56
  end
113
57
 
114
- # Parsers
115
-
116
- def source(position:, aip_file: nil)
117
- aip_file ||= @aip
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
118
69
  [
119
70
  options[:region],
120
- aip_file.split('-').first,
71
+ section,
121
72
  aip_file,
122
73
  options[:airac].date.xmlschema,
123
74
  position
124
75
  ].join('|')
125
76
  end
126
77
 
127
- def xy_from(text)
128
- parts = text.strip.split(/\s+/)
129
- AIXM.xy(lat: parts[0], long: parts[1])
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
130
88
  end
131
89
 
132
- def z_from(limit)
133
- case limit
134
- when nil then nil
135
- when 'SFC' then AIXM::GROUND
136
- when 'UNL' then AIXM::UNLIMITED
137
- when /(\d+)ftASFC/ then AIXM.z($1.to_i, :qfe)
138
- when /(\d+)ftAMSL/ then AIXM.z($1.to_i, :qnh)
139
- when /FL(\d+)/ then AIXM.z($1.to_i, :qne)
140
- else fail "z `#{limit}' not recognized"
141
- end
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)
142
97
  end
143
98
 
144
- def d_from(text)
145
- case text
146
- when nil then nil
147
- when /(\d+)(\w+)/ then AIXM.d($1.to_i, $2.to_sym)
148
- else fail "d `#{text}' not recognized"
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
149
114
  end
150
115
  end
151
116
 
152
- def elevation_from(text)
153
- value, unit = text.strip.split
154
- AIXM.z(AIXM.d(value.to_i, unit).to_ft.dist, :qnh)
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])
155
124
  end
156
125
 
157
- def layer_from(text_for_limit, text_for_class=nil)
158
- above, below = text_for_limit.gsub(/ /, '').split(/\n+/).select(&:blank_to_nil).split { _1.match? '---+' }
159
- AIXM.layer(
160
- class: text_for_class,
161
- vertical_limit: AIXM.vertical_limit(
162
- upper_z: z_from(above[0]),
163
- max_z: z_from(above[1]),
164
- lower_z: z_from(below[0]),
165
- min_z: z_from(below[1])
166
- )
167
- )
168
- end
169
-
170
- def geometry_from(text)
126
+ # Build geometry from content
127
+ #
128
+ # @param content [String] source content
129
+ # @return [AIXM::Component::Geometry]
130
+ def geometry_from(content)
171
131
  AIXM.geometry.tap do |geometry|
172
132
  buffer = {}
173
- text.gsub(/\s+/, ' ').strip.split(/ - /).append('end').each do |element|
174
- case element
175
- when /arc (anti-)?horaire .+ sur (\S+) , (\S+)/i
176
- geometry.add_segment AIXM.arc(
177
- xy: buffer.delete(:xy),
178
- center_xy: AIXM.xy(lat: $2, long: $3),
179
- clockwise: $1.nil?
180
- )
181
- when /cercle de ([\d\.]+) (NM|km|m) .+ sur (\S+) , (\S+)/i
182
- geometry.add_segment AIXM.circle(
183
- center_xy: AIXM.xy(lat: $3, long: $4),
184
- radius: AIXM.d($1.to_f, $2)
185
- )
186
- when /end|(\S+) , (\S+)/
187
- geometry.add_segment AIXM.point(xy: buffer[:xy]) if buffer.has_key?(:xy)
188
- buffer[:xy] = AIXM.xy(lat: $1, long: $2) if $1
189
- if border = buffer.delete(:border)
190
- from = border.nearest(xy: geometry.segments.last.xy)
191
- to = border.nearest(xy: buffer[:xy], geometry_index: from.geometry_index)
192
- geometry.add_segments border.segment(from_position: from, to_position: to).map(&:to_point)
193
- end
194
- when /^frontière ([\w-]+)/i, /^(\D[^(]+)/i
195
- border_name = BORDERS.fetch($1.downcase.strip)
196
- if borders.has_key? border_name # border from GeoJSON
197
- buffer[:border] = borders[border_name]
198
- else # named border
199
- buffer[:xy] ||= INTERSECTIONS.fetch("#{buffer[:border_name]}|#{border_name}")
200
- buffer[:border_name] = border_name
201
- if border_name == 'FRANCE_SPAIN' # specify which part of this split border
202
- border_name += buffer[:xy].lat < 42.55 ? '_EAST' : '_WEST'
203
- end
204
- geometry.add_segment AIXM.border(
205
- xy: buffer.delete(:xy),
206
- name: border_name
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(' '))
207
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"
208
182
  end
209
- else
210
- fail "geometry `#{element}' not recognized"
211
- end
183
+ )
212
184
  end
213
185
  end
214
186
  end
215
187
 
216
- def timetable_from!(text)
217
- if text.gsub!(/^\s*#{AIXM::H_RE}\s*$/, '')
218
- AIXM.timetable(code: Regexp.last_match&.to_s&.strip)
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)
219
212
  end
220
213
  end
221
214
 
@@ -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