aipp 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,101 @@
1
+ module AIPP
2
+ module LF
3
+
4
+ # ENR Navaids
5
+ class ENR41 < AIP
6
+ using AIPP::Refinements
7
+
8
+ def parse
9
+ load_html.css('tbody').each do |tbody|
10
+ tbody.css('tr').to_enum.with_index(1).each do |tr, index|
11
+ tds = cleanup(node: tr).css('td')
12
+ master, slave = tds[1].text.strip.gsub(/[^\w-]/, '').downcase.split('-')
13
+ navaid = AIXM.send(master, base_from(tds).merge(send("#{master}_from", tds)))
14
+ navaid.source = source_for(tr)
15
+ navaid.timetable = timetable_from(tds[4])
16
+ navaid.remarks = remarks_from(tds[5], tds[7], tds[9])
17
+ navaid.send("associate_#{slave}", channel: channel_from(tds[3])) if slave
18
+ aixm.features << navaid
19
+ rescue => error
20
+ warn("error parsing navigational aid at ##{index}: #{error.message}", context: error)
21
+ end
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def base_from(tds)
28
+ {
29
+ organisation: organisation_lf,
30
+ id: tds[2].text.strip,
31
+ name: tds[0].text.strip,
32
+ xy: xy_from(tds[5]),
33
+ z: z_from(tds[6])
34
+ }
35
+ end
36
+
37
+ def vor_from(tds)
38
+ {
39
+ type: :conventional,
40
+ f: frequency_from(tds[3]),
41
+ north: :magnetic,
42
+ }
43
+ end
44
+
45
+ def dme_from(tds)
46
+ {
47
+ channel: channel_from(tds[3])
48
+ }
49
+ end
50
+
51
+ def ndb_from(tds)
52
+ {
53
+ type: :en_route,
54
+ f: frequency_from(tds[3])
55
+ }
56
+ end
57
+
58
+ def tacan_from(tds)
59
+ {
60
+ channel: channel_from(tds[3])
61
+ }
62
+ end
63
+
64
+ def z_from(td)
65
+ parts = td.text.strip.split(/\s+/)
66
+ AIXM.z(parts[0].to_i, :qnh) if parts[1] == 'ft'
67
+ end
68
+
69
+ def frequency_from(td)
70
+ parts = td.text.strip.split(/\s+/)
71
+ AIXM.f(parts[0].to_f, parts[1]) if parts[1] =~ /hz$/i
72
+ end
73
+
74
+ def channel_from(td)
75
+ parts = td.text.strip.split(/\s+/)
76
+ parts.last if parts[-2].downcase == 'ch'
77
+ end
78
+
79
+ def timetable_from(td)
80
+ code = td.text.strip
81
+ AIXM.timetable(code: code) unless code.empty?
82
+ end
83
+
84
+ def remarks_from(*parts)
85
+ part_titles = ['RANGE', 'SITUATION', 'OBSERVATIONS']
86
+ [].tap do |remarks|
87
+ parts.each.with_index do |part, index|
88
+ text = if index == 0
89
+ part = part.text.strip.split(/\s+/)
90
+ part.shift(2)
91
+ part.join(' ').blank_to_nil
92
+ else
93
+ part.text.strip.blank_to_nil
94
+ end
95
+ remarks << "#{part_titles[index]}:\n#{text}" if text
96
+ end
97
+ end.join("\n\n").blank_to_nil
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,26 @@
1
+ module AIPP
2
+ module LF
3
+
4
+ # Designated Points
5
+ class ENR43 < AIP
6
+
7
+ def parse
8
+ load_html.css('tbody').each do |tbody|
9
+ tbody.css('tr').to_enum.with_index(1).each do |tr, index|
10
+ tds = cleanup(node: tr).css('td')
11
+ designated_point = AIXM.designated_point(
12
+ type: :icao,
13
+ id: tds[0].text.strip,
14
+ xy: xy_from(tds[1])
15
+ )
16
+ designated_point.source = source_for(tr)
17
+ aixm.features << designated_point
18
+ rescue => error
19
+ warn("error parsing designated point at ##{index}: #{error.message}", context: error)
20
+ end
21
+ end
22
+ end
23
+
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,65 @@
1
+ module AIPP
2
+ module LF
3
+
4
+ # D/P/R Zones
5
+ class ENR51 < AIP
6
+ using AIPP::Refinements
7
+
8
+ TYPES = {
9
+ 'D' => 'D',
10
+ 'P' => 'P',
11
+ 'R' => 'R',
12
+ 'ZIT' => 'P'
13
+ }.freeze
14
+
15
+ def parse
16
+ load_html.css('tbody:has(tr[id^=mid])').each do |tbody|
17
+ airspace = nil
18
+ tbody.css('tr').to_enum.with_index(1).each do |tr, index|
19
+ if tr.attr(:class) =~ /keep-with-next-row/
20
+ airspace = airspace_from cleanup(node: tr)
21
+ else
22
+ begin
23
+ tds = cleanup(node: tr).css('td')
24
+ airspace.geometry = geometry_from tds[0]
25
+ fail("geometry is not closed") unless airspace.geometry.closed?
26
+ airspace.layers << layer_from(tds[1])
27
+ airspace.layers.first.timetable = timetable_from tds[2]
28
+ airspace.layers.first.remarks = remarks_from(tds[2], tds[3], tds[4])
29
+ aixm.features << airspace
30
+ rescue => error
31
+ warn("error parsing airspace `#{airspace.name}' at ##{index}: #{error.message}", context: error)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def airspace_from(tr)
41
+ spans = tr.css(:span)
42
+ AIXM.airspace(
43
+ name: [spans[1], spans[2], spans[3], spans[5].text.blank_to_nil].compact.join(' '),
44
+ local_type: [spans[1], spans[2], spans[3]].compact.join(' '),
45
+ type: TYPES.fetch(spans[2].text)
46
+ ).tap do |airspace|
47
+ airspace.source = source_for(tr)
48
+ end
49
+ end
50
+
51
+ def remarks_from(*parts)
52
+ part_titles = ['TIMETABLE', 'RESTRICTION', 'AUTHORITY/CONDITIONS']
53
+ [].tap do |remarks|
54
+ parts.each.with_index do |part, index|
55
+ if part = part.text.gsub(/ +/, ' ').gsub(/(\n ?)+/, "\n").strip.blank_to_nil
56
+ unless index.zero? && part == 'H24'
57
+ remarks << "#{part_titles[index]}:\n#{part}"
58
+ end
59
+ end
60
+ end
61
+ end.join("\n\n").blank_to_nil
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,177 @@
1
+ module AIPP
2
+ module LF
3
+ module Helper
4
+ using AIPP::Refinements
5
+ using AIXM::Refinements
6
+
7
+ BORDERS = {
8
+ 'franco-allemande' => 'FRANCE_GERMANY',
9
+ 'franco-espagnole' => 'FRANCE_SPAIN',
10
+ 'franco-italienne' => 'FRANCE_ITALY',
11
+ 'franco-suisse' => 'FRANCE_SWITZERLAND',
12
+ 'franco-luxembourgeoise' => 'FRANCE_LUXEMBOURG',
13
+ 'franco-belge' => 'BELGIUM_FRANCE',
14
+ 'germano-suisse' => 'GERMANY_SWITZERLAND',
15
+ 'hispano-andorrane' => 'ANDORRA_SPAIN',
16
+ 'la côte atlantique française' => 'FRANCE_ATLANTIC_COAST',
17
+ 'côte méditérrannéenne' => 'FRANCE_MEDITERRANEAN_COAST',
18
+ 'limite des eaux territoriales atlantique françaises' => 'FRANCE_ATLANTIC_TERRITORIAL_SEA',
19
+ 'parc national des écrins' => 'FRANCE_ECRINS_NATIONAL_PARK'
20
+ }.freeze
21
+
22
+ INTERSECTIONS = {
23
+ 'FRANCE_SPAIN|ANDORRA_SPAIN' => AIXM.xy(lat: 42.502720, long: 1.725965),
24
+ 'ANDORRA_SPAIN|FRANCE_SPAIN' => AIXM.xy(lat: 42.603571, long: 1.442681),
25
+ 'FRANCE_SWITZERLAND|FRANCE_ITALY' => AIXM.xy(lat: 45.922701, long: 7.044125),
26
+ 'BELGIUM_FRANCE|FRANCE_LUXEMBOURG' => AIXM.xy(lat: 49.546428, long: 5.818415),
27
+ 'FRANCE_LUXEMBOURG|FRANCE_GERMANY' => AIXM.xy(lat: 49.469438, long: 6.367516),
28
+ 'FRANCE_GERMANY|FRANCE_SWITZERLAND' => AIXM.xy(lat: 47.589831, long: 7.589049),
29
+ 'GERMANY_SWITZERLAND|FRANCE_GERMANY' => AIXM.xy(lat: 47.589831, long: 7.589049)
30
+ }
31
+
32
+ ANGLICISE_MAP = {
33
+ /[^A-Z0-9 .\-]/ => '',
34
+ / 0(\d)/ => ' \1',
35
+ /(\d)-(\d)/ => '\1.\2',
36
+ /PARTIE/ => '',
37
+ /DELEG\./ => 'DELEG ',
38
+ /FRANCAISE?/ => 'FR',
39
+ /ANGLAISE?/ => 'UK',
40
+ /BELGE/ => 'BE',
41
+ /LUXEMBOURGEOISE?/ => 'LU',
42
+ /ALLEMANDE?/ => 'DE',
43
+ /SUISSE/ => 'CH',
44
+ /ITALIEN(?:NE)?/ => 'IT',
45
+ /ESPAGNOLE?/ => 'ES',
46
+ /ANDORRANE?/ => 'AD',
47
+ /NORD/ => 'N',
48
+ /EST/ => 'E',
49
+ /SUD/ => 'S',
50
+ /OEST/ => 'W',
51
+ /ANGLO NORMANDES/ => 'ANGLO-NORMANDES',
52
+ / +/ => ' '
53
+ }.freeze
54
+
55
+ # Download URL
56
+
57
+ def url_for(aip_file)
58
+ "https://www.sia.aviation-civile.gouv.fr/dvd/eAIP_%s/FRANCE/AIRAC-%s/html/eAIP/FR-%s-fr-FR.html" % [
59
+ options[:airac].date.strftime('%d_%^b_%Y'), # 04_JAN_2018
60
+ options[:airac].date.xmlschema, # 2018-01-04
61
+ aip_file # ENR-5.1 or AD-2.LFMV
62
+ ]
63
+ end
64
+
65
+ # Templates
66
+
67
+ def organisation_lf
68
+ @organisation_lf ||= AIXM.organisation(
69
+ name: 'FRANCE',
70
+ type: 'S'
71
+ ).tap do |organisation|
72
+ organisation.id = 'LF'
73
+ end
74
+ end
75
+
76
+ # Transformations
77
+
78
+ def cleanup(node:)
79
+ node.tap do |root|
80
+ root.css('del').each { |n| n.remove } # remove deleted entries
81
+ end
82
+ end
83
+
84
+ def anglicise(name:)
85
+ name.uptrans.tap do |string|
86
+ ANGLICISE_MAP.each do |regexp, replacement|
87
+ string.gsub!(regexp, replacement)
88
+ end
89
+ end
90
+ end
91
+
92
+ # Parsers
93
+
94
+ def source_for(element)
95
+ [
96
+ options[:region],
97
+ @aip.split('-').first,
98
+ @aip,
99
+ options[:airac].date.xmlschema,
100
+ element.line
101
+ ].join('|')
102
+ end
103
+
104
+ def xy_from(td)
105
+ parts = td.text.strip.split(/\s+/)
106
+ AIXM.xy(lat: parts[0], long: parts[1])
107
+ end
108
+
109
+ def z_from(limit)
110
+ case limit
111
+ when nil then nil
112
+ when 'SFC' then AIXM::GROUND
113
+ when 'UNL' then AIXM::UNLIMITED
114
+ when /(\d+)ftASFC/ then AIXM.z($1.to_i, :qfe)
115
+ when /(\d+)ftAMSL/ then AIXM.z($1.to_i, :qnh)
116
+ when /FL(\d+)/ then AIXM.z($1.to_i, :qne)
117
+ else fail "z `#{limit}' not recognized"
118
+ end
119
+ end
120
+
121
+ def layer_from(td)
122
+ above, below = td.text.gsub(/ /, '').split(/\n+/).select(&:blank_to_nil).split { |e| e.match? '---+' }
123
+ above.reverse!
124
+ AIXM.layer(
125
+ vertical_limits: AIXM.vertical_limits(
126
+ max_z: z_from(above[1]),
127
+ upper_z: z_from(above[0]),
128
+ lower_z: z_from(below[0]),
129
+ min_z: z_from(below[1])
130
+ )
131
+ )
132
+ end
133
+
134
+ def geometry_from(td)
135
+ AIXM.geometry.tap do |geometry|
136
+ buffer = {}
137
+ td.text.gsub(/\s+/, ' ').strip.split(/ - /).append('end').each do |element|
138
+ case element
139
+ when /arc (anti-)?horaire .+ sur (\S+) , (\S+)/i
140
+ geometry << AIXM.arc(
141
+ xy: buffer.delete(:xy),
142
+ center_xy: AIXM.xy(lat: $2, long: $3),
143
+ clockwise: $1.nil?
144
+ )
145
+ when /cercle de ([\d\.]+) (NM|km|m) .+ sur (\S+) , (\S+)/i
146
+ geometry << AIXM.circle(
147
+ center_xy: AIXM.xy(lat: $3, long: $4),
148
+ radius: AIXM.d($1.to_f, $2)
149
+ )
150
+ when /end|(\S+) , (\S+)/
151
+ geometry << AIXM.point(xy: buffer[:xy]) if buffer.has_key?(:xy)
152
+ buffer[:xy] = AIXM.xy(lat: $1, long: $2) if $1
153
+ when /^frontière ([\w-]+)/i, /^(\D[^(]+)/i
154
+ border_name = BORDERS.fetch($1.downcase.strip)
155
+ buffer[:xy] ||= INTERSECTIONS.fetch("#{buffer[:border_name]}|#{border_name}")
156
+ buffer[:border_name] = border_name
157
+ if border_name == 'FRANCE_SPAIN' # specify which part of this split border
158
+ border_name += buffer[:xy].lat < 42.55 ? '_EAST' : '_WEST'
159
+ end
160
+ geometry << AIXM.border(
161
+ xy: buffer.delete(:xy),
162
+ name: border_name
163
+ )
164
+ else
165
+ fail "geometry `#{element}' not recognized"
166
+ end
167
+ end
168
+ end
169
+ end
170
+
171
+ def timetable_from(td)
172
+ AIXM::H24 if td.text.gsub(/\W/, '') == 'H24'
173
+ end
174
+
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,46 @@
1
+ module AIPP
2
+
3
+ # Topologically sortable hash for dealing with dependencies
4
+ #
5
+ # Example:
6
+ # dependency_hash = THash[
7
+ # dns: %i(net),
8
+ # webserver: %i(dns logger),
9
+ # net: [],
10
+ # logger: []
11
+ # ]
12
+ # # Sort to resolve dependencies of the entire hash
13
+ # dependency_hash.tsort # => [:net, :dns, :logger, :webserver]
14
+ # # Sort to resolve dependencies of one node only
15
+ # dependency_hash.tsort(:dns) # => [:net, :dns]
16
+ class THash < Hash
17
+ include TSort
18
+
19
+ alias_method :tsort_each_node, :each_key
20
+
21
+ def tsort_each_child(node, &block)
22
+ fetch(node).each(&block)
23
+ end
24
+
25
+ def tsort(node=nil)
26
+ if node
27
+ subhash = subhash_for node
28
+ super().select { |n| subhash.include? n }
29
+ else
30
+ super()
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def subhash_for(node, memo=[])
37
+ memo.tap do |m|
38
+ fail TSort::Cyclic if m.include? node
39
+ m << node
40
+ fetch(node).each { |n| subhash_for(n, m) }
41
+ end
42
+ end
43
+
44
+ end
45
+
46
+ end
@@ -1,3 +1,3 @@
1
1
  module AIPP
2
- VERSION = "0.1.3".freeze
2
+ VERSION = "0.2.0".freeze
3
3
  end
@@ -8,7 +8,7 @@ describe AIPP::AIRAC do
8
8
  end
9
9
  end
10
10
 
11
- context "on AIRAC date" do
11
+ context "on AIRAC date (as Date)" do
12
12
  subject do
13
13
  AIPP::AIRAC.new(Date.parse('2018-01-04'))
14
14
  end
@@ -30,9 +30,9 @@ describe AIPP::AIRAC do
30
30
  end
31
31
  end
32
32
 
33
- context "one day before AIRAC date" do
33
+ context "one day before AIRAC date (as String)" do
34
34
  subject do
35
- AIPP::AIRAC.new(Date.parse('2018-01-03'))
35
+ AIPP::AIRAC.new('2018-01-03')
36
36
  end
37
37
 
38
38
  it "must calculate correct #date" do
@@ -4,49 +4,119 @@ using AIPP::Refinements
4
4
 
5
5
  describe AIPP::Refinements do
6
6
 
7
- describe 'String#blank_to_nil' do
8
- it "must convert blank to nil" do
9
- "\n \n ".blank_to_nil.must_be :nil?
10
- end
7
+ context String do
8
+ describe :blank_to_nil do
9
+ it "must convert blank to nil" do
10
+ "\n \n ".blank_to_nil.must_be :nil?
11
+ end
12
+
13
+ it "must leave non-blank untouched" do
14
+ "foobar".blank_to_nil.must_equal "foobar"
15
+ end
11
16
 
12
- it "must leave non-blank untouched" do
13
- "foobar".blank_to_nil.must_equal "foobar"
17
+ it "must leave non-blank with whitespace untouched" do
18
+ "\nfoo bar\n".blank_to_nil.must_equal "\nfoo bar\n"
19
+ end
14
20
  end
15
21
 
16
- it "must leave non-blank with whitespace untouched" do
17
- "\nfoo bar\n".blank_to_nil.must_equal "\nfoo bar\n"
22
+ describe :blank? do
23
+ it "all whitespace must return true" do
24
+ "\n \n ".blank?.must_equal true
25
+ end
26
+
27
+ it "not all whitespace must return false" do
28
+ "\nfoo bar\n".blank?.must_equal false
29
+ end
18
30
  end
19
- end
20
31
 
21
- describe 'NilClass#blank_to_nil' do
22
- it "must return self" do
23
- nil.blank_to_nil.must_be :nil?
32
+ describe :classify do
33
+ it "must convert file name to class name" do
34
+ "ENR-5.1".classify.must_equal "ENR51"
35
+ "helper".classify.must_equal "Helper"
36
+ "foo_bar".classify.must_equal "FooBar"
37
+ end
24
38
  end
25
39
  end
26
40
 
27
- describe 'Array#split' do
28
- it "must split at pattern" do
29
- [1, 2, '---', 3, 4].split(/-+/).must_equal [[1, 2], [3, 4]]
41
+ context NilClass do
42
+ describe :blank_to_nil do
43
+ it "must return self" do
44
+ nil.blank_to_nil.must_be :nil?
45
+ end
30
46
  end
31
47
 
32
- it "won't split arrays with no pattern matches" do
33
- [1, 2, 3].split(/-+/).must_equal [[1, 2, 3]]
48
+ describe :blank? do
49
+ it "must return true" do
50
+ nil.blank?.must_equal true
51
+ end
34
52
  end
53
+ end
35
54
 
36
- it "must keep leading empty subarrays" do
37
- ['---', 1, 2, '---', 3, 4].split(/-+/).must_equal [[], [1, 2], [3, 4]]
38
- end
55
+ context Array do
56
+ describe :constantize do
57
+ it "must convert to constant" do
58
+ %w(AIPP Refinements).constantize.must_equal AIPP::Refinements
59
+ end
39
60
 
40
- it "must keep empty subarrays in the middle" do
41
- [1, 2, '---', '---', 3, 4].split(/-+/).must_equal [[1, 2], [], [3, 4]]
61
+ it "fails to convert to inexistant constant" do
62
+ -> { %w(Foo Bar).constantize }.must_raise NameError
63
+ end
42
64
  end
65
+ end
43
66
 
44
- it "must drop trailing empty subarrays" do
45
- [1, 2, '---', 3, 4, '---'].split(/-+/).must_equal [[1, 2], [3, 4]]
46
- end
67
+ context Enumerable do
68
+ describe :split do
69
+ context "by object" do
70
+ it "must split at matching element" do
71
+ [1, 2, 0, 3, 4].split(0).must_equal [[1, 2], [3, 4]]
72
+ end
73
+
74
+ it "won't split when no element matches" do
75
+ [1, 2, 3].split(0).must_equal [[1, 2, 3]]
76
+ end
77
+
78
+ it "won't split zero length enumerable" do
79
+ [].split(0).must_equal []
80
+ end
81
+
82
+ it "must keep leading empty subarrays" do
83
+ [0, 1, 2, 0, 3, 4].split(0).must_equal [[], [1, 2], [3, 4]]
84
+ end
85
+
86
+ it "must keep empty subarrays in the middle" do
87
+ [1, 2, 0, 0, 3, 4].split(0).must_equal [[1, 2], [], [3, 4]]
88
+ end
89
+
90
+ it "must drop trailing empty subarrays" do
91
+ [1, 2, 0, 3, 4, 0].split(0).must_equal [[1, 2], [3, 4]]
92
+ end
93
+ end
94
+
95
+ context "by block" do
96
+ it "must split at matching element" do
97
+ [1, 2, 0, 3, 4].split { |e| e.zero? }.must_equal [[1, 2], [3, 4]]
98
+ end
99
+
100
+ it "won't split when no element matches" do
101
+ [1, 2, 3].split { |e| e.zero? }.must_equal [[1, 2, 3]]
102
+ end
103
+
104
+ it "won't split zero length enumerable" do
105
+ [].split { |e| e.zero? }.must_equal []
106
+ end
107
+
108
+ it "must keep leading empty subarrays" do
109
+ [0, 1, 2, 0, 3, 4].split { |e| e.zero? }.must_equal [[], [1, 2], [3, 4]]
110
+ end
111
+
112
+ it "must keep empty subarrays in the middle" do
113
+ [1, 2, 0, 0, 3, 4].split { |e| e.zero? }.must_equal [[1, 2], [], [3, 4]]
114
+ end
47
115
 
48
- it "won't alter empty arrays" do
49
- [].split(/-+/).must_equal []
116
+ it "must drop trailing empty subarrays" do
117
+ [1, 2, 0, 3, 4, 0].split { |e| e.zero? }.must_equal [[1, 2], [3, 4]]
118
+ end
119
+ end
50
120
  end
51
121
  end
52
122