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.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +2 -0
  3. data/CHANGELOG.md +38 -0
  4. data/README.md +222 -88
  5. data/exe/aip2aixm +2 -2
  6. data/exe/aip2ofmx +2 -2
  7. data/lib/aipp/aip.rb +113 -31
  8. data/lib/aipp/border.rb +77 -46
  9. data/lib/aipp/debugger.rb +101 -0
  10. data/lib/aipp/downloader.rb +39 -26
  11. data/lib/aipp/executable.rb +41 -22
  12. data/lib/aipp/parser.rb +94 -21
  13. data/lib/aipp/patcher.rb +5 -2
  14. data/lib/aipp/pdf.rb +1 -1
  15. data/lib/aipp/regions/LF/README.md +49 -0
  16. data/lib/aipp/regions/LF/aerodromes.rb +223 -0
  17. data/lib/aipp/regions/LF/d_p_r_airspaces.rb +56 -0
  18. data/lib/aipp/regions/LF/dangerous_activities.rb +49 -0
  19. data/lib/aipp/regions/LF/designated_points.rb +47 -0
  20. data/lib/aipp/regions/LF/fixtures/aerodromes.yml +608 -0
  21. data/lib/aipp/regions/LF/helipads.rb +122 -0
  22. data/lib/aipp/regions/LF/helpers/base.rb +218 -0
  23. data/lib/aipp/regions/LF/helpers/surface.rb +49 -0
  24. data/lib/aipp/regions/LF/helpers/usage_limitation.rb +20 -0
  25. data/lib/aipp/regions/LF/navigational_aids.rb +85 -0
  26. data/lib/aipp/regions/LF/obstacles.rb +153 -0
  27. data/lib/aipp/regions/LF/serviced_airspaces.rb +70 -0
  28. data/lib/aipp/regions/LF/services.rb +172 -0
  29. data/lib/aipp/t_hash.rb +4 -5
  30. data/lib/aipp/version.rb +1 -1
  31. data/lib/aipp.rb +11 -5
  32. data/lib/core_ext/enumerable.rb +9 -9
  33. data/lib/core_ext/hash.rb +21 -5
  34. data/lib/core_ext/nokogiri.rb +54 -0
  35. data/lib/core_ext/string.rb +38 -66
  36. data.tar.gz.sig +2 -0
  37. metadata +180 -188
  38. metadata.gz.sig +0 -0
  39. data/.gitignore +0 -8
  40. data/.ruby-version +0 -1
  41. data/.travis.yml +0 -8
  42. data/.yardopts +0 -3
  43. data/Guardfile +0 -7
  44. data/TODO.md +0 -6
  45. data/aipp.gemspec +0 -44
  46. data/gems.rb +0 -3
  47. data/lib/aipp/airac.rb +0 -55
  48. data/lib/aipp/regions/LF/AD-1.3.rb +0 -162
  49. data/lib/aipp/regions/LF/AD-1.6.rb +0 -31
  50. data/lib/aipp/regions/LF/AD-2.rb +0 -313
  51. data/lib/aipp/regions/LF/AD-3.1.rb +0 -185
  52. data/lib/aipp/regions/LF/ENR-2.1.rb +0 -92
  53. data/lib/aipp/regions/LF/ENR-4.1.rb +0 -97
  54. data/lib/aipp/regions/LF/ENR-4.3.rb +0 -28
  55. data/lib/aipp/regions/LF/ENR-5.1.rb +0 -75
  56. data/lib/aipp/regions/LF/ENR-5.5.rb +0 -53
  57. data/lib/aipp/regions/LF/fixtures/AD-1.3.yml +0 -511
  58. data/lib/aipp/regions/LF/fixtures/AD-2.yml +0 -185
  59. data/lib/aipp/regions/LF/fixtures/AD-3.1.yml +0 -10
  60. data/lib/aipp/regions/LF/helpers/AD_radio.rb +0 -90
  61. data/lib/aipp/regions/LF/helpers/URL.rb +0 -26
  62. data/lib/aipp/regions/LF/helpers/common.rb +0 -217
  63. data/lib/core_ext/object.rb +0 -43
  64. data/rakefile.rb +0 -12
  65. data/spec/fixtures/archive.zip +0 -0
  66. data/spec/fixtures/border.geojson +0 -201
  67. data/spec/fixtures/document.pdf +0 -0
  68. data/spec/fixtures/document.pdf.json +0 -1
  69. data/spec/fixtures/new.html +0 -6
  70. data/spec/fixtures/new.pdf +0 -0
  71. data/spec/fixtures/new.txt +0 -1
  72. data/spec/lib/aipp/airac_spec.rb +0 -98
  73. data/spec/lib/aipp/border_spec.rb +0 -135
  74. data/spec/lib/aipp/downloader_spec.rb +0 -81
  75. data/spec/lib/aipp/patcher_spec.rb +0 -46
  76. data/spec/lib/aipp/pdf_spec.rb +0 -124
  77. data/spec/lib/aipp/t_hash_spec.rb +0 -44
  78. data/spec/lib/aipp/version_spec.rb +0 -7
  79. data/spec/lib/core_ext/enumberable_spec.rb +0 -76
  80. data/spec/lib/core_ext/hash_spec.rb +0 -27
  81. data/spec/lib/core_ext/integer_spec.rb +0 -15
  82. data/spec/lib/core_ext/nil_class_spec.rb +0 -11
  83. data/spec/lib/core_ext/string_spec.rb +0 -112
  84. data/spec/sounds/failure.mp3 +0 -0
  85. data/spec/sounds/success.mp3 +0 -0
  86. 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