aipp 0.2.4 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +2 -0
- data/CHANGELOG.md +38 -0
- data/README.md +222 -88
- data/exe/aip2aixm +2 -2
- data/exe/aip2ofmx +2 -2
- data/lib/aipp/aip.rb +113 -31
- data/lib/aipp/border.rb +77 -46
- data/lib/aipp/debugger.rb +101 -0
- data/lib/aipp/downloader.rb +39 -26
- data/lib/aipp/executable.rb +41 -22
- data/lib/aipp/parser.rb +94 -21
- data/lib/aipp/patcher.rb +5 -2
- data/lib/aipp/pdf.rb +1 -1
- data/lib/aipp/regions/LF/README.md +49 -0
- data/lib/aipp/regions/LF/aerodromes.rb +223 -0
- data/lib/aipp/regions/LF/d_p_r_airspaces.rb +56 -0
- data/lib/aipp/regions/LF/dangerous_activities.rb +49 -0
- data/lib/aipp/regions/LF/designated_points.rb +47 -0
- data/lib/aipp/regions/LF/fixtures/aerodromes.yml +608 -0
- data/lib/aipp/regions/LF/helipads.rb +122 -0
- data/lib/aipp/regions/LF/helpers/base.rb +218 -0
- data/lib/aipp/regions/LF/helpers/surface.rb +49 -0
- data/lib/aipp/regions/LF/helpers/usage_limitation.rb +20 -0
- data/lib/aipp/regions/LF/navigational_aids.rb +85 -0
- data/lib/aipp/regions/LF/obstacles.rb +153 -0
- data/lib/aipp/regions/LF/serviced_airspaces.rb +70 -0
- data/lib/aipp/regions/LF/services.rb +172 -0
- data/lib/aipp/t_hash.rb +4 -5
- data/lib/aipp/version.rb +1 -1
- data/lib/aipp.rb +11 -5
- data/lib/core_ext/enumerable.rb +9 -9
- data/lib/core_ext/hash.rb +21 -5
- data/lib/core_ext/nokogiri.rb +54 -0
- data/lib/core_ext/string.rb +38 -66
- data.tar.gz.sig +2 -0
- metadata +180 -188
- metadata.gz.sig +0 -0
- data/.gitignore +0 -8
- data/.ruby-version +0 -1
- data/.travis.yml +0 -8
- data/.yardopts +0 -3
- data/Guardfile +0 -7
- data/TODO.md +0 -6
- data/aipp.gemspec +0 -44
- data/gems.rb +0 -3
- data/lib/aipp/airac.rb +0 -55
- data/lib/aipp/regions/LF/AD-1.3.rb +0 -162
- data/lib/aipp/regions/LF/AD-1.6.rb +0 -31
- data/lib/aipp/regions/LF/AD-2.rb +0 -313
- data/lib/aipp/regions/LF/AD-3.1.rb +0 -185
- data/lib/aipp/regions/LF/ENR-2.1.rb +0 -92
- data/lib/aipp/regions/LF/ENR-4.1.rb +0 -97
- data/lib/aipp/regions/LF/ENR-4.3.rb +0 -28
- data/lib/aipp/regions/LF/ENR-5.1.rb +0 -75
- data/lib/aipp/regions/LF/ENR-5.5.rb +0 -53
- data/lib/aipp/regions/LF/fixtures/AD-1.3.yml +0 -511
- data/lib/aipp/regions/LF/fixtures/AD-2.yml +0 -185
- data/lib/aipp/regions/LF/fixtures/AD-3.1.yml +0 -10
- data/lib/aipp/regions/LF/helpers/AD_radio.rb +0 -90
- data/lib/aipp/regions/LF/helpers/URL.rb +0 -26
- data/lib/aipp/regions/LF/helpers/common.rb +0 -217
- data/lib/core_ext/object.rb +0 -43
- data/rakefile.rb +0 -12
- data/spec/fixtures/archive.zip +0 -0
- data/spec/fixtures/border.geojson +0 -201
- data/spec/fixtures/document.pdf +0 -0
- data/spec/fixtures/document.pdf.json +0 -1
- data/spec/fixtures/new.html +0 -6
- data/spec/fixtures/new.pdf +0 -0
- data/spec/fixtures/new.txt +0 -1
- data/spec/lib/aipp/airac_spec.rb +0 -98
- data/spec/lib/aipp/border_spec.rb +0 -135
- data/spec/lib/aipp/downloader_spec.rb +0 -81
- data/spec/lib/aipp/patcher_spec.rb +0 -46
- data/spec/lib/aipp/pdf_spec.rb +0 -124
- data/spec/lib/aipp/t_hash_spec.rb +0 -44
- data/spec/lib/aipp/version_spec.rb +0 -7
- data/spec/lib/core_ext/enumberable_spec.rb +0 -76
- data/spec/lib/core_ext/hash_spec.rb +0 -27
- data/spec/lib/core_ext/integer_spec.rb +0 -15
- data/spec/lib/core_ext/nil_class_spec.rb +0 -11
- data/spec/lib/core_ext/string_spec.rb +0 -112
- data/spec/sounds/failure.mp3 +0 -0
- data/spec/sounds/success.mp3 +0 -0
- data/spec/spec_helper.rb +0 -28
data/lib/aipp/airac.rb
DELETED
@@ -1,55 +0,0 @@
|
|
1
|
-
module AIPP
|
2
|
-
|
3
|
-
# AIRAC cycle date calculations
|
4
|
-
#
|
5
|
-
# @example
|
6
|
-
# airac = AIPP::AIRAC.new('2018-01-01')
|
7
|
-
# airac.date # => #<Date: 2017-12-07 ((2458095j,0s,0n),+0s,2299161j)>
|
8
|
-
# airac.id # => 1713
|
9
|
-
# airac.next_date # => #<Date: 2018-01-04 ((2458123j,0s,0n),+0s,2299161j)>
|
10
|
-
# airac.next_id # => 1801
|
11
|
-
class AIRAC
|
12
|
-
# First AIRAC date following the last cycle length modification
|
13
|
-
ROOT_DATE = Date.parse('2015-06-25').freeze
|
14
|
-
|
15
|
-
# Length of one AIRAC cycle
|
16
|
-
DAYS_PER_CYCLE = 28
|
17
|
-
|
18
|
-
# @return [Date] AIRAC effective on date
|
19
|
-
attr_reader :date
|
20
|
-
|
21
|
-
# @return [Integer] AIRAC cycle ID
|
22
|
-
attr_reader :id
|
23
|
-
|
24
|
-
# @param any_date [Date] any date within the AIRAC cycle (default: today)
|
25
|
-
def initialize(any_date = nil)
|
26
|
-
any_date = any_date ? Date.parse(any_date.to_s) : Date.today
|
27
|
-
fail(ArgumentError, "cannot calculate dates before #{ROOT_DATE}") if any_date < ROOT_DATE
|
28
|
-
@date = date_for(any_date)
|
29
|
-
@id = id_for(@date)
|
30
|
-
end
|
31
|
-
|
32
|
-
# @return [Date] next AIRAC effective on date
|
33
|
-
def next_date
|
34
|
-
date + DAYS_PER_CYCLE
|
35
|
-
end
|
36
|
-
|
37
|
-
# @return [Integer] next AIRAC cycle ID
|
38
|
-
def next_id
|
39
|
-
id_for next_date
|
40
|
-
end
|
41
|
-
|
42
|
-
private
|
43
|
-
|
44
|
-
# Find the AIRAC date for +any_date+
|
45
|
-
def date_for(any_date)
|
46
|
-
ROOT_DATE + (any_date - ROOT_DATE).to_i / DAYS_PER_CYCLE * DAYS_PER_CYCLE
|
47
|
-
end
|
48
|
-
|
49
|
-
# Find the AIRAC ID for the AIRAC +date+
|
50
|
-
def id_for(date)
|
51
|
-
(date.year % 100) * 100 + ((date.yday - 1) / 28) + 1
|
52
|
-
end
|
53
|
-
|
54
|
-
end
|
55
|
-
end
|
@@ -1,162 +0,0 @@
|
|
1
|
-
module AIPP
|
2
|
-
module LF
|
3
|
-
|
4
|
-
# Aerodromes
|
5
|
-
class AD13 < AIP
|
6
|
-
|
7
|
-
include AIPP::LF::Helpers::Common
|
8
|
-
|
9
|
-
DEPENDS = %w(AD-2)
|
10
|
-
|
11
|
-
# Map names of id-less airports to unofficial ids
|
12
|
-
ID_LESS_AIRPORTS = {
|
13
|
-
"ALBE" => 'LF9001',
|
14
|
-
"BEAUMONT DE LOMAGNE" => 'LF9002',
|
15
|
-
"BERDOUES" => 'LF9003',
|
16
|
-
"BOULOC" => 'LF9004',
|
17
|
-
"BUXEUIL ST REMY / CREUSE" => 'LF9005',
|
18
|
-
"CALVIAC" => 'LF9006',
|
19
|
-
"CAYLUS" => 'LF9007',
|
20
|
-
"CORBONOD" => 'LF9008',
|
21
|
-
"L'ISLE EN DODON" => 'LF9009',
|
22
|
-
"LACAVE LE FRAU" => 'LF9010',
|
23
|
-
"LUCON CHASNAIS" => 'LF9011',
|
24
|
-
"PEYRELEVADE" => 'LF9012',
|
25
|
-
"SAINT CYR LA CAMPAGNE" => 'LF9013',
|
26
|
-
"SEPTFONDS" => 'LF9014',
|
27
|
-
"TALMONT VENDEE AIR PARK" => 'LF9015'
|
28
|
-
}
|
29
|
-
|
30
|
-
def parse
|
31
|
-
ad2_exists = false
|
32
|
-
tbody = prepare(html: read).css('tbody').first # skip altiports
|
33
|
-
tbody.css('tr').to_enum.with_index(1).each do |tr, index|
|
34
|
-
if tr.attr(:id).match?(/-TXT_NAME-/)
|
35
|
-
add @airport if @airport && !ad2_exists
|
36
|
-
@airport = airport_from tr
|
37
|
-
verbose_info "Parsing #{@airport.id}"
|
38
|
-
ad2_exists = false
|
39
|
-
if airport = select(:airport, id: @airport.id).first
|
40
|
-
ad2_exists = true
|
41
|
-
@airport = airport
|
42
|
-
end
|
43
|
-
add_usage_limitations_from tr
|
44
|
-
next
|
45
|
-
end
|
46
|
-
@airport.add_runway(runway_from(tr)) unless ad2_exists
|
47
|
-
rescue => error
|
48
|
-
warn("error parsing #{@airport.id} at ##{index}: #{error.message}", pry: error)
|
49
|
-
end
|
50
|
-
add @airport if @airport && !ad2_exists
|
51
|
-
end
|
52
|
-
|
53
|
-
private
|
54
|
-
|
55
|
-
def airport_from(tr)
|
56
|
-
tds = tr.css('td')
|
57
|
-
id = tds[0].text.strip.blank_to_nil || ID_LESS_AIRPORTS.fetch(tds[1].text.strip)
|
58
|
-
AIXM.airport(
|
59
|
-
source: source(position: tr.line),
|
60
|
-
organisation: organisation_lf, # TODO: not yet implemented
|
61
|
-
id: id,
|
62
|
-
name: tds[1].text.strip,
|
63
|
-
xy: xy_from(tds[3].text)
|
64
|
-
).tap do |airport|
|
65
|
-
airport.z = AIXM.z(tds[4].text.strip.to_i, :qnh)
|
66
|
-
airport.declination = tds[2].text.remove('°').strip.to_f
|
67
|
-
# airport.transition_z = AIXM.z(5000, :qnh) # TODO: default - exceptions exist
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
def add_usage_limitations_from(tr)
|
72
|
-
raw_limitation = tr.css('td:nth-of-type(8)').text.cleanup.downcase
|
73
|
-
raw_conditions = tr.css('td:nth-of-type(6)').text.cleanup.downcase.split(%r([\s/]+))
|
74
|
-
limitation = case raw_limitation
|
75
|
-
when /ouv.+cap|milit/ then :permitted
|
76
|
-
when /usa.+restr|priv/ then :reservation_required
|
77
|
-
end
|
78
|
-
@airport.add_usage_limitation(limitation) do |l|
|
79
|
-
l.add_condition do |c|
|
80
|
-
c.realm = :military if raw_limitation.match?(/milit/)
|
81
|
-
c.origin = :national if raw_conditions.include?('ntl')
|
82
|
-
c.origin = :international if raw_conditions.include?('intl')
|
83
|
-
c.rule = :ifr if raw_conditions.include?('ifr')
|
84
|
-
c.rule = :vfr if raw_conditions.include?('vfr')
|
85
|
-
c.purpose = :scheduled if raw_conditions.include?('s')
|
86
|
-
c.purpose = :not_scheduled if raw_conditions.include?('ns')
|
87
|
-
c.purpose = :private if raw_conditions.include?('p')
|
88
|
-
end
|
89
|
-
l.remarks = "Usage restreint (voir VAC) / restricted use (see VAC)" if raw_limitation.match?(/usa.+restr/)
|
90
|
-
l.remarks = "Propriété privée / privately owned" if raw_limitation.match?(/priv/)
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
def runway_from(tr)
|
95
|
-
tds = tr.css('td')
|
96
|
-
AIXM.runway(
|
97
|
-
name: tds[0].text.strip.split.join('/')
|
98
|
-
).tap do |runway|
|
99
|
-
@runway = runway # TODO: needed for now for surface composition patches to work
|
100
|
-
runway.length = AIXM.d(tds[1].css('span[id$="VAL_LEN"]').text.to_i, :m)
|
101
|
-
runway.width = AIXM.d(tds[1].css('span[id$="VAL_WID"]').text.to_i, :m)
|
102
|
-
unless (text = tds[1].css('span[id*="SURFACE"]').text).blank?
|
103
|
-
surface = SURFACES.metch(text)
|
104
|
-
runway.surface.composition = surface[:composition]
|
105
|
-
runway.surface.preparation = surface[:preparation]
|
106
|
-
runway.surface.remarks = surface[:remarks]
|
107
|
-
end
|
108
|
-
runway.remarks = tds[7].text.cleanup.blank_to_nil
|
109
|
-
values = tds[2].text.remove('°').strip.split
|
110
|
-
runway.forth.geographic_orientation = AIXM.a(values.first.to_i)
|
111
|
-
runway.back.geographic_orientation = AIXM.a(values.last.to_i)
|
112
|
-
parts = tds[3].text.strip.split(/\n\s+\n\s+/)
|
113
|
-
runway.forth.xy = (xy_from(parts[0]) unless parts[0].blank?)
|
114
|
-
runway.back.xy = (xy_from(parts[1]) unless parts[1].blank?)
|
115
|
-
values = tds[4].text.strip.split
|
116
|
-
runway.forth.z = AIXM.z(values.first.to_i, :qnh)
|
117
|
-
runway.back.z = AIXM.z(values.last.to_i, :qnh)
|
118
|
-
displaced_thresholds = displaced_thresholds_from(tds[5])
|
119
|
-
runway.forth.displaced_threshold = displaced_thresholds.first
|
120
|
-
runway.back.displaced_threshold = displaced_thresholds.last
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
def displaced_thresholds_from(td)
|
125
|
-
values = td.text.strip.split
|
126
|
-
case values.count
|
127
|
-
when 1 then []
|
128
|
-
when 2 then [AIXM.xy(lat: values[0], long: values[1]), nil]
|
129
|
-
when 3 then [nil, AIXM.xy(lat: values[1], long: values[2])]
|
130
|
-
when 4 then [AIXM.xy(lat: values[0], long: values[1]), AIXM.xy(lat: values[2], long: values[3])]
|
131
|
-
else fail "cannot parse displaced thresholds"
|
132
|
-
end
|
133
|
-
end
|
134
|
-
|
135
|
-
patch AIXM::Component::Runway, :width do |parser, object, value|
|
136
|
-
throw :abort unless value.zero?
|
137
|
-
airport_id = parser.instance_variable_get(:@airport).id
|
138
|
-
runway_name = object.name.to_s
|
139
|
-
throw :abort if (width = parser.fixture.dig('runways', airport_id, runway_name, 'width')).nil?
|
140
|
-
AIXM.d(width.to_i, :m)
|
141
|
-
end
|
142
|
-
|
143
|
-
patch AIXM::Component::Runway::Direction, :xy do |parser, object, value|
|
144
|
-
throw :abort unless value.nil?
|
145
|
-
airport_id = parser.instance_variable_get(:@airport).id
|
146
|
-
direction_name = object.name.to_s
|
147
|
-
throw :abort if (xy = parser.fixture.dig('runways', airport_id, direction_name, 'xy')).nil?
|
148
|
-
lat, long = xy.split(/\s+/)
|
149
|
-
AIXM.xy(lat: lat, long: long)
|
150
|
-
end
|
151
|
-
|
152
|
-
patch AIXM::Component::Surface, :composition do |parser, object, value|
|
153
|
-
throw :abort unless value.blank?
|
154
|
-
airport_id = parser.instance_variable_get(:@airport).id
|
155
|
-
runway_name = parser.instance_variable_get(:@runway).name
|
156
|
-
throw :abort if (composition = parser.fixture.dig('runways', airport_id, runway_name, 'composition')).nil?
|
157
|
-
composition
|
158
|
-
end
|
159
|
-
|
160
|
-
end
|
161
|
-
end
|
162
|
-
end
|
@@ -1,31 +0,0 @@
|
|
1
|
-
module AIPP
|
2
|
-
module LF
|
3
|
-
|
4
|
-
# Aerodromes radiocommunication facilities (VFR only)
|
5
|
-
class AD16 < AIP
|
6
|
-
|
7
|
-
include AIPP::LF::Helpers::Common
|
8
|
-
include AIPP::LF::Helpers::ADRadio
|
9
|
-
|
10
|
-
DEPENDS = %w(AD-1.3)
|
11
|
-
|
12
|
-
ID_FIXES = {
|
13
|
-
'LF04' => 'LF9004', # illegal ID as per AIXM
|
14
|
-
'LFPY' => nil # decommissioned - see https://fr.wikipedia.org/wiki/Base_a%C3%A9rienne_217_Br%C3%A9tigny-sur-Orge
|
15
|
-
}
|
16
|
-
|
17
|
-
def parse
|
18
|
-
prepare(html: read).css('tbody').first do |tbody|
|
19
|
-
tbody.css('tr').group_by_chunks { |e| e.attr(:id).match?(/-TXT_NAME-/) }.each do |tr, trs|
|
20
|
-
id = tr.css('span[id*="CODE_ICAO"]').text.cleanup
|
21
|
-
next unless id = ID_FIXES.fetch(id, id)
|
22
|
-
@airport = select(:airport, id: id).first
|
23
|
-
addresses_from(trs).each { |a| @airport.add_address(a) }
|
24
|
-
units_from(trs).each(&method(:add))
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
data/lib/aipp/regions/LF/AD-2.rb
DELETED
@@ -1,313 +0,0 @@
|
|
1
|
-
module AIPP
|
2
|
-
module LF
|
3
|
-
|
4
|
-
# Airports (IFR capable) and their CTR, AD navigational aids etc
|
5
|
-
class AD2 < AIP
|
6
|
-
|
7
|
-
include AIPP::LF::Helpers::Common
|
8
|
-
include AIPP::LF::Helpers::ADRadio
|
9
|
-
using AIXM::Refinements
|
10
|
-
|
11
|
-
# Map source types to type and optional local type
|
12
|
-
SOURCE_TYPES = {
|
13
|
-
'CTR' => { type: 'CTR' },
|
14
|
-
'RMZ' => { type: 'RAS', local_type: 'RMZ' },
|
15
|
-
'TMZ' => { type: 'RAS', local_type: 'TMZ' },
|
16
|
-
'RMZ-TMZ' => { type: 'RAS', local_type: 'RMZ-TMZ' }
|
17
|
-
}.freeze
|
18
|
-
|
19
|
-
# Airports without VAC (e.g. military installations)
|
20
|
-
NO_VAC = %w(LFOA LFBC LFQE LFOE LFSX LFBM LFSO LFMO LFQP LFSI LFKS LFPV).freeze
|
21
|
-
|
22
|
-
# Airports without VFR reporting points
|
23
|
-
# TODO: designated points on map but no list (LFLD LFSN LFBS) or no AD info (LFRL)
|
24
|
-
NO_DESIGNATED_POINTS = %w(LFAB LFAC LFAV LFAY LFBK LFBN LFBX LFCC LFCI LFCK LFCY LFDH LFDJ LFDN LFEC LFEY LFGA LFHP LFHV LFHY LFJR LFJY LFLA LFLH LFLO LFLV LFLW LFMQ LFMQ LFNB LFOH LFOQ LFOU LFOV LFOZ LFPO LFQA LFQB LFQG LFQM LFRC LFRI LFRM LFRT LFRU LFSD LFSG LFSM LFLD LFSN LFBS LFRL).freeze
|
25
|
-
|
26
|
-
# Map synonyms for +correlate+
|
27
|
-
SYNONYMS = [
|
28
|
-
'nord', 'north',
|
29
|
-
'est', 'east',
|
30
|
-
'sud', 'south',
|
31
|
-
'ouest', 'west',
|
32
|
-
'inst', 'instruction',
|
33
|
-
'junction', 'intersection',
|
34
|
-
'harbour', 'port',
|
35
|
-
'mouth', 'embouchure',
|
36
|
-
'tower', 'chateau'
|
37
|
-
].freeze
|
38
|
-
|
39
|
-
def parse
|
40
|
-
index_html = prepare(html: read("AD-0.6")) # index for AD-2.xxxx files
|
41
|
-
index_html.css('#AD-0\.6\.eAIP > .toc-block:nth-of-type(3) .toc-block a').each do |a|
|
42
|
-
@id = a.attribute('href').value[-4,4]
|
43
|
-
begin
|
44
|
-
aip_file = "AD-2.#{@id}"
|
45
|
-
html = prepare(html: read(aip_file))
|
46
|
-
# Airport
|
47
|
-
@remarks = []
|
48
|
-
@airport = AIXM.airport(
|
49
|
-
source: source(position: html.css('tr[id*="CODE_ICAO"]').first.line, aip_file: aip_file),
|
50
|
-
organisation: organisation_lf, # TODO: not yet implemented
|
51
|
-
id: @id,
|
52
|
-
name: html.css('tr[id*="CODE_ICAO"] td span:nth-of-type(2)').text.uptrans,
|
53
|
-
xy: xy_from(html.css('#AD-2\.2-Position_Geo_Arp td:nth-of-type(3)').text)
|
54
|
-
).tap do |airport|
|
55
|
-
airport.z = elevation_from(html.css('#AD-2\.2-Altitude_Reference td:nth-of-type(3)').text)
|
56
|
-
airport.declination = declination_from(html.css('#AD-2\.2-Declinaison_Magnetique td:nth-of-type(3)').text)
|
57
|
-
# airport.transition_z = AIXM.z(5000, :qnh) # TODO: default - exceptions may exist
|
58
|
-
airport.timetable = timetable_from!(html.css('#AD-2\.3-Gestionnaire_AD td:nth-of-type(3)').text)
|
59
|
-
end
|
60
|
-
runways_from(html.css('div[id*="-AD-2\.12"] tbody')).each { |r| @airport.add_runway(r) if r }
|
61
|
-
helipads_from(html.css('div[id*="-AD-2\.16"] tbody')).each { |h| @airport.add_helipad(h) if h }
|
62
|
-
text = html.css('#AD-2\.2-Observations td:nth-of-type(3)').text
|
63
|
-
@airport.remarks = ([remarks_from(text)] + @remarks).compact.join("\n\n").blank_to_nil
|
64
|
-
add @airport
|
65
|
-
# Airspaces
|
66
|
-
airspaces_from(html.css('div[id*="-AD-2\.17"] tbody')).each(&method(:add))
|
67
|
-
# Radio
|
68
|
-
trs = html.css('div[id*="-AD-2\.18"] tbody tr')
|
69
|
-
addresses_from(trs).each { |a| @airport.add_address(a) }
|
70
|
-
units_from(trs).each(&method(:add))
|
71
|
-
# Landing aids
|
72
|
-
# TODO: LOC/GP/DME as of section 2.19
|
73
|
-
# Designated points
|
74
|
-
unless NO_VAC.include?(@id) || NO_DESIGNATED_POINTS.include?(@id)
|
75
|
-
pdf = read("VAC-#{@id}")
|
76
|
-
designated_points_from(pdf).tap do |designated_points|
|
77
|
-
fix_designated_point_remarks(designated_points)
|
78
|
-
# debug(designated_points)
|
79
|
-
designated_points.each(&method(:add))
|
80
|
-
end
|
81
|
-
end
|
82
|
-
rescue => error
|
83
|
-
warn("error parsing airport #{@id}: #{error.message}", pry: error)
|
84
|
-
end
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|
88
|
-
private
|
89
|
-
|
90
|
-
def declination_from(text)
|
91
|
-
value, direction = text.strip.split('°')
|
92
|
-
value = value.to_f * (direction == 'W' ? -1 : 1)
|
93
|
-
end
|
94
|
-
|
95
|
-
def remarks_from(text)
|
96
|
-
text.sub(/NIL|\(\*\)\s+/, '').strip.gsub(/(\s)\s+/, '\1').blank_to_nil
|
97
|
-
end
|
98
|
-
|
99
|
-
def runways_from(tbody)
|
100
|
-
directions_map = tbody.css('tr[id*="TXT_DESIG"]').map do |tr|
|
101
|
-
[AIXM.a(tr.css('td:first-of-type').text.strip), tr]
|
102
|
-
end.to_h
|
103
|
-
remarks_map = tbody.css('tr[id*="TXT_RMK_NAT"]').map do |tr|
|
104
|
-
[tr.text.strip[/\A\((\d+)\)/, 1].to_i, tr.css('span')]
|
105
|
-
end.to_h
|
106
|
-
directions = directions_map.keys
|
107
|
-
grouped_directions = directions.map do |direction|
|
108
|
-
inverted_direction = direction.invert
|
109
|
-
if directions.include? inverted_direction
|
110
|
-
[direction, inverted_direction].map(&:to_s).sort.join('/')
|
111
|
-
else
|
112
|
-
direction.to_s
|
113
|
-
end
|
114
|
-
end.uniq
|
115
|
-
grouped_directions.map do |runway_name|
|
116
|
-
AIXM.runway(name: runway_name).tap do |runway|
|
117
|
-
%i(forth back).each do |direction_attr|
|
118
|
-
if direction = runway.send(direction_attr)
|
119
|
-
tr = directions_map[direction.name]
|
120
|
-
if direction_attr == :forth
|
121
|
-
length, width = tr.css('td:nth-of-type(3)').text.strip.split('x')
|
122
|
-
runway.length = AIXM.d(length.strip.to_i, :m)
|
123
|
-
runway.width = AIXM.d(width.strip.to_i, :m)
|
124
|
-
unless (text = tr.css('td:nth-of-type(5)').text.strip.split(%r<\W+/\W+>).first).blank?
|
125
|
-
surface = SURFACES.metch(text)
|
126
|
-
runway.surface.composition = surface[:composition]
|
127
|
-
runway.surface.preparation = surface[:preparation]
|
128
|
-
runway.surface.remarks = surface[:remarks]
|
129
|
-
end
|
130
|
-
if (text = tr.css('td:nth-of-type(4)').text).match?(AIXM::PCN_RE)
|
131
|
-
runway.surface.pcn = text
|
132
|
-
end
|
133
|
-
end
|
134
|
-
text = tr.css('td:nth-of-type(6)').text.strip
|
135
|
-
direction.xy = (xy_from(text) unless text.match?(/\A(\(.*)?\z/m))
|
136
|
-
if (text = tr.css('td:nth-of-type(7)').text.strip[/thr:\s+(\d+\s+\w+)/i, 1]).present?
|
137
|
-
direction.z = elevation_from(text)
|
138
|
-
end
|
139
|
-
if (text = tr.css('td:nth-of-type(2)').text.strip.sub(/\A(\d+).*$/m, '\1')).present?
|
140
|
-
direction.geographic_orientation = AIXM.a(text.to_i)
|
141
|
-
end
|
142
|
-
if (text = tr.css('td:nth-of-type(6)').text[/\((.+)\)/m, 1]).present?
|
143
|
-
direction.displaced_threshold = xy_from(text)
|
144
|
-
end
|
145
|
-
if (text = tr.css('td:nth-of-type(10)').text.strip[/\A\((\d+)\)/, 1]).present?
|
146
|
-
direction.remarks = remarks_from(remarks_map.fetch(text.to_i).text)
|
147
|
-
end
|
148
|
-
end
|
149
|
-
end
|
150
|
-
end
|
151
|
-
end
|
152
|
-
end
|
153
|
-
|
154
|
-
def helipads_from(tbody)
|
155
|
-
text_fr = tbody.css('td:nth-of-type(3)').text.compact
|
156
|
-
text_en = tbody.css('td:nth-of-type(4)').text.compact
|
157
|
-
case text_fr
|
158
|
-
when /NIL/, /\A\W*\z/
|
159
|
-
[]
|
160
|
-
when /instructions?\s+twr/i
|
161
|
-
@remarks << "HELICOPTER:\nSur instructions TWR.\nOn TWR clearance."
|
162
|
-
[]
|
163
|
-
when AIXM::DMS_RE
|
164
|
-
text_fr.scan(AIXM::DMS_RE).each_slice(2).with_index(1).map do |(lat, long), index|
|
165
|
-
AIXM.helipad(
|
166
|
-
name: "H#{index}",
|
167
|
-
xy: AIXM.xy(lat: lat.first, long: long.first)
|
168
|
-
)
|
169
|
-
end
|
170
|
-
else
|
171
|
-
@remarks << ['HELICOPTER:', text_fr.blank_to_nil, text_en.blank_to_nil].compact.join("\n")
|
172
|
-
[]
|
173
|
-
end
|
174
|
-
end
|
175
|
-
|
176
|
-
def airspaces_from(tbody)
|
177
|
-
return [] if tbody.text.blank?
|
178
|
-
airspace = nil
|
179
|
-
tbody.css('tr').to_enum.with_object([]) do |tr, array|
|
180
|
-
if tr.attr(:class) =~ /keep-with-next-row/
|
181
|
-
airspace = airspace_from tr
|
182
|
-
else
|
183
|
-
tds = tr.css('td')
|
184
|
-
airspace.geometry = geometry_from tds[0].text
|
185
|
-
fail("geometry is not closed") unless airspace.geometry.closed?
|
186
|
-
airspace.layers << layer_from(tds[2].text, tds[1].text.strip)
|
187
|
-
airspace.layers.first.timetable = timetable_from! tds[4].text
|
188
|
-
airspace.layers.first.remarks = remarks_from(tds[4].text)
|
189
|
-
array << airspace
|
190
|
-
end
|
191
|
-
end
|
192
|
-
end
|
193
|
-
|
194
|
-
def airspace_from(tr)
|
195
|
-
spans = tr.css(:span)
|
196
|
-
source_type = spans[1].text.blank_to_nil
|
197
|
-
fail "unknown type `#{source_type}'" unless SOURCE_TYPES.has_key? source_type
|
198
|
-
AIXM.airspace(
|
199
|
-
name: [spans[2].text, anglicise(name: spans[3]&.text)].compact.join(' '),
|
200
|
-
type: SOURCE_TYPES.dig(source_type, :type),
|
201
|
-
local_type: SOURCE_TYPES.dig(source_type, :local_type)
|
202
|
-
).tap do |airspace|
|
203
|
-
airspace.source = source(position: tr.line)
|
204
|
-
end
|
205
|
-
end
|
206
|
-
|
207
|
-
def designated_points_from(pdf, recursive=false)
|
208
|
-
from = (pdf.text =~ /^(.*?coordinates.*?names?)/i)
|
209
|
-
return [] if recursive && !from
|
210
|
-
warn("no designated points section begin found for #{@id}", pry: binding) unless from
|
211
|
-
from += $1.length
|
212
|
-
to = from + (pdf.text.from(from) =~ /\n\s*\n\s*\n|^.*(?:ifr|vfr|ad\s*equipment|special\s*activities|training\s*flights|mto\s*minima)/i)
|
213
|
-
warn("no designated points section end found for #{@id}", pry: binding) unless to
|
214
|
-
from, to = from + pdf.range.min, to + pdf.range.min # offset when recursive
|
215
|
-
buffer = {}
|
216
|
-
pdf.from(from).to(to).each_line.with_object([]) do |(line, page, last), designated_points|
|
217
|
-
line.remove!(/\u2190/) # remove arrow symbols
|
218
|
-
has_id = $1 if line.sub!(/^\s{,20}([A-Z][A-Z\d ]{1,3})(?=\W)/, '')
|
219
|
-
has_xy = line.match?(AIXM::DMS_RE)
|
220
|
-
designated_points << designated_point_from(buffer, pdf) if has_id || has_xy
|
221
|
-
if has_xy
|
222
|
-
2.times { (buffer[:xy] ||= []) << $1 if line.sub!(AIXM::DMS_RE, '') }
|
223
|
-
buffer[:xy]&.compact!
|
224
|
-
line.remove!(/\d{3,4}\D.+?MTG/) # remove extra columns (e.g. LFML)
|
225
|
-
line.remove!(/[\s#{AIXM::MIN}#{AIXM::SEC}]*[-\u2013]/) # remove dash between coordinates
|
226
|
-
end
|
227
|
-
buffer[:page] = page
|
228
|
-
buffer[:id] = has_id if has_id
|
229
|
-
buffer[:remarks] = [buffer[:remarks], line].join("\n")
|
230
|
-
designated_points << designated_point_from(buffer, pdf) if last
|
231
|
-
end.compact + designated_points_from(pdf.from(to).to(:end), true)
|
232
|
-
end
|
233
|
-
|
234
|
-
def designated_point_from(buffer, pdf)
|
235
|
-
if buffer[:id] && buffer[:xy]&.size == 2
|
236
|
-
buffer[:remarks].gsub!(/ {20}/, "\n") # recognize empty column space
|
237
|
-
buffer[:remarks].remove!(/\(\d+\)/) # remove footnotes
|
238
|
-
buffer[:remarks] = buffer[:remarks].unglue # separate glued words
|
239
|
-
AIXM.designated_point(
|
240
|
-
source: source(position: buffer[:page], aip_file: pdf.file.basename('.*').to_s),
|
241
|
-
type: :vfr_mandatory_reporting_point,
|
242
|
-
id: buffer[:id].remove(/\W/),
|
243
|
-
xy: AIXM.xy(lat: buffer[:xy].first, long: buffer[:xy].last)
|
244
|
-
).tap do |designated_point|
|
245
|
-
designated_point.airport = @airport
|
246
|
-
designated_point.remarks = buffer[:remarks].compact.blank_to_nil
|
247
|
-
buffer.clear
|
248
|
-
end
|
249
|
-
end
|
250
|
-
end
|
251
|
-
|
252
|
-
# Assign scattered similar remarks to one and the same designated point
|
253
|
-
def fix_designated_point_remarks(designated_points)
|
254
|
-
one = nil
|
255
|
-
designated_points.map do |two|
|
256
|
-
if one
|
257
|
-
one_lines, two_lines = one.remarks&.lines, two.remarks&.lines
|
258
|
-
if one_lines && two_lines
|
259
|
-
if one_lines.count > 1 && (line = one_lines.last) !~ %r(\s/\s)
|
260
|
-
# Move up
|
261
|
-
if line.correlate(remainder = one_lines[0..-2].join, SYNONYMS) < line.correlate(two.remarks)
|
262
|
-
two.remarks = [line, two.remarks].join("\n").compact
|
263
|
-
one.remarks = remainder.compact
|
264
|
-
end
|
265
|
-
elsif two_lines.count > 1 && (line = two_lines.first) !~ %r(\s/\s)
|
266
|
-
# Move down
|
267
|
-
line = two_lines.first
|
268
|
-
if line.correlate(remainder = two_lines[1..-1].join, SYNONYMS) < line.correlate(one.remarks)
|
269
|
-
one.remarks = [one.remarks, line].join("\n").compact
|
270
|
-
two.remarks = remainder.compact
|
271
|
-
end
|
272
|
-
end
|
273
|
-
end
|
274
|
-
end
|
275
|
-
one = two
|
276
|
-
end.map do |designated_point|
|
277
|
-
designated_point.remarks = designated_point.remarks&.cleanup.blank_to_nil
|
278
|
-
end
|
279
|
-
end
|
280
|
-
|
281
|
-
# def debug(dp)
|
282
|
-
# f = "/Users/sschwyn/Desktop/okay/#{@id}.txt"
|
283
|
-
# result = "\n--- #{@id} ---\n\n".red
|
284
|
-
# dp.each do |d|
|
285
|
-
# result += d.id.red + "\t#{d.xy.lat} - #{d.xy.long}\n"
|
286
|
-
# result += "#{d.remarks}\n\n".blue
|
287
|
-
# end
|
288
|
-
# result += "#{dp.count} point(s) for #{@id}".red
|
289
|
-
# unless File.exist?(f) && result == File.read(f)
|
290
|
-
# puts result
|
291
|
-
# gets
|
292
|
-
# puts "\e[H\e[2J"
|
293
|
-
# end
|
294
|
-
# File.write(f, result)
|
295
|
-
# end
|
296
|
-
|
297
|
-
patch AIXM::Component::Runway::Direction, :xy do |parser, object, value|
|
298
|
-
throw :abort unless value.nil?
|
299
|
-
airport_id = parser.instance_variable_get(:@airport).id
|
300
|
-
direction_name = object.name.to_s
|
301
|
-
throw :abort if (xy = parser.fixture.dig('runways', airport_id, direction_name, 'xy')).nil?
|
302
|
-
lat, long = xy.split(/\s+/)
|
303
|
-
AIXM.xy(lat: lat, long: long)
|
304
|
-
end
|
305
|
-
|
306
|
-
patch AIXM::Feature::NavigationalAid, :remarks do |parser, object, value|
|
307
|
-
airport_id, designated_point_id = object.airport.id, object.id
|
308
|
-
parser.fixture.dig('designated_points', airport_id, designated_point_id, 'remarks') || throw(:abort)
|
309
|
-
end
|
310
|
-
|
311
|
-
end
|
312
|
-
end
|
313
|
-
end
|