aipp 0.2.6 → 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.
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